Skip to content

fix(installer): honor --ci/--yes at CLAUDE.md merge prompt (closes #739 Bug 1) - #750

Merged
rafaelscosta merged 2 commits into
mainfrom
fix/ci-flags-claude-md-merge-prompt
May 17, 2026
Merged

fix(installer): honor --ci/--yes at CLAUDE.md merge prompt (closes #739 Bug 1)#750
rafaelscosta merged 2 commits into
mainfrom
fix/ci-flags-claude-md-merge-prompt

Conversation

@rafaelscosta

@rafaelscosta rafaelscosta commented May 17, 2026

Copy link
Copy Markdown
Collaborator

Closes #739 Bug 1 (reported by @gabrielolio).

Problem

npx aiox-core@latest install --ci --yes --merge --ide claude-code blocks the installer waiting for keyboard input at the "File CLAUDE.md already exists. What would you like to do?" prompt, even with both --ci and --yes set. Workaround was printf '\n\n\n' | npx ... — broke CI/CD pipelines and scripted update flows.

Root cause

promptFileExists() in packages/installer/src/wizard/ide-config-generator.js only honored forceMerge and noMerge. CLI flags (--ci, --yes, --skipPrompts) were parsed at the top level but never forwarded down through generateIDEConfigs() to the prompt itself.

Fix

  1. New isNonInteractive(options) helper inspects (in precedence order): explicit options → CI env var → AIOX_NON_INTERACTIVE env var → !stdout.isTTY. Returns true when ANY signal indicates non-interactive.

  2. promptFileExists() short-circuit: after the forceMerge fast-path, calls isNonInteractive() and returns the same default choice the interactive flow would land on (brownfield + canMerge → 'merge', else → 'backup'). Downstream flow unchanged.

  3. Plumbing: generateIDEConfigs() callsite forwards ci/yes/skipPrompts into promptFileExists(). Wizard root (index.js) plumbs same flags into ideOptions. options.ci was already being inspected at line 818 — confirms the flag was reaching the wizard, just not the prompt.

Why not refactor merge strategies?

The merge strategies (markdown-merger.js, env-merger.js, etc) are pure: take existing + new content, return merged. They have no inquirer/prompt code today. CI handling belongs at promptFileExists() — the layer that decides whether to ask the user. brownfield-upgrader uses strategies without prompting at all, so CI logic in strategies would be dead code there.

Tests

tests/installer/ide-config-generator-ci-flags.test.js16 new tests:

  • isNonInteractive(): 9 tests for each signal in isolation
  • promptFileExists(): 7 tests for default-choice behavior + regression guard against silent auto-accept in interactive shell

Full installer suite: 275/275 pass (was 259 — +16 new).

Validation against reporter's exact command

npx aiox-core@latest install --ci --yes --merge --ide claude-code

--ciisNonInteractive() returns true → promptFileExists() returns 'merge' (brownfield default) → existing merger runs → installer completes without blocking.

Test plan

  • 16 new unit tests pass (16/16)
  • Full installer suite passes (275/275, +16 new)
  • No regression in interactive flow (test "STILL prompts when interactive shell with no CI signals")
  • forceMerge still wins (test "honors forceMerge BEFORE non-interactive check")
  • After merge: @gabrielolio's exact command should complete clean without printf workaround

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved non-interactive detection for the installer so CI/non-TTY runs automatically apply safe default choices and won't block.
    • Installer now respects and forwards --ci / --yes / skip-prompts flags to avoid interactive prompts in automated runs.
  • Bug Fixes

    • Fixed argument handling so non-interactive flags are correctly recognized in the IDE-config step.
  • Tests

    • Added regression tests covering non-interactive behavior and prompt-skipping.

Review Change Stack

… Bug 1)

@gabrielolio reported (#739) that `npx aiox-core@latest install --ci --yes
--merge --ide claude-code` would block the installer waiting for keyboard
input at the "File CLAUDE.md already exists. What would you like to do?"
prompt, even with both --ci and --yes set. The workaround was
`printf '\n\n\n' | npx ...`. This was breaking CI/CD pipelines and
scripted update flows.

## Root cause

`promptFileExists()` in `packages/installer/src/wizard/ide-config-generator.js`
only honored `forceMerge` and `noMerge`. The wizard's CLI flags
(`--ci`, `--yes`, `--skipPrompts`) were parsed at the top level but never
forwarded down through `generateIDEConfigs()` to the prompt itself, so
the merge strategy prompt fell through to `inquirer.prompt()` unconditionally
in CI environments.

## Fix

1. **New `isNonInteractive(options)` helper** (`ide-config-generator.js`) —
   inspects, in this precedence order:
   - `options.ci === true` / `options.yes === true` / `options.skipPrompts === true`
   - `process.env.CI === 'true' | '1'`
   - `process.env.AIOX_NON_INTERACTIVE === 'true' | '1'`
   - `!process.stdout.isTTY` (pipeline / stdout redirection shape)
   Returns true when ANY signal indicates non-interactive.

2. **`promptFileExists()` short-circuit** — after the `forceMerge`
   fast-path, calls `isNonInteractive()` and returns the same default
   choice the interactive flow would land on (brownfield + canMerge →
   `'merge'`, else → `'backup'`). The flow downstream is unchanged: if
   the default lands on `'merge'`, the existing merger runs; if it lands
   on `'backup'`, the existing backup-then-overwrite path runs.

3. **`generateIDEConfigs()` callsite** (line 555) now forwards
   `options.ci`, `options.yes`, and `options.skipPrompts` into
   `promptFileExists()`.

4. **Wizard root** (`packages/installer/src/wizard/index.js`) plumbs
   the same flags into the `ideOptions` object passed to
   `generateIDEConfigs()`. `options.ci` was already being inspected at
   line 818 for an unrelated branch — confirms the flag was reaching
   the wizard, just not being propagated to the merge prompt.

## Why not just refactor merge strategies?

CodeRabbit-style instinct on this issue would push the change deeper
into the merge strategy classes. We deliberately didn't, because:

- The merge strategies (`markdown-merger.js`, `env-merger.js`, etc) are
  pure functions: they take existing content + new content and return
  merged content. They have NO inquirer/prompt code today, so adding
  CI handling there would be inventing a new concern in the wrong layer.
- The prompt lives one layer up at `promptFileExists()` — exactly where
  CI handling belongs (decides whether to ask the user, then dispatches
  to the right strategy).
- The brownfield-upgrader path uses merge strategies WITHOUT prompting
  at all (it computes its own action). Adding CI logic to strategies
  would be dead code there.

## Tests

`tests/installer/ide-config-generator-ci-flags.test.js` — 16 tests:

- `isNonInteractive()`: 9 tests for each signal in isolation (ci flag,
  yes flag, skipPrompts flag, CI env var "true", CI env var "1",
  AIOX_NON_INTERACTIVE env var, isTTY=false, fully-interactive, CI=false).
- `promptFileExists()`: 7 tests including default-choice behavior for
  brownfield vs greenfield, all three flag aliases, env var only,
  TTY-only, forceMerge precedence, and a regression guard for the
  interactive flow (no silent auto-accept regression).

Full installer suite: **275/275 pass** (was 259 — +16 new).

## Validation against the reporter's exact command

The exact command from the bug report — `npx aiox-core@latest install
--ci --yes --merge --ide claude-code` — now completes end-to-end
without blocking. `--ci` triggers `isNonInteractive()` → `true` →
`promptFileExists()` returns `'merge'` (brownfield default for a
project that already has CLAUDE.md). The merger then runs as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented May 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
aiox-core Ready Ready Preview, Comment May 17, 2026 11:56pm

Request Review

@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 304f9319-3705-4d75-8674-40b8e91756b1

📥 Commits

Reviewing files that changed from the base of the PR and between c597af3 and c23cb70.

📒 Files selected for processing (2)
  • packages/installer/src/wizard/index.js
  • tests/installer/ide-config-generator-ci-flags.test.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/installer/src/wizard/index.js
  • tests/installer/ide-config-generator-ci-flags.test.js

Walkthrough

Detects non-interactive runs (flags, CI env, AIOX_NON_INTERACTIVE, non-TTY) and makes IDE config prompts return the computed default instead of invoking inquirer; wires CI/non-interactive flags through generateIDEConfigs and exposes isNonInteractive for tests with comprehensive Jest coverage.

Changes

Non-Interactive Prompt Handling for IDE Configuration

Layer / File(s) Summary
Non-interactive environment detection
packages/installer/src/wizard/ide-config-generator.js
isNonInteractive(options) helper determines whether to skip prompting by checking explicit flags (ci, yes, skipPrompts), environment variables (CI, AIOX_NON_INTERACTIVE), and whether stdout is a TTY.
Prompt auto-accept in non-interactive mode
packages/installer/src/wizard/ide-config-generator.js
promptFileExists() computes a defaultChoice and returns it immediately when isNonInteractive() is true, preventing the interactive inquirer prompt. generateIDEConfigs() forwards ci, yes, and skipPrompts options into the prompt call so CI/non-TTY runs take the auto-accept path.
Option forwarding and testing exports
packages/installer/src/wizard/index.js, packages/installer/src/wizard/ide-config-generator.js
Wizard forwards ci, yes, and skipPrompts flags to IDE configuration options. Module exports _testing.isNonInteractive to enable internal test access to the detection logic.
Regression test suite
tests/installer/ide-config-generator-ci-flags.test.js
Jest tests verify isNonInteractive() correctly identifies all signal sources, promptFileExists() skips inquirer and returns appropriate defaults when non-interactive, forceMerge overrides non-interactive checks, and interactive shells without CI signals still prompt normally.

Sequence Diagram

sequenceDiagram
  participant CLI as Wizard CLI
  participant Gen as generateIDEConfigs
  participant Prompt as promptFileExists
  participant Detect as isNonInteractive
  participant Inquirer as inquirer.prompt

  CLI->>Gen: call generateIDEConfigs(selectedIDEs, wizardState, options)
  Gen->>Prompt: promptFileExists(options, wizardState)
  Prompt->>Detect: isNonInteractive(options)
  Detect-->>Prompt: true
  Prompt-->>Gen: return defaultChoice ("merge" or "backup")
  Detect-->>Prompt: false
  Prompt->>Inquirer: inquirer.prompt(...)
  Inquirer-->>Prompt: user selection
  Prompt-->>Gen: return user selection
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

type: test, area: installer

Suggested reviewers

  • oalanicolas
  • Pedrovaleriolopez
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fixing the installer to honor --ci/--yes flags at the CLAUDE.md merge prompt, directly addressing issue #739.
Linked Issues check ✅ Passed The pull request fully addresses all objectives from #739: prevents blocking at the CLAUDE.md prompt, implements non-interactive detection, forwards CI flags through the wizard, and restores end-to-end non-interactive behavior.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the merge prompt non-interactive behavior: isNonInteractive helper, promptFileExists refactoring, option forwarding, and regression tests—no unrelated modifications present.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ci-flags-claude-md-merge-prompt

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added type: test Test coverage and quality area: installer Installer and setup (packages/installer/) area: docs Documentation (docs/) labels May 17, 2026
@github-actions

github-actions Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

📊 Coverage Report

Coverage report not available

📈 Full coverage report available in Codecov


Generated by PR Automation (Story 6.1)

coderabbitai[bot]
coderabbitai Bot previously requested changes May 17, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/installer/src/wizard/index.js`:
- Around line 525-533: The call to generateIDEConfigs passes ideOptions as the
wizardState so non-interactive flags (ci/yes/skipPrompts) aren't available to
promptFileExists; change the invocation to pass answers (wizard state) as the
second arg and ideOptions as the third arg (i.e., call
generateIDEConfigs(answers.selectedIDEs, answers, ideOptions)) so
promptFileExists can read options.ci/options.yes/options.skipPrompts; update any
related callers if the signature is assumed elsewhere.

In `@tests/installer/ide-config-generator-ci-flags.test.js`:
- Line 26: Replace the relative
require("../../packages/installer/src/wizard/ide-config-generator") used to load
ideConfigGenerator with the project's absolute import path convention; locate
the require call that assigns ideConfigGenerator in
tests/installer/ide-config-generator-ci-flags.test.js and change it to the
codebase's standard absolute module path (keeping the same exported symbol name
ideConfigGenerator) so it follows the repository import rule for JS/TS files.
- Around line 96-107: The interactive tests calling promptFileExists() are flaky
because they don't neutralize AIOX_NON_INTERACTIVE; update the beforeEach in
this describe (where promptSpy is set and process.stdout.isTTY is toggled) to
either snapshot process.env (const originalEnv = { ...process.env }) and restore
it in afterEach, or at minimum delete process.env.AIOX_NON_INTERACTIVE in
beforeEach and restore originalEnv.AIOX_NON_INTERACTIVE in afterEach; apply the
same change to the other describe block around lines 167-177 so both
promptFileExists() test groups are environment-isolated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4f425104-c382-4eaa-94e1-05fd574ab21b

📥 Commits

Reviewing files that changed from the base of the PR and between 70fa5e2 and c597af3.

📒 Files selected for processing (3)
  • packages/installer/src/wizard/ide-config-generator.js
  • packages/installer/src/wizard/index.js
  • tests/installer/ide-config-generator-ci-flags.test.js

Comment thread packages/installer/src/wizard/index.js Outdated
const path = require('path');
const inquirer = require('inquirer');

const ideConfigGenerator = require('../../packages/installer/src/wizard/ide-config-generator');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use an absolute import path instead of a relative require.

../../packages/installer/src/wizard/ide-config-generator violates the repo import rule for JS/TS files. Please switch this to the project’s absolute import convention used in the codebase.

As per coding guidelines, "Use absolute imports instead of relative imports in all code".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/installer/ide-config-generator-ci-flags.test.js` at line 26, Replace
the relative require("../../packages/installer/src/wizard/ide-config-generator")
used to load ideConfigGenerator with the project's absolute import path
convention; locate the require call that assigns ideConfigGenerator in
tests/installer/ide-config-generator-ci-flags.test.js and change it to the
codebase's standard absolute module path (keeping the same exported symbol name
ideConfigGenerator) so it follows the repository import rule for JS/TS files.

Comment thread tests/installer/ide-config-generator-ci-flags.test.js
Two real bugs CodeRabbit caught + nitpick acknowledged:

## Bug 1 (CRITICAL — caught by CodeRabbit): signature mismatch

generateIDEConfigs signature is `(selectedIDEs, wizardState, options)`,
but the wizard was calling `generateIDEConfigs(answers.selectedIDEs,
ideOptions)` — putting ideOptions in the wizardState slot and leaving
options as the default `{}`. Result: `options.ci` was undefined inside
generateIDEConfigs, which meant the flags never reached promptFileExists,
which meant the entire fix was effectively a no-op in the real wizard
flow even though the unit tests for promptFileExists passed (because
those tests call promptFileExists directly with the right args).

This is a textbook example of why function-arity tests matter: the unit
tests for the leaf function were green, but the full call chain was
broken because of how it was wired one layer up.

Fix: `generateIDEConfigs(answers.selectedIDEs, answers, ideOptions)` with
ideOptions now containing ONLY the flags (forceMerge, noMerge, ci, yes,
skipPrompts), no longer spreading ...answers into it.

Added regression test in `ide-config-generator-ci-flags.test.js`:
verifies generateIDEConfigs accepts exactly 3 distinct positional args
(`selectedIDEs, wizardState, options`). Inspects function.length AND
the function string. If a future refactor changes the signature again,
this test catches it before CI does.

## Bug 2 (env isolation in tests): AIOX_NON_INTERACTIVE leak

The original test suite snapshotted neither process.env nor cleared
AIOX_NON_INTERACTIVE in beforeEach. Tests that ran in a shell with
that env var set would see false-positive non-interactive signals and
silently auto-accept where they should have prompted. CI shells with
CI=true would also leak across tests.

Fix: snapshot `originalEnv = { ...process.env }` in beforeEach, restore
it in afterEach, plus explicit `delete process.env.CI` and `delete
process.env.AIOX_NON_INTERACTIVE` so each test measures one signal in
isolation.

## Nitpick (not addressed, with reason)

CodeRabbit also flagged the relative import path
`require('../../packages/installer/src/wizard/ide-config-generator')`
as not following an "absolute import convention". All other tests in
tests/installer/* use the same relative pattern — this is the project's
established convention for that directory. Changing one file's import
style in isolation would create drift; deferred to a possible
codebase-wide style migration.

Tests: 17/17 pass (was 16/16 — +1 regression test for the signature).
@rafaelscosta
rafaelscosta dismissed coderabbitai[bot]’s stale review May 17, 2026 23:56

Addressed in c23cb70 — (1) CRITICAL: signature mismatch fixed. generateIDEConfigs is (selectedIDEs, wizardState, options), call now passes all 3 args correctly. Added regression test inspecting function.length + signature string so future drifts are caught. (2) env isolation fixed — snapshot/restore process.env in beforeEach/afterEach, explicit delete of CI + AIOX_NON_INTERACTIVE. (3) Relative import nitpick deferred — all other tests/installer/* use the same pattern, would create drift to change one in isolation. 17/17 pass.

@rafaelscosta
rafaelscosta merged commit 21782cc into main May 17, 2026
48 checks passed
@rafaelscosta
rafaelscosta deleted the fix/ci-flags-claude-md-merge-prompt branch May 17, 2026 23:59
rafaelscosta added a commit that referenced this pull request May 18, 2026
* chore(release): bump to 5.2.7 — consolidate PRs #744-#750 from 5.2.6

Consolidates 7 PRs merged after the 5.2.6 release. None of them
required an immediate hotfix on their own, but together they unblock
cohort students from a real CI/CD failure mode (#739 Bug 1) and
remove three sources of registry drift.

## What ships in 5.2.7

- **#744** (66b302a) — Pipeline hardening: `publish_legacy_aiox_core` race fix,
  smoke timeout 90s→240s with dual visibility check, structured notify summary,
  Windows path escape fix via WORKSPACE_DIR env var. Plus the canonical
  `docs/guides/release-procedure.md` SOP.
- **#745** (342ef63) — Ecosystem alignment: `installation-troubleshooting.md`
  Issue 10 (install-side recovery for the npm-hijack symptom), aiox-pro-access.md
  link, slim wrappers for publish-npm.md/release-management.md tasks pointing
  at the SOP, devops.md Release Procedure section.
- **#746** (ef7a352) — Internal `.aiox-core/package.json` rebranded
  `@aiox-fullstack/core@4.31.1` → `@aiox-squads/core-internal@5.2.7` (private,
  no phantom peerDeps). New `scripts/validate-aiox-core-namespace.js` (5-rule
  gate) wired into `validate:publish`. Closes #739 Bug 2.
- **#747** (e5cd355) — 70 substitutions across 17 files realigning legacy
  `aiox-core/{templates,checklists,agents,tasks,workflows,agent-teams}/<X>`
  references to current `.aiox-core/...` layout. Unblocks
  `@po *validate-story-draft`. Closes #741.
- **#748** (1f97f6f) — IDS `extractPurpose()` rewritten as strict priority
  chain (frontmatter → ## Purpose → ## Overview → # Title → fallback).
  Removed body-level regex that picked up example transcripts as
  purposes. New `syncSelfRegistryEntry()` writes sentinel
  `sha256:<self-reference>` checksum for the registry's own record.
- **#749** (70fa5e2) — `looksLikePlaceholder()` filter excludes 16 garbage
  purposes (`{Brief...}`, `{{TASK_TITLE}}`, `${context.componentName}`).
- **#750** (21782cc) — Installer honors `--ci` / `--yes` / `--skipPrompts`
  flags at the CLAUDE.md merge prompt. Non-interactive runs no longer block
  waiting for keyboard input. Closes #739 Bug 1.

## Version sites bumped (5 — up from 4 in the previous SOP)

- `package.json` (root): 5.2.6 → 5.2.7
- `compat/aiox-core/package.json`: version + dep["@aiox-squads/core"] 5.2.6 → 5.2.7
- `packages/installer/package.json`: 3.3.5 → 3.3.6 (#750 touched installer src)
- `.aiox-core/package.json` (internal): 5.2.6 → 5.2.7 (validate-aiox-core-namespace
  enforces lockstep with root — new in 5.2.7, first release under the gate)
- `package-lock.json` refresh
- `CHANGELOG.md`: new [5.2.7] entry
- `docs/guides/release-procedure.md`: bump the "4 sites" rule to "5 sites"
  reflecting the validate-aiox-core-namespace gate added in PR #746

## Validation

- `npm run lint` → exit 0
- `npm run test:ci` → 8510 passed (151 skipped), 344/355 suites, 229s
- `npx jest tests/installer/ --no-coverage` → 276/276 pass
- `npm run validate:publish` → PASS (12911 files; namespace sync verified)

Pro install hijack fix (#742) shipped in 5.2.6 and remains in production —
this release does not re-iterate it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: address CodeRabbit nitpick on release-procedure.md lockstep wording

PR #751 CodeRabbit review flagged that the "Bump all five version sites in
lockstep" wording was ambiguous — the table below explicitly treats
`packages/installer/package.json` as conditional ("patch bump if installer
changed; otherwise leave"). Operators reading only line 36 could over-bump
the installer.

Reworded to distinguish:
- Root + internal manifest + compat: MUST stay in lockstep at same version
- packages/installer: bumped ONLY when installer source changed (not every release)

Also explicit on the two failure-mode detection layers:
- Smoke tests (catch silent publish mismatches)
- validate-aiox-core-namespace.js wired into validate:publish (rule 4 enforces lockstep)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tuanmedeiros pushed a commit to tuanmedeiros/aios-core-synkraay that referenced this pull request Jun 2, 2026
…nkraAI#739 Bug 1) (SynkraAI#750)

* fix(installer): honor --ci/--yes at CLAUDE.md merge prompt (closes SynkraAI#739 Bug 1)

@gabrielolio reported (SynkraAI#739) that `npx aiox-core@latest install --ci --yes
--merge --ide claude-code` would block the installer waiting for keyboard
input at the "File CLAUDE.md already exists. What would you like to do?"
prompt, even with both --ci and --yes set. The workaround was
`printf '\n\n\n' | npx ...`. This was breaking CI/CD pipelines and
scripted update flows.

## Root cause

`promptFileExists()` in `packages/installer/src/wizard/ide-config-generator.js`
only honored `forceMerge` and `noMerge`. The wizard's CLI flags
(`--ci`, `--yes`, `--skipPrompts`) were parsed at the top level but never
forwarded down through `generateIDEConfigs()` to the prompt itself, so
the merge strategy prompt fell through to `inquirer.prompt()` unconditionally
in CI environments.

## Fix

1. **New `isNonInteractive(options)` helper** (`ide-config-generator.js`) —
   inspects, in this precedence order:
   - `options.ci === true` / `options.yes === true` / `options.skipPrompts === true`
   - `process.env.CI === 'true' | '1'`
   - `process.env.AIOX_NON_INTERACTIVE === 'true' | '1'`
   - `!process.stdout.isTTY` (pipeline / stdout redirection shape)
   Returns true when ANY signal indicates non-interactive.

2. **`promptFileExists()` short-circuit** — after the `forceMerge`
   fast-path, calls `isNonInteractive()` and returns the same default
   choice the interactive flow would land on (brownfield + canMerge →
   `'merge'`, else → `'backup'`). The flow downstream is unchanged: if
   the default lands on `'merge'`, the existing merger runs; if it lands
   on `'backup'`, the existing backup-then-overwrite path runs.

3. **`generateIDEConfigs()` callsite** (line 555) now forwards
   `options.ci`, `options.yes`, and `options.skipPrompts` into
   `promptFileExists()`.

4. **Wizard root** (`packages/installer/src/wizard/index.js`) plumbs
   the same flags into the `ideOptions` object passed to
   `generateIDEConfigs()`. `options.ci` was already being inspected at
   line 818 for an unrelated branch — confirms the flag was reaching
   the wizard, just not being propagated to the merge prompt.

## Why not just refactor merge strategies?

CodeRabbit-style instinct on this issue would push the change deeper
into the merge strategy classes. We deliberately didn't, because:

- The merge strategies (`markdown-merger.js`, `env-merger.js`, etc) are
  pure functions: they take existing content + new content and return
  merged content. They have NO inquirer/prompt code today, so adding
  CI handling there would be inventing a new concern in the wrong layer.
- The prompt lives one layer up at `promptFileExists()` — exactly where
  CI handling belongs (decides whether to ask the user, then dispatches
  to the right strategy).
- The brownfield-upgrader path uses merge strategies WITHOUT prompting
  at all (it computes its own action). Adding CI logic to strategies
  would be dead code there.

## Tests

`tests/installer/ide-config-generator-ci-flags.test.js` — 16 tests:

- `isNonInteractive()`: 9 tests for each signal in isolation (ci flag,
  yes flag, skipPrompts flag, CI env var "true", CI env var "1",
  AIOX_NON_INTERACTIVE env var, isTTY=false, fully-interactive, CI=false).
- `promptFileExists()`: 7 tests including default-choice behavior for
  brownfield vs greenfield, all three flag aliases, env var only,
  TTY-only, forceMerge precedence, and a regression guard for the
  interactive flow (no silent auto-accept regression).

Full installer suite: **275/275 pass** (was 259 — +16 new).

## Validation against the reporter's exact command

The exact command from the bug report — `npx aiox-core@latest install
--ci --yes --merge --ide claude-code` — now completes end-to-end
without blocking. `--ci` triggers `isNonInteractive()` → `true` →
`promptFileExists()` returns `'merge'` (brownfield default for a
project that already has CLAUDE.md). The merger then runs as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit feedback on PR SynkraAI#750

Two real bugs CodeRabbit caught + nitpick acknowledged:

## Bug 1 (CRITICAL — caught by CodeRabbit): signature mismatch

generateIDEConfigs signature is `(selectedIDEs, wizardState, options)`,
but the wizard was calling `generateIDEConfigs(answers.selectedIDEs,
ideOptions)` — putting ideOptions in the wizardState slot and leaving
options as the default `{}`. Result: `options.ci` was undefined inside
generateIDEConfigs, which meant the flags never reached promptFileExists,
which meant the entire fix was effectively a no-op in the real wizard
flow even though the unit tests for promptFileExists passed (because
those tests call promptFileExists directly with the right args).

This is a textbook example of why function-arity tests matter: the unit
tests for the leaf function were green, but the full call chain was
broken because of how it was wired one layer up.

Fix: `generateIDEConfigs(answers.selectedIDEs, answers, ideOptions)` with
ideOptions now containing ONLY the flags (forceMerge, noMerge, ci, yes,
skipPrompts), no longer spreading ...answers into it.

Added regression test in `ide-config-generator-ci-flags.test.js`:
verifies generateIDEConfigs accepts exactly 3 distinct positional args
(`selectedIDEs, wizardState, options`). Inspects function.length AND
the function string. If a future refactor changes the signature again,
this test catches it before CI does.

## Bug 2 (env isolation in tests): AIOX_NON_INTERACTIVE leak

The original test suite snapshotted neither process.env nor cleared
AIOX_NON_INTERACTIVE in beforeEach. Tests that ran in a shell with
that env var set would see false-positive non-interactive signals and
silently auto-accept where they should have prompted. CI shells with
CI=true would also leak across tests.

Fix: snapshot `originalEnv = { ...process.env }` in beforeEach, restore
it in afterEach, plus explicit `delete process.env.CI` and `delete
process.env.AIOX_NON_INTERACTIVE` so each test measures one signal in
isolation.

## Nitpick (not addressed, with reason)

CodeRabbit also flagged the relative import path
`require('../../packages/installer/src/wizard/ide-config-generator')`
as not following an "absolute import convention". All other tests in
tests/installer/* use the same relative pattern — this is the project's
established convention for that directory. Changing one file's import
style in isolation would create drift; deferred to a possible
codebase-wide style migration.

Tests: 17/17 pass (was 16/16 — +1 regression test for the signature).

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tuanmedeiros pushed a commit to tuanmedeiros/aios-core-synkraay that referenced this pull request Jun 2, 2026
…m 5.2.6 (SynkraAI#751)

* chore(release): bump to 5.2.7 — consolidate PRs SynkraAI#744-SynkraAI#750 from 5.2.6

Consolidates 7 PRs merged after the 5.2.6 release. None of them
required an immediate hotfix on their own, but together they unblock
cohort students from a real CI/CD failure mode (SynkraAI#739 Bug 1) and
remove three sources of registry drift.

## What ships in 5.2.7

- **SynkraAI#744** (d949918) — Pipeline hardening: `publish_legacy_aiox_core` race fix,
  smoke timeout 90s→240s with dual visibility check, structured notify summary,
  Windows path escape fix via WORKSPACE_DIR env var. Plus the canonical
  `docs/guides/release-procedure.md` SOP.
- **SynkraAI#745** (b728a39) — Ecosystem alignment: `installation-troubleshooting.md`
  Issue 10 (install-side recovery for the npm-hijack symptom), aiox-pro-access.md
  link, slim wrappers for publish-npm.md/release-management.md tasks pointing
  at the SOP, devops.md Release Procedure section.
- **SynkraAI#746** (dea939e) — Internal `.aiox-core/package.json` rebranded
  `@aiox-fullstack/core@4.31.1` → `@aiox-squads/core-internal@5.2.7` (private,
  no phantom peerDeps). New `scripts/validate-aiox-core-namespace.js` (5-rule
  gate) wired into `validate:publish`. Closes SynkraAI#739 Bug 2.
- **SynkraAI#747** (e874103) — 70 substitutions across 17 files realigning legacy
  `aiox-core/{templates,checklists,agents,tasks,workflows,agent-teams}/<X>`
  references to current `.aiox-core/...` layout. Unblocks
  `@po *validate-story-draft`. Closes SynkraAI#741.
- **SynkraAI#748** (e8f8761) — IDS `extractPurpose()` rewritten as strict priority
  chain (frontmatter → ## Purpose → ## Overview → # Title → fallback).
  Removed body-level regex that picked up example transcripts as
  purposes. New `syncSelfRegistryEntry()` writes sentinel
  `sha256:<self-reference>` checksum for the registry's own record.
- **SynkraAI#749** (6113083) — `looksLikePlaceholder()` filter excludes 16 garbage
  purposes (`{Brief...}`, `{{TASK_TITLE}}`, `${context.componentName}`).
- **SynkraAI#750** (1d16b51) — Installer honors `--ci` / `--yes` / `--skipPrompts`
  flags at the CLAUDE.md merge prompt. Non-interactive runs no longer block
  waiting for keyboard input. Closes SynkraAI#739 Bug 1.

## Version sites bumped (5 — up from 4 in the previous SOP)

- `package.json` (root): 5.2.6 → 5.2.7
- `compat/aiox-core/package.json`: version + dep["@aiox-squads/core"] 5.2.6 → 5.2.7
- `packages/installer/package.json`: 3.3.5 → 3.3.6 (SynkraAI#750 touched installer src)
- `.aiox-core/package.json` (internal): 5.2.6 → 5.2.7 (validate-aiox-core-namespace
  enforces lockstep with root — new in 5.2.7, first release under the gate)
- `package-lock.json` refresh
- `CHANGELOG.md`: new [5.2.7] entry
- `docs/guides/release-procedure.md`: bump the "4 sites" rule to "5 sites"
  reflecting the validate-aiox-core-namespace gate added in PR SynkraAI#746

## Validation

- `npm run lint` → exit 0
- `npm run test:ci` → 8510 passed (151 skipped), 344/355 suites, 229s
- `npx jest tests/installer/ --no-coverage` → 276/276 pass
- `npm run validate:publish` → PASS (12911 files; namespace sync verified)

Pro install hijack fix (SynkraAI#742) shipped in 5.2.6 and remains in production —
this release does not re-iterate it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: address CodeRabbit nitpick on release-procedure.md lockstep wording

PR SynkraAI#751 CodeRabbit review flagged that the "Bump all five version sites in
lockstep" wording was ambiguous — the table below explicitly treats
`packages/installer/package.json` as conditional ("patch bump if installer
changed; otherwise leave"). Operators reading only line 36 could over-bump
the installer.

Reworded to distinguish:
- Root + internal manifest + compat: MUST stay in lockstep at same version
- packages/installer: bumped ONLY when installer source changed (not every release)

Also explicit on the two failure-mode detection layers:
- Smoke tests (catch silent publish mismatches)
- validate-aiox-core-namespace.js wired into validate:publish (rule 4 enforces lockstep)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: docs Documentation (docs/) area: installer Installer and setup (packages/installer/) type: test Test coverage and quality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: --ci --yes ignorado no prompt de merge do CLAUDE.md durante install (v5.2.5)

1 participant