feat!: close 7 feature-toggle gaps (user-scope-only, clean-break major version)#244
Conversation
Dream-Task: decisions Dream-Session: 29694b18-4416-44a3-a469-d4c8cf769749 Co-Authored-By: Devflow Dream <dream@devflow.local>
Dream-Task: knowledge Dream-Session: 29694b18-4416-44a3-a469-d4c8cf769749 Co-Authored-By: Devflow Dream <dream@devflow.local>
Add dual-signal gate in dream-evaluate so DECISIONS_ENABLED is folded down when .devflow/decisions/.disabled sentinel is present, regardless of dream config. This means eval-decisions and eval-curation both respect config AND sentinel with zero per-module plumbing. - dream-evaluate: fold sentinel into DECISIONS_ENABLED after config read - eval-curation: add DECISIONS_ENABLED guard as FIRST executable statement before .curation-last is read/written, preventing 7-day suppression after re-enable; use return not exit since file is sourced under set -e - eval-decisions: drop stale comment claiming sentinel is session-start-context only - dream-capture: mirror dual-signal for decisions-usage-scan via config + sentinel - dream-collect-tasks: sweep curation markers when decisions disabled; curation depends on decisions data; update header comment accordingly - tests/shell-hooks.test.ts: update AC-3 to reflect new contract where curation is swept when decEnabled=false rather than passed through Applies PF-007, ADR-001; avoids PF-013. Co-Authored-By: Claude <noreply@anthropic.com>
The .devflow/memory/.working-memory-disabled sentinel is dead code — no hook reads it at runtime since memory is config-only gated via dream config (ADR-001 clean-break). Remove the helper from all three copies of project-paths and add a per-project migration to clean up stray files. - src/cli/utils/project-paths.ts: remove getWorkingMemoryDisabledSentinel - scripts/hooks/lib/project-paths.cjs: remove function and its export entry - tests/project-paths.test.ts: remove import, assertion, and parity table entry (full-export parity test will now pass since both copies no longer export it) - tests/sentinel.test.ts Part E: repoint 4 .working-memory-disabled fixtures onto .devflow/decisions/.disabled (a live sentinel path that actually exists) - src/cli/utils/migrations.ts: add purge-dead-working-memory-sentinel-v1 per-project migration that unlinks the stale sentinel; ENOENT is no-op; mirrors purge-stale-memory-markers-v1 in shape - CLAUDE.md: update two approved spots per plan Applies ADR-001; avoids PF-002/PF-004 (idempotent, ENOENT-tolerant). Co-Authored-By: Claude <noreply@anthropic.com>
Previously hud --disable only wrote hud.json enabled:false but left the statusLine in settings.json, meaning the HUD statusLine continued to run even after "disabling" the HUD. This was a gap between hud.json state and the actual settings.json runtime configuration. Now --disable: 1. Writes hud.json enabled:false (existing) 2. Reads settings.json and calls removeHudStatusLine to remove any Devflow statusLine (surgical — preserves non-Devflow statusLines) 3. Writes updated settings.json back (only if changed, to avoid unnecessary writes) Also removes the misleading "Version upgrade notifications will still appear" message which was false once the statusLine is removed. Co-Authored-By: Claude <noreply@anthropic.com>
…ray bug - Fix mergeDenyList: Array.isArray guard so non-array deny neither throws nor spreads chars - Add DEVFLOW_HISTORICAL_DENY: frozen Set of all 154 ever-shipped deny entries - Add assertHistoricalDenySuperset(): drift detector; throws if template has unlisted entries - Add stripUserDenyList(): pure + idempotent; removes historical entries, preserves user-only - Add detectDenyState(): scans for any historical entry; subset installs read as on - Add resolveSecurityAction(): pure resolver; flag > manifest-agree > conflict > fresh-default-user - Add applyUserSecurityDenyList(): atomic write helper using writeFileAtomicExclusive - Remove deny-list injection from installSettings (moved to dedicated init step in F2) - SecurityMode now includes 'none' (was 'user' or 'managed' only)
…+ --security flag - Add ManifestData.features.security ('user'|'managed'|'none'|undefined) wired into readManifest builder so it round-trips and is never dropped - Replace hardcoded securityMode='user' with resolveSecurityAction() call: detects current deny state, reads manifest mode, resolves action - Add --security <mode> flag (user/managed/none) to initCommand - Add dedicated security step after managed-install that always targets ~/.claude/settings.json for user mode; managed path falls back to user on failure with a real write (not just a warning); none path strips - Assert historical superset at install time to catch template drift - Write resolved securityMode to manifest features on every init - Preserve backward compat: pre-Phase-F manifests with no security field read as undefined (unknown/fresh); readManifest defaults gracefully
… New devflow security command (src/cli/commands/security.ts): --status: reports none/user/managed/both + entry count derived from live deny array length; flags anomalous dual-location installs --enable [--managed|--user]: add-to-target-first, verify, then strip-other; self-heals prior interrupted mode switch; updates manifest --disable: strips from user settings and removes managed settings unconditionally; reports every removed entry; hard-fails on unparseable user settings.json; updates manifest to 'none' Default (no flag) = --status - Uninstall F5: replace separate managed-settings prompt with unified security prompt covering user+managed locations; TTY confirm with initialValue:false (deny list is protective); non-TTY preserves; reports removed entry count on success - Register securityCommand in cli.ts + addHelpText examples
New devflow safe-delete command (src/cli/commands/safe-delete.ts): - --enable: resolves platform/shell/profile; warns if trash CLI absent and writes nothing; upgrades outdated block in-place (remove+install) so blocks never stack; no-ops when already at current version - --disable: surgical marker-delimited removal via removeFromProfile; deletes profile file when it becomes empty (fish function files) - --status (default): tri-state installed/outdated/absent/unknown; includes resolved profile path; $SHELL unset => unknown, never crashes - Re-exports getSafeDeleteStatus() for use by list.ts (no subprocess) - Registered in cli.ts with examples line in addHelpText block Tests: tests/safe-delete-command.test.ts covers enable/disable/status, no-trash-CLI, upgrade-no-duplicate, $SHELL unset, fish file deletion via tmpdir with HOME+SHELL overrides; no real profile mutation
init --no-memory previously updated dream config to memory:false but left .pending-turns.jsonl and .pending-turns.processing in place. On re-enable, the background-memory-update worker would process stale turns from a prior session, producing incorrect context. Now mirrors the drain logic in memory.ts --disable (~line 382-385): Promise.all(unlink pending-turns + unlink pending-turns.processing), ENOENT-tolerant, runs only when !memoryEnabled and gitRoot is set. Requires getPendingTurnsPath and getPendingTurnsProcessingPath from project-paths.ts (newly imported).
…elete in list Manifest sync (Phase E1): - ambient --enable/--disable, decisions --enable/--disable, hud --enable/--disable, memory --enable/--disable: each now reads the existing manifest and updates manifest.features.<name> to match. Guards: only updates when manifest already exists (readManifest != null). Mirrors the exact pattern in knowledge/toggle.ts. Coder-2 security field (manifest.features.security) is not touched here. list.ts tri-state surfacing (Phase E3): - Adds TriState type, resolveSecurityTriState() pure function - Extends formatFeatures() with optional extra param for security + safe-delete tri-state values (backward-compatible: existing callers still pass only features) - security: reads manifest.features.security; probes managed settings path as live fallback when manifest field is absent (try/catch wraps fs.access per spec — throws on unsupported platforms) - safe-delete: getSafeDeleteStatus() from Phase D (no subprocess) installed->on, absent/outdated->off, unknown->unknown - Old manifests without security field still render fine (unknown) tests/list-logic.test.ts: - Order-asserting test for rules->security->safe-delete ordering - resolveSecurityTriState suite (on/off/unknown cases) - formatFeatures with extras suite (8 cases covering all tri-states)
…elete in list Manifest sync (Phase E1): - ambient --enable/--disable, decisions --enable/--disable, hud --enable/--disable, memory --enable/--disable: each now reads the existing manifest and updates manifest.features.<name> to match. Guards: only updates when manifest already exists (readManifest != null). Mirrors the exact pattern in knowledge/toggle.ts. Coder-2 security field (manifest.features.security) is not touched here. list.ts tri-state surfacing (Phase E3): - Adds TriState type, resolveSecurityTriState() pure function - Extends formatFeatures() with optional extra param for security + safe-delete tri-state values (backward-compatible: existing callers still pass only features) - security: reads manifest.features.security; probes managed settings path as live fallback when manifest field is absent (try/catch wraps fs.access per spec -- throws on unsupported platforms) - safe-delete: getSafeDeleteStatus() from Phase D (no subprocess) installed->on, absent/outdated->off, unknown->unknown - Old manifests without security field still render fine (unknown) tests/list-logic.test.ts: - Order-asserting test for rules->security->safe-delete ordering - resolveSecurityTriState suite (on/off/unknown cases) - formatFeatures with extras suite (8 cases covering all tri-states)
…ver-truncate-on-crash for the file Claude Code reads every turn): - init.ts security step: the managed-success strip and the 'none' strip paths wrote ~/.claude/settings.json via plain fs.writeFile while the merge paths used the atomic helper. Both strips now go through writeFileAtomicExclusive (temp+rename), matching applyUserSecurityDenyList and the security command. - uninstall.ts section 6: the unified security deny-list strip now uses writeFileAtomicExclusive, mirroring 'devflow security --disable'. Manifest-sync consolidation (Simplifier pass, finalized here): - ambient/decisions/hud/memory/security: replace the inline readManifest -> if-exists -> mutate -> writeManifest blocks with the syncManifestFeature helper. Single consistent pattern across all toggle commands; only updates an existing manifest (never creates one). Verified: tsc clean (zero warnings), npm run build full pipeline, 617 targeted vitest tests green, Snyk code scan 0 issues.
…en when already disabled Previously, hud --disable early-returned when hud.json already said disabled, before the statusLine removal ran. If settings.json had a lingering Devflow statusLine from a partial prior state (crash between config-write and settings-write), the drift was never cleaned up. Now: statusLine removal always runs regardless of config.enabled. The "already disabled" message only prints when BOTH config was already disabled AND no lingering statusLine was found. A non-Devflow statusLine is never touched (removeHudStatusLine only removes Devflow-owned entries). settings.json write remains atomic (writeFileAtomicExclusive) and conditional (only when content actually changed). Added 4 drift self-healing contract tests to hud.test.ts covering: - lingering hud.sh removed when config already disabled - lingering legacy statusline.sh removed when config already disabled - non-Devflow statusLine survives --disable - no-op (no write) when already clean
Dream-Task: knowledge Dream-Session: 29694b18-4416-44a3-a469-d4c8cf769749 Co-Authored-By: Devflow Dream <dream@devflow.local>
BLOCKING: Security deny list not written on declined-sudo fallbackFile: The BugThe write step for the managed-mode
When a user selects "Managed settings" but then declines the sudo prompt (line 848),
The manifest is then written as Why This MattersThe UI explicitly offers a labeled fallback: "No, fall back to settings.json" (line 839). The user actively selected this, yet receives zero deny-list protection and a manifest/reality mismatch. FixRoute the declined-sudo path to the user-settings write. Either: Option A: Flip the mode at decline time (init.ts:848): managedSettingsConfirmed = sudoChoice === 'yes';
if (!managedSettingsConfirmed) {
securityMode = 'user'; // "fall back to settings.json" must actually write to settings.json
}Option B: Add the terminal branch at the write step: } else if (securityMode === 'managed' && !managedSettingsConfirmed) {
// User declined sudo and chose the settings.json fallback — honor it.
if (templateDeny.length > 0) {
try {
await applyUserSecurityDenyList(userSettingsPath, templateDeny);
securityMode = 'user'; // keep manifest in sync with reality
} catch (e) {
p.log.warn(\`Could not write deny list to user settings: ${e instanceof Error ? e.message : e}\`);
}
}
}Either approach fixes the reachable gap and keeps the manifest in sync. |
Should-Fix SummaryTesting Gaps (HIGH confidence — established patterns not followed)1. New migration
2.
3.
4.
Structural/Maintainability Issues1. TypeScript:
2. Complexity:
3. Reliability:
4. Performance:
5. Consistency:
6. Architecture:
7. Documentation (HIGH): CLI reference missing two new commands and init flag (docs/cli-reference.md, 96% confidence)
Note on PR Description ClaimThe PR states "all settings.json writes are now atomic," but this is broader than reality:
This PR correctly establishes the invariant for the security seam; a follow-up PR should extend it repo-wide. |
Additional Should-Fix & Lower-Confidence FindingsAdditional Type-Safety Issues (MEDIUM, 82-88% confidence)1.
2.
Reliability & Fault Tolerance (MEDIUM, 85-90% confidence)1. Strip-path duplication: 4 near-identical read-strip-write blocks (init.ts + security.ts, 80% confidence)
2. Template deny-list loading duplicated in 4 places (security.ts + init.ts + post-install.ts ×2, 88% confidence)
3.
Pre-Existing Issues Surfaced by New Code1. HUD
2. Uninstall security comment misleads about hard-fail behavior (uninstall.ts:461 comment vs :464-468 code, 90% confidence)
Documentation Gaps (MEDIUM-HIGH, 85-96% confidence)1. README omits new public commands (README.md:111-123, 88% confidence)
2. CLAUDE.md migrations enumeration incomplete (CLAUDE.md:68, 85% confidence)
Lower-Confidence Observations (60-79% range)
Summary of Issues by Category
No findings block merge once the BLOCKING security issue is fixed. Most should-fixes are low-effort consolidations (extract helpers, add guards, fix comments). Reviewed with: security.md, reliability.md, performance.md, complexity.md, consistency.md, testing.md, typescript.md, documentation.md, architecture.md, regression.md |
Four cases mirroring purge-stale-memory-markers-v1 style: - removes .working-memory-disabled sentinel when present + returns info - ENOENT no-ops without throwing (idempotency invariant, avoids PF-004) - non-ENOENT errors rethrow (ENOTDIR via file-where-dir trick, avoids PF-004) - registered in MIGRATIONS with per-project scope and ordered after purge-stale-memory-markers-v1 (applies ADR-001) Co-Authored-By: Claude <noreply@anthropic.com>
safe-delete: no-flag now shows p.note('Usage') and returns — matches
ambient/memory/hud/decisions incumbents exactly. --status is its own
branch with a proper exhaustive switch on SafeDeleteStatus (applies
TypeScript rule: exhaustive switch via `never` in default case).
list: triLabel inner function converted from if/if/return to exhaustive
switch on TriState — compiler will catch any future 4th member silently.
Rendered output byte-identical; tsc clean; 47 tests green.
Co-Authored-By: Claude <noreply@anthropic.com>
…helper + tests Extract resolveSecurityRemovalDecision() pure function from section 6 of the uninstall orchestration, covering the non-interactive-preserve invariant (isTTY=false → always 'preserve'), the keepDocs skip gate, and the interactive prompt path. Add 9 behaviour-focused unit tests that lock down all three return values — including a named regression test for the safety property that non-interactive mode never strips the deny list (avoids PF-004 half-applied-state hazard). Also correct the misleading comment at the strip call site: uninstall does NOT hard-fail on corrupt JSON — detectDenyState catches the parse error and sets user=false, so the stripUserDenyList call is skipped entirely (unlike devflow security --disable which process.exit(1)s). tsc clean, 27 tests green. Co-Authored-By: Claude <noreply@anthropic.com>
…h-E-foundation) SF1: Move canonical SecurityMode to manifest.ts (owns the ManifestData interface field); post-install.ts re-exports it; init.ts imports it via post-install.ts re-export. Removes local 'type SecurityMode' alias in manifest.ts:readManifest and the duplicate inline 'user'|'managed'|'none' in resolveSecurityAction's signature and InitOptions.security. Single declaration, 7 sites collapsed. Applies PF-009 (stale references swept in owned files). SF2: Extract keepTargetFor(DetectedMode): SecurityMode helper — collapses the three open-coded detectedMode→target ladders to one named function. Add never exhaustiveness guard in the flag dispatch block. Applies typescript rule (exhaustive switch/never guards). SF19: Extract classifyDetected() — lifts the 4-way boolean→DetectedMode ternary to a named helper, pairs with keepTargetFor so the pipeline reads as two steps. SF4: Add exported loadTemplateDenyEntries(rootDir): Promise<string[]> in post-install.ts — single canonical template reader (defensive parse, Array.isArray guard, String coerce, returns [] on any failure). Replaces inline reads in installManagedSettings, removeManagedSettings, and init.ts security step. Downstream: security.ts resolver will adopt it. SF16: Add @throws JSDoc to mergeDenyList and stripUserDenyList documenting that SyntaxError propagates on malformed JSON; callers must pre-validate or wrap. NEW HELPER: Export stripUserSecurityDenyList(settingsPath): Promise<{removed}|null> — owns the read→strip→guard→writeFileAtomicExclusive→swallow-ENOENT sequence currently duplicated in init.ts (×2) and security.ts (×1). Downstream resolvers will adopt it (not wired in yet per spec). Build: tsc clean, 158 tests green, Snyk code scan 0 issues. Co-Authored-By: Claude <noreply@anthropic.com>
…d fix no-flag UX SF4-consumer: delete local loadTemplateDenyEntries and import the canonical one from post-install.ts (single source of truth). SF9-consumer: replace open-coded read→strip→guard→atomic-write block in --enable managed path with stripUserSecurityDenyList (canonical helper from post-install.ts). SF5: gate managed-settings removal on detected.managed (parse-safe signal from detectDenyState) rather than raw managedExists — prevents uncaught JSON.parse crash in removeManagedSettings when the managed file exists but contains corrupt JSON, which would leave user settings stripped but manifest sync skipped (half-applied state, applies engineering.md never-throw-in-business-logic rule). SF6a: bare 'devflow security' now prints a Usage note (mirrors memory.ts incumbent pattern) instead of silently delegating to --status. SF11: switch process.exit(1) at operational failure sites (template load failed, managed write failed) to p.log.error + return; retain process.exit(1) at the unparseable user settings precondition guard (genuine fail-safe). test(security): add tests/security-command.test.ts covering countDenyEntries pure helper (SF12): valid array, parse error, no deny key, non-array deny, absent permissions, empty array, null deny. Co-Authored-By: Claude <noreply@anthropic.com>
…batch-F-init)
B1 (Security): when securityMode='managed' but managedSettingsConfirmed=false the
write-step chain had no matching branch — deny list was written nowhere and manifest
falsely recorded 'managed'. Restructured the if/else-if chain so the outer arm is
securityMode==='managed' with an inner split on managedSettingsConfirmed; the false
arm writes to user settings and flips securityMode to 'user' before manifest write.
applies ADR-010 (declined-sudo path is the user-scope write ADR-010 mandates).
SF3 (TypeScript): added terminal `else { const _exhaustive: never = securityMode; }`
after all discriminant arms so the compiler enforces exhaustiveness when SecurityMode
gains new variants. avoids PF-009 (stale references after rename/refactor).
SF9 (Complexity): replaced the two inline read-strip-write-if-changed blocks (managed-
success arm and none arm) with the canonical stripUserSecurityDenyList helper, which
owns atomic temp+rename, ENOENT-swallow, and only-write-if-changed in one place.
SF1 (TypeScript): replaced three inline 'user'|'managed'|'none' union casts (lines
904, 905, 1415) with the canonical SecurityMode type. avoids PF-009.
tsc: 0 errors, 0 warnings. 110 init-logic + 85 security/uninstall/manifest tests green.
Snyk code scan: 0 issues.
Co-Authored-By: Claude <noreply@anthropic.com>
…ateDenyEntries Adds 12 new test cases across init-logic.test.ts and manifest.test.ts to cover the write-path seams introduced by the foundation resolver: init-logic.test.ts new cases: - applyUserSecurityDenyList: merge into existing file with siblings preserved, ENOENT fallback creates from empty, idempotency across two applies - loadTemplateDenyEntries: valid template, non-array deny returns empty, missing file returns empty - resolveSecurityAction: unknownDetected fixture now exercised, two missing non-TTY CONFLICT sub-cases added manifest.test.ts new cases: - syncManifestFeature: no-op when manifest absent, updates field and bumps updatedAt when present, security field update round-trips correctly All 170 tests pass, tsc clean with zero warnings. Co-Authored-By: Claude <noreply@anthropic.com>
…n post-install list.ts: replace nested ternary for safeDeleteTriState with a flat switch statement — avoids the banned nested-ternary pattern, aligns with the SafeDeleteStatus union shape (clearer than combining absent+outdated in a compound condition), and leaves room for a future exhaustive guard. post-install.ts: update two stale JSDoc comments — loadTemplateDenyEntries already serves security.ts (not "will adopt"), and stripUserSecurityDenyList's "will call" future-tense wording is now "use" since both callers are adopted.
…it flag Closes the SF15/SF17/SF18 documentation review issues for PR #244: - docs/cli-reference.md: add Security (Deny List) + Safe-Delete sections, plus the --security <user|managed|none> Init Options row. - README.md: surface security/safe-delete in the CLI Reference block; note post-init toggling in the Security paragraph. - CLAUDE.md: append purge-dead-working-memory-sentinel-v1 to the Migrations enumeration (applies ADR-001).
Summary
Closes all 7 gaps from the feature-toggle audit where "disabled" didn't fully disable (or there was no off switch). One PR, clean-break major version, user-scope-only — no local-scope writes or
<gitRoot>/.devflowresolution anywhere in these changes.The 7 gaps
Gap 1 — Security deny list: full lifecycle (the careful one)
devflow securitycommand:--status(none/user/managed/both + derived entry count + location),--enable [--user|--managed](add-to-target-first → verify → strip-the-other; self-heals an interrupted switch),--disable(strips user + removes managed, reports exactly which entries were removed, hard-fails on unparseable settings.json).DEVFLOW_HISTORICAL_DENYsuperset — detection + removal use the historical set; enable adds the current template. Entry count is always derived, never hardcoded.init --no-securityflag; uninstall has a unified security-removal prompt (TTY-confirm default-keep / non-TTY preserve).~/.claude/settings.json, never a project file.Gaps 2 & 4 — Decisions/curation runtime gates
dream-evaluate.eval-decisions,eval-curation,dream-capture(usage scanner), anddream-collect-tasks(curation sweep) all honor it. Disabling no longer stamps.curation-last(no 7-day suppression after re-enable).Gap 3 — HUD hard-disable
hud --disableremoves the Devflow statusLine from settings.json (surgically — a non-Devflow statusLine survives) AND setshud.json enabled:false. Self-healing under drift.Gap 5 — Dead memory sentinel removed
getWorkingMemoryDisabledSentinelremoved from all copies + the.cjsexport; new ENOENT-tolerant per-project migrationpurge-dead-working-memory-sentinel-v1; CLAUDE.md updated to config-only (per ADR-001).Gap 6 — Manifest sync + list surfacing
ambient/decisions/hud/memory --enable/--disableupdatemanifest.features.*(only when a manifest already exists — never created as a toggle side-effect).devflow listsurfacessecurity+safe-deleteas tri-state (on/off/unknown).Gap 7 — safe-delete toggle + queue drain
devflow safe-delete --enable/--disable/--status(state lives in the shell-profile block; no-trash-CLI warns + writes nothing; upgrade replaces in place — no duplicate blocks;$SHELLunset → "unknown").init --no-memorynow drains the pending-turns queue..claudeignoreunchanged (user-owned scaffold).Cross-cutting
settings.jsonwrites now go throughwriteFileAtomicExclusive(temp+rename) — no more truncate-on-crash of the file Claude reads every turn.syncManifestFeaturehelper deduplicates toggle→manifest sync across 5 commands.Breaking / behavioral changes
devflow listinstead).eval-decisionsnow honors the.disabledsentinel (consistency).SecurityModegains'none'.--disable/uninstall removal report lets them re-add.Testing
mainand pass cleanly in isolation.)stripUserDenyList/detectDenyState/resolveSecurityActioncontracts, manifestsecurityround-trip,safe-deletecommand,listtri-state, shell-hook dual-signal gating, HUD self-healing.src/cli.Note
security --enable --managederrors (with guidance) on managed-write failure rather than auto-falling-back — honoring "explicit flag wins". The init path auto-falls-back to user mode. This asymmetry is intentional.🤖 Generated with Claude Code