feat(flags): typed flag registry, settings-page TUI, new upstream flags - #300
Merged
Conversation
… 8 new flags
Rewrites `src/core/flags.ts` from a boolean-only `ClaudeCodeFlag` interface
to a fully typed discriminated-union registry (BooleanFlagDef | EnumFlagDef |
NumberFlagDef | StringFlagDef keyed by `kind`). Adds 8 new upstream flags and
a compile bridge so existing call sites continue to build unchanged.
New types: ClaudeCodeFlag (union), FlagsRecord (Record<string, FlagsRecordValue>),
FlagTarget ({ type:'env'|'setting'; key: string }).
Registry: 28 flags total (20 existing converted + 8 new):
max-concurrent-subagents (number, recommended, default 40, env)
subagent-spawn-depth (number, optional, env)
workflow-size-guideline (enum small|medium|large|unrestricted, optional, setting)
— domain verified from Claude Code 2.1.241 binary string analysis (Phase 0)
default-model (string, optional, env ANTHROPIC_DEFAULT_MODEL)
enable-todo-tools (boolean, optional, env)
goal-checkin-minutes (number, optional, env; 0 = off but ACTIVE)
spellcheck (string, optional, setting; wrapKey:'command' → {command:...})
view-mode (enum default|verbose|focus, neutralValue:'default', folds viewMode)
— replaces standalone applyViewMode/stripViewMode (kept as deprecated shims)
New exports: getDefaultFlagsRecord, getRecommendedFlagIds, neutralValueOf,
isNeutral (0 is ACTIVE), coerceFlagValue (sink validation, applies PF-023),
parseFlagValueInput, formatFlagValue, countActiveFlags, readViewMode,
sanitizeFlagsRecord, migrateLegacyFlagsToRecord (ADR-014 transition contract),
legacyIdsToRecord (compile bridge, deprecated).
applyFlags(settingsJson, FlagsRecord) replaces applyFlags(settingsJson, string[]).
- Neutral values delete target key; active values write payload
- Env number payloads stringified ('40' not 40)
- String flags with wrapKey shaped as { [wrapKey]: value }
- __proto__/constructor/prototype keys skipped (prototype pollution guard)
stripFlags now covers viewMode and spellcheck via the registry.
Compile bridge call sites updated:
init.ts: applyFlags(content, legacyIdsToRecord(enabledFlags))
f.recommended replaces f.defaultEnabled (UI partitioning)
flags CLI: applyFlags(stripped, legacyIdsToRecord(flagIds))
init-seed.ts: f.kind === 'boolean' && f.defaultValue === true
Deprecated shims kept: getDefaultFlags(), applyViewMode(), stripViewMode().
Tests (168, all green):
- Registry structural invariants (unique IDs, unique target keys, bounds, etc.)
- getDefaultFlagsRecord() pinned snapshot (28 flags)
- neutralValueOf / isNeutral (0 is ACTIVE — PF-023 documented)
- coerceFlagValue hostile cases (Infinity/NaN/1e309, out-of-range, non-integer,
overlong string, control chars)
- applyFlags with FlagsRecord (neutral-deletes-key, env stringification)
- stripFlags covering viewMode and spellcheck
- Per-new-flag blocks for all 8 new flags
- migrateLegacyFlagsToRecord + legacyIdsToRecord
- Deprecated shim (getDefaultFlags includes max-concurrent-subagents)
RED-check evidence:
- Neutral-deletes-key inversion: 27 tests failed → 141 passed
- Env-stringification inversion: 4 tests failed → 164 passed
Applies ADR-014 (typed flags contract), ADR-016 (view-mode fold).
Avoids PF-023 (validates at convergence sink, not per-call-site).
Keybinding-flavor CUT: domain unverifiable from binary analysis.
…ed seed
feat/flags-typed-registry Phase 2 of 5
Changes:
- src/core/manifest.ts: features.flags changed from string[] to FlagsRecord;
readManifest heals legacy formats in-reader (array→record migration,
viewMode fold, knownFlags strip); writeManifest uses writeFileAtomicExclusive;
heal-write failure returns in-memory manifest not null (D39)
- src/core/flags.ts: sanitizeFlagsRecord gets __proto__/constructor/prototype
pollution guard
- src/cli/commands/init-seed.ts: resolveSeedFlags accepts FlagsRecord|null
(drops knownFlags pair); resolveInitSeed reads viewMode via readViewMode(flags);
InitSeed.flags/viewMode kept as deprecated Phase 6 bridges
- src/cli/commands/flags.ts: Phase 2 bridges — FlagsRecord ↔ string[] adapter
for resolveEnabledFlags and updateManifestFlags
- src/cli/commands/init.ts: one mechanical bridge (legacyIdsToRecord) + flags:{}
for HUD manifest; marked for Phase 6 cleanup
- tests/helpers.ts: canonical makeManifest() factory with FlagsRecord flags
- tests/manifest.test.ts: fixture sweep (flags:[]/viewMode deprecated fields
→ FlagsRecord); 4 new Phase 2 heal round-trip tests (idempotency, __proto__,
deliberate-disable preservation, canonical roundtrip)
- tests/init-seed.test.ts: resolveSeedFlags tests rewritten for FlagsRecord API;
viewMode tests updated to use flags['view-mode']; re-init fixture updated
…ctory) Eliminates the legacyIdsToRecord bridge call site in flags.ts CLI. The flags command now works directly with FlagsRecord throughout: - createFlagsCommand() factory export (fresh Commander instance per call; singleton flagsCommand kept for src/cli.ts compatibility) - --list: all registry metadata, no manifest required - --status: typed values from manifest record; degrades gracefully without manifest - --enable/--disable: boolean flags only; error + exit 1 for valued flags suggesting --set/--unset instead - --set id=value (repeatable): any kind; splits on first = only; parseFlagValueInput validates at the CLI boundary (applies PF-023) - --unset ids: any kind → neutral value per flag type - Bare invocation: status table + Phase 5 seam note (// Phase 5 wires runFlagsTui here); non-TTY path exits 1 per the plan - Persist pipeline: stripFlags → applyFlags (strip-then-apply invariant INV-1) via writeFileAtomicExclusive; per-artifact error handling (avoids PF-015) - Malformed settings.json → abort with exit 1, never silent clobber - No manifest for mutating ops → abort with exit 1 (avoids settings/manifest desync) - Hostile inputs (__proto__, 1e309, NaN) → exit 1, both files byte-untouched Tests (tests/flags-cli.test.ts, 34 tests): - hud-enable-selfheal harness pattern (vi.mock clack, vi.stubEnv, temp dirs) - Whole-post-state asserts (full JSON deep-equal) per PF-015 - Hostile value quartet (applies PF-014, PF-023) - Idempotent second-run assertion Co-Authored-By: Claude <noreply@anthropic.com>
…D-P4-1) Proxy-routed models (GPT-4o, etc.) are not recognised as Claude models, so Claude Code enforces a conservative context-window limit and triggers surprise compaction mid-session. Fix: pair ANTHROPIC_BASE_URL with CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT='1' so the enforcement is lifted for the relay session. Changes: - proxy.ts: add UNKNOWN_MODEL_WINDOW_ENV constant (D-P4-1) - _applyProxyEnvToObject: set both vars; independent comparisons per PF-015 (no early-return that could skip the second write) - _stripProxyEnvFromObject: URL ownership remains the SOLE strip gate; delete both vars together when ANTHROPIC_BASE_URL matches our managed relay URL; foreign/absent URL → touch nothing - runEnable success block: info line noting enforcement is disabled and applies to new Claude Code sessions (PF-022 applies-on-restart messaging) Tests (tests/proxy.test.ts — 14 new, all green): - applyProxyEnv quartet: sets window var, idempotent, preserves unrelated env, port-change re-apply keeps window var at '1' - stripProxyEnv ownership gate: our URL strips both; foreign URL preserves both; absent URL preserves orphan; ours-other-port preserves both - T7-extended whole-end-state: applyDisableToSettings removes hooks + URL + window var; env block gone entirely with no extras; applyProxyEnv produces both vars
…iew adapter Extracts a generic runTui<S,A> driver (src/cli/tui/terminal.ts) shared by both the agents-view and the new flags-view TUI. Wires runFlagsTui into the devflow flags bare-invocation TTY branch (Phase 5 seam resolved). Key changes: - src/cli/tui/terminal.ts: generic TUI shell, MAX_KEYPRESSES=50_000, RunTuiSpec<S,A> interface, normalizeKey extended with backspace/delete/home/end. Bug fix: ERASE_BELOW after last frame line (stale-frame ghosting on terminal shrink). - src/cli/tui/cells.ts: shared padToVisible/truncateVisible/sanitizeCell extracted from agents-view/render.ts (avoids PF-017). - src/cli/agents-view/terminal.ts: rewritten as thin adapter over runTui; public API frozen (runAgentsTui, MAX_KEYPRESSES, TuiIO, TuiResult unchanged). - src/cli/flags-view/state.ts: pure reducer with viewMode GLUE (null↔'default'), strict number parsing (007/' 8' rejected), BUFFER_MAX_LEN=64, allowUnset. - src/cli/flags-view/render.ts: FIXED_ROWS=10, inverse-video caret, hint zone, dirty dot, scroll indicators, narrow-width safe. - src/cli/flags-view/terminal.ts: runFlagsTui adapter, signalAction='abort'. - src/cli/commands/flags.ts: lazy import runFlagsTui in TTY branch; persist on save, "No changes made." on cancel/abort. - src/hud/colors.ts: inverse() helper added. - tests/flags-view-state.test.ts: 58 tests (reducer, viewMode glue, strict parsing) - tests/flags-view-render.test.ts: 30 tests (frame contract, per-kind display, edit) - tests/flags-view-terminal.test.ts: 10 tests (pause(), flood cap, key routing) Tests: 3538/3538 green (99 files, +98 from Phase 5). Build: clean. RED-checks: (a) pause() removal → 5 failures; (b) MAX_KEYPRESSES raise → flood hangs.
… bridge cleanup Replace legacy multiselect + viewMode select in the Advanced wizard with runFlagsTui. Remove all compile bridge shims: legacyIdsToRecord, getDefaultFlags, applyViewMode, stripViewMode. Write FlagsRecord directly to the manifest; no knownFlags/viewMode residue in manifest.features. Critical fix (PF-015, fold-before-strip): resolveExistingViewMode now runs BEFORE stripFlags in the settings apply block. Reading after strip always returned undefined because stripFlags removes the viewMode key; fold-before-strip is the correct order. Changes: - src/core/flags.ts: delete deprecated shims section (legacyIdsToRecord, getDefaultFlags, applyViewMode, stripViewMode); update module comment - src/cli/commands/init-seed.ts: InitSeed.flags: FlagsRecord (was string[]); remove InitSeed.viewMode; resolveSeedFlags returns FlagsRecord; resolveInitSeed sets flags['view-mode'] in-place and returns without separate viewMode field - src/cli/commands/init.ts: import countActiveFlags/readViewMode/getDefaultFlagsRecord; enabledFlags: FlagsRecord replaces viewMode: ViewMode; Advanced path opens flags TUI (runFlagsTui + buildFlagRows + collectFlagRecord); Recommended path uses countActiveFlags; manifest write uses enabledFlags directly (no legacyIdsToRecord); no knownFlags write; fold-before-strip ordering fix with PF-015 comment - src/cli/commands/uninstall.ts: remove stripViewMode (covered by stripFlags via view-mode registry entry) - tests/flags.test.ts: remove deprecated shim describe blocks - tests/init-seed.test.ts: update for FlagsRecord/readViewMode - tests/init-e2e-flags.test.ts: 3 subprocess e2e tests (PF-018 seeded temp HOME): old-format manifest heal + viewMode preservation, fresh install defaults, idempotency Applies ADR-014, ADR-015, PF-015, PF-018, PF-029.
Remove phase-scaffolding comments that outlived the 6-phase implementation sequence: section headers naming Phase N, transitional sub-bullets in JSDoc (legacyIdsToRecord bridge, Phase 5 seam scaffolding), deprecated field rationale referencing Phase 6 as a future target, and tombstone notes in manifest.ts (init.ts still writes both fields, Phase 6 removes those writes). Drop unused coerceFlagValue import from init-seed.ts. Clarify cells.ts module doc to describe shared purpose (PF-017) without provenance history. No behavior changes; build clean; 81 tests pass, 24 pre-existing failures unchanged.
…w-mode leak
Six defects found by driving the real code paths rather than reading them.
1. P1 flags CLI completed silently. persistFlagConfig returned
`process.exitCode === 0` and all five call sites gated their confirmation on
the same test. Node initialises process.exitCode to `undefined`, not 0, so on
every real invocation that test was false: `devflow flags --enable X` wrote
both artifacts and printed nothing. The suite could not see it — it sets
process.exitCode = 0 in beforeEach, normalising away the exact condition under
which production failed. Success is now tracked in locals (also fixes the
process-global cross-talk: an unrelated earlier failure misreported this run).
2. P1 init-e2e-flags test 1 was vacuous. Its seeded hook used a flattened
`{matcher, command}` entry; removeCaptureHooks does `entry.hooks.some(...)`,
which throws on the missing array, and init.ts wraps its ENTIRE settings pass
in one try/catch that only warns. The pass aborted, settings.json was never
touched, and every settings assertion passed because nothing ran. Fixture
corrected to Claude Code's real hook shape, plus a non-vacuity gate asserting
the warning is absent. The env-var assertion removed during implementation is
restored: with a valid fixture the manifest/settings convergence holds
(max-concurrent-subagents 40 → CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS "40"), so
there was no convergence bug — only a test that could not observe one.
(The removeCaptureHooks crash on a malformed entry is pre-existing on main and
left out of scope; reported separately.)
3. P0 shared TUI shell leaked raw mode. Alt-screen, raw mode and cursor-hide are
set before the first render, but cleanup() was unreachable from the setup
window and both event handlers were unguarded — and a throw inside an
EventEmitter listener escapes as an uncaughtException rather than rejecting the
promise. Any throw from renderFrame/onResize/reduce killed the process with the
user's shell in raw mode (no echo, no line editing) until `stty sane`. Cleanup
now runs on every path; errors are surfaced, not swallowed. avoids PF-014.
4. P1 space was untypable in the flags TUI edit buffer. normalizeKey maps the
space bar to the name 'space' (5 chars) and the insert branch tested
key.length === 1, so it was dropped silently: typing "aspell list" yielded
"aspelllist". spellcheck exists to hold a shell command, so its whole purpose
was unreachable; default-model likewise.
5. P1 ctrl-c was dead while editing — reduceEditMode had no case for it so it fell
through to 'none', and raw mode suppresses the SIGINT that would otherwise
rescue the user. Also: control chars could enter the buffer (ctrl-keys arrive as
raw bytes), where they are uncommittable and desync the caret from the rendered
string, which renderBuffer strips. Rejected at insert instead.
6. P1 resize stranded the cursor. onResize set viewportHeight without re-clamping
viewportOffset, so shrinking the terminal could put the cursor outside the
visible slice — and no selection marker rendered at all until an arrow key.
Also removes dead code flagged in review (unused stripAnsi import, unused
COL_DIRTY, dead RenderDims re-export) and corrects the row-width doc comment.
Every fix ships a test proven RED against the pre-fix code. agents-view's public
API is untouched; tests/agents-terminal.test.ts and tests/init-proxy.test.ts are
unmodified. Full suite: 102 files / 3576 tests green.
…iterals, residue, coverage
Item 1 (render.ts): implement ADR-016 value vocabulary — cyan value when
configuredValue !== devflowDefault; cyan ‹ › chevrons for focused control /
live edit buffer (mirror agents-view pattern); dirty dot yellow unconditionally
(was cursor-only).
Item 2 (render.ts): route all disk-sourced non-boolean values through
sanitizeCell before display; prevents embedded LF/TAB (coerceFlagValue permits
both) from breaking the one-string-per-terminal-line frame contract. Adds render
test with embedded \n and \t confirming one line per row.
Item 3 (state.ts): drop 'j' and 'k' from the up/down noop case in
reduceEditMode so they insert literally in edit buffers (spec: literal q d u j
k). Extends literal-keys test to cover j/k alongside space.
Item 4 (manifest.ts): delete dead knownFlags? and viewMode? declarations from
ManifestData — readManifest reads them from the raw record cast, not the typed
interface; syncManifestFeature can no longer write them. End-state, no tombstones.
Item 5 (state.ts): drop unused neutralValueOf import — tuiToRecord inlines the
exact same null→neutralValue mapping and is behaviorally equivalent for all
practical inputs (boolean rows are never null in TUI). Decision: drop the import.
Item 6 (teammate-mode-cleanup.ts): update doc comment to reference stripFlags
only (stripViewMode was deleted when viewMode was folded into FlagsRecord).
Item 7 (flags-cli.test.ts): add bare non-TTY invocation test (zero args, stdout
status table, one stderr note, exitCode 1, zero writes) — pins flags.ts:509-520.
Item 8 (manifest.test.ts): add D39 heal-write-failure test — legacy manifest in
a read-only directory, readManifest returns migrated non-null manifest and does
not throw; permissions restored in finally block.
Item 9 (flags-cli.test.ts): extend malformed-settings guard to --set, with
post-run re-read asserting byte-untouched (anti-clobber previously only for
--enable).
Item 10 (init-e2e-flags.test.ts): replace silent early-return requireBuiltCli
guard with module-level CLI_BUILT existsSync flag + it.skipIf — silent green
(PF-018 forbidden state) replaced by explicit SKIP in vitest output.
Item 11 (flags-view-render.test.ts): pin exact FIXED_ROWS + viewportHeight
count; replace trivially-satisfiable unsaved disjunction with toContain('1
unsaved change').
Item 12 (init-e2e-flags.test.ts): retitle "byte-stable" test to "content-stable"
with inline comment explaining why toEqual (not toBe) is correct.
…hygiene Markdown changes explicitly approved by maintainer (recorded per plan verification item 6).
Owner
Author
Code Review — Cycle 1Full summary withheld (public repository).
Full report: /Users/dean/Sandbox/devflow/.devflow/docs/reviews/feat-flags-typed-registry/2026-08-25_1217/review-summary.md (not committed; ask the author) Posted by devflow · cycle 1 |
…ock seam
CPLX-SF1: Extract the three-case flags parse (string[] / object / missing) into
a pure `parseManifestFlags` helper that returns `{ flags, legacy }`. The boolean
`flagsWereLegacy` replaces the inline `Array.isArray(features.flags)` clause in
needsHeal, keeping the predicate in lockstep with the parse branch by deriving it
from the same parse result. Zero behavior change — all 79 manifest tests pass.
TEST-S1: Replace the D39 heal-write-failure injection from `chmod 0o555` (vacuous
under root UID — avoids PF-018) with `vi.spyOn(fs, 'rename').mockRejectedValueOnce`.
The mock intercepts the atomic rename inside writeFileAtomicExclusive at the seam,
making the test UID-independent. Proof of RED: removing the try/catch around
writeManifest causes the outer catch to return null, failing expect(result).not.toBeNull().
Co-Authored-By: Claude <noreply@anthropic.com>
…2 chain
TS-M1 (applies ADR-003): drop orphaned ViewMode import from init.ts — this
diff removed the last reference (the let viewMode binding) without trimming
the import list.
ARCH-S3 / CPLX-SF6 / CONS-S3: copy seed.flags at the alias site so
enabledFlags is a fresh record and writes through the alias cannot corrupt
the seed. Rebind the view-mode assignment with a spread rather than mutating
in place. In init-seed.ts, resolveInitSeed returns { ...flags, 'view-mode':
resolvedViewMode } instead of writing flags in place — honoring the module
docblock ("all exported functions are pure").
REL-S2 (avoids PF-014): replace process.exit(0) on TUI abort with
process.exitCode = 130 + return so the exit code signals cancellation to
wrappers and buffered terminal-restore escapes are not dropped on pipe stdout.
REL-S3 (applies PF-029): restrict the modifiedCount filter to `id in defaults`
so forward-compat unknown IDs (defaults[id] === undefined) no longer inflate
the outcome-line count.
CPLX-S2: replace the `?? (ternary) ??` view-mode fold in resolveInitSeed
with an explicit three-branch if-ladder; each branch carries its own meaning
without needing comment scaffolding.
Regression tests added in tests/init-seed.test.ts for the ARCH-S3 mutation
guard (two cases: input manifest unchanged, returned flags isolated from
caller mutations).
REL-S2 and REL-S3 are inline in the init action handler and not directly
exercisable from unit tests without full init scaffolding — noted per PF-018.
… (SEC-M2, applies PF-015, PF-022, ADR-003)
src/cli/tui/cells.ts was importing stripAnsi/truncate from src/hud/colors.ts (a feature module), and this PR added inverse() to hud/colors with its only consumer in flags-view/render.ts. The dependency arrow pointed generic-CLI → feature, which is the wrong direction under ADR-013 (src/core/ = agent-neutral logic). It also caused composeScripts to carry the transitive dist/hud/ graph into the hooks runtime unnecessarily. Promote all ANSI primitives to src/core/ansi.ts (neutral home), then: - src/cli/tui/cells.ts imports stripAnsi, truncate from core/ansi - src/cli/flags-view/render.ts imports from core/ansi - src/cli/agents-view/render.ts imports from core/ansi - src/hud/colors.ts becomes a re-export barrel (export * from ../core/ansi) so all existing HUD component call sites (hud/components/*.ts, hud/render.ts, src/cli/commands/agents.ts, src/cli/commands/proxy.ts) are untouched. Byte-equivalent behaviour: functions are identical implementations. applies ADR-013; applies PF-017 corollary (one shared definition). Co-Authored-By: Claude <noreply@anthropic.com>
…casts
Three-generic form `RunTuiSpec<S, A extends string, C extends A>` with
`signalAction: Exclude<A, C>` and `continueIntent: C` makes `runTui` return
`Promise<{ intent: Exclude<A, C>; state: S }>`.
Issues fixed:
- TS-M4: exhaustiveness guard lost when generic replaced switch/never; now enforced
at the type level — adapters assign `result.intent` directly to their action
field, so adding a new Intent/FlagsIntent member is a compile error.
- ARCH-M6: unconstrained `A` prevented expressing the signalAction invariant;
`Exclude<A,C>` on signalAction makes it impossible to pass continueIntent there.
- TS-S2: `A extends string` closes the object-shaped intent footgun; the `!==`
comparison is now provably a string equality check.
Deleted all six unsound `as` casts across both adapters:
agents-view: 'cancel' as Intent, 'none' as Intent, result.intent as 'save'|'cancel'
flags-view: 'abort' as FlagsIntent, 'none' as FlagsIntent,
result.intent as 'save'|'cancel'|'abort'
One minimal driver-side cast retained (intent as Exclude<A,C>) at the keypress
`!== continueIntent` check — TypeScript cannot narrow A to Exclude<A,C> from a
generic C comparison; the cast is documented inline (D-TS).
Tests updated: tui-terminal.test.ts callers add the third type arg 'none'.
Verified: `tsc --noEmit` clean; 25/25 tests pass.
… (DOC-C1/CONS-H2, applies PF-025, ADR-002) Replace every pre-branch flag API reference with FlagsRecord semantics: - features.knownFlags:string[] → features.flags:FlagsRecord (key-presence=known) - InitSeed.flags:string[]+viewMode:ViewMode → InitSeed.flags:FlagsRecord - resolveSeedFlags(enabledFlags,knownFlags,registry) → resolveSeedFlags(FlagsRecord|null,registry) - getDefaultFlags → getDefaultFlagsRecord; add coerceFlagValue, parseFlagValueInput, migrateLegacyFlagsToRecord, neutralValueOf, isNeutral, sanitizeFlagsRecord, countActiveFlags, readViewMode - Add parseManifestFlags (three-shape migration: string[]→migrate, object→spread, missing→empty) - viewMode resolution: settings.json→readViewMode(flags)→'default' encoded in flags['view-mode'] - Fix knownPlugins/knownFlags gotcha: knownFlags no longer in ManifestData - Fix ADR-014 Related note: FlagsRecord key-presence replaces knownFlags snapshot Add new modules introduced by this PR: - src/cli/commands/flags.ts (createFlagsCommand, lookupFlag, persistFlagConfig) - src/cli/flags-view/ (FlagsViewState, buildFlagRows, collectFlagRecord, reduce, …) - src/cli/tui/ (sanitizeCell, padToVisible, truncateVisible) Update frontmatter directories: and description: keywords. Update index.md cache line to match.
…trees (DOC-H1/M1/M3/P1/S1/S2, CONS-H3, applies PF-025) - DOC-H1 (CLAUDE.md): Two-Mode Init Advanced path no longer has a standalone view-mode selector; view-mode is now the enum row inside the flags editor TUI - DOC-M3 (CLAUDE.md): add tui/ and flags-view/ to Project Structure tree; note agents-view/terminal.ts is a thin adapter over the shared tui/ driver - DOC-M1 (docs/cli-reference.md): rewrite Feature Flags command block to use npx devflow-kit (57-occurrence file convention; was the only devflow-bare section) - DOC-P1 (docs/reference/file-organization.md): add tui/, flags-view/, agents-view/ to src/cli/ block; drop utils/ tombstone (applies ADR-003) - DOC-S1 (docs/reference/file-organization.md): document view-mode neutralValue contract — viewMode settings key written only when non-default - DOC-S2 (docs/cli-reference.md): replace one-off pin-sonnet-4-6 parenthetical with a general footnote on how boolean env-var flags serialize their string value - CONS-H3 (CLAUDE.md, docs/cli-reference.md, KNOWLEDGE.md): surface CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT across all three proxy doc surfaces; update strip invariant to name both vars and their asymmetric scoping (window var unconditional, URL port-gated); add tui/ to KB directories + keywords
TEST-SF1 (applies PF-023): add tests/tui-cells.test.ts — 25 assertions pinning sanitizeCell (TAB/LF collapse, ANSI strip), padToVisible (visible- length measurement), and truncateVisible (unchanged-when-fits + styling dropped across the truncation boundary) at the sink rather than through two renderers. TEST-M1 + REG-S3 (avoids PF-018): extend tests/tui-terminal.test.ts with the renderToStdout frame output contract — byte-level assertions on the exact escape sequence for a 2-line frame (HOME + ERASE_EOL + ERASE_BELOW) and the no-trailing-newline guard before ERASE_BELOW. Failure modes noted in the test header: deleting either fix causes independent assertion failures.
Coherent fix of the one validation contract (applies PF-023) at the sink every caller reaches (src/core/flags.ts), so all paths inherit the invariants. SEC-M1 — coerceFlagValue string branch now rejects LF (\x0a) alongside other control chars. Comment explains WHY LF must be rejected: `spellcheck` is an executed shell command (LF = statement separator) and --status is line-oriented. TAB remains the sole documented exception. Added sanitizeCell wrap at both CLI display sites (--status loop + non-TTY table) for defence in depth. TS-H1 + SEC-S2 — parseFlagValueInput number branch now enforces strict decimal grammar (rejects empty, padded, hex, exponent, leading zeros) instead of bare Number(). String branch now returns null for empty string (empty is UNSET, not an active value — prevents ANTHROPIC_DEFAULT_MODEL="" written to settings.json). Deleted checkNumberFormat from state.ts; commitEdit now delegates to parseFlagValueInput so CLI and TUI share one grammar (avoids PF-023 dual-validator). TS-M3 — hoisted asPlainObject() guard used at all three settings.env access sites in applyFlags/stripFlags so "env": [] cannot delete user keys via the Object.keys([]).length === 0 empty-env cleanup. sanitizeFlagsRecord now drops unknown-id non-primitive values instead of laundering them into FlagsRecordValue. Removed double assertion in parseManifestFlags case B. REL-S1 — sanitizeFlagsRecord now DROPS the key for invalid non-null known-flag values (absent = adopt default on next init) rather than writing null = "deliberately unset" (applies ADR-014 key-presence semantics). Explicit null input is still preserved (deliberate unset). Applies PF-023, ADR-014. Tests: RED→GREEN for all six required scenarios.
…es PF-015 PF-017 ADR-014 Root cause: stripFlags unconditionally destroys all registry-targeted keys in settings.json regardless of ownership. Six newly-registered valued flags had no preservation path; view-mode had a preservation path in init.ts only (violating the DRY/pipeline invariant identified as ARCH-H1). Fix — convergeFlagsIntoSettings (src/core/flags.ts): - Single pipeline entry point shared by init.ts AND persistFlagConfig (ARCH-H1, applies PF-015/PF-017: invariant lives in the pipeline, not at call sites) - Folds valued flags from pre-strip settings into the record when devflow does not own them (REG-H1/SEC-M3, applies ADR-014: absent = unknown = adopt) - "Owned" = present in ownedRecord (any value including null = explicitly unset); ownedRecord=null → nothing owned (fresh install or upgrade from old manifest) - Uninstall: stripFlags(json) full-sweep semantics unchanged — convergeFlagsIntoSettings is never called from uninstall.ts init.ts: replace inline view-mode fold + stripFlags + applyFlags with single convergeFlagsIntoSettings call; ownedRecord=existingManifest?.features.flags??null correctly distinguishes keys devflow previously wrote from newly-adopted defaults (resolveSeedFlags adopted default 40 for max-concurrent-subagents; ownedRecord=null ensures the hand-set '8' in settings wins on upgrade — REG-H1 probe) flags.ts (persistFlagConfig): ownedRecord=undefined → claimedIn=record (manifest IS the owned set); null in record = explicitly unset = still claimed → do not fold from settings (fixes unset-then-fold regression) TUI save path: viewModeExplicit = newRecord['view-mode'] !== record['view-mode'] Tests (RED→GREEN, whole-post-state per PF-015): - convergeFlagsIntoSettings: /focus survival, explicit override, owned wins, REG-H1 probe (six managed keys survive; concurrency stays 8 not 40), uninstall full-sweep pin - flags-cli: --enable brief with viewMode:'focus' survives; --set view-mode=verbose overrides - init-e2e: REG-H1 subprocess probe (hand-set managed keys + concurrency '8' survive reinit)
CONS-H1 (Careful): reorder formatFlagValue so boolean branch wins before isNeutral — boolean false now returns 'disabled' (not 'unset'). Routes --disable success line through formatFlagValue (applies ADR-016 — one syntax, one semantic). Adds vocabulary table test (RED on old formatter, GREEN after fix) pinning enabled|disabled|unset for each flag kind. TS-SF1 + CONS-M2: export defaultValueOf(flag) from core/flags.ts as the single authoritative default-rule source. getDefaultFlagsRecord, both resolveSeedFlags branches (init-seed.ts), and buildDevflowDefault (flags-view/state.ts) now call it — no more three-site drift risk. CONS-M1: delete getRecommendedFlagIds() (new in this PR, called only from its own test — no real caller). Removes it from tests/flags.test.ts import and describe block (applies ADR-003 — leave end-state not transition). PERF-L3: export findFlag(id) backed by the existing private FLAG_REGISTRY_MAP. lookupFlag (flags.ts) and commitEdit (flags-view/state.ts) use it; collectFlagRecord drops the per-call Map construction (applies ADR-016 module-stated O(1) pattern). Co-Authored-By: Claude <noreply@anthropic.com>
…nable-todo-tools reorder, render.ts docblock, probe notes extraction - ARCH-S1: note beside FLAG_REGISTRY that CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT is proxy-owned (paired with ANTHROPIC_BASE_URL in proxy.ts, lifecycle-coupled to relay enable/disable) — mirrors the agent-teams/teammate-mode-cleanup precedent - DOC-SF1: one-line JSDoc on FlagKind (union discriminant); contract doc block on FLAG_REGISTRY (IDs = stable manifest keys, array order drives --list/TUI row order) - CPLX-S3/CONS-M5: move enable-todo-tools (kind:'boolean') from under the '── Valued flags' banner into the optional-boolean block where it belongs; display order changes accordingly (--list and TUI); no test pins old position - CONS-M5 (render.ts): fix renderRow docblock — DIRTY column is 2 chars not 3; the '≤78+PREFIX=≤80' arithmetic disagreed with the file-header total of 77; replace the column breakdown with a reference to the file-header table - DOC-S3: move Phase 0 probe block (2026-08-23, CC 2.1.241) to docs/reference/claude-code-flags-probe.md; leave a one-line pointer in flags.ts (applies ADR-003 — leave the end-state, not the transition) Co-Authored-By: Claude <noreply@anthropic.com>
…fy === comparison
CPLX-SF2: the `if (n === 0) return { state, intent: 'none' }` guard appeared 9×
across browse-mode cases. Replaced with a single unified guard that fires once after
the keys that are intentionally exempt (escape/q → cancel, ctrl-c → abort, enter →
save even on an empty list). The twin up/k and down/j branches collapse into move(state,
±1); the three space/left/right cycle branches collapse into cycle(state, row, dir).
Zero behaviour change — all 77 tests pass unchanged.
TS-S3: cycleForward/cycleBackward used Object.is for stop lookup while render.ts
dirty-detection used !==. Changed to === (consistent with render.ts). The only
difference is -0 vs 0, which does not survive the JSON round-trip and is therefore
unreachable in practice. One-line comment at each call site explains the choice.
Co-Authored-By: Claude <noreply@anthropic.com>
…ze, dead code, comment hygiene CONS-M4: hoist scale/labelW/valueW out of renderRow into renderFrame so the column header and data rows share one binding. Header now uses ' ' (2 chars, matching the 2-char prefix) before FLAG instead of ' ' (4), and ' ' before VALUE instead of ' '; at 80 cols FLAG lands at col 2 and VALUE at col 31, matching the data row layout exactly. Tests pin the offsets at 80 and 60 cols (ANSI-stripped). SEC-S1/CONS-S2: add sanitizeCell(row.label) call in renderRow — the comment claimed sanitization but rawLabel was used verbatim (PF-023). DOC-M2: replace three ADR-016 citations in flags-view/render.ts that attributed colour/glyph vocabulary rules the ADR does not contain. New self-standing comments carry the same intent without false attribution. state.ts:5's fair citation is untouched. ARCH-M5: make renderFrame in both flags-view and agents-view read state.viewportHeight as the single owner (clamped to MIN_VIEWPORT), removing the duplicate derivation from dims.rows. Tests that set state.viewportHeight explicitly now render exactly that many data rows. Invariant tests added for both views. TS-M1 (COL_PREFIX sub-item): delete never-read COL_PREFIX constant (applies ADR-003). Co-Authored-By: Claude <noreply@anthropic.com>
Resolves ARCH-M1 + CPLX-H1 + CPLX-H2 + CPLX-SF5 + CONS-M3 from the feat/flags-typed-registry review (complexity.md, architecture.md, consistency.md on 2026-08-25). ARCH-M1 + CPLX-H1: the single 348-line .action() handler is replaced by six named async functions (handleList, handleStatus, handleSetBooleans, handleSet, handleUnset, handleBare) and a ~15-line dispatcher. Each handler carries one responsibility and is independently readable. CPLX-H2: the manifest-load + settings-load preamble that was copy-pasted verbatim into four mutating branches is extracted into loadFlagContext(), which returns a discriminated result and never exits itself. The --enable and --disable branches collapse into one handleSetBooleans(value: boolean) parameterized handler — their only deltas were the record assignment and one error-message string. Applies PF-017: one shared load path means a fix lands once, not four times. CPLX-SF5 + CONS-M3 (deliberate user-visible wording convergence): the status table was rendered by two divergent loops — --status used a long "not adopted — default X applies on next devflow init" sentence, while the bare non-TTY path used a bare "not adopted". Both now call formatStatusRows(record): string[], which uses the LONGER wording in both surfaces. The non-TTY table output now includes the actionable second half that the short form had been dropping. All existing tests pass unchanged (43 flags-cli + 184 flags.test + 4 init-e2e-flags = 231 total). No test assertions check the specific short "not adopted" wording, so no test changes were required. Deferred (later batches): --list kind-label ternary, p.outro polish, redundant `as` casts, bare-TTY manifest gate, stdout.isTTY predicate, persistFlagConfig manifest re-read.
TS-H2 / ARCH-H2 / REL-H2 / REG-SF2 — one coherent root cause, four angles:
1. PersistResult discriminated type replaces `boolean` return from persistFlagConfig.
Three distinct states: {ok:true} | {ok:false,failed:ReadonlyArray<'settings'|'manifest'>}
| {ok:false,reason:'no-manifest'}. An absent manifest is now a failure (exitCode 1,
error logged), not a no-op that silently returned true. Applies PF-015: each artifact's
write is evaluated independently into `failed`; the no-manifest branch gets its own
discriminant so no caller can accidentally suppress it.
2. handleBare (TTY path) reuses loadFlagContext — the same manifest guard used by every
mutating handler (--enable/--disable/--set/--unset). Guard fires BEFORE the TUI import
and BEFORE any settings.json write. This kills the silent-factory-reset path: TUI seeded
from {} could write settings.json while manifest was never updated; next devflow init
re-adopted registry defaults and silently reverted the user's choices.
3. Non-TTY bare path is unchanged — still degrades gracefully (reads manifest for status
table; shows registry defaults when absent; never writes).
4. Three callers of persistFlagConfig updated to check result.ok instead of boolean.
Tests (RED → GREEN):
- bare TTY + no manifest → hard-refuse, exitCode 1, p.log.error, settings.json not written
- bare TTY + corrupt manifest → hard-refuse, exitCode 1, p.log.error, settings.json not written
- --set no manifest (REG-SF2) → exitCode 1, settings.json not written (was exitCode-only)
Applies PF-015 (per-artifact convergence independently evaluated and reported).
Cites ADR-016 (vocabulary unchanged across surfaces).
…ignal; PERF-L2 dedup
ARCH-M7a — Chevron composition: separate cyan segments
Each chevron is now its own self-contained cyan() call rather than wrapping the
entire `‹ value ›` string. Inner RESETs from green('enabled') / bold(str) no
longer kill the outer cyan, so the closing chevron renders styled on every
focused row. (applies ADR-016 amendment lesson)
ARCH-M7b — Caret survival beyond budget
renderBuffer now accepts a `budget` parameter and windows the plain buffer around
the caret BEFORE inserting inverse(). Previously renderRow called
truncateVisible(bufStr, budget) on the already-styled output, stripping the ESC[7m
caret whenever the buffer exceeded 42 visible chars (80-col frame).
ARCH-M7c — Deviation signal: bold not cyan
formatValue's non-boolean deviation path changed from cyan(str) to bold(str).
cyan is now exclusively the focus indicator (chevron wrapper); bold signals
"deviates from devflow default". Vocabulary comment block updated.
(applies ADR-016 amendment lesson — one colour, one semantic)
PERF-L2 — Duplicate filter in renderFrame
totalDirty is computed once and reused for both the summary line and the unsaved
line. Removes the identical rows.filter() call that appeared 50 lines later.
Three RED→GREEN pinned tests added (escape-sequence assertions on the specific
cursor/non-cursor row, not whole-frame joins per the review's anti-pattern flag).
…ead after TUI REL-H1: handleBare now requires stdin AND stdout to both be TTYs before entering the interactive branch (previously checked stdout alone). runTui bails with a rejection before writing ENTER_ALT when stdin is not a TTY and no io.stdin was injected. REL-M2 + PERF-L4: readSettingsSafe parses once and rejects non-plain-object content (null/array/scalar) with a clear error. applyFlags/stripFlags in flags.ts gain the same plain-object guard at the sink (PF-023). REL-M3: handleBare re-reads settings.json AFTER runFlagsTui returns so concurrent writes (e.g. proxy --enable setting ANTHROPIC_BASE_URL) are not clobbered by the stale pre-TUI snapshot (PF-022). ARCH-M2 + PERF-L1: persistFlagConfig accepts the already-read manifest as a parameter, eliminating the second readManifest call (and the implicit self-heal write it caused). All callers (handleSetBooleans, handleSet, handleUnset, handleBare) thread the manifest from loadFlagContext. Regression tests added for each issue across tui-terminal.test.ts, flags.test.ts, and flags-cli.test.ts (REL-H1 non-TTY guard, REL-M2 sink guards, REL-M3 vi.doMock concurrent-write scenario).
… TUI exit
CPLX-SF3: replace 4-level nested ternary in handleList (kind label) with
describeFlagKind(flag: ClaudeCodeFlag): string in src/core/flags.ts.
Exhaustive switch — TypeScript narrows each arm so the per-kind import()
casts at the call site are gone. Output strings are byte-identical to the
former ternary (regression test proves it for all 28 registry flags).
CPLX-SF4: replace triple-nested conditional + inline import() cast in the
--set Expected:-hint line with expectedInputFor(flag: ClaudeCodeFlag): string
in src/core/flags.ts (next to describeFlagKind). Same exhaustive-switch
shape, same byte-identical output guarantee via regression test.
CONS-M6: the flags TUI exit path now uses p.outro instead of raw
process.stdout.write: p.outro(color.green('Flags saved.')) and
p.outro(color.dim('No changes made.')), matching agents.ts (line 584/605).
handleList and handleStatus lacked a closing outro — each now ends with
p.outro(color.dim('Use --enable / --disable / --set / --unset …')),
matching the hud.ts:141 / learning.ts:47 house style.
Tests: 48 CLI tests + 208 core tests all pass; 36 new tests added for the
two helpers (kind-label parity, expected-input parity, every-registry-flag
coverage).
… applyTuiResult seam
TS-M2: remove two redundant `as` assertions in state.ts — `(flag.values as
readonly string[])` inside a narrowed `case 'enum'` block (flag is already
EnumFlagDef with values: readonly string[]) and `[...flag.values] as
FlagsRecordValue[]` (string[] is assignable to readonly FlagsRecordValue[]
without a cast). Update the JSDoc example in the FlagRow.stops comment to
match. tsc --noEmit validates both deletions.
TS-S1: eliminate non-null assertions where narrowing is available.
(a) reduceEditMode now accepts `editing: EditState` as a third parameter;
the caller in `reduce` passes the already-narrowed `state.editing`
(guarded by `state.editing !== null`) — no ! needed.
(b) handleSetBooleans: collect flags into `flagDefs[]` during the validation
loop; the success log iterates `flagDefs` directly, eliminating the
`lookupFlag(id)!` re-lookup after the guard.
(c) handleUnset: same pattern — collect `flagDefs[]` during validation,
iterate for the neutral-value mutation, no re-lookup.
TEST-M5: extract `applyTuiResult` from handleBare (exported) to close the
interactive-surface coverage gap (applies PF-017(c)). The function owns the
save/cancel dispatch and the persistFlagConfig call; handleBare retains only
stream ownership, the conditional settings.json re-read (REL-M3), and the
`p.outro()` call.
Also: export `PersistResult` (return-type component of applyTuiResult);
move buildFlagRows + collectFlagRecord to static imports from flags-view/state.js
(pure, no TTY machinery); keep runFlagsTui lazy in handleBare.
Seam test in tests/flags-cli.test.ts drives runFlagsTui with PassThrough
streams, feeds its result to applyTuiResult, and asserts the whole post-state
of both artifacts (PF-015 shape), covering save and cancel('unchanged') paths.
applies PF-015 (whole-state seam test)
applies PF-017(c) (closing interactive-surface coverage gap)
Co-Authored-By: Claude <noreply@anthropic.com>
…JSDoc (D1) ARCH-M3: drop buildFlagRows registry parameter — function owns FLAG_REGISTRY directly; no caller ever passed anything but the global. Update both call sites (flags.ts, init.ts) and all tests. Remove FLAG_REGISTRY from init.ts import and from flags-cli.test.ts import (no longer needed after arg drop). ARCH-M4: embed def: ClaudeCodeFlag on FlagRow so commitEdit and collectFlagRecord no longer reach back into the module-global registry via findFlag. Delete FLAG_HINT_MAP from render.ts (dead — row.hint already holds flag.hint); use selectedRow.hint directly. Remove findFlag from state.ts imports (applies ADR-003). CPLX-SF7: move recordToTui / tuiToRecord from state.ts to core/flags.ts, next to neutralValueOf — their definition dependency (PF-017 one-shared-definition corollary). Export from flags.ts; import into state.ts. Glue-rule documentation travels with the functions to their new home. CONS-S1: delete unknown-flag else-branch in collectFlagRecord — structurally impossible after ARCH-M4 (row.def is always defined; rows are registry-derived). Replace with JSDoc invariant on buildFlagRows and on collectFlagRecord (applies ADR-003 leave-the-end-state). DOC-M4: add JSDoc to FlagsIntent (none/save/cancel/abort semantics + load-bearing cancel-vs-abort distinction at the init.ts consumer), ReduceResult (intent loop semantics), and FlagsTuiResult (action discrimination + cancel-vs-abort note). All 377 tests pass. npx tsc --noEmit clean.
Three call sites left runFlagsTui/runAgentsTui rejection unhandled, and program.parse() did not await async handlers, so any rejection surfaced as an unhandled rejection with a bare stack. - src/cli.ts: program.parse() → await program.parseAsync() so async handler rejections propagate instead of surfacing as unhandled. - src/cli/commands/flags.ts (handleBare): wrap runFlagsTui in try/catch — on rejection: p.log.error + exitCode 1, no settings write. - src/cli/commands/agents.ts: wrap runAgentsTui in try/catch — same shape as flags (error + exitCode 1, no partial write). - src/cli/commands/init.ts: wrap runFlagsTui in try/catch — on rejection: log + continue with seeded defaults; init must not abort mid-run after assets are partially installed (PF-009 spirit). - tests/flags-cli.test.ts: add rejection-path test for handleBare using vi.doMock (C3 precedent); asserts p.log.error, exitCode 1, and no settings.json write. Applies PF-014 (exitCode not exit()).
…ntradicting name, typed-input commit path
Applies PF-018 (mechanisms 4 and 7) — each test now exercises a named behaviour
and would fail if the production code reverted.
TEST-C1 (vacuous assertions — 9 render + 1 terminal):
- render.ts per-kind tests (:140,:148,:168,:177,:187): assert the SPECIFIC cursor
row (lines.find ❯-prefix) instead of the joined frame; add negative controls.
The frame always contains 'enabled', 'disabled', 'unset', and devflow-default
numbers from other flags, making whole-frame containment unconditionally true.
- dirty-dot clean test (:216/:226): replace Array.isArray(lines) with
not.toContain('●') on the ANSI-stripped joined frame.
- cursor indicator (:235): assert exactly one line starts with '❯' (and names
the expected flag) instead of the disjunction that included '→' from the hint.
- scroll indicators (:319,:340): assert lines[3] (upIndicator slot) and
lines[7] (downIndicator slot) by exact layout position, not joined.includes
whose '↑'/'↓' always match the footer keybinding line.
- terminal cancel test (:194/:203): toEqual(rowsIn) instead of toBeDefined(),
so the test actually observes the "unchanged" claim.
TEST-M3 (buffer-clamp):
- Replace toBeLessThanOrEqual(64) with exact: toBe(BUFFER_MAX_LEN),
toBe(BUFFER_MAX_LEN) for caret, toBe('a'.repeat(BUFFER_MAX_LEN)) for content.
Import BUFFER_MAX_LEN (single source of truth). The old assertion passed when
insertChar was a no-op (buffer length 0 satisfies ≤ 64).
TEST-SF2 (self-contradicting test name):
- Remove '007 is a valid input … actually NO …' from the valid-inputs describe.
- Replace the parallel '007 → error' test in invalid-inputs with it.each over
both number flag ids [subagent-spawn-depth, max-concurrent-subagents].
TEST-S2 (commit path via typed input):
- Extract typeInto helper to module scope so commit tests can reuse it.
- Convert number-commit test: enter edit mode → backspace '40' → type '50' →
enter (exercises backspace + insertChar path, not direct buffer injection).
- Convert string-commit test: typeInto('default-model', [...'claude-3-5-sonnet'])
→ enter (exercises insertChar for each char, buffer starts empty).
TEST-S3 (viewportHeight invariant):
- Already covered by the 'viewportHeight ownership' describe added by D3
(lines 478–505 of render test): two tests set state.viewportHeight directly
and assert lines.length === FIXED_ROWS + that height. No additions needed.
RED proofs (production breaks → observed failures, all reverted):
- :140 enabled: green('ACTIVE') → cursorRow missing 'enabled'
- :148 disabled: yellow('INACTIVE') → cursorRow missing 'disabled'
- :168/:187 unset: dim('(none)') → cursorRow missing 'unset'
- :177 number value: String(99999) → cursorRow missing '40'
- :216 no dirty: always yellow('● ') → plain contains '●'
- :235 cursor: always ' ' prefix → zero lines start with '❯'
- :319/:340 scroll: always '' indicators → lines[3]/[7] empty strings
- TEST-M3: insertChar clamp removed → buffer.length 70 ≠ 64
- TEST-S2 number: backspace no-op → buffer stays '40' not '50'
- TEST-S2 string: insertChar no-op → buffer empty, commit → null not 'claude-3-5-sonnet'
- terminal cancel: rows modified on cancel → toEqual(rowsIn) fails
…ate, e2e timeout TEST-H1: Add 9 caret-manipulation branch tests to flags-view-state.test.ts covering backspace@caret=0 no-op, delete-at-caret, delete-at-end no-op, home, end, left/right and their clamp boundaries. RED proof: broke case 'home' to 'return state' — 5 tests failed, reverted. Add normalizeKey it.each table in tui-terminal.test.ts: all 12 readline key names → normalized names, ctrl-c pre-check, and the default-branch (unknown key / undefined str) paths. applies PF-018. TEST-M2: Correct flags-cli.test.ts header — it claimed "full JSON deep-equal, not key-picking" but only key-picked. Upgrade one representative test per mutation verb (--enable, --disable, --set, --unset) to toEqual on the COMPLETE settings.json and COMPLETE manifest.features.flags record. Discovery: every persistFlagConfig call writes 'view-mode':'default' via convergeFlagsIntoSettings even when view-mode is not in the input record (neutralValue keeps it out of settings.json but lands it in the manifest). applies PF-015 + ADR-003. TEST-M4: Pass SUBPROCESS_TIMEOUT_MS as the third argument to every it.skipIf(!CLI_BUILT)(...) call in init-e2e-flags.test.ts (including the test added in a later batch). Without it vitest's 5s default fires before the 60s subprocess timeout on a loaded CI runner.
- Remove `lookupFlag` wrapper in flags.ts (cmd): all three callers use truthiness; `findFlag` already returns a falsy `undefined` on miss. Replace all call sites with `findFlag` directly. - Strip transition-residue comments across 5 files: - "Validate-then-discard was pure overhead" in readSettingsSafe - "the former triple-nested ternary" in describeFlagKind/expectedInputFor - "Gains … (were leaking raw bytes in agents-view)" in normalizeKey - "was the source; this is the generalisation" in tui/terminal.ts module doc - "(Before this fix, renderRow called truncateVisible …)" in renderBuffer - "Fix 1" / "Fix 4" labels in agents-view/render.ts - "replacing the deleted switch/never guard" in agents-view/terminal.ts
This was referenced Aug 25, 2026
Owner
Author
Resolution SummaryFull summary withheld (public repository).
Full report: /Users/dean/Sandbox/devflow/.devflow/docs/reviews/feat-flags-typed-registry/2026-08-25_1217/resolution-summary.md (not committed; ask the author) Posted by devflow |
…line TUI, registry tests
Changes:
1. Effective value rendering (D-EFFDV): replaces 'unset' across all 4 sites.
- effectiveDisplay(flag, value) → { text, isDefault } as single-definition seam.
- boolean: 'on'/'off'; enum null: neutralValue text; number null: devflow/upstream default;
string null: '—'. formatFlagValue delegates to effectiveDisplay.
- Sites A (TUI formatValue), B (--enable/--disable), C (--list defaultLabel),
D (formatStatusRows not-adopted message) all updated.
2. Per-flag blurb field (D-BLURB): 28 blurbs added, all ≤ 30 chars.
- blurb: string on FlagDefCommon; populated in FlagRow.
- TUI layout rebalanced: COL_VALUE 46→16, COL_BLURB 30 (same total).
- Blurb shown as dim trailing column in data rows and HINT in column header.
3. Inline TUI mode (D-INLINE): screen: 'alt' | 'inline' added to RunTuiSpec.
- Inline mode: no ENTER_ALT/LEAVE_ALT; cursor-up repaints; ERASE_BELOW on exit.
- Height clamped to stdout.rows - INLINE_MARGIN (2).
- runFlagsTui passes screen: 'inline'; agents-view unchanged (alt mode default).
4. Tests: 22 new tests — effectiveDisplay vocabulary table, blurb hard-cap registry
test, persistence round-trip, inline mode driver tests (ENTER_ALT absent, cursor-up
present, ERASE_BELOW on exit, alt mode unchanged). Updated vocabulary assertions
from enabled/disabled/unset → on/off/<effective-default>.
Captures effectiveDisplay one-definition seam (D-EFFDV), per-flag blurb field (D-BLURB), inline TUI mode (D-INLINE), and updated vocabulary (on/off replaces enabled/disabled; never shows 'unset' at render sites) introduced in commit 82a9c83.
…s non-interactively (D40)
The interactive flags editor is removed from devflow init (both Recommended and
Advanced paths). init now applies seed.flags directly — seeded defaults on fresh
install, preserved values on re-init (ADR-014). Users customize flags exclusively
via the standalone `devflow flags` command, which keeps its inline TUI.
Changes:
- Drop dynamic import of runFlagsTui/buildFlagRows/collectFlagRecord from init.ts
- Remove abort/save/cancel handling block (flags TUI abort path is gone)
- Replace TUI invocation with D40 JSDoc comment + single outcome line
('Flags: N active — customize any time with devflow flags')
- Remove now-unused getDefaultFlagsRecord from flags.ts import
- Update viewModeExplicit comment: no longer set by TUI save; only set by --reset
Tests:
- Add (c) assertion to fresh-install test: no 'Opening the flags editor' in transcript
- Add (b) test: re-init preserves user-set flag (tui=false), adopts defaults for absent flags
- Update file-level doc comment to reflect new test scenarios
Co-Authored-By: Claude <noreply@anthropic.com>
Claude Code Flags section: - Remove '(also used by the init Advanced path)' from TUI description - Document blurb registry field: ≤30-char per-flag short hint, shown as dim HINT column in TUI and --status rows - Document effectiveDisplay vocabulary: booleans render on/off; neutral/ unset enum shows neutralValue dim; unset number shows applicable default dim with '(default)' suffix; unset string shows '—'; literal 'unset' is never a displayed value; active non-boolean renders plain or bold - Document RunTuiSpec.screen: flags editor uses 'inline' (normal scroll buffer, no alt-screen); agents-view defaults to 'alt' Two-Mode Init section: - Replace stale 'Advanced path opens the interactive flags editor' clause with accurate description: both paths apply seeded flag values non- interactively (fresh install = registry defaults; re-init = existing values preserved, new flags adopt defaults per ADR-014; view-mode resolved from settings.json at seed time) and emit outcome line pointing to 'devflow flags' for customization; Advanced adds proxy prompt as before Project Structure / file-organization.md: - Update flags-view/ comment to reflect current role: standalone devflow flags command, inline screen mode (not used by init) Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
string[]flags with a discriminated-unionClaudeCodeFlagregistry. Each flag carries kind (boolean|enum|number|string), target (env|setting), default value, and display metadata. 8 new upstream-verified flags added (max-concurrent-subagents default=40, subagent-spawn-depth, workflow-size-guideline, etc.).Record<string, FlagValue|null>instead of parallelstring[]/knownFlags/viewModefields. Bridge shims (legacyIdsToRecord,applyViewMode,stripViewMode,getDefaultFlags) deleted.runFlagsTui) replaces the multiselect in the Advanced wizard. Agents-view reuses the same genericrunTuishell.resolveExistingViewModenow runs BEFOREstripFlagsin the settings apply block. Prior order strippedviewModefrom settings before reading it, causing silent viewMode loss on every re-init.devflow flags):--list/--status/--enable/--disable/--set/--unsetwith input validation and typed persistence.Changes
Core (
src/core/)flags.ts: discriminated-unionClaudeCodeFlagtype; 20 flags in typed registry;applyFlags(json, FlagsRecord)new canonical API;sanitizeFlagsRecord,migrateLegacyFlagsToRecordfor backward compat; deprecated shims deleted in Phase 6manifest.ts:FlagsRecordinManifestFeatures; in-reader heal (migrateLegacyFlagsToRecord) writes healed manifest back — noMIGRATIONSentry neededCLI (
src/cli/commands/)init-seed.ts:InitSeed.flags: FlagsRecord;resolveSeedFlagsreturnsFlagsRecord(adopt defaults for absent flags);resolveInitSeedsetsflags['view-mode']in-placeinit.ts: TUI flags editor in Advanced path; fold-before-strip ordering fix; manifest write usesFlagsRecorddirectly; noknownFlags/viewModeresidueuninstall.ts:stripViewModeremoved (covered bystripFlagsviaview-moderegistry entry)flags.ts: newcreateFlagsCommand()factory with full CRUDproxy.ts:UNKNOWN_MODEL_WINDOW_ENVmanaged as a pair withANTHROPIC_BASE_URLTUI (
src/cli/tui/,src/cli/flags-view/,src/cli/agents-view/)runTui<S,A>(spec)driver insrc/cli/tui/terminal.tsflags-view/: state, render, terminal, index — full keyboard flags editoragents-view/: thinned to adapter overrunTui(removes duplicated TUI driver)Tests
Breaking Changes
Bare
devflow flagson non-TTY: now prints a status table and exits with code 1 (was: usage line, exit 0). Any script invoking baredevflow flagsmust use--status(exit 0) or handle exit 1. (Already documented in docs/cli-reference.md.)Seven settings.json/env keys become Devflow-managed:
ANTHROPIC_DEFAULT_MODEL,CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS,CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH,CLAUDE_CODE_GOAL_CHECKIN_MINUTES,CLAUDE_CODE_ENABLE_TODO_TOOLS(env);workflowSizeGuideline,spellcheck(settings). Hand-set values are PRESERVED by the fold-before-strip pipeline (adopted into the manifest record on firstdevflow initor flags mutation; explicitdevflow flags --set/--unsetstill wins).max-concurrent-subagentsadopts the devflow default of 40 (upstream default: 20) only when the key is absent in settings.json. Full removal still occurs ondevflow uninstall.The manifest in-reader heal is backward-compatible: old
flags: string[]manifests are healed on firstdevflow initafter upgrade. All external interfaces (applyFlags,FlagsRecord) are new additions.Reviewer Focus Areas
src/cli/commands/init.ts~line 1629): ordering ofresolveExistingViewMode→stripFlags→applyFlagsis criticalresolveSeedFlagsadoption (src/cli/commands/init-seed.ts): absent key = new flag → adopt registry default (ADR-014)migrateLegacyFlagsToRecord(src/core/flags.ts): knownSet membership gates entry/absence;view-modehandled separatelyrunTuigeneric shell (src/cli/tui/terminal.ts): shared driver reused by both flags-view and agents-viewapplyFlagsnumber-flag path (src/core/flags.ts):buildPayloadstringifies for env targets (String(40)→'40'); 0 is active, null is neutralQuality Gates
Simplify pass (fca49ff — transition-residue cleanup):
viewMode-only referencesScrutinize 9-pillar pass (3ac14d6 — 6 defects fixed with RED-proven tests):
getCommand()did not return process.exit() result on programmatic error pathsEvaluate alignment audit (3352da7 — 12-item alignment pass, re-checked ALIGNED):
sanitizeCellon all disk-persisted values (flags display, agent model cache)flags: string[] | nullfield deleted)Validate green at every step:
tests/proxy.test.tsNote on TUI shared shell: The generic shared-shell fixes are:
These are shared by flags-view and agents-view. Note:
agents-view's viewport re-clamp on resize remains deliberately unchanged (planned as a separate follow-up PR).Pending Ship Gates
removeCaptureHookscrashes the entire init settings pass on a hook entry missing its hooks array (src/cli/commands/capture.ts:93+ the warning-only try/catch atinit.tssettings pass). Recommend filing an issue.