diff --git a/.claude/skills/runnable-worktrees/SKILL.md b/.claude/skills/runnable-worktrees/SKILL.md index c9163691d1..a55f595d55 100644 --- a/.claude/skills/runnable-worktrees/SKILL.md +++ b/.claude/skills/runnable-worktrees/SKILL.md @@ -29,6 +29,11 @@ command. Opt-in via `.codev/config.json`; unconfigured repos see zero behavior c **unless** the entry ends in a slash: `".local-user-data/"` is treated as a literal path and links the directory whole — shared with the parent, not branch-isolated. A dangling link is fine if the source does not exist yet. + **Native Windows:** links are real symlinks when Developer Mode is on. Otherwise a directory + becomes a junction and a file a hard link, or a copy when no hard link can be made (another + volume); a copy does not reflect later edits, and `afx spawn` warns when it makes one. +- **Scripts on Windows** — `postSpawn` and `devCommand` run under Git Bash, so a script file + they call needs LF line endings (`codev doctor` warns about CRLF ones). - **`postSpawn`** — commands run sequentially with `cwd` = worktree path. A non-zero exit aborts the spawn loudly; the half-built worktree stays for inspection. - **`devCommand`** — the foreground command that starts your dev process. Required for diff --git a/.codex/skills/runnable-worktrees/SKILL.md b/.codex/skills/runnable-worktrees/SKILL.md index c9163691d1..a55f595d55 100644 --- a/.codex/skills/runnable-worktrees/SKILL.md +++ b/.codex/skills/runnable-worktrees/SKILL.md @@ -29,6 +29,11 @@ command. Opt-in via `.codev/config.json`; unconfigured repos see zero behavior c **unless** the entry ends in a slash: `".local-user-data/"` is treated as a literal path and links the directory whole — shared with the parent, not branch-isolated. A dangling link is fine if the source does not exist yet. + **Native Windows:** links are real symlinks when Developer Mode is on. Otherwise a directory + becomes a junction and a file a hard link, or a copy when no hard link can be made (another + volume); a copy does not reflect later edits, and `afx spawn` warns when it makes one. +- **Scripts on Windows** — `postSpawn` and `devCommand` run under Git Bash, so a script file + they call needs LF line endings (`codev doctor` warns about CRLF ones). - **`postSpawn`** — commands run sequentially with `cwd` = worktree path. A non-zero exit aborts the spawn loudly; the half-built worktree stays for inspection. - **`devCommand`** — the foreground command that starts your dev process. Required for diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..99c27b3f39 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +# Shell scripts run under bash, which is Git Bash on native Windows (Spec 1658), +# and bash fails on CRLF line endings. Pin LF in the working tree whatever the +# checkout's core.autocrlf, so the forge concept scripts that ship in +# packages/codev/scripts/forge/ (and every other .sh) stay runnable. +*.sh text eol=lf +*.bash text eol=lf + +# Windows batch files need CRLF; cmd.exe misparses some LF-only scripts. +*.cmd text eol=crlf + +# Render-gate fixtures are raw PTY bytes (Spec 1658): never convert their line +# endings, or a Windows checkout with core.autocrlf=true rewrites the captures. +packages/codev/src/agent-farm/__tests__/fixtures/gate/** -text diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 9884c569b4..afed190f06 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,29 +1,63 @@ name: Windows (on demand) +# Each dispatch names its purpose, so the run list says what every run was for. +run-name: "Windows: ${{ inputs.purpose || 'unlabelled' }}" # Native Windows checks for Spec 1658. Manual dispatch ONLY: this workflow never runs on # push or pull_request and is never a required check (owner ruling, 2026-10-21). Dispatch it # before a release and on any PR that touches platform code: # -# gh workflow run windows.yml --ref [-f filter=] [-f tower=true] +# gh workflow run windows.yml --ref -f purpose='' [-f suites=true] [-f filter=] [-f tower=true] # # Jobs: -# probe scripts/windows-probe.mjs on Node 20/22/24: Git Bash, PATHEXT, named pipes, +# probe scripts/windows-probe.mjs on Node 20/22/24 (x64) and 22/24 (ARM64): Git Bash, PATHEXT, named pipes, # CIM census latency, job objects, detached survival, native-dependency installs. -# With tower=true the Node 22 leg also starts Tower from the built checkout. +# With tower=true the x64 Node 22 leg also starts Tower from the built checkout (building +# the repo on ARM64 fails in the artifact-canvas bundler; users install prebuilt packages). # targeted runs the packages/codev vitest files matching `filter` (skipped when empty). +# install-guard (install_guard=true) packs the branch packages, then on Node 20 installs them +# globally with node-gyp replaced by a failing stub: the install must fail with the +# supported-versions message and never reach the stub; on Node 22 it must succeed. +# unit, cli-e2e, post-release-e2e (suites=true) the full suites on windows-latest, Node 22 and 24 +# (Node 20 has no better-sqlite3 prebuild on Windows, so it cannot install there): +# unit runs the sdk, core and codev unit tests; cli-e2e runs the built CLI tests; +# post-release-e2e packs the dispatched ref's packages and installs those tarballs +# with verify-install.mjs, or a published release when `version` is set (setting +# `version` without `suites` runs this job and the probes, not unit or cli-e2e). on: workflow_dispatch: inputs: + purpose: + description: 'What this run is for (shown as the run name)' + required: false + default: '' filter: description: 'Vitest filter for the targeted job (packages/codev); empty skips it' required: false default: '' tower: - description: 'Probe: also try `afx tower start` from the built checkout' + description: 'Probe: also try `afx tower start` (built checkout on Node 22; published package on every leg)' + type: boolean + required: false + default: false + install_guard: + description: 'Run the native-dependency install guard check (Node 20 refused, Node 22 installs)' type: boolean required: false default: false + suites: + description: 'Run the full unit, cli-e2e and post-release-e2e suites (about an hour)' + type: boolean + required: false + default: false + version: + description: 'post-release-e2e: verify this published @cluesmith/codev version instead of the packed branch (a release after 3.3.5)' + required: false + default: '' + published: + description: 'Published @cluesmith/codev versions to probe with tower=true (space-separated; 3.3.3 was current on 2026-09-09)' + required: false + default: 'latest' defaults: run: @@ -36,13 +70,21 @@ env: jobs: probe: - name: Probe (Node ${{ matrix.node }}) - runs-on: windows-latest + name: Probe (${{ matrix.os }}, Node ${{ matrix.node }}) + runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: fail-fast: false matrix: + os: [windows-latest] node: ['20', '22', '24'] + # ARM64: the spec supports it only if the native dependencies install from prebuilds; + # measured here rather than inferred from release asset names. + include: + - os: windows-11-arm + node: '22' + - os: windows-11-arm + node: '24' steps: - name: Checkout repository uses: actions/checkout@v4 @@ -56,34 +98,99 @@ jobs: node-version: ${{ matrix.node }} - name: Probe platform facts and native-dependency installs - run: node scripts/windows-probe.mjs --deps --out "$RUNNER_TEMP/probe-node${{ matrix.node }}.json" + run: node scripts/windows-probe.mjs --deps --out "$RUNNER_TEMP/probe-${{ matrix.os }}-node${{ matrix.node }}.json" - name: Install dependencies - if: ${{ inputs.tower && matrix.node == '22' }} + if: ${{ !cancelled() && inputs.tower && matrix.node == '22' && matrix.os == 'windows-latest' }} run: pnpm install - name: Build packages/codev - if: ${{ inputs.tower && matrix.node == '22' }} + if: ${{ !cancelled() && inputs.tower && matrix.node == '22' && matrix.os == 'windows-latest' }} working-directory: packages/codev run: pnpm build - name: Probe Tower start - if: ${{ inputs.tower && matrix.node == '22' }} - run: node scripts/windows-probe.mjs --tower --out "$RUNNER_TEMP/probe-tower.json" + if: ${{ !cancelled() && inputs.tower && matrix.node == '22' && matrix.os == 'windows-latest' }} + run: node scripts/windows-probe.mjs --tower --out "$RUNNER_TEMP/probe-tower-${{ matrix.os }}.json" + + - name: Probe Tower start from the published package (global install) + if: ${{ !cancelled() && inputs.tower }} + # Node 20 cannot install the package on Windows (no better-sqlite3 prebuild; audit 1.1), so + # that leg records the failure without turning the run red. + continue-on-error: ${{ matrix.node == '20' }} + env: + PUBLISHED: ${{ inputs.published }} + run: | + specs=() + for v in $PUBLISHED; do specs+=(--tower-published "$v"); done + node scripts/windows-probe.mjs "${specs[@]}" --out "$RUNNER_TEMP/probe-published-${{ matrix.os }}-node${{ matrix.node }}.json" - name: Upload probe reports if: always() uses: actions/upload-artifact@v4 with: - name: windows-probe-node${{ matrix.node }} + name: windows-probe-${{ matrix.os }}-node${{ matrix.node }} path: ${{ runner.temp }}/probe-*.json if-no-files-found: warn + install-guard: + name: Install guard (native dependencies) + if: ${{ inputs.install_guard }} + runs-on: windows-latest + timeout-minutes: 45 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js 22 (build and pack) + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install + + - name: Build packages/codev + working-directory: packages/codev + run: pnpm build + + - name: Pack the branch packages + run: | + mkdir -p "$RUNNER_TEMP/packs" + for p in codev-types codev-core codev-sdk codev; do + pnpm --filter "@cluesmith/$p" pack --pack-destination "$RUNNER_TEMP/packs" + done + ls -l "$RUNNER_TEMP/packs" + + - name: Setup Node.js 20 (unsupported on native Windows) + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Control - without the guard, better-sqlite3 falls back to node-gyp on Node 20 + run: node scripts/windows-install-guard-check.mjs --control "$RUNNER_TEMP/packs" + + - name: Guard - Node 20 install fails with the supported versions, node-gyp never runs + run: node scripts/windows-install-guard-check.mjs --unsupported "$RUNNER_TEMP/packs" + + - name: Setup Node.js 22 (supported) + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Supported - Node 22 installs from prebuilds + run: node scripts/windows-install-guard-check.mjs --supported "$RUNNER_TEMP/packs" + targeted: name: Targeted tests if: ${{ inputs.filter != '' }} runs-on: windows-latest - timeout-minutes: 45 + # The full unit suite (filter `src`) runs about 50 minutes on windows-latest. + timeout-minutes: 120 steps: - name: Checkout repository uses: actions/checkout@v4 @@ -108,4 +215,161 @@ jobs: working-directory: packages/codev env: FILTER: ${{ inputs.filter }} - run: pnpm exec vitest run $FILTER + # Read-only GitHub concepts run live (Spec 1658 scenario 8). + GH_TOKEN: ${{ github.token }} + run: pnpm exec vitest run "$FILTER" --reporter=default --reporter=json --outputFile.json="$RUNNER_TEMP/vitest-report.json" + + - name: Upload the test report + if: always() + uses: actions/upload-artifact@v4 + with: + name: windows-vitest-report + path: ${{ runner.temp }}/vitest-report.json + if-no-files-found: warn + + unit: + name: Unit tests (Node ${{ matrix.node }}) + if: ${{ inputs.suites }} + runs-on: windows-latest + # The codev unit suite alone runs about 50 minutes on windows-latest. + timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + node: ['22', '24'] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install + + # Builds codev's workspace dependencies (types, sdk, core) first. + - name: Build packages/codev + working-directory: packages/codev + run: pnpm build + + - name: Run sdk unit tests + working-directory: packages/sdk + run: pnpm test + + - name: Run core unit tests + working-directory: packages/core + run: pnpm test + + - name: Copy skeleton for unit tests + working-directory: packages/codev + run: pnpm copy-skeleton + + - name: Run codev unit tests + working-directory: packages/codev + env: + # Read-only GitHub concepts run live (Spec 1658 scenario 8). + GH_TOKEN: ${{ github.token }} + run: pnpm exec vitest run --reporter=default --reporter=json --outputFile.json="$RUNNER_TEMP/vitest-unit-report.json" + + - name: Upload the test report + if: always() + uses: actions/upload-artifact@v4 + with: + name: windows-unit-report-node${{ matrix.node }} + path: ${{ runner.temp }}/vitest-unit-report.json + if-no-files-found: warn + + cli-e2e: + name: CLI tests (Node ${{ matrix.node }}) + if: ${{ inputs.suites }} + runs-on: windows-latest + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + node: ['22', '24'] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install + + - name: Build packages/codev + working-directory: packages/codev + run: pnpm build + + - name: Run CLI integration tests + working-directory: packages/codev + run: pnpm exec vitest run --config vitest.cli.config.ts + + post-release-e2e: + name: Install verification (Node ${{ matrix.node }}) + if: ${{ inputs.suites || inputs.version != '' }} + runs-on: windows-latest + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + node: ['22', '24'] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install pnpm + if: ${{ inputs.version == '' }} + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + + - name: Install dependencies + if: ${{ inputs.version == '' }} + run: pnpm install + + - name: Build packages/codev + if: ${{ inputs.version == '' }} + working-directory: packages/codev + run: pnpm build + + # codev-types, codev-core and codev-sdk are runtime dependencies of codev, so the branch + # install takes their tarballs too (as test.yml's install verification does). + - name: Pack the branch packages + if: ${{ inputs.version == '' }} + run: | + mkdir -p "$RUNNER_TEMP/packs" + for p in codev-types codev-core codev-sdk codev; do + pnpm --filter "@cluesmith/$p" pack --pack-destination "$RUNNER_TEMP/packs" + done + ls -l "$RUNNER_TEMP/packs" + + - name: Download the published package + if: ${{ inputs.version != '' }} + env: + VERSION: ${{ inputs.version }} + run: | + mkdir -p "$RUNNER_TEMP/packs" + npm view "@cluesmith/codev@$VERSION" version + npm pack "@cluesmith/codev@$VERSION" --pack-destination "$RUNNER_TEMP/packs" + + # A user's npm runs install scripts under cmd.exe, so the workflow's Git Bash script shell + # (needed only to build this repo) is removed for the install, as windows-probe.mjs does. + - name: Verify installation + run: env -u npm_config_script_shell node packages/codev/scripts/verify-install.mjs "$RUNNER_TEMP"/packs/*.tgz diff --git a/AGENTS.md b/AGENTS.md index e0813146ac..3927277f30 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -198,6 +198,12 @@ Configuration lives in `.codev/config.json`. UI changes (tower, dashboard, terminal) must be verified in a browser via Playwright before being called done — see `codev/resources/testing-guide.md`. +Native Windows has no automatic CI. A PR that touches platform code (process spawning, shells, +paths, terminals/shellper, Tower lifecycle, install scripts) dispatches the on-demand Windows +workflow on its branch and links the run in the PR: +`gh workflow run windows.yml --ref -f purpose='' -f suites=true` +(add `-f install_guard=true` when the PR touches install scripts). + ## Releasing Say "Let's release v1.6.0"; the RELEASE protocol (`codev/protocols/release/protocol.md`) diff --git a/CLAUDE.md b/CLAUDE.md index e0813146ac..3927277f30 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -198,6 +198,12 @@ Configuration lives in `.codev/config.json`. UI changes (tower, dashboard, terminal) must be verified in a browser via Playwright before being called done — see `codev/resources/testing-guide.md`. +Native Windows has no automatic CI. A PR that touches platform code (process spawning, shells, +paths, terminals/shellper, Tower lifecycle, install scripts) dispatches the on-demand Windows +workflow on its branch and links the run in the PR: +`gh workflow run windows.yml --ref -f purpose='' -f suites=true` +(add `-f install_guard=true` when the PR touches install scripts). + ## Releasing Say "Let's release v1.6.0"; the RELEASE protocol (`codev/protocols/release/protocol.md`) diff --git a/README.md b/README.md index eba61a42d1..bce449c07a 100644 --- a/README.md +++ b/README.md @@ -114,10 +114,14 @@ If Tower is running during an upgrade, restart it afterwards so it picks up the | Dependency | Install | Purpose | |------------|---------|---------| -| Node.js 18+ | `brew install node` | Runtime | +| Node.js 20+ (22 recommended) | `brew install node` | Runtime | | git 2.5+ | (pre-installed) | Version control | | AI CLIs | See below | All three recommended | +On Linux and WSL, also install `build-essential` and `python3` (node-pty compiles from source). + +**Windows:** two supported paths. **Native**: Node.js 22 or 24 (x64 or ARM64), [Git for Windows](https://git-scm.com/download/win) and `jq`, no admin rights or Developer Mode needed. **WSL 2**: follow the Linux instructions inside your distribution. See [DEPENDENCIES.md](codev-skeleton/DEPENDENCIES.md#windows) for setup and limitations. + **AI CLIs** (install all three for multi-model consultation): - Claude Code: `npm install -g @anthropic-ai/claude-code` - Antigravity CLI (`agy`, the `gemini` consult lane): `curl -fsSL https://antigravity.google/cli/install.sh | bash`, then run `agy` once to sign in (OAuth — replaces the retired Gemini CLI) diff --git a/apps/vscode/src/__tests__/agent-cycle-order.test.ts b/apps/vscode/src/__tests__/agent-cycle-order.test.ts index 7311460d81..400c633b20 100644 --- a/apps/vscode/src/__tests__/agent-cycle-order.test.ts +++ b/apps/vscode/src/__tests__/agent-cycle-order.test.ts @@ -63,11 +63,16 @@ const { BuildersProvider, partitionArchitectGroups, agentTargetIsFocused, agentC const { BuilderGroupTreeItem, IdleArchitectsGroupTreeItem, BuilderTreeItem } = await import('../views/builder-tree-item.js'); import type { AgentTarget } from '../views/builders.js'; import type { BuilderGroup } from '../views/builder-grouping.js'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +/** A worktree path that does not exist: the review-file probe (existsSync) finds nothing. */ +const MISSING_WORKTREE = join(tmpdir(), 'codev-test-missing-worktree'); function builder(overrides: Partial): OverviewBuilder { return { id: 'pir-1', issueId: '1', issueTitle: 't', phase: 'implement', protocolPhase: 'implement', - mode: 'strict', gates: {}, worktreePath: '/tmp/wt', roleId: null, protocol: 'pir', + mode: 'strict', gates: {}, worktreePath: MISSING_WORKTREE, roleId: null, protocol: 'pir', planPhases: [], progress: 0, blocked: null, blockedGate: null, blockedSince: null, startedAt: null, idleMs: 0, lastDataAt: null, spawnedByArchitect: null, area: 'Uncategorized', prReady: false, diff --git a/apps/vscode/src/__tests__/builder-grouping.test.ts b/apps/vscode/src/__tests__/builder-grouping.test.ts index af8df6a9cb..d12b809858 100644 --- a/apps/vscode/src/__tests__/builder-grouping.test.ts +++ b/apps/vscode/src/__tests__/builder-grouping.test.ts @@ -4,6 +4,7 @@ * and flatten rule; this pins those per-axis decisions in one place. Pure / * vscode-free, so it runs under the vitest harness. */ +// spec-1658-tmp: opaque (/tmp/wt is a worktreePath field in fake builder rows for pure grouping helpers) import { describe, it, expect } from 'vitest'; import type { OverviewBuilder } from '@cluesmith/codev-types'; diff --git a/apps/vscode/src/__tests__/builder-row.test.ts b/apps/vscode/src/__tests__/builder-row.test.ts index 7a2c11321a..0033e47b7b 100644 --- a/apps/vscode/src/__tests__/builder-row.test.ts +++ b/apps/vscode/src/__tests__/builder-row.test.ts @@ -8,6 +8,7 @@ * Lives in `__tests__/` (vitest) rather than `src/test/` (vscode-test Electron * harness) because the helpers touch no `vscode` APIs. */ +// spec-1658-tmp: opaque (/tmp/wt is a worktreePath field in fake builder rows for pure label/icon helpers) import { describe, it, expect } from 'vitest'; import type { OverviewBuilder } from '@cluesmith/codev-types'; diff --git a/apps/vscode/src/__tests__/builders-accordion.test.ts b/apps/vscode/src/__tests__/builders-accordion.test.ts index 977680b051..80104802c9 100644 --- a/apps/vscode/src/__tests__/builders-accordion.test.ts +++ b/apps/vscode/src/__tests__/builders-accordion.test.ts @@ -18,6 +18,11 @@ import { describe, it, expect, vi } from 'vitest'; import type { OverviewBuilder, OverviewData } from '@cluesmith/codev-types'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +/** A worktree path that does not exist: the review-file probe (existsSync) finds nothing. */ +const MISSING_WORKTREE = join(tmpdir(), 'codev-test-missing-worktree'); vi.mock('vscode', () => { class FakeEventEmitter { @@ -60,7 +65,7 @@ const { BuilderTreeItem, BuilderGroupTreeItem } = await import('../views/builder function builder(overrides: Partial): OverviewBuilder { return { id: 'pir-1', issueId: '1', issueTitle: 't', phase: 'implement', protocolPhase: 'implement', - mode: 'strict', gates: {}, worktreePath: '/tmp/wt', roleId: null, protocol: 'pir', + mode: 'strict', gates: {}, worktreePath: MISSING_WORKTREE, roleId: null, protocol: 'pir', planPhases: [], progress: 0, blocked: null, blockedGate: null, blockedSince: null, startedAt: null, idleMs: 0, lastDataAt: null, spawnedByArchitect: null, area: 'Uncategorized', prReady: false, diff --git a/apps/vscode/src/__tests__/builders-architect-header-command.test.ts b/apps/vscode/src/__tests__/builders-architect-header-command.test.ts index 52984dd995..6ac8cdee44 100644 --- a/apps/vscode/src/__tests__/builders-architect-header-command.test.ts +++ b/apps/vscode/src/__tests__/builders-architect-header-command.test.ts @@ -13,6 +13,7 @@ * carry no command. Mocks `vscode` per the established `__tests__` pattern (see * builders-autoreveal.test.ts). */ +// spec-1658-tmp: opaque (/tmp/wt is a worktreePath field in fake builder rows; only group headers render, so no builder row probes the disk) import { describe, it, expect, vi } from 'vitest'; import type { OverviewBuilder, OverviewData } from '@cluesmith/codev-types'; diff --git a/apps/vscode/src/__tests__/builders-autoreveal.test.ts b/apps/vscode/src/__tests__/builders-autoreveal.test.ts index 341c820a4e..9653050283 100644 --- a/apps/vscode/src/__tests__/builders-autoreveal.test.ts +++ b/apps/vscode/src/__tests__/builders-autoreveal.test.ts @@ -18,6 +18,12 @@ import { describe, it, expect, vi } from 'vitest'; import type { OverviewBuilder, OverviewData } from '@cluesmith/codev-types'; import type { BuilderFileChange } from '../views/builder-diff-cache.js'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +// spec-1658-tmp: opaque (the remaining /tmp/wt strings are labels on BuilderFileTreeItem, never read from disk) +/** A worktree path that does not exist: the review-file probe (existsSync) finds nothing. */ +const MISSING_WORKTREE = join(tmpdir(), 'codev-test-missing-worktree'); /** Per-test config overrides the mocked `workspace.getConfiguration` reads. */ const configValues: Record = {}; @@ -78,7 +84,7 @@ function mk(relPath: string): BuilderFileChange { function builder(overrides: Partial): OverviewBuilder { return { id: 'pir-1', issueId: '1', issueTitle: 't', phase: 'implement', protocolPhase: 'implement', - mode: 'strict', gates: {}, worktreePath: '/tmp/wt', roleId: null, protocol: 'pir', + mode: 'strict', gates: {}, worktreePath: MISSING_WORKTREE, roleId: null, protocol: 'pir', planPhases: [], progress: 0, blocked: null, blockedGate: null, blockedSince: null, startedAt: null, idleMs: 0, lastDataAt: null, spawnedByArchitect: null, area: 'Uncategorized', prReady: false, diff --git a/apps/vscode/src/__tests__/builders-childless-architect-header.test.ts b/apps/vscode/src/__tests__/builders-childless-architect-header.test.ts index 48a4cfb2d1..a8b40881fc 100644 --- a/apps/vscode/src/__tests__/builders-childless-architect-header.test.ts +++ b/apps/vscode/src/__tests__/builders-childless-architect-header.test.ts @@ -12,6 +12,7 @@ * the worst-of tri-state green/yellow. They also pin that stage/area axes are * unaffected (roster ignored). Mocks `vscode` per the `__tests__` pattern. */ +// spec-1658-tmp: opaque (/tmp/wt is a worktreePath field in fake builder rows; only group headers render, so no builder row probes the disk) import { describe, it, expect, vi } from 'vitest'; import type { ArchitectState, OverviewBuilder, OverviewData } from '@cluesmith/codev-types'; diff --git a/apps/vscode/src/__tests__/builders-idle-architects-group.test.ts b/apps/vscode/src/__tests__/builders-idle-architects-group.test.ts index 5889001729..15e88cc3bb 100644 --- a/apps/vscode/src/__tests__/builders-idle-architects-group.test.ts +++ b/apps/vscode/src/__tests__/builders-idle-architects-group.test.ts @@ -20,6 +20,7 @@ * * Mocks `vscode` per the established `__tests__` pattern. */ +// spec-1658-tmp: opaque (/tmp/wt is a worktreePath field in fake builder rows; only group and architect headers render, so no builder row probes the disk) import { describe, it, expect, vi } from 'vitest'; import type * as vscode from 'vscode'; diff --git a/apps/vscode/src/__tests__/cli-process.test.ts b/apps/vscode/src/__tests__/cli-process.test.ts new file mode 100644 index 0000000000..e01d2b5765 --- /dev/null +++ b/apps/vscode/src/__tests__/cli-process.test.ts @@ -0,0 +1,47 @@ +/** + * Spec 1658 Phase 6: the extension spawns `afx`/`codev` through the shared shim + * resolver, with no console window. On macOS/Linux the command and arguments + * pass through unchanged. + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const childProcess = vi.hoisted(() => ({ spawn: vi.fn(() => ({ unref: vi.fn() })) })); +vi.mock('node:child_process', () => ({ spawn: childProcess.spawn })); + +const sdk = vi.hoisted(() => ({ resolveSpawnable: vi.fn() })); +vi.mock('@cluesmith/codev-sdk/node/executable', () => ({ resolveSpawnable: sdk.resolveSpawnable })); + +import { spawnCodevCli } from '../cli-process.js'; + +beforeEach(() => { + childProcess.spawn.mockClear(); + sdk.resolveSpawnable.mockReset(); +}); + +describe('spawnCodevCli', () => { + it('spawns the resolver output with its verbatim flag, hidden', () => { + sdk.resolveSpawnable.mockReturnValue({ + command: 'C:\\Windows\\system32\\cmd.exe', + args: ['/d', '/s', '/c', '"C:\\npm\\afx.cmd ^"tower^" ^"start^""'], + windowsVerbatimArguments: true, + }); + spawnCodevCli('afx', ['tower', 'start'], { detached: true, stdio: 'ignore' }); + expect(sdk.resolveSpawnable).toHaveBeenCalledWith('afx', ['tower', 'start']); + expect(childProcess.spawn).toHaveBeenCalledWith( + 'C:\\Windows\\system32\\cmd.exe', + ['/d', '/s', '/c', '"C:\\npm\\afx.cmd ^"tower^" ^"start^""'], + { detached: true, stdio: 'ignore', windowsHide: true, windowsVerbatimArguments: true }, + ); + }); + + it('passes a POSIX command through unchanged', () => { + sdk.resolveSpawnable.mockImplementation((command: string, args: string[]) => ({ command, args, windowsVerbatimArguments: false })); + spawnCodevCli('codev', ['--version'], { cwd: '/repo' }); + expect(childProcess.spawn).toHaveBeenCalledWith('codev', ['--version'], { + cwd: '/repo', + windowsHide: true, + windowsVerbatimArguments: false, + }); + }); +}); diff --git a/apps/vscode/src/__tests__/fleet-current-workspace.test.ts b/apps/vscode/src/__tests__/fleet-current-workspace.test.ts new file mode 100644 index 0000000000..013d547bb9 --- /dev/null +++ b/apps/vscode/src/__tests__/fleet-current-workspace.test.ts @@ -0,0 +1,26 @@ +/** + * Spec 1658, finding M6: on Windows the Fleet view must still recognise this + * window's workspace when Tower reports the directory's canonical spelling + * (`C:\repo`) and VS Code's `uri.fsPath` lower-cases the drive letter (`c:\repo`). + */ + +import { describe, it, expect } from 'vitest'; +import { isSameWorkspacePath } from '../views/fleet-order.js'; + +describe('isSameWorkspacePath', () => { + it('matches across drive-letter and folder case on Windows', () => { + expect(isSameWorkspacePath('C:\\repos\\App', 'c:\\repos\\App', 'win32')).toBe(true); + expect(isSameWorkspacePath('C:\\Repos\\App', 'c:\\repos\\app', 'win32')).toBe(true); + expect(isSameWorkspacePath('C:\\repos\\App', 'c:\\repos\\Other', 'win32')).toBe(false); + }); + + it('keeps the exact comparison on POSIX', () => { + expect(isSameWorkspacePath('/Users/x/Repo', '/Users/x/Repo', 'darwin')).toBe(true); + expect(isSameWorkspacePath('/Users/x/Repo', '/users/x/repo', 'darwin')).toBe(false); + expect(isSameWorkspacePath('/home/x/repo', '/home/x/repo', 'linux')).toBe(true); + }); + + it('is never current without a window folder', () => { + expect(isSameWorkspacePath('C:\\repo', null, 'win32')).toBe(false); + }); +}); diff --git a/apps/vscode/src/cli-process.ts b/apps/vscode/src/cli-process.ts new file mode 100644 index 0000000000..60a20ffa49 --- /dev/null +++ b/apps/vscode/src/cli-process.ts @@ -0,0 +1,17 @@ +import { spawn, type ChildProcess, type SpawnOptions } from 'node:child_process'; +import { resolveSpawnable } from '@cluesmith/codev-sdk/node/executable'; + +/** + * Spawn a Codev CLI (`afx`, `codev`). On Windows each is an npm `.cmd` shim, + * which `spawn` cannot run without a shell, so it goes through the shim + * resolver (the `node ` it wraps, or a quoted `cmd.exe` line), with no + * console window. On macOS and Linux the command is spawned as given. + */ +export function spawnCodevCli(command: string, args: string[], options: SpawnOptions): ChildProcess { + const spawnable = resolveSpawnable(command, args); + return spawn(spawnable.command, spawnable.args, { + ...options, + windowsHide: true, + windowsVerbatimArguments: spawnable.windowsVerbatimArguments, + }); +} diff --git a/apps/vscode/src/commands/dev-shared.ts b/apps/vscode/src/commands/dev-shared.ts index 54d0b38f3e..e4581b474d 100644 --- a/apps/vscode/src/commands/dev-shared.ts +++ b/apps/vscode/src/commands/dev-shared.ts @@ -126,6 +126,8 @@ export async function startDevForTarget( await new Promise((r) => setTimeout(r, SWAP_GRACE_MS)); } + // On a Windows Tower this POSIX shell request runs in the resolved Git Bash + // (Tower maps it in handleTerminalCreate, Spec 1658), so no local resolver is needed. const terminal = await client.createTerminal({ command: '/bin/sh', args: ['-lc', devCommand], diff --git a/apps/vscode/src/commands/spawn.ts b/apps/vscode/src/commands/spawn.ts index 987ca60bff..5f70953d92 100644 --- a/apps/vscode/src/commands/spawn.ts +++ b/apps/vscode/src/commands/spawn.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { spawn } from 'node:child_process'; +import { spawnCodevCli } from '../cli-process.js'; /** * Codev: Spawn Builder. @@ -43,7 +43,7 @@ export async function spawnBuilder(issueArg?: string): Promise { } function runAfxCommand(args: string[]): void { - const child = spawn('afx', args, { detached: true, stdio: 'ignore' }); + const child = spawnCodevCli('afx', args, { detached: true, stdio: 'ignore' }); child.unref(); vscode.window.showInformationMessage(`Codev: Running afx ${args.join(' ')}`); } diff --git a/apps/vscode/src/preflight/preflight-core.ts b/apps/vscode/src/preflight/preflight-core.ts index af0763d57e..4410ec2c9f 100644 --- a/apps/vscode/src/preflight/preflight-core.ts +++ b/apps/vscode/src/preflight/preflight-core.ts @@ -9,8 +9,9 @@ * lives in `preflight.ts` and is reviewed by running the worktree. */ -import { spawn } from 'node:child_process'; +import type { spawn } from 'node:child_process'; import { resolve } from 'node:path'; +import { spawnCodevCli } from '../cli-process.js'; /** * Default hard cap on the `codev --version` probe so a hung binary can't stall @@ -46,7 +47,7 @@ export function runCodevVersion( let child: ReturnType; try { - child = spawn(codevPath, ['--version'], { cwd: cwd ?? undefined }); + child = spawnCodevCli(codevPath, ['--version'], { cwd: cwd ?? undefined }); } catch { finish(false, false); return; diff --git a/apps/vscode/src/tower-starter.ts b/apps/vscode/src/tower-starter.ts index bd870d5536..587ad3833c 100644 --- a/apps/vscode/src/tower-starter.ts +++ b/apps/vscode/src/tower-starter.ts @@ -1,10 +1,10 @@ import * as vscode from 'vscode'; -import { spawn } from 'node:child_process'; import { existsSync } from 'node:fs'; import { resolve } from 'node:path'; import { TowerClient } from '@cluesmith/codev-sdk/tower-client'; import { readLocalKey } from '@cluesmith/codev-sdk/node'; import { getTowerAddress } from './workspace-detector.js'; +import { spawnCodevCli } from './cli-process.js'; /** * How long `restartTower` waits for the old Tower to release `/health` after @@ -45,7 +45,7 @@ export async function autoStartTower( try { // Spawn as detached process — Tower outlives VS Code - const child = spawn(afxPath, ['tower', 'start'], { + const child = spawnCodevCli(afxPath, ['tower', 'start'], { detached: true, stdio: 'ignore', cwd: workspacePath ?? undefined, @@ -96,7 +96,7 @@ export async function restartTower( log('INFO', 'Restarting Tower (stop + start)'); try { await new Promise((res) => { - const child = spawn(afxPath, ['tower', 'stop'], { + const child = spawnCodevCli(afxPath, ['tower', 'stop'], { stdio: 'ignore', cwd: workspacePath ?? undefined, }); diff --git a/apps/vscode/src/views/fleet-order.ts b/apps/vscode/src/views/fleet-order.ts index 14f14b7fe6..beffd2de54 100644 --- a/apps/vscode/src/views/fleet-order.ts +++ b/apps/vscode/src/views/fleet-order.ts @@ -15,6 +15,23 @@ export interface OrderedFleet { dormant: LabelledEntry[]; } +/** + * Whether Tower's path for a workspace is this window's folder. On Windows paths + * are case-insensitive and the two sides spell them differently: VS Code's + * `uri.fsPath` lower-cases the drive letter, while Tower reports the directory's + * canonical spelling with an upper-case drive (Spec 1658, finding M6). POSIX + * keeps the exact comparison. + */ +export function isSameWorkspacePath(towerPath: string, currentPath: string | null, platform: NodeJS.Platform = process.platform): boolean { + if (currentPath === null) { + return false; + } + if (platform === 'win32') { + return towerPath.toLowerCase() === currentPath.toLowerCase(); + } + return towerPath === currentPath; +} + /** * The single ordering the Tower tree and the switch quick-pick both use, so the two surfaces can * never disagree on order. Labels are disambiguated once; active workspaces are sorted by label and @@ -27,7 +44,7 @@ export function orderFleet(fleet: ReadonlyArray, currentPath: string const label = (entry: FleetEntry): LabelledEntry => ({ entry, label: labels.get(entry.workspace.path) ?? entry.workspace.name, - isCurrent: entry.workspace.path === currentPath, + isCurrent: isSameWorkspacePath(entry.workspace.path, currentPath), }); const active = fleet.filter((e) => e.workspace.active).map(label); diff --git a/codev-skeleton/.claude/skills/runnable-worktrees/SKILL.md b/codev-skeleton/.claude/skills/runnable-worktrees/SKILL.md index c9163691d1..a55f595d55 100644 --- a/codev-skeleton/.claude/skills/runnable-worktrees/SKILL.md +++ b/codev-skeleton/.claude/skills/runnable-worktrees/SKILL.md @@ -29,6 +29,11 @@ command. Opt-in via `.codev/config.json`; unconfigured repos see zero behavior c **unless** the entry ends in a slash: `".local-user-data/"` is treated as a literal path and links the directory whole — shared with the parent, not branch-isolated. A dangling link is fine if the source does not exist yet. + **Native Windows:** links are real symlinks when Developer Mode is on. Otherwise a directory + becomes a junction and a file a hard link, or a copy when no hard link can be made (another + volume); a copy does not reflect later edits, and `afx spawn` warns when it makes one. +- **Scripts on Windows** — `postSpawn` and `devCommand` run under Git Bash, so a script file + they call needs LF line endings (`codev doctor` warns about CRLF ones). - **`postSpawn`** — commands run sequentially with `cwd` = worktree path. A non-zero exit aborts the spawn loudly; the half-built worktree stays for inspection. - **`devCommand`** — the foreground command that starts your dev process. Required for diff --git a/codev-skeleton/.codex/skills/runnable-worktrees/SKILL.md b/codev-skeleton/.codex/skills/runnable-worktrees/SKILL.md index c9163691d1..a55f595d55 100644 --- a/codev-skeleton/.codex/skills/runnable-worktrees/SKILL.md +++ b/codev-skeleton/.codex/skills/runnable-worktrees/SKILL.md @@ -29,6 +29,11 @@ command. Opt-in via `.codev/config.json`; unconfigured repos see zero behavior c **unless** the entry ends in a slash: `".local-user-data/"` is treated as a literal path and links the directory whole — shared with the parent, not branch-isolated. A dangling link is fine if the source does not exist yet. + **Native Windows:** links are real symlinks when Developer Mode is on. Otherwise a directory + becomes a junction and a file a hard link, or a copy when no hard link can be made (another + volume); a copy does not reflect later edits, and `afx spawn` warns when it makes one. +- **Scripts on Windows** — `postSpawn` and `devCommand` run under Git Bash, so a script file + they call needs LF line endings (`codev doctor` warns about CRLF ones). - **`postSpawn`** — commands run sequentially with `cwd` = worktree path. A non-zero exit aborts the spawn loudly; the half-built worktree stays for inspection. - **`devCommand`** — the foreground command that starts your dev process. Required for diff --git a/codev-skeleton/DEPENDENCIES.md b/codev-skeleton/DEPENDENCIES.md index 472b4e5669..11411c584a 100644 --- a/codev-skeleton/DEPENDENCIES.md +++ b/codev-skeleton/DEPENDENCIES.md @@ -20,21 +20,34 @@ These are required for Agent Farm to function. | Requirement | Value | |-------------|-------| -| Minimum Version | 18.0.0 | +| Minimum Version | 20.0.0 (native Windows: 22 or 24) | +| Recommended | 22 | | Purpose | Runtime for Agent Farm server | +Codev's native dependencies (node-pty, better-sqlite3) install from prebuilt binaries where one +exists for your platform and Node version. Where none exists, npm builds them from source, which +needs a C/C++ toolchain: + +- **Linux and WSL:** node-pty compiles from source, so install `build-essential` and `python3` + before `npm install -g @cluesmith/codev`. +- **Native Windows:** Node 22 and 24 (x64 and ARM64) are supported. On a Node version with no + Windows prebuild (Node 20, for example) the install stops and names the supported versions, + rather than starting a source build. + **Installation:** ```bash # macOS brew install node -# Ubuntu/Debian -curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - -sudo apt install -y nodejs +# Ubuntu/Debian (and WSL) +curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - +sudo apt install -y nodejs build-essential python3 + +# Windows: the Node.js 22 or 24 installer from https://nodejs.org # Verify -node --version # Should show v18.x or higher +node --version # Should show v20.x or higher (v22.x or v24.x on native Windows) ``` ### git @@ -159,8 +172,9 @@ codex --version | Dependency | Minimum Version | Required? | |------------|-----------------|-----------| -| Node.js | 18.0.0 | Yes | -| git | 2.5.0 | Yes | +| Node.js | 20.0.0 (native Windows: 22 or 24) | Yes | +| git | 2.5.0 (native Windows: Git for Windows) | Yes | +| jq | any | Native Windows only | | gh | latest | Yes | | Claude Code | latest | At least one AI CLI | | Antigravity CLI (`agy`) | latest | At least one AI CLI | @@ -187,21 +201,74 @@ xcode-select --install Most dependencies are available via apt: ```bash -# Core dependencies -sudo apt install nodejs npm git +# Core dependencies (build-essential and python3 build node-pty) +sudo apt install nodejs npm git build-essential python3 # gh requires adding GitHub's apt repository (see above) ``` ### Windows -Codev is designed for Unix-like systems. On Windows, use WSL2: +Codev supports two paths on Windows: native, with Git for Windows, and WSL 2. + +#### Native Windows (Git for Windows) + +Codev runs directly on Windows 10 22H2 or Windows 11, x64 or ARM64, with no admin rights. + +| Prerequisite | Notes | +|--------------|-------| +| Node.js 22 or 24 | Node 20 is not supported on native Windows: better-sqlite3 has no Windows prebuild for it | +| [Git for Windows](https://git-scm.com/download/win) | Provides `git` and Git Bash, the shell Codev uses for its scripts | +| jq | `winget install jqlang.jq`; the forge scripts need it | +| gh | `winget install GitHub.cli`, then `gh auth login` | + +```powershell +# After installing Node.js 22 or 24 from https://nodejs.org +winget install Git.Git +winget install jqlang.jq +winget install GitHub.cli +npm install -g @cluesmith/codev +codev doctor +``` + +How it works and what to know: + +- **Shells.** Architect and builder terminals run their configured agent CLI (`claude` by + default) directly. Plain shell terminals open Windows PowerShell 5.1 by default (set + `shell.shell` to `"pwsh"` in `.codev/config.json` to use PowerShell 7). Codev's own scripts + (forge concepts, the builder launch script, `postSpawn`) run under Git Bash. Codev never runs + bare `bash`, because `C:\Windows\System32\bash.exe` starts WSL. It finds Git Bash from + `windows.gitBashPath` in `.codev/config.json` or `CLAUDE_CODE_GIT_BASH_PATH`, else the standard + Git for Windows install paths, else `PATH`. An override that points at a bash other than Git + Bash is an error. +- **Long paths.** Builder worktrees nest under `.builders//`, and dependency trees inside them + can pass Windows' 260-character path limit. Keep the workspace root short (for example + `C:\src\`) and enable long paths in git for your user (no admin needed): + `git config --global core.longpaths true`. `codev doctor` checks both. +- **Developer Mode is optional.** Runnable-worktree links (`worktree.symlinks`) are real symlinks + when Developer Mode is on. When it is off, a directory link becomes a junction and a file link + becomes a hard link, or a copy when no hard link can be made. A copy does not write through to + the original, and `afx spawn` warns when it makes one. +- **Privacy.** Codev's state (`%USERPROFILE%\.agent-farm`, `%USERPROFILE%\.codev`) relies on + your user folder being private to you, which is the Windows default. Codev sets no ACLs of its + own. Terminal sessions use named pipes that require a per-session secret, so other users on + the machine cannot read or drive them. +- **Line endings.** Scripts Codev ships are pinned to LF. A `postSpawn` or `devCommand` script you + write must use LF line endings to run under Git Bash; `codev doctor` warns about CRLF ones. +- **PowerShell.** Tower uses the built-in `powershell.exe` (CIM) to list and stop the processes + each session starts. A machine policy that blocks it, or Constrained Language Mode, is not + supported; `codev doctor` and Tower's log report it. +- **Known limitations.** Doctor cannot show Tower's environment on Windows (the process table does + not expose it). Building this repository from source on ARM64 fails in one bundler; installing + the published package on ARM64 works. + +#### WSL 2 + +Codev also runs inside WSL 2 as on Linux. Install a distribution, then follow the Ubuntu +instructions above inside it, including `build-essential` and `python3`: ```bash -# Install WSL2 with Ubuntu wsl --install -d Ubuntu - -# Then follow Ubuntu installation instructions inside WSL ``` --- @@ -220,6 +287,14 @@ echo $PATH export PATH="$PATH:$(npm config get prefix)/bin" ``` +On native Windows, npm's global bin directory is `npm config get prefix` itself (for example +`%APPDATA%\npm`), with no `bin` subdirectory; the Node.js installer adds it to `PATH`. + +### Install fails on native Windows with "supported Node versions" + +Your Node version has no prebuilt better-sqlite3 for Windows. Install Node 22 or 24 and run the +install again. + ### Terminal connection issues Ensure no firewall is blocking the ports (default: 4200-4299): diff --git a/codev/DEPENDENCIES.md b/codev/DEPENDENCIES.md index 472b4e5669..11411c584a 100644 --- a/codev/DEPENDENCIES.md +++ b/codev/DEPENDENCIES.md @@ -20,21 +20,34 @@ These are required for Agent Farm to function. | Requirement | Value | |-------------|-------| -| Minimum Version | 18.0.0 | +| Minimum Version | 20.0.0 (native Windows: 22 or 24) | +| Recommended | 22 | | Purpose | Runtime for Agent Farm server | +Codev's native dependencies (node-pty, better-sqlite3) install from prebuilt binaries where one +exists for your platform and Node version. Where none exists, npm builds them from source, which +needs a C/C++ toolchain: + +- **Linux and WSL:** node-pty compiles from source, so install `build-essential` and `python3` + before `npm install -g @cluesmith/codev`. +- **Native Windows:** Node 22 and 24 (x64 and ARM64) are supported. On a Node version with no + Windows prebuild (Node 20, for example) the install stops and names the supported versions, + rather than starting a source build. + **Installation:** ```bash # macOS brew install node -# Ubuntu/Debian -curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - -sudo apt install -y nodejs +# Ubuntu/Debian (and WSL) +curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - +sudo apt install -y nodejs build-essential python3 + +# Windows: the Node.js 22 or 24 installer from https://nodejs.org # Verify -node --version # Should show v18.x or higher +node --version # Should show v20.x or higher (v22.x or v24.x on native Windows) ``` ### git @@ -159,8 +172,9 @@ codex --version | Dependency | Minimum Version | Required? | |------------|-----------------|-----------| -| Node.js | 18.0.0 | Yes | -| git | 2.5.0 | Yes | +| Node.js | 20.0.0 (native Windows: 22 or 24) | Yes | +| git | 2.5.0 (native Windows: Git for Windows) | Yes | +| jq | any | Native Windows only | | gh | latest | Yes | | Claude Code | latest | At least one AI CLI | | Antigravity CLI (`agy`) | latest | At least one AI CLI | @@ -187,21 +201,74 @@ xcode-select --install Most dependencies are available via apt: ```bash -# Core dependencies -sudo apt install nodejs npm git +# Core dependencies (build-essential and python3 build node-pty) +sudo apt install nodejs npm git build-essential python3 # gh requires adding GitHub's apt repository (see above) ``` ### Windows -Codev is designed for Unix-like systems. On Windows, use WSL2: +Codev supports two paths on Windows: native, with Git for Windows, and WSL 2. + +#### Native Windows (Git for Windows) + +Codev runs directly on Windows 10 22H2 or Windows 11, x64 or ARM64, with no admin rights. + +| Prerequisite | Notes | +|--------------|-------| +| Node.js 22 or 24 | Node 20 is not supported on native Windows: better-sqlite3 has no Windows prebuild for it | +| [Git for Windows](https://git-scm.com/download/win) | Provides `git` and Git Bash, the shell Codev uses for its scripts | +| jq | `winget install jqlang.jq`; the forge scripts need it | +| gh | `winget install GitHub.cli`, then `gh auth login` | + +```powershell +# After installing Node.js 22 or 24 from https://nodejs.org +winget install Git.Git +winget install jqlang.jq +winget install GitHub.cli +npm install -g @cluesmith/codev +codev doctor +``` + +How it works and what to know: + +- **Shells.** Architect and builder terminals run their configured agent CLI (`claude` by + default) directly. Plain shell terminals open Windows PowerShell 5.1 by default (set + `shell.shell` to `"pwsh"` in `.codev/config.json` to use PowerShell 7). Codev's own scripts + (forge concepts, the builder launch script, `postSpawn`) run under Git Bash. Codev never runs + bare `bash`, because `C:\Windows\System32\bash.exe` starts WSL. It finds Git Bash from + `windows.gitBashPath` in `.codev/config.json` or `CLAUDE_CODE_GIT_BASH_PATH`, else the standard + Git for Windows install paths, else `PATH`. An override that points at a bash other than Git + Bash is an error. +- **Long paths.** Builder worktrees nest under `.builders//`, and dependency trees inside them + can pass Windows' 260-character path limit. Keep the workspace root short (for example + `C:\src\`) and enable long paths in git for your user (no admin needed): + `git config --global core.longpaths true`. `codev doctor` checks both. +- **Developer Mode is optional.** Runnable-worktree links (`worktree.symlinks`) are real symlinks + when Developer Mode is on. When it is off, a directory link becomes a junction and a file link + becomes a hard link, or a copy when no hard link can be made. A copy does not write through to + the original, and `afx spawn` warns when it makes one. +- **Privacy.** Codev's state (`%USERPROFILE%\.agent-farm`, `%USERPROFILE%\.codev`) relies on + your user folder being private to you, which is the Windows default. Codev sets no ACLs of its + own. Terminal sessions use named pipes that require a per-session secret, so other users on + the machine cannot read or drive them. +- **Line endings.** Scripts Codev ships are pinned to LF. A `postSpawn` or `devCommand` script you + write must use LF line endings to run under Git Bash; `codev doctor` warns about CRLF ones. +- **PowerShell.** Tower uses the built-in `powershell.exe` (CIM) to list and stop the processes + each session starts. A machine policy that blocks it, or Constrained Language Mode, is not + supported; `codev doctor` and Tower's log report it. +- **Known limitations.** Doctor cannot show Tower's environment on Windows (the process table does + not expose it). Building this repository from source on ARM64 fails in one bundler; installing + the published package on ARM64 works. + +#### WSL 2 + +Codev also runs inside WSL 2 as on Linux. Install a distribution, then follow the Ubuntu +instructions above inside it, including `build-essential` and `python3`: ```bash -# Install WSL2 with Ubuntu wsl --install -d Ubuntu - -# Then follow Ubuntu installation instructions inside WSL ``` --- @@ -220,6 +287,14 @@ echo $PATH export PATH="$PATH:$(npm config get prefix)/bin" ``` +On native Windows, npm's global bin directory is `npm config get prefix` itself (for example +`%APPDATA%\npm`), with no `bin` subdirectory; the Node.js installer adds it to `PATH`. + +### Install fails on native Windows with "supported Node versions" + +Your Node version has no prebuilt better-sqlite3 for Windows. Install Node 22 or 24 and run the +install again. + ### Terminal connection issues Ensure no firewall is blocking the ports (default: 4200-4299): diff --git a/codev/plans/1658-windows-native-non-wsl-support.md b/codev/plans/1658-windows-native-non-wsl-support.md new file mode 100644 index 0000000000..7829e3e340 --- /dev/null +++ b/codev/plans/1658-windows-native-non-wsl-support.md @@ -0,0 +1,984 @@ +# Plan: Native (non-WSL) Windows Support + +**Specification**: [codev/specs/1658-windows-native-non-wsl-support.md](../specs/1658-windows-native-non-wsl-support.md) + +## Executive Summary + +The spec selects, per area: + +- **A1**: a platform endpoint abstraction. Shellpers use named pipes on Windows, gated by a + Windows-only per-session HELLO secret. POSIX keeps its Unix socket and today's handshake. +- **B1 + census snapshot**: a single "kill session tree" primitive (POSIX group signals, Windows + `taskkill /T /F` plus explicit per-PID kills from a pre-kill census snapshot). Tower is + relaunched outside a kill-on-close job through built-in PowerShell CIM when needed. +- **C1**: on Windows, reconnect identity is the authenticated WELCOME, not process start time. + Sweeps use one Windows census provider (inline PowerShell CIM: pid, ppid, cmdline, creation + time). Tower stop/start learn Tower's PID from the owner lock and `/api/status`, not `lsof`. +- **D**: one Git Bash resolver, one PATHEXT shim resolver (promoted from `codev bootstrap-docs`), + one MSYS path translator. Every plumbing launch site routes through them. +- **E1**: junction for directories, hard link or copy for files, real symlinks when allowed. +- **F**: PowerShell 5.1 for human shell terminals; agent terminals run their harness directly. + +One deviation from the spec's wording: the spec names the `tree-kill` package for B1. This plan +calls `taskkill` directly from a new primitive, because the pre-kill snapshot and per-PID +creation-time checks need control `tree-kill` does not offer (on Windows it is a thin +`taskkill /T /F` wrapper). The only `tree-kill` user today, `killProcessTree` in +`agent-farm/utils/shell.ts`, has no callers and is deleted along with the dependency (Phase 3). + +Owner ruling 1 fixes the phase order: audit → transport + lifecycle → forge/launcher routing → +POSIX residue sweep → CI leg. This plan keeps that order and splits the large buckets into +phases that each commit and verify on their own: + +| Ruling-1 bucket | Plan phases | +|---|---| +| Audit | 1 | +| Transport + lifecycle | 2, 3, 4 | +| Forge/launcher routing | 5, 6, 7 | +| (ConPTY gate and IDE server, both of which need working launchers) | 8, 9 | +| POSIX residue sweep | 10 | +| CI leg | 11 | + +Every Windows change enters through the platform seam built in Phase 1, so every Windows branch +is unit-testable on macOS/Linux by forcing the seam to `win32`. Real-Windows behavior is +verified by the on-demand Windows workflow (also started in Phase 1) and by the owner's Windows +machine where CI cannot reach (interactive launchers, authenticated harness captures, the VS +Code extension, the joint IDE check). + +### How Windows verification works across phases + +The builder works on macOS. Windows facts come from three places: + +1. **Seam-forced unit tests** on macOS/Linux, run in every phase. +2. **`windows.yml` dispatches** (`workflow_dispatch` only, never automatic, never required). + Phase 1 creates it with a `probe` job and a `targeted` job that runs a vitest filter given as + a dispatch input. Each later phase adds its Windows-only tests (`describe.runIf(win32)`) and + dispatches the `targeted` job against the branch. Phase 11 adds the full suites. +3. **The owner's Windows machine** for what CI cannot do. These are listed per phase and + collected into one manual test pass at the PR gate. + +**Early `windows.yml` landing (owner ruling at plan approval, 2026-10-21T06:15:45Z: yes).** +GitHub only dispatches a workflow whose file exists on the default branch; a branch's version of +the file then runs via `--ref`. So right after Phase 1, the builder opens a small separate PR, +from a branch cut from `origin/main`, that lands the dispatch-only `windows.yml` skeleton on +`main`. It is manual dispatch only, never automatic, never a required check. This is the one +sanctioned exception to "one PR at the end". Windows checks in Phases 1 to 10 then run through +`targeted` dispatches as written. + +### Standing constraints applied to every phase + +- POSIX behavior does not change. POSIX implementations stay today's code. Where a phase + changes a shared contract, it says whether POSIX adopts the change and tests that explicitly. + Each phase runs the macOS/Linux unit suite plus `check-types` (`pnpm build` first) before its + commit. +- Framework content is mirrored in `codev/` and `codev-skeleton/`. Shipped skill edits touch all + four copies (`.claude`/`.codex` × root/skeleton). +- The mailbox invariant (Spec 1313) and server/client isolation (#1189) hold. No Windows code + enters `codev-sdk` outside its `/node` adapter. +- Commits: `[Spec 1658][Phase: ] ...`, files staged explicitly. One PR at the end, plus the + early `windows.yml` PR above. + +## Phases (Machine Readable) + +```json +{ + "phases": [ + {"id": "phase_1", "title": "Windows audit, probe harness, and platform seam"}, + {"id": "phase_2", "title": "Shellper named-pipe transport, per-session secret, and reconnect identity"}, + {"id": "phase_3", "title": "Process census and session-tree teardown"}, + {"id": "phase_3b", "title": "Per-session Job Object (B2) for orphaned descendants"}, + {"id": "phase_4", "title": "Tower lifecycle and detached survival on Windows"}, + {"id": "phase_5", "title": "Git Bash, executable shim, and MSYS path resolution with doctor prerequisites"}, + {"id": "phase_6", "title": "Launcher routing and shell roles"}, + {"id": "phase_7", "title": "Runnable-worktree links and worktree removal on Windows"}, + {"id": "phase_8", "title": "ConPTY render-gate validation captures"}, + {"id": "phase_9", "title": "Codev IDE server on native Windows"}, + {"id": "phase_10", "title": "POSIX residue sweep and native-dependency install guard"}, + {"id": "phase_11", "title": "Windows CI leg and install docs"} + ] +} +``` + +## Phase Breakdown + +### Phase 1: Windows audit, probe harness, and platform seam + +**Dependencies**: None + +#### Objective + +Turn the spec's assumptions into measured facts before any surgery, and give every later phase +a testable platform seam and a way to run code on Windows. + +#### Files to Create / Modify + +- `codev/projects/1658-windows-native-non-wsl-support/audit.md` (new): the measured facts and + the site inventory, each site tagged with the phase that fixes it. +- `scripts/windows-probe.mjs` (new, repo root, not shipped in the package). +- `.github/workflows/windows.yml` (new): `workflow_dispatch` only; jobs `probe` and `targeted` + (input: vitest filter). Node 20/22 × x64. +- `packages/codev/src/lib/platform.ts` (new): `hostPlatform()`, `isWindows()`, and a test-only + override, so Windows branches can be exercised on any host. +- `packages/codev/src/lib/__tests__/platform.test.ts` (new). + +#### Deliverables + +- [ ] **Site inventory**, re-audited against current `main`, covering every site class in the + spec's Current State. It includes code that landed after the spec audit: the IDE server + spawn/stop (`agent-farm/servers/ide-server.ts`, `ide-forward.ts`, from #1668) and, if merged by + then, #1789 (`commands/ide.ts`, `lib/ide-version.ts`, `lib/ide-extensions.ts`), #1595 hooks, and + #1672 lane card. It also lists the sites the plan review surfaced: `commands/tower.ts` + (dev-mode `npx`, the `forceKillAllChildProcesses` `pgrep` branch, `towerLog --follow`'s + `tail -f`), `porch/checks.ts` group-exit wait and `detached` grouping, the cmdline matchers in + `session-manager.ts`, `shellper-husk-sweep.ts` and `architect-session-holder.ts`. Re-run at + the start of Phases 6, 9 and 10 for anything merged since. +- [ ] **Probe script** measuring, on the host it runs on: + - native dependency install behavior: for node-pty and better-sqlite3 at the pinned versions, + Node 20/22/24, win32-x64 and win32-arm64, whether a prebuilt binary is used and what the + install script does when none matches (falls back to `node-gyp` or fails); + - a named-pipe listen/connect round trip carrying a HELLO/WELCOME frame; + - **CIM census latency**: cold and warm wall time of the inline `Win32_Process` query, to size + the Phase 3 cache and the `probeTowerHealth` budget; + - detached-child survival: spawn a detached child, end the launching process, check the child + survives (an empirical kill-on-close job test that needs no native API); + - PATHEXT resolution of `claude`, `codex`, `afx`, `npx`, `codev-ide-server`; + - Git Bash candidate locations and the System32 `bash.exe` trap; + - the `afx tower start` hang: start the daemon with its stderr/log captured, record the first + failure. +- [ ] **Launcher survival matrix**: CI runner (from the `probe` job); Windows Terminal, + PowerShell console, VS Code integrated terminal and extension host, SSH (owner runs + `windows-probe.mjs --survival` on their machine). +- [ ] **Root cause of the 2026-09-09 hang**, recorded in `audit.md` (carried to the review). +- [ ] Platform seam plus tests. + +#### Acceptance Criteria + +- [ ] `audit.md` lists every site class with its owning phase, the prebuild and install-script + matrix (which defines the supported Windows Node versions and the ARM64 verdict), the CIM + latency numbers, the launcher survival matrix, and the hang root cause. +- [ ] `windows.yml` `probe` job runs green from a dispatch once the early PR has landed it on + `main`. +- [ ] Platform seam tests pass; POSIX unit suite and `check-types` green. + +#### Test Plan + +- Unit: platform seam defaults to `process.platform`; override and reset behave. +- Windows: dispatch `probe`; owner runs the survival mode on their launchers. +- If the audit contradicts a spec assumption (for example no prebuild for a supported Node + version, every launcher kills detached children, or CIM latency too high for any cache to + hide), stop and report to the architect before Phase 2. Those facts change scope. + +--- + +### Phase 2: Shellper named-pipe transport, per-session secret, and reconnect identity + +**Dependencies**: Phase 1 + +#### Objective + +Shellpers listen and reconnect on Windows over named pipes, other local users can neither read +from nor control them, and reconnect identity on Windows is the authenticated WELCOME. POSIX +connections stay byte-identical. + +#### Files to Create / Modify + +- `packages/codev/src/terminal/shellper-endpoint.ts` (new): resolve endpoint (POSIX + `/shellper-.sock`; Windows `\\.\pipe\codev-shellper--`), readiness + probe, stale-endpoint cleanup (no-op on Windows), log path decoupled from the endpoint (POSIX + log path unchanged). +- `packages/codev/src/terminal/session-manager.ts`: socket-file wait → endpoint readiness probe; + `isSocket`/`lstat` checks → endpoint probe; session id from the DB row, not the + `shellper-.sock` basename; attach fallback scan uses the DB registry on Windows; + `.sock`→`.log` string replace → `shellper-endpoint` log path; `socketDir` scope marker uses + `path.sep`. **Reconnect identity**: on Windows the start-time check (`getProcessStartTime`, + which returns null on win32 and so fails closed today) is replaced by WELCOME verification; + POSIX keeps the start-time check unchanged. +- `packages/codev/src/terminal/shellper-protocol.ts`: optional HELLO `secret`, and optional + WELCOME `shellperPid` and `sessionId` (today WELCOME's `pid` is the PTY child, not the + shellper). All three are sent only on Windows; POSIX frames are unchanged. +- `packages/codev/src/terminal/shellper-main.ts`, `shellper-process.ts`: read the secret from the + environment, delete it from the env the PTY child inherits, write nothing to a connection + until a valid HELLO, drop on mismatch. The shellper's config (argv JSON) keeps carrying + `socketDir`, so the Tower-instance scope marker still appears in its command line on Windows. +- `packages/codev/src/terminal/shellper-client.ts`: send the secret; verify WELCOME + (`sessionId`, `shellperPid` against the DB row). Reconnect returns a distinct + **identity-refused** result rather than a bare `null`, so callers can tell a confirmed + mismatch from a transient connect failure. +- `packages/codev/src/agent-farm/servers/tower-terminals.ts`: `reconcileTerminalSessions` and + `getTerminalsForWorkspace` clean the row on identity-refused even when the PID is alive (the + #1686 keep-row-if-alive guard stays for transient failures), so scenario 5 works. +- `packages/codev/src/agent-farm/db/migrations.ts`: nullable secret column on the session row in + `global.db` (always null on POSIX). +- `packages/codev/src/agent-farm/commands/attach.ts`: pass endpoint and secret through attach. + +#### Deliverables + +- [ ] Endpoint abstraction; POSIX endpoint, log path and handshake unchanged. +- [ ] Windows secret: generated by Tower per session (crypto random), passed via environment, + never argv; persisted for reconnect; stripped before the PTY child starts. +- [ ] Unauthenticated connection: zero bytes written, frames ignored, connection dropped. +- [ ] Windows reconnect identity via WELCOME, with identity-refused distinguished from transient + failure. +- [ ] Tests. + +#### Acceptance Criteria + +- [ ] Golden test: POSIX endpoint path, log path and HELLO/WELCOME bytes are identical to today; + POSIX reconnect still runs the start-time check. +- [ ] With the seam forced to `win32` (over a POSIX socket in unit tests): a client without or + with a wrong secret receives zero bytes, its frames have no effect, and it is dropped; the + correct secret attaches; a WELCOME with the wrong `sessionId` or `shellperPid` yields + identity-refused, and the reconcile path deletes the row even though the PID is alive. +- [ ] The secret never appears in argv or in the PTY child's environment. +- [ ] Windows (`targeted`): a real `\\.\pipe\` shellper is spawned by one `SessionManager`, that + manager is discarded, and a fresh `SessionManager` reconnects with replay. (Reconnect across + a real `afx tower stop`/`start` is Phase 4.) +- [ ] Windows (`targeted`, scenario 14): a second non-admin local user created on the runner + opens the pipe and receives zero bytes; its frames have no effect. +- [ ] POSIX suite and `check-types` green. + +#### Test Plan + +- Unit: endpoint resolution per platform; log path; secret gating; WELCOME verification; + identity-refused vs transient in reconcile; migration up on an existing DB. +- Windows CI: pipe round trip and cross-manager reconnect; second-user access test. + +--- + +### Phase 3: Process census and session-tree teardown + +**Dependencies**: Phase 2 + +#### Objective + +A Windows census provider with creation time, and a teardown primitive that leaves no +descendants, wired into every process-group kill and wait site and into the orphan/husk sweeps. + +#### Files to Create / Modify + +- `packages/codev/src/agent-farm/servers/process-census.ts`: provider interface; + `ProcessCensusEntry` gains an optional `creationTime`. The POSIX provider is today's `ps` code. + The Windows provider runs an inline `powershell.exe -NoProfile -NonInteractive -Command` CIM + `Win32_Process` query (JSON out: pid, ppid, cmdline, creation time, working set). Because the + census sits on `/health` (`tower-routes.ts`, two awaits) and `/health` latency sizes + `probeTowerHealth`'s budget in `tower-owner.ts` (the owner-lock guard), the Windows provider + has a short TTL cache plus single-flight coalescing, with TTL and the probe budget derived + from the Phase 1 latency numbers. +- **Failure contract**: Windows only. On Windows a census failure is reported (thrown to sweep + callers, logged by Tower, flagged by doctor), never an empty list. POSIX keeps today's + `[]`-on-failure behavior, asserted by a test. +- `packages/codev/src/lib/process-tree.ts` (new): + - `killSessionTree(rootPid, opts)`. POSIX: today's group SIGTERM→SIGKILL. Windows: census + snapshot of descendants (pid, ppid, creation time) → `taskkill /T /F` → explicit kill of + each recorded descendant whose creation time still matches → tolerant of an already-dead + root. + - `waitForTreeExit(rootPid, snapshot, timeoutMs)`: POSIX is today's `process.kill(-pgid, 0)` + loop; Windows polls the snapshot PIDs (with creation time) until gone or timeout. +- Group-kill sites → `killSessionTree`: `terminal/pty-session.ts`, `terminal/session-manager.ts` + (orphan sweep, reaping), `agent-farm/servers/architect-session-holder.ts`, + `agent-farm/commands/cleanup.ts`, `commands/porch/checks.ts`. +- `commands/porch/checks.ts`: `waitForGroupExit` → `waitForTreeExit`, so the 5 s grace still + applies on Windows. The `detached: true` spawn (#1814: keeps test workers from holding stdout + open) has no process-group meaning on Windows; there the check spawns with `windowsHide` and + its tree is tracked from the snapshot instead. +- `agent-farm/servers/shellper-husk-sweep.ts`: on Windows, creation time comes from the census + entry, not a per-PID `getStartTime` call (the seam is reshaped so one census serves the whole + sweep; no N-spawn loop). Same age race guard. +- Cmdline matchers (`session-manager.ts` `findShellperProcesses`, `shellper-husk-sweep.ts`, + `architect-session-holder.ts` `extractJsonStringField`): JSON-unescape before comparing, so + `C:\\Users\\...` in a shellper's argv JSON matches `C:\Users\...`; scope by `socketDir` + (carried in argv per Phase 2). +- `agent-farm/utils/shell.ts`: delete the uncalled `killProcess`/`killProcessTree`; + `packages/codev/package.json`: drop `tree-kill`. + +#### Deliverables + +- [ ] Windows census provider (cached, coalesced) and `creationTime`. +- [ ] `killSessionTree` + `waitForTreeExit`, all group sites migrated. +- [ ] Sweeps on Windows; JSON-aware cmdline matching. +- [ ] Dead teardown code and `tree-kill` removed. +- [ ] Tests. + +#### Acceptance Criteria + +- [ ] POSIX: existing lifecycle tests pass unchanged; `killSessionTree`/`waitForTreeExit` on POSIX + issue exactly today's signals and probes (asserted); census failure on POSIX still yields `[]`. +- [ ] Unit (seam `win32`, injected exec): snapshot taken before kill; re-parented descendant + killed; dead root still clears snapshot descendants; creation-time mismatch skips the PID; + census failure surfaces as an error and a Tower log line; concurrent census calls within the + TTL spawn one query; husk sweep makes one census call per sweep; escaped-backslash cmdline + matches. +- [ ] Windows (`targeted`): scenario 6 (no descendants after kill, including the dead-root + variant), scenario 7 (an unregistered shellper is detected and reaped by the sweep), porch + check timeout kills its tree and honors the grace period. The re-parented variant is met by + Phase 3b: Windows CI run 39574693242 showed that a descendant orphaned by a non-Node launcher + (`cmd start /b`) survives the snapshot and `taskkill /T`, the spec's B2 trigger. Until then + the test stays pinned as a known gap (`it.fails`). (Amended 2026-10-21, architect ruling.) +- [ ] POSIX suite and `check-types` green. + +#### Test Plan + +- Unit: CIM JSON parser (fixtures, including CRLF and empty output); kill-tree decision logic; + cache and single-flight; matcher unescaping. +- Windows CI: real node→bash→node trees. + +--- + +### Phase 3b: Per-session Job Object (B2) for orphaned descendants + +**Dependencies**: Phase 3 + +*Added 2026-10-21 under the spec's B2 fallback clause (architect ruling, Option C). Windows CI +run 39574693242 showed that a descendant orphaned by a non-Node launcher (`cmd start /b`) +survives the Phase 3 snapshot and `taskkill /T`, and the shellper-only sweeps cannot catch it. +Plan diff approved 2026-10-21T09:35Z with six conditions, folded in below.* + +*Porch slot: porch's plan-phase list was fixed at init, so 3b runs inside porch's `phase_4` slot +as its own iteration first. Commits are tagged `[Phase: phase_3b]`, and 3b gets its own 3-way +consultation before any Phase 4 work starts. The audit records the commit ranges for 3b and 4 +within that slot.* + +#### Objective + +Every process descended from a shellper is in a per-session Job Object, so teardown can end the +whole session with `TerminateJobObject`, including orphans that the census cannot link. The +Phase 3 snapshot kill stays as is; it is still the POSIX-parity path and the fallback when the +job is unavailable. + +#### Design + +- **No binary, no compile to disk.** The Win32 calls (`CreateJobObjectW`, + `SetInformationJobObject`, `AssignProcessToJobObject`, `OpenJobObjectW`, `TerminateJobObject`, + `OpenProcess`, `CloseHandle`) are bound inside PowerShell with `System.Reflection.Emit` + (`DefineDynamicAssembly` + `DefinePInvokeMethod`), so the assembly exists only in memory. + Windows PowerShell 5.1's `Add-Type` is not used: it compiles a temporary DLL into `%TEMP%` and + loads it from disk, which is the AppLocker-blockable binary the spec rejected. +- **Off the hot path: one long-lived helper per Tower.** At startup Tower starts a single + `powershell.exe -NoProfile -NonInteractive -Command -` helper in the background. The helper + binds the P/Invoke methods once, then serves line-delimited JSON requests over stdin/stdout: + `assign {name, pid}` and `terminate {name}`. A session create therefore pays one round trip + to an already-warm helper, not a PowerShell start. The helper never wraps Tower: it creates + jobs for shellper pids only and never assigns its own process or Tower's. If a session is + created before the helper is ready (the first seconds after Tower boot), that create waits for + it once. +- **Ordering: the job is in place before the PTY exists.** On Windows the shellper defers its + first PTY spawn until the first authenticated HELLO from a `tower` client. It still listens and + prints its info as soon as it boots. Tower reads the shellper pid, has the helper create the job + `Local\codev-session-` and assign the pid to it, and only then connects. + The PTY and everything it starts inherit the job. POSIX is unchanged: the shellper spawns + immediately, as today. + - **Only the `tower` role starts the PTY (condition 2).** A HELLO from any other role (for + example `attach`) that arrives first neither spawns the PTY nor errors. That client sees an + empty replay until Tower connects. + - **PTY-less deadline (condition 1).** If no `tower` HELLO arrives within 30 s (Tower crashed + between spawn and connect, or the helper hung past its own timeout), the shellper exits on + its own. It never lingers as a PTY-less process that the scenario 7 husk sweep might not + recognise. 30 s covers a cold helper start (~4 s measured for a cold PowerShell) plus the + assign timeout, with margin. +- **Helper timeouts (condition 3).** Each `assign` waits at most 5 s. On timeout, that create + proceeds without a job and the degradation is logged once, on the same path as a failed bind. + Restart-on-exit is bounded: at most 3 restarts in 5 minutes, after which the helper stays + degraded for the life of that Tower and does not spin. +- **Name collision (condition 5).** The job is created with `CreateJobObjectW`, and + `ERROR_ALREADY_EXISTS` is checked. If the name already exists, the shellper is NOT assigned to + a job we did not create. That session is created without a job, and the case is logged. +- **Nested jobs (condition 4).** The shellper may already be in a job, as on the GitHub runner, + so assignment relies on nested-job support (Windows 8+). A failed assignment is a degradation + for that session, never a thrown error. The Win8+ requirement goes under Windows limitations. +- **Job limits.** `JOB_OBJECT_LIMIT_BREAKAWAY_OK`, so a launcher that requests breakaway + explicitly leaves the session. Node's `detached: true` does not request it (measured; see the + acceptance criteria), so it gets no `setsid` parity (ruling (c): BREAKAWAY_OK is kept). No + `KILL_ON_JOB_CLOSE`: the helper exiting or Tower restarting must never kill sessions, because + shellpers survive Tower by design. The job lives as long as any process is in it, but its + NAME lives only while a handle is open, so the helper duplicates a non-inheritable job + handle into the shellper (measured fix, audit 1.8). +- **Teardown (condition 6).** The job is terminated first, then the Phase 3 snapshot pass runs. + The POSIX code path stays byte-identical. `killSessionTree` on Windows asks for `terminate {name}` once a job name is + known (persisted shellper session id, v19). If Tower's helper is not running (an `afx cleanup` + with Tower down), teardown runs a one-shot helper; teardown is not a hot path, so the cold cost + is acceptable there. The Phase 3 snapshot kill still runs either way. +- **Reported degradation.** If the helper cannot bind (Constrained Language Mode, or policy + blocking `powershell.exe`), Tower logs once that per-session jobs are disabled. Doctor reports + it, the same way as a census failure. Sessions still start, without a job, and teardown falls + back to the Phase 3 path. The degradation is never silent. It is documented under Windows + limitations (Phase 11 docs). + +#### Cost gate (first step, before the rest of the phase) + +Measure on the CI host, cold and warm: helper start plus binding, and the per-session +`assign` round trip. Also record that assignment succeeds while the shellper is already in the +runner's job (nested). Record the numbers in the audit. Target: warm per-session cost under ~1 s +(expected to be milliseconds once the helper is warm). If the measured warm cost misses the +target, stop and report to the architect before building further. + +#### Files to Create / Modify + +- `packages/codev/src/lib/windows-job.ts` (new): the helper process (lazy start, single-flight + readiness, request/response, restart on exit, degradation state) and the inline PowerShell + program. +- `terminal/session-manager.ts`: on Windows, assign the job between reading the shellper info and + connecting. +- `terminal/shellper-main.ts` / `shellper-process.ts`: on Windows, defer the first PTY spawn + until the first authenticated HELLO. +- `lib/process-tree.ts`: `killSessionTree` takes an optional job name and terminates the job + before the snapshot pass. +- `agent-farm/servers/tower-server.ts`: start the helper at boot and log the degradation. +- `commands/doctor.ts`: report job availability on Windows. + +#### Deliverables + +- [ ] Cost measured and recorded (the gate above). +- [ ] Helper with in-memory P/Invoke; job assignment before the first PTY spawn. +- [ ] Teardown terminates the job; the Phase 3 snapshot path is kept. +- [ ] Reported degradation (Tower log + doctor) when the helper cannot bind. +- [ ] Tests. + +#### Acceptance Criteria + +- [ ] Windows (`targeted`): the pinned scenario 6 re-parented test passes. Its `it.fails` turns + red, and `.fails` is removed. Before un-pinning, confirm that the test's own setup (the + `cmd start /b` orphan and the wait on the intermediate) still works: `it.fails` also "passes" + when setup breaks. A `cmd start /b` orphan in a real shellper session does not + survive `killSession`. A Node `detached: true` child ends with its session on Windows + (measured, runs 39581676362 and 39582608788: libuv never requests breakaway; no setsid + parity; architect ruling 2026-10-21T10:20Z). +- [ ] Windows (`targeted`): a Tower restart (helper exit) does not kill a running session. +- [ ] Unit (seam `win32`, injected helper): job assigned before the connect/HELLO; helper failure + or `assign` timeout → degradation logged once, session still created, teardown falls back to + the snapshot path; bounded helper restarts; an existing job name is not joined; POSIX never + starts a helper. +- [ ] Unit (shellper): no `tower` HELLO within the deadline → the shellper exits; an `attach` + HELLO first neither spawns the PTY nor errors, and sees an empty replay. +- [ ] Windows (`targeted`): assignment succeeds while the shellper is inside the runner's job + (nested), or the degradation is reported. +- [ ] Measured warm per-session cost recorded in the audit, and under the target. +- [ ] POSIX suite and `check-types` green. + +#### Test Plan + +- Unit: helper protocol (fake helper process), ordering, degradation, teardown with and without + a job name. +- Windows CI: real job assignment, `cmd start /b` orphan, a Node detached child ending with its + session, a CIM launch escaping it, helper exit. + +--- + +### Phase 4: Tower lifecycle and detached survival on Windows + +**Dependencies**: Phases 2, 3 + +#### Objective + +`afx tower start` and `stop` work on Windows without `lsof`/`pgrep`, with a graceful shutdown, +hidden console windows, and shellpers that survive a Tower restart on every documented launch +path. + +#### Files to Create / Modify + +- `agent-farm/commands/tower.ts`: + - stop via a local-only HTTP shutdown request, then `killSessionTree` as fallback; + - Tower PID from the owner lock and `/api/status`; + - the `forceKillAllChildProcesses` branch (`pgrep -P`, `pgrep -f shellper-main`) uses the + census and `killSessionTree` on Windows; + - job escape: if Phase 1 found the launcher kills detached children, start the daemon through + CIM `Win32_Process.Create`; if that is blocked, refuse with an error naming a supported + launch path. +- `agent-farm/servers/tower-routes.ts`: loopback-only shutdown route. +- `agent-farm/utils/port.ts`, `agent-farm/db/tower-owner.ts`: no `lsof` on Windows; port owner via + `Get-NetTCPConnection` only where still needed; `probeTowerHealth` budget per Phase 3. +- Every detached spawn (Tower daemon, shellpers, helpers): `windowsHide: true`. +- The hang root cause from Phase 1, if it lives in this area. + +#### Deliverables + +- [ ] Tower start/stop on Windows; graceful shutdown; job escape; hidden consoles. +- [ ] Tests. + +#### Acceptance Criteria + +- [ ] POSIX: `afx tower start`/`stop` behavior and tests unchanged. +- [ ] Unit: shutdown route rejects non-loopback; job-escape decision (survival measured / + relaunch blocked → refusal text) with injected exec. +- [ ] Windows (`targeted`): scenario 3's stop/start half (ready reached, owner lock released), + scenario 4 (spawned builder stub survives `afx tower stop`/`start`, same PID, replay, input + works), scenario 5 end to end (PID reuse refused, row cleaned). +- [ ] Owner machine: survival from Windows Terminal, PowerShell console, VS Code terminal and + extension host (feeds the PR-gate pass). +- [ ] POSIX suite and `check-types` green. + +#### Test Plan + +Unit as above; Windows CI restart-survival test; owner launcher checks. + +--- + +### Phase 5: Git Bash, executable shim, and MSYS path resolution with doctor prerequisites + +**Dependencies**: Phase 1 + +#### Objective + +One resolver each for Git Bash, executable shims and MSYS paths, and a `codev doctor` that +reports the Windows prerequisites. This phase routes nothing yet; it builds and exposes the +pieces, so it is observable through doctor alone. + +#### Files to Create / Modify + +- `packages/codev/src/lib/git-bash.ts` (new): resolution per ruling 5 as amended + (2026-10-21T06:15:45Z): the explicit override (config setting, or `CLAUDE_CODE_GIT_BASH_PATH` + if the user set it) wins when it points at a real Git Bash; otherwise standard install + locations (`%ProgramFiles%\Git\bin\bash.exe`, `%LocalAppData%\Programs\Git\bin\bash.exe`), + then `PATH`. `%SystemRoot%\System32\bash.exe` is always rejected; an override pointing at it + or at a non-Git bash fails loudly (no silent fallback). Returns path and Git version. +- `packages/sdk/src/node/executable.ts` (new): `which`-style PATH/PATHEXT lookup promoted from + `commands/bootstrap-docs.ts`, plus `resolveSpawnable(cmd)` that turns a `.cmd`/`.bat` shim into + `%COMSPEC% /d /s /c` form or its underlying `node `. It lives in `codev-sdk/node`, the + sanctioned home for node-requiring shared client code, because both `packages/codev` (already a + dependent) and `apps/vscode` (which cannot import `@cluesmith/codev`, and already imports + `readLocalKey` from `codev-sdk/node`) need it. `bootstrap-docs.ts` imports it. +- `packages/codev/src/lib/msys-path.ts` (new): native→POSIX (`C:\x` → `/c/x`), POSIX→native + (`/c/x` → `C:\x`), and the per-call opt-out env (`MSYS_NO_PATHCONV=1`, + `MSYS2_ARG_CONV_EXCL=*`). +- `packages/codev/src/commands/doctor.ts`: Windows section: Git for Windows (found, version, + bash path; never System32), jq (found or install hint), CRLF warning for user-authored + `postSpawn`/`devCommand` script files. +- `.gitattributes` (root): `eol=lf` for shipped `.sh` (forge concepts in `codev-skeleton/` and + `codev/`, templates). + +#### Deliverables + +- [ ] Three resolvers with unit tests; `bootstrap-docs` uses the shared lookup. +- [ ] Doctor Windows checks; missing Git Bash or jq is a named, actionable failure. +- [ ] LF pinning for shipped scripts. + +#### Acceptance Criteria + +- [ ] Resolver precedence tests (fake fs/env): a valid override wins over an installed Git; + install path before `PATH`; System32 never returned even when first on `PATH`; an override at + System32 or at a non-Git bash fails loudly; nothing found → error naming Git for Windows. +- [ ] Shim tests: `claude` → `claude.cmd` via PATHEXT; spawnable form is executable by + `CreateProcess`. The sdk boundary test still passes (no `vscode` import, zero runtime deps). +- [ ] MSYS round-trip table, including UNC and spaces. +- [ ] Doctor (seam `win32`): scenario 2 outputs (jq missing; only System32 bash) name the missing + prerequisite. On POSIX the doctor output is unchanged (snapshot). +- [ ] Windows (`targeted`): doctor reports the runner's Git Bash and jq. +- [ ] POSIX suite and `check-types` green. + +#### Test Plan + +Unit tests as above; one Windows dispatch running doctor and the resolver tests for real. + +--- + +### Phase 6: Launcher routing and shell roles + +**Dependencies**: Phases 3, 4, 5 + +#### Objective + +Every plumbing launch site runs through the resolved Git Bash with correct path translation, +agent terminals spawn their harness through the shim resolver, and human shell terminals open +PowerShell. After this phase, spawn, forge, porch and `afx dev` work on Windows. + +#### Files to Create / Modify + +- `packages/codev/src/lib/forge.ts`: concepts execute via ` -c` / `