Skip to content

feat(init): state-aware re-init, registry accuracy, complete uninstall#267

Merged
dean0x merged 30 commits into
mainfrom
feat/init-composability
Jul 24, 2026
Merged

feat(init): state-aware re-init, registry accuracy, complete uninstall#267
dean0x merged 30 commits into
mainfrom
feat/init-composability

Conversation

@dean0x

@dean0x dean0x commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the "Devflow Init Composability & Registry Accuracy Overhaul" plan — six workstreams:

WS1: State-aware initdevflow init now reads prior state (manifest + .devflow/config.json + settings.json) before prompting. Re-init (interactive) skips the Recommended/Advanced question and pre-seeds every prompt from actual state — Enter-through is a no-op. Non-interactive/--recommended re-init preserves all existing state and adopts defaults only for genuinely new flags/features/plugins, tracked via new knownFlags/knownPlugins manifest snapshots. New --reset flag = factory reset (also forces viewMode to default; --reset --plugin rejected). Pure seeding helpers in new src/cli/commands/init-seed.ts (resolveInitSeed, resolveSeedFeatures/Flags/Plugins, applyCliToggles, resolveResetGatedInputs, resolveNonSelectableOptionalCarry); viewMode helpers (resolveExistingViewMode/resolveFinalViewMode) in src/core/flags.ts; readConfigIfPresent in feature-config. Settings write now gates viewMode (explicit selection wins, external /focus preserved otherwise), strips legacy teammateMode, and warns instead of silently swallowing write failures.

WS2: Registry accuracy — DEVFLOW_PLUGINS agent arrays aligned with actual subagent_type spawns in compiled commands (debug +simplifier+knowledge; implement/resolve/self-review +knowledge; plan −git−knowledge; release −synthesizer). New registry Guards 4 (command forward/reverse integrity) + 5 (spawn-vs-declaration, would have caught all six deltas). getAllCommandNames() added; skills arrays documented as ownership declarations.

WS3: Subcommand safetydevflow list deleted outright (broken; no back-compat). Unknown subcommands now exit 1 with an error instead of exiting 0 via help.

WS4: Stale asset cleanup — Installer sweeps stale devflow:* skill dirs on full install via registry prefix-diff (bare pre-namespace dirs untouched — PF-012 invariant intact); replaces reliance on manual legacy-list upkeep for future removals.

WS5: Complete uninstall — Uninstall is now a scope-aware complete cleanup: user-scope removes the entire devflow dir behind an explicit confirm that enumerates user-authored content to back up (skill/rule shadows, preference-profile.md, learning.json); declined/non-TTY falls back to install-artifacts-only; local-scope never touches project data (memory/, learning/, features/, docs/, config.json).

WS6: HUD self-healinghud --enable is self-healing (repairs statusLine + manifest even when already enabled); installer hard-errors on missing DECLARED agent/skill/rule sources (parity with commands; shadow paths stay tolerant).

Behavior Change

Non-interactive re-init now preserves the prior plugin set instead of resetting to all-non-optional plugins. devflow init --reset restores the old behavior.

Verification

  • Full test suite: 66 test files, 1,915 tests green; build zero-warnings; tsc --noEmit clean.
  • Quality pipeline: Validator (full) → Simplifier → Scrutinizer (fixed a P0: --reset previously failed to force viewMode default when settings.json held /focus) → re-validate → Evaluator (ALIGNED after preserving non-selectable optional plugins across re-init) → Tester: 10/10 acceptance scenarios PASS, including the original composability bug (flags --disable tui + memory --disable survive init --recommended), knownFlags/knownPlugins snapshots, old-manifest migration adopting nothing, --reset semantics, registry subset install (debug plugin gets simplifier+knowledge agents), orphan sweep full-vs-partial, uninstall scope safety, HUD self-heal, external /focus preservation.
  • Security: Snyk code scans on changed first-party code: 0 issues.

dean0x and others added 17 commits July 23, 2026 18:56
Six registry deltas verified against dist/commands/*.md grep output:
  - devflow-debug: add simplifier, knowledge (debug.md spawns both)
  - devflow-implement: add knowledge (implement.md spawns Knowledge writeback)
  - devflow-resolve: add knowledge (resolve.md spawns Knowledge writeback)
  - devflow-self-review: add knowledge (self-review.md spawns Knowledge writeback)
  - devflow-plan: remove git, knowledge (plan.md uses gh directly; knowledge_load
    only, no Knowledge agent spawn)
  - devflow-release: remove synthesizer (release.md spawns only Validator + Git)

Also adds PluginDefinition JSDoc D-series comments clarifying that skills[] are
ownership declarations (universal-install invariant, not usage lists) and that
agents[] must match compiled command subagent_type spawns exactly.

Adds getAllCommandNames() helper near the existing getAll* cluster.

Adds Guard 4 (command source forward/reverse + dist check) and Guard 5 (build-
gated bidirectional spawn accuracy) to tests/registry-integrity.test.ts. Guard 5
uses normalize(name) = name.replace(/-/g,'').toLowerCase() to reconcile registry
filenames (bug-analyzer) with frontmatter subagent_type values (BugAnalyzer).
Guard 5 correctly failed pre-fix on all six deltas, now passes.
The devflow list command was the sole user of src/cli/commands/list.ts
and its utility functions (formatFeatures, resolveSecurityTriState,
getPluginInstallStatus, formatPluginCommands, resolveScope). No replacement
or back-compat shim — clean end-state per ADR-003.

Removes:
  - src/cli/commands/list.ts
  - tests/list-logic.test.ts
  - import + addCommand in src/cli.ts
  - help-text example mentioning devflow list
  - devflow list entries in README.md, docs/cli-reference.md,
    docs/reference/file-organization.md
On full install only (!isPartialInstall — mirrors the existing cleanup guard),
installViaFileCopy now scans ~/.claude/skills/ for devflow:* directories and
removes any whose bare name (via unprefixSkillName) is no longer present in
getAllSkillNames(). The devflow: namespace is exclusively Devflow's so removal
is safe. Bare (pre-namespace) dirs are intentionally untouched — they are
handled by the frozen LEGACY_SKILLS_* lists in legacy.ts (avoids PF-012).

Adds SKILL_NAMESPACE, unprefixSkillName, getAllSkillNames to the installer
import from plugins.ts (they were already exported, just not imported here).

Tests (installer-new.test.ts, 3 new):
  - planted devflow:zzz-orphan removed on full install
  - devflow:zzz-orphan retained on partial (--plugin) install
  - bare non-prefixed dir untouched on full install (PF-012 guard)

Also fixes plugins.test.ts assertion: 'git' is now first declared by
devflow-implement (not devflow-plan, which no longer lists it per WS2 fix).
A declared source file absent from disk is a build/packaging failure —
throw a plain Error with a clear message rather than silently skipping.
This matches the existing command pattern and surfaces broken installs
immediately rather than deploying a partial plugin set.

Scope: only DEVFLOW_PLUGINS-declared sources hard-error. Shadow
validation paths remain tolerant (ADR-010: invalid shadow →
warn-and-install-source). Per-item copy failures are still isolated
(avoids PF-009 blast-radius).

Tests: 3 new cases in installer-new.test.ts covering agent, skill, and
rule missing-source scenarios.

Co-Authored-By: Claude <noreply@anthropic.com>
User scope (~/.devflow/): offer full directory removal behind an explicit
confirm gate that enumerates user-authored content worth backing up
(skill shadows, rule shadows, preference-profile.md, learning.json).
Only items that actually exist are listed. If the user declines or the
session is non-interactive, fall back to install-artifacts-only removal
(scripts/ already removed + manifest.json).

Local scope (gitRoot/.devflow/): always remove only install artifacts
(scripts/ + manifest.json). Never touch project data directories
(memory/, learning/, features/, docs/, config.json).

Also removes manifest.json in both scopes (was previously left behind).

The post-hoc shadow-leftover warning block is replaced by the pre-deletion
confirm gate, which captures and presents user content BEFORE any removal.

New export: enumerateUserDevFlowContent() — pure async helper, unit-tested
in uninstall-logic.test.ts (12 new cases). computeShadowLeftoverWarnings
retained for existing test coverage.

Co-Authored-By: Claude <noreply@anthropic.com>
Remove the already-enabled early-return from the --enable path so that
statusLine-ensure and syncManifestFeature always run. This mirrors the
--disable self-healing design (D1) that already runs unconditionally.

Before: calling --enable when config.enabled=true returned after logging
"HUD already enabled", bypassing syncManifestFeature entirely. A drifted
manifest (hud=false while config says enabled) required a
--disable/--enable cycle to repair.

After (D2): --enable always calls syncManifestFeature so a single
--enable call repairs any drift between config, statusLine, and manifest
regardless of the prior config state.

Tests: 2 new behavioral tests in hud-enable-selfheal.test.ts:
- already-enabled --enable repairs a drifted manifest (hud=false → true)
- already-enabled --enable adds a missing statusLine to settings.json

Co-Authored-By: Claude <noreply@anthropic.com>
…source

WS6a changed installRuleFile to throw when a declared rule source is absent.
Three tests in rules.test.ts documented the old silent-skip behavior; update
them to assert the new Error throw. Keeps description, addsrejects.toThrow
matcher, removes now-false 'skipped' return expectations.
Add three groups of pure helpers that Phase 4 will wire into the init
prompts (no behavior change until then):

src/core/flags.ts
- resolveExistingViewMode(settingsJson): ViewMode | undefined
  Extracts a non-default viewMode from a settings JSON string so callers
  can chain via ?? to manifest / 'default' fallbacks.  Returns undefined
  for absent/default/unrecognised values rather than 'default', enabling
  clean ?? chaining in resolveInitSeed.
- resolveFinalViewMode(current, selected, explicit): ViewMode
  Composes explicit CLI flag, non-default existing setting, and prompt
  selection into the final value to write (Phase 4 wire-in).

src/core/feature-config.ts
- readConfigIfPresent(projectRoot): Promise<FeatureConfig | null>
  Null-returning variant of readConfig — distinguishes "not configured
  yet" (null) from "all features enabled by default" (DEFAULT_CONFIG).
  Applies ADR-001: .devflow/config.json is the source of truth; null
  means the file is absent or unreadable.

src/cli/commands/init-seed.ts (new)
  FeatureSeed / FEATURE_DEFAULTS / InitSeed types plus five pure helpers:
  - resolveSeedFeatures: manifest + projectConfig → FeatureSeed
    projectConfig wins for memory/learning/knowledge whenever present
    (ADR-001); manifest/defaults govern ambient/hud/rules.
  - resolveSeedFlags: enabledFlags + knownFlags + registry → string[]
    null=fresh → default-ON set; undefined knownFlags → adopt nothing
    (migration-safe); else union existing with newly-added default-ON
    flags (never auto-adds default-OFF flags).
  - resolveSeedPlugins: manifestPlugins + knownPlugins → buckets
    null=fresh → non-optional workflow plugins preselected; undefined
    knownPlugins → split into buckets, adopt nothing; else split + adopt
    new non-optional selectable plugins ∉ knownPlugins.
  - resolveInitSeed: composes all three + viewMode resolution
  - applyCliToggles: per-key toggles.X ?? base.X

Applies ADR-013: seeding helpers are CLI-init-specific, live beside
init.ts in src/cli/commands/ rather than src/core/.
Uses a local ManifestWithKnownFields intersection type to forward-read
knownFlags/knownPlugins (added to ManifestData in 7b) without
touching manifest.ts in this commit.

Tests: 53 new tests (init-seed.test.ts 30, flags.test.ts +15,
learning-config.test.ts +8). Full suite: 65 files, 1896 tests, all green.

Co-Authored-By: Claude <noreply@anthropic.com>
Add optional snapshot fields to ManifestData:
- features.knownFlags?: string[] — FLAG_REGISTRY ids at last install
- knownPlugins?: string[] — DEVFLOW_PLUGINS names at last install

readManifest self-heals non-array/absent values to undefined
(never partial/garbage). init.ts manifest write block populates
both snapshots from the live registries.

Tests: 7 new round-trip, absent→undefined, and non-array→undefined
tests in tests/manifest.test.ts.

TASK-2025-07-23_init-composability-phase3
No behavior change — pure read hoist for Phase 4 pre-seeding.

Move existingManifest, project FeatureConfig, and settings.json reads
to before the plugin multiselect. getInstallationPaths(scope) is
deterministic for a given scope, so the early call is safe. The
authoritative error gate for failed path resolution stays at the
install-begins spinner.

Computes _seed = resolveInitSeed(...) immediately after the hoisted
reads. _seed is unused in Phase 3; Phase 4 wires it into plugin and
feature prompts.

Removes duplicate late reads:
- existingManifest: was re-read via readManifest(devflowDir) at ~L869
- userSettingsJson: was re-read from claudeDir/settings.json at ~L882

New imports: ManifestData (type), readConfigIfPresent, FeatureConfig
(type), resolveInitSeed from init-seed.ts.

TASK-2025-07-23_init-composability-phase3
…reset flag

Implements plan commits 7d + 7e + 7f:

7d — Seed interactive prompts + skip mode prompt on re-init
- Renames _seed → seed (touched every use)
- workflowInitialValues uses seed.workflowPlugins (prior selection on re-init,
  non-optional defaults on fresh install)
- Language multiselect gains initialValues: seed.languagePlugins
- Re-init routing gate: when seedManifest !== null (and not --recommended /
  --advanced / non-TTY), skip the Recommended/Advanced mode prompt entirely;
  print "Existing installation detected — press Enter to keep current settings."
- Feature toggle declarations initialised from seed.features.* instead of
  hardcoded true; enabledFlags = seed.flags; viewMode = seed.viewMode
- Advanced path: all six feature confirm prompts gain initialValue: seed.features.X
- Flag multiselect: initialValues: seed.flags (removes getDefaultFlags() call)
- viewMode select: initialValue: seed.viewMode; sets viewModeExplicit = true
  so commit 7g's resolveFinalViewMode knows the selection was explicit

7e — State-aware non-interactive path
- Recommended branch: replaces six individual if-checks with applyCliToggles
  (precedence: explicit CLI flag > seed > registry default)
- enabledFlags and viewMode already seeded above; no redundant re-assignment
- Non-interactive re-init plugin preserve: when !options.plugin && !isTTY &&
  seedManifest !== null, sets selectedPlugins from seed.workflowPlugins +
  seed.languagePlugins (fixes the composability bug — prior plugin set is now
  preserved instead of resetting to all-non-optional)
- Summary line View mode now shows the real effective viewMode (seed value)
  — absorbs the fix/init-viewmode-summary-display branch intent

7f — --reset flag
- Adds reset?: boolean to InitOptions and .option('--reset', ...)
- Early validation: --reset + --plugin → p.log.error + process.exit(1)
- Introduces seedManifest / seedConfig (null when --reset, real values
  otherwise); resolveInitSeed called with seed inputs, not real values
- Routing gate and non-interactive plugin preserve both use seedManifest !== null
  so --reset shows the mode prompt again and takes fresh defaults
- viewModeExplicit = !!options.reset (forces viewMode 'default' through 7g gate)
- Real existingManifest is still used for installedAt preservation and upgrade
  messaging — not nulled by --reset

Tests: extends tests/init-seed.test.ts with 3 WS1 composability scenarios:
- Non-interactive re-init preserves existing workflow + language plugin selection
- Factory reset (null manifest) → fresh seed, not prior state
- applyCliToggles on seed correctly preserves non-overridden keys

Co-Authored-By: Claude <noreply@anthropic.com>
…undant warning

- Drop ManifestWithKnownFields type alias and extManifest cast from
  init-seed.ts — ManifestData now carries knownFlags/knownPlugins
  directly; the bridging type was a mid-commit artifact that outlived
  its purpose.
- Rename _earlyProjectConfig/_earlySettingsJson to earlyProjectConfig/
  earlySettingsJson in init.ts — these variables are used, not ignored;
  the underscore prefix misleadingly signals 'intentionally unused'.
- Rename block-local _earlyPaths/_earlyGitRoot to earlyPaths/
  earlyGitRootHoist to match.
- Collapse settingsConfigured boolean + post-try warn into a single
  catch message; the two separate warnings conveyed the same failure
  with duplicated guidance.
- Strip D-hoist-7c commit-reference tag from the hoist-reads comment;
  keep the explanation, drop the artifact identifier.
….json mode

Under --reset the REAL settings.json snapshot was still passed to
resolveInitSeed, so a persisted viewMode (e.g. /focus) surfaced as
seed.viewMode and — with viewModeExplicit=true — survived the factory
reset at write time, violating the USER-LOCKED contract '--reset forces
viewMode default'.

Extract resolveResetGatedInputs (pure) to null the manifest, config, AND
settings snapshot together under --reset, and wire it in. Add regression
tests covering the settings-snapshot path the prior --reset test missed
(it passed an empty '{}' snapshot instead of a real one).
The scope-aware cleanup replaced the post-removal leftover-shadow warning
with the enumerateUserDevFlowContent + confirm-gate flow, leaving
computeShadowLeftoverWarnings, its ShadowWarning type, and the now-unused
getDevFlowDirectory import as transition residue (production-dead, kept
alive only by tests). Remove the function, type, import, and its tests —
users are now informed of leftover content up front by the enumerator.
devflow-audit-claude (and any future non-selectable optional plugin)
was silently dropped on any plugin-less re-init — interactive or
non-interactive — because partitionSelectablePlugins excludes it from
the prompt buckets, so it never appeared in seed.workflowPlugins or
seed.languagePlugins, and the full-install dir wipe removed its
command dir before re-installing without it.

Root cause chain:
  - partitionSelectablePlugins excludes devflow-audit-claude (by design)
  - resolveSeedPlugins only carries selectable bucket entries
  - selectedPlugins therefore never contained audit-claude on re-init
  - pluginsToInstall built from selectedPlugins → audit-claude absent
  - resolvePluginList (full install, !isPartialInstall) returns only
    installedPluginNames → audit-claude dropped from manifest too

Fix: add resolveNonSelectableOptionalCarry() to init-seed.ts —
a pure helper that returns manifest plugins that are optional and
absent from the selectable buckets. init.ts injects the carry set
into pluginsToInstall on plugin-less full re-inits only.

--reset falls out correctly: resolveResetGatedInputs sets seedManifest
to null → carry is empty → factory reset drops audit-claude as intended.

--plugin X partial installs are unaffected: resolvePluginList already
merges the full manifest list; command dirs are not wiped on partial
installs; the carry block is guarded by !options.plugin.

Co-Authored-By: Claude <noreply@anthropic.com>
Commander's root .action() fires for unrecognised subcommands (e.g. the
removed `devflow list`) because they land in program.args rather than
routing to a registered handler. The prior code called program.help()
unconditionally, exiting 0 for any input — including typos.

Check program.args.length inside the root action: when it is non-empty an
unknown subcommand was given, so emit an error via program.error() (which
writes "error: unknown command '<cmd>'" to stderr and exits 1). Bare
`devflow` with no arguments keeps the existing help + exit-0 path.

Add tests/cli-unknown-command.test.ts to assert the three required
behaviours: bare invocation exits 0, unknown commands exit 1, real
subcommands (init/flags/rules --help) remain unaffected.

Co-Authored-By: Claude <noreply@anthropic.com>
@dean0x

dean0x commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

PR Code Review — High-Confidence Findings

1. Precondition Guard + Pure Extraction (uninstall.ts:387)

Confidence: 80-85% (Architecture + Reliability)

The recursive fs.rm(devflowDir, { recursive: true, force: true }) is a destructive operation on user data with no invariant validation.

Recommended Fixes:

  1. Reliability (80%): Add a precondition assertion immediately before the delete:
// Guard the destructive op: devflowDir must be a resolved .devflow path, never home/root.
if (!devflowDir || path.basename(devflowDir) !== '.devflow' || devflowDir === os.homedir()) {
  throw new Error(`Refusing to rm -rf unexpected devflow dir: ${devflowDir}`);
}
  1. Architecture (85%): Extract the cleanup logic to a pure helper matching the pattern already established by resolveSecurityRemovalDecision. This makes the TTY/content/confirm decision matrix testable in isolation.

@dean0x

dean0x commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

2. JSDoc Accuracy — Always-Installed vs Optional (init-seed.ts ~line 152)

Confidence: 90%

JSDoc comment states: "Excluded always-installed plugins (devflow-core-skills, devflow-ambient, devflow-audit-claude)..."

However, in src/core/plugins.ts line 270, devflow-audit-claude is declared with optional: true, not as an always-installed plugin.

Fix: Update the comment to accurately reflect the plugin classification:

Excluded always-installed plugins (devflow-core-skills, devflow-ambient) and
optional auxiliary plugins (devflow-audit-claude) are filtered out by...

3. JSDoc References Non-Existent Flag (flags.ts ~line 1339)

Confidence: 85%

The resolveFinalViewMode JSDoc references a --view-mode flag that does not exist in the actual init command. The real triggers for viewMode selection are --reset + interactive selection.

Fix: Correct the JSDoc to reflect the actual implementation:

  • Remove reference to non-existent --view-mode flag
  • Document the actual triggers: factory reset (--reset) or interactive prompts during re-init

4. Transitional Residue Comments (init.ts ~lines 476, 755)

Confidence: 80%

Comments reference intermediate commit label "7g" and "commit 7g gate" — residue from the development process (ADR-003 transition residue).

Example: "// Apply per WS1 seeding + 7g gate: plug-carry only when..."

Fix: Remove these transitional references. Code should reflect the end-state, not intermediate development artifacts.


5. Test Guard Scoping Issue (tests/registry-integrity.test.ts:320)

Confidence: 85%

Guard 5 reverse-check runs inside the per-command loop but declares intent to verify "at least one command" spawns each declared agent.

Failure Scenario: When a plugin gains a second subagent_type-spawning command with a different agent subset (e.g., cmd-A spawning git + coder and cmd-B spawning synthesizer), Guard 5 emits false violations failing CI on correct code.

Fix: Accumulate spawned agents across all of the plugin's commands first, then run a single reverse check per plugin:

// build spawnedByPlugin: Map<pluginName, Set<normAgent>> in the command loop (forward check stays per-command)
// after the loop, for each plugin: for (const a of declared) if (!spawnedByPlugin.get(name)?.has(a)) violation(...)

6. Private State Mutation in Tests (tests/hud-enable-selfheal.test.ts:106)

Confidence: 85%

Test resets Commander's private _optionValues between tests, coupling test isolation to a library implementation detail that can change in any minor upgrade.

Failure Scenario: Commander renames/relocates the private field → the reset becomes a no-op → subsequent tests leak state silently with no signal of broken isolation.

Fix: Construct a fresh Command per test (extract the action into an exported handler), or parse commands with a freshly-built instance rather than mutating a shared singleton's internals.


7. Stale Compiled Output in Process Tests (tests/cli-unknown-command.test.ts:13)

Confidence: 80%

Test spawns dist/cli.js directly with no freshness/existence guard. The test script is vitest run with no pretest build.

Failure Scenario: Developer edits src/cli.ts and runs npm test without npm run build → test exercises stale dist/cli.js and reports false results.

Fix: Add a pretest build, build step in test setup, or guard checking dist/cli.js exists and skip-with-message otherwise (matching Guard 4/5 pattern already used in this PR).


8. Undocumented User-Facing Flag (docs/cli-reference.md ~lines 15-29)

Confidence: 95% (HIGH)

New --reset flag is a user-facing, factory-reset flag that destroys prior state, but it is not documented in the Init Options table. It appears in devflow init --help but not in the reference docs.

Fix: Add to the Init Options table:

| `--reset` | Factory reset — restore all defaults, ignoring prior installation state (mutually exclusive with `--plugin`) |

Note the mutual-exclusivity gate with --plugin (init.ts:201-204) is worth stating since it exits 1.

@dean0x

dean0x commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

Summary Comment: Medium-Confidence & Pre-Existing Findings

Medium-Confidence Issues (60-79%)

Security (65-70%): Uninstall confirm prompt understates deletion scope. The enumerateUserDevFlowContent lists only skill shadows, rule shadows, preference-profile.md, and learning.json, but the confirmed fs.rm(devflowDir, { recursive: true, force: true }) also removes unlisted content (~/.devflow/logs/, ~/.devflow/migrations.json). Actual harm is low (migrations are idempotent-on-reinstall; logs are disposable), but consider updating the enumerator to mention "plus logs and install metadata" so the confirm fully reflects what is deleted.

Performance (85% LOW): Redundant git rev-parse subprocess at init.ts:290. In the default scope === 'user' path, line 290 spawns git once (earlyPaths.gitRoot ?? await getGitRoot()) and line 464 spawns it again. Git root cannot change within a single init invocation. Fix: resolve once and thread through both sites.

TypeScript (LOW): manifest.ts:93-99 self-heal validates array-ness only while comment claims "never partial/garbage". Downstream-benign (non-string elements simply never match in Set lookups), consistent with existing patterns in the file, but comment overstates the guarantee.

Testing (80% MEDIUM): No integration coverage for the non-selectable-optional carry wiring — the core "re-init preserves all state" behavior (init.ts:1025). resolveNonSelectableOptionalCarry is unit-tested in isolation, but the wiring (pushing carry into pluginsToInstall + skipping under --plugin) has no test. Fix: add integration test asserting full re-init carries audit-claude; --plugin install does not force it in; --reset drops it.

Testing (82% LOW): "idempotent — does not overwrite existing package.json content" test doesn't actually verify non-overwrite (tests/installer-new.test.ts:74). Fix: write a sentinel key before the second call, then assert the sentinel survives.

Reliability (62-65%): rules --enable wipes the rules dir (fs.rm) before a now-throwing installAllRules with no surrounding try/catch (src/cli/commands/rules.ts:264-267). On packaging-bug path, user is left with rules fully removed and manifest update never runs. Consider wrapping so hard-error reports cleanly without leaving wiped rules dir.

Reliability (62-65%): Cancelling the full-cleanup confirm exits 0 after assets were already removed (uninstall.ts:381-384), leaving a stale manifest.json. Consider removing install artifacts on cancel or treating cancel as "keep everything" for consistent end-state.

Pre-Existing Issues & Documentation Gaps

CLAUDE.md (MEDIUM): Still lists removed list command module in the Project Structure section (CLAUDE.md:76). This PR deleted list.ts, so the reference now names a non-existent module.

CLAUDE.md (MEDIUM): "Two-Mode Init" section omits state-aware re-init and --reset factory-reset escape hatch. The section describing this exact feature is now incomplete since the PR adds these behaviors.

cli-reference.md (PRE-EXISTING, MEDIUM): Uninstall --scope default is misdocumented as "user" (docs/cli-reference.md:181), but actual default is auto-detect all scopes. For a destructive command, this understates the blast radius.


Overall Assessment

Blocking Issues: None. No CRITICAL or HIGH defects in the changed lines.

High-Confidence Should-Fix (Items 1-8 above): 8 findings across architecture, reliability, testing, and documentation. These are all fixable and improve quality without blocking merge.

Low-Risk Pre-Existing: The god-function length of init.ts (1356 lines) and redundant git spawns are architectural debt noted for future refactoring, not merge-blockers.

Regression Testing: Excellent. The pure seeding helpers are well-covered; the feature behavior (re-init with state carry, --reset handling, non-selectable-optional preservation) is correct per manual inspection. 1915 tests pass green.

Recommendation: APPROVED_WITH_SUGGESTED_FIXES

Merge with medium priority on Items 1-8 (especially the scope-cleanup guard at uninstall.ts:387 and the --reset documentation at cli-reference.md). The high-confidence testing and reliability fixes ensure the destructive uninstall and seeding logic remain robust.

— Claude Code

dean0x and others added 10 commits July 24, 2026 01:26
…, fix cancel stale-manifest

ARCH/CPLX: Extract exported `resolveDevflowDirCleanup(scope, isTTY, userContent,
devflowDir, homeDir)` pure decision function mirroring the existing
`resolveSecurityRemovalDecision` pattern. No I/O inside the resolver; the
.action() caller performs all prompting. Collapses the ~5-level inline nesting
into a flat if/else on the two return values ('artifacts-only' | 'prompt').

REL/SEC (precondition guard): the pure resolver checks four invariants before
allowing 'prompt' to proceed to an fs.rm on devflowDir:
  1. basename(devflowDir) === '.devflow'
  2. devflowDir is not the home directory itself
  3. devflowDir is not the filesystem root '/'
  4. devflowDir resides inside $HOME (guards DEVFLOW_DIR env overrides)
Any failing invariant falls back to 'artifacts-only' — business logic must not
throw (engineering rule); explicit check satisfies the reliability rule that
invariants be asserted in production code, not only in tests.

SEC: Confirm prompt now enumerates the ENTIRE scope: "plus logs and install
metadata" added to the message body so the destructive action is honest about
what fs.rm(devflowDir, {recursive}) actually removes.

REL (avoids PF-014): cancel branch previously called process.exit(0) AFTER
removeAllDevFlow had already run, leaving manifest.json stale (pointing to
assets no longer on disk). Now falls through to removeDevFlowInstallArtifacts
on both cancel and decline paths, leaving a clean end-state (applies ADR-003).

Tests: TDD red-green on resolveDevflowDirCleanup covering all 5 required cases
(local-scope, user+TTY+content, non-TTY, outside-HOME guard, bad-basename guard)
plus home-dir-itself guard, root guard, no-content case, and exhaustiveness.
All 1927 tests pass. Snyk code scan: 0 issues.

Co-Authored-By: Claude <noreply@anthropic.com>
…s test

The @param explicit doc referenced --view-mode which does not exist; the
real triggers are --reset and the Advanced interactive prompt selection.
Updated the JSDoc to name both sources of truth from init.ts.

The getDefaultFlags test was tautological — its expected value was
re-derived from the implementation's own filter, so adding or removing a
default-on flag would never fail it. Pin it to the hard-coded list of the
eight current default-on flag IDs.

Co-Authored-By: Claude <noreply@anthropic.com>
…test assertion

CONS-1: resolveSeedPlugins JSDoc incorrectly listed devflow-audit-claude as an
"always-installed plugin". It is optional: true and non-selectable — exactly why
resolveNonSelectableOptionalCarry exists. Updated the comment to distinguish
always-installed plugins (core-skills, ambient) from non-selectable optional
plugins (audit-claude).

PERF-3: resolveNonSelectableOptionalCarry used allPlugins.find() inside .filter()
(O(n·m)). Replaced with a name→plugin Map built once before the filter (O(n+m)),
matching the file's existing Set-based style.

CPLX-4: resolveSeedFeatures repeated an identical projectConfig !== null ternary
three times. Extracted a local helper `fromConfig` that removes the duplication
while keeping identical behavior.

TEST-6: Two assertions in init-seed.test.ts were tautological — one re-derived
the expected flag list using the same filter/map the implementation uses, and one
re-derived expected values from the seed itself. Both are now pinned to hard-coded
values (the 8 known default-ON flag IDs; explicit boolean true for ambient/learning/
knowledge) so they actually constrain behavior.

Co-Authored-By: Claude <noreply@anthropic.com>
- Add --recommended and --advanced rows to Init Options table
- Add --reset row with factory-reset description and --plugin mutual-exclusivity note
- Fix uninstall --scope entry: "default: user" → "default: auto-detect all installed scopes"
- Add init-seed.ts to CLI module list in file-organization.md
- Remove deleted list.ts from CLAUDE.md src/cli/ module enumeration
- Expand Two-Mode Init paragraph: state-aware re-init pre-seeding and --reset flag

Co-Authored-By: Claude <noreply@anthropic.com>
The only production caller (uninstall shadow-warning block) was removed
in an earlier commit; only skills.test.ts still referenced it.

Applies ADR-003 (leave the end-state, no tombstone residue).
…n-string elements

Array.isArray alone accepted mixed arrays like [1, null] and cast them
as string[], bypassing the self-heal invariant. Add .every(e => typeof e
=== 'string') so garbage arrays produce undefined, matching the comment's
"never partial/garbage" contract. Add two regression tests.
…d rejection

installAllRules hard-errors when a declared rule source is missing
(packaging bug). Without a catch, the preceding fs.rm leaves the rules
directory cleared while the error becomes an unhandled rejection with no
user-visible message. Wrap the call in try/catch and surface a clean
error before exiting 1. Avoids PF-009 blast-radius.
…nel)

TEST-1 (registry-integrity): Guard 5 reverse-check now aggregates spawned
agents per-plugin across all commands before checking each declared agent is
spawned in at least one command. Previously the per-command spawned set was
checked against the whole plugin's declaredAgents, asserting every command
spawned every agent — contradicting the documented "at least one command"
intent. The agentType-only skip is preserved (plugins with no subagent_type
syntax never enter the aggregate map).

TEST-3 (cli-unknown-command): Added a dist-existence guard using a top-level
await + describe.skipIf(!distExists) on all three describe blocks. When
dist/cli.js is absent (no pretest build step), all tests skip gracefully
instead of failing with a confusing ENOENT error. Mirrors the Guard 4/5 skip
pattern in registry-integrity.test.ts.

TEST-5 (installer-new): The "does not overwrite existing package.json" test
now writes a _sentinel key into the package.json between the two composeScripts
calls. After the second call the sentinel must survive, proving the wx
(exclusive-create) flag left the existing file untouched. Previously both
calls wrote identical content, so the test passed whether the flag was wx or w.

Co-Authored-By: Claude <noreply@anthropic.com>
… wiring helper

CONS-3 (applies ADR-003): Remove ephemeral squash-label references `(7g)` and
`commit 7g gate` from two viewModeExplicit comments. Keep the substantive
explanation; only the transient commit-sigils are removed.

PERF-1: Resolve git root once and reuse. `earlyGitRoot` is now declared before
the hoisted-reads try block and assigned inside it (`earlyPaths.gitRoot ??
getGitRoot()`), eliminating the redundant unconditional `getGitRoot()` call that
previously fired at the Feature-toggles site regardless of the earlier result.

TEST-4 (seam): Extract `applyNonSelectableCarry` from the inline wiring in
initAction into a pure exported helper in init-seed.ts. The helper encapsulates
the `!options.plugin` gate + resolveNonSelectableOptionalCarry call + dedup-merge
loop, making the carry wiring independently testable. initAction is updated to
delegate to the helper (no behavior change).

Co-Authored-By: Claude <noreply@anthropic.com>
Tests the full carry wiring (gate + merge + dedup) extracted in the companion
fix commit. Covers: partial-install gate skips carry, null manifest is a no-op,
audit-claude is carried on full re-init, dedup prevents double-add, stale names
are safely excluded, and the pluginsToInstall argument is not mutated.

Co-Authored-By: Claude <noreply@anthropic.com>
dean0x and others added 2 commits July 24, 2026 01:51
…test isolation

CONS-5: Replace inline D1:/D2: sigils with @D1/@D2 to match the JSDoc
@D-series convention used in sibling files (uninstall.ts).

TEST-7: Add three tests for the previously uncovered non-Devflow statusLine
branch in hud --enable: non-interactive skip, user-decline, and user-confirm.
The confirm test also exposed a production bug: addHudStatusLine's guard
for non-Devflow statusLines fired even after the user confirmed the
overwrite, leaving settings.json unchanged. Fixed by clearing the
non-Devflow statusLine from settingsContent before the addHudStatusLine
call when the user confirms.

TEST-2 / TEST-8: Replace Commander's private _optionValues = {} reset
with a createHudCommand() factory export so tests build a fresh Command
per test without coupling to Commander internals. Replace manual
process.env save/restore with vi.stubEnv() + vi.unstubAllEnvs() for
leak-proof env-mutation hygiene.

Co-Authored-By: Claude <noreply@anthropic.com>
@dean0x
dean0x merged commit 0089084 into main Jul 24, 2026
1 of 2 checks passed
@dean0x
dean0x deleted the feat/init-composability branch July 24, 2026 06:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant