Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .squad/agents/fenster/history.md
Original file line number Diff line number Diff line change
Expand Up @@ -749,3 +749,20 @@ Fixed 4 runtime bugs:
- The @github/copilot SDK bundles node:sqlite imports in its minified output. Cannot fix at source — pre-flight checks with clear messages are the right pattern.
- resolveSquad() returns the .squad/ directory path itself, not the parent. Callers must not re-join with '.squad'.
- Ink re-renders on every React state change. Multiple high-frequency animation timers compound into excessive redraws. Keep intervals ≥120ms for animations, ≥1000ms for counters.
## Phase 3 CLI Config Fixes — 2026-03-07

**PR #233 (squad/phase3-cli-config → dev)**

Fixed 4 bugs in a single branch:

1. **#226 — aspire not wired:** Added routing block + help text for squad aspire in cli-entry.ts. The command existed (175 lines) but was never connected to the router.
2. **#229 — doctor not exported:** CLI routing was present but unDoctor/doctorCommand were not exported from the barrel. Added exports to cli/index.ts for SDK consumers.
3. **#201 — workflows opt-in:** FRAMEWORK_WORKFLOWS filter was already in place. Added --no-workflows flag to squad init and threaded includeWorkflows through RunInitOptions.
4. **#202 — config.json gitignore:** Both link.ts and init-remote.ts now auto-append .squad/config.json to .gitignore after writing config, using the same pattern as orchestration-log entries.

## Learnings
- CLI command wiring requires both a routing block in cli-entry.ts AND help text in the help section. Easy to miss one.
- The .gitignore append pattern (check exists, check includes, append with header comment) is reusable across init, link, and init-remote.
- FRAMEWORK_WORKFLOWS already filters init to 4 safe workflows; the opt-in flag is about user control, not safety.
- `process.env.NODE_NO_WARNINGS = '1'` set at runtime does NOT suppress Node.js ExperimentalWarning -- the env var is only checked at process start. Use a `process.emit` override to filter warnings at runtime.
- CI failures in --version tests were caused by `node:sqlite` ExperimentalWarning leaking into terminal output (3 lines instead of 1). Fixed with process.emit hook in cli-entry.ts.
Expand Down
5 changes: 5 additions & 0 deletions .squad/decisions/inbox/fenster-init-workflows-opt-in.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### 2026-03-07: squad init --no-workflows flag for opt-in workflow installation
**By:** Fenster (Core Dev)
**What:** `squad init` now accepts `--no-workflows` to skip GitHub workflow installation. Default remains `true` (framework workflows are installed). The `RunInitOptions` interface accepts `includeWorkflows?: boolean`.
**Why:** Users in repos with existing CI/CD should be able to skip Squad's framework workflow installation. The 4 framework workflows are safe, but user control matters.
**Impact:** CLI help text updated, `RunInitOptions` extended, `initSquad` respects the flag.
32 changes: 32 additions & 0 deletions .squad/decisions/inbox/fenster-pr233-ci-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Decision: Runtime ExperimentalWarning suppression via process.emit hook

**Date:** 2026-03-07
**Author:** Fenster (Core Dev)
**Context:** PR #233 CI failure — 4 tests failed

## Problem

PR #233 (CLI wiring fixes for #226, #229, #201, #202) passed all 74 tests locally but failed 4 tests in CI:

- `test/cli-p0-regressions.test.ts` — bare semver test (expected 1 line, got 3)
- `test/speed-gates.test.ts` — version outputs one line (expected 1, got 3)
- `test/ux-gates.test.ts` — no overflow beyond 80 chars (ExperimentalWarning line >80)
- `test/ux-gates.test.ts` — version bare semver (expected 1 line, got 3)

Root cause: `node:sqlite` import triggers Node.js `ExperimentalWarning` that leaks to stderr. The existing `process.env.NODE_NO_WARNINGS = '1'` in cli-entry.ts was ineffective because Node only reads that env var at process startup, not when set at runtime.

The warning likely didn't appear locally because the local Node.js version may have already suppressed it or the env var was set in the shell.

## Decision

Added a `process.emit` override in cli-entry.ts that intercepts `warning` events with `name === 'ExperimentalWarning'` and swallows them. This is placed:
- After `process.env.NODE_NO_WARNINGS = '1'` (which still helps child processes)
- Before the `await import('node:sqlite')` pre-flight check

This is the standard Node.js pattern for runtime warning suppression when you can't control the process launch flags.

## Impact

- **cli-entry.ts**: 12 lines added (comment + override function)
- **Tests**: All 4 previously failing tests now pass; no regressions in structural tests (#624)
- **Behavior**: ExperimentalWarning messages no longer appear in CLI output; other warnings (DeprecationWarning, etc.) are unaffected
54 changes: 50 additions & 4 deletions packages/squad-cli/src/cli-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,39 @@

process.env.NODE_NO_WARNINGS = '1';

// Suppress ExperimentalWarning (e.g. node:sqlite) from leaking to terminal.
// process.env.NODE_NO_WARNINGS only works when set BEFORE process starts;
// this runtime hook catches warnings emitted during dynamic imports below.
const _origEmit = process.emit;
// @ts-expect-error — narrowing emit signature for warning suppression
process.emit = function (evt: string, ...args: unknown[]) {
if (evt === 'warning' && (args[0] as { name?: string })?.name === 'ExperimentalWarning') {
return false;
}
return _origEmit.apply(this, [evt, ...args] as Parameters<typeof _origEmit>);
};

// Pre-flight: detect missing node:sqlite before the Copilot SDK tries to use it.
// The @github/copilot SDK lazily imports node:sqlite for session storage.
// Node.js <22.5.0 and some 22.x builds don't include the builtin. (#214)
try {
await import('node:sqlite');
} catch {
// Module not available — install a shim so the SDK's lazy require succeeds.
// We register a minimal stub that throws a descriptive error only if
// DatabaseSync is actually instantiated (it may never be on short sessions).
const { register } = await import('node:module');
// No shim to register — just surface a clear message at startup so users
// know what's happening instead of seeing an opaque stack trace.
const nodeVersion = process.versions.node;
console.warn(
`⚠ node:sqlite is not available in Node.js v${nodeVersion}.\n` +
' The Copilot SDK uses node:sqlite for session storage.\n' +
' Upgrade to Node.js ≥22.5.0 or launch with --experimental-sqlite.\n' +
' Squad will attempt to continue, but session persistence may fail.\n',
);
}

import fs from 'node:fs';
import path from 'node:path';
import { fatal, SquadError } from './cli/core/errors.js';
Expand Down Expand Up @@ -62,6 +95,7 @@ async function main(): Promise<void> {
console.log(` Flags: --global (init in personal squad directory)`);
console.log(` ${BOLD}init${RESET} Initialize Squad (skip files that already exist)`);
console.log(` Flags: --global (init in personal squad directory)`);
console.log(` --no-workflows (skip GitHub workflow installation)`);
console.log(` Usage: init --mode remote <team-repo-path>`);
console.log(` Creates .squad/config.json pointing to an external team root`);
console.log(` ${BOLD}upgrade${RESET} Update Squad-owned files to latest version`);
Expand Down Expand Up @@ -109,6 +143,8 @@ async function main(): Promise<void> {
console.log(` ${BOLD}build${RESET} Compile squad.config.ts into .squad/ markdown`);
console.log(` Flags: --check (validate only), --dry-run (preview)`);
console.log(` --watch (rebuild on change)`);
console.log(` ${BOLD}aspire${RESET} Launch .NET Aspire dashboard for observability`);
console.log(` Flags: --docker (force Docker), --port <n> (dashboard port)`);

console.log(` ${BOLD}help${RESET} Show this help message`);
console.log(`\nFlags:`);
Expand Down Expand Up @@ -154,7 +190,8 @@ async function main(): Promise<void> {
}

const dest = hasGlobal ? resolveGlobalSquadPath() : process.cwd();
runInit(dest).catch(err => {
const noWorkflows = args.includes('--no-workflows');
runInit(dest, { includeWorkflows: !noWorkflows }).catch(err => {
fatal(err.message);
});
return;
Expand Down Expand Up @@ -324,11 +361,11 @@ async function main(): Promise<void> {

if (cmd === 'nap') {
const { runNap, formatNapReport } = await import('./cli/core/nap.js');
const squadRoot = resolveSquad(process.cwd());
if (!squadRoot) {
// resolveSquad() returns the .squad/ directory itself — use it directly (#207)
const squadDir = resolveSquad(process.cwd());
if (!squadDir) {
fatal('No squad found. Run "squad init" first.');
}
const squadDir = path.join(squadRoot, '.squad');
const deep = args.includes('--deep');
const dryRun = args.includes('--dry-run');
const result = await runNap({ squadDir, deep, dryRun });
Expand All @@ -354,6 +391,15 @@ async function main(): Promise<void> {
return;
}

if (cmd === 'aspire') {
const { runAspire } = await import('./cli/commands/aspire.js');
const useDocker = args.includes('--docker');
const portIdx = args.indexOf('--port');
const port = (portIdx !== -1 && args[portIdx + 1]) ? parseInt(args[portIdx + 1]!, 10) : undefined;
await runAspire({ docker: useDocker, port });
return;
}

if (cmd === 'link') {
const { runLink } = await import('./cli/commands/link.js');
const teamPath = args[1];
Expand Down
14 changes: 14 additions & 0 deletions packages/squad-cli/src/cli/commands/init-remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,18 @@ export function writeRemoteConfig(projectDir: string, teamRepoPath: string): voi
JSON.stringify(config, null, 2) + '\n',
'utf-8',
);

// Ensure .squad/config.json is in .gitignore (machine-local path, never commit)
const gitignorePath = path.join(projectDir, '.gitignore');
const ignoreEntry = '.squad/config.json';
let existingIgnore = '';
if (fs.existsSync(gitignorePath)) {
existingIgnore = fs.readFileSync(gitignorePath, 'utf-8');
}
if (!existingIgnore.includes(ignoreEntry)) {
const block = (existingIgnore && !existingIgnore.endsWith('\n') ? '\n' : '')
+ '# Squad: local config (machine-specific paths, never commit)\n'
+ ignoreEntry + '\n';
fs.appendFileSync(gitignorePath, block);
}
}
14 changes: 14 additions & 0 deletions packages/squad-cli/src/cli/commands/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,19 @@ export function runLink(projectDir: string, teamRepoPath: string): void {
'utf-8',
);

// Ensure .squad/config.json is in .gitignore (machine-local path, never commit)
const gitignorePath = path.join(projectDir, '.gitignore');
const ignoreEntry = '.squad/config.json';
let existingIgnore = '';
if (fs.existsSync(gitignorePath)) {
existingIgnore = fs.readFileSync(gitignorePath, 'utf-8');
}
if (!existingIgnore.includes(ignoreEntry)) {
const block = (existingIgnore && !existingIgnore.endsWith('\n') ? '\n' : '')
+ '# Squad: local config (machine-specific paths, never commit)\n'
+ ignoreEntry + '\n';
fs.appendFileSync(gitignorePath, block);
}

console.log(`✅ Linked to team root: ${relativePath}`);
}
4 changes: 3 additions & 1 deletion packages/squad-cli/src/cli/core/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ export interface RunInitOptions {
prompt?: string;
/** If true, disable extraction from consult sessions (read-only consultations) */
extractionDisabled?: boolean;
/** If false, skip GitHub workflow installation (default: true) */
includeWorkflows?: boolean;
}

/**
Expand Down Expand Up @@ -111,7 +113,7 @@ export async function runInit(dest: string, options: RunInitOptions = {}): Promi
],
configFormat: 'typescript',
skipExisting: true,
includeWorkflows: true,
includeWorkflows: options.includeWorkflows !== false,
includeTemplates: true,
includeMcpConfig: true,
projectType: projectType as any,
Expand Down
1 change: 1 addition & 0 deletions packages/squad-cli/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export {
export * from './core/workflows.js';
export * from './core/team-md.js';
export { runCopilot, type CopilotFlags } from './commands/copilot.js';
export { runDoctor, doctorCommand, type DoctorCheck, type DoctorMode } from './commands/doctor.js';
export { runExport } from './commands/export.js';
export { runImport } from './commands/import.js';
export { splitHistory } from './core/history-split.js';