Skip to content

Claudian-reboot epic: P0→P12 agent surface rebuild#455

Open
Luis85 wants to merge 14 commits into
developfrom
next
Open

Claudian-reboot epic: P0→P12 agent surface rebuild#455
Luis85 wants to merge 14 commits into
developfrom
next

Conversation

@Luis85
Copy link
Copy Markdown
Owner

@Luis85 Luis85 commented May 27, 2026

Claudian-reboot epic — P0→P12 integration (nextdevelop)

Clean-room rebuild of the agent surface. P0 gutted the legacy feature/workflow/chat/MCP/onboarding stack (ADR-PSR-001) and booted an empty agent sidebar; P1–P12 regrew each subsystem behind the six narrow ports, on the next integration branch. 14 commits, 13 squash-merged phase PRs.

Phase PR Scope
P0 plugin-shell #432 gut features, boot empty agent sidebar
P1 chat-core #433 provider-agnostic ChatRuntime + Claude CLI streaming chat
P2 rich-rendering #436 rich message rendering
P3 threads/sessions #437 tabs + history/resume + fork/rewind/compact + title-gen
P4 composer #442 slash / skills / @mention / instruction / plan / bang-bash
P5 context/attachments #446 file chips, images, selection, inline-edit
P6 toolbar/controls #447 model/mode/thinking selectors + usage meter + seams
P7 approvals/security #448 ApprovalManager + permission modes + rule persistence
P8 MCP client #449 manager/parser/tester + stdio/SSE/HTTP + settings UI
P9 providers/registry #450 Codex + Opencode + registry/routing/secret/home-fs
P10 settings-shell #451 provider tabs + per-provider settings + env snippets
P11 i18n #452 full 10-locale set (es/fr/ja/ko/pt/ru/zh-CN/zh-TW)
P12 accessibility #453 accessibility.css + WCAG 2.2 AA sweep (final phase)
— CI fix #454 CI-resilient P12 additivity/discipline diff tests

Charter constraints honored

  • CHARTER-REQ-SEC — secrets persist to app.secretStorage via SecretStorePort, never data.json / settings / logs / DTOs.
  • CHARTER-REQ-SET — device-local settings (ADR-PSR-002).
  • CHARTER-REQ-FRESH — no backwards-compat / migration code.
  • HomeFsPort beyond-vault access is read-only, scoped, user-consented; stdio spawns are bounded with no shell-eval; every MCP / provider tool call is gated by the P7 ApprovalManager.
  • minAppVersion 1.12.7 left intentional (not bumped).
  • No innerHTML / v-html / window.confirm; raw-HTML + forbidden-term + locale key-parity guards enforced in CI.

Verification

  • Each phase merged to next only on a green gate (typecheck / lint / unit / build / build:web / docs:api) and green CI conclusion.
  • Final P0–P12 build deployed to the local Obsidian test vault.

⚠️ Human acceptance required — do NOT auto-merge

Per Constitution Article VII (humans own acceptance), this PR is opened for review, not for automated merge. Outstanding human-owned legs that could not be self-verified and accumulate here for a single acceptance gate:

  • Manual in-Obsidian smoke of the live agent sidebar across P5–P12 (real provider streaming, MCP servers, approvals UX, settings round-trip).
  • Visual / screenshot legs (e.g. TEST-AY-017 forced-colors + focus-ring render, attachment chip/thumb UX, toolbar selectors).
  • Real-provider credential flows (Claude CLI / Codex / Opencode) against app.secretStorage.

🤖 Generated with Claude Code

Luis85 and others added 14 commits May 24, 2026 13:57
* docs(psr): scaffold P0 plugin-shell-reboot spec + claudian-reboot epic plan

Seeds the whole-plugin clean-room rewrite (Claudian-shaped, keep-skeleton).
P0 = gut features, keep architecture, boot empty agent sidebar.
Epic phases P0-P7 + branch strategy (next) recorded in workflow-state.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): stage 1 idea — accept IDEA-PSR-001, skip research

Analyst refined idea.md against the real src/ tree, corrected the
keep/delete inventory (bridges carry chat coupling), resolved OQ-PSR-1..3,
flagged R-PSR-3 (next branch has no CI). Research skipped (Claudian sole ref).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): stage 3 requirements — accept PRD-PSR-001

12 EARS functional reqs (REQ-PSR-001..012) + 9 NFRs. Resolves Q1 (keep
build:web w/ empty entry), Q2 (slim settings to locale+logLevel), Q3 (CI
must cover next). Q4/Q5 deferred to architect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): clarify gate post-requirements — resolve CL-1, CL-2

CL-1: keep locale + minimal i18n/TranslationPort stub (amends REQ-PSR-006).
CL-2: deleted-symbol check is automated guard, ESLint + CI test (amends REQ-PSR-005).
CL-3 (open affordance) + CL-4 (Vue mount vs bare view) deferred to architect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): stage 4 design — design.md + ADR-PSR-001

Architect decisions: prune IconPort/SpIcon (no consumer); command-palette-only
open affordance; mount Vue (AgentPanelRoot in ErrorBoundary) over bare ItemView;
leaf-first compiler-guided 6-wave delete strategy. ADR-PSR-001 records the reboot
and supersedes feature-facing scope of ADR-008 + MPS/AUX agent surface. Honours
CL-1 (i18n stub keeps locale) + CL-2 (deleted-symbol ESLint+test guard).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): clarify gate post-design — design sound, OC defaults accepted

No blocking clarifications. OC-PSR-1 (WorkspacePort -> openFile-only),
OC-PSR-2 (always MockBridge in P0), OC-PSR-3 (ADR index + superseded-by
pointers) accepted with architect defaults. Migration contract = strip-on-read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): stage 5 specification — accept SPEC-PSR-001

17 spec items, 23 TEST-PSR scenarios. Pins: strip-on-read migration (v3->4),
WorkspacePort reverted to openFile-only, deleted-symbol guard test contract
(ESLint Node API). Corrects design: ObsidianBridge also carries ChatTransportPort
+ IconPort -> Wave-3 de-couples it too. Adds toSupportedLocale narrowing.
OC-PSR-4..7 flagged to planner (non-blocking).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): stage 6 tasks — 34 TDD-ordered tasks (T-PSR-001..034)

4 phases: stand up slim surface + RED tests -> 6 delete waves -> guard +
ci/docs/ADR -> zero-bypass verify. All 12 REQ + 9 NFR + 17 SPEC + 23 TEST-PSR
mapped. Deleted-symbol guard sequenced last so ban-globs resolve. OC-PSR-4..7
placed as concrete early tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): analyze gate post-tasks — traceability consistent

Full chain REQ->SPEC->TEST->Task verified via tasks.md coverage table; no
orphans, no contradictions. One non-blocking finding: design C.5 ObsidianBridge
'six ports' line superseded by SPEC-PSR-009 + T-PSR-021 (Wave 3b de-couples it).
idea->tasks scope complete; P0 ready for implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): pause P0 after tasks — ready for Stage 7 implementation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): add claudian-reboot parity charter (1:1 experience goal)

Audited claudian-main (298 TS, 45 CSS, 10 locales). The coarse P0-P7 roadmap
missed major surface (rich renderers, toolbar widgets, image/file attachments,
bang-bash, rewind, compact, subagents, agents, skills, per-provider settings UX,
ACP transport). Charter at specs/claudian-reboot/parity-charter.md captures the
full inventory, the expanded P0-P12 phase map, the bounding constraints, and the
per-surface screenshot parity acceptance method. Mandatory input to every phase
design + review. workflow-state phase table now points to it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): deep per-surface Claudian audits + charter consolidation

Two read-only audits of claudian-main written to specs/claudian-reboot/:
- claudian-audit-frontend.md: chat/render/composer/context/toolbar (§3.1-3.5),
  per-surface CSS->--sp-* + Vue + port mappings, parity-critical visual details,
  keyboard + motion inventory. New ports: FilePicker/EditorSelection/Clipboard/AuxModel.
- claudian-audit-backend.md: provider runtime (ChatRuntime), MCP, settings, i18n,
  a11y, security; port-seam table. New ports: ChatRuntime/ProviderRegistry/
  ProviderHistory/HomeFs/McpConfigStore/McpClient/Secret/ApprovalRuleStore.
Charter §6 now lists decisions needing ADRs (ChatRuntimePort shape, HomeFsPort,
secret handling) + scope confirmations + the recommended port set; §7 points to
the audits. Note: audits read the pre-P0 tree; P0 deletes that scaffold, so P1-P6
rebuild clean using the deleted code/AUX design as reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): settings/secret storage requirements (CHARTER-REQ-SET/SEC)

Adds REQ-PSR-013 (user-scoped settings -> device-local store, not data.json,
collaborative git-vault hygiene) + REQ-PSR-014 (secrets -> SecretStorePort via
app.secretStorage, P0-vacuous). NFR-PSR-010/011. ADR-PSR-002 (settings storage
location). Charter binds both as CHARTER-REQ-SET + CHARTER-REQ-SEC.

NOTE: superseded in part by the next commit (no-backwards-compat removes the
legacy data.json migration this delta introduced).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): charter — no backwards compatibility (CHARTER-REQ-FRESH)

Complete rewrite: no migration of legacy data.json/settings/sessions, no compat
shims. Removes all settings-migration work (supersedes the relocate-and-clear
drafted under CHARTER-REQ-SET) — settings just load-or-default from device-local.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): consolidated no-backwards-compat re-spec (drop settings migration)

CHARTER-REQ-FRESH ripple across requirements/spec/design/tasks/ADR:
- requirements: NG8 (no backwards compat); REQ-PSR-013 trimmed to load-or-default
- spec: SPEC-PSR-002 = load-or-default (no migrate/settingsVersion); SPEC-PSR-002a
  + TEST-PSR-025 deleted; TEST-PSR-001..004 re-scoped; 24 TEST-PSR
- design: C.3 load-or-default; relocate-and-clear removed
- ADR-PSR-002 amended: device-local kept, migration section dropped
- tasks: T-PSR-001..004 re-scoped, T-PSR-008 loadSettings load-or-default,
  T-PSR-021 device-local re-point + TEST-PSR-024 hygiene kept; 34 tasks, path intact
Settings now: device-local store, load-or-default, no migration, data.json-clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(psr): RED load-or-default core-settings contract (T-PSR-001/002)

TEST-PSR-001..007: validateSettings is load-or-default with no migrate() and no settingsVersion bump; unknown keys ignored; two-dropdown schema; DEFAULT_SETTINGS slimmed to {locale,logLevel}. Fails against the fat module; green after T-PSR-003/004. SPEC-PSR-001/002/003/004; CHARTER-REQ-FRESH/NG8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(psr): RED empty AgentPanelRoot placeholder (T-PSR-005)

TEST-PSR-008: mounts AgentPanelRoot, asserts data-testid=agent-panel-empty renders agent.empty.placeholder. Missing component -> RED; green after T-PSR-007. SPEC-PSR-006.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(psr): RED i18n placeholder + toSupportedLocale (T-PSR-006)

TEST-PSR-009/010: agent.empty.placeholder EN/DE round-trip + toSupportedLocale narrowing (fr->en, de->de). Green after T-PSR-007. SPEC-PSR-010/011/012.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(psr): trace ErrorBoundary E10 to TEST-PSR-015 (T-PSR-011)

Annotate the existing ErrorBoundary test as TEST-PSR-015 coverage (SPEC-PSR-005 E10). Already green against the kept component that the empty panel mounts inside (OC-PSR-7).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(psr): RED WorkspacePort openFile-only (T-PSR-012)

TEST-PSR-011: compile-time exact-key assertion + MockBridge conformance. Fails typecheck against the fat 7-member port; green after T-PSR-013 reverts to ADR-008 openFile-only. SPEC-PSR-009 / OC-PSR-1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(psr): RED ci.yml next trigger (T-PSR-028)

TEST-PSR-023: ci.yml on.push and on.pull_request branch lists must include next. Green after T-PSR-029. SPEC-PSR-015; REQ-PSR-012, NFR-PSR-008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): impl-log + test-plan; QA harness recon (T-PSR-024)

Seed the Stage-7 implementation log and Stage-8 test plan; record the Batch-1 RED state. OC-PSR-6 closed: reuse tests/eslint-boundaries.test.ts (ESLint Node API) for the SPEC-PSR-014 guard; __fixtures__ carve-out confirmed in eslint.config.js. NFR-PSR-009; SPEC-PSR-014.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): create ADR index + superseded-by pointers (T-PSR-031)

Create docs/adr/README.md (none existed); add scoped superseded-by: ADR-PSR-001 to ADR-008 (feature-port scope only, six core ports kept) and set ADR-PSR-001 supersedes: [ADR-008]. MPS/AUX feature-surface supersession recorded in the index (no standalone ADR-MPS/AUX files) — OC-PSR-3 re-scope, flagged to maintainer. REQ-PSR-009; ADR-PSR-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(psr): RED slim settings-tab round-trip (T-PSR-014)

TEST-PSR-014: mount SpecoratorSettingTab, drive the schema locale dropdown onChange, assert SettingsPort save->read round-trip (E12). Proxy obsidian mock + fakeModulePorts. RED: fat tab display() throws at renderAboutYouSection; green after T-PSR-015. SPEC-PSR-008; REQ-PSR-007.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(psr): slim PluginSettings to {locale,logLevel} (T-PSR-003)

Reduce PluginSettings + DEFAULT_SETTINGS to the two device-scoped fields with a live consumer; drop the 16 feature/provider/workflow fields and both @/domain/chat imports. SPEC-PSR-001; REQ-PSR-006.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(psr): load-or-default coreSettingsModule, no migration (T-PSR-004)

Rewrite core-settings to load-or-default: no migrate(), no settingsVersion, two-field validateSettings, two-dropdown schema; delete the provider validators/VALID_* constants/chat imports. T-PSR-001/002 GREEN (13 tests). SPEC-PSR-002/003/004; CHARTER-REQ-FRESH/NG8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(psr): AgentPanelRoot + trimmed i18n + toSupportedLocale (T-PSR-007)

Add the empty AgentPanelRoot.vue (data-testid=agent-panel-empty), trim en/de catalogues to the single agent.empty.placeholder key, export toSupportedLocale as the shared setLocale narrowing helper. T-PSR-005/006 GREEN (11 tests). SPEC-PSR-006/010/011/012.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(psr): revert WorkspacePort to openFile-only (T-PSR-013)

Drop the chat-era WorkspacePort methods + ActiveFileSnapshot interface/re-export; keep openFile (ADR-008) and the Unsubscriber barrel re-export. SPEC-PSR-009; OC-PSR-1; REQ-PSR-005.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(psr): trigger CI on the next integration branch (T-PSR-029)

Add next to ci.yml on.push and on.pull_request branch lists (only change; no uses: touched, SHA-pin gate clean). T-PSR-028 GREEN. SPEC-PSR-015; REQ-PSR-012, NFR-PSR-008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(psr): slim SpecoratorSettingTab to the module-schema loop (T-PSR-015)

Drop every render*/handle*/_bumpAllViews helper and the node:*/binary-resolver/SECRET_ID_*/deleted-view imports; keep the generic schema loop + saveField persisting through SettingsPort. T-PSR-014 GREEN. SPEC-PSR-008; REQ-PSR-007.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(psr): ObsidianBridge SettingsPort -> device-local store (T-PSR-021 settings slice)

Re-point getSettings/saveSettings to app.loadLocalStorage/saveLocalStorage (key specorator:settings), never data.json (NFR-PSR-010); constructor app-only; load-or-default read. TEST-PSR-024 GREEN. Pulled forward from Wave 3b because the slim main.ts settings path needs the non-recursive bridge; the chat/icon de-couple + other bridges + ports.ts/fake-ports trim remain T-021. SPEC-PSR-008; REQ-PSR-013; ADR-PSR-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(psr): AgentSidebarView + VIEW_TYPE_AGENT (T-PSR-009)

Empty agent sidebar ItemView: mounts AgentPanelRoot inside ErrorBoundary's default slot via createApp+h, installs Pinia+i18n, provides the six core ports, getIcon()='bot' (native), locale narrowed via toSupportedLocale; onClose unmounts; bridge===null no-op. SPEC-PSR-005; REQ-PSR-001/002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(psr): slim main.ts surviving surface (T-PSR-008)

One view (VIEW_TYPE_AGENT) + one command (open-agent-sidebar), no ribbon, settings tab. loadSettings load-or-default via bridge.getSettings (device-local, no legacy data.json read); onload drops the settings saveData; updateSettings -> bridge.saveSettings; _storedData.specorator dropped post-init for data.json hygiene. ALL_MODULES=[coreSettingsModule,helloModule] (OC-PSR-4); device-local API verified at minAppVersion 1.12.7 (NFR-PSR-011, no escalation). SPEC-PSR-016; REQ-PSR-001/003/013, NFR-PSR-010/011.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(psr): activateAgentSidebar E1/E2 + Batch-3 log (T-PSR-010)

TEST-PSR-012/013: reveal-or-create twice -> one leaf + reveal (E1); getRightLeaf null -> no throw (E2). RED watched (method absent on fat main), then GREEN. Logs OC-PSR-4/NFR-PSR-011 closure + the T-021 settings-slice pull-forward. SPEC-PSR-007.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(psr): minimal standalone entry, always MockBridge (T-PSR-016)

RED smoke (fat entry mounts AppRoot) -> rewrite src/ui/main.ts to mount AgentPanelRoot inside ErrorBoundary with MockBridge + six core ports; drop router/AppRoot/FeatureService/secret/DEV_FIXTURES/bootstrapModules; keep CSS + the no-restricted-imports carve-out. TEST-PSR-022 GREEN; vite build exits 0. OC-PSR-2 closed. SPEC-PSR-017; REQ-PSR-011, NFR-PSR-005.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(psr): Wave 0 — delete UI leaves (T-PSR-017)

Delete chat/feature/onboarding/design-canvas components, routed views, router, layouts, Pinia chat/feature stores, SpIcon/Markdown/Thinking/ToolCall blocks, slash/mention + deleted-port composables, AppRoot, AgentSidepanelRoot, and their mirrored tests + stories (~345 files). Keep ErrorBoundary.vue (OC-PSR-7), agent/AgentPanelRoot.vue, the six ADR-008 port composables, i18n, main.ts. typecheck green-or-expected: errors trace only to Wave 1+ importers. REQ-PSR-004/005; SPEC-PSR-006 §9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(psr): Wave 1 — delete plugin views/wiring (T-PSR-018)

Delete old SpecoratorView/AgentSidepanelView, chat/approval persistence, leafLoader, loadSettings-migrate, uriProviderParam, settings/CursorSettingsSection, transport/** and their tests. src/plugin = {AgentSidebarView, main, settings}. typecheck green-or-expected (errors trace to Wave 2/3). REQ-PSR-004/005/003; SPEC-PSR-016 §9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(psr): Wave 2 — delete application chat/feature/migration (T-PSR-019)

Delete src/application/{chat,feature,migration}/** + tests; keep shared/FeedbackService. OC-PSR-5: @/application/migration/** confirmed real (now deleted) for the guard glob. typecheck green-or-expected (errors trace to Wave 3). REQ-PSR-004/005; §9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(psr): Wave 3a — delete infra adapters/registrars/mocks (T-PSR-020)

Delete Claude/Cursor adapters+resolvers, Obsidian MCP/CLI/metadata/canvas/secret/confirm/markdown adapters, mcp/** registrars, subprocess plumbing, cursor/**, FeatureRepository, degradedClaudeCliPort, LocalStorageSecretStore, mock chat/secret/mcp adapters+fixtures, workflow-state/** + tests. OC-PSR-5: MCP registrars under obsidian/mcp/** not @/infrastructure/mcp/** (recorded for T-026). src/infrastructure = 3 bridges + ports + VaultPath. typecheck green-or-expected (-> 3b de-couple). REQ-PSR-004/005; §9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(psr): Wave 3b — de-couple bridges + slim ports/fakes (T-PSR-021)

All three bridges implement only the six core ports; drop chat/icon members + imports. Slim ports.ts (6 InjectionKeys) + fake-ports.ts. Fix kept bridge/contract tests; drop ICON_PORT from storybook preview. typecheck GREEN tree-wide (Wave 3 done). REQ-PSR-005/013, NFR-PSR-010; SPEC-PSR-008/009; ADR-PSR-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(psr): Wave 4 — delete domain chat/feature/ports + trim PluginCore MCP (T-PSR-022)

Delete domain/chat, domain/feature, the 10 deleted port interfaces, Slug, orphan FeatureDto; slim ports/index.ts to 6 core + TranslationPort + Unsubscriber. Compiler-surfaced spec gap: trimmed PluginCore's MCP surface (deleted ObsidianMcpServerPort dep) — flagged to maintainer. Phase B exit: typecheck GREEN, npm run test GREEN (307 tests). REQ-PSR-004/005; SPEC-PSR-009 §9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(psr): Wave 5a — delete dead no-legacy-claude-cli rule (T-PSR-023)

Remove the no-legacy-claude-cli-port-names custom rule + its tests + lint:rules half + config registration + useClaudeCliPort override; keep no-claude-home-reads. NFR-PSR-009; SPEC-PSR-013 §9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(psr): deleted-subsystem guard — ban + positive control + arch test (T-PSR-025/026/027)

Add DELETED_SUBSYSTEM_BAN + DELETED_INJECTION_KEYS to the project-wide no-restricted-imports block (OC-PSR-5: drop dead @/infrastructure/mcp/** glob, repoint to obsidian/mcp/**). Positive-control fixture + tests/architecture/no-deleted-subsystem-refs.test.ts (ESLint Node API): TEST-PSR-016 zero over src/**, TEST-PSR-017 fixture trips the ban. Both GREEN. REQ-PSR-005, NFR-PSR-009; SPEC-PSR-013/014.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): gut CLAUDE.md to reboot state + coverage report (T-PSR-030/032)

Rewrite CLAUDE.md architecture/key-files/router/vault/CI sections to the gutted P0 state (no deleted-subsystem refs; REQ-PSR-010). Record T-032 coverage: 94.5/85/87/94.7 over the include set (>80/70/80/80, R-PSR-5 not triggered); 308 tests pass. NFR-PSR-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build(psr): emit styles.css + storybook story; verify gate green (T-PSR-033/034)

Re-add token/animation CSS to main.ts so the plugin build emits styles.css (slim entry had dropped CSS); tag AgentSidebarView host .specorator-root. Add AgentPanelRoot story so test:storybook has >=1 test (R-PSR-4; CI-verified). npm run verify exits 0; manifest unchanged; zero bypasses. Manual Obsidian checks + CI-on-next pending human (checkpoint). REQ-PSR-004/012, NFR-PSR-001..009; §9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): workflow-state — P0 implemented, pending review/manual (T-PSR-034)

Mark Stage 7 complete (34/34 T-PSR, verify green); record the three maintainer flags (PluginCore MCP trim, OC-PSR-3 ADR scope, T-021 settings re-point pull-forward) and the pending human gates (test:storybook/CI on next, manual Obsidian TEST-PSR-018..021).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(psr): pre-bundle story deps to stabilize test:storybook (T-PSR-034)

With only the AgentPanelRoot story left after the reboot, the browser-mode storybook test triggered an on-the-fly dep-optimize + reload mid-run -> 'Vitest failed to find the current suite'. optimizeDeps.include vue/vue-i18n/pinia/vue-router so they pre-bundle. R-PSR-4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(psr): record human-confirmed manual Obsidian pass (TEST-PSR-018..021)

Maintainer ran the build in D:/TestVault: 'P0 is clean'. TEST-PSR-018..021 PASS (human-attributed) + static build-artifact corroboration of TEST-PSR-020 (one command, no ribbon, no deleted markers, manifest unchanged). NFR-PSR-003.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Luis Mendez <hallo@luis-mendez.de>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…aming chat (#433)

P1 of the claudian-reboot epic: provider-agnostic ChatRuntimePort + StreamChunk
union (mirrors claudian-main), Claude CLI subprocess runtime (no stored secret),
single-thread chat, streaming render, send/stop composer. DDD + narrow ports +
3 bridges; no v-html/innerHTML; Result<T,E>; device-local settings.

Functional sign-off in Obsidian (TEST-CC-031). All 3 Codex review findings fixed
(terminal-done, EPIPE guard, runtime-throw double-handle). verify + test:all green
(438 tests). Parity-screenshot matrix deferred to #434.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
P2 of claudian-reboot: rich message rendering on the P1 chat surface — tool-calls,
thinking, todo, write/edit word-diff (hunked), collapsible, subagent, usage. Real-CLI
reducer emits the P2 chunks (subagent/async/compaction/notice via parent_tool_use_id +
system subtypes); async MarkdownRenderPort backed by Obsidian's renderer (ADR-RR-002);
claudian-parity tool icons; blocked-status detection. DDD + ports + no v-html; verify +
test:all green (722); parity-reviewed vs claudian (review.md). Manual real-CLI leg
(T-RR-043) + parity screenshots + P3-polish (R-RR-006/007/009/010/011) carried to the
final epic review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…act + title-gen (#437)

P3 of claudian-reboot: multi-tab chat + conversation history/resume + fork/rewind/
compact + auto title-gen on the P1/P2 surface. New ProviderHistoryPort (vault-file
history, never data.json); tabsStore with per-tab runtime + streaming isolation;
3 additive ChatRuntimePort members + 3 ChatMessage rewind fields; Obsidian Modal
fork/delete via injection seam (Vue obsidian-free). ADR-TS-001/002/003/004. Parity
self-reviewed vs claudian; 3 P1 + 3 P2 + 1 brand findings fixed pre-merge. verify +
test:all green (906); coverage 95.67/88.53/93.08/96.47. Manual-Obsidian legs +
P3-polish carried to the final epic review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ruction/plan/bang-bash) (#442)

P4 of claudian-reboot: composer power on the P1-P3 surface. Slash/skills/
@mention/instruction/plan-mode + inline ask-user/exit-plan/approval blocks/
bang-bash. 3 new ports (MentionDataProvider/ProviderCommandCatalog/
ShellExec[S1-S5 secure]); 3 additive StreamChunk request members + 3
ChatRuntimePort callback-setters + 2 caps flags; capability-gated inline
response (CLI honesty); instruction --append-system-prompt. ADR-CP-001..004.
Parity self-reviewed; 2 P2 conditions fixed pre-merge. verify + test:all
green (1100); coverage 96.01/88.2/93.29/96.78. Manual legs + P3-polish
carried to the final epic review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nline-edit (#446)

* docs(ca): bootstrap P5 context-attachments feature + workflow-state

P5 of claudian-reboot (charter §4 P5 / §3.4): file chips, images
(context/embed/modal), browser/canvas selection, inline-edit + word-diff
(reuses P2 computeDiff/DiffView). Off next (P0-P4 merged). Autonomous
drive. Inline-edit is the 3rd side-query consumer → AuxModelPort
extract-now decision flagged.

Traces: charter §3.4/§4; epic claudian-reboot P5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ca): P5 requirements PRD-CA-001 (context & attachments, draft)

28 EARS REQ-CA + 13 NFR-CA for file chips / images / selection / inline-
edit, each mapped to a claudian §3.4 path + acceptance. Held draft until
the 4 P5 ADRs: CLAR-CA-001 attachment model + image transport; CLAR-CA-002
selection sources (editor+canvas ship, browser capability-gated);
CLAR-CA-003 inline-edit modalSeam; CLAR-CA-004 extract AuxModelPort now
(3rd side-query consumer) + word-level diff feeds the reused DiffView
renderer (P2 computeDiff is line-level — needs a word-diff fn, no new dep).

Traces: PRD-CA-001; charter §3.4/§4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ca): P5 design DESIGN-CA-001 + ADR-CA-001..004 (accepted)

ADR-CA-001 attachment model: ChatTurnRequest grows 5 optional context
fields; images = bounded base64-inline (8MiB, allow-list, no secret) +
VaultPort.readBinary (additive method, no new port). ADR-CA-002 EXTRACT
AuxModelPort (run(prompt,opts):Result<string>) + refactor GenerateTitle
(P3) + RefineInstruction (P4) onto it. ADR-CA-003 SelectionSourcePort +
SelectionHighlightPort — editor+canvas ship, browser capability-gated.
ADR-CA-004 inline-edit via modalSeam + AuxModelPort side-query +
parseInlineEditResponse + pure computeWordDiff (DP/LCS, no new dep)
feeding the REUSED DiffView. All additive (P1 send byte-identical when
no context); PRD-CA accepted.

Traces: DESIGN-CA-001, ADR-CA-001..004, PRD-CA-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ca): P5 spec SPEC-CA-001..030 (context & attachments)

Implementation-ready spec per ADR-CA-001..004. ChatTurnRequest +5
optional context fields + AttachedFileRef/AttachedImage/CapturedSelection
DTOs; AuxModelPort + SelectionSourcePort + SelectionHighlightPort +
VaultPort.readBinary (additive); pure computeWordDiff (DP/LCS, no dep) +
parseInlineEditResponse; AddFileContext/AddImage/CaptureSelection/
InlineEdit use cases; GenerateTitle+RefineInstruction re-pointed onto
AuxModelPort (behavior-preserving); InlineEditModal reuses DiffView. 32
TEST-CA + 3 manual legs. browser-selection capability-gated.

Traces: SPEC-CA-001..030, DESIGN-CA-001, ADR-CA-001..004.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ca): P5 tasks TASKS-CA-001 (48 tasks, TDD-ordered)

T-CA-001..048, RED-before-green, DDD inward layering (domain → aux-port+
refactor EARLY → infra → application → ui → styles → wire-in → gate).
AuxModelPort extraction + GenerateTitle/RefineInstruction re-point
sequenced before inline-edit to keep P3/P4 green. Each Vue component
pairs a data-testid PageObject + no-v-html/no-window.confirm DoD;
InlineEditModal/ImagePreviewModal are Obsidian Modals via the seam.
Obsidian selection/binary-read/modals coverage-excluded → manual legs.
No guard-relax needed. Coverage maps all 30 SPEC-CA + 28 REQ-CA + 13
NFR-CA + 32 TEST-CA + 3 manual legs.

Traces: TASKS-CA-001, SPEC-CA-001..030.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ca): T-CA-001 scaffold P5 parity matrix + guard verification

Scaffold the parity-screenshots baseline matrix (4 sub-surfaces x 320/520/720
x light/dark) and record the deleted-symbol guard verification in test-plan.md.
Confirms AUX_MODEL_PORT / SELECTION_SOURCE_PORT / SELECTION_HIGHLIGHT_PORT and
the new attachment/inline-edit paths are not caught by DELETED_INJECTION_KEYS /
DELETED_SUBSYSTEM_BAN — no guard relaxation needed. No src/ change.

Satisfies NFR-CA-007 (baseline leg), NFR-CA-001 (guard verification). SPEC-CA §0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-002 RED attachment DTOs + CapturedSelection union + 5 ChatTurnRequest fields

Author the failing structural/serialisation tests: AttachedFileRef/AttachedImage
(four-member ImageMimeType allow-list, all readonly); the three-member
CapturedSelection union (editor/canvas/browser, narrowing on kind, startLine
0-based); ChatTurnRequest gains exactly the five additive optional fields with
the P1 text/currentNotePath byte-identical and a {text}-only request serialising
identically to P1; PreparedChatTurn/ChatRuntimeQueryOptions/EnsureReady unchanged.

RED: the DTOs / union / five fields do not yet exist (vue-tsc -p tsconfig.lint.json
fails — the canonical type-shape RED signal).

TEST-CA-001/002/003 + TEST-CA-013 (type-shape leg). SPEC-CA-001/002/003/028.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-003 attachment DTOs + CapturedSelection union + barrel

Implement SPEC-CA-002/003 under src/domain/chat/attachments/: AttachedFileRef +
AttachedImage (four-member ImageMimeType allow-list) in Attachments.ts; the
three-member CapturedSelection union (editor/canvas/browser, narrowing on kind,
startLine 0-based, lineCount >= 1) in Selection.ts; index.ts barrel. Plain
readonly domain data — no obsidian/node:*/Vue/class (NFR-CA-004).

Greens the TEST-CA-003 DTO legs + the TEST-CA-013 type-shape leg. Adds the P5
implementation-log (T-CA-001..004 entries).

TEST-CA-003/013. SPEC-CA-002/003. REQ-CA-001/007/010/013/017/018. NFR-CA-004.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-004 ChatTurnRequest five additive optional context fields

Append attachedFiles?/images?/editorSelection?/canvasSelection?/browserSelection?
to ChatTurnRequest (SPEC-CA-001, ADR-CA-001 §1), importing the DTOs from
./attachments/{Attachments,Selection}. P1 text/currentNotePath stay
byte-identical; a {text}-only request serialises identically to P1;
PreparedChatTurn/ChatRuntimeQueryOptions/ChatRuntimeEnsureReadyOptions unchanged.
externalContextPaths?/enabledMcpServers? stay excluded (NG3).

Greens TEST-CA-001 (exact keys + per-field DTO types) + TEST-CA-002.

TEST-CA-001/002. SPEC-CA-001/028. REQ-CA-004/010/019. NFR-CA-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-005 RED AuxModelPort + selection ports + VaultPort.readBinary shapes

Author the failing structural type-shape tests: AuxModelPort.run(prompt, options?)
-> Promise<Result<string>> with AuxModelRunOptions (systemPrompt?/model?/signal?)
+ AUX_MODEL_PORT key; SelectionSourcePort (getCurrentSelection/onSelectionChange/
supportsBrowserSelection) + SELECTION_SOURCE_PORT key; SelectionHighlightPort
(show/clear) + SELECTION_HIGHLIGHT_PORT key; VaultPort gains exactly
readBinary(path) -> Promise<Uint8Array> with the seven P0-P4 members
byte-identical. All re-exported from the @/domain/ports barrel.

RED: the three ports / three keys / readBinary member do not yet exist
(vue-tsc -p tsconfig.lint.json fails — the canonical type-shape RED signal).

TEST-CA-010/021/028 (shape legs). SPEC-CA-004/005/006/028.
ADR-CA-001 §3 / ADR-CA-002 §1 / ADR-CA-003 §1. REQ-CA-010/013..019/021.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-006 AuxModelPort + selection ports + VaultPort.readBinary + keys

Implement SPEC-CA-004/005/006: three narrow port interfaces under
src/domain/ports/ (AuxModelPort + AuxModelRunOptions; SelectionSourcePort with
getCurrentSelection/onSelectionChange/supportsBrowserSelection; SelectionHighlightPort
with show/clear); append readBinary(path) -> Promise<Uint8Array> to VaultPort
(seven P0-P4 members byte-identical, no AttachmentPort — ADR-CA-001 §3); add the
three InjectionKeys (AUX_MODEL_PORT/SELECTION_SOURCE_PORT/SELECTION_HIGHLIGHT_PORT)
and barrel re-exports.

Throwing readBinary stubs added to the three bridges to keep the build green
between the interface widening here and the real impls (T-CA-013 Mock/LS,
T-CA-014 Obsidian) — TDD red-first for T-CA-012 preserved (the stub throws).

Greens the four T-CA-005 RED port tests. Full vue-tsc + eslint + the port tests
green. Deleted-symbol guard green (no relaxation).

TEST-CA-010/021/028 (shape legs). SPEC-CA-004/005/006/028.
REQ-CA-010/013/014/015/018/021. ADR-CA-001 §3 / ADR-CA-002 §1 / ADR-CA-003 §1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-007 RED aux-model impls + scriptable fake-ports member

Author the failing unit tests for the three-bridge AuxModelPort impls:
the scriptable Mock aux (setAuxResponse/setAuxError/setAuxEmpty, aborted
signal -> err, records prompt/systemPrompt), the browser-safe never-throw
LocalStorage aux, and the fake-ports auxModel member.

TEST-CA-021 (Mock-aux leg), TEST-CA-018 (aux backing).
SPEC-CA-008, SPEC-CA-009, SPEC-CA-004. REQ-CA-021, NFR-CA-001, NFR-CA-010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-008 MockBridge + LocalStorageBridge AuxModelPort impls

Add the scriptable Mock aux (setAuxResponse/setAuxError/setAuxEmpty, aborted
signal -> err, records prompt/systemPrompt, empty -> err; mirrors MockShellExec),
the browser-safe never-throw LocalStorage aux, expose auxModel getters on both
bridges, and wire the fake-ports auxModel member. No node:*, no spawn, no obsidian;
Result-mapped error/empty/abort, never throws across the boundary.

Greens TEST-CA-021 (Mock-aux leg).
SPEC-CA-008, SPEC-CA-009, SPEC-CA-004. REQ-CA-021, NFR-CA-001, NFR-CA-010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-009 ObsidianBridge AuxModelPort cold-start delegate

createAuxModel() builds a fresh cold-start ChatRuntimePort, drains
query(turn, [], {forceColdStart:true}), accumulates text (tool/thinking/usage
ignored, done terminates), maps a streaming error chunk / empty-accumulated /
aborted signal -> Result.err and non-empty text -> ok. The signal aborts the
subprocess via the runtime cancel(); cold-start only, never resumes. Wrapped in
tryAsync; no obsidian symbol leaks past this file. Coverage-excluded — behaviour
gated by the manual leg TEST-CA-M1 + TEST-CA-029.

SPEC-CA-007 (aux leg), SPEC-CA-004. REQ-CA-021, NFR-CA-001, NFR-CA-010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-010 RED re-point title-gen + refine onto AuxModelPort

Migrate the two use-case tests to inject the scriptable MockAuxModel instead of a
MockChatRuntime. Same observable assertions (title parsed / refined + clarification
outcomes / err on empty / err on aux error / never surfaces showError); the
prompt + systemPrompt handed to the aux are asserted; the chunk-scripting +
ignores-tool/thinking cases collapse (that concern now lives in the aux impl).
A byte-identity block calls titleGeneration.ts / instructionRefine.ts directly.

RED: MockAuxModel is not assignable to the constructors' ChatRuntimePort param.

TEST-CA-018. SPEC-CA-018, ADR-CA-002 §3. REQ-CA-021, NFR-CA-004, NFR-CA-010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-011 re-point title-gen + refine onto AuxModelPort

Change GenerateTitleUseCase + RefineInstructionUseCase ctors (runtime) -> (aux);
delete the prepareTurn + accumulate drain loops; replace with a single aux.run(...).
Outcome mapping unchanged (parse -> ok / err; no showError; no providerId branch).
Wire ChatSurface.vue: inject AUX_MODEL_PORT optionally; title-gen degrades to
err('aux model unavailable') when absent (prod provide deferred to T-CA-033); refine
built only when aux present. Migrate instructionLadder.test injected double to a
scripted MockAuxModel (assertions unchanged). Pure transforms byte-identical; no dead
ChatRuntimePort side-query code.

Greens TEST-CA-018. SPEC-CA-018, ADR-CA-002 §3. REQ-CA-021, NFR-CA-004, NFR-CA-010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ca): T-CA-011 record real commit SHAs in log + workflow-state

Backfill the T-CA-011 commit SHA (248b289) in implementation-log.md and the
workflow-state hand-off note (the SHA is real only after the code commit lands).

SPEC-CA-018, ADR-CA-002 §3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-012 RED Mock/LS selection ports + readBinary + fake-ports members

Failing unit tests for the Layer-3 selection + readBinary infra:
- MockSelectionPorts.test.ts: inert-by-default scriptable Mock SelectionSourcePort
  (getCurrentSelection null, supportsBrowserSelection false, setSelection pushes
  to listeners + backs getCurrentSelection, canvas path, unsubscribe) + recording
  no-op Mock SelectionHighlightPort (show/clear recorded for assertion).
- MockReadBinary.test.ts: Mock VaultPort.readBinary returns seeded bytes from an
  in-memory map (seedBinary helper); a missing path rejects.
- LocalStorageSelectionPorts.test.ts: inert LS selection ports
  (supportsBrowserSelection false, onSelectionChange never fires, no-op highlight)
  + localStorage-backed readBinary; missing path rejects.
- fake-ports.test.ts: assert new scriptable selectionSource + recording
  selectionHighlight members exist + are scriptable.

RED confirmed: 8 failed (missing MockSelectionPorts module / seedBinary /
selectionSource / selectionHighlight members).

Traces: TEST-CA-010/013/014/015, SPEC-CA-008, SPEC-CA-009, REQ-CA-013/017/018,
NFR-CA-010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-013 Mock/LS selection ports + readBinary + fake-ports members

Green the T-CA-012 RED tests (31/31):
- MockSelectionPorts.ts: scriptable MockSelectionSource (inert by default,
  setSelection(captured|null) pushes to listeners + backs getCurrentSelection,
  supportsBrowserSelection:false, unsubscriber) + recording MockSelectionHighlight
  (show/clear → .calls array). The canvas path is driven by setSelection of a
  CanvasSelectionContext.
- MockBridge: in-memory binaries Map; real readBinary returns a defensive copy,
  missing path rejects (the Result.err path of AddImageUseCase); seedBinary helper;
  get selectionSource / get selectionHighlight.
- LocalStorageComposerPorts: inert LocalStorageSelectionSource (onSelectionChange
  registers but never fires, supportsBrowserSelection:false) + no-op
  LocalStorageSelectionHighlight.
- LocalStorageBridge: base64-backed readBinary over a specorator:binary: key +
  seedBinary; get selectionSource / get selectionHighlight.
- fake-ports.ts: selectionSource / selectionHighlight members + FakePorts entries.

No node:*, no obsidian in Mock/LocalStorage. Both bridges ship
supportsBrowserSelection:false (SPEC-CA-005 contract for these two bridges).

Traces: SPEC-CA-008, SPEC-CA-009, REQ-CA-013/017/018, NFR-CA-001, NFR-CA-010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-014 ObsidianBridge selection ports + real readBinary

Implement the production (coverage-excluded) selection seam + vault byte read
per SPEC-CA-007:
- ObsidianSelectionPorts.ts (new): ObsidianSelectionSource reads the CM6 editor
  selection (0-based startLine carried verbatim per SPEC-CA-003) + the Obsidian
  canvas node selection, polled at 250 ms (parity claudian), firing
  onSelectionChange on a change; a transient read error is swallowed -> null
  (NFR-CA-010, EC-CA-12). supportsBrowserSelection is an honest fixed false (the
  P5 defer of the fragile embedded-view leg, REQ-CA-018). ObsidianSelectionHighlight
  paints/removes a CM6 Decoration over the captured editor range (show/clear,
  ported from claudian SelectionHighlight; clear idempotent).
- ObsidianBridge: real readBinary (vault.readBinary -> new Uint8Array, missing
  file rejects); lazily-created get selectionSource / get selectionHighlight
  mirroring the get shellExec idiom. No obsidian/CM6 symbol leaks past
  ObsidianSelectionPorts.ts.

Coverage-excluded infra; behavioural gate is the MANUAL legs TEST-CA-M1/M3 +
TEST-CA-017 (scheduled in test-plan.md, not self-claimed green).

@codemirror/state + @codemirror/view promoted to devDependencies (Obsidian
runtime externals, already in vite.config.ts ALL_EXTERNALS + installed; never
bundled) so the ported CM6 decoration's imports pass import/no-extraneous-deps.
Not a runtime dependency.

Traces: SPEC-CA-007, REQ-CA-010/013/014/015/017/018, NFR-CA-001 (manual leg),
NFR-CA-010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-015 RED bounded base64 image-encode + gate constants

Failing unit tests for the pure encode + gate constants (SPEC-CA-010):
- MAX_IMAGE_BYTES === 8 * 1024 * 1024 (8 MiB);
- IMAGE_MIME_ALLOW_LIST is exactly ['image/png','image/jpeg','image/webp',
  'image/gif'];
- encodeImageBase64(bytes, mime) returns base64 with no data-URI prefix, pure /
  deterministic, round-trips through atob, empty bytes -> '';
- resolveImageMime(path) maps .png/.jpg/.jpeg/.webp/.gif (case-insensitive) to an
  allow-list member and .exe + any non-image (.md/.svg/.bmp/.ico/extensionless)
  to null (EC-CA-2); every resolved MIME is an allow-list member.

RED confirmed: the import of @/infrastructure/image/imageEncode fails to resolve
(module absent) -> the test file errors at import time.

Traces: TEST-CA-010 (encode leg), TEST-CA-012 (gate-constant leg), SPEC-CA-010,
REQ-CA-010/012, NFR-CA-009/011.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-016 bounded base64 image-encode + gate constants

Green the T-CA-015 RED tests (11/11) per SPEC-CA-010:
- src/infrastructure/image/imageEncode.ts (new): MAX_IMAGE_BYTES = 8 * 1024 * 1024;
  IMAGE_MIME_ALLOW_LIST = the exact four members; the pure encodeImageBase64(bytes,
  mime) — btoa over a chunked byte->char fold in browser/Obsidian, Buffer fallback
  in Node, NO data-URI prefix, empty -> ''; resolveImageMime(path) extension ->
  allow-list resolver, case-insensitive, .exe + any non-image -> null (EC-CA-2).

Pure / total; never throws on valid input; no obsidian import; no new package.json
runtime dependency. The 8 MiB gate ORDER is enforced later by AddImageUseCase
(T-CA-020) — this file is only the constants + transforms.

Traces: SPEC-CA-010, REQ-CA-010/012, NFR-CA-009/011.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-017 RED pure computeWordDiff word-level LCS diff

Author the failing unit tests for the pure word-level diff (SPEC-CA-011):
tokenise on split(/(\s+)/), LCS over the tokens, single-row ToolDiffData with
word-granular equal/insert/delete ops, filePath '', stats counts. Covers the
REQ-CA-023 bank->riverbank acceptance, EC-CA-10 identical-text no-op, empty
inputs, all-insert/all-delete, and never-throws. RED: computeWordDiff.ts does
not yet exist.

TEST-CA-023 (U leg), TEST-CA-023b, SPEC-CA-011, REQ-CA-023, NFR-CA-011, EC-CA-10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-018 computeWordDiff pure word-level DP/LCS diff

Tokenise both sides on split(/(\s+)/), classic LCS DP table, back-trace into a
single-row ToolDiffData with one word-granular equal/insert/delete DiffLine per
token, filePath '', stats counting non-whitespace insert/delete tokens. Greens
the T-CA-017 RED tests (7/7): bank->riverbank acceptance, EC-CA-10 identical
no-op, empty/one-sided-empty edges. The ToolDiffData feeds the unchanged P2
DiffView verbatim. Pure/total, never throws; no new runtime dep; no
obsidian/node/Vue import.

SPEC-CA-011, REQ-CA-023, NFR-CA-011, EC-CA-10, TEST-CA-023/023b.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-019 RED parseInlineEditResponse + inlineEditPrompt

Author the failing unit tests for the two pure transforms (SPEC-CA-012/013):
parseInlineEditResponse maps first <replacement> -> replacement (trimmed inner),
else <insertion> -> insertion, else non-empty trimmed -> clarification, else ->
failure (REQ-CA-022 acceptances: Bonjour replacement / "Which meaning?"
clarification / "" failure); INLINE_EDIT_SYSTEM_PROMPT documents the
replacement/insertion/clarification contract, buildInlineEditPrompt frames the
selection + instruction (+ optional notePath), both pure/total. RED: neither
module exists yet.

TEST-CA-022, TEST-CA-021 (prompt leg), SPEC-CA-012, SPEC-CA-013, REQ-CA-021,
REQ-CA-022, NFR-CA-004.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-020 parseInlineEditResponse + inlineEditPrompt

Port the inline-edit parse + prompt under src/application/chat/inlineEdit/.
parseInlineEditResponse: InlineEditParse union + parse (first <replacement> ->
replacement trimmed, else <insertion> -> insertion trimmed, else non-empty
trimmed -> clarification, else failure), ported from claudian
core/prompt/inlineEdit.ts. inlineEditPrompt: INLINE_EDIT_SYSTEM_PROMPT documents
the replacement/insertion/clarification contract; buildInlineEditPrompt frames
instruction + <editor_selection> (+ optional notePath). Greens the T-CA-019 RED
tests (14/14). Pure/total, never throws; no obsidian/Vue import.

SPEC-CA-012, SPEC-CA-013, REQ-CA-021, REQ-CA-022, NFR-CA-004, TEST-CA-022,
TEST-CA-021 (prompt leg).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-021 RED AddFileContextUseCase pure file-set ops

Author the failing unit tests for the pure file-set ops (SPEC-CA-014):
add(current, path) appends { path, displayName } (basename-without-extension)
unless path is already present (idempotent no-op, EC-CA-3); remove(current,
path) drops the matching entry (EC-CA-4); both Result.ok(nextSet); empty/
whitespace path -> Result.err. RED: AddFileContextUseCase does not yet exist.

TEST-CA-001 (add leg), TEST-CA-003 (displayName leg), SPEC-CA-014,
REQ-CA-001/002/003, NFR-CA-004, EC-CA-3/4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-022 AddFileContextUseCase pure file-set ops

add/remove over readonly AttachedFileRef[] — path-unique idempotent add (EC-CA-3),
remove (EC-CA-4), displayName = basename-without-extension (final extension only,
dotfiles whole, / and \ separators), empty/whitespace path -> err. No port (pure
set math; the store owns the reactive set). Greens the T-CA-021 RED tests (9/9).
Result-returning, pure, never throws; no obsidian/Vue import.

SPEC-CA-014, REQ-CA-001, REQ-CA-002, REQ-CA-003, NFR-CA-004, TEST-CA-001 (add
leg), TEST-CA-003 (displayName leg).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-023 RED AddImageUseCase gate-order + no-secret

Author the failing unit tests against the in-memory Mock readBinary
(SPEC-CA-015): execute(path) runs the gate IN ORDER — (1) MIME resolve, .exe ->
err before any read (EC-CA-2); (2) readBinary via tryAsync, missing file -> err;
(3) byteSize > MAX_IMAGE_BYTES -> err measured before encode (EC-CA-1), the 8 MiB
boundary accepted; (4) else encode -> ok({path, mimeType, byteSize, dataBase64}).
The payload carries no secret (exactly the four fields, base64 alphabet only);
never throws. RED: AddImageUseCase does not yet exist.

TEST-CA-007 (U leg), TEST-CA-012, TEST-CA-030 (no-secret leg), SPEC-CA-015,
REQ-CA-007/012, NFR-CA-009/004, EC-CA-1/2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-024 AddImageUseCase allow-list + 8 MiB gate + base64

execute(path) runs the gate in order: (1) resolveImageMime -> null rejects a
non-image before any read (EC-CA-2); (2) vault.readBinary wrapped in tryAsync,
missing file -> err; (3) byteSize > MAX_IMAGE_BYTES -> err measured before encode
(EC-CA-1); (4) encodeImageBase64 -> ok({path, mimeType, byteSize, dataBase64}).
Greens the T-CA-023 RED tests (7/7). A rejected image never becomes an
AttachedImage; payload carries no secret. Result-returning, never throws; no
provider branch; no obsidian/Vue import (the encode helper is the
SPEC-CA-010-sanctioned application->infra import).

SPEC-CA-015, REQ-CA-007, REQ-CA-012, NFR-CA-009, NFR-CA-004, TEST-CA-007 (U leg),
TEST-CA-012, TEST-CA-030 (no-secret leg), EC-CA-1/2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-025 RED CaptureSelectionUseCase capture + highlight + retain

Author the failing unit tests against the Mock source + recording highlight
(SPEC-CA-016): an editor selection drives highlight.show + is captured
(REQ-CA-014); null + focusWithinChat false -> drop + highlight.clear + null
(EC-CA-5-clear); null + focusWithinChat true -> retain prior selection, highlight
stays (EC-CA-11); canvas/browser capture with NO highlight; current() returns the
latest captured or null; never throws. RED: CaptureSelectionUseCase does not yet
exist.

TEST-CA-013/014/015/016 (U legs), TEST-CA-018b (U leg), SPEC-CA-016,
REQ-CA-013..018, NFR-CA-010, EC-CA-5-clear, EC-CA-11.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-026 CaptureSelectionUseCase capture + highlight + retain

onChange(sel, focusWithinChat): editor selection drives highlight.show + capture
(REQ-CA-014); null + focus-not-in-chat drops + highlight.clear + null
(EC-CA-5-clear); null + focus-in-chat retains the prior selection, highlight
stays (EC-CA-11); canvas/browser capture with no highlight. current() seeds from
source.getCurrentSelection() until the first observed tick, then the tracked
value is authoritative (an explicit deselection never resurrects a stale read).
Greens the T-CA-025 RED tests (7/7). Result-returning, never throws; no provider
branch; no obsidian/Vue import.

SPEC-CA-016, REQ-CA-013, REQ-CA-014, REQ-CA-015, REQ-CA-016, REQ-CA-017,
REQ-CA-018, NFR-CA-010, TEST-CA-013/014/015/016 (U legs), TEST-CA-018b (U leg).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-027 RED InlineEditUseCase aux -> parse -> outcome

Author the failing unit tests against the scriptable Mock aux (SPEC-CA-017):
execute(selectedText, instruction, notePath?, signal?) wires
buildInlineEditPrompt + INLINE_EDIT_SYSTEM_PROMPT + signal through aux.run;
replacement -> ok with computeWordDiff preview, insertion -> ok, clarification ->
ok (REQ-CA-026), failure/aux-err/empty/abort -> err (EC-CA-8/9), empty/whitespace
instruction -> err with NO aux query; continue re-frames the prior exchange +
reply and re-runs; never throws; no providerId branch. RED: InlineEditUseCase
does not yet exist.

TEST-CA-021 (use-case leg), TEST-CA-026, TEST-CA-027, SPEC-CA-017,
REQ-CA-021/022/026/027/028, NFR-CA-004/010, EC-CA-8/9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-028 InlineEditUseCase over AuxModelPort

InlineEditOutcome union (replacement carries diff: computeWordDiff). execute
guards an empty instruction -> err, else runs aux.run(buildInlineEditPrompt,
{ systemPrompt: INLINE_EDIT_SYSTEM_PROMPT, signal }) -> shared run() mapping via
parseInlineEditResponse (replacement -> ok+diff, insertion -> ok, clarification
-> ok, failure/aux-err -> err); continue re-frames the prior exchange + reply
into one instruction and re-runs. Greens the T-CA-027 RED tests (12/12).
Result-returning, never throws (aux maps error/empty/abort; parse + diff are
pure/total); no providerId branch (SPEC-CA-029); no obsidian/Vue import.

SPEC-CA-017, REQ-CA-021, REQ-CA-022, REQ-CA-026, REQ-CA-027, REQ-CA-028,
NFR-CA-004, NFR-CA-010, TEST-CA-021 (use-case leg), TEST-CA-026, TEST-CA-027,
EC-CA-8/9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ca): T-CA-017..028 stage-7 close-out — tick tasks + workflow-state

Tick the Layer-4 APPLICATION DoD boxes (T-CA-017..028) in tasks.md and update
workflow-state.md: implementation-log.md artifact + Stage-7 progress row now read
batches 0-4 (T-CA-001..028); appended the dev hand-off note (12 commit SHAs,
verification, deviations) handing off to T-CA-029 (qa, UI composables RED).

SPEC-CA-011..017, TASKS-CA-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-029 RED port composables + useCapturedSelection

Author the failing composable tests (SPEC-CA-025): useAuxModelPort /
useSelectionSourcePort / useSelectionHighlightPort inject-or-throw (mirror
useVaultPort), and useCapturedSelection (subscribe onSelectionChange, compute
focus-within-chat, feed CaptureSelectionUseCase.onChange, expose reactive
current + clear()).

RED: the four composables under src/ui/composables/ do not yet exist.

TEST-CA-013 (composable leg), TEST-CA-016 (composable leg). SPEC-CA-025.
REQ-CA-013/016/021. NFR-CA-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-030 port composables + useCapturedSelection

Implement per SPEC-CA-025: useAuxModelPort / useSelectionSourcePort /
useSelectionHighlightPort (inject the key, throw when unprovided — mirror
useVaultPort), and useCapturedSelection (subscribe onSelectionChange, compute
focus-within-chat from the active element relative to a chatRoot ref, feed
CaptureSelectionUseCase.onChange, expose reactive current + clear()).

Greens T-CA-029 (10/10). No obsidian import under src/ui/**; DTO-only.

SPEC-CA-025. REQ-CA-013/016/021. NFR-CA-002/004.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-031 RED FileChips.vue + FileChips.po.ts

Author the failing component test + co-located data-testid PageObject
(SPEC-CA-019): one chip per AttachedFileRef showing displayName, the wikilink
[[path]] exposed declaratively (title attr, no raw HTML), keyboard Enter/Space
→ open, a labelled remove control Enter/Space → remove, EC-CA-14 <script>
renders verbatim. data-testid: file-chips, file-chip, file-chip-link,
file-chip-remove — queried via the PageObject only.

RED: FileChips.vue does not yet exist.

TEST-CA-001 (A leg), TEST-CA-003 (A leg), TEST-CA-005, TEST-CA-031 (file leg).
SPEC-CA-019. REQ-CA-001/003/005. NFR-CA-002/003/005/008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-032 FileChips.vue removable wikilink chips

Implement per SPEC-CA-019: <script setup> component rendering the attached-file
set as removable chips. Each chip is a button showing displayName with the
wikilink [[path]] on a declarative :title (no v-html); Enter/Space → open;
a labelled remove button Enter/Space → remove. Adds the
agent.chat.context.{files,images,selection} i18n group (en + de, NFR-CA-013).

Greens T-CA-031 (10/10). No v-html/innerHTML; no obsidian import; no
window.confirm/alert/prompt.

SPEC-CA-019. REQ-CA-001/003/005. NFR-CA-002/003/008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-033 RED ImageContextBar.vue + ImageThumb.vue + POs

Author the failing component tests + co-located PageObjects (SPEC-CA-020):
ImageContextBar renders one ImageThumb per AttachedImage binding
<img :src="resolveThumbSrc(path)"> declaratively (no v-html, REQ-CA-011) with
alt = basename; opening a thumb emits preview (REQ-CA-008); a remove control
emits remove (REQ-CA-009). data-testid: image-context-bar, image-thumb,
image-thumb-img, image-thumb-remove, image-thumb-preview — PageObject only.

RED: ImageContextBar.vue / ImageThumb.vue do not yet exist.

TEST-CA-007 (A leg), TEST-CA-009, TEST-CA-011. SPEC-CA-020.
REQ-CA-007/008/009/011. NFR-CA-002/003/005/008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-034 ImageContextBar.vue + ImageThumb.vue thumbnails

Implement per SPEC-CA-020: ImageThumb binds <img :src="resolveThumbSrc(path)">
declaratively (no v-html) with alt = basename, click + Enter/Space → preview,
a labelled remove button → remove; ImageContextBar is a labelled row of
ImageThumbs re-emitting preview/remove. resolveThumbSrc is injected so the
components stay obsidian-free; the turn payload stays the base64 dataBase64.

Greens T-CA-033 (11/11). No v-html/innerHTML; no obsidian import; no
window.confirm/alert/prompt.

SPEC-CA-020. REQ-CA-007/008/009/011. NFR-CA-002/003/008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-035 RED SelectionIndicator.vue + PO

Author the failing component test + co-located PageObject (SPEC-CA-021): a
captured-selection chip with a TEXT label per kind (editor/canvas/browser) +
a labelled clear control emitting clear (REQ-CA-015); the browser-capture
affordance is GATED — renders only when supportsBrowserSelection is true, no
error otherwise (REQ-CA-018, EC-CA-7, SPEC-CA-029). data-testid:
selection-indicator, selection-indicator-label, selection-indicator-clear,
selection-indicator-browser-capture — PageObject only.

RED: SelectionIndicator.vue does not yet exist.

TEST-CA-015 (A leg), TEST-CA-018b (A leg). SPEC-CA-021. REQ-CA-015/018.
NFR-CA-005/008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-036 SelectionIndicator.vue captured-selection chip

Implement per SPEC-CA-021: <script setup> chip with a TEXT label per kind
(editor note+line span / canvas+node count / browser title ?? source) + a
labelled clear control emitting clear. The browser-capture affordance is GATED
behind v-if="supportsBrowserSelection" — no affordance + no error otherwise
(EC-CA-7, SPEC-CA-029, the honest defer). Adds the selection.browserCapture
i18n label (en + de).

Greens T-CA-035 (7/7). No v-html; no obsidian import; no
window.confirm/alert/prompt.

SPEC-CA-021. REQ-CA-015/018. NFR-CA-002/003/008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-037 RED modalSeam OpenInlineEdit/OpenImagePreview handles

Extend tests/ui/chat/modalSeam.ts.test.ts with the P5 seam additions
(SPEC-CA-023): OpenInlineEditFn / OpenImagePreviewFn, the OPEN_INLINE_EDIT /
OPEN_IMAGE_PREVIEW keys, useOpenInlineEdit() falling back to an auto-reject
(null — no silent apply, mirrors useInstructionConfirm), useOpenImagePreview()
falling back to a no-op resolve. The four P3/P4 handles stay byte-identical.

RED: the inline-edit + image-preview seam handles do not yet exist (4 new
tests fail, the 2 P3/P4 tests stay green).

TEST-CA-020 (fallback leg). SPEC-CA-023. REQ-CA-008/020. NFR-CA-003.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-038 modalSeam OpenInlineEdit/OpenImagePreview handles

Implement per SPEC-CA-023 (additive — the four P3/P4 handles unchanged):
InlineEditDecision type, OpenInlineEditFn, OpenImagePreviewFn, the
OPEN_INLINE_EDIT / OPEN_IMAGE_PREVIEW InjectionKeys, useOpenInlineEdit()
(auto-reject null fallback — no silent apply) + useOpenImagePreview() (no-op
resolve fallback).

Greens T-CA-037 (6/6). No obsidian import; no window.*.

SPEC-CA-023. REQ-CA-008/020. NFR-CA-003.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-039 InlineEditModal + ImagePreviewModal (Obsidian Modal)

Implement per SPEC-CA-024 in src/plugin/modals/ (coverage-excluded):
InlineEditModal drives the Prompt → Querying → Preview / Clarify / Failed →
Applied / Rejected state machine, reusing the UNCHANGED DiffView (mounted as a
tiny Vue app over InlineEditOutcome.diff) for the replacement preview; dismiss
aborts the AbortController → InlineEditUseCase Result.err (EC-CA-8); accept
resolves {kind:'accept',editedText}, reject {kind:'reject'}, failure surfaces a
NotificationPort notice + resolves null. ImagePreviewModal shows the full-size
image via declarative createEl('img', { attr:{src} }) — no innerHTML —
dismissable by Escape + a close control.

Both are Obsidian Modal subclasses — never window.confirm/alert/prompt. No RED
test; behavioural gate is the human-run TEST-CA-M2 (+ TEST-CA-024/025),
scheduled in test-plan.md.

SPEC-CA-024. REQ-CA-008/020/023/024/025/026/027. NFR-CA-003/008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-040 RED ChatComposer context-bar slot extension

Extend ChatComposer.test.ts + ChatComposer.po.ts with the P5 context-bar slot
(SPEC-CA-022): the additive region above the textarea hosts FileChips +
ImageContextBar + SelectionIndicator when their props are non-empty; the
composer gains optional props (attachedFiles/images/capturedSelection/
supportsBrowserSelection/resolveThumbSrc) and re-emits the children's
removeFile/openFile/removeImage/previewImage/clearSelection; with no context
the bar is hidden (P4 byte-identical, G2); the P1 send path stays unchanged.
data-testid: composer-context-bar.

RED: the context-bar slot + the optional props do not yet exist (7 new tests
fail, the 14 P1 tests stay green).

TEST-CA-004, TEST-CA-006. SPEC-CA-022. REQ-CA-001/004/006/010/019. NFR-CA-005.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-041 ChatComposer context-bar slot (additive)

Implement per SPEC-CA-022 (additive — no rename/removal of any P4 member): an
optional context-bar region above the textarea hosting FileChips +
ImageContextBar + SelectionIndicator, rendered only when their props are
non-empty (the bar is hidden when all three empty → byte-identical to P4, G2).
Adds optional props (attachedFiles/images/capturedSelection/
supportsBrowserSelection/resolveThumbSrc) and re-emits the children's
removeFile/openFile/removeImage/previewImage/clearSelection to the parent.

Greens T-CA-040 (21/21); P4 extension regression stays green (9/9) —
additivity holds, the send path is byte-identical when the slot is absent.
<script setup>; no v-html; no obsidian import; no window.confirm/alert/prompt.

SPEC-CA-022. REQ-CA-001/004/006/010/019. NFR-CA-002/003.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ca): tick Layer 5 tasks + batch-5 hand-off (T-CA-029..041)

Mark the 13 Layer 5 UI tasks (T-CA-029..041) DoDs complete in tasks.md, bump
the implementation-log + Stage 7 rows in workflow-state.md to batches 0-5
(in-progress — Layers 6-8 + the human manual legs remain), and append the
batch-5 dev hand-off note (the 13 commit SHAs, verification performed,
deviations, next-agent → dev T-CA-042 / orchestrator for Layer 6/7).

TASKS-CA-001. SPEC-CA-019..025.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-042 mint --sp-* context/attachment tokens + contract

Adds the §4.12 token block to src/ui/styles/tokens.css: the eight P5
surfaces (file/image chips, context-bar gap, image thumb + preview modal,
selection highlight, inline-edit modal width) as token-layer var lookups
or bare dimensions. The word-diff preview rides the P2 §4.9 diff tokens —
no new diff token. Updates tokens.test with the eight-token presence
contract + the TEST-CA-032 leak guard (no raw hex / raw Obsidian var in
the §4.12 block).

Implements SPEC-CA-027. TEST-CA-032; NFR-CA-007.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-043 RED wire-in: ports+launchers provided + context bar mounts

Adds tests/ui/chat/attachmentsMount.ts.test.ts asserting AUX_MODEL_PORT,
SELECTION_SOURCE_PORT, SELECTION_HIGHLIGHT_PORT + the OPEN_INLINE_EDIT /
OPEN_IMAGE_PREVIEW launchers are provided in both src/ui/main.ts and
AgentSidebarView, and the context bar mounts. RED: neither entry point
provides them and ChatSurface does not yet inject the selection ports or
call useCapturedSelection, so a scripted Mock selection never renders the
selection-indicator and the standalone mount never reads the new bridge
members.

TEST-CA-020 (mount leg); SPEC-CA-026; REQ-CA-008/020/021; NFR-CA-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): T-CA-044 wire P5 ports + launchers + mount the context bar

Provides AUX_MODEL_PORT (closing the deferred T-CA-011 provide so title/
refine no longer degrade), SELECTION_SOURCE_PORT, SELECTION_HIGHLIGHT_PORT,
and the OPEN_INLINE_EDIT / OPEN_IMAGE_PREVIEW launchers in both entry
points: AgentSidebarView (real Obsidian Modals via the new
src/plugin/inlineEditLauncher.ts — the only wiring file importing obsidian
+ the P5 modals) and src/ui/main.ts (Mock aux + inert selection ports +
browser-safe auto-reject/no-op stand-ins). Registers the inline-edit editor
command gated on a non-empty selection (main.ts). ChatSurface injects the
selection ports optionally, owns the attached-file/image sets +
useCapturedSelection, resolves a data: thumb src, and mounts the context bar
into the ChatComposer slot. P1-P4 surfaces stay behaviour-identical.

AgentSidebarView.resolveAuxModel tolerates the ObsidianBridge
createAuxModel() factory and the MockBridge auxModel getter so MockBridge-fed
mount tests keep working.

Implements SPEC-CA-026. REQ-CA-008/020/021; NFR-CA-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): T-CA-045 standalone attachments smoke (dev leg)

Extends tests/ui/main.ts.test.ts with the deterministic standalone
attachments smoke: the P5-wired surface + composer mount against MockBridge
without an inject-or-throw, and the composer-context-bar (plus chip/thumb/
indicator children) is hidden when the file/image/selection sets are empty
(the P4-byte-identical gate, SPEC-CA-022 G2). The interactive file-chip /
image-thumb / scripted-selection / inline-edit-stand-in flows depend on the
attach affordance + store sets (T-CA-033/034) and a live npm run dev server,
recorded as a deferred human-run leg in test-plan.md (the agent does not
start the long-running dev server). Ticks T-CA-042..045 in tasks.md.

TEST-CA-007/004 (dev leg); NFR-CA-002; SPEC-CA-026.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ca): T-CA-042..045 Stage 7 close-out — batches 6-7 done

Updates workflow-state.md: implementation-log.md stays in-progress (batch 8
GATE — human-owned manual legs T-CA-046/047 + close-out — remains); records
the Layer 6 STYLES + Layer 7 WIRE-IN completion, the four commit SHAs, the
verification run, and the hand-off to the human/release gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ca): T-CA-048 gate fixes — lint + docs:api + regenerate styles.css

Deterministic-gate fixes found running the full pre-PR chain:
- imageEncode.ts: drop the dead `Buffer` Node fallback (TypeDoc had no @types/node
  → docs:api error); `btoa` is global in Obsidian/Electron, browsers, and Node ≥16
  (vitest), so the primary path always wins. Pure/total unchanged; 11/11 green.
- ImagePreviewModal.ts + useCapturedSelection.test.ts: brace two void-returning
  arrow shorthands (@typescript-eslint/no-confusing-void-expression).
- styles.css: regenerate the shipped CSS with the P5 --sp-* tokens (T-CA-042) +
  the new context-bar/chip/thumbnail/selection component styles.

Full gate green: typecheck 0 errors, eslint 0 errors, vitest 1279 passed (185
files), npm run build + build:web + docs:api clean, npm audit (high) clean.

T-CA-048 (deterministic legs). NFR-CA-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ca): T-CA-048 mark deterministic gate green in workflow-state

Records the full pre-PR gate result (typecheck/lint/test 1279/build/build:web/
docs:api/audit all green). Only the human-owned manual legs T-CA-046/047 and the
draft-PR push into `next` remain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): FIX-1 RED — assert per-tab context folds into the submitted turn + clears

Adds RED assertions to tabsStore.test.ts that the present context sets
(attachedFiles/images + the captured selection, mapped by union member into
editorSelection/canvasSelection/browserSelection) travel with the submitted
ChatTurnRequest and that onConsumed fires on a successful submit (the clear
seam) but not when the send is guarded out. Was R-CA-001 + R-CA-004.

Covers REQ-CA-004/010/019; SPEC-CA-001/022/028. Goes RED until the fold lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): FIX-1 — fold per-tab context into the submitted turn + clear on submit

Extends tabsStore.sendMessage additively with an optional SendMessageContext:
attachedFiles/images + the captured selection (mapped by union member into
editorSelection/canvasSelection/browserSelection) fold into the ChatTurnRequest,
written only when non-empty so a no-context turn stays byte-identical to P1–P4
(G2, SPEC-CA-028). On a successful submit `onConsumed` fires; ChatSurface.onSubmit
assembles the snapshot and clears attachedFiles/images/the captured selection for
the next turn. Was R-CA-001 + R-CA-004 — closes the never-travels / never-clears gap.

Implements REQ-CA-004/010/019. SPEC-CA-001 §1, SPEC-CA-022.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): FIX-2.1 RED — a file @mention adds a context chip (onFileMention)

Adds RED assertions to useComposerMode.test.ts: confirming a `file`-kind referent
fires `onFileMention` with the file's vault path (the chip) AND still inserts the
mentionText token (P4 REQ-CP-013 unchanged); a non-file referent (subagent) does
not fire it. Was R-CA-002 (the @mention-adds-a-chip affordance). Goes RED until
the additive `onFileMention` option lands.

Covers REQ-CA-001; SPEC-CA-022.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): FIX-2.1 — a file @mention adds a context chip (additive)

useComposerMode gains an optional `onFileMention(path)` callback: confirming a
`file`-kind referent fires it with the referent's vault path (its `detail`)
ALONGSIDE the unchanged P4 mentionText insertion (REQ-CP-013 preserved). ChatSurface
wires it to `AddFileContextUseCase.add` so a resolved file mention now also pins a
removable chip. Adds the i18n `context.attach` + `context.images.rejected` keys
(en/de) for the later attach legs. Was part of R-CA-002.

Implements REQ-CA-001. SPEC-CA-022. No P4 mention/composer regression.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): FIX-2.3 RED — drop/paste files into the composer + gate in-hand bytes

Adds RED assertions: (a) AddImageUseCase.executeBytes(name, bytes) runs the same
MIME→size→encode gate over in-hand File bytes (no readBinary round-trip) for the
drop/paste path; (b) ChatComposer emits `attachFiles` with the dropped/pasted
File[] (paste-with-image prevents default; a plain-text paste does not). Was part
of R-CA-002. Goes RED until executeBytes + the composer drop/paste handlers land.

Covers REQ-CA-007/012; SPEC-CA-015/022.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): FIX-2.3 — drop/paste images into the composer through the 8 MiB/MIME gate

AddImageUseCase gains `executeBytes(name, bytes)` — the same MIME→size→encode gate
as `execute` over in-hand File bytes (no readBinary). ChatComposer marshals @drop/
@dragover/@paste DOM events and emits `attachFiles(File[])` (paste-with-image
prevents the default insert). ChatSurface gates each dropped/pasted image via
executeBytes, adds it idempotently, and surfaces NotificationPort.showWarning on a
reject (oversize/non-image); non-image files are skipped (parity with claudian's
image-only drop). The composer never imports `obsidian` and never reads bytes. Was
part of R-CA-002.

Implements REQ-CA-007/012. SPEC-CA-015/022.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): FIX-2.2 RED — paperclip attach control + PICK_ATTACHMENT seam

Adds RED assertions: (a) the additive `usePickAttachment` seam handle resolves a
picked vault path + kind (file/image) or null, falling back to a no-op null when
unwired (no attach); (b) ChatComposer renders a labelled attach control that emits
`attach` on click so the parent opens the picker via the seam. Was part of R-CA-002.
Goes RED until the seam handle + the composer attach button land.

Covers REQ-CA-001/007; SPEC-CA-022/026.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): FIX-2.2 — paperclip attach control + vault file/image picker seam

Adds the additive `PickAttachmentFn`/`PICK_ATTACHMENT`/`usePickAttachment` modal
seam (falls back to a no-op null when unwired). ChatComposer renders a labelled
paperclip control emitting `attach`; ChatSurface opens the picker via the seam and
routes the picked path — an image through `AddImageUseCase.execute` (8 MiB/MIME
gate, warns on reject), a file through `AddFileContextUseCase.add`. The real
Obsidian `FuzzySuggestModal` lives in src/plugin/attachmentPicker.ts
(coverage-excluded, manual leg TEST-CA-M4); ui/main.ts provides a browser-safe
null stand-in. Was part of R-CA-002 — completes the three attach affordances.

Implements REQ-CA-001/007/012. SPEC-CA-022/026.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ca): FIX-3 RED — context resets on a new / loaded conversation

Adds a RED ChatSurface test: a captured selection renders the context bar; opening
a new conversation (the TabBar `+`, i.e. tabs.openTab) must clear it (bar hidden).
Was R-CA-003. Goes RED until the surface resets attachedFiles/images/the captured
selection on a conversation change.

Covers REQ-CA-006; SPEC-CA-022; EC-CA-6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ca): FIX-3 — reset context sets on a new / loaded conversation

ChatSurface watches the active conversation identity (activeTabId + conversationId)
and clears attachedFiles/images/the captured selection whenever it changes — the
TabBar `+` / `/new` (tabs.openTab), a fork loading into a new tab (loadIntoNewTab),
and a resume into the current tab (loadIntoTab). A plain re-render changes neither
key, so draft context is never cleared mid-edit. Was R-CA-003.

Implements REQ-CA-006. SPEC-CA-022, EC-CA-6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ca): log Stage-9 review fixes R-CA-001..004 in implementation-log

Appends the per-fix entries (FIX-1 context-travels-and-clears, FIX-2.1/2.2/2.3 the
three attach affordances, FIX-3 reset-on-conversation-change) with files, RED+green
SHAs, proving tests, outcomes, and deviations. Closes R-CA-001/002/003/004.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ca): record Stage-9 fix close-out + dev hand-off in workflow-state

Marks the implementation-log artifact line with the R-CA-001/002/003 fixes (FIX-1/2/3
GREEN), schedules the new TEST-CA-M4 picker manual leg, and appends a dev hand-off
note. R-CA-004 (the travel+clear assertion) stays qa-owned; next agent = qa
(assertion + manual legs), then reviewer re-verify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ca): commit stage-9 review artifacts + fix remediation lint

- review.md (REVIEW-CA-001) + traceability.md (TRACE-CA-001) from the parity
  self-review (verdict was changes-requested; R-CA-001..004 since fixed).
- ChatComposer.test.ts: drop two unnecessary `as unknown[][] | undefined`
  assertions on wrapper.emitted() (@typescript-eslint/no-unnecessary-type-assertion)
  — full `npm run lint` flagged them; per-file lint had missed them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build(ca): regenerate styles.css with attach-control + context-bar styles

Shipped CSS rebuilt after the Stage-9 remediation (attach button, drop/paste
affordance, context-bar component styles + refreshed Vue scoped hashes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Luis Mendez <hallo@luis-mendez.de>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ge meter + seams (#447)

* chore(tc): bootstrap P6 toolbar-controls feature + workflow-state

Cut feature/toolbar-controls off next (P0-P5 merged). Scope = parity-charter
§3.5 input toolbar control strip (model/mode/permission/thinking/service-tier/MCP
selectors + external-context control + usage/context meter) + §3.10 toolbar CSS.
Idea/research skipped (charter + audits + claudian-main stand in). Autonomous
drive. Next: /spec:requirements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(tc): P6 requirements PRD-TC-001 (toolbar & controls, accepted)

27 EARS reqs (REQ-TC-001..004 cross-cutting, 010..027 per-widget, 040..042
a11y/state) + NFR-TC-001..014, each mapped to a claudian InputToolbar path + a
TEST-TC id. Per-widget classification: BACKED = model/mode/thinking/usage-meter
(additive ChatRuntimeQueryOptions fields); SEAM (honest-defer) = service-tier→P9,
permission→P7, MCP→P8, external-context deferred (externalContextPaths stays
NG3-excluded). All P0-P5 query-option members byte-identical (NFR-TC-001).
CLAR-TC-001..003 resolved-by-recommendation (autonomous drive).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(tc): P6 design DESIGN-TC-001 + ADR-TC-001..004 (accepted)

Toolbar = additive ChatComposer region (P5 context-bar pattern; absent →
byte-identical) + per-tab TabControls state, folded on submit (ADR-TC-001).
Additive ChatRuntimeQueryOptions fields mode?/reasoning?(ReasoningChoice union)/
serviceTier?, fold only non-defaults (ADR-TC-002). Capability flags via additive
ChatRuntimePort.getToolbarCapabilities(); option lists via new narrow
ToolbarCatalogPort; no providerId branch (ADR-TC-003/004). 8 leaf widgets +
ToolbarStrip + UsageMeter; seam widgets (service-tier/permission/MCP/external-
context) defer honestly (hidden or visible-disabled). en+de i18n; toolbar/* tokens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(tc): P6 spec SPEC-TC-001..030 (toolbar & controls)

6 layer groups (domain/infra/application/ui/styles/cross-cutting), 30 SPEC items,
14 edge cases (EC-TC-1..14), 3 manual legs (TEST-TC-M1/M2/M3). Pins the additive
ChatRuntimeQueryOptions fields (mode?/reasoning?:ReasoningChoice/serviceTier?),
ReasoningChoice union (effort high|medium|low / budget tokens), ToolbarCapabilities
(getToolbarCapabilities on ChatRuntimePort), ToolbarCatalogPort + TOOLBAR_CATALOG_PORT,
TabControls, the pure foldControlOptions/buildToolbarViewModel, UsageMeter (240° arc,
warn >80%). Full REQ-TC/NFR-TC ↔ SPEC ↔ TEST coverage table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(tc): P6 tasks TASKS-TC-001 (35 tasks, TDD-ordered, 7 batches)

T-TC-001 baseline+guard-verify (no guard-relax needed — verified); DOMAIN
002..008, INFRA 009..012, APPLICATION 013..016, UI 017..028, STYLES 029,
WIRE-IN 030..032, GATE 033..035. RED(qa)→green(dev) per contract; coverage-
excluded Obsidian catalog/caps → manual legs T-TC-033/034 (TEST-TC-M1/M2/M3).
getToolbarCapabilities 3-runtime stub lands with the interface add (T-TC-008).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(tc): T-TC-001 baseline-capture + guard verification

Scaffold the P6 parity-screenshot baseline matrix (seven widget groups ×
320/520/720 × light/dark) keyed to claudian-main InputToolbar.ts widget
classes + the 240deg ContextUsageMeter; the test-plan guard-verification note
+ the TEST-TC-M1/M2/M3 manual legs; the implementation-log skeleton.

Confirms (one whole-project lint run, 0 errors) the new TOOLBAR_CATALOG_PORT
key + the new toolbar domain/application/ui paths match no
DELETED_SUBSYSTEM_BAN / DELETED_INJECTION_KEYS glob — no guard-relax task.
No src/ change.

Traces: NFR-TC-008 (baseline leg), NFR-TC-001 (guard), SPEC-TC-012/020/026.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(tc): T-TC-002 RED Reasoning union + ToolbarCatalog/TabControls DTOs + query fields

Author the failing structural/type-level + serialisation tests:
- Reasoning.test.ts — ReasoningEffort is exactly 'high'|'medium'|'low';
  ReasoningChoice is exactly the two-member readonly discriminated union,
  narrowing on kind, surfaced through @/domain/ports.
- toolbar/ToolbarCatalog.test.ts — ModelOption/ModeDescriptor/
  ReasoningDescriptor/ServiceTierDescriptor/ToolbarCatalog shapes (readonly),
  re-exported from @/domain/chat/toolbar.
- toolbar/TabControls.test.ts — the exact four optional members.
- ChatTurn.ts.test.ts — extend the additivity legs: ChatRuntimeQueryOptions
  gains exactly mode?/reasoning?/serviceTier? after appendSystemPrompt; the
  P0-P5 members byte-identical; a P5-shaped query serialises byte-identically
  to P5 (TEST-TC-002/027).

RED confirmed: vue-tsc -p tsconfig.lint.json fails on the missing
@/domain/chat/Reasoning, @/domain/chat/toolbar, and the three query fields.

Traces: TEST-TC-002/006/010/013/017/018/019/027, SPEC-TC-001/002/003/006/027,
REQ-TC-002/017/042, NFR-TC-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tc): T-TC-003 Reasoning.ts + ChatRuntimeQueryOptions additive fields

Add src/domain/chat/Reasoning.ts (ReasoningEffort closed lower-case union +
the two-member readonly discriminated ReasoningChoice) and append the three
optional fields mode?/reasoning?/serviceTier? after appendSystemPrompt in
ChatRuntimeQueryOptions; re-export ReasoningChoice/ReasoningEffort from the
ports barrel. The P0-P5 members + PreparedChatTurn/EnsureReadyOptions/
ChatTurnRequest stay byte-identical; a P5-shaped query serialises identically
to P5.

Greens TEST-TC-018 (shape) + TEST-TC-002 (serialisation) + TEST-TC-027
(additivity). No obsidian/node/Vue import in src/domain/chat.

Traces: SPEC-TC-001/002/027, REQ-TC-004/014/017/018/020, NFR-TC-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tc): T-TC-004 ToolbarCatalog descriptor DTOs + TabControls bag + barrel

Add src/domain/chat/toolbar/: ToolbarCatalog.ts (ModelOption / ModeDescriptor
/ ReasoningDescriptor / ServiceTierDescriptor / ToolbarCatalog, all readonly),
TabControls.ts (the four optional members), index.ts barrel. Plain domain DTOs
— string/number/enum/readonly-array only; no obsidian/node/Vue/class, so they
cross the Pinia store boundary cleanly. No secret / no path outside the catalog.

Greens TEST-TC-010/013/017/019 + TEST-TC-006 type-shape legs.

Traces: SPEC-TC-003/006, REQ-TC-010/011/013/017/019/042, NFR-TC-005/011.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(tc): T-TC-005 RED ToolbarCatalogPort + TOOLBAR_CATALOG_PORT key + barrel

Author the failing structural/type-level test asserting ToolbarCatalogPort
exposes exactly getCatalog(providerId: ProviderId): ToolbarCatalog
(synchronous + total — type-level shape); TOOLBAR_CATALOG_PORT is its own
InjectionKey in @/infrastructure/bridge/ports; @/domain/ports re-exports the
port. RED confirmed: vue-tsc fails on the missing port + key + barrel re-export.

Traces: TEST-TC-003/010 (port-shape legs), SPEC-TC-004, REQ-TC-003/010,
NFR-TC-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tc): T-TC-006 ToolbarCatalogPort + TOOLBAR_CATALOG_PORT key + barrel

Add src/domain/ports/ToolbarCatalogPort.ts (getCatalog(providerId):
ToolbarCatalog — synchronous + total, never throws, unknown-provider/load-miss
→ safe default, never branched on by the consumer); append the
TOOLBAR_CATALOG_PORT InjectionKey to src/infrastructure/bridge/ports.ts (own
key, no aggregate); re-export the port + the ToolbarCatalog/TabControls/
descriptor DTOs from src/domain/ports/index.ts. One consumer, one port.

Greens TEST-TC-003/010 port-shape legs. Deleted-symbol guard green (new
key/port imports resolve clean — no relaxation).

Traces: SPEC-TC-004, REQ-TC-003/010, NFR-TC-002/010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(tc): T-TC-007 RED ToolbarCapabilities shape + getToolbarCapabilities additivity

Extend the ChatRuntimePort additivity test: ToolbarCapabilities is exactly the
five readonly flags (supportsMcpTools / reasoningControl / hasServiceTier /
hasModeToggle / permissionMode), surfaced through @/domain/ports;
ChatRuntimePort gains exactly getToolbarCapabilities(): ToolbarCapabilities
appended after getCapabilities (sixteen members); the P0-P5 members + the four
RuntimeCapabilities flags stay byte-identical.

RED confirmed: vue-tsc fails on the missing ToolbarCapabilities + the missing
getToolbarCapabilities member.

Traces: TEST-TC-003/019/021/027, SPEC-TC-005/027, REQ-TC-003/015/019/021,
NFR-TC-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tc): T-TC-008 ToolbarCapabilities + getToolbarCapabilities + 3-runtime stub

Add the ToolbarCapabilities interface (the five readonly flags) + append
getToolbarCapabilities(): ToolbarCapabilities to ChatRuntimePort; re-export
ToolbarCapabilities from the ports barrel. The P0-P5 members + the four
RuntimeCapabilities flags stay byte-identical.

Build-green companion (the P5 readBinary lesson, T-CA-006): every class that
implements ChatRuntimePort gains the member in the SAME commit —
MockChatRuntime (fixed Claude default, scriptable in T-TC-010),
FixtureChatRuntime (inert), ClaudeCliChatRuntime (Claude-shaped stub, real
flags in T-TC-012), the EnqueueRuntime decorator (forwards to inner), and the
two ScriptedRuntime test doubles (runnability stub, no assertion change).

Greens TEST-TC-003/019/021 shape legs + TEST-TC-027 ChatRuntimePort
additivity. Synchronous + total; no providerId branch.

Traces: SPEC-TC-005/027, REQ-TC-003/015/019/021, NFR-TC-001/002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(tc): T-TC-001..008 DOMAIN batch close-out — tick tasks + workflow-state

Stage-7 DOMAIN batch (T-TC-001..008) complete: tick the DoD boxes in tasks.md,
advance workflow-state to current_stage=implementation / last_agent=dev,
mark implementation-log.md + test-plan.md in-progress (INFRA/APP/UI/STYLES/
WIRE-IN/GATE batches remain), and append the dev hand-off note with the
per-task commit SHAs + the INFRA-batch next-owner pointer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(tc): T-TC-009 RED scriptable Mock catalog/caps + inert LS + fake-ports toolbarCatalog

Author the failing unit tests for the INFRA batch:
- MockToolbarCatalog scriptable (setToolbarCatalog → getCatalog; default
  Claude-shaped; empty-models degrade path; total/never-throws) + exposed on
  MockBridge via `get toolbarCatalog`.
- MockChatRuntime.getToolbarCapabilities scriptable (setToolbarCapabilities
  drives the seam matrix; default Claude-shaped; total).
- LocalStorageToolbarCatalog inert Claude-shaped catalog (no service-tier) +
  LocalStorageBridge.toolbarCatalog + FixtureChatRuntime inert caps.
- tests/__fakes__/fake-ports.ts gains a `toolbarCatalog` member assertion.

RED confirmed: MockToolbarCatalog/LocalStorageToolbar fail at import (modules
absent); MockToolbarCapabilities + fake-ports fail at runtime
(setToolbarCapabilities / toolbarCatalog absent beyond the T-TC-008 stub).

Traces: TEST-TC-003/010/011/013/017/019/021/030 (Mock/LS backing),
SPEC-TC-008, SPEC-TC-009, REQ-TC-003/013/019/021, NFR-TC-001/010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tc): T-TC-010 scriptable Mock ToolbarCatalogPort + caps + fake-ports member

Implement the Mock half of the INFRA batch (SPEC-TC-008):
- MockToolbarCatalog: scriptable ToolbarCatalogPort (setToolbarCatalog backs
  getCatalog; default = a small Claude-shaped catalog — 2 models + mode + effort
  reasoning, no service-tier; empty-models degrade available). Total — never
  throws, no providerId branch.
- MockBridge.toolbarCatalog: a `get` accessor mirroring `auxModel` exposing the
  scriptable port (the bridge IS the port; stable instance).
- MockChatRuntime.getToolbarCapabilities: now scriptable via
  setToolbarCapabilities (private state, default Claude-shaped), replacing the
  T-TC-008 fixed stub. Synchronous + total.
- tests/__fakes__/fake-ports.ts gains a `toolbarCatalog` member so multi-port
  tests see the scriptable catalog.

Greens the Mock RED legs of T-TC-009 (MockToolbarCatalog / MockToolbarCapabilities
/ fake-ports). No node:*/obsidian in Mock.

Verify: vitest 47/47 over the Mock-side files; vue-tsc 0 errors on the Mock
surface (the 2 remaining errors are the still-RED T-TC-011 LS legs); npm run
lint 0 errors (12 pre-existing warnings).

Traces: TEST-TC-003/010/011/013/017/019/021, SPEC-TC-008, REQ-TC-003/013/019/021,
NFR-TC-001/010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tc): T-TC-011 LocalStorage inert ToolbarCatalogPort + inert caps

Implement the LocalStorage (GitHub Pages demo) half of the INFRA batch
(SPEC-TC-009):
- LocalStorageToolbarCatalog: a fixed inert Claude-shaped catalog (model list +
  mode + effort descriptors, NO service-tier) so the demo renders the full strip
  with the backed model/mode widgets + the honest-defer seams. Synchronous +
  total — never throws across the boundary (NFR-TC-010); same for every
  providerId.
- LocalStorageBridge.toolbarCatalog: a `get` accessor exposing the inert port.
- FixtureChatRuntime.getToolbarCapabilities already reports the inert flags
  (supportsMcpTools:false, hasServiceTier:false, reasoningControl:'none',
  hasModeToggle:true, permissionMode:'default') from T-TC-008 — the T-TC-009 RED
  leg now confirms it.

Greens the LocalStorage RED legs of T-TC-009. No node:* in LocalStorage.

Verify: vitest 35/35 over the LS-side files; vue-tsc 0 errors (whole project);
npm run lint 0 errors (12 pre-existing warnings).

Traces: TEST-TC-019/021 (LS inert leg), SPEC-TC-009, REQ-TC-019/021,
NFR-TC-002/010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tc): T-TC-012 Obsidian real Claude ToolbarCatalogPort + real caps (coverage-excluded)

Implement the coverage-excluded Obsidian half of the INFRA batch (SPEC-TC-007),
under src/infrastructure/obsidian/** — behaviour gated by the MANUAL leg
TEST-TC-M1 (NOT agent-self-claimed green):
- ObsidianToolbarCatalog: the real static-for-now Claude catalog (model list +
  mode descriptor + effort ReasoningDescriptor; NO service-tier). Total — never
  throws, same for every providerId (P6 ships only 'claude'). Multi-provider +
  env-derived models are P9/P10 (NG4/NG5). Imports only domain types — no
  `obsidian`/`node:*` symbol leaks past the file.
- ObsidianBridge.toolbarCatalog: a lazily-created `get` accessor exposing the real
  port (mirrors selectionSource/shellExec).
- ClaudeCliChatRuntime.getToolbarCapabilities: fleshed from the T-TC-008 stub into
  the documented REAL flags — supportsMcpTools:false (honest CLI gating, MCP backing
  is P8/NG2, same posture as supportsInlineResponse:false), reasoningControl:'effort',
  hasServiceTier:false, hasModeToggle:true, permissionMode:'default' (mirrors the P4
  plan state; the --print one-shot transport reports supportsPlanMode:false, NG6,
  display only). Synchronous + total; no providerId branch.
- test-plan.md: scheduled/confirmed the manual leg TEST-TC-M1 (INFRA batch table).

Verify: vue-tsc 0 errors (whole project); npm run lint 0 errors (12 pre-existing
warnings); the four T-TC-009 batch test files 33/33 green. The Obsidian leg is
coverage-excluded — its behaviour is the human-run TEST-TC-M1 gate.

Traces: SPEC-TC-007, REQ-TC-010/015/019/021, NFR-TC-001 (manual leg), NFR-TC-010,
TEST-TC-M1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(tc): T-TC-009..012 INFRA batch close-out — log + tick tasks + workflow-state

Append the four INFRA-batch implementation-log entries (T-TC-009 RED, T-TC-010
Mock scriptable, T-TC-011 LS inert, T-TC-012 Obsidian real coverage-excluded with
the deviation notes), tick the T-TC-009..012 DoD checkboxes in tasks.md, and
advance workflow-state.md (implementation-log.md stays in-progress — APP/UI/
STYLES/WIRE-IN/GATE batches remain; Stage 7 row + hand-off note recorded).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(tc): T-TC-013 RED foldControlOptions pure guarded fold

Authors the failing unit tests for the pure guarded fold (SPEC-TC-010):
each present-field fold, the empty {} -> {} fold (EC-TC-1), the
empty-string / descriptor-default never-folded leg (EC-TC-6), additive
only, and never-throws. RED — foldControlOptions.ts does not yet exist.

Traces: TEST-TC-002 (fold leg), TEST-TC-004 (fold leg), SPEC-TC-010,
REQ-TC-004, NFR-TC-001, NFR-TC-005, EC-TC-1, EC-TC-6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tc): T-TC-014 foldControlOptions pure guarded fold

Implements SPEC-TC-010: foldControlOptions(controls) ->
Partial<Pick<ChatRuntimeQueryOptions,'model'|'mode'|'reasoning'|
'serviceTier'>>. Additive + guarded — writes only the present
(non-empty) controls, so an untouched toolbar yields {} (byte-identical
to a P5 turn, EC-TC-1/NFR-TC-001); a descriptor default is never folded
(EC-TC-6); seam widgets contribute nothing. Pure + total, never throws,
no providerId branch, no obsidian/node/Vue import. Greens the T-TC-013
RED tests.

Traces: SPEC-TC-010, REQ-TC-004, NFR-TC-001, NFR-TC-005, NFR-TC-007.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(tc): T-TC-015 RED buildToolbarViewModel per-widget decision

Authors the failing unit tests for the pure/total decision function
(SPEC-TC-011): the full per-widget matrix (model always visible/enabled
with options + selectedId fallback + emptyNotice; mode/thinking/
serviceTier capability+descriptor gating; permission/external always
visible-disabled; mcp hidden vs visible-empty; usage hidden when null,
warning strictly above USAGE_WARNING_THRESHOLD=80), the EC-TC-2/3/4/5/7
edge cases, the empty-catalog degrade (never throws), and a source grep
asserting zero providerId / "claude" branch (SPEC-TC-029). RED —
buildToolbarViewModel.ts does not yet exist.

Traces: TEST-TC-003/010/013/017/019/021/027/030 (VM legs), SPEC-TC-011,
SPEC-TC-018, SPEC-TC-029, REQ-TC-003/010/013/015/016/017/019/021/023/027,
NFR-TC-010, EC-TC-2/3/4/5/7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tc): T-TC-016 buildToolbarViewModel pure per-widget decision

Implements SPEC-TC-011/018/029: the WidgetVisibility union + the eight
per-widget VM interfaces + ToolbarViewModel + USAGE_WARNING_THRESHOLD=80;
buildToolbarViewModel(catalog, capabilities, controls, usage) applies the
per-widget visible/enabled/hidden rules reading only capabilities +
catalog + controls + usage (no per-provider branch, SPEC-TC-029). Seam
widgets are decided from capabilities + catalog descriptors alone:
permission/external always visible-disabled; mcp hidden vs visible-empty
on supportsMcpTools; mode/thinking/serviceTier hidden when their
capability flag is off or the descriptor is absent. Usage hidden when
null, warning strictly above 80. Pure + total — never throws; partial /
empty catalog degrades the dependent widget. Greens the T-TC-015 RED
tests (the test path-resolution is fixed to make the source-grep leg
runnable — assertions unchanged).

Traces: SPEC-TC-011, SPEC-TC-018, SPEC-TC-029, REQ-TC-003/010/013/015/
016/017/019/021/023/027, NFR-TC-010, NFR-TC-007.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(tc): T-TC-013..016 APPLICATION batch close-out — log + tick tasks + workflow-state

Appends the T-TC-013..016 implementation-log entries (per-task files,
commit SHAs, spec refs, outcomes, deviations), ticks the four task DoD
checkboxes in tasks.md, and updates workflow-state.md (implementation-log
artifact + Stage 7 progress row + the dated APPLICATION-batch hand-off
note). implementation-log.md stays in-progress — the UI/STYLES/WIRE-IN/
GATE batches remain.

Traces: SPEC-TC-010, SPEC-TC-011, TASKS-TC-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(tc): T-TC-017 RED useToolbarCatalogPort inject-or-throw

TEST-TC-003 composable leg. SPEC-TC-024 §4. REQ-TC-003/010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tc): T-TC-018 useToolbarCatalogPort composable

Implements SPEC-TC-024. REQ-TC-003/010, NFR-TC-002/003.
Mirrors useVaultPort/useAuxModelPort inject-or-throw (ADR-008).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(tc): T-TC-019 RED ModelSelector + ModeSelector widgets

TEST-TC-010/011/013/014/040/041 A legs. SPEC-TC-013/014. REQ-TC-010/011/013/014/040/041.
Co-located data-testid PageObjects (ADR-009).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tc): T-TC-020 ModelSelector + ModeSelector widgets + toolbar i18n

Implements SPEC-TC-013/014/028. REQ-TC-010/011/013/014/040/041, NFR-TC-003/004/009/014.
ModelSelector: grouped keyboard listbox (combobox button, role=listbox, group
separators, aria-selected/activedescendant, Arrow/Home/End/Enter/Esc, empty notice).
ModeSelector: descriptor-driven role=switch toggle. agent.chat.toolbar.* keys en+de.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(tc): T-TC-021 RED ThinkingSelector + ServiceTierToggle widgets

TEST-TC-017/018/019/020/040/041 A legs. SPEC-TC-016/017.
REQ-TC-017/018/019/020/040/041. Co-located data-testid PageObjects (ADR-009).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tc): T-TC-022 ThinkingSelector + ServiceTierToggle widgets

Implements SPEC-TC-016/017/028. REQ-TC-017/018/019/020/040/041, NFR-TC-003/004/009/014.
ThinkingSelector: effort/budget keyboard listbox (same a11y as model selector;
effort label + localised level / budget label + token amount). ServiceTierToggle:
capability-gated zap role=switch; toggling emits toggle(!active) (declared-now/emitted-P9).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(tc): T-TC-023 RED PermissionToggle + McpSelector + ExternalContextControl seams

TEST-TC-015/016/021/022/023 A legs. SPEC-TC-015/018/019/029.
REQ-TC-015/016/021/022/023/041. The three honest-defer seams — counter-metric:
zero live-looking-but-dead controls. Co-located data-testid PageObjects (ADR-009).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tc): T-TC-024 PermissionToggle + McpSelector + ExternalContextControl seams

Implements SPEC-TC-015/018/019/028/029/030. REQ-TC-015/016/021/022/023/041,
NFR-TC-003/004/011/014. PermissionToggle: PLAN label vs disabled switch; deferred
activation → NotificationPort notice, no rule. McpSelector: visible-empty coming-later
panel, connects nothing. ExternalContextControl: disabled folder, no picker/path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(tc): T-TC-025 RED UsageMeter + ToolbarStrip

TEST-TC-001/024/025/026/027 A legs. SPEC-TC-012/020. REQ-TC-001/003/024/025/026/027.
Declarative SVG arc (no v-html); Claudian-order strip + hidden-slot-collapse.
Co-located data-testid PageObjects (ADR-009).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tc): T-TC-026 UsageMeter + ToolbarStrip

Implements SPEC-TC-012/020/027/028. REQ-TC-001/003/024/025/026/027,
NFR-TC-003/004/008/009/012/014. UsageMeter: declarative 240-degree SVG arc gauge
(stroke-dasharray computed in-repo, no chart lib), warning style + /compact tooltip
above 80%, role=img aria-label, hidden when usage null. ToolbarStrip: the only
view-model reader; lays widgets in Claudian order, collapses hidden slots, re-emits
the four backed changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(tc): T-TC-027 RED tabsStore controls/fold + ChatComposer region + ChatSurface wiring

TEST-TC-001/002/003/004/006/012/042/043 store/composer/surface legs.
SPEC-TC-021/022/023. REQ-TC-001/002/003/004/012/014/018/020/042. Additive PageObject
extensions; data-testid only (ADR-009).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tc): T-TC-028 tabsStore controls/fold + ChatComposer region + ChatSurface wiring

Implements SPEC-TC-021/022/023/029. REQ-TC-001/002/003/004/012/014/018/020/042,
NFR-TC-001/003/004/005. tabsStore: additive per-tab TabControls + setControl draft
mutation; _turnQueryOptions folds foldControlOptions(controls) ALONGSIDE the P5
appendSystemPrompt fold (both coexist); loadIntoTab/freshTab reset controls.
ChatComposer: optional toolbar region between textarea + footer (byte-identical to P5
when absent). ChatSurface: optional inject(TOOLBAR_CATALOG_PORT) → buildToolbarViewModel
reactively, routes the four backed changes to setControl; no providerId branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(tc): T-TC-017..028 UI batch close-out — log + tick tasks + workflow-state

Append the per-task implementation-log entries (T-TC-017..028, RED+green SHAs,
outcomes, deviations) + the UI-batch verification summary; tick the T-TC-017..028
DoD checkboxes; update workflow-state Stage 7 + artifact status + hand-off note.
implementation-log.md stays in-progress (STYLES/WIRE-IN/GATE + manual legs remain).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tc): T-TC-029 mint toolbar/* --sp-* token slice + tokens contract

Implements SPEC-TC-026 (§4.13 toolbar/controls token slice). Adds the
twelve spec-named P6 tokens (--sp-toolbar-*, --sp-toggle-*, --sp-usage-arc-*,
--sp-service-tier-glow) on .specorator-root, each a token-layer var lookup
or bare dimension/shadow (no hex, no raw Obsidian var, no physical property).
The nine toolbar widgets now resolve every referenced token. Updates the
tokens-contract test (TEST-TC-026) with the §4.13 presence list + the
no-raw-hex / no-Obsidian-var leak guard, isolating the §4.12 block.

Satisfies SPEC-TC-026, TEST-TC-026, NFR-TC-008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(tc): T-TC-030 RED wire-in — provide TOOLBAR_CATALOG_PORT + strip mounts

Authors the failing wire-in mount test (SPEC-TC-025): both entry points
(src/ui/main.ts standalone + AgentSidebarView.onOpen) must provide
TOOLBAR_CATALOG_PORT so the ChatComposer/ChatSurface toolbar region mounts
the backed widgets + honest seams. RED today — neither entry point provides
the port, so the optional inject resolves undefined (no toolbar-strip) and
the bridge's toolbarCatalog getter is never read. Also ticks T-TC-029 in
tasks.md + records its real SHA in the implementation log.

Satisfies TEST-TC-001/003 (mount legs), TEST-TC-M1 (wiring leg), SPEC-TC-025,
REQ-TC-003/010/021, NFR-TC-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tc): T-TC-031 provide TOOLBAR_CATALOG_PORT in both entry points

Implements SPEC-TC-025. AgentSidebarView.onOpen and src/ui/main.ts each
app.provide(TOOLBAR_CATALOG_PORT, bridge.toolbarCatalog) alongside the
existing chat/composer ports — the ObsidianBridge real Claude static catalog
in the sidebar, the MockBridge scriptable Claude-shaped catalog in the
standalone demo. The per-tab Claude runtime already reports
getToolbarCapabilities() via tabs.activeRuntime(), so the ChatComposer/
ChatSurface toolbar region now mounts the backed widgets + honest seams.
Greens T-TC-030. The provide is additive — the P1-P5 surfaces stay
byte-identical (absent port → no strip, the optional inject). No obsidian
symbol enters src/ui/**; no router reintroduced.

Satisfies SPEC-TC-025, REQ-TC-003/010/021, NFR-TC-002/003.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(tc): T-TC-032 standalone toolbar smoke (dev leg) + deferred manual leg

Adds the deterministic standalone toolbar smoke leg to tests/ui/main.ts.test.ts
(mirroring the P3/P4/P5 dev-leg blocks): against MockBridge via src/ui/main.ts
the strip mounts in Claudian order with the backed widgets (model/mode/thinking)
+ the honest seams (permission/external visible-disabled, MCP/service-tier
capability-hidden), the usage meter hidden on a fresh tab (EC-TC-7), and a tab
switch re-derives every widget (EC-TC-8). The live npm-run-dev interactive feel
is recorded as a DEFERRED human-run leg in test-plan.md (the agent does not
start the long-running dev server, project rule).

Satisfies TEST-TC-001/004/042 (dev leg), SPEC-TC-025, REQ-TC-042, NFR-TC-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(tc): T-TC-029..032 close-out — real SHAs, task ticks, Stage-7 state

Records the real commit SHAs in the implementation log for T-TC-030/031/032,
ticks the T-TC-030/031/032 DoD checkboxes in tasks.md, and updates
workflow-state.md Stage-7 close-out: STYLES (T-TC-029) + WIRE-IN
(T-TC-030..032) done; implementation-log.md stays in-progress because the
human-owned GATE legs (T-TC-033/034) + the parent final-DoD (T-TC-035) +
the deferred T-TC-032 live-dev-server leg remain. Hand-off note appended.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(tc): T-TC-035 gate — lightningcss-safe tokens comment + regenerate styles.css

The §4.13 toolbar token-slice comment used backticks / slashes / braces
(`toolbar/{...}-selector.css`) that the standalone build:web lightningcss
minifier rejects ("Unexpected token Delim('/')") though the plugin esbuild
tolerated them. Rewrote the comment slash/backtick/brace-free (keeps the §4.13
slice marker the tokens-contract test asserts). Regenerate the shipped styles.css
with the P6 toolbar/widget/usage-meter styles.

Full gate green: typecheck 0, eslint 0, vitest 1434/209, build + build:web +
docs:api clean, npm audit (high) clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(tc): T-TC-035 Stage-9 review (approve-with-nits) + traceability

REVIEW-TC-001 verdict APPROVE-WITH-NITS (0 P1, 0 P2, 3 P3, 3 P4). Live
fold-on-submit path verified connected (foldControlOptions invoked on
tabsStore.sendMessage → queryOptions; coexists with the P5 context fold on
request) + production provide verified (AgentSidebarView + main.ts). TRACE-TC-001
REQ↔SPEC↔TEST↔code matrix; manual legs TEST-TC-M1/M2/M3 pending. Nits
(R-TC-001..006) recorded with owners for the final epic gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Luis Mendez <hallo@luis-mendez.de>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s + rule persistence (#448)

* chore(as): bootstrap P7 approvals-security feature + workflow-state

Cut feature/approvals-security off next (P0-P6 merged). Scope = parity-charter
§3.9 — ApprovalManager + permission updates + approval rules + persistence; backs
the P6 permission-toggle seam; consumes the P4 inline approval blocks. Key ADR:
ApprovalRuleStorePort persistence (device-local, CHARTER-REQ-SET). Autonomous
full-epic drive (P7→P12). Next: /spec:requirements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(as): P7 requirements PRD-AS-001 (approvals & security, accepted)

35 EARS REQ-AS (permission mode / rules+matching / decision flow / persistence /
status UI / a11y+additivity) + 16 NFR-AS, each mapped to a claudian path + TEST-AS
id. Permission modes = normal/plan/yolo (claudian PermissionMode, all Claude-backed).
Rule model {toolName, action-pattern?, decision allow|deny, lifetime} with claudian
matchesRulePattern semantics + explicit deny. Persistence (CLAR-AS-001→ADR-AS-001):
dedicated ApprovalRuleStorePort, device-local (ADR-PSR-002), no migration. Additive
default = P4 always-prompt path (REQ-AS-052). CLAR-AS-001..005 resolved-by-recommendation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(as): P7 design DESIGN-AS-001 + ADR-AS-001..003 (accepted)

ADR-AS-001 ApprovalRuleStorePort (store-only narrow port) + ApprovalRule DTO +
pure domain matcher (claudian matchesRulePattern semantics) + device-local backing
(saveLocalStorage, no data.json/vault, no migration), fail-safe-to-prompt.
ADR-AS-002 additive ChatRuntimeQueryOptions.permissionMode? + TabControls.permissionMode?
(folded non-normal only by P6 foldControlOptions); ToolbarCapabilities.permissionMode
widens to live normal|plan|yolo; SDK mapping in the Claude runtime, no providerId branch.
ADR-AS-003 application ApprovalManager decision flow (mode-gate → match → unchanged
P4 prompt → persist; deny-wins; +deny-always). No-rule+normal = byte-identical P4.
Components: PermissionToggle(live)/ApprovalsPanel/ApprovalRuleRow/InlineApproval.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(as): P7 spec SPEC-AS-001..028 (approvals & security)

6 layer groups, 28 SPEC items. Pins PermissionMode (normal|plan|yolo), additive
ChatRuntimeQueryOptions.permissionMode?/TabControls.permissionMode? (fold non-normal),
ApprovalDecision +deny-always (P4 byte-identical), ToolbarCapabilities.permissionMode
widen, ApprovalRule DTO + ApprovalRuleStorePort + APPROVAL_RULE_STORE_PORT (device-local),
the pure matcher (claudian matchesRulePattern: bash explicit-wildcard-only, path-segment
boundaries, deny-wins), ApprovalManager.decide (mode-gate→match→prompt→persist,
fail-safe-to-prompt). Manual legs TEST-AS-M1/M2/M3 + plan-gate. Full coverage table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(as): P7 tasks TASKS-AS-001 (40 tasks, TDD-ordered, 7 batches)

T-AS-001 baseline+guard-verify (no guard-relax needed — verified); DOMAIN 002..011,
INFRA 012..015, APPLICATION 016..019, UI 020..029, STYLES 030, WIRE-IN 031..033,
GATE 034..040. RED(qa)→green(dev) per contract; ToolbarCapabilities.permissionMode
widen lands its implements fan-out (3 runtimes + EnqueueRuntime + ScriptedRuntime
doubles) in T-AS-011 (build-green discipline). Coverage-excluded Obsidian device-local
store + Claude SDK-map/setMode → manual legs T-AS-036/037/038 (TEST-AS-M1/M2/M3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(as): T-AS-001 baseline-capture + guard verification

Scaffold the P7 parity-screenshot matrix (baseline column from claudian-main
ApprovalManager/ClaudeApprovalHandler/ClaudePermissionUpdates +
permission-toggle.css/status-panel.css), the test-plan (guard-verify note +
manual legs TEST-AS-M1/M2/M3 + DOMAIN-batch status), and the implementation-log.
Confirms APPROVAL_RULE_STORE_PORT + the new approvals domain/app/ui paths are not
guard-banned (no relaxation task). No src/ change.

NFR-AS-012 NFR-AS-001 SPEC-AS-004/012/013/015/020/026.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(as): T-AS-002 RED PermissionMode + additive optionals + grown ApprovalDecision

Failing structural/serialisation legs: PermissionMode = 'normal'|'plan'|'yolo'
(closed union + barrel surface); ChatRuntimeQueryOptions.permissionMode? appended
after serviceTier with a P6-shaped query byte-identical; TabControls.permissionMode?
appended; ApprovalDecision grown to the four-member union (deny-always) with the
P4 members + ApprovalRequest/ApprovalOption byte-identical. vue-tsc fails RED.

TEST-AS-001 TEST-AS-002 TEST-AS-016 SPEC-AS-001/002/003/021
REQ-AS-001/002/006/016/052 NFR-AS-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(as): T-AS-003 PermissionMode + additive optionals + grown ApprovalDecision

Add src/domain/chat/PermissionMode.ts ('normal'|'plan'|'yolo' closed union);
append permissionMode?: PermissionMode after serviceTier on ChatRuntimeQueryOptions
+ TabControls; grow the ApprovalDecision union by 'deny-always'; re-export
PermissionMode from the ports barrel. Purely additive — no implements break (the
runtimes read the optional field; the union grows additively). The P4
inlineBlockDtos union-exactness assertion is updated to the grown four-member
union (the union-grow fan-out). Greens TEST-AS-001/002/016; whole-project vue-tsc 0.

SPEC-AS-001/002/003/021 REQ-AS-001/002/006/016/052 NFR-AS-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(as): T-AS-004 RED pure matcher truth table

Failing unit tests for getActionPattern/getActionDescription/matchesRulePattern
(@/domain/chat/approvals/ApprovalMatcher): the full SPEC-AS-026 table — per-tool
pattern/description derivation, no-rule/'*'/exact, the null-action guard (EC-AS-9),
bash explicit-wildcard only ("git *"↦"git status" yes, "git"↦"git status" no,
"npm:*"↦"npm install" yes, "git *"↦"github" no — EC-AS-7), file path-segment
boundary ("/a/b"↦"/a/b/c" yes, ↦"/a/bc" no, trailing-/ subtree, \->/ normalise —
EC-AS-8), other-tool simple prefix, and never-throws. Module missing → RED.

TEST-AS-010/011/012/013/014/015 SPEC-AS-004/026 REQ-AS-010..015 NFR-AS-009
EC-AS-7/8/9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(as): T-AS-005 ApprovalMatcher pure getActionPattern/Description/matchesRulePattern

Add src/domain/chat/approvals/ApprovalMatcher.ts ported verbatim from claudian
core/security/ApprovalManager.ts: the seven tool-name constants;
getActionPattern (string|null), getActionDescription (string), matchesRulePattern
(boolean) with the private isPathPrefixMatch + matchesBashPrefix helpers — \->/
normalise, no-rule/'*' match-all, exact, bash explicit-wildcard-only, file
path-segment boundary, other-tool simple prefix, the null-action guard. Pure +
total, never throws (NFR-AS-009); string comparison only, no eval/exec
(NFR-AS-002). Barrel re-export added. Greens TEST-AS-010/011/012/013/014/015.
Two targeted complexity disables (irreducible per-tool dispatch, justified).

SPEC-AS-004/026 REQ-AS-010..015 NFR-AS-002/009.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(as): T-AS-006 RED ApprovalRule DTO + ApprovalRuleInput + ruleDedupeKey

Failing structural + dedupe-key tests: the six readonly members
(id/toolName/actionPattern?/decision:'allow'|'deny'/lifetime:'session'|'persisted'/
createdAt); ApprovalRuleInput = Omit<ApprovalRule,'id'|'createdAt'>; ruleDedupeKey
returns the `${toolName} ${actionPattern ?? ''} ${decision}` triple (absent vs ''
collapse, opposite decision distinct); no secret/token field; barrel re-export.
Module missing → RED.

TEST-AS-016 SPEC-AS-005/024 REQ-AS-016/030/031 NFR-AS-002/008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(as): T-AS-007 ApprovalRule DTO + ApprovalRuleInput + ruleDedupeKey

Add src/domain/chat/approvals/ApprovalRule.ts: the six readonly members
(id/toolName/actionPattern?/decision:'allow'|'deny'/lifetime:'session'|'persisted'/
createdAt), ApprovalRuleInput = Omit<ApprovalRule,'id'|'createdAt'>, and
ruleDedupeKey returning the `${toolName} ${actionPattern ?? ''} ${decision}`
triple (pure, string-only). Plain inert DTO — no secret/token field, no class, no
obsidian/node/Vue (NFR-AS-002/008). Barrel re-export added. Greens TEST-AS-016
DTO leg (6/6).

SPEC-AS-005/024 REQ-AS-016/030/031 NFR-AS-002/008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(as): T-AS-008 RED ApprovalRuleStorePort + APPROVAL_RULE_STORE_PORT key + barrel

Failing structural tests: the four Result-typed methods (loadRules: Promise<Result
<readonly ApprovalRule[]>>, addRule: (input)=>Promise<Result<ApprovalRule>>,
removeRule: (id)=>Promise<Result<void>>, clear: ()=>Promise<Result<void>>); the own
APPROVAL_RULE_STORE_PORT InjectionKey; the @/domain/ports barrel re-exports of the
port + ApprovalRule/ApprovalRuleInput/PermissionMode. vue-tsc fails RED.

TEST-AS-053 SPEC-AS-006 REQ-AS-001/032/033/034/053 NFR-AS-005.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(as): T-AS-009 ApprovalRuleStorePort + APPROVAL_RULE_STORE_PORT key + barrel

Add src/domain/ports/ApprovalRuleStorePort.ts (loadRules/addRule/removeRule/clear,
all Promise<Result<...>>, store-only persisted lifetime, documented per-method
contract: load-or-default, dedupe-by-ruleDedupeKey, idempotent remove,
fail-safe-via-err); add the APPROVAL_RULE_STORE_PORT InjectionKey to bridge/ports
(own key, no aggregate); re-export the port + ApprovalRule/ApprovalRuleInput from
the @/domain/ports barrel (PermissionMode already re-exported in T-AS-003). Greens
TEST-AS-053 port-shape leg (2/2); deleted-symbol guard green (new key/port resolve
clean, no relaxation).

SPEC-AS-006 REQ-AS-001/032/033/034/053 NFR-AS-005/010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(as): T-AS-010 RED ToolbarCapabilities.permissionMode widen + additivity

Extend tests/domain/ports/ChatRuntimePort.ts.test.ts: assert
ToolbarCapabilities.permissionMode is WIDENED from 'default'|'plan' to the live
PermissionMode ('normal'|'plan'|'yolo'), the P6 'default' value mapping to
'normal'; the four other ToolbarCapabilities flags + the five RuntimeCapabilities
flags + the P0-P6 ChatRuntimePort members stay byte-identical; all three live modes
representable. vue-tsc fails RED (permissionMode still narrow 'default'|'plan').

TEST-AS-001 TEST-AS-021 SPEC-AS-006/021 REQ-AS-003 NFR-AS-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(as): T-AS-011 widen ToolbarCapabilities.permissionMode + implements fan-out

Widen ToolbarCapabilities.permissionMode from 'default'|'plan' to PermissionMode
('normal'|'plan'|'yolo') in ChatRuntimePort (importing from PermissionMode; the four
other ToolbarCapabilities flags + the five RuntimeCapabilities flags + the P0-P6
members byte-identical). In the SAME commit, map the P6 'default' -> 'normal' on
every getToolbarCapabilities() impl that implements ChatRuntimePort — the three
runtimes (MockChatRuntime, FixtureChatRuntime, ClaudeCliChatRuntime) + the two
ScriptedRuntime test doubles (RunChatTurnUseCase.test/.rr.test) + the P6 capability
fixtures (buildToolbarViewModel, MockToolbarCapabilities, LocalStorageToolbar,
main.ts) — so vue-tsc + lint + the suite stay green (the P6 T-TC-008 lesson).
EnqueueRuntime forwards getToolbarCapabilities() verbatim — no change. No
providerId branch; synchronous + total. Greens TEST-AS-001 capabilities-shape +
TEST-AS-021 additivity (68/68 across the affected files).

SPEC-AS-006/021 REQ-AS-003 NFR-AS-001/005.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(as): DOMAIN batch T-AS-001..011 close-out (impl-log + workflow-state)

Log T-AS-002..011 entries (RED/green commits, files, verify, deviations) + the
DOMAIN-batch close-out (vue-tsc 0, lint 0, vitest tests/domain 116/116, additivity
proven). Advance workflow-state to stage 7 implementation in-progress; record the
DOMAIN-batch hand-off to the INFRA batch (T-AS-012..015). implementation-log.md +
test-plan.md set in-progress (INFRA/APP/UI/GATE batches remain).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(as): correct corrupted §4.13 styles.css comment block from the P6 gate

The P6 gate committed a stale styles.css built BEFORE the tokens.css lightningcss
comment fix — the old slash/brace-laden §4.13 comment had confused the plugin
build's CSS processor, mangling `.specorator-root {` into invalid
`.specorator-root) {` with selector text bled into the comment. This commits the
clean rebuild (from the fixed tokens.css; the P6 testvault deploy already shipped
this clean version). Corrects the artifact on next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(as): T-AS-012 Obsidian device-local ApprovalRuleStorePort + Claude SDK mode mapping + plan-exit setMode

Implements SPEC-AS-007 (coverage-excluded src/infrastructure/obsidian/**):
- ObsidianApprovalRuleStore backs ApprovalRuleStorePort on the device-local
  store under 'specorator:approval-rules' (app.saveLocalStorage/loadLocalStorage,
  ADR-PSR-002 pattern) — never data.json, never a vault file (NFR-AS-003,
  REQ-AS-034). loadRules is load-or-default (missing/unparseable -> ok([]),
  malformed entries dropped); addRule dedupes by ruleDedupeKey + mints
  id/createdAt; removeRule idempotent; clear; all Result-typed, total (never
  throws -> tryAsync). Exposed via get approvalRuleStore on ObsidianBridge.
- ClaudeCliChatRuntime maps queryOptions.permissionMode to the SDK
  --permission-mode string (yolo->bypassPermissions / plan->plan /
  normal/absent->no flag); records the live mode so
  getToolbarCapabilities().permissionMode reflects it; on plan-exit syncs the
  session-scoped mode (parity ClaudeApprovalHandler setMode destination:session).
  No providerId branch (SPEC-AS-023, NG6).

Behavioural gate is the MANUAL legs TEST-AS-M1/M3 (scheduled in test-plan.md);
not self-claimed. REQ-AS-002/004/005/030/034/053, NFR-AS-003.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(as): T-AS-013 RED scriptable Mock ApprovalRuleStorePort + runtime mode + fake-ports member

Authors the failing unit tests for SPEC-AS-008:
- MockApprovalRuleStore.test.ts — the scriptable in-memory store (seedRules,
  loadRules-default-ok([]), addRule mint + dedupe-by-ruleDedupeKey + opposite-
  decision append, idempotent removeRule, clear, setFailMode('load'|'save'|'none')
  forcing Result.err, never-throws) + the MockBridge.approvalRuleStore accessor.
- MockApprovalRuntimeMode.test.ts — MockChatRuntime records the last query's
  permissionMode (getLastPermissionMode) + the scriptable
  getToolbarCapabilities().permissionMode three-mode representability.
- fake-ports.test.ts — the approvalRuleStore factory member (seedable + setFailMode).

RED confirmed: MockApprovalRuleStore module + MockBridge.approvalRuleStore +
getLastPermissionMode + the fake-ports member do not yet exist.

Traces: TEST-AS-002/003/006/020/021/030/032/033/040/053/054, SPEC-AS-008,
REQ-AS-020/021/032/053/054, NFR-AS-010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(as): T-AS-014 scriptable MockBridge ApprovalRuleStorePort + runtime mode + fake-ports member

Greens the T-AS-013 RED tests (SPEC-AS-008):
- MockApprovalRuleStore — scriptable in-memory store: seedRules pre-populates;
  loadRules defaults ok([]); addRule mints id/createdAt + dedupes by
  ruleDedupeKey (same-triple no-op ok(existing), opposite-decision appended);
  removeRule idempotent; clear; setFailMode('load'|'save'|'none') forces
  Result.err for the fail-safe-to-prompt driver (TEST-AS-054); total, never
  throws (NFR-AS-010). Exposed via get approvalRuleStore on MockBridge.
- MockChatRuntime records the last query's permissionMode (getLastPermissionMode,
  TEST-AS-002); the scriptable getToolbarCapabilities().permissionMode covers the
  three-mode representability (TEST-AS-003/006/040).
- fake-ports.ts gains the approvalRuleStore member (seedable + setFailMode) so
  multi-port ApprovalManager + panel tests see it.

Runnability fix to the T-AS-013 RED fixture (no assertion change): ChatTurnRequest
has no conversationId; the drain loop no longer binds an unused chunk.

No node:*/obsidian in Mock. vitest run 32/32 green.

Traces: TEST-AS-002/003/006/020/021/030/032/033/053/054, SPEC-AS-008,
REQ-AS-020/021/032/053/054, NFR-AS-010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(as): T-AS-015 LocalStorage browser-localStorage ApprovalRuleStorePort + inert runtime mode

Implements SPEC-AS-009 (RED leg authored first, then greened):
- LocalStorageApprovalRuleStore backs ApprovalRuleStorePort on browser
  localStorage under the same key as the Obsidian device-local store
  ('specorator:approval-rules') so the GitHub Pages demo persists rules across a
  reload with no Obsidian runtime (REQ-AS-053). loadRules is load-or-default
  (missing/unparseable/corrupt -> ok([]), malformed entries dropped); addRule
  dedupes by ruleDedupeKey + mints id/createdAt; removeRule idempotent; clear;
  all Result-typed, never throws across the boundary (NFR-AS-010). Exposed via
  get approvalRuleStore on LocalStorageBridge.
- The runtime mode is inert: FixtureChatRuntime reports permissionMode 'normal'
  (T-AS-011) and fires no live setMode (no live SDK); the toggle/panel still
  reflect the per-tab mode draft via the fold.

No node:*. vitest run 8/8 (store) green; full infra+fakes surface 346/346 green.

Traces: TEST-AS-053 (LocalStorage round-trip leg), SPEC-AS-009, REQ-AS-053,
NFR-AS-010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(as): INFRA batch T-AS-012..015 close-out (impl-log + tasks + workflow-state)

Records the INFRA batch (T-AS-012..015) in implementation-log.md (per-task
entries + batch close-out), ticks the T-AS-012..015 DoD boxes in tasks.md, and
updates workflow-state.md (Stage 7 row, implementation-log.md artifact status,
INFRA-batch hand-off note -> APPLICATION batch T-AS-016..019). Manual legs
TEST-AS-M1/M3 remain scheduled for the final epic-review gate (not self-claimed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(as): T-AS-016 RED foldControlOptions guarded permissionMode clause

Adds the failing fold-leg cases: non-normal ('plan'/'yolo') folded, the
'normal'/absent guard folds nothing (EC-AS-2/13 byte-identical P6), the
P6-clause byte-identity, and never-throws.
TEST-AS-002 (fold leg). SPEC-AS-011 §3. REQ-AS-002/052. NFR-AS-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(as): T-AS-017 add guarded permissionMode clause to foldControlOptions

Writes folded.permissionMode only when present AND non-'normal', so a
no-rule/normal tab folds nothing -> byte-identical P6 (EC-AS-2/13). The
return type widens by the one optional permissionMode key; the P6
model/mode/reasoning/serviceTier clauses + behaviour stay byte-identical.
Pure + total; no providerId branch.
Implements SPEC-AS-011 §3. REQ-AS-002/052. NFR-AS-001. TEST-AS-002 (fold leg).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(as): T-AS-018 RED ApprovalManager decision-flow matrix

Adds the failing use-case matrix over the scriptable Mock store + a
scripted mode: mode-gate-first (yolo auto-allow no-lookup / plan defer /
normal continue), load-await + match deny-wins, fail-safe-to-prompt on a
store err (notice, never auto-allow, no rule content logged, never throws),
applyDecision (session vs persisted, {-leading JSON-fallback stored without
actionPattern, dedupe, null cancel), listRules persisted-union-session, the
bash/path matcher edges (EC-AS-7/8), and the no-stale-snapshot re-read.
TEST-AS-003/004/020/021/023/025/030/031/032/033/052/054. SPEC-AS-010/023/027/028.
REQ-AS-004/005/020..025/030/031/052/054. NFR-AS-004/009. EC-AS-1/3/5/6/10/11/12/16/20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(as): T-AS-019 ApprovalManager decision-flow use case

decide(action, mode): mode-gate-FIRST (yolo->ok('allow') no lookup /
plan->ok('prompt') defer to the P4 exit-plan gate / normal->continue) ->
await store.loadRules via tryAsync (err -> log no-content + storeError
notice + ok('prompt'), never auto-allow) -> match persisted-union-session
via the pure matcher (deny-wins -> ok('deny'), else allow -> ok('allow'),
else ok('prompt')). applyDecision: allow/deny -> in-memory session rule
(dedupe by ruleDedupeKey); allow-always/deny-always -> store.addRule
persisted (the {-leading JSON-fallback stored WITHOUT actionPattern); null
-> cancel. listRules -> persisted-union-session, Result-typed. No providerId
branch; never throws across the callback boundary; logs no rule content.

Also folds the lint fix to the T-AS-018 RED test (unnecessary optional
chains on non-nullish index access) so the file lints clean — no assertion
change.

Implements SPEC-AS-010/023/025/027/028. REQ-AS-004/005/020..025/030/031/052/054.
NFR-AS-002/004/009. ADR-AS-003.
TEST-AS-003/004/020/021/023/025/030/031/032/033/052/054. EC-AS-1/3/5/6/10/11/12/16/20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(as): T-AS-020 RED useApprovalRuleStorePort inject-or-throw

TEST-AS-053 (composable leg). SPEC-AS-018 §4. REQ-AS-040/042/053, NFR-AS-005.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(as): T-AS-021 useApprovalRuleStorePort composable

Implements SPEC-AS-018 §4 (inject-or-throw, ADR-008 one-port-one-composable).
REQ-AS-040/042/053, NFR-AS-005/006. Greens TEST-AS-053 composable leg.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(as): T-AS-022 RED PermissionToggle live three-mode

TEST-AS-001/002/003/006/050/051 (A legs). SPEC-AS-012 §4.
REQ-AS-001/002/003/006/050/051, NFR-AS-006/013/015.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(as): T-AS-023 PermissionToggle live three-mode + PLAN label

Implements SPEC-AS-012 §4 (live normal/plan/yolo control, additive over the P6
honest-defer seam) + SPEC-AS-022 i18n (en+de). Greens TEST-AS-001/002/003/006/050/051
A legs. REQ-AS-001/002/003/006/050/051, NFR-AS-006/007/013/015.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(as): T-AS-024 RED ApprovalsPanel + ApprovalRuleRow

TEST-AS-040/041/042/043/050/051 (A legs). SPEC-AS-013/014 §4.
REQ-AS-040/041/042/043/050/051, NFR-AS-006/013/015.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(as): T-AS-025 ApprovalsPanel + ApprovalRuleRow

Implements SPEC-AS-013/014 §4 (status/approvals surface + one-rule row, live,
remove-by-id). Greens TEST-AS-040/041/042/043/050/051 A legs. SPEC-AS-022 i18n.
REQ-AS-040/041/042/043/050/051, NFR-AS-006/007/013/015.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(as): T-AS-026 RED InlinePlanApproval +deny-always option

TEST-AS-016 (option-row leg), TEST-AS-022 (four-option-row leg), TEST-AS-025 (cancel
leg). SPEC-AS-015/018 §4. REQ-AS-022/025/030, NFR-AS-006/007/015.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(as): T-AS-027 InlinePlanApproval +deny-always option (additive)

Implements SPEC-AS-015/018 §4 (the fourth deny-always option arrives via
request.options; an additive data-decision attribute targets it; render byte-identical
to P4, NG4). Greens TEST-AS-016 option-row/022/025 legs. REQ-AS-022/025/030,
NFR-AS-006/007/015.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(as): T-AS-028 RED ChatSurface approvals wiring + tabsStore permissionMode

TEST-AS-002 (store-fold leg), TEST-AS-006/020/021/022/025/040/042/043 (surface legs).
SPEC-AS-016/017/023/028 §4. REQ-AS-002/004/005/006/020..025/040/043, NFR-AS-008,
EC-AS-17/18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(as): T-AS-029 ChatSurface approval gating + permissionMode wiring

Implements SPEC-AS-016/017/023/028 §4. A new ApprovalGateRuntime decorator gates the
active runtime's approval callback through the per-surface ApprovalManager
(mode-gate -> match -> auto-decide OR the unchanged P4 prompt -> applyDecision);
degrades to the byte-identical P4 always-prompt path when APPROVAL_RULE_STORE_PORT is
absent. The PermissionToggle set + the ApprovalsPanel remove + the live mode thread
through ToolbarStrip/ChatComposer to tabs.setControl('permissionMode'). No providerId
branch. Greens TEST-AS-002 store-fold + TEST-AS-006/020/021/022/025/040/042/043 surface
legs. REQ-AS-002/004/005/006/020..025/040/043, NFR-AS-006/007/008, EC-AS-17/18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(as): UI batch T-AS-020..029 close-out (tasks ticks + workflow-state)

Tick T-AS-020..029 DoD checkboxes; record the UI-batch hand-off + Stage 7 progress in
workflow-state.md (implementation-log.md remains in-progress — STYLES/WIRE-IN/GATE +
manual legs remain). SPEC-AS-012..018.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(as): record T-AS-029 green SHA in implementation-log

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(as): T-AS-030 mint status-panel/permission-toggle token slice

Mint the §4.14 approvals/security --sp-* token slice (lightningcss-safe
ASCII comment): --sp-approvals-row-gap, --sp-approvals-decision-allow,
--sp-approvals-decision-deny, --sp-permission-mode-active. Apply the
active-mode token to PermissionToggle; extend the tokens-contract test
with the §4.14 presence + leak guard.

Implements SPEC-AS-020. NFR-AS-012, TEST-AS-062.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(as): T-AS-031 RED wire-in (provide store + mount panel + live gate)

Add the standalone approvals-panel mount leg (RED: main.ts does not yet
provide APPROVAL_RULE_STORE_PORT) + the structured-action-pattern gate leg
(RED: the gate derives from req.context, not getActionPattern over input).

SPEC-AS-019. TEST-AS-022/040/043/053, TEST-AS-032. REQ-AS-002/030/053.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(as): T-AS-032 provide APPROVAL_RULE_STORE_PORT in both entry points

AgentSidebarView provides ObsidianBridge.approvalRuleStore (device-local);
src/ui/main.ts provides MockBridge.approvalRuleStore. The ChatSurface gate
now runs live + the approvals panel mounts; absent → the P4 degrade. Greens
the standalone approvals-panel mount RED leg. The action-pattern follow-up
is escalated (CLAR-AS-006): threading structured input onto ApprovalRequest
conflicts with the frozen SPEC-AS-003 byte-identical-shape QA test. Also
records the T-AS-033 standalone smoke (deterministic leg automated; live-dev
deferred-manual).

Implements SPEC-AS-019. REQ-AS-002/030/053. NFR-AS-005/006. TEST-AS-022/040/043/053.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(as): R-AS-001 en permission.mode labels + locale key-parity guard

Stage-9 review (REVIEW-AS-001, approve-with-conditions) found en.ts (default
locale) missing the permission.mode.{normal,plan,yolo} labels PermissionToggle
renders → English UI showed raw keys (de.ts had them). Add the en labels.

Add a locale key-parity test (tests/ui/i18n/index.test.ts) asserting en and de
declare the EXACT same leaf key set — snapshotted at module-load so the sibling
i18nMerge mutation tests don't pollute it. Would have caught R-AS-001; guards the
i18n-heavy P8-P12. Also commits the Stage-9 review.md + traceability.md (CLAR-AS-006
deferred P3 per reviewer).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build(as): regenerate styles.css with approvals + §4.14 tokens

Shipped CSS rebuilt for P7 — ApprovalsPanel/ApprovalRuleRow + the live
PermissionToggle mode styling + the §4.14 status-panel/permission-toggle token
slice. Gate green: typecheck 0, lint 0, build + build:web + docs:api clean,
npm audit (high) clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Luis Mendez <hallo@luis-mendez.de>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ttings UI (#449)

* chore(mc): bootstrap P8 mcp-client feature + workflow-state

Cut feature/mcp-client off next (P0-P7 merged). Scope = parity-charter §3.7 in-app
Claude MCP — McpServerManager/McpConfigParser/McpTester + stdio/SSE/HTTP transports
+ settings UI (McpServerModal/McpSettingsManager/McpTestModal); backs the P6
MCP-selector seam. Key ADRs: McpClientPort (transport, stdio spawn security) +
McpConfigStorePort (config source — vault Claude-CLI-readable vs device-local).
Claude-only (non-Claude MCP is P9+). Autonomous full-epic drive. Next: /spec:requirements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(mc): P8 requirements PRD-MC-001 (mcp-client, accepted)

36 EARS REQ-MC (config/parse · manager lifecycle · transports · tester · settings
UI · selector+runtime · security · a11y+additivity) + 12 NFR-MC, each mapped to a
claudian path + TEST-MC id. Config source = vault .claude/mcp.json (Claude CLI reads
it; diverges from device-local intentionally, CLAR-MC-001). Transports stdio/SSE/HTTP
all Claude-backed; McpTester 10s timeout + partial-success. MCP tool calls gated by
the P7 ApprovalManager. Additive enabledMcpServers? (was excluded). Bundles
@modelcontextprotocol/sdk (CLAR-MC-003, rationale to record). No-servers = byte-identical
P1-P7 (REQ-MC-082). CLAR-MC-001..005 resolved-by-recommendation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(mc): P8 design DESIGN-MC-001 + ADR-MC-001..003 (accepted)

ADR-MC-001 McpConfigStorePort over vault .claude/mcp.json (Claude-CLI-readable;
diverges from device-local intentionally) + pure McpConfigParser (4 paste formats).
ADR-MC-002 McpClientPort transport seam (isAvailable/test/connect/listTools/callTool/
disconnect; never throws) over stdio/SSE/HTTP via @modelcontextprotocol/sdk, real
impl coverage-excluded obsidian/**, SDK externalized like @codemirror/* (build:web
never sees it). ADR-MC-003 additive ChatRuntimeQueryOptions.enabledMcpServers?
(fold non-empty) + McpServerManager use case; MCP tool calls route through the
unchanged P7 ApprovalManager (mcp__server__tool, no special-case, no providerId).
Components: McpSettingsManager/McpServerRow/McpServerModal/McpTestModal + expanded
McpSelector. No-servers = byte-identical P1-P7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(mc): P8 spec SPEC-MC-001..030 (mcp-client)

6 layer groups, 30 SPEC items. Pins McpConfigStorePort (vault .claude/mcp.json,
load-or-default), McpClientPort (isAvailable/test/connect/listTools/callTool/disconnect;
callTool off the P8 turn-time path), additive ChatRuntimeQueryOptions.enabledMcpServers?,
the pure McpConfigParser (4 paste formats + getMcpServerType + validate), McpServerManager
use case (await-save), McpTestResult state model (success/partial/timeout/error/unavailable),
the 3-bridge impls (SDK externalized, real transports coverage-excluded). EC-MC-1..20.
Manual legs TEST-MC-M1/M2 + real SSE/HTTP/stdio. Full coverage table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(mc): P8 tasks TASKS-MC-001 (42 tasks, TDD-ordered, 7 batches)

T-MC-001 baseline+guard-verify; DOMAIN 002..011, INFRA 012..017 (T-MC-012 adds
@modelcontextprotocol/sdk + confirms externals), APPLICATION 018..021, UI 022..033,
STYLES 034, WIRE-IN 035..037, GATE 038..043. NO guard-relax — but a file-naming
directive (VaultMcpConfigStore/SdkMcpClient, NOT ObsidianMcp*, NOT under obsidian/mcp/)
avoids the still-active ObsidianMcp* / obsidian/mcp ban. Real SDK transports + stdio
spawn coverage-excluded → manual legs T-MC-041/042 (TEST-MC-M1/M2). additive
enabledMcpServers? = no implements break.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(mc): T-MC-001 baseline-capture + guard verification + file-naming directive

Scaffold parity-screenshots.md (claudian-main baseline) + test-plan.md
(guard-verify note, Obsidian-infra file-naming directive VaultMcpConfigStore/
SdkMcpClient, manual legs TEST-MC-M1/M2 + TEST-MC-021/022/061/064) +
implementation-log.md. Confirm MCP_CONFIG_STORE_PORT/MCP_CLIENT_PORT keys + the
new @/domain/chat/mcp, @/application/chat/mcp, @/ui/chat/mcp paths + the two new
ports are not caught by DELETED_SUBSYSTEM_BAN/DELETED_INJECTION_KEYS. No src/ change.

NFR-MC-009 NFR-MC-005. SPEC-MC-001/003/004/009/015/016/017/018/021/030.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(mc): T-MC-002 RED McpTypes shapes + additive enabledMcpServers?

McpTypes.test.ts asserts the config union / ManagedMcpServer / McpTool /
McpTestResult / ParsedMcpConfig / EnabledMcpServers / DEFAULT_MCP_SERVER shapes
re-exported from @/domain/chat/mcp. ChatTurn.ts.test.ts grows the _queryKeys
exact-keys to eight (appending enabledMcpServers), adds the type leg + the
externalContextPaths-stays-EXCLUDED leg + the P7-shaped byte-identical
serialisation leg + the empty-Set mcpMentions seam. RED: the McpTypes module +
the enabledMcpServers? field do not yet exist.

TEST-MC-001 TEST-MC-082. SPEC-MC-001/002/022. REQ-MC-052/082. NFR-MC-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mc): T-MC-003 McpTypes + ChatRuntimeQueryOptions.enabledMcpServers? + barrel

Add src/domain/chat/mcp/McpTypes.ts (config union + ManagedMcpServer + McpTool +
McpTestResult + ParsedMcpConfig + EnabledMcpServers + DEFAULT_MCP_SERVER, regrown
verbatim from claudian core/types/mcp.ts + McpTester.ts:13-25) + the barrel
index.ts. Append enabledMcpServers?: EnabledMcpServers to ChatRuntimeQueryOptions
after permissionMode; P0-P7 members byte-identical, externalContextPaths? stays
EXCLUDED. Additive-only: no implements ChatRuntimePort break. Greens the
TEST-MC-001 type-shape + TEST-MC-082 serialisation legs (15/15).

SPEC-MC-001/002/022. REQ-MC-052/082. NFR-MC-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(mc): T-MC-004 RED pure McpConfigParser truth table

parseClipboardConfig across the four formats (mcpServers wrapper / single
un-named needsName:true / single named / multiple named) + the malformed/err
cases (Invalid JSON / Invalid MCP configuration format / no valid entry) +
getMcpServerType (sse/http/bare-url->http/stdio) + isValidMcpServerConfig
per-shape table + the never-throws assertion. RED: McpConfigParser.ts does not
yet exist (27 failing).

TEST-MC-003/004/005/006. SPEC-MC-004/029. REQ-MC-003/004/005/006. NFR-MC-004.
EC-MC-2/3/5/6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mc): T-MC-005 pure McpConfigParser (4 formats + type/validate) + barrel

Add src/domain/chat/mcp/McpConfigParser.ts: parseClipboardConfig (the four
Claudian formats -> Result<ParsedMcpConfig>, malformed -> Result.err) +
getMcpServerType (sse/http/bare-url->http/stdio, total) + isValidMcpServerConfig
(non-empty command OR url). Ported verbatim from claudian McpConfigParser.ts:17 +
core/types/mcp.ts:74/81 with throw paths converted to Result.err (ADR-004);
JSON.parse wrapped in trySync (domain Result-discipline). Pure + total - never
throws. Greens TEST-MC-003/004/005/006 (27/27).

SPEC-MC-004/029. REQ-MC-003/004/005/006. NFR-MC-003/004. EC-MC-2/3/5/6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(mc): T-MC-006 RED pure McpConfigCodec round-trip

deserializeMcpConfig load-or-default (null/empty/unparseable/no-mcpServers/
non-object-mcpServers -> ok([])) + sidecar default application + disabledTools
filter + skip-invalid; serializeMcpConfig default-pruning (all-default writes no
sidecar) + non-default-only fields + 2-space indent + round-trip + CLI-key
preservation (unknown top-level keys + non-servers _claudian keys) + empty
_claudian deletion + the never-throws assertion. RED: McpConfigCodec.ts does not
yet exist (19 failing).

TEST-MC-001/002/007. SPEC-MC-003. REQ-MC-001/002/007. NFR-MC-004. EC-MC-12/19/20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mc): T-MC-007 pure McpConfigCodec (round-trip + default-pruning) + barrel

Add src/domain/chat/mcp/McpConfigCodec.ts: deserializeMcpConfig (load-or-default
ok([]); DEFAULT_MCP_SERVER defaults; trimmed-non-empty disabledTools filter;
skip-invalid) + serializeMcpConfig (mcpServers + ONLY non-default _claudian.servers;
preserve unknown top-level + non-servers _claudian keys; delete empty _claudian;
2-space indent). Ported from claudian McpStorage.load:14-56 + save:58-134;
JSON.parse via trySync; delete operator replaced by rest-spread rebuild (codec
ban). Pure + total. Greens TEST-MC-001/002/007 (19/19; full mcp suite 61/61).

SPEC-MC-003. REQ-MC-001/002/007. NFR-MC-004. EC-MC-12/19/20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(mc): T-MC-008 RED pure parseCommand + getActiveServers/collectDisallowedMcpTools

parseCommand.test.ts: providedArgs passthrough, quote-aware split,
empty/whitespace command -> { cmd:'', args:[] }, single/double quote grouping,
never-throws. getActiveServers.test.ts: enabled/disabled/context-saving(∅) filter,
mentioned inclusion, fresh map; collectDisallowedMcpTools enabled-only
pre-register ignoring contextSaving, mcp__server__tool trim/dedupe/sort,
never-throws. RED: parseCommand.ts + getActiveServers.ts do not yet exist (23
failing).

TEST-MC-020a/052/053/054. SPEC-MC-005/006. REQ-MC-020/023/052/053/054/061.
NFR-MC-004. EC-MC-7/9/10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mc): T-MC-009 pure parseCommand + getActiveServers/collectDisallowedMcpTools

Add src/domain/chat/mcp/parseCommand.ts (parseCommand + splitCommandString, the
no-shell quote-aware tokeniser, stepCharacter helper for complexity) +
getActiveServers.ts (getActiveServers enabled AND (NOT contextSaving OR mentioned)
+ collectDisallowedMcpTools enabled-only trim/dedupe/sort). Ported verbatim from
claudian utils/mcp.ts:46/59 + McpServerManager.getActiveServers:38 +
getAllDisallowedMcpTools:74-94. Pure + total. Greens TEST-MC-020a/052/053/054
(23/23; full mcp suite 73/73).

SPEC-MC-005/006. REQ-MC-020/023/052/053/054/061. NFR-MC-002/004. EC-MC-7/9/10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(mc): T-MC-010 RED McpConfigStorePort + McpClientPort shapes + keys + barrel

McpConfigStorePort.test.ts: exactly load/save/exists (Result-typed) + own
MCP_CONFIG_STORE_PORT key + barrel re-export of the port + ManagedMcpServer.
McpClientPort.test.ts: exactly isAvailable/test/connect/listTools/callTool/
disconnect (test -> McpTestResult never throws, live methods Result-typed) +
McpConnection { readonly id } + own MCP_CLIENT_PORT key + barrel re-exports. RED:
the two ports + the two keys + the barrel re-exports do not yet exist.

TEST-MC-081. SPEC-MC-007/008. REQ-MC-001/002/007/020..023/030..034. NFR-MC-005.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mc): T-MC-011 McpConfigStorePort + McpClientPort + keys + barrel re-exports

Add src/domain/ports/McpConfigStorePort.ts (load/save/exists, Result-typed) +
McpClientPort.ts (isAvailable/test/connect/listTools/callTool/disconnect +
McpConnection; test -> McpTestResult never throws). Append MCP_CONFIG_STORE_PORT +
MCP_CLIENT_PORT InjectionKeys to bridge/ports.ts (own keys, no aggregate).
Re-export the two ports + McpConnection + the MCP DTOs from domain/ports barrel.
Greens TEST-MC-081 (4/4; domain chat+ports 196/196). Deleted-symbol guard green;
no implements break (new ports, no prior impl).

SPEC-MC-007/008. REQ-MC-001/002/007/020..023/030..034. NFR-MC-004/005.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(mc): T-MC-011 DOMAIN batch close-out + workflow-state hand-off

Record the DOMAIN-batch (T-MC-001..011) close-out in implementation-log.md (the
final whole-project gate: vue-tsc 0, lint 0, domain chat+ports 196/196; additivity
proven; deleted-symbol guard green) and advance workflow-state.md to stage 7
implementation (implementation-log.md + test-plan.md = in-progress; INFRA->GATE
pending) with the dev hand-off note for the INFRA batch.

SPEC-MC-001..008. NFR-MC-001/004/005.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(mc): T-MC-012 verify @modelcontextprotocol/sdk dep + externals + record rationale

Implements SPEC-MC-030 / REQ-MC-080 / NFR-MC-010 (ADR-MC-002 §3). The SDK
@modelcontextprotocol/sdk@^1.29.0 (MIT, Anthropic-maintained) is in package.json
dependencies + the committed lockfile. vite.config.ts ALL_EXTERNALS already covers
its node:* entry points (builtinModules + node: forms); the SDK package bundles into
the plugin main.js like the agent-sdk; it is imported ONLY under
src/infrastructure/obsidian/** so build:web (MockBridge) never sees it. Rationale
recorded in implementation-log.md per AGENTS.md §8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mc): T-MC-013 ObsidianBridge real vault McpConfigStore + SDK McpClient

Implements SPEC-MC-009 (REQ-MC-001/007/020..023/030..034/061..064/080,
NFR-MC-002/006 manual leg). VaultMcpConfigStore = thin Result-typed VaultPort I/O on
.claude/mcp.json delegating the round-trip to the pure McpConfigCodec (never
data.json/device-local). SdkMcpClient = real stdio (bounded explicit spawn, no-shell
parseCommand cmd+args, merged env + enhanced PATH, stderr:'ignore') / SSE / HTTP
transports over @modelcontextprotocol/sdk + a Node http(s) fetch shim, 10s
AbortController, partial-success/friendly-error mapping, torn down in finally; test
never throws. Exposed via get mcpConfigStore / get mcpClient on ObsidianBridge. Files
named per the directive (not ObsidianMcp*, not obsidian/mcp/); SDK imported only here;
coverage-excluded — behavioural gate = manual TEST-MC-M1 + TEST-MC-021/022/061/064.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(mc): T-MC-014 RED scriptable Mock McpConfigStore + McpClient + fake-ports members

RED for SPEC-MC-010. tests/infrastructure/mock/MockMcpConfigStore.test.ts (seedMcpServers
+ load/save/exists codec round-trip + setMcpStoreFailMode), MockMcpClient.test.ts
(isAvailable + setClientMode SPEC-MC-028 matrix success/partial/timeout/error/unavailable
+ scriptTestResult per-server + connect/listTools/callTool/disconnect canned + never-throws),
and the extended fake-ports.test.ts (mcpConfigStore + mcpClient members). Fails — the Mock
modules + the fake-ports members do not yet exist (T-MC-015 greens).

Traces TEST-MC-001/002/007/030..034/072/080, SPEC-MC-010, REQ-MC-002/004/007/030..033/080,
NFR-MC-006.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mc): T-MC-015 MockBridge scriptable McpConfigStore + McpClient + fake-ports members

Implements SPEC-MC-010 (REQ-MC-002/004/030..033/080, NFR-MC-006). MockMcpConfigStore =
scriptable in-memory document store round-tripped through the pure McpConfigCodec
(seedMcpServers + load/save/exists + setMcpStoreFailMode). MockMcpClient = scriptable
client (isAvailable→true, scriptTestResult per-server, setClientMode driving the
SPEC-MC-028 success/partial/timeout/error/unavailable matrix, canned connect/listTools/
callTool/disconnect). Exposed via get mcpConfigStore / get mcpClient on MockBridge +
the mcpConfigStore/mcpClient members on tests/__fakes__/fake-ports.ts. Greens the prior
RED 39/39; total, never throws; no node:*/obsidian.

Traces TEST-MC-001/002/007/030..034/072/080.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mc): T-MC-016/017 LocalStorage McpConfigStore + inert McpClient + bridge getters

Completes the P8 INFRA batch (the prior agent timed out after T-MC-015).
LocalStorageMcpConfigStore persists the .claude/mcp.json document text in
localStorage via the pure McpConfigCodec (load-or-default round-trip); the inert
LocalStorageMcpClient reports isAvailable:false + a structured test failure +
Result.err live methods + idempotent disconnect (no Node transport in the browser
demo). Wired get mcpConfigStore / get mcpClient on LocalStorageBridge. 6/6 green;
typecheck + whole-project lint 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(mc): T-MC-018 RED McpServerManager lifecycle over scriptable Mock store

Authors the failing unit tests for the lifecycle use case (SPEC-MC-012):
load (load-or-default + err-degrades-to-[]), add (default-apply + await-save +
empty/duplicate-name reject + rollback-on-save-err), edit/remove/setEnabled/
setToolDisabled (locate-by-name + await-save), getEnabledCount,
getActiveServers(∅), getEnabledMcpServers(∅ → undefined when empty), and the
never-throws-across-a-port-boundary path.

TEST-MC-010..016/050/051/052..054/072. SPEC-MC-012. REQ-MC-010..016/050/051/
052..054/071/072. NFR-MC-004. EC-MC-4/8/9/10/18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(mc): T-MC-020 RED foldEnabledMcpServers + buildMcpViewModel

Authors the failing unit tests for the two pure application transforms:
foldEnabledMcpServers (empty→undefined / all-disabled→undefined /
all-context-saving(∅)→undefined / non-empty fold with pre-registered
disallowedTools) and buildMcpViewModel (supported gate, empty-seam-vs-live kind,
McpServerVm transport-type mapping, enabledCount, never-throws).

TEST-MC-015/040/050/052/082. SPEC-MC-013/014. REQ-MC-015/040/050/051/052/082.
NFR-MC-001. EC-MC-1/8/9/13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mc): T-MC-021 foldEnabledMcpServers + buildMcpViewModel (pure)

Implements the two pure/total application transforms. foldEnabledMcpServers
(SPEC-MC-013) computes getActiveServers + collectDisallowedMcpTools and omits the
field (returns undefined) when the active set is empty, so a no-servers /
all-disabled / all-context-saving(∅) turn stays byte-identical to P7
(REQ-MC-082, NFR-MC-001). buildMcpViewModel (SPEC-MC-014) derives the
empty-seam-vs-live McpViewModel + McpServerVm rows + enabledCount, DTO-only,
no providerId branch.

SPEC-MC-013/014. REQ-MC-015/040/050/051/052/082. NFR-MC-001/005.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mc): T-MC-019 McpServerManager lifecycle use case + McpServerDraft

Implements the MCP lifecycle use case over McpConfigStorePort (SPEC-MC-012):
load (load-or-default; err degrades to a notice + empty list, never crashes),
add (DEFAULT_MCP_SERVER enabled + draft contextSaving; empty/duplicate-name
reject leaving the existing server unchanged), edit/remove/setEnabled/
setToolDisabled (locate-by-name, missing → err). Every mutation awaits store.save
before resolving and rolls the in-memory list back + surfaces a notice on a save
err (open item #4, EC-MC-18). getActiveServers(∅)/getEnabledMcpServers(∅)
delegate to the pure SPEC-MC-006/013 helpers. Result-returning, never throws
across the port boundary; no secret/config value logged.

SPEC-MC-012. REQ-MC-010..016/050/051/052..054/071/072. NFR-MC-003/004.
EC-MC-4/8/9/10/18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(mc): T-MC-018..021 APPLICATION batch close-out + workflow-state hand-off

Append the per-task implementation-log entries (T-MC-018/020 RED, T-MC-021/019
green) + the APPLICATION-batch close-out; tick the T-MC-018..021 DoD boxes in
tasks.md; advance the Stage 7 row + the implementation-log.md artifact line +
last_agent + the dev hand-off note to reflect DOMAIN + INFRA + APPLICATION
complete, UI batch (T-MC-022 onward) pending.

SPEC-MC-012/013/014. TASKS-MC-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(mc): T-MC-022 RED useMcpConfigStorePort + useMcpClientPort

TEST-MC-081 composable leg. SPEC-MC-019 REQ-MC-081 NFR-MC-005.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mc): T-MC-023 useMcpConfigStorePort + useMcpClientPort composables

Implements SPEC-MC-019. REQ-MC-081 NFR-MC-005. Inject-or-throw, one port per
composable, no aggregate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(mc): T-MC-024 RED MCP modal-seam launchers + fallbacks

TEST-MC-042/044 seam legs. SPEC-MC-023 REQ-MC-042/044 NFR-MC-007.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mc): T-MC-025 append MCP modal-seam launchers + fallback composables

Implements SPEC-MC-023. REQ-MC-042/044 NFR-MC-007. OpenMcpServerModalFn /
OpenMcpTestModalFn + keys + auto-dismiss/no-op fallbacks; P3/P4/P5 handles
byte-identical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(mc): T-MC-026 RED McpSettingsManager + McpServerRow + POs

TEST-MC-013/014/040/041/070 A legs. SPEC-MC-015 REQ-MC-013/014/040/041/070.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mc): T-MC-027 McpSettingsManager + McpServerRow + mcp i18n (en+de)

Implements SPEC-MC-015. SPEC-MC-024 REQ-MC-013/014/040/041/070 NFR-MC-005/006/007/008.
Presentational list+row; gated/empty/live; accessible-named controls; full
agent.chat.mcp.* microcopy in en+de.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(mc): T-MC-028 RED McpServerModal add/edit + paste/parse + PO

TEST-MC-010/011/012/042/043/070 A legs + EC-MC-2/3/4. SPEC-MC-016/023
REQ-MC-010/011/012/042/043/070.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mc): T-MC-029 McpServerModal add/edit + paste/parse + name validation

Implements SPEC-MC-016/023. SPEC-MC-024 REQ-MC-010/011/012/042/043/070/072
NFR-MC-006/007. parseClipboardConfig-driven; name required/duplicate block;
edit pre-fill + replacing draft; Escape cancels; no v-html/window.prompt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(mc): T-MC-030 RED McpTestModal 5-state machine + PO

TEST-MC-016/030..034/044 A legs. SPEC-MC-017/028 REQ-MC-016/023/030..034/044/070/072.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mc): T-MC-031 McpTestModal 5-state machine + per-tool toggles

Implements SPEC-MC-017/028. SPEC-MC-024 REQ-MC-016/023/030..034/044/070/072
NFR-MC-006/007/008. Probe via injected McpClientPort; running/success/partial/
timeout/error/unavailable; per-tool set-tool-disabled; live region; no secret render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(mc): T-MC-032 RED McpSelector expanded (McpViewModel list+toggle+badge)

TEST-MC-050/051/082 A legs + EC-MC-1/8. SPEC-MC-018 REQ-MC-050/051/070/082.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mc): T-MC-033 McpSelector expanded list+toggle+badge; keeps P6 empty seam

Implements SPEC-MC-018. SPEC-MC-024 REQ-MC-050/051/070/082 NFR-MC-006/007/008.
McpViewModel prop; empty-seam kept byte-identical; live list+toggle+enabledCount
badge; ToolbarStrip adapts McpWidgetVm to an empty-seam McpViewModel (additive).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(mc): T-MC-022..033 UI batch close-out + workflow-state hand-off

Records the UI-batch RED+green SHAs, the component/modal/PO inventory, the
fold/selector/approval wiring readiness + absent-port degrade, and the
P6-selector/P7-approval/tabsStore regression-green gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mc): T-MC-034 mint mcp-settings/mcp-modal/mcp-selector --sp-* token slice

Adds the §4.15 (ASCII `section 4.15` marker) MCP token slice to
src/ui/styles/tokens.css: --sp-mcp-row-gap, --sp-mcp-status-ok,
--sp-mcp-status-error, --sp-mcp-selector-badge, each a token-layer
var(--sp-*) lookup (no hex / no raw Obsidian var / no physical property,
ASCII-only comments for lightningcss). Updates the tokens-contract test
with the §4.15 presence + leak guard (TEST-MC-045) and bounds the §4.14
slice with the new marker.

Implements SPEC-MC-021. NFR-MC-009, REQ-MC-045, TEST-MC-045.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(mc): T-MC-034 backfill commit SHA in implementation-log

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(mc): T-MC-035 RED wire-in (per-surface manager + fold + P7 gating + degrade)

Adds tests/ui/chat/ChatSurface.mcp.test.ts asserting the surface builds one
per-surface McpServerManager over MCP_CONFIG_STORE_PORT, mounts the settings
surface + threads the live selector at supportsMcpTools, folds
getEnabledMcpServers(empty) into queryOptions.enabledMcpServers only when
defined, routes an mcp__<server>__<tool> approval through the UNCHANGED P7
ApprovalManager (no special-case), and degrades gracefully on a store fault /
absent ports. Extends ChatSurface.po.ts + the standalone main.ts MCP smoke leg.

RED: the production provide + the per-surface manager + the fold do not exist
yet (4 failed / 3 passed in ChatSurface.mcp.test.ts).

SPEC-MC-020, SPEC-MC-026. TEST-MC-052/065/071/072/082 + TEST-MC-081 wiring leg.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(mc): T-MC-035 backfill commit SHA in implementation-log

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mc): T-MC-036 wire MCP ports + modal hosts + per-surface manager + fold

AgentSidebarView provides MCP_CONFIG_STORE_PORT=ObsidianBridge.mcpConfigStore +
MCP_CLIENT_PORT=ObsidianBridge.mcpClient + the OPEN_MCP_SERVER_MODAL /
OPEN_MCP_TEST_MODAL launchers that open the new Obsidian Modal hosts
(McpServerModalHost / McpTestModalHost in src/plugin/**, the only obsidian +
MCP-modal imports in the wiring). src/ui/main.ts provides the Mock ports +
browser-safe seam stand-ins. ChatSurface builds one per-surface McpServerManager,
mounts McpSettingsManager, threads the live mcpVm + set-mcp-enabled through
ChatComposer/ToolbarStrip/McpSelector, and folds getEnabledMcpServers(empty)
into queryOptions.enabledMcpServers only when defined (omitted when empty so a
no-MCP turn stays byte-identical to P7). An mcp__<server>__<tool> tool call
routes through the UNCHANGED P7 ApprovalManager via ApprovalGateRuntime — no
special-case, no providerId branch.

Greens T-MC-035. SPEC-MC-020, SPEC-MC-026. REQ-MC-052/065/071/072/082.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(mc): T-MC-036 backfill commit SHA in implementation-log

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(mc): T-MC-037 record standalone MCP smoke dev leg + deferred-manual

TEST-MC-040/043/044/050/052/082 dev leg: deterministic mount automated + PASS; interactive live-dev flow deferred to human run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(mc): T-MC-034..037 close-out — tick tasks + Stage 7 hand-off note

STYLES + WIRE-IN complete; GATE T-MC-038..043 (invariant gate + human manual legs + final DoD/PR) remain. implementation-log.md stays in-progress.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(mc): T-MC Stage-9 review (approve-with-nits) + traceability

REVIEW-MC-001 verdict approve-with-nits (0 P1, 0 P2, 2 P3, 3 P4). Security
confirmed: vault .claude/mcp.json never data.json, no plaintext secret, stdio
spawn bounded/no-shell, malformed→Result.err, mcp__server__tool gated by the
unchanged P7 ApprovalManager (no providerId branch), SDK externalized. Live wiring
+ per-surface manager + enabled-servers fold + dual provide verified; file-naming
ban honoured (VaultMcpConfigStore/SdkMcpClient). TRACE-MC-001 matrix; manual legs
TEST-MC-M1/M2 + real transports pending. Nits = Windows PATH (M1), timer-untestable,
test-report (Stage-8) — non-blocking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build(mc): regenerate styles.css with §4.15 mcp tokens

Shipped CSS rebuilt for P8 — McpSettingsManager/McpServerRow/McpServerModal/
McpTestModal/McpSelector styles + the §4.15 mcp-* token slice. Gate green:
typecheck 0, lint 0, vitest 1772, build (SDK bundled) + build:web (SDK externalized,
not bundled) + docs:api clean, npm audit (high) clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Luis Mendez <hallo@luis-mendez.de>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ng/secret/home-fs (#450)

* chore(pv): bootstrap P9 providers-registry feature + workflow-state

Cut feature/providers-registry off next (P0-P8 merged). Scope = parity-charter §3.6
multi-provider — ProviderRegistryPort + Codex (app-server JSON-RPC) + Opencode (ACP)
+ shared ACP transport + model routing/capabilities/workspace registry. POSTURE
(§6a): Claude complete, Codex/Opencode capability-gated + feature-incomplete OK.
Key ADRs: ProviderRegistryPort + routing seam, HomeFsPort (beyond-vault security),
SecretStorePort (native secret storage, lands this phase + minAppVersion), ACP/
JSON-RPC transports. Largest phase. Autonomous full-epic drive. Next: /spec:requirements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(pv): P9 requirements PRD-PV-001 (providers-registry, accepted)

64 EARS REQ-PV (registry/selection · routing · capabilities matrix · Codex · Opencode
· ACP transport · model routing · secret storage · home-fs/history · settings UI ·
security · additivity) + 14 NFR-PV + 7 CLAR-PV (resolved-by-recommendation). BINDING
posture: Claude COMPLETE default, Codex/Opencode CAPABILITY-GATED + feature-incomplete
OK. Per-provider matrix grounded in claudian capabilities.ts. ProviderRegistryPort =
data-driven routing (no switch(id), NFR-PV-014). SecretStorePort → app.secretStorage
never data.json; HomeFsPort beyond-vault read-scoped/consented. minAppVersion: keep
1.12.7 + capability-gate (escalate if app.secretStorage forces a bump). Claude-only =
byte-identical P0-P8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(pv): P9 design DESIGN-PV-001 + ADR-PV-001..003 (accepted)

ADR-PV-001 ProviderRegistryPort + data-driven routing seam (widens CHAT_RUNTIME_FACTORY
to (providerId)->Result, parameterises createProviderHistoryPort/getCatalog by providerId;
capability-flag-gated UI, NO switch(providerId); Claude-only = byte-identical P8).
ADR-PV-002 SecretStorePort -> app.secretStorage, never data.json/device-local/log/DTO;
in-memory Mock/LS; capability-gate when unavailable; minAppVersion keep-1.12.7 +
escalate-not-bump. ADR-PV-003 HomeFsPort read-scoped/consented beyond-vault (~/.codex,
~/.claude) read-only + coverage-excluded Codex JSON-RPC + shared ACP transports behind
the registry; history into the unchanged P3 ProviderHistoryPort. No new runtime dep
(thin in-tree JSON-RPC/ACP). Frozen capability matrix (Claude complete; Codex/Opencode
capability-gated). Components ProviderChooser/ProviderOption/ProviderSecretField +
provider-aware selectors. Minimal selection surface (full per-provider settings = P10).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(pv): P9 spec SPEC-PV-001..034 (providers-registry)

6 layer groups, 34 SPEC items. Pins ProviderRegistryPort (pure reads:
list/enable/getDescriptor/getCapabilities/resolveActiveProvider/resolveProviderForModel),
SecretStorePort (isAvailable/get/set/delete/listKeys, provider.<id>.apiKey),
HomeFsPort (read-only, HOME_FS_ROOTS=.codex/.claude, path-escape->err), ProviderDescriptor
(frozen capability bag + isEnabled/ownsModel), widened ChatRuntimeFactory (providerId)->Result
+ OPEN_PROVIDER_CONSENT. Codex JSON-RPC + ACP transports coverage-excluded. Frozen matrix
(BACKED caps only, GATED-OFF honest-false, NG1). Claude-only = byte-identical P8 (SPEC-PV-027).
Manual legs M1 Codex / M2 Opencode / M3 secret / M4 screenshots. Full coverage table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(pv): P9 tasks TASKS-PV-001 (44 tasks, TDD-ordered, 7 batches)

T-PV-001 baseline+guard-verify; DOMAIN 002..010 (T-PV-010 widens CHAT_RUNTIME_FACTORY
(providerId)->Result with full call-site fan-out), INFRA 011..018, APPLICATION 019..024,
UI 025..032, STYLES 033, WIRE-IN 034..036, GATE 037..044. NO guard-relax — but the active
ObsidianSecretStore* ban handled via file-naming (SecretStorage.ts/HomeFileSystem.ts, NOT
ObsidianSecretStore*; transports at obsidian/ root). Real Codex JSON-RPC / Opencode ACP /
app.secretStorage / ~/.codex reads coverage-excluded → manual legs T-PV-040..043
(TEST-PV-M1/M2/M3/M4). Capability-matrix discipline: BACKED only, honest-false GATED-OFF.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(pv): T-PV-001 baseline + guard-verify + file-naming directive

Scaffolds parity-screenshots.md (claudian baseline col), test-plan.md
(guard-verify + manual legs TEST-PV-M1/M2/M3/M4 + Obsidian-infra naming
directive), implementation-log.md. Records the SecretStorePort/SECRET_STORE_PORT
P9-regrow guard-relax (ICON_PORT precedent) needed in T-PV-009 — the planner's
no-relax verdict missed the P0-deleted secret symbols. No src/ change.

SPEC-PV-002/008/009/010/022, NFR-PV-007/009.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(pv): T-PV-002 RED widened ProviderId union + settings provider fields

Asserts ProviderId widens to 'claude'|'codex'|'opencode' (exactly three,
'claude' stays assignable) + PluginSettings.activeProvider (default 'claude')
+ enabledProviders (default []). RED: type-level + runtime fail until T-PV-003.

TEST-PV-005, TEST-PV-114, SPEC-PV-001/027, REQ-PV-005/103/114, NFR-PV-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-003 widen ProviderId + add settings provider fields

Widens ProviderId to 'claude'|'codex'|'opencode' (additive). Appends
activeProvider (default 'claude') + enabledProviders (default []) to
PluginSettings + DEFAULT_SETTINGS with pure coerceActiveProvider/
coerceEnabledProviders load-or-default helpers. Same-task additive fan-out:
core-settings + ObsidianBridge coerce the two fields; the round-trip fixture +
exact-key guard grow. Greens TEST-PV-005 + TEST-PV-114.

SPEC-PV-001/027, REQ-PV-005/103/114, NFR-PV-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(pv): T-PV-004 RED frozen ProviderDescriptor capability matrix

Asserts the ProviderCapabilities/ProviderDescriptor shapes, the three frozen
descriptors per the SPEC-PV-022 truth table (claude all-true; codex
rewind/commands/MCP off, steer/fork on; opencode rewind/fork/steer/MCP off,
commands on; reasoningControl effort; needsApiKey/readsHomeDir per provider),
Object.freeze, distinct blankTabOrder 10/15/20, isEnabled (claude-always /
non-claude membership), the pure ownsModel predicate, PROVIDER_DESCRIPTORS,
DEFAULT_CHAT_PROVIDER_ID, never-throws. RED until T-PV-005.

TEST-PV-020/021/022/023, SPEC-PV-002/022, REQ-PV-001/020/021/022/023/103, NFR-PV-014.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-005 frozen ProviderDescriptor capability matrix + barrel

Adds the ProviderCapabilities/ProviderDescriptor interfaces + the three
Object.freeze'd descriptors per the SPEC-PV-022 matrix (claude all-true; codex
rewind/commands/MCP off, steer/fork on; opencode rewind/fork/steer/MCP off,
commands on; reasoningControl effort; needsApiKey/readsHomeDir per provider) +
PROVIDER_DESCRIPTORS + DEFAULT_CHAT_PROVIDER_ID. Pure isEnabled (claude-always /
non-claude membership) + ownsModel predicates grounded verbatim in claudian.
BACKED caps wired, GATED-OFF literal false (NG1). No switch(providerId). Greens
TEST-PV-020/021/022/023.

SPEC-PV-002/022, REQ-PV-001/020/021/022/023/103, NFR-PV-014.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(pv): T-PV-006 RED pure resolveProvider helpers

Asserts listEnabledProviders (isEnabled-filtered, blank-tab order, claude always
present, fresh array), resolveActiveProvider (recorded-if-enabled else claude;
no-record/disabled fallback), resolveProviderForModel (ownsModel match else
active/claude fallback), and never-throws. RED until T-PV-007.

TEST-PV-002/003/060/061, SPEC-PV-003/029, REQ-PV-002/003/006/060/061, NFR-PV-014,
EC-PV-2/3/9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-007 pure resolveProvider helpers + barrel

Adds listEnabledProviders (isEnabled-filter + blankTabOrder sort, fresh array,
claude always present), resolveActiveProvider (recorded-if-enabled else claude),
resolveProviderForModel (first ownsModel else active/claude fallback). Pure +
total, no switch(providerId). Greens TEST-PV-002/003/060/061 + EC-PV-2/3/9.

SPEC-PV-003/029, REQ-PV-002/003/006/060/061, NFR-PV-014, EC-PV-2/3/9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(pv): T-PV-008 RED ProviderRegistryPort/SecretStorePort/HomeFsPort shapes

Asserts ProviderRegistryPort (7 pure-sync reads), SecretStorePort
(isAvailable + getSecret/setSecret/deleteSecret/listKeys Result-typed +
providerSecretKey = provider.<id>.apiKey), HomeFsPort (isAvailable +
readFile/exists/listFolders, no write/delete, HOME_FS_ROOTS [.codex,.claude]),
the 3 own keys + the barrel re-exports. RED until T-PV-009.

TEST-PV-112, SPEC-PV-004/006/007, REQ-PV-001/070..073/080..083/112, NFR-PV-006.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-009 ProviderRegistryPort/SecretStorePort/HomeFsPort + keys + barrel

Adds the three narrow read ports (ProviderRegistryPort 7 pure-sync reads;
SecretStorePort isAvailable + 4 Result methods + providerSecretKey; HomeFsPort
read-first isAvailable/readFile/exists/listFolders + HOME_FS_ROOTS, no
write/delete) + the PROVIDER_REGISTRY_PORT/SECRET_STORE_PORT/HOME_FS_PORT keys +
barrel re-exports. Greens TEST-PV-112.

Guard-relax (P9 secret-seam regrow, ICON_PORT precedent): drops the stale
P0-deleted @/domain/ports/SecretStorePort + SECRET_STORE_PORT from the
deleted-symbol guard; the Obsidian-layer ObsidianSecretStore* glob stays banned.

SPEC-PV-004/006/007, REQ-PV-001/070..073/080..083/112, NFR-PV-006, ADR-PV-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(pv): T-PV-010 RED widened CHAT_RUNTIME_FACTORY + OPEN_PROVIDER_CONSENT seam

Extends modalSeam.ts.test.ts: ChatRuntimeFactory widens to
(providerId)=>Result<ChatRuntimePort> (construct-fail = Result.err not throw);
useChatRuntimeFactory still throws-when-absent; OpenProviderConsentFn +
OPEN_PROVIDER_CONSENT key; useOpenProviderConsent auto-declines (false) when
absent. The P3-P8 handles stay byte-identical. RED until the widen (T-PV-010 green).

TEST-PV-010/011/082/113/114, SPEC-PV-005/031, REQ-PV-010/011/012/082/113/114,
NFR-PV-001/008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-010 widen CHAT_RUNTIME_FACTORY + OPEN_PROVIDER_CONSENT + fan-out

Widens ChatRuntimeFactory to (providerId)=>Result<ChatRuntimePort> (construct-fail
= Result.err not throw) + appends OpenProviderConsentFn + OPEN_PROVIDER_CONSENT +
useOpenProviderConsent (auto-decline false fallback). Same-task interface-change
fan-out keeping vue-tsc 0 whole-project: AgentSidebarView + main.ts provide
(providerId)=>ok(createChatRuntime()); ChatSurface adapts the widened seam to the
UNCHANGED P3 store binding (default 'claude', unwrap); the 7 ChatSurface mount
fixtures wrap their factory in ok(). Byte-identical at runtime to P8 for 'claude'.
Greens TEST-PV-010/011/082/113/114.

SPEC-PV-005/031, REQ-PV-010/011/012/082/113/114, NFR-PV-001/008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(pv): T-PV-010 close-out + Stage 7 DOMAIN-batch hand-off

Records the T-PV-010 commit SHA in implementation-log.md and updates
workflow-state.md (stage->implementation/in-progress; DOMAIN batch T-PV-001..010
done; hand-off to the INFRA batch). Escalates the planner guard-verification
defect (SECRET_STORE_PORT/SecretStorePort were still banned; resolved via the
ICON_PORT per-phase regrow precedent in T-PV-009).

SPEC-PV-001..007, NFR-PV-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-011/012 shared descriptor-table ProviderRegistry impl

Implements SPEC-PV-008 (TEST-PV-001/002/003/013/020/060/061). Data-driven
registry over the frozen PROVIDER_DESCRIPTORS + pure resolveProvider helpers;
no switch(providerId) (NFR-PV-014). Coverage-included pure data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-013/014 Mock scriptable runtime/transport + secret + home-fs + fake-ports

Implements SPEC-PV-011 (TEST-PV-011/050..053/070..073/080..083/100). Scriptable
per-provider runtime registry (construct gate + stream/timeout/error-chunk),
in-memory SecretStorePort (availability switch), inert/seedable HomeFsPort
(path-escape rule), shared on MockBridge + fake-ports members. No node:*/obsidian.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-015/016 LocalStorage inert non-Claude runtime + secret + home-fs

Implements SPEC-PV-012 (TEST-PV-073/083/100 LS legs). Demo runtime registry
(Claude fixture ok, non-Claude err 'unavailable' — degrades), in-memory
SecretStorePort (isAvailable true), inert HomeFsPort (isAvailable false). No node:*.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-018 Codex JSON-RPC + shared ACP stdio transports

Implements SPEC-PV-010/026 (manual legs TEST-PV-M1/M2). Shared in-tree
line-delimited JSON-RPC 2.0 channel (bounded spawn, per-request timeout->err,
dying-subprocess->terminal error chunk, SIGTERM->SIGKILL 3s); CodexRpcTransport
(turn stream + steer) + AcpTransport (initialize + prompt, no steer). No new dep;
coverage-excluded. No obsidian symbol leak.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-017 ObsidianBridge runtime registry + SecretStorage + HomeFileSystem

Implements SPEC-PV-009/031/034 (manual legs TEST-PV-M1/M2/M3). Data-driven
runtime registry (Claude reuse / Codex JSON-RPC / Opencode ACP, no switch),
real app.secretStorage SecretStorePort (never data.json), real node:fs HomeFsPort
(root-scoped, path-escape->err). Files named per the T-PV-001 directive
(SecretStorage.ts/HomeFileSystem.ts, never ObsidianSecretStore*). Coverage-excluded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(pv): T-PV-011..018 INFRA-batch close-out + Stage 7 hand-off

INFRA batch complete (commits 7af60ea/50a0fdd7/58f53787/dcba7b99/988d7997).
Records verification (vue-tsc 0, lint 0 errors, 1877 tests), file-naming
confirmation, registry/bridge/transport notes, deviations, manual legs, and the
APPLICATION-batch hand-off. implementation-log.md stays in-progress (APP/UI/
STYLES/WIRE-IN/GATE + manual legs remain).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(pv): T-PV-019 RED SelectProviderUseCase select/auto-switch/honest-gate

Author the failing unit tests for the provider select use case over the
scriptable Mock registry + runtime-registry factory + in-memory settings:
select (persist device-local / construct-ok / construct-err honest-notice /
reset-prior / no-throw / no-secret-leak), selectForModel (auto-switch / no-op /
unowned-fallback), and the no-switch(providerId) source guard. RED: the module
does not yet exist.

Implements SPEC-PV-013/023/029. REQ-PV-004/010/011/012/060/061/071/100/102.
TEST-PV-004/010/011/012/060/071/100. NFR-PV-005/014. EC-PV-4/5/8/13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-020 SelectProviderUseCase resolve+activate+persist+construct

Implement the provider select use case: select(id, prior) tears down the prior
runtime (reset+cancel), persists activeProvider device-local via read-modify-
write SettingsPort (never data.json/secret), then constructs the active runtime
through the widened (providerId)=>Result factory — a construct err surfaces an
honest secret-free notice and returns the err (chat stays usable, never throws).
selectForModel auto-switches to the model's owning provider or no-ops on prior.
Capability-gated routing, never switch(providerId). Barrel re-export added.

Implements SPEC-PV-013/023/029. REQ-PV-004/010/011/012/060/061/071/100/102.
NFR-PV-005/014.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(pv): T-PV-019/020 implementation-log entry + fix T-PV-017 commit SHA

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(pv): T-PV-023 RED buildProviderViewModel chooser + capability-gated widgets

Author the failing unit tests for the pure chooser/widget view-model: options
(blank-tab-ordered rows + isActive/isDefault), showChooser = enabled>1 (single-
Claude → false, byte-identical P8; empty list total), widgets read the active
capability bag field-for-field (Claude all-but-steer; Codex no rewind/commands/
MCP, fork+steer+service-tier on; Opencode no rewind/fork/steer/MCP, commands on),
plus the no-switch(providerId) source guard + never-throws. RED: module absent.

Implements SPEC-PV-015/029. REQ-PV-002/006/013/024/034/043/062/063/064/090/114.
TEST-PV-006/013/024/034/043/062/063/064/090. NFR-PV-014. EC-PV-1/14/15.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-024 pure buildProviderViewModel chooser + capability-gated widgets

Implement the pure, total provider view-model: options maps the blank-tab-ordered
enabled descriptors to rows with isActive/isDefault; showChooser = enabled>1
(single-Claude → false, byte-identical P8); widgets read the active capability bag
field-for-field (rewind/fork/turn-steer/provider-commands/MCP/reasoning + service-
tier gated on the turn-steer/Codex config). DTO-only, never branched on the provider
id, never throws. ProviderOptionVM/ProviderWidgetVM/ProviderViewModel + barrel.

Implements SPEC-PV-015/029. REQ-PV-002/006/013/024/034/043/062/063/064/090/114.
NFR-PV-014.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(pv): T-PV-021 RED ProviderConsentGate one-time beyond-vault consent

Author the failing unit tests for the consent gate over the in-memory Mock
settings + a stubbed openConsent: recorded-true → ok(true) no prompt (EC-PV-6);
no-record → openConsent once, record the accept device-local, ok(true);
declining → ok(false) persisted (no re-prompt); the auto-decline launcher →
ok(false) recorded; per-provider records do not clobber each other; never throws.
RED: the gate module does not yet exist.

Implements SPEC-PV-014/024. REQ-PV-082/113/114. TEST-PV-082. NFR-PV-003/005.
EC-PV-6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-022 ProviderConsentGate one-time beyond-vault consent

Implement the consent gate: ensureConsent(id) reads provider.homeFsConsent.<id>
device-local — a recorded accept/decline returns without a prompt (one-time, no
re-prompt); no record opens the modal seam once (auto-decline when absent),
records the boolean outcome device-local, returns ok(outcome). Declining → ok(false)
so the caller disables history honestly; never throws; never window.confirm; never
a secret. Adds the OPTIONAL homeFsConsent settings field (absent from DEFAULT_SETTINGS,
non-breaking) + homeFsConsentKey helper required by the spec's device-local record.

Implements SPEC-PV-014/024. REQ-PV-082/113/114. NFR-PV-003/005.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(pv): T-PV-021/022/023/024 implementation-log entries + consent escalation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(pv): APPLICATION batch T-PV-019..024 close-out + Stage 7 hand-off

Record the APPLICATION batch as done in workflow-state (SelectProviderUseCase,
ProviderConsentGate, buildProviderViewModel), the verification performed, and the
homeFsConsent ObsidianBridge round-trip escalation for the WIRE-IN batch. Next
agent: UI batch T-PV-025..032.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(pv): T-PV-025 RED useProviderRegistryPort/useSecretStorePort/useHomeFsPort

RED inject-or-throw composable tests mirroring useVaultPort/useToolbarCatalogPort
(ADR-008 one-port-one-composable, no aggregate). Returns the injected port when
provided; throws a clear "was not provided" error otherwise.

TEST-PV-112 (composable leg). SPEC-PV-019. REQ-PV-112, NFR-PV-006.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-026 useProviderRegistryPort/useSecretStorePort/useHomeFsPort

The three P9 port composables, mirroring useVaultPort's inject-or-throw
(ADR-008 one-port-one-composable, no aggregate). Each injects its own key
(PROVIDER_REGISTRY_PORT / SECRET_STORE_PORT / HOME_FS_PORT), returns the port,
throws a clear "was not provided" error when unprovided. No obsidian/node:*
under src/ui. The prior RED TEST-PV-112 composable leg now passes.

SPEC-PV-019. REQ-PV-112, NFR-PV-006.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(pv): T-PV-025/026 implementation-log entries

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(pv): T-PV-027 RED ProviderChooser + ProviderOption component tests

Author the failing component tests + co-located data-testid PageObjects per
SPEC-PV-016: ProviderChooser renders nothing at showChooser=false (byte-identical
P8), lists enabled providers in blank-tab order with icon + active marker +
select-emits when true; ProviderOption is one keyboard-operable row. A11y:
accessible name, aria-current active announce, text+icon cues (never colour-only),
no v-html. Traces TEST-PV-001/002/006/090/110/113/114 (A legs), REQ-PV-001/002/003/004/006/090/110/113/114.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-028 ProviderChooser + ProviderOption components

Implement src/ui/chat/providers/ProviderChooser.vue + ProviderOption.vue per
SPEC-PV-016: presentational props-in/events-out; chooser renders nothing at
showChooser=false (byte-identical P8), else a role=listbox of provider rows in
blank-tab order; each row keyboard-operable (Enter/Space), announces active via
aria-current, conveys state by text+icon (never colour-only). Adds the
agent.chat.providers.* i18n keys (en+de, locale-parity preserved) + the Codex/
Opencode/Claude/API brand allowlist entries. No obsidian/v-html.
Implements SPEC-PV-016/030. Traces REQ-PV-001/002/003/004/006/090/110/113/114, NFR-PV-006/007/008/009.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(pv): T-PV-027/028 implementation-log entries

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(pv): T-PV-029 RED ProviderSecretField component test

Author the failing component test + co-located data-testid PageObject per
SPEC-PV-018: masked type=password input + save(value)-emits; the typed value is
never echoed into the DOM/markup (REQ-PV-102); disabled-with-unavailable-message
(no plain-store fallback) when available=false (EC-PV-10); accessible name; no
v-html. Traces TEST-PV-070/072/092/102/110 (A legs), REQ-PV-070/072/092/102/110, NFR-PV-002/008/009.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-030 ProviderSecretField masked secret-entry field

Implement src/ui/chat/providers/ProviderSecretField.vue per SPEC-PV-018:
presentational masked type=password input; emits save(value) on submit/click; the
typed secret lives only in a transient local ref, cleared on emit, never echoed
into the DOM value attribute / notice / log / store / DTO (REQ-PV-102, NFR-PV-002);
disabled with the honest providers.secret.unavailable message and no plain-store
fallback when available=false (EC-PV-10). Accessible name + label association; no
obsidian/v-html. Implements SPEC-PV-018/025/030. Traces REQ-PV-070/072/092/102/110, NFR-PV-002/006/008/009.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(pv): T-PV-029/030 implementation-log entries

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(pv): T-PV-031 RED provider-aware ModelSelector + no-switch(providerId) guard

Extend the P6 ModelSelector PO + test per SPEC-PV-017: an additive optional
providerId prop renders the opencode-model-picker shape when the active provider is
opencode; absent / claude stays byte-identical P6 (NFR-PV-001). Add a source-level
no-switch(providerId)/no-provider-equality guard over the toolbar widgets + the
provider components (SPEC-PV-029). Traces TEST-PV-013/062 (A legs), REQ-PV-013/062, NFR-PV-001/014.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-032 provider-aware ModelSelector opencode picker shape

Implement SPEC-PV-017 at the component layer: ModelSelector gains an additive
optional providerId prop that selects a per-provider picker variant from a
data-driven PICKER_VARIANT map (the opencode-model-picker shape) — a pure lookup,
never a switch(providerId)/provider-id branch (NFR-PV-014); absent / claude renders
byte-identical P6 (NFR-PV-001). ToolbarStrip threads the optional providerId through
to ModelSelector. The ThinkingSelector/ServiceTierToggle + rewind/fork/steer/MCP/
provider-command affordances already gate on the capability bag (getToolbarCapabilities
/ getCapabilities / buildProviderViewModel), so P9 supplies the per-provider flags and
the gating "just works". Harden the no-switch source guard to strip comments.
Implements SPEC-PV-017/029/030. Traces REQ-PV-013/024/034/043/062/063/064, NFR-PV-001/006/008/014.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(pv): T-PV-031/032 implementation-log entries + UI-batch scope note

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(pv): UI batch T-PV-027..032 close-out + Stage 7 hand-off

Tick the T-PV-027..032 DoD boxes; update workflow-state (last_agent, Stage 7 row,
implementation-log artifact = in-progress with STYLES/WIRE-IN/GATE + manual legs
remaining); append the UI-batch hand-off note (component+PO inventory, degrade-when-
absent, the parent-owned WIRE-IN scope boundary, next agent = STYLES T-PV-033).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-033 mint provider/opencode-model-picker --sp-* token slice

Implements SPEC-PV-021. Adds the section 4.16 token slice (four minted tokens:
--sp-provider-brand-claude/-codex/-opencode aliasing the section 4.2 brand
literals + --sp-model-picker-group-gap reusing --sp-space-5) with an ASCII-only
comment for the lightningcss pass; applies the slice to ProviderOption.vue
(per-provider brand swatch) + the opencode-model-picker variant in
ModelSelector.vue; extends tests/ui/styles/tokens.test.ts with the §4.16
presence + no-leak guard (TEST-PV-091) and re-bounds the §4.15 leak guard.

REQ-PV-091, NFR-PV-010, TEST-PV-091. SPEC-PV-021.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(pv): T-PV-034 RED wire-in surface routing + chooser mount

Adds tests/ui/chat/ChatSurface.providers.test.ts (provides PROVIDER_REGISTRY_PORT
+ SETTINGS_PORT + the widened CHAT_RUNTIME_FACTORY spy + a recording
ToolbarCatalogPort; asserts the chooser mounts + lists enabled providers in
blank-tab order, selecting a provider routes through SelectProviderUseCase so the
factory is re-called with the selected id + the selection persists device-local,
the toolbar reads getCatalog(active), and single-Claude / no-registry = no chooser
= byte-identical P8) + the ChatSurface.po.ts chooser PageObject helpers. RED: 4
fail / 2 pass.

REQ-PV-010/012/062/082/084/114, NFR-PV-001. SPEC-PV-020/031/034.
TEST-PV-010/012/062/084/112/114.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-035 wire provider registry/factory/consent into the surface

Implements SPEC-PV-020/031/034. ChatSurface injects PROVIDER_REGISTRY_PORT,
resolves the active provider from the registry + settings, passes it to the
widened CHAT_RUNTIME_FACTORY(providerId) per tab, mounts the ProviderChooser
(hidden at <=1 enabled = byte-identical P8), routes a selection through
SelectProviderUseCase + tabs.rebindActiveRuntime (gating a readsHomeDir provider
through ProviderConsentGate), and reads getCatalog(activeProvider). Adds the
tabsStore.rebindActiveRuntime action (provider-switch path, EC-PV-13).
AgentSidebarView provides the three ObsidianBridge ports + the registry-routed
widened factory + the ProviderConsentModal launcher; src/ui/main.ts provides the
Mock equivalents + a browser-safe consent stand-in. The Mock runtime registry
construction becomes a data-driven builder table whose claude entry reuses the P1
MockChatRuntime so the standalone demo stays byte-identical P8 (NFR-PV-001).

REQ-PV-010/012/062/082/084/114, NFR-PV-001/006/008/014. SPEC-PV-020/031/034.
TEST-PV-010/012/062/084/112/114.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pv): T-PV-036 round-trip homeFsConsent + standalone providers smoke

Implements the SPEC-PV-014/024 device-local consent persistence. Adds the pure
coerceHomeFsConsent helper and load-or-defaults homeFsConsent in BOTH coercion
sites (ObsidianBridge._coerceSettings + core-settings.validateSettings) — the
prior explicit key list DROPPED it, so a recorded one-time beyond-vault consent
would not survive a production reload (the gate re-prompted every reload). Adds a
save->reload round-trip test (REQ-PV-082, EC-PV-6) + a byte-identical-absent test
(NFR-PV-001) + a standalone providers smoke dev leg (the provider-wired surface
mounts, no chooser on the single-Claude default). Moves the mount.ts.test.ts
runtime spy to the new providerRuntimeRegistry.createChatRuntime factory seam (the
only test broken by the T-PV-035 factory routing; the inline-ask assertion is
unchanged).

REQ-PV-082, NFR-PV-001/006. SPEC-PV-014/024. TEST-PV-006/062/072/090/100 (dev legs),
EC-PV-6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(pv): T-PV-036 backfill real commit SHA in log + hand-off note

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(pv): T-PV-037 whitelist provider key-entry strings in forbidden-terms guard

The P9 provider secret field (agent.chat.providers.secret.*) + the key-required
notice (agent.chat.providers.notice.keyRequired) legitimately say "API key" — the
user is entering one; it is a credential-configuration affordance sharing the
settings-context exception (NFR-MPS-011). Add both to ALLOWED_PREFIXES. Guard 1/1 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(pv): P9 Stage-9 review (approved-with-conditions) + traceability

REVIEW-PV-001 verdict approved-with-conditions (0 P1, 0 P2, 3 P3, 4 P4). Security
confirmed: SecretStorePort→app.secretStorage only (never data.json/log/DTO), HomeFsPort
read-only/scoped/consented (path-escape→err), stdio bounded/no-shell, honest gate,
NO switch(providerId) (source-guard 6/6). Live wiring + dual provide + consent +
homeFsConsent round-trip + getCatalog un-hardcode verified; capability matrix verbatim
parity; file-naming ban honoured (SecretStorage/HomeFileSystem); scoped guard-relax.
TRACE-PV-001 matrix; manual legs TEST-PV-M1/M2/M3/M4 + minAppVersion check pending.
P3 follow-ups (Codex stream-park / service-tier proxy / history mapping) non-blocking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build(pv): regenerate styles.css with §4.16 provider tokens

Shipped CSS rebuilt for P9 — ProviderChooser/ProviderOption/ProviderSecretField +
the opencode-model-picker variant + the §4.16 provider brand-swatch/model-picker
token slice. Gate green: typecheck 0, lint 0, vitest 1945 passed (3 load-induced
teardown leaks under the 22-min run, not reproduced in a focused run; all tests pass),
build (SDK bundled 1.76MB) + build:web (346kB) + docs:api clean, npm audit (high) clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Luis Mendez <hallo@luis-mendez.de>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ env snippets (#451)

* chore(ss): bootstrap P10 settings-shell feature + workflow-state

Cut feature/settings-shell off next (P0-P9 merged). Scope = parity-charter §3.8
settings shell — provider tabs + per-provider settings UX (model picker / agent-
skill-subagent read-only / slash-command) + environment settings + env-snippet
manager + keyboard nav + approvals surfaced. Mostly surfaces P6-P9 seams into the
Obsidian PluginSettingTab (coverage-excluded DOM → automated weight in a pure
settings view-model + any new env-snippet store). Autonomous full-epic drive; split
big batches (P8/P9 timeout lesson). Next: /spec:requirements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ss): P10 requirements PRD-SS-001 (settings-shell, accepted)

37 EARS REQ-SS (shell tabs · per-provider settings+secret key · model picker ·
agent/skill/subagent read-only surfacing · slash-command · environment settings ·
env-snippet manager · keyboard nav · approvals/MCP surfacing · security · additivity)
+ 12 NFR-SS, each tagged NEW vs SURFACED + mapped to a claudian path + TEST-SS id.
~30 SURFACED (wire P6-P9 ports), ~10 NEW (env-snippet manager + pure settings
view-model + WCAG-AA keyboard-nav shell). Env-snippet store (CLAR-SS-001, ADR-needed):
structure device-local via SettingsPort, secret-bearing values via SecretStorePort
(never data.json). Settings stays Obsidian Setting-API DOM (not Vue, CLAR-SS-002);
tested weight = pure buildSettingsViewModel, DOM coverage-excluded. agent/skill/subagent
= read-only (no CRUD, NG1). Claude-only = byte-identical. CLAR-SS-001..006 resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ss): P10 design DESIGN-SS-001 + ADR-SS-001/002 (accepted)

ADR-SS-001 env-snippet store: EnvSnippetStruct (envEntries = inline|secretRef union)
— non-secret structure device-local via SettingsPort (additive optional PluginSettings
fields, _coerceSettings round-trips them, P9 homeFsConsent pattern), secret-bearing
values via SecretStorePort under env.<scope>.<KEY>; pure classifier + EnvSnippetService
(composes existing ports, no new port); injects into the P9 runtime env; no plaintext
secret in data.json. ADR-SS-002 pure buildSettingsViewModel over the P6-P9 ports
(capability-gated, no switch(providerId)); PluginSettingTab stays Obsidian Setting-API
DOM (coverage-excluded), tested weight = the view-model + store + classifier + coerce
helpers. Keyboard-nav via native Setting controls in view-model order (WCAG 2.2 AA).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ss): P10 spec SPEC-SS-001..028 (settings-shell)

6 layer groups, 28 SPEC items. Pins EnvSnippetStruct/EnvEntry (inline|secretRef union)
+ envSecretKey + EnvSnippetCodec (masks secretRefs) + classifyEnvKey (13-key
SHARED_ENVIRONMENT_KEYS, isSecretEnvKey) via additive ProviderDescriptor.environmentKeyPatterns?
(no switch(providerId)); 6 additive optional PluginSettings fields + coerce* round-trip;
pure buildSettingsViewModel (ordered capability-gated sections, 14-member SettingsControl
union); EnvSnippetService (secret-split, composes SettingsPort+SecretStorePort, no new port);
parseNavMappings. Read-only agent/skill source = P4 ProviderCommandCatalogPort. Settings DOM
coverage-excluded → manual legs M1-M4; tested weight = view-model/classifier/codec/service.
Claude-only = byte-identical P9. Full coverage table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ss): P10 tasks TASKS-SS-001 (35 tasks, TDD-ordered, 7 batches)

T-SS-001 baseline+guard-verify (NO guard-relax — env subsystem composes SETTINGS_PORT
+SECRET_STORE_PORT, no new key, no banned-glob collision); DOMAIN 002..013, APPLICATION
014..019, INFRA 020..024, PLUGIN(cov-excluded) 025..026, STYLES 027..028, WIRE-IN
029..030, GATE 031..035. Additive ProviderDescriptor.environmentKeyPatterns? + 6 optional
PluginSettings fields = no implements break (P9 frozen-matrix + settings round-trip stay
green). Settings DOM coverage-excluded → manual legs T-SS-032..034 (TEST-SS-M1..M4). Chunk
boundaries for impl: C1=001 C2=002..011 C3=012..017 C4=018..024 C5=025..030 C6=031..035.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ss): T-SS-001 baseline-capture + guard verification

Scaffold parity-screenshots.md (baseline column from claudian settings tabs)
+ test-plan.md (guard-verify note + Claude-only additivity baseline + manual
legs TEST-SS-M1..M4) + implementation-log.md. Lint confirms the new env paths
(@/domain/chat/environment/**, @/domain/settings/keyboardNav,
@/application/settings/**) are not guard-banned; NO new InjectionKey; NO new
obsidian/** file. Verdict: no guard-relax task in P10. No src/ change.

NFR-SS-009 NFR-SS-001 NFR-SS-011 SPEC-SS-015 SPEC-SS-020 SPEC-SS-028

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ss): T-SS-002 RED six additive PluginSettings fields + coerce* + envSecretKey

The six OPTIONAL device-local fields (envSnippets/envScopes/keyboardNav/
providerDefaultModel/defaultPermissionMode/providerCliPath) absent from
DEFAULT_SETTINGS (exact-key byte-identity) + the six coerce* load-or-default
table (pure/total, round-trip, never-throw) + envSecretKey. RED: the helpers
do not yet exist.

TEST-SS-092 TEST-SS-093 SPEC-SS-001 SPEC-SS-020 NFR-SS-001

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ss): T-SS-004 RED additive ProviderDescriptor.environmentKeyPatterns field

Extend the P9 frozen-matrix suite with the OPTIONAL environmentKeyPatterns?:
readonly RegExp[] field-shape leg + the three pinned per-provider pattern
arrays (claude ^ANTHROPIC_/^CLAUDE_, codex ^OPENAI_/^CODEX_, opencode
^OPENCODE_). The P9 matrix assertions (TEST-PV-020..023) stay green (additive).
RED: the field + patterns do not yet exist.

TEST-SS-051 SPEC-SS-002 SPEC-SS-020 NFR-SS-008

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ss): T-SS-005 ProviderDescriptor.environmentKeyPatterns additive field

Append the OPTIONAL environmentKeyPatterns?: readonly RegExp[] field + the
three pinned frozen pattern arrays (claude ^ANTHROPIC_/^CLAUDE_, codex
^OPENAI_/^CODEX_, opencode ^OPENCODE_), each Object.freeze'd. The P9
frozen-matrix suite (TEST-PV-020..023) stays fully green (capabilities/freeze/
order/predicates unchanged). Also drops three redundant `as unknown` casts in
the T-SS-002 RED test to satisfy no-unnecessary-type-assertion.

TEST-SS-051 SPEC-SS-002 SPEC-SS-020 NFR-SS-008

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ss): T-SS-006 RED EnvSnippet shape + codec + parseContextLimit

The EnvironmentScope/EnvEntry/EnvSnippetStruct shapes, parseEnvironmentVariables
byte-parity (comments/blank/export/first-=/quote/empty-key), serializeEnvEntries
(inline verbatim + secretRef MASKED, never resolved), parseContextLimit (k/m
multiplier + [1_000,10_000_000] bounds + null-on-invalid + never-throws). RED:
src/domain/chat/environment/EnvSnippet.ts does not yet exist.

TEST-SS-060 TEST-SS-067 SPEC-SS-003 EC-SS-12

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ss): T-SS-007 EnvSnippet shape + codec + parseContextLimit + barrel

src/domain/chat/environment/EnvSnippet.ts: EnvironmentScope/EnvEntry/
EnvSnippetStruct shapes; PURE parseEnvironmentVariables (byte-parity);
serializeEnvEntries (inline verbatim, secretRef MASKED never resolved);
parseContextLimit (k/m multiplier, [1_000,10_000_000] bounds, null-on-invalid,
total). Re-exported from the new environment barrel. All total — never throw.

TEST-SS-060 TEST-SS-067 SPEC-SS-003 EC-SS-12

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ss): T-SS-008 RED classifyEnvKey + SHARED_ENVIRONMENT_KEYS + isSecretEnvKey

The 13-key shared set (verbatim), descriptor-driven classifyEnvKey (shared-known
/ provider-pattern / shared-unknown, empty-key fallback, total), isSecretEnvKey
(provider-owned auth-suffix OR markSecret), and a source guard asserting no
switch(providerId)/=== branch. RED: classifyEnvKey.ts does not yet exist.

TEST-SS-051 SPEC-SS-002 REQ-SS-051 REQ-SS-066 NFR-SS-008 EC-SS-3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ss): T-SS-009 classifyEnvKey + SHARED_ENVIRONMENT_KEYS + isSecretEnvKey + barrel

src/domain/chat/environment/classifyEnvKey.ts: the 13-key SHARED_ENVIRONMENT_KEYS
(verbatim), EnvKeyOwnership union, PURE classifyEnvKey (descriptor-pattern
iteration, no provider-id branch), PURE isSecretEnvKey (provider-owned
auth-suffix OR markSecret). Re-exported from the environment barrel. Also fixes
the RED source-guard test to resolve the module via node:path (file URL scheme)
and rewords the doc comment so the no-switch grep does not match the comment.

TEST-SS-051 SPEC-SS-002 REQ-SS-051 REQ-SS-066 NFR-SS-008 EC-SS-3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ss): T-SS-010 RED keyboardNav parseNavMappings/buildNavMappingText

The canonical map-text render, the valid w/s/i round-trip (inverse of
buildNavMappingText), and each error class (non-map / unknown action /
multi-char / non-unique case-insensitive / duplicate action / missing action) →
{error}, nothing persisted, never throws. RED: keyboardNav.ts does not yet exist.

TEST-SS-070 TEST-SS-071 SPEC-SS-005 REQ-SS-070 REQ-SS-071 EC-SS-7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ss): T-SS-011 keyboardNav parseNavMappings/buildNavMappingText + barrel

src/domain/settings/keyboardNav.ts: NAV_ACTIONS/NavAction/NavMappings, PURE
buildNavMappingText, PURE parseNavMappings (each line map <single-char> <action>;
rejects unknown action / multi-char / non-unique case-insensitive / duplicate
action / missing action → {error}; defaults w/s/i; NAV_MAPPING_INVALID_KEY i18n
key). Total — never throws. New src/domain/settings/index.ts barrel re-exports it.

TEST-SS-070 TEST-SS-071 SPEC-SS-005 REQ-SS-070 REQ-SS-071 EC-SS-7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ss): T-SS-003 six additive PluginSettings fields + coerce* + envSecretKey

Append the six OPTIONAL device-local fields (envSnippets/envScopes/keyboardNav/
providerDefaultModel/defaultPermissionMode/providerCliPath) to PluginSettings,
each ABSENT from DEFAULT_SETTINGS (exact-key byte-identity, mirroring
homeFsConsent). Add envSecretKey + the six pure/total coerce* helpers per the
SPEC-SS-001 load-or-default table (coerceKeyboardNav composes parseNavMappings;
coerceEnvSnippets composes the EnvEntry validators). The P9 frozen-matrix +
settings round-trip + core-settings stay green (37 tests). Whole-project vue-tsc
0; lint 0.

TEST-SS-092 TEST-SS-093 SPEC-SS-001 SPEC-SS-020 NFR-SS-001 NFR-SS-004

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ss): T-SS-012 RED envScope PURE scope routing

getEnvironmentReviewKeysForScope (out-of-scope review keys), inferEnvironmentSnippetScope
(single-scope infer), resolveEnvironmentSnippetScope (fallback only on no meaningful
content), getEnvironmentScopeUpdates (multi-key blob split + comment/blank decorator
attach + fallback bucket), the classifier-reuse no-switch guard, never-throws. RED:
envScope.ts does not yet exist.

TEST-SS-052 TEST-SS-053 TEST-SS-064 SPEC-SS-004 NFR-SS-008 EC-SS-4 EC-SS-14

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ss): T-SS-013 envScope PURE scope routing + barrel

src/domain/chat/environment/envScope.ts: EnvironmentScopeUpdate +
getEnvironmentReviewKeysForScope / inferEnvironmentSnippetScope /
resolveEnvironmentSnippetScope / getEnvironmentScopeUpdates, ported 1:1 from
providerEnvironment.ts:273-364 with throw-paths converted to total returns and
the per-provider branch replaced by classifyEnvKey (branch-free, NFR-SS-008).
The fallback bucket fires only on meaningful-but-unsplittable content. Total —
never throws. Re-exported from the environment barrel.

TEST-SS-052 TEST-SS-053 TEST-SS-064 SPEC-SS-004 NFR-SS-008 EC-SS-4 EC-SS-14

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ss): T-SS-001..013 DOMAIN-batch implementation-log + workflow-state close-out

Record the per-task impl-log entries (files/SHAs/spec refs/outcomes/deviations)
for the completed DOMAIN batch T-SS-001..013 and update workflow-state (stage 7
in-progress; DOMAIN done, APPLICATION/INFRA-PLUGIN/STYLES/WIRE-IN/GATE remain;
hand-off note with all 13 commit SHAs + the additivity proof + the flaky-UI-smoke
finding).

SPEC-SS-001..005 NFR-SS-001 NFR-SS-008 NFR-SS-011

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ss): T-SS-014 RED buildSettingsViewModel ordered capability-gated VM + SettingsControl union

Authors the failing unit suite for SPEC-SS-006/007: section ordering
[shared, enabled providers blank-tab-order, environment], the per-provider
capability-gated control visibility (apiKeyField tri-state, modelPicker empty
flag + preselect, mcpManager/mcpDocNote gate, slash/agent definition gate,
unconditional approvals/permissionMode/keyboardNav), the Claude-only additivity
baseline, the 14-member SettingsControl union (no secret value, read-only
members carry no onChange), and the no-switch(providerId) source guard.

TEST-SS-001/002/004/005/007/010/011/015/020/022/080/081/082/083/093.
SPEC-SS-006 SPEC-SS-007. NFR-SS-008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ss): T-SS-015 buildSettingsViewModel PURE capability-gated VM + SettingsControl union

Implements src/application/settings/buildSettingsViewModel.ts + the 14-member
SettingsControl discriminated union per SPEC-SS-006/007 (ADR-SS-002). The PURE,
total, deterministic VM emits ordered sections [shared, enabled providers in
blank-tab order, environment]; each section emits only the controls its
capability bag supports (apiKeyField tri-state from secretKeysSet/availability,
modelPicker empty + preselect, mcpManager else mcpDocNote, slash/agent gated on
the definition predicate, approvals/permissionMode/keyboardNav). No member
carries a secret value. Gating reads the capability bag + the registry's enabled
list + the descriptor enablement predicate — no switch(providerId) (NFR-SS-008).

SPEC-SS-006 SPEC-SS-007 SPEC-SS-016/020/021. NFR-SS-008. EC-SS-1/2/8/9/10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ss): T-SS-016 RED read-only agent/skill + slash discovery mapping (P4 catalog)

Authors the failing unit suite for SPEC-SS-008: command->slash {name,description}
+ skill->agent {name,description,kind} read-only mapping over the P4
ProviderCommandCatalogPort, load-or-default [] (never throws on a rejected
getEntries), no write affordance on the rows, and the hasProviderDefinitions
predicate (agent always false; slash/skill from the non-empty catalogs; omit
when both empty).

TEST-SS-030/031/040/041. SPEC-SS-008. EC-SS-9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ss): T-SS-017 discoverDefinitions read-only P4 discovery mapping + hasProviderDefinitions

Implements src/application/settings/discoverDefinitions.ts per SPEC-SS-008: maps
ProviderCommandCatalogPort.getEntries('command'|'skill') to the read-only
slashList {name,description} + agentList {name,description,kind} shapes, and
exposes makeHasProviderDefinitions building the hasProviderDefinitions(id)
predicate buildSettingsViewModel consumes (agent:false — no P9 seam; slash/skill
from the non-empty catalogs; agent list omitted when both empty). Load-or-default
via tryAsync — never throws (Result discipline, no raw try/catch).

SPEC-SS-008. REQ-SS-030/031/040/041. EC-SS-9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ss): T-SS-018 RED EnvSnippetService list/create/edit/remove/apply/applyScopeText/readScope

Authors the failing unit suite for SPEC-SS-009 over fake-ports (secretStore +
settings): the per-method contract, the secret-split (provider-owned auth +
markSecretKeys -> SecretStorePort under env.<scope>.<KEY>, struct keeps only a
secretRef), the name guard (nothing persisted), the zero-secret-bytes data.json
assertion, edit secret-slot reconcile, remove-both-stores + idempotence, apply
scope inference, applyScopeText split + out-of-scope review keys, the
masked-secretRef readScope (never resolved), the Result.err-on-failure with no
secret value substring, and the no-switch(providerId) source guard.

TEST-SS-052/053/060/061/062/063/064/066/067/090/094. SPEC-SS-009. NFR-SS-002/008.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ss): T-SS-019 EnvSnippetService composing SettingsPort + SecretStorePort (secret-split, Result-typed)

Implements src/application/settings/EnvSnippetService.ts per SPEC-SS-009
(ADR-SS-001): list/create/edit/remove/apply/applyScopeText/readScope composing
SettingsPort (the non-secret struct) + SecretStorePort (the secret values under
env.<scope>.<KEY>) behind a pure service holding the injected ProviderDescriptor[]
for the classifier. The secret split routes provider-owned auth keys + caller
markSecretKeys to setSecret + a {kind:'secretRef'} entry (struct keeps no
plaintext, zero secret bytes in data.json); non-secret -> {kind:'inline'}. Name
guard persists nothing; edit reconciles secret slots; remove clears both stores
idempotently; apply infers an undeclared scope; applyScopeText splits via
getEnvironmentScopeUpdates + returns the out-of-scope review keys; readScope keeps
secretRefs MASKED (resolved only at the subprocess boundary). Every method returns
Result (no throw, no secret substring in err); no switch(providerId) (NFR-SS-008).

SPEC-SS-009 SPEC-SS-018/019/022. REQ-SS-050..053/060..064/066/067/090/094.
NFR-SS-002/006/008. EC-SS-5/6/11/12/13/14.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ss): T-SS-019 RED env-scope -> subprocess-env resolution helper (resolveEnvScope/mergeScopeEnvs)

Authors the failing unit suite for SPEC-SS-013's pure composition the P9 runtimes
consume: resolveEnvScope reads an inline entry verbatim + a secretRef via
SecretStorePort.getSecret (the ONE place a secret is read), omits an absent
secret, errs (no value substring) when storage is unavailable; mergeScopeEnvs
composes {...base, ...shared, ...provider} with provider winning and propagates a
resolution failure as err.

TEST-SS-065. SPEC-SS-013. REQ-SS-065. NFR-SS-002. EC-SS-15.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ss): T-SS-019 resolveEnvScope/mergeScopeEnvs env-scope -> subprocess-env helper

Implements src/application/settings/resolveEnvScope.ts per SPEC-SS-013: the pure
Result-typed composition the P9 provider runtimes consume at turn start.
resolveEnvScope resolves a scope's EnvEntry[] into Record<string,string> — inline
verbatim, secretRef via SecretStorePort.getSecret at the boundary (the one place a
secret value is read; absent -> omitted; failure -> err with no value substring).
mergeScopeEnvs composes {...base, ...shared, ...provider} in that precedence. The
resolved value is returned only to be merged into the subprocess env — never into a
DTO/notice/log (SPEC-SS-019). No throw across the port.

SPEC-SS-013. REQ-SS-065. NFR-SS-002. EC-SS-15.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ss): T-SS-014..019 APPLICATION-batch implementation-log + workflow-state close-out

Records the APPLICATION batch (buildSettingsViewModel + SettingsControl union,
discoverDefinitions + hasProviderDefinitions, EnvSnippetService secret-split,
resolveEnvScope/mergeScopeEnvs) — files, commit SHAs, spec refs, outcomes,
deviations. Sets implementation-log.md in-progress (T-SS-020..035 remain) and the
Stage 7 row + hand-off note to the INFRA batch.

SPEC-SS-006/007/008/009/013. TASKS-SS-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ss): T-SS-020 RED _coerceSettings six-field round-trip + Mock/LS env-slot SecretStore + Mock runtime env-capture

RED for SPEC-SS-012/014/019, TEST-SS-065/066/091/092. The six additive OPTIONAL
PluginSettings fields round-trip a save->fresh-bridge reload via the six coerce*
calls (present only when present; absent/garbage -> absent, no migration); the
Mock SecretStore env.<scope>.<KEY> slot round-trips through the generic key/value
store + the availability switch; the each-setting-in-its-correct-store routing;
the Mock runtime env-capture (MockProviderEnvCapture) records the merged subprocess
env via mergeScopeEnvs (inline as-is + secretRef resolved at the boundary, never logged).
REQ-SS-015/065/066/091/092; NFR-SS-001/002/004/007.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ss): T-SS-021 _coerceSettings six-field round-trip + Mock runtime env-capture

Implements SPEC-SS-012/014. Both write-path twins (ObsidianBridge._coerceSettings +
core-settings.validateSettings) round-trip the six additive OPTIONAL P10 fields via
the shared pure coerceOptionalSettingsFields helper (PluginSettings.ts), conditionally
spread so an unrecorded field stays ABSENT (byte-identical P9, the homeFsConsent
pattern). The helper keeps both methods under the complexity budget + dedupes the
assembly. Adds MockProviderEnvCapture (the automated leg for the env->subprocess merge)
recording { ...base, ...resolve(shared), ...resolve(provider) } via mergeScopeEnvs
(inline as-is + secretRef resolved at the boundary only, never logged). Mock/LS
SettingsPort + SecretStore env.<scope>.<KEY> slots round-trip unchanged (generic
key/value). REQ-SS-015/065/066/091/092; NFR-SS-001/002/004/007; no migration (NG8).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ss): T-SS-022 env->subprocess merge leg (TEST-SS-065) over MockProviderEnvCapture

The automated merge leg for SPEC-SS-013 / REQ-SS-065 / EC-SS-15: drives the runtime
turn-start composition { ...process.env, ...resolve(envScopes.shared),
...resolve(envScopes[provider:<id>]) } through MockProviderEnvCapture over a
settings-shaped envScopes record + an in-memory SecretStore. Asserts the precedence
order (provider > shared > base), the inline-as-is + secretRef-resolved-at-boundary
merge, and that the resolved secret value reaches only the captured env (never the
settings record / a DTO / a log, NFR-SS-002). Pass-as-guard for the established merge
composition; the real subprocess injection is the coverage-excluded manual leg TEST-SS-M2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ss): T-SS-023 env->subprocess merge wired into the P9 runtimes

Implements SPEC-SS-013 / REQ-SS-065 / EC-SS-15. New buildScopeEnv helper
(coverage-excluded obsidian/**) reads the applied envScopes off the SettingsPort and
merges { ...base, ...resolve(shared), ...resolve(provider:<id>) } over the runtime's
spawn env via the application mergeScopeEnvs — the ONE place an env-scope secret is
read (secretRef -> getSecret at the spawn boundary only, never logged, NFR-SS-002).
ClaudeCliChatRuntime / CodexRuntime / OpencodeRuntime each gain an optional
settings?: SettingsPort dep and call buildScopeEnv at the spawn boundary; the
ObsidianProviderRuntimeRegistry threads deps.settings to each builder and ObsidianBridge
wires settings: this. Total — never throws: a settings/secret read failure degrades to
the unmodified base env. Optional deps -> absent leaves the P9 env byte-identical
(NFR-SS-001). The real injection is the coverage-excluded manual leg TEST-SS-M2; the
automated leg is MockProviderEnvCapture (TEST-SS-065). No new obsidian/** banned-glob
file; no shell:true/eval.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ss): T-SS-024 no-secret-leak + correct-store + Result-boundary invariant guards

The automated gate guards for SPEC-SS-019/022/026, TEST-SS-014/090/091/094. secretLeak.test.ts:
across every key + snippet + scope flow ZERO secret bytes appear in the device-local
SettingsPort blob (the counter-metric, TEST-SS-090); each setting in its correct store
(secrets -> SecretStorePort under provider.<id>.apiKey + env.<scope>.<KEY>; device prefs
-> SettingsPort, TEST-SS-091); readScope returns a secretRef MASKED, the resolved value
never echoes back (TEST-SS-014, NFR-SS-002). resultBoundary.test.ts: a failed store
write -> Result.err with NO secret value substring + no throw across a port; the service
stays operable after a failure (TEST-SS-094, EC-SS-13). Pass-as-guard for the established
invariants, recorded as the gate baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ss): T-SS-020..024 INFRA-batch implementation-log + workflow-state close-out

Records the five INFRA tasks (the _coerceSettings six-field round-trip via the shared
coerceOptionalSettingsFields helper in both write-path twins; the Mock/LS env-slot
SecretStore + Mock runtime env-capture; the env->subprocess merge wired into the 3 P9
runtimes via buildScopeEnv; the no-secret/correct-store/Result-boundary guards) with
per-task SHAs, verification, and deviations. Stage 7 implementation-log.md stays
in-progress: PLUGIN T-SS-025..026 / STYLES / WIRE-IN / GATE + manual legs TEST-SS-M1..M4
remain. SPEC-SS-012/013/014/019/022.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ss): T-SS-025 render the settings view-model in SpecoratorSettingTab

Implements SPEC-SS-010 (+ SPEC-SS-007/021/023/026). Grows the slim P0
SpecoratorSettingTab additively: keeps the module-schema core loop, then walks
buildSettingsViewModel and renders each SettingsControl via the Setting API /
createEl / setText (safe DOM only). The renderer switches on control.kind (the
ONE allowed switch, never on providerId); each onChange wires its narrow port /
EnvSnippetService and surfaces a Result.err as a NotificationPort notice.
Coverage-excluded src/plugin/** -> manual leg TEST-SS-M1.

REQ-SS-001..005/010..015/020..022/030/040/050/060..064/070/080..083/094/095.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ss): T-SS-026 add env-snippet edit + delete-confirm modals

Implements SPEC-SS-011. createSnippetEditLauncher returns the SnippetEditLauncher
the settings tab drives: an Obsidian Modal hosting the snippet editor
(name/description/env/scope) wired to EnvSnippetService.create/edit, and a
separate delete-confirm Modal wired to remove (struct + secret slots). An empty
name shows the nameRequired notice and does not close/persist; safe DOM only
(Setting/createEl/setText), no window.confirm. Coverage-excluded src/plugin/** ->
manual leg TEST-SS-M1.

REQ-SS-060/061/062/063/072/095.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ss): T-SS-027 mint the settings/* --sp-* token slice

Implements SPEC-SS-015 (NFR-SS-009). Adds the section 4.17 settings-shell token
block to tokens.css (four minted tokens: section-heading gap, list-row gap,
snippet item radius + background) covering the seven settings/* modules; each
value is a token-layer var() lookup with ASCII-only comments (lightningcss-safe).
Extends tokens.test.ts with the §4.17 presence + no-leak guard and bounds the
§4.16 block at the new marker. Folds the T-SS-028 token+additivity gate into the
DoD (the additivity serialisation leg is already green).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ss): T-SS-029 add no-switch + safe-DOM + i18n source guards

Implements TEST-SS-010/014/095 (SPEC-SS-021/023/026). Source-level guards over
the settings shell: (a) zero switch(providerId)/provider-id equality across
src/application/settings/** + src/domain/chat/environment/** and the renderer
switches on control.kind only; (b) no innerHTML/insertAdjacentHTML + no blocking
window.confirm/alert/prompt in the settings tab + the snippet modals; (c) every
notification call goes through t(...) (no raw literal, no secret/env value).
Pass-as-guard for the established invariants.

REQ-SS-010/014/095.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ss): T-SS-030 register the expanded settings tab in main.ts

Implements SPEC-SS-010 (+ SPEC-SS-007/009/013). main.ts now constructs
SpecoratorSettingTab with a SettingsTabDeps bundle assembled from the
ObsidianBridge ports (registry/secret/catalog/mcp/approvals/command-catalog)
plus a composed EnvSnippetService (SettingsPort + SecretStorePort +
PROVIDER_DESCRIPTORS, no new port) and the env-snippet modal launcher. The
standalone browser entry is unaffected (no settings tab there). The real-Obsidian
DOM render is the deferred manual leg TEST-SS-M1.

REQ-SS-001/050/065/080/082/083.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ss): record T-SS-030 commit SHA in implementation-log

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ss): Stage 7 close-out for the PLUGIN+STYLES+WIRE-IN batch (T-SS-025..030)

Records T-SS-025..030 complete in workflow-state.md (implementation-log.md stays
in-progress: GATE T-SS-031..035 + manual legs TEST-SS-M1..M4 remain) + the
hand-off note (SHAs, verification, remaining owner, next agent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ss): R-SS-001 populate the read-only slash/agent lists with discovered entries

Stage-9 review (REVIEW-SS-001, approve-with-conditions) found agentList/slashList
emitting entries:[] hardcoded — discoverDefinitions produced the names but only the
boolean presence reached the VM, so the read-only lists always rendered empty
(REQ-SS-030). Add an optional getProviderDefinitions(id) to the VM input carrying the
slash/agent entries (the presence gate via hasProviderDefinitions unchanged); emit them
via a buildDefinitionListControls helper (keeps buildProviderSection under the complexity
cap); makeGetProviderDefinitions maker + plugin wiring. New test asserts the lists
populate. Plus the Stage-9 review.md + traceability.md (0 P1/P2; R-SS-002/003 low,
non-blocking). typecheck 0, full lint 0, settings tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build(ss): regenerate styles.css with §4.17 settings tokens

Shipped CSS rebuilt for P10 — the settings-shell + env-snippet token slice. Gate
green: typecheck 0, lint 0, vitest 286 files/2129 passed (0 errors) + the R-SS-001
fix, build + build:web + docs:api clean, npm audit (high) clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Luis Mendez <hallo@luis-mendez.de>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…W) (#452)

* chore(il): bootstrap P11 i18n-locales feature + workflow-state

Cut feature/i18n-locales off next (P0-P10 merged). Scope = parity-charter §3.9 — add
the 8 missing locales (es/fr/ja/ko/pt/ru/zh-CN/zh-TW) matching en's keyset exactly;
widen SupportedLocale/SUPPORTED_LOCALES/messages/toSupportedLocale to 10; generalise
the en↔de key-parity test to all-10-against-en. Translate from our en.ts (claudian
JSONs = wording reference). Largely mechanical + additive. Autonomous; SPLIT the locale
impl into 2-3-locale chunks (timeout lesson). Next: /spec:requirements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(il): P11 requirements PRD-IL-001 (i18n-locales, accepted)

12 REQ-IL + 9 NFR-IL (concise — mechanical phase). 10 locales registered/selectable;
all-10 leaf-keyset parity against en (generalise the en↔de test); toSupportedLocale
narrows the 10 incl. zh-CN/zh-TW (unknown→en, fallback en); placeholder multiset
preserved (CLAR-IL-001 → dedicated test); forbidden-terms guard green for all locales;
en/de byte-identical; missing key → en fallback no crash; build succeeds (note bundle
delta). Keyset = our en.ts; claudian JSONs = wording reference only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(il): P11 design DESIGN-IL-001 + spec SPEC-IL-001..009 (no new ADR)

Additive 2→10 widening: SupportedLocale union + SUPPORTED_LOCALES + messages
registration to all 10; toSupportedLocale body UNCHANGED (membership test auto-extends,
covers zh-CN/zh-TW, unknown→en). 8 new locales/<code>.ts mirror de.ts (en's exact keyset,
placeholders preserved, claudian wording). All-10 key-parity test (snapshot-at-load) +
placeholder-multiset test + forbidden-terms-all-10. en/de byte-identical; fallback→en.
EC-IL-001..010. Chunks: es/fr/pt · ja/ko · zh-CN/zh-TW/ru · wiring+tests. No new ADR/port.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(il): P11 tasks TASKS-IL-001 (13 tasks, mechanical, no guard-relax)

T-IL-001 baseline+guard-verify (NO new key/port — additive string-union widen);
locale-file chunks es/fr/pt · ja/ko · zh-CN/zh-TW/ru; wiring widen index.ts (4 sites);
test tasks all-10 key-parity (snapshot-at-load) + placeholder-multiset + forbidden-terms-
all-10 + narrowing/fallback; gate (full verify + bundle delta + draft PR). Native-speaker
polish deferred (non-gating). Orchestrator note: land the 8 locale FILES before the
index widen so typecheck stays green (importing absent files would break it), then
wiring+tests, then gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(il): T-IL-007 es locale catalog

Add Spanish (es) translation of en.ts under src/ui/i18n/locales/es.ts —
exact en keyset (226 leaves, 0 missing / 0 extra), every leaf an idiomatic
Spanish string, every {token} interpolation placeholder preserved verbatim.
Mirrors de.ts file shape; index.ts wiring deferred to T-IL-002.

SPEC-IL-002, SPEC-IL-003, REQ-IL-002, REQ-IL-007, REQ-IL-008. Language: Spanish.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(il): T-IL-007 fr locale catalog

Add French (fr) translation of en.ts under src/ui/i18n/locales/fr.ts —
exact en keyset (226 leaves, 0 missing / 0 extra), every leaf an idiomatic
French string, every {token} interpolation placeholder preserved verbatim.
Mirrors de.ts file shape; index.ts wiring deferred to T-IL-002.

SPEC-IL-002, SPEC-IL-003, REQ-IL-002, REQ-IL-007, REQ-IL-008. Language: French.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(il): T-IL-007 pt locale catalog

Add Portuguese (pt) translation of en.ts under src/ui/i18n/locales/pt.ts —
exact en keyset (226 leaves, 0 missing / 0 extra), every leaf an idiomatic
Portuguese string, every {token} interpolation placeholder preserved verbatim.
Mirrors de.ts file shape; index.ts wiring deferred to T-IL-002.

SPEC-IL-002, SPEC-IL-003, REQ-IL-002, REQ-IL-007, REQ-IL-008. Language: Portuguese.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(il): T-IL-007 record es/fr/pt catalog implementation log

Create specs/i18n-locales/implementation-log.md with the Romance-chunk entry
(es/fr/pt): 226-leaf parity vs en.ts, 0 placeholder mismatches, 0 forbidden-term
offenders, claudian wording reference, per-locale commit SHAs.

SPEC-IL-003, REQ-IL-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(il): T-IL-008 ja locale catalog

Add the Japanese (ja) locale catalogue: a full `export default {…} as const`
mirror of `en.ts` with all 226 leaves translated. Placeholders preserved
verbatim per key; brand tokens (Specorator/Claude/Codex/Opencode/MCP) carried
through; "API キー" confined to the whitelisted settings./provider secret keys.

Trace: SPEC-IL-002, REQ-IL-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(il): T-IL-008 ko locale catalog

Add the Korean (ko) locale catalogue: a full `export default {…} as const`
mirror of `en.ts` with all 226 leaves translated. Placeholders preserved
verbatim per key; brand tokens (Specorator/Claude/Codex/Opencode/MCP) carried
through; "API 키" confined to the whitelisted settings./provider secret keys.

Trace: SPEC-IL-002, REQ-IL-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(il): T-IL-008 log ja + ko locale catalogues

Append the T-IL-008 CJK section to the implementation log: per-locale commit
SHAs, 226-leaf parity, placeholder/forbidden-terms verification, claudian
wording reference.

Trace: SPEC-IL-002, REQ-IL-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(il): T-IL-009 zh-CN locale catalog

Full Simplified Chinese (zh-Hans) translation of en.ts: 226-leaf keyset
exact match, every {placeholder} preserved verbatim, brand tokens and the
whitelisted "API key" strings carried per en.

Implements SPEC-IL-002. Traces REQ-IL-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(il): T-IL-009 zh-TW locale catalog

Full Traditional Chinese (zh-Hant) translation of en.ts: 226-leaf keyset
exact match, every {placeholder} preserved verbatim, brand tokens and the
whitelisted "API key" strings carried per en. Diverges from zh-CN where
Simplified/Traditional and Taiwan UI idiom differ.

Implements SPEC-IL-002. Traces REQ-IL-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(il): T-IL-009 ru locale catalog

Full Russian translation of en.ts: 226-leaf keyset exact match, every
{placeholder} preserved verbatim, brand tokens and the whitelisted
"API key" strings carried per en.

Implements SPEC-IL-002. Traces REQ-IL-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(il): T-IL-009 log zh-CN + zh-TW + ru locale catalogues

Records the three CJK + Cyrillic catalogue commits with keyset-parity,
placeholder, forbidden-terms, and zh-CN/zh-TW divergence evidence.

Implements SPEC-IL-002. Traces REQ-IL-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(il): T-IL-001 baseline + guard-verify note in test-plan.md

Record the en.ts keyset (226 leaves), the placeholder inventory (35
interpolating leaves, 17 distinct tokens), the all-9-non-en parity +
placeholder + forbidden-terms baseline (0 missing/extra/mismatch/offenders),
the claudian WORDING-only reference + the deferred native-speaker-polish leg
(T-IL-012, P12), the two-locale bundle-baseline note (delta recorded by
T-IL-013), and the guard verdict: NO new InjectionKey/port, NO guard-relax,
en/de/manifest untouched. No src/ change.

Implements TEST-IL targets. SPEC-IL-003/004/005/006/007/009 §1-7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(il): T-IL-002 widen i18n index to 10 locales

Widen the four declaration sites in src/ui/i18n/index.ts from 2 → 10:
SupportedLocale union ('en'|'de'|'es'|'fr'|'ja'|'ko'|'pt'|'ru'|'zh-CN'|
'zh-TW'); SUPPORTED_LOCALES array (en first, de second, alphabetical, zh-*
last); the eight catalogue imports; the ten-entry messages map (quoted
'zh-CN'/'zh-TW' keys), keeping the `as unknown as Record<SupportedLocale,
MessageSchema>` cast and fallbackLocale: 'en'. toSupportedLocale body
byte-unchanged (narrows via SUPPORTED_LOCALES → auto-extends to all ten incl.
the regional tags; unknown → 'en'); the stale doc-comment example fr → it.

No new function/port/InjectionKey/composable; no obsidian/node:* import.

Implements SPEC-IL-001, SPEC-IL-002. REQ-IL-001/005/006, NFR-IL-005.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(il): T-IL-003/004/006 all-ten parity + placeholder + narrowing tests

Generalise tests/ui/i18n/index.test.ts from en↔de to all ten locales — these
three test legs are co-located in the one shared file per SPEC-IL spec:

- T-IL-003 (TEST-IL-003/004, SPEC-IL-004): replace the en↔de key-parity block
  with a table-driven all-ten-against-en assertion over
  SUPPORTED_LOCALES.filter(c => c !== 'en'); snapshot each locale's leafKeys at
  module load (dodging the i18nMerge mutation); assert missing/extra both [] with
  a locale+keys failure message.
- T-IL-004 (TEST-IL-008, SPEC-IL-005): placeholder-multiset test — per non-en
  locale, per en leaf key, value.match(/\{[^}]+\}/g) multiset === en's; failure
  names locale+key+diff; no-placeholder leaf passes trivially.
- T-IL-006 (TEST-IL-001/002/005/006/011, SPEC-IL-001/002/008): registration
  completeness (length 10 + SUPPORTED_LOCALES set deep-equals the messages-map
  key set + every entry non-empty), per-catalogue import shape, narrows-the-ten
  round-trip (incl. zh-CN/zh-TW), unknown→en (it/zh/''/EN/de-DE), and the
  synthetic missing-key fallback (en string, no throw).

The existing i18nMerge/flatToNested + agent.empty.placeholder tests are
unchanged. 51 tests green. REQ-IL-001..006/008/011, NFR-IL-001/002/004,
EC-IL-001..004/006..009.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(il): T-IL-005 run forbidden-terms guard over all ten locales

Generalise tests/i18n/forbidden-terms.test.ts from the en-only scan to a
table-driven scan over all ten catalogues (SUPPORTED_LOCALES). Reuse the
unchanged FORBIDDEN (/\bAPI key\b/i, /\bsubprocess\b/i, /\bSDK\b/i), flatten,
and isAllowed helpers; ALLOWED_PREFIXES is byte-unchanged from P9. Each locale
asserts zero offenders outside the allow-list with a locale+key+value+pattern
failure message; a note records that any ALLOWED_PREFIXES extension is a
defect-escalation (EC-IL-005), not a default. 10 locales scanned, 0 offenders.

Implements TEST-IL-009. SPEC-IL-006, REQ-IL-009, NFR-IL-003, EC-IL-005.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(il): T-IL-001..006 log entries + DoD ticks + Stage 7 close-out

Append Layer A implementation-log entries for T-IL-001 (baseline + guard
verdict), T-IL-002 (index widen), T-IL-003/004/006 (all-ten parity +
placeholder + registration/narrowing/fallback), T-IL-005 (forbidden-terms
all-ten) with their real commit SHAs; mark the prior catalogue entries'
"all-ten parity test does not exist yet" deviation note superseded. Tick the
T-IL-001..006 DoD boxes (the two-locale bundle baseline deferred to T-IL-013;
the RED-first boxes annotated GREEN-after-catalogues since T-IL-007..009 landed
first). Update workflow-state.md artifacts (implementation-log.md / test-plan.md
in-progress) + a Stage 7 hand-off note: vue-tsc 0, lint 0 errors, 61 i18n tests
green; gate chunk T-IL-010..013 remain.

SPEC-IL-001..009 §wiring+tests chunk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(il): P11 Stage-9 review (approve) + traceability

REVIEW-IL-001 verdict APPROVE (0 blockers). All-10 automated gates green (key-parity +
placeholder-multiset + forbidden-terms-all-10 + narrowing/fallback, 61 tests). Additivity
confirmed (en/de/manifest byte-identical; index widen = the 4 specced sites; toSupportedLocale
body unchanged). Spot-checked fr/zh-CN genuinely translated, brand tokens intact, zh-CN≠zh-TW.
permission.plan CJK-badge rendering non-blocking → native-polish (T-IL-012, deferred). TRACE-IL-001
all 12 REQ-IL chained. Native-speaker polish pending for the final epic gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Luis Mendez <hallo@luis-mendez.de>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…final reboot phase) (#453)

* chore(ay): bootstrap P12 accessibility feature + workflow-state (FINAL phase)

Cut feature/accessibility off next (P0-P11 merged). Scope = parity-charter §3.10
accessibility.css (reduced-motion/forced-colors/focus-visible/sr-only) + a11y behaviour
polish across all P1-P11 surfaces + WCAG 2.2 AA sweep. The final parity screenshot
sign-off (all surfaces) is HUMAN-owned — the accumulated P5-P11 manual legs converge.
No new port/ADR expected. After P12 merges: present final review + open (don't merge)
next→develop (the original /goal end-state). Next: /spec:requirements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ay): P12 requirements PRD-AY-001 (accessibility, accepted)

17 REQ-AY + 10 NFR-AY. accessibility.css 3rd global layer (reduced-motion guard over
the 5 keyframes + forced-colors system-color mapping + :focus-visible ring + .sr-only)
registered in both CSS pipelines; a11y behaviour sweep (ARIA/labels/live-regions/keyboard);
modal focus trap+restore at the P5/P7/P8/P10 seams; WCAG 2.2 AA bar. REQ-AY-001..016
automatable (TEST-AY-*); REQ-AY-017 = the HUMAN final parity screenshot sign-off (all
surfaces, the single final epic gate, not self-claimed). Additive (no surface regresses).
CLAR-AY-001 (read the actual claudian accessibility.css at design) + CLAR-AY-002 resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ay): P12 design DESIGN-AY-001 + spec SPEC-AY-001..011 (no new ADR)

claudian accessibility.css = 41 lines, focus-rings only (meet that; beat with the rest).
accessibility.css = 6 .specorator-root-scoped rule groups (reduced-motion global guard +
explicit spin halt + forced-colors system-color mapping + forced-colors visible borders +
:focus-visible ring reusing existing --sp-focus-ring/--sp-shadow-focus-ring + .sr-only),
lightningcss-safe; joins tokens.css+animations.css at both import sites. Behaviour-fixes
mostly verify-only (ChatSurface aria-live + TabBar tablist + Obsidian Modal native focus
trap already present) + targeted fills (aria-expanded, icon-only labels, RG-4 selectors).
TEST-AY-001..016 automatable; TEST-AY-017 = HUMAN final parity screenshot sign-off (the
single final epic gate). No new port/ADR. Chunks: css+registration · behaviour sweep · tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ay): P12 tasks TASKS-AY-001 (18 tasks, FINAL phase, no guard-relax)

T-AY-001 baseline+guard-verify+parity-screenshots scaffold; C1 accessibility.css
(RG-1..6) + register both import sites + RED rule/registration tests (T-AY-002..005);
C2 behaviour-fix sweep RED + fills (aria-expanded/icon-labels/notice live-region/modal
trap verify, T-AY-006..013); C3 gate tests (additivity diff + no-raw-HTML scan +
screenshots-matrix, T-AY-014..016); GATE T-AY-018 (full verify + build:web + draft PR
into next) then T-AY-017 👤 HUMAN final parity screenshot sign-off (the single final
epic gate). No new key/port/ADR (CSS layer + additive ARIA; reuse --sp-focus-ring).
Post-merge: present final review + open (don't merge) next→develop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ay): T-AY-001 baseline + guard-verify + parity-screenshots scaffold

Scaffolds the test-plan baseline (token/import-site/reduced-motion/forced-colors
references + the claudian meet/beat split), the parity-screenshots.md matrix
(all charter §3 surfaces × 320/520/720 × light/dark + a11y-condition columns,
the human TEST-AY-017 sign-off artifact), and the implementation log. Records
the guard verdict: NO guard-relax, NO new InjectionKey/port/component/ADR,
manifest + locales untouched. No src/ change.

Implements REQ-AY-016. SPEC-AY-001/002/003/006, NFR-AY-002/004/008. TEST-AY-016.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ay): T-AY-003/004 RED rule-group + registration tests for accessibility.css

RED file-read tests: RG-1 reduced-motion guard, RG-2 explicit spin halt
(animation: none), RG-3 forced-colors system-colour mapping, RG-5 :focus-visible
ring consuming --sp-focus-ring (no bare :focus), RG-6 .sr-only clip technique,
the CSS discipline scan (no hex / no raw var outside forced-colors, ASCII
comments). RED registration test: accessibility.css imported as the 3rd CSS
import after tokens + animations at both src/plugin/main.ts and src/ui/main.ts.
RED until T-AY-002 (file + imports absent).

Implements TEST-AY-001/002/003/004/005/007/009/015. SPEC-AY-001/002/003/011,
REQ-AY-001/002/003/004/005/007/009/015, NFR-AY-002/006, EC-AY-001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ay): T-AY-002 add accessibility.css (RG-1..RG-6) + register at both CSS entries

New src/ui/styles/accessibility.css with the six rule groups, .specorator-root
scoped, ASCII-only lightningcss-safe comments, no hex / no raw non --sp-* var
outside the forced-colors block:
- RG-1 reduced-motion global guard (!important, the only !important in the file)
- RG-2 explicit spin halt (animation: none, not a near-zero duration)
- RG-3 forced-colors surface mapping (forced-color-adjust + system colours)
- RG-4 forced-colors border guarantee (T-AY-005 enumerates the full selector list)
- RG-5 :focus-visible ring reusing --sp-focus-ring / --sp-shadow-focus-ring (no new token)
- RG-6 .sr-only clip utility (never display:none / visibility:hidden)
Registered as the 3rd CSS import after tokens + animations at src/plugin/main.ts
and src/ui/main.ts; vite.config.ts unchanged. Greens TEST-AY-001/002/003/004/005/
007(file)/009(file)/015(css); folds in the RED test-helper corrections (assertions
unchanged). vue-tsc 0, lint 0 errors, build:web lightningcss green.

Implements SPEC-AY-001/002/003. REQ-AY-001/002/003/004/005/006/007/009/015,
NFR-AY-002/005/006. TEST-AY-001/002/003/004/005/007/009/015.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ay): T-AY-005 enumerate RG-4 forced-colors-border selectors

Completes the RG-4 background-cue-only control enumeration per SPEC-AY-006: the
toggle switch (.sp-toggle-switch), state pills ([data-state]), file/image chips
(.sp-chip), tab badges (.sp-tab), and the selected dropdown option
([role=option][aria-selected=true]) each gain border: 1px solid currentColor
inside the forced-colors block, so each stays distinguishable under HCM. Adds
the TEST-AY-006 file-read leg asserting the enumeration. Inert outside
forced-colors (default render unchanged). vue-tsc 0, lint 0 errors.

Implements SPEC-AY-006, SPEC-AY-001 (RG-4). REQ-AY-006, NFR-AY-009, EC-AY-003.
TEST-AY-006 (file leg).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ay): tick T-AY-001..005 + Chunk-1 close-out in workflow-state

Marks Chunk 1 (accessibility.css + registration) complete: ticks the T-AY-001..005
DoD checkboxes, sets implementation-log.md + test-plan.md to in-progress, moves
stages 7/8 to in-progress, records the Chunk-1 hand-off note (commit SHAs + the
green vue-tsc/lint/test/build:web results + the remaining Chunk 2/3/gate work).

SPEC-AY-001/002/003/006. T-AY-001..005.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ay): T-AY-006 forced-colors RG-4 controls mount leg + real selectors

Adds the TEST-AY-006 mount leg: mounts the RG-4-listed background-cue-only
controls (tab badge / service-tier switch / file chip / image thumb /
selected permission option) and asserts each exists in the rendered DOM, and
that RG-4 enumerates a selector matching each real swept control.

The mount leg surfaced that the T-AY-005 RG-4 enumeration used placeholder
selectors (.sp-toggle-switch / .sp-chip) that match no real component. Corrects
RG-4 to the real swept-component selectors: [role="switch"] (the toggle
switches), .sp-file-chips__chip / .sp-image-thumb (the chips), [data-state]
(state pills), .sp-tab (tab badges), [role="option"][aria-selected="true"]
(selected option). Additive, forced-colors-only — no default-render change.
Updates the TEST-AY-006 file-read leg to the real selectors.

Implements SPEC-AY-006. TEST-AY-006. REQ-AY-006, NFR-AY-009, EC-AY-003.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ay): T-AY-007 focus-visible reachability + keyboard operability + names

Adds the TEST-AY-007 mount leg + TEST-AY-008: mounts the audited toolbar /
composer / chat controls (tab badge/close/new, composer textarea/attach/send,
file chip link/remove, image thumb preview/remove, service-tier + permission
toggles) and asserts each matches the RG-5 focus-visible target selector (so the
keyboard ring reaches it) and exposes a non-empty accessible name. Verify-only:
every audited control already carries an aria-label / role / tabindex (the
per-phase a11y sweep was thorough) so no icon-only-label fill is needed.

Implements SPEC-AY-007, SPEC-AY-008. TEST-AY-007, TEST-AY-008. REQ-AY-007/008,
NFR-AY-001, EC-AY-005, EC-AY-006.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ay): T-AY-008 RED live-region presence + severity (busy + notice host)

Adds the TEST-AY-010 mount leg. Two legs:
- busy region (verify-only): ChatSurface streaming busy region carries
  aria-live="polite" + role="status" (passes against current code).
- notice host (RED): asserts a NoticeLiveRegion announces error=assertive
  (role=alert) / info=polite (role=status) via an aria-live region that mirrors
  the notice text declaratively, without stealing focus. RED until T-AY-011
  lands NoticeLiveRegion.vue (driven by the existing sp:notice channel).

Adds a busyRole() accessor to ChatSurface.po.

Implements SPEC-AY-004. TEST-AY-010. REQ-AY-010, NFR-AY-001, EC-AY-011/012.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ay): T-AY-011 standalone notice-host live region (error assertive/info polite)

Adds NoticeLiveRegion.vue: a visually-hidden (.sr-only) ARIA live region that
announces non-blocking notices to screen readers in the standalone / GitHub
Pages host. It subscribes to the existing sp:notice CustomEvent channel that
LocalStorageBridge already dispatches (no new port, no new channel). Error
notices map to aria-live="assertive" + role="alert"; info/success/warning map
to polite + role="status". The notice text is bound declaratively as {{ }}
text (no innerHTML/v-html). The region is passive (never calls .focus()), so an
announcement never steals focus. The .sr-only clip gives zero visible footprint
-> the default render stays additive (REQ-AY-014).

Wires it into the standalone entry alongside ChatSurface inside ErrorBoundary.
The ChatSurface busy region (aria-live=polite + role=status) was already
present -> verify-only (T-AY-008 green leg).

Implements SPEC-AY-004. REQ-AY-010, NFR-AY-003, EC-AY-011/012.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ay): T-AY-009 collapsible aria-expanded flips + icon-only sr-only/label

Adds the TEST-AY-011 + TEST-AY-009 mount legs. Asserts (a) the SpCollapsible
header (reused by every rich block: tool call / thinking / subagent /
write-edit) exposes aria-expanded that flips on Enter/Space/click and an
accessible name; verified through a direct mount + through ToolCallBlock; and
(b) icon-only controls (file-chip-remove, image-thumb-remove) carry a non-empty
aria-label while their decorative glyph is aria-hidden="true". Verify-only:
every collapsible already binds aria-expanded + a dynamic aria-label, and every
icon-only control already labels itself -> no aria-expanded / icon-label fill
needed (T-AY-010/T-AY-012 are verify-only).

Implements SPEC-AY-005, SPEC-AY-007. TEST-AY-011, TEST-AY-009. REQ-AY-009/011,
EC-AY-007.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ay): T-AY-013 modal focus trap + restore verify (8 modal seams)

Adds the TEST-AY-012/013 structural verify: asserts all 8 Specorator modal
seams (ProviderConsent, DeleteConfirm, ForkTarget, InstructionConfirm,
InlineEdit, ImagePreview, McpServerHost, McpTestHost) extend Obsidian Modal,
which natively traps Tab/Shift+Tab and restores document.activeElement on close
(D-AY-3). The live Tab-cycle + focus-restore is the human TEST-AY-017 leg / the
Obsidian runtime; this is the structural Modal-subclass property the JSDOM
harness can assert. Verify-only -> no hand-rolled trap. A modal NOT extending
Modal would be a defect-escalation (ADR-AY-001 + a new task); all 8 conform.

Adds a minimal Modal export to the shared obsidian test stub (additive) so the
extends chain forms for the structural verify.

Implements SPEC-AY-009. TEST-AY-012, TEST-AY-013. REQ-AY-012/013, EC-AY-008/009.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ay): T-AY-014 additivity invariant (no swept surface / locale regresses)

Adds the TEST-AY-014 additivity proof, the cardinal P12 counter-metric:
- locale output byte-identical to next (git diff --name-only next -- locales empty);
- manifest.json byte-identical to next (NFR-AY-008);
- the entire src/ diff vs next touches ONLY the P12 allow-list (accessibility.css,
  the two CSS-import entry edits, the new .sr-only NoticeLiveRegion) -> no swept
  component template under src/ui/chat / src/ui/agent / src/plugin/modals changed,
  so the P0-P11 default render is byte-identical at the source;
- representative swept components (TabBar, FileChips, ImageThumb, ChatComposer)
  keep their visible default-render structure (the aria-* attrs + the .sr-only
  clip do not alter the visible render).

Implements SPEC-AY-010. TEST-AY-014. REQ-AY-014, NFR-AY-004, EC-AY-010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ay): T-AY-015 discipline scan (no added raw-HTML sink in the P12 diff)

Adds the TEST-AY-015 diff leg (the CSS token/comment leg rides T-AY-003): scans
the added (+) lines of the P12 src/ diff vs next and asserts no
innerHTML/outerHTML assignment, no insertAdjacentHTML call, no v-html directive,
and no new eslint-disable of the raw-HTML / v-html guards. The additive ARIA
edits bind attributes declaratively and the .sr-only notice text renders as
{{ }} text -> the fills are declarative. Green against the actual diff.

Implements SPEC-AY-011. TEST-AY-015. REQ-AY-015, NFR-AY-003.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ay): T-AY-016 parity-screenshots.md completeness + complete the matrix

Adds the TEST-AY-016 artifact-completeness check: asserts parity-screenshots.md
lists every charter §3 surface (§3.1..§3.9) at 320/520/720 px in light + dark,
each paired with a claudian baseline + a Specorator capture leg, plus the two
a11y-condition columns (reduced-motion / forced-colors). Artifact-completeness
only — the visual judgment is the human TEST-AY-017 leg.

Marks the matrix complete (status: complete; the row/cell structure is fully
populated, baseline column filled from claudian); the Specorator + a11y-condition
cells are left for the human reviewer to populate + judge at T-AY-017.

Implements REQ-AY-016. TEST-AY-016.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ay): T-AY-006..016 implementation-log + tick tasks + Chunk-2/3 close-out

Logs Chunk 2 (T-AY-006..013) + Chunk 3 (T-AY-014..016): the forced-colors mount
leg + RG-4 real-selector correction, focus/keyboard/labels verify, the
live-region RED + the NoticeLiveRegion fill, the collapsible/icon-only verify,
the 8-modal-seam trap/restore verify, the additivity invariant, the discipline
scan, and the parity-screenshots completeness. Records T-AY-010 + T-AY-012 as
verify-only (no gap). Ticks the T-AY-006..016 DoD boxes; updates workflow-state
Stage-7 close-out (implementation-log in-progress: T-AY-017 human + T-AY-018
gate remain, parent-owned).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ay): P12 Stage-9 review (approve-with-nits) + traceability

REVIEW-AY-001 verdict approve-with-nits (0 P1/P2). accessibility.css 6 rule groups
(RG-1..6) present + scoped + registered both import sites; build:web lightningcss green.
Additivity proven: src/ diff = ONLY accessibility.css + plugin/main.ts + ui/main.ts +
NoticeLiveRegion.vue (no swept component/locale/manifest change). WCAG 2.2 AA met at the
automatable level (focus-visible, modal trap/restore native, forced-colors, reduced-motion,
aria-live, sr-only). No new port/ADR. TRACE-AY-001 REQ↔SPEC↔TEST chained; REQ-AY-017
HUMAN final parity screenshot sign-off recorded PENDING (the single final epic gate).
Nits R-AY-001/002 (doc-sync) low/non-blocking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build(ay): T-AY-018 regenerate styles.css with the accessibility layer

Shipped CSS rebuilt for P12 — the accessibility.css rules (reduced-motion guard,
forced-colors mapping/borders, :focus-visible ring, .sr-only) bundled in. FINAL-phase
gate green: typecheck 0, lint 0, vitest 296 files/2234 passed (0 errors), build +
build:web (lightningcss) + docs:api clean, npm audit (high) clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Luis Mendez <hallo@luis-mendez.de>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ay): make the P12 additivity/discipline diff tests CI-resilient

The T-AY-014 additivity + T-AY-015 discipline-scan tests shelled out to `git diff
next -- …`, which works in the dev worktree (local `next` branch) but ERRORS in CI's
shallow PR checkout (no local `next` ref) — green locally, red in the PR CI (#453's
unit job). Add a resolveBaseRef() that tries `next` then `origin/next` and returns
null when neither is reachable; the diff-based legs `it.skipIf(BASE_REF===null)` /
`describe.skipIf` so they run wherever a baseline exists (locally + base-fetched CI)
and skip gracefully otherwise — never error. The mount-based render-additivity checks
still run unconditionally. `BASE_REF!` (non-null) under the skip guard. 9/9 local;
typecheck + lint 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ay): guard discipline-scan git diff against a null baseline ref

describe.skipIf only skips the it() runs — the describe factory body still
executes at collection time, so addedSrcLines() ran `git diff <null> -- src`
in CI's shallow checkout (no `next`/`origin/next` ref) and crashed the suite.
Return [] when no baseline is reachable; the it bodies remain skipped.

TEST-AY-015

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Luis Mendez <hallo@luis-mendez.de>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d2e9b6bfce

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// is never in the update patch).
createdAt: tab.createdAt,
updatedAt: Date.now(),
providerId: 'claude',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist the active runtime provider

When the user selects Codex or Opencode, ChatSurface rebinds the active tab to a runtime whose providerId is no longer Claude, but every completed conversation is still saved with meta.providerId: 'claude'. That corrupts the history metadata for non-Claude chats and makes resume/history/provider-aware code see the wrong provider; build the record from the tab's active runtime providerId instead of hard-coding Claude.

Useful? React with 👍 / 👎.

Comment thread src/ui/i18n/index.ts
Comment on lines +34 to +41
'es',
'fr',
'ja',
'ko',
'pt',
'ru',
'zh-CN',
'zh-TW',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expose all registered locales in settings

These added locales are registered and accepted by toSupportedLocale, but the settings schema still only offers en and de in src/core/core-settings.ts. As a result, users cannot select Spanish/French/Japanese/etc. from the Settings UI despite the new catalogues, so the newly shipped locales are unreachable unless someone manually edits device-local storage; add matching dropdown options or derive them from SUPPORTED_LOCALES.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant