Skip to content

feat(ss): P10 settings shell — provider tabs + per-provider settings + env snippets#451

Merged
Luis85 merged 43 commits into
nextfrom
feature/settings-shell
May 26, 2026
Merged

feat(ss): P10 settings shell — provider tabs + per-provider settings + env snippets#451
Luis85 merged 43 commits into
nextfrom
feature/settings-shell

Conversation

@Luis85
Copy link
Copy Markdown
Owner

@Luis85 Luis85 commented May 26, 2026

P10 — Settings Shell (claudian-reboot)

The Obsidian settings-tab shell consolidating the P6–P9 seams + the env-snippet manager (charter §3.8). Spec: specs/settings-shell/ (PRD-SS-001, DESIGN-SS-001, SPEC-SS-001..028, ADR-SS-001/002, TASKS-SS-001 = 35 tasks).

Delivered (T-SS-001..030 + gate)

  • Pure buildSettingsViewModel → ordered, capability-gated SettingsSection[] (the 14-member SettingsControl union); no switch(providerId) (the one allowed switch is on control.kind). The expanded SpecoratorSettingTab renders it via the Obsidian Setting API (safe DOM, coverage-excluded) — each control's onChange calls its real port: provider enable/order + model default + permission mode (SettingsPort), per-provider API key (SecretStorePort, masked), MCP servers (McpConfigStorePort), approval rules (ApprovalRuleStorePort), env scopes/snippets (EnvSnippetService), read-only agent/skill/slash lists (P4 ProviderCommandCatalogPort).
  • Env-snippet manager (ADR-SS-001) — structure device-local via SettingsPort (6 additive OPTIONAL PluginSettings fields, _coerceSettings round-trips them), secret-bearing values via SecretStorePort under env.<scope>.<KEY> (struct holds only a secretRef; never in data.json); resolved into the provider subprocess env ONLY at the spawn boundary (buildScopeEnv/mergeScopeEnvs, wired into the 3 P9 runtimes). Edit/delete via Obsidian Modal (no window.confirm).
  • Keyboard-navigable native controls (WCAG 2.2 AA); settings/* --sp-* tokens; en+de i18n. The P0 module-schema core loop stays unchanged (additive). Claude-only = byte-identical P9.

Gate (local, green)

vue-tsc 0 · eslint 0 · vitest 286 files / 2129 passed (0 errors) + the R-SS-001 fix · build + build:web + docs:api clean · npm audit --audit-level=high clean.

Parity self-review (Stage 9)

review.md approve-with-conditions (0 P1/P2). Security confirmed: no secret in data.json, resolve-only-at-spawn, masked readScope, safe-DOM/no-window.confirm, no-switch(providerId). Live wiring + 6-field coerce round-trip + env→runtime merge confirmed; additivity (P0 core tab + P9 frozen-matrix green); read-only agent/skill (NG1). R-SS-001 (medium, the read-only lists rendered empty) FIXED — discovered entries now thread through the VM (+ regression test). R-SS-002/003 (lightweight MCP manager, scope-inference) low/non-blocking.

Deferred — final epic gate

Manual legs TEST-SS-M1 (real PluginSettingTab DOM render + keyboard-nav + modals), M2 (real subprocess env injection), M3 (real app.secretStorage env-secret round-trip + no-data.json proof), M4 (parity screenshots).

🤖 Generated with Claude Code

Symprowire and others added 30 commits May 26, 2026 19:43
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>
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>
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>
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>
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>
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>
… 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>
…erns 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>
… 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>
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>
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>
…ecretEnvKey

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>
…tEnvKey + 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>
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>
… 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>
…SecretKey

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>
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>
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>
…ate 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>
…d 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>
…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>
…g (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>
… + 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>
…y/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>
…torePort (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>
…(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>
…cess-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>
…ow-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>
… 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>
…e 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>
Symprowire and others added 13 commits May 26, 2026 23:03
…roviderEnvCapture

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>
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>
…nvariant 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>
…te 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>
…gTab

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>
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>
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>
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>
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>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-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>
…vered 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>
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>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fcfa04504c

ℹ️ 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".

Comment on lines +242 to +247
const enabled = input.registry.listEnabledProviders(input.settings);
const enabledIds = enabled.map((descriptor) => descriptor.id);

const sections: SettingsSection[] = [buildSharedSection(input.settings)];
for (const descriptor of enabled) {
sections.push(buildProviderSection(descriptor.id, input));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Render provider sections for disabled providers too

The view model is built only from registry.listEnabledProviders(settings), so provider sections (and their providerToggle) are created only for currently enabled providers. With DEFAULT_SETTINGS.enabledProviders set to [], Codex/Opencode never appear in settings, and if a user disables one it disappears with no UI path to re-enable it. This effectively makes provider enablement one-way from the settings shell.

Useful? React with 👍 / 👎.

Comment on lines +256 to +258
const byKey = new Map<string, EnvEntry>(existing.map((entry) => [entry.key, entry]));
for (const entry of additions) byKey.set(entry.key, entry);
scopes[scope] = [...byKey.values()];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace env scope entries instead of only merging additions

mergeScopeEntries seeds from existing scope entries and only overwrites keys present in the new text, which means keys removed from the scope editor are never deleted. In applyScopeText, clearing a variable (or the whole textarea) leaves stale entries in envScopes, and old secret-backed entries can remain active/injected even though the user removed them in the editor.

Useful? React with 👍 / 👎.

@Luis85 Luis85 merged commit c5d8b22 into next May 26, 2026
6 checks passed
@Luis85 Luis85 deleted the feature/settings-shell branch May 26, 2026 23:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants