Pulls in 2 main commits (#1487 orchestrator identity fix, #1238 gh CLI
tracer + scm/tracker migration Phase A1a) plus the auto-merged
follow-on changes from upstream.
Conflict resolutions:
- packages/core/src/session-manager.ts:
Took main's reorganized opencode-agents-md import + new
getOrchestratorSessionId import. Kept main's reformatted sessionCache
type and added the new ensureOrchestratorPromises Map.
- packages/core/src/agent-workspace-hooks.ts:
Took main's WRAPPER_VERSION = "0.6.0" (and matching test fixture).
- packages/core/src/__tests__/agent-workspace-hooks.test.ts:
Merged both sides' imports — kept branch's buildNodeWrapper +
node:path/join (still used in test body) and added main's
AO_METADATA_HELPER + GH_WRAPPER constant exports.
- packages/plugins/agent-codex/src/index.ts:
Dropped now-unused PREFERRED_GH_PATH import (env injection moved to
session-manager). Kept isWindows — needed by branch's new
formatLaunchCommand, resolveCodexBinaryWindows, and pre-existing
isProcessRunning code.
- packages/plugins/agent-codex/src/index.test.ts:
Took main's collapsed PATH/GH_PATH undefined assertions and no-op
setupWorkspaceHooks test — the agent no longer constructs PATH or
installs wrappers (session-manager owns those paths now). Removed
the orphaned wrapper-write tests. Kept branch's
describe.skipIf(win32) on the shell-wrapper-content block (those
tests verify Unix shell-script content that genuinely can't run on
PowerShell).
- packages/plugins/agent-aider/src/index.test.ts +
packages/plugins/agent-cursor/src/index.test.ts:
Took main's `expect(env["PATH"]).toBeUndefined()` — agents no
longer set PATH directly.
- packages/web/server/mux-websocket.ts:
Kept branch's Windows pipe handler branch and updated the inner
Unix `terminalManager.open(id, tmuxName)` call to main's new
signature with the optional tmuxName argument.
Verified: typecheck clean, lint 0 errors, full build (28 pkgs), tests
942/946 passing — the 4 failing tests in plugin-integration.test.ts
also fail on main itself (verified by running main's untouched test
file), so they're pre-existing flakes unrelated to this merge.
- platform.ts: add windowsHide:true to pwsh/powershell/taskkill/netstat
spawns so AO no longer flashes a console window for each subprocess.
- script-runner.ts: auto-detect Git Bash at the common install paths on
Windows when AO_BASH_PATH is unset; tighter error if neither auto-detect
nor override succeeds. WSL bash intentionally excluded — invoking it
from Windows-native Node mixes Linux paths with Windows cwd and
silently breaks repo scripts. Also adds windowsHide:true to the spawn.
- web/services.ts: register @aoagents/ao-plugin-runtime-process so the
dashboard can spawn sessions on projects using runtime: process
(the Windows default per getDefaultRuntime).
- pty-client.ts: chunk ptyHostSendMessage into 512-char frames with a
15ms gap so large prompts (~3-4KB+) are no longer truncated by
ConPTY's input buffer. Cross-platform safe; Unix PTYs absorb chunks
at full speed. Trailing Enter still sent as a separate frame after
the existing 300ms pause.
- Mock test helpers updated to handle the (cmd, args, options, callback)
arity introduced by passing windowsHide; new test covers Git Bash
auto-detection.
killDashboardOnPort runs a unix-only `ps` cmdline check before killing.
Without mocking exec, the call rejects and the catch returns false, so
killProcessTree is never called and the assertion fails on Linux CI.
Resolve tmux-utils.ts conflict: rewrite resolvePipePath to read
runtimeHandle.data.pipePath from on-disk session metadata instead of
recomputing a hash from AO_CONFIG_PATH. Mirrors the storageKey
disambiguation pattern upstream introduced for resolveTmuxSession in
PR #1488 (eca3001c). Recomputing the hash drifted from the runtime's
deriveStorageKey on Windows (raw backslash paths vs POSIX-normalized),
so the dashboard's named-pipe relay was connecting to the wrong path
and falling back to ENOENT while ao session attach worked fine.
Also normalize path separators in 6 upstream resolveTmuxSession tests
that hard-coded Unix forward slashes in their exists() mocks; on
Windows path.join produces backslashes and the assertions never
matched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before this, vitest worker isolation only overrode HOME, but on Windows
os.homedir() reads USERPROFILE. Tests that wrote to ~/.agent-orchestrator
ended up sharing the real user's data directory across workers, leading
to flaky cross-test pollution.
- test-utils: createTestEnvironment / setupTestContext now override both
HOME and USERPROFILE to the per-test fake home, restore both on
teardown. rmSync uses maxRetries:5/retryDelay:50 to ride out the same
Windows file-lock window that atomic-write retries cover.
- core test files (global-config, plugin-integration, portfolio-*,
project-resolver, recovery-actions, orchestrator-prompt*): set fake
USERPROFILE alongside HOME and use the retry-aware rmSync.
- update-check.test: normalize path separators in assertions
(path.replace(/\/g, "/")) so script-path matching works on Windows
without forcing the production code to emit posix paths.
- orchestrator-prompt.dist.test: pass shell:true on Windows to execFileSync
for .cmd targets, working around Node CVE-2024-27980's hardening.
Removed lifecycle-service.test.ts — it covered stopLifecycleWorker, which
was deleted when lifecycle was moved in-process during the merge with
main. The remaining lifecycle paths are exercised by lifecycle-manager
tests in core.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
runtime-process:
- Reserve the per-instance processes-map slot before the platform split
so the Windows ConPTY branch participates in duplicate-create detection
and getMetrics/getAttachInfo bookkeeping. Previously, Windows returned
a handle without storing it, so duplicate session IDs were silently
accepted and getMetrics always reported 0 uptime.
- Add 500ms graceful-exit poll before SIGKILLing the pty-host on destroy
so node-pty can dispose its ConPTY handle. Skipping this orphaned the
conpty_console_list_agent helper and triggered Windows Error Reporting
dialogs (0x800700e8) on real runs, not just tests.
- pty-host: install SIGTERM/SIGINT/SIGHUP/SIGBREAK/beforeExit handlers
that drive the same shutdown sequence (kill pty, drop clients, close
pipe, exit after 50ms grace), and route MSG_KILL_REQ through the same
path. Previously MSG_KILL_REQ only called pty.kill() and left the host
process lingering.
- Add windowsHide:true to the pty-host child spawn so node-pty's helper
console window stays hidden on errors.
workspace-worktree: normalize paths to a comparable POSIX form
(backslash→slash, lowercase drive letter) when matching git worktree
list --porcelain output against project directories. git emits
forward-slash paths on Windows; path.join produces native backslashes
— the comparison failed and list() returned empty.
agent-opencode: guard tmux/ps usage with isWindows() in isProcessRunning
so process-runtime sessions on Windows take the PID-signal path instead
of attempting Unix-only commands.
cli/start: detect Windows local paths in isLocalPath (drive letter
prefix, UNC path, .\, ..\) so spawn arguments like C:\... aren't
mistaken for project names.
integration test: replace cat + /tmp with platform-native echo (findstr
"x*" on Windows, cat on Unix) and os.tmpdir(); the original used
Unix-only tooling and would never run on Windows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): hide legacy spawn second positional (#1490)
Keep ao spawn help aligned with the real interface by exposing a single optional issue positional and rejecting extra positional arguments with replacement usage before any spawn work starts.
* fix(cli): align spawn usage with optional issue (#1490)
Keep the arity error and its test aligned with the optional [issue] contract surfaced by ao spawn help so review-thread guidance and user-facing usage match exactly.
* feat: add zsh completion for ao (#1371)
Add a generated zsh completion command and dynamic completion backend so ao can tab-complete projects and session IDs without relying on jq or brittle text parsing. Document standard zsh and Oh My Zsh install paths, and cover the new flow with CLI tests.
* fix(cli): address copilot completion review feedback for PR 1374
* fix completion and harden local workflow parsing
* Update packages/cli/src/lib/completion.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* fix completion regressions and agent-ci review feedback
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* fix(cli): derive projectId from prefixed issue id on spawn
When `ao spawn <projectId>/<issue>` is used in a multi-project config,
route the spawn to the prefixed project and strip the prefix from the
issue id. Previously the projectId fell back to whichever project
`ao start` was running for, tagging cross-project sessions with the
wrong project (and session prefix).
Applies to both `ao spawn` and `ao batch-spawn`; batch-spawn errors
out if issues mix multiple project prefixes.
Fixes#1329
* refactor(core,cli): lift spawn target resolution into core
Move the issue-prefix → project routing from the CLI into a reusable
core utility, `resolveSpawnTarget(projects, issueRef, fallback?)`.
- Exposes routing to any spawn entry point (CLI, web API, programmatic).
- Accepts either a project id or sessionPrefix as the prefix; project id
wins on collision.
- `ao batch-spawn` now groups issues by resolved project and preflights
once per group instead of erroring on mixed prefixes.
Tests:
- 8 unit tests for `resolveSpawnTarget` in core.
- CLI spawn.test.ts: adds a sessionPrefix routing test (23 tests total).
Refs #1329
* test(cli): cover batch-spawn grouping; tighten resolveSpawnTarget API
Self-review found three gaps:
- `resolveSpawnTarget` accepted `undefined` issueRef and returned
`{ issueId: "" }` with a fallback project. Dead path — both callers
guard against undefined. Drop it and make issueRef required.
- No tests for `batch-spawn`. Added two:
- routes cross-project prefixed issues to the correct project and
lists each project's sessions separately
- skips a prefixed issue when the target project already has an
active session for it
- `spawn` and `batch-spawn` help text didn't document the
`<projectId>/<issue>` or `<sessionPrefix>/<issue>` forms.
* fix(core): guard resolveSpawnTarget against prototype-key matches
Second review pass found a latent bug:
Plain JS objects inherit `__proto__`, `constructor`, `toString`,
`hasOwnProperty`, etc. from Object.prototype. The previous
`if (projects[prefix])` check entered the routing branch for any of
these keys, mis-routing a user typing `ao spawn __proto__/42` with
`projectId: "__proto__"`. Downstream session-manager would then receive
a junk project object (Object.prototype itself) and fail with a
non-obvious error.
Fix: use `Object.prototype.hasOwnProperty.call(projects, prefix)` so
only actual configured project ids match.
Also:
- Document the case-sensitive matching semantic explicitly.
- Lock in the batch-spawn grouping contract by asserting exact
`list()` and `spawn()` call counts in the cross-project test.
* feat(cli): hide terminal session rows by default in session ls
- Filter with TERMINAL_STATUSES unless --include-terminated
- Hint when completed sessions are hidden; docs/CLI.md updated
Made-with: Cursor
* fix(cli): keep session ls JSON inventory stable
Address review concerns by preserving terminal sessions in `ao session ls --json` output while keeping terminal rows hidden by default in text output. Use the core terminal-session predicate, improve hidden-row messaging, and add regression tests for mixed/hinted and JSON behavior.
Made-with: Cursor
* fix(cli): resolve rebase conflicts with latest session ls behavior
Align session command and CLI docs with the current mainline JSON contract (`data` + `meta.hiddenTerminatedCount`) while preserving terminal-filter defaults and updated regression coverage.
Made-with: Cursor
* fix: fail send when killed session delivery is not confirmed
* fix(core): drop requireConfirmation throw that re-introduced duplicate-message bug
The PR's sendWithConfirmation added a throw when confirmation heuristics
did not flip within SEND_CONFIRMATION_ATTEMPTS on a restored session.
But runtimePlugin.sendMessage had already fired, so the throw bubbled up
to the lifecycle manager's catch-all, leaving lastCIFailureDispatchHash
(and its merge-conflict twin) unset. Next poll re-dispatched the same
message — exactly the duplicate-message bug that commit 77685a5 removed.
Real fix for #1074 is preserved: restoreForDelivery still throws when
waitForRestoredSession returns false, which happens before sendMessage
fires. Killed sessions that cannot be revived are still reported as
failures, with no duplicate-send risk.
- sendWithConfirmation no longer takes requireConfirmation; unconfirmed
delivery always returns (soft success).
- prepareSession returns Session (the tuple only existed to drive the
removed throw).
- send's retry predicate reverts to prepared.restoredAt === undefined
&& isRestorable(prepared).
- Adds regression test: restored session + sendMessage fires +
confirmation never flips → send() resolves.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat(cli): filter terminated sessions from ao session ls / ao status by default
Closes#1310. Terminated sessions (killed/terminated/done/merged/errored/cleanup,
plus lifecycle-driven terminal states) are now hidden from `ao session ls` and
`ao status` by default. A dim footer reports how many were hidden and how to
surface them. Pass `--include-terminated` to restore the full list.
JSON output wraps into `{ data: [...], meta: { hiddenTerminatedCount } }` on
both commands so text and machine-readable views tell the same story. This is a
breaking change for script consumers of `--json`; `--include-terminated` is the
escape hatch.
Orthogonal to `-a, --all` (orchestrator visibility, unchanged). Restore of
terminated sessions by id is unaffected — that path goes through `sm.get`, not
`sm.list`.
Docs (`SETUP.md`, `docs/CLI.md`) updated to match. Tests cover both the legacy
status branch and the canonical lifecycle branch of `isTerminalSession`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): drop unused `lc` param in lifecycle-alive test case
ESLint's no-unused-vars rejects unprefixed unused args. The "alive — should
remain visible" branch of the new lifecycle-driven filter test in
`session.test.ts` took `lc` but never touched it. Switch to `()`. Matches the
equivalent case in `status.test.ts`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(core): preserve pr.state=merged when legacy metadata lacks pr= URL
Review blocker on PR #1340: a metadata file with `status=merged` but no `pr=`
URL was still showing as active in `ao session ls` / `ao status` by default.
Root cause: `synthesizePRState()` in lifecycle-state.ts short-circuited to
`{ state: "none" }` whenever no PR URL was present, ignoring the fact that the
legacy `status` column already encodes terminal truth. Once lifecycle was
synthesized as `session.state="idle"` + `pr.state="none"`, `deriveLegacyStatus`
returned `"idle"` and `isTerminalSession()` (lifecycle branch) returned false.
The new CLI filter then let the session through.
Fix: when legacy `status === "merged"` and no URL is available, synthesize
`pr.state="merged", reason="merged"` with `number: null, url: null`. The
terminal signal survives the flat-metadata → canonical-lifecycle round trip.
Also:
- Export `sessionFromMetadata` from the core barrel. CLI tests and external
consumers need it to round-trip metadata through the canonical lifecycle.
- Update CLI `buildSessionsFromDir` helpers to route through `sessionFromMetadata`
so mocked `sm.list()` reflects production reconstruction (the old shortcut
bypassed synthesis entirely and was the reason the bug slipped past the
original test suite).
- Add regression tests: one at the core level (`parseCanonicalLifecycle` for
merged-without-URL) and one integration-style test per CLI command asserting
the reviewer's exact repro produces the expected filtered output.
- One pre-existing test expectation updated: when metadata has `status=working`
and `pr=<url>`, the reconstructed status is `pr_open`, not `working`. That's
what production `sm.list()` has always returned; the test was previously
hiding behind the reconstruction shortcut.
Changeset bumped to include ao-core (patch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(cli): route review-check helper through sessionFromMetadata
Last remaining test-fidelity shortcut flagged by codex on PR #1340. The
`buildSessionsFromDir` helper in review-check.test.ts fabricated Session
objects by hand, bypassing the canonical lifecycle reconstruction that
production `sm.list()` runs. Doesn't affect review-check's actual behavior
(which reads `session.metadata["pr"]` directly), but aligns this test with
the equivalent helpers in session.test.ts and status.test.ts so future
lifecycle changes don't silently skip this surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes#1048. ao start used to allocate a fresh `{prefix}-orchestrator-N`
on every invocation instead of reattaching to the previous session, and
the dashboard's orchestrator link pointed at a different id than the
CLI just printed.
Changes:
runStartup (packages/cli/src/commands/start.ts):
- On startup, list existing orchestrators for the project, partition
them into live (runtime still running) and restorable (terminal but
sm.restore()-able) buckets, pick the most-recently-active from the
chosen bucket, and reuse/restore that id instead of spawning a new
one. Only spawn fresh when both buckets are empty.
- Live is preferred UNCONDITIONALLY over restorable — a newer killed
record can never beat an older-but-running one. Without this, a
cross-bucket sort could resurrect a killed record via sm.restore()
while the live orchestrator kept running, leaving two alive.
- Restored sessions get an explicit "(restored)" marker in the CLI
summary so the resurfaced id isn't a surprise.
- The phantom `${prefix}-orchestrator` id constant is removed from
every URL print, browser-open target, and summary line. Everything
now uses the real selected id.
registerStop (same file):
- ao stop now resolves the real orchestrator via sm.list(projectId)
+ isOrchestratorSession filter + most-recently-active sort, then
calls sm.kill on that id. The old phantom `${prefix}-orchestrator`
target never matched a real numbered record, so ao stop was a
silent no-op and the orchestrator kept running on disk between
start cycles. sm.list-failure warning no longer duplicates with the
generic "no orchestrator found" message.
isOrchestratorSession (packages/core/src/types.ts):
- Tightened: legacy bare-id records (`{projectId}-orchestrator` with
no role metadata) are no longer recognized as orchestrators by the
public predicate. This was the source of the dashboard/CLI id
divergence — stale bare records with a different prefix than the
numbered form were leaking into the dashboard's orchestrator list.
session-manager repair (packages/core/src/session-manager.ts):
- Split `isOrchestratorSessionRecord` (permissive, used by cleanup
protection) from a new `isRepairableOrchestratorRecord` (stricter,
used only by repairSingleSessionMetadataOnRead and
repairSessionMetadataOnRead).
- The strict repair predicate accepts role-stamped records, the bare
`{sessionPrefix}-orchestrator` correct-prefix legacy shape, and the
numbered `{sessionPrefix}-orchestrator-N` worktree shape. It
rejects foreign bare names like `{projectId}-orchestrator`, so
those records never get `role: orchestrator` backfilled on read
and therefore can no longer pass `isOrchestratorSession()` in real
`sm.list()` output via the role-metadata branch.
Tests added (~12):
- runStartup: live reuse, restore-on-killed, ignore-stale-bare
legacy records, live-beats-restorable regression, multi-live
reuse, URL fallback when --no-orchestrator.
- ao stop: kills the actual numbered id (not the phantom), handles
multiple orchestrators, tolerates sm.list throwing.
- isOrchestratorSession: rejects stale bare ids without role
metadata; accepts bare ids with role metadata stamped.
- listDashboardOrchestrators: stale bare excluded, numbered live
included, role-stamped legacy included.
- session-manager repair: does not backfill role onto foreign bare-id
records (issue #1048 regression guard).
Unblocks: review comments from cursor[bot] (dead else-if branch,
double messaging, redundant isTerminalSession check) and illegalcall
(cross-bucket sort, repair-backfill bypass of predicate tightening) —
all addressed in-place with the multi-orchestrator model preserved.
Verified: core 606/606, cli 450/450, typecheck clean across core/cli/web.
Represent missing activity probes as first-class signal states so lifecycle inference only treats valid idle evidence as proof. This prevents false stuck transitions, keeps API/UI lifecycle truth aligned, and makes root monorepo verification deterministic by serializing recursive build and typecheck.
Introduce an explicit reporting channel so worker agents can self-declare
their workflow phase (started/working/waiting/needs-input/fixing-ci/
addressing-reviews/completed). Fresh reports are trusted over weak inference
but runtime death, activity-based waiting_input, and SCM ground truth still
take precedence.
- Add `applyAgentReport`, validator, canonical mapping, and freshness helper
in `packages/core/src/agent-report.ts`.
- Wire the fallback into `determineStatus` just before the idle-beyond-
threshold promotion, skipping orchestrator and terminal sessions.
- Add `ao acknowledge` and `ao report <state>` CLI commands. Both resolve
the session from `AO_SESSION_ID` when no argument is passed.
- Teach the base agent prompt and orchestrator prompt about the new
reporting commands.
- Ship unit tests covering normalization, mapping, transition validation,
metadata persistence, freshness, and first-start behavior.
Stage 3 of the state-machine redesign (see aa-2/state-machine-redesign-
rollout-plan.md).
- New repo-utils.ts with extractOwnerRepo() and isValidRepoString()
shared across detectEnvironment, autoCreateConfig, addProjectToConfig
- Remote regex now supports GitLab subgroup paths (group/subgroup/repo)
- Repo validation accepts multi-segment paths (owner/repo and deeper)
- Updated prompt text to mention group/subgroup/repo format
- Updated ProjectConfig.repo docstring to be provider-neutral
- Tests import from shared helpers instead of duplicating regex
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@clack/prompts text() can return undefined when submitted with just
the placeholder value. Guard with typeof check in promptText and
defensive (entered || "") in both callers to prevent .trim() crash.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- detect-env: test GitHub/GitLab HTTPS/SSH remote extraction, unknown
hosts returning null, missing remote, and non-git directories
- repo-validation: test the anchored regex accepts owner/repo, rejects
empty, lone slash, missing segments, whitespace, and nested paths
- config-validation: test SCM/tracker inference skipped when repo is
missing or has no slash, and inferred correctly with owner/repo
- scm-webhooks: test eventMatchesProject returns false when project
has no repo configured
- orchestrator-prompt: existing test covers repo:undefined → "not configured"
- prompt-builder: existing test covers BASE_AGENT_PROMPT_NO_REPO selection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Anchor repo validation regex with $ to reject trailing junk like
"acme/repo extra" or "acme/repo#frag" — both prompt locations
- Widen detectEnvironment and addProjectToConfig remote regex to
match gitlab.com in addition to github.com, so GitLab repos get
auto-detected owner/repo instead of silently skipping it
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
afterEach calls vi.restoreAllMocks() which restores the top-level mock
to the real function. Use vi.spyOn on the module namespace instead of
vi.mocked on the destructured import so the mock survives restoration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The test has no ownerRepo detected, which triggers the interactive
repo prompt. Mock isHumanCaller to false so the prompt is skipped
in the non-interactive test environment.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Fix ao start <path> regression: when target path differs from cwd,
run autoCreateConfig on the target (which is a git repo) instead of
cwd (which may not be). Removes the double-create pattern that added
a second project entry.
2. Gate remaining PR/CI sections in orchestrator-prompt: PR Takeover,
PR Review Flow, Bulk Issue Processing, and Monitoring Progress
details are now wrapped in project.repo checks so repo-less projects
don't get contradictory instructions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move ensureGit() after path dedup check so already-registered paths
return early without requiring git
- Use realpathSync for canonical path comparison (resolves symlinks,
case variants, trailing slashes)
- Bail out with error when both SIGTERM and SIGKILL fail to stop AO
instead of unconditionally unregistering a live instance
- Rewrite path-dedup test to exercise the path-arg branch via
AO_CONFIG_PATH, covering addProjectToConfig's dedup lines
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The test was mocking detectEnvironment with isGitRepo: false, which now
correctly triggers the fail-fast error for non-git directories. Updated
to isGitRepo: true since the test is verifying config generation
defaults, not non-git behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>