Conversation
* 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>
There was a problem hiding this comment.
💡 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', |
There was a problem hiding this comment.
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 👍 / 👎.
| 'es', | ||
| 'fr', | ||
| 'ja', | ||
| 'ko', | ||
| 'pt', | ||
| 'ru', | ||
| 'zh-CN', | ||
| 'zh-TW', |
There was a problem hiding this comment.
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 👍 / 👎.
Claudian-reboot epic — P0→P12 integration (
next→develop)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
nextintegration branch. 14 commits, 13 squash-merged phase PRs.ChatRuntime+ Claude CLI streaming chatApprovalManager+ permission modes + rule persistenceaccessibility.css+ WCAG 2.2 AA sweep (final phase)Charter constraints honored
app.secretStorageviaSecretStorePort, neverdata.json/ settings / logs / DTOs.HomeFsPortbeyond-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 P7ApprovalManager.minAppVersion1.12.7 left intentional (not bumped).innerHTML/v-html/window.confirm; raw-HTML + forbidden-term + locale key-parity guards enforced in CI.Verification
nextonly on a green gate (typecheck / lint / unit / build / build:web / docs:api) and green CI conclusion.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:
app.secretStorage.🤖 Generated with Claude Code