From 6f1073870d083a5c7c27754eb0e56d087e1c9813 Mon Sep 17 00:00:00 2001 From: Onur Date: Tue, 27 Oct 2026 23:23:48 +0300 Subject: [PATCH 1/8] docs: define native Windows support design --- .../specs/2026-10-27-native-windows-design.md | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 docs/superpowers/specs/2026-10-27-native-windows-design.md diff --git a/docs/superpowers/specs/2026-10-27-native-windows-design.md b/docs/superpowers/specs/2026-10-27-native-windows-design.md new file mode 100644 index 0000000..e0d8d19 --- /dev/null +++ b/docs/superpowers/specs/2026-10-27-native-windows-design.md @@ -0,0 +1,51 @@ +# Native Windows support (issue #42) + +## Intent and decisions + +A Windows user with OpenCode 1.x must be able to install and use OpenCode Team from PowerShell. The user is blocked at the documented `./install.sh` step, not by a demonstrated OpenCode runtime failure. Their installed `opencode --version` was independently confirmed as `1.19.4`. + +The user accepts enabling Windows Developer Mode or running an elevated PowerShell to create symbolic links. Use real symbolic links, matching the existing Bash installer, so repository edits take effect without reinstalling. Do not add a copy, junction, or hard-link fallback. + +The following are proposed implementation choices, not additional user requirements: support Windows PowerShell 5.1 and PowerShell 7; add a PowerShell memory compactor because the orchestrator currently invokes a Bash script at every gate; use npm only for the explicitly requested optional OpenCode installation. These choices are subject to approval of this design. + +## Installer behavior + +Add `install.ps1` at the repository root with mutually exclusive `-Check` and `-Uninstall` modes, default installation, and an optional `-InstallOpenCode` switch for installation mode. Document commands that work from a freshly cloned repository, including a process-scoped execution-policy workaround when needed; never change machine execution policy or enable Developer Mode automatically. + +Use `OPENCODE_CONFIG_DIR` when set, otherwise `%USERPROFILE%\.config\opencode`. Resolve paths independently of the caller's working directory and handle spaces, Unicode and Windows path separators. Link `agents`, `commands`, `team.yaml`, `team/AGENTS.md`, and each repository-local `skills/team-*` directory to their corresponding repository targets. Keep the containing `skills` and `team` directories as real directories. + +Require a successful, parseable OpenCode 1.x version for install and check. An existing valid 1.x installation is reused. If installation is requested and OpenCode is missing or incompatible, run the officially documented npm distribution with a major-version constraint (`npm install --global opencode-ai@1`), check its exit status, and verify the resulting executable/version. Without that switch, never install or replace OpenCode. Explain missing npm, unsupported versions, and missing symlink privileges with actionable errors. Uninstall does not require OpenCode. + +Never modify `opencode.json` (including JSONC). Print the recommended `default_agent` and `instructions` snippet when needed, using paths for the actual config directory. The user merges that snippet into their own configuration; this work does not change provider or model settings. + +## Existing files and ownership + +Repeated installation succeeds when expected links already exist. A link pointing elsewhere inside this repository may be corrected. An ordinary existing core file/directory or a core link outside the repository causes a clear failure and is preserved. Existing local or externally managed `skills/team-*` entries are skipped and preserved, matching the Bash installer. + +Check validates that core links resolve to the exact expected targets, required targets exist, and required local skills are available; local/external skill collisions remain informational. A missing recommended JSON setting is informational, not a failed check. + +Uninstall removes only links managed by this repository, never their targets, external links, ordinary files, or user configuration. Determine ownership with normalized absolute paths and directory boundaries, including relative link targets and case-insensitive Windows comparisons. Reject unexpected linked parent containers before writing through them. Preflight predictable collisions and link-creation capability before changing installation entries, and clean up any preflight probe. No recursive deletion of installed directory links. + +## Native memory operation + +Add `skills/team-memory/memory-compact.ps1` with the existing script's contract: optional role; `MEMORY_DIR` override or current project's `.team/memory`; logs above 40 entries archive all but the newest 10 into monthly role archives; append without repeating archive headers. Remove exact duplicate index lines only when the index exceeds 60 lines. If it still exceeds 60 unique lines, fail without changing the index. Preserve Unicode and original entry order. Validate role/path-derived names before writes and keep writes within the selected memory directory. + +Update the orchestrator and memory skill to select the PowerShell helper on native Windows and the existing Bash helper on macOS/Linux. Respect the configured global config directory and the existing project memory-directory setting when describing invocation. Preserve the pipeline, roles, memory format, and human approval gates. + +## Tests and documentation + +Add dependency-free PowerShell smoke tests using disposable config directories and fake OpenCode/npm commands, with no network or real user configuration writes. Cover installation, repeat installation, check, uninstall, missing/bad versions, optional installation, unchanged JSON, custom config paths, spaces, Unicode, local skill collisions, in-repository relinking, external links and parent-container protection. Include memory tests for empty logs, thresholds, repeated/monthly archives, Unicode, current-project paths, custom memory directories, exact duplicate removal, and an unchanged oversized unique index. + +Add a `windows-latest` smoke job to `.github/workflows/checks.yml`; exercise the PowerShell installer and memory helper under the supported PowerShell versions. Keep Ubuntu/macOS jobs. Update both READMEs, install guide, FAQ, contribution instructions, and affected memory/runtime instructions. Regenerate reference pages if source manifests change. Describe symlink privileges, config paths, execution policy, and manual JSON configuration accurately. + +## Verification and completion + +Run the Windows smoke and memory tests locally under the available PowerShell versions. Use the actual OpenCode 1.19.4 binary with an isolated configuration to verify agent/config discovery without paid model requests. Run `ruby scripts/validate.rb`, Bash syntax checks and existing shell/memory smoke tests where their required runtimes are available, and build the documentation. Report any unavailable runtime/check explicitly instead of claiming it passed. A configured Windows CI job is not evidence of a completed remote CI run. + +After isolated tests, install into the user's actual configuration within the authorized scope and run the installer check, preserving existing entries and JSON. If local collisions or permissions prevent installation, retain all existing data and report the precise remaining step. Provide the concrete JSON snippet if the orchestrator default/instructions need manual configuration. + +## Scope and builder discretion + +No OpenCode 2 migration, WSL requirement, new test framework, provider authentication changes, paid agent executions, automatic elevation, machine setting changes, publishing, issue comments, or PR merging. Existing Bash behavior stays compatible. This is a Windows support change, not a rewrite of the pipeline or documentation toolchain. + +The builder chooses function organization, test fixture structure, safe .NET/PowerShell filesystem APIs, and the exact failure messages. Do not broaden scope solely to install contributor tools; use available or portable runtimes and document genuine verification limits. From f4f11da86edb5e7d44a7223870612ad41b96cafc Mon Sep 17 00:00:00 2001 From: Onur Date: Tue, 27 Oct 2026 23:35:14 +0300 Subject: [PATCH 2/8] feat: add native Windows installer --- .../plans/2026-10-27-native-windows.md | 80 ++++++++ install.ps1 | 175 ++++++++++++++++ scripts/smoke-install.ps1 | 191 ++++++++++++++++++ 3 files changed, 446 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-27-native-windows.md create mode 100644 install.ps1 create mode 100644 scripts/smoke-install.ps1 diff --git a/docs/superpowers/plans/2026-10-27-native-windows.md b/docs/superpowers/plans/2026-10-27-native-windows.md new file mode 100644 index 0000000..7fb8e77 --- /dev/null +++ b/docs/superpowers/plans/2026-10-27-native-windows.md @@ -0,0 +1,80 @@ +# Native Windows Support Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Install and run OpenCode Team natively on Windows with OpenCode 1.x, preserving user configuration and existing Unix behavior. + +**Architecture:** A standalone PowerShell installer manages symbolic links and version checks. A separate PowerShell memory helper mirrors the Bash helper. Dependency-free subprocess smoke tests exercise each public script; Windows CI runs them under Windows PowerShell 5.1 and PowerShell 7. + +**Tech Stack:** PowerShell 5.1/7, .NET filesystem APIs, GitHub Actions, existing Bash/Ruby/VitePress checks. + +**Spec:** `docs/superpowers/specs/2026-10-27-native-windows-design.md` (approved by the user). + +## Global Constraints + +- Support Windows PowerShell 5.1 and PowerShell 7. +- Require a successful, parseable OpenCode 1.x version for install and check. +- Never modify `opencode.json` (including JSONC). +- No new test framework, automatic elevation, machine setting changes, or paid agent executions. +- Use `OPENCODE_CONFIG_DIR` when set, otherwise `%USERPROFILE%\.config\opencode`. +- Use real symbolic links; do not add a copy, junction, or hard-link fallback. +- Preserve Bash behavior, user entries, memory entry order, and Unicode. + +## Review Focus + +1. Linked parent containers must not redirect writes/deletes into unrelated user directories (installer negative fixtures). +2. Relative link targets, case differences, and a repository-prefix sibling must not confuse link ownership (installer ownership fixtures). +3. Native-command failures must propagate even on Windows PowerShell 5.1 (version/npm failure fixtures). +4. Oversized unique indexes and malformed archive dates must preserve existing memory (memory failure fixtures). +5. Paths with spaces/Unicode and an alternate caller working directory must work without changing the target project (both script fixtures). + +## File Responsibilities + +- `install.ps1`: public install/check/uninstall/version/bootstrap interface and safe link management. +- `scripts/smoke-install.ps1`: disposable fixtures, fake commands, subprocess execution, installer assertions. +- `skills/team-memory/memory-compact.ps1`: native memory maintenance. +- `scripts/test-memory-compact.ps1`: native memory behavior and data-preservation assertions. +- `.github/workflows/checks.yml`: Windows PowerShell matrix alongside existing Unix checks. +- READMEs, install/FAQ/contribution/memory guides, `team/AGENTS.md`, `agents/orchestrator.md`, `skills/team-memory/SKILL.md`: supported commands, constraints, and runtime helper selection. +- `docs/reference/*`: regenerated from source after instruction changes. + +### Task 1: PowerShell installer and lifecycle tests + +**Interfaces:** `install.ps1 [-Check | -Uninstall] [-InstallOpenCode]` exits zero on success; nonzero on errors. Environment: `OPENCODE_CONFIG_DIR`, normal command lookup for `opencode` and `npm`. `-InstallOpenCode` is valid only for install. Test runner uses the current PowerShell executable for child processes. + +- [ ] Write `scripts/smoke-install.ps1` with fake `opencode.cmd` returning `1.19.4`, disposable configs, and assertions that install creates the exact links, reinstall succeeds, check succeeds, uninstall preserves repository targets, and check then fails. Run before `install.ps1` exists and confirm the expected missing-script failure. +- [ ] Implement `install.ps1` using `[CmdletBinding(DefaultParameterSetName='Install')]`, switch parameters, strict error handling, normalized paths, and explicit native exit-code handling. Keep filesystem helpers private to the script; use .NET `Directory.Delete(path)` / `File.Delete(path)` for link-only deletion. +- [ ] Extend the smoke tests for unchanged JSON/JSONC, missing settings as a hint, spaces/Unicode, invocation from another working directory, and a custom config directory. +- [ ] Add collision fixtures: local skills survive and check succeeds; core local entries fail unchanged; internal wrong links are repaired; relative external/dangling links and prefix-sibling links survive; linked config child containers cannot redirect writes or uninstall. +- [ ] Add version/bootstrap fixtures: missing/unparseable/2.x/failing executable fail install/check; uninstall works without OpenCode; existing 1.x skips npm; explicit bootstrap calls `npm install --global opencode-ai@1`; missing/failing npm and failed post-install verification return nonzero. Invalid mode combinations fail. +- [ ] Implement link preflight, cleanup probes, readable actionable errors, accurate config snippet paths, and the explicit npm bootstrap needed by those fixtures. Run all installer tests under both shells; verify fixture cleanup never traverses link targets. +- [ ] Commit installer and tests: `feat: add native Windows installer`. + +### Task 2: Native memory helper and regression tests + +**Interfaces:** `memory-compact.ps1 [[-Role] ]` exits zero for successful/no-op compaction, nonzero for unsafe input or unique indexes above 60 lines. Environment: `MEMORY_DIR` or `$PWD/.team/memory`. With no role, process the same 14 roles as the Bash helper, then index; with a role, process only it, then index. + +- [ ] Write `scripts/test-memory-compact.ps1` using the current shell in child processes. Assert an empty log succeeds, 40 entries remain unchanged, and 45 entries yield exactly 35 archived + 10 retained. Run before the helper exists and confirm the expected failure. +- [ ] Implement `skills/team-memory/memory-compact.ps1`: UTF-8 reads/writes, stable exact comparisons, monthly append archives, unique headers, validation before writes, and safe temporary replacements. Index threshold is 60 lines; exact duplicate removal preserves first occurrence; unique overflow fails with unchanged index. +- [ ] Add tests for repeated compaction (70 archive entries and one header), multiple months, Unicode/case-sensitive facts, 60/61-line indexes, role filtering, default CWD, custom memory path, malformed dates, unsafe role paths, and linked paths that escape the memory directory. Add no-trailing-newline and CRLF inputs. +- [ ] Run native memory tests under both shells and existing Bash memory regression tests. Confirm retained/archive entries match the intended ordering and failure fixtures preserve data. +- [ ] Commit helper and tests: `feat: support memory compaction in PowerShell`. + +### Task 3: Runtime instructions, docs, and Windows CI + +**Interfaces:** Windows invocation uses `powershell.exe -NoProfile -ExecutionPolicy Bypass -File ` from the target project root, passing `MEMORY_DIR` when configured. Unix invocation remains the existing `.sh`. Config-root precedence is `OPENCODE_CONFIG_DIR`, then the documented default. + +- [ ] Update orchestrator and memory skill instructions with concrete native Windows helper invocation and custom config/memory behavior. Update `team/AGENTS.md` installation/config precedence without changing pipeline gates or role behavior. +- [ ] Add a `windows-latest` CI job with a shell matrix (`powershell`, `pwsh`) running `scripts/smoke-install.ps1` and `scripts/test-memory-compact.ps1`. Keep existing jobs and pinned checkout action. +- [ ] Update `README.md`, `README.tr.md`, `docs/install.md`, `docs/faq.md`, `CONTRIBUTING.md`, and `docs/memory.md` with native commands, supported shells, Developer Mode/elevation, execution-policy workaround, custom config, optional npm bootstrap, and user-merged JSON snippet. Do not suggest changing machine policy. +- [ ] Regenerate `docs/reference/*` using the existing Ruby generator. Validate manifests, Bash syntax, existing install/memory tests, reference freshness, and VitePress docs build. If Ruby is absent, locate an available/portable runtime without changing machine installation; report any genuine limits. +- [ ] Commit runtime instructions, docs and CI: `docs: enable and document native Windows support`. + +### Task 4: Actual binary verification and final review + +**Interfaces:** Use installed OpenCode `1.19.4`; discovery only (`opencode agent list`, `opencode debug ...`), with no provider calls. Do not print provider credentials or whole user configuration. + +- [ ] Install into a temporary isolated config with the actual binary and verify the 14 team agents, 19 commands, and 13 team skills are discovered using bounded output/parsed JSON. Verify default-agent/instruction resolution with a disposable `opencode.json`. +- [ ] Inspect only paths/collisions in the actual user config, run the installer there when permitted, and run `-Check`. Preserve all existing entries; never edit user JSON. Report any necessary snippet or collision without overwriting it. +- [ ] Use `requesting-code-review` for a fresh whole-branch review. Correct actionable issues and rerun the tests affected by fixes. +- [ ] Use `verification-before-completion`, inspect the final diff/status, and record exactly which checks passed or could not run. Preserve implementation in the task branch and show the runnable Windows command, any remaining manual config step, and verification limits. Do not publish or merge without a request. diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..21c12cf --- /dev/null +++ b/install.ps1 @@ -0,0 +1,175 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS +Links OpenCode Team into the native Windows OpenCode configuration. +.EXAMPLE + .\install.ps1 + .\install.ps1 -Check + .\install.ps1 -Uninstall + .\install.ps1 -InstallOpenCode +#> +[CmdletBinding(DefaultParameterSetName = 'Install')] +param( + [Parameter(ParameterSetName = 'Check', Mandatory = $true)][switch]$Check, + [Parameter(ParameterSetName = 'Uninstall', Mandatory = $true)][switch]$Uninstall, + [Parameter(ParameterSetName = 'Install')][switch]$InstallOpenCode +) +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +$repo = [IO.Path]::GetFullPath($PSScriptRoot).TrimEnd('\', '/') +$config = if ($env:OPENCODE_CONFIG_DIR) { $env:OPENCODE_CONFIG_DIR } else { Join-Path $env:USERPROFILE '.config/opencode' } +$config = [IO.Path]::GetFullPath($config).TrimEnd('\', '/') +$core = @('agents', 'commands', 'team.yaml', 'team/AGENTS.md') +$skills = @(Get-ChildItem -LiteralPath (Join-Path $repo 'skills') -Directory | Where-Object { $_.Name -like 'team-*' }) + +function Get-Entry([string]$Path) { + Get-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue +} +function Test-Link($Entry) { + return $null -ne $Entry -and [bool]($Entry.Attributes -band [IO.FileAttributes]::ReparsePoint) +} +function Get-LinkTarget($Entry) { + if (-not (Test-Link $Entry) -or -not $Entry.Target) { return $null } + $target = [string]@($Entry.Target)[0] + if ($target.StartsWith('\\?\')) { $target = $target.Substring(4) } + if (-not [IO.Path]::IsPathRooted($target)) { $target = Join-Path (Split-Path $Entry.FullName -Parent) $target } + return [IO.Path]::GetFullPath($target).TrimEnd('\', '/') +} +function Test-Owned($Entry) { + $target = Get-LinkTarget $Entry + return $null -ne $target -and $target.StartsWith($repo + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) +} +function Assert-RealParents([string]$Path) { + $current = [IO.Path]::GetFullPath($Path) + while ($current) { + $item = Get-Entry $current + if ($null -ne $item -and ((Test-Link $item) -or -not $item.PSIsContainer)) { + throw "Config container must be a real directory, not a file or link: $current. Existing data was left untouched." + } + $parent = [IO.Path]::GetDirectoryName($current) + if ($parent -eq $current) { break } + $current = $parent + } +} +function Remove-Link($Entry) { + # Never recurse: deleting the link itself must not touch its target. + if ($Entry.PSIsContainer) { [IO.Directory]::Delete($Entry.FullName) } else { [IO.File]::Delete($Entry.FullName) } +} +function Get-OpenCodeVersion { + $command = Get-Command opencode -ErrorAction SilentlyContinue + if ($null -eq $command) { return $null } + try { + $global:LASTEXITCODE = 0 + $output = (& $command --version 2>$null | Out-String).Trim() + if ($LASTEXITCODE -eq 0 -and $output -match '^v?(1\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$') { return $Matches[1] } + } catch { return $null } + return $null +} +function Require-OpenCode { + $version = Get-OpenCodeVersion + if ($version) { Write-Host " OK OpenCode $version"; return } + if ($InstallOpenCode) { + $npm = Get-Command npm.cmd -ErrorAction SilentlyContinue + if ($null -eq $npm) { throw 'npm is required for -InstallOpenCode. Install Node.js/npm or OpenCode 1.x manually, then rerun.' } + Write-Host 'Installing OpenCode 1.x using npm. An incompatible OpenCode binary may be replaced.' + & $npm install --global opencode-ai@1 + if ($LASTEXITCODE -ne 0) { throw "npm installation failed (exit $LASTEXITCODE)." } + $version = Get-OpenCodeVersion + if ($version) { Write-Host " OK OpenCode $version"; return } + throw 'OpenCode 1.x could not be verified after npm installation. Check PATH and restart PowerShell.' + } + throw 'A working OpenCode 1.x executable is required on PATH. Run opencode --version, or rerun .\install.ps1 -InstallOpenCode. OpenCode 2 is not supported.' +} +function Show-ConfigHint { + $jsonPath = Join-Path $config 'opencode.json' + if ([IO.File]::Exists($jsonPath) -and [IO.File]::ReadAllText($jsonPath) -match '"default_agent"\s*:\s*"orchestrator"') { + Write-Host ' OK opencode.json: default_agent is orchestrator' + return + } + Write-Host "Add these recommended settings to $jsonPath (merge with your existing configuration):" + [ordered]@{ + '$schema' = 'https://opencode.ai/config.json' + default_agent = 'orchestrator' + instructions = @((Join-Path $config 'team/AGENTS.md').Replace('\', '/'), (Join-Path $config 'team.yaml').Replace('\', '/')) + } | ConvertTo-Json -Depth 3 | Write-Host +} +function Assert-CoreEntry([string]$Name) { + $entry = Get-Entry (Join-Path $config $Name) + if ($null -ne $entry -and -not (Test-Owned $entry)) { + throw "Core entry $Name already exists locally or points outside this repo. Left untouched: $($entry.FullName)" + } +} +function Install-Link([string]$Name, [bool]$Skill = $false) { + $path = Join-Path $config $Name + $target = Join-Path $repo $Name + $entry = Get-Entry $path + if ($null -ne $entry) { + if (-not (Test-Owned $entry)) { + if ($Skill) { Write-Host " SKIP $Name (local or externally managed)"; return } + throw "Existing entry is not managed by this repo: $path" + } + if ((Get-LinkTarget $entry) -eq $target) { Write-Host " OK $Name (already linked)"; return } + Remove-Link $entry + } + New-Item -ItemType SymbolicLink -Path $path -Target $target | Out-Null + Write-Host " OK $Name -> $target" +} + +try { + # Validate parents in every mode, including uninstall, before traversing them. + Assert-RealParents $config + Assert-RealParents (Join-Path $config 'skills') + Assert-RealParents (Join-Path $config 'team') + if ($Uninstall) { + $names = @($core) + @($skills | ForEach-Object { 'skills/' + $_.Name }) + # Also remove managed skills no longer present in the current checkout. + if (Test-Path -LiteralPath (Join-Path $config 'skills')) { + $names += @(Get-ChildItem -LiteralPath (Join-Path $config 'skills') -Force | Where-Object { $_.Name -like 'team-*' } | ForEach-Object { 'skills/' + $_.Name }) + } + foreach ($name in $names | Select-Object -Unique) { + $entry = Get-Entry (Join-Path $config $name) + if (Test-Owned $entry) { Remove-Link $entry; Write-Host " OK removed $name" } + } + Write-Host 'opencode.json was not modified.' + exit 0 + } + Require-OpenCode + if ($Check) { + $ok = $true + foreach ($name in $core) { + $entry = Get-Entry (Join-Path $config $name) + if ((Test-Link $entry) -and (Get-LinkTarget $entry) -eq (Join-Path $repo $name) -and (Test-Path -LiteralPath (Join-Path $repo $name))) { + Write-Host " OK $name" + } else { Write-Host " MISSING or wrong target: $name"; $ok = $false } + } + foreach ($skill in $skills) { + $name = 'skills/' + $skill.Name + $entry = Get-Entry (Join-Path $config $name) + if ($null -eq $entry) { Write-Host " MISSING $name"; $ok = $false } + elseif ((Test-Owned $entry) -and ((Get-LinkTarget $entry) -ne $skill.FullName -or -not (Test-Path -LiteralPath $entry.FullName))) { + Write-Host " WRONG target: $name"; $ok = $false + } else { Write-Host " OK $name (local/external entries are preserved)" } + } + Show-ConfigHint + if (-not $ok) { exit 1 } + exit 0 + } + foreach ($name in $core) { Assert-CoreEntry $name } + [IO.Directory]::CreateDirectory($config) | Out-Null + # Probe before replacing or creating any installation links. + $probe = Join-Path $config ('.opencode-team-link-probe-' + [guid]::NewGuid().ToString('N')) + try { + New-Item -ItemType SymbolicLink -Path $probe -Target (Join-Path $repo 'team.yaml') | Out-Null + } catch { + throw 'Cannot create symbolic links. Run this installer from an Administrator PowerShell, or enable Windows Developer Mode and use PowerShell 7. No installation links were changed.' + } finally { if ($null -ne (Get-Entry $probe)) { [IO.File]::Delete($probe) } } + [IO.Directory]::CreateDirectory((Join-Path $config 'skills')) | Out-Null + [IO.Directory]::CreateDirectory((Join-Path $config 'team')) | Out-Null + foreach ($name in $core) { Install-Link $name } + foreach ($skill in $skills) { Install-Link ('skills/' + $skill.Name) $true } + Show-ConfigHint + Write-Host 'Restart OpenCode. opencode.json was not modified.' +} catch { + [Console]::Error.WriteLine($_.Exception.Message) + exit 1 +} diff --git a/scripts/smoke-install.ps1 b/scripts/smoke-install.ps1 new file mode 100644 index 0000000..63675f0 --- /dev/null +++ b/scripts/smoke-install.ps1 @@ -0,0 +1,191 @@ +# Dependency-free Windows installer integration tests. Never uses user config. +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +$repo = Split-Path $PSScriptRoot -Parent +$installer = Join-Path $repo 'install.ps1' +$shell = (Get-Process -Id $PID).Path +$work = Join-Path ([IO.Path]::GetTempPath()) ('opencode-team-install-' + [guid]::NewGuid().ToString('N')) +$oldPath = $env:PATH +$oldConfig = $env:OPENCODE_CONFIG_DIR +$utf8 = New-Object Text.UTF8Encoding($false) +$script:assertions = 0 + +function Assert($Condition, [string]$Message) { + if (-not $Condition) { throw "FAIL: $Message" } + $script:assertions++ +} +function Write-Text([string]$Path, [string]$Text) { + [IO.Directory]::CreateDirectory((Split-Path $Path -Parent)) | Out-Null + [IO.File]::WriteAllText($Path, $Text, $utf8) +} +function Remove-Fixture([string]$Path) { + $item = Get-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue + if ($null -eq $item) { return } + if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { + if ($item.PSIsContainer) { [IO.Directory]::Delete($Path) } else { [IO.File]::Delete($Path) } + } elseif ($item.PSIsContainer) { + foreach ($child in Get-ChildItem -LiteralPath $Path -Force) { Remove-Fixture $child.FullName } + [IO.Directory]::Delete($Path) + } else { [IO.File]::Delete($Path) } +} +function Run-Installer([string]$Config, [string[]]$Arguments = @(), [bool]$Success = $true) { + $env:OPENCODE_CONFIG_DIR = $Config + $start = New-Object Diagnostics.ProcessStartInfo + $start.FileName = $shell + $start.Arguments = '-NoProfile -ExecutionPolicy Bypass -File "' + $installer + '" ' + ($Arguments -join ' ') + $start.UseShellExecute = $false + $start.RedirectStandardOutput = $true + $start.RedirectStandardError = $true + $process = [Diagnostics.Process]::Start($start) + $stdout = $process.StandardOutput.ReadToEndAsync() + $stderr = $process.StandardError.ReadToEndAsync() + $process.WaitForExit() + $output = $stdout.Result + $stderr.Result + $code = $process.ExitCode + $process.Dispose() + Assert (($code -eq 0) -eq $Success) "installer $Arguments exit $code (expected success=$Success): $output" + return ($output -join "`n") +} +function Assert-Link([string]$Path, [string]$Target) { + $item = Get-Item -LiteralPath $Path -Force + Assert ($item.LinkType -eq 'SymbolicLink') "$Path must be a symbolic link" + Assert ([IO.Path]::GetFullPath([string]@($item.Target)[0]) -eq [IO.Path]::GetFullPath($Target)) "$Path target must be $Target" +} +function Fake-Version([string]$Version = '1.19.4', [int]$Code = 0) { + Write-Text (Join-Path $work 'bin/opencode.cmd') "@echo off`r`necho $Version`r`nexit /b $Code`r`n" +} + +try { + Assert (Test-Path -LiteralPath $installer) 'install.ps1 must exist' + [IO.Directory]::CreateDirectory($work) | Out-Null + Fake-Version + $env:PATH = (Join-Path $work 'bin') + ';' + $env:SystemRoot + '\System32' + $config = Join-Path $work ('config space ' + [char]0x00F6) + $json = '{"default_agent":"orchestrator","custom":"preserve"}' + Write-Text (Join-Path $config 'opencode.json') $json + Write-Host 'install / repeat / check / uninstall' + Run-Installer $config | Out-Null + Run-Installer $config | Out-Null + foreach ($name in @('agents','commands','team.yaml','team/AGENTS.md')) { + Assert-Link (Join-Path $config $name) (Join-Path $repo $name) + } + foreach ($skill in Get-ChildItem (Join-Path $repo 'skills/team-*') -Directory) { + Assert-Link (Join-Path $config ('skills/' + $skill.Name)) $skill.FullName + } + Run-Installer $config @('-Check') | Out-Null + Assert ([IO.File]::ReadAllText((Join-Path $config 'opencode.json')) -ceq $json) 'JSON must remain byte-for-byte unchanged' + Run-Installer $config @('-Uninstall') | Out-Null + Assert (-not (Test-Path (Join-Path $config 'agents'))) 'uninstall must remove managed link' + Assert (Test-Path (Join-Path $repo 'agents/orchestrator.md')) 'uninstall must preserve target' + Assert (Test-Path (Join-Path $config 'opencode.json')) 'uninstall must preserve JSON' + Run-Installer $config @('-Check') $false | Out-Null + + Write-Host 'local skill collision and in-repository relink' + $collision = Join-Path $work 'collision' + Write-Text (Join-Path $collision 'skills/team-build/marker') 'keep' + [IO.Directory]::CreateDirectory($collision) | Out-Null + New-Item -ItemType SymbolicLink -Path (Join-Path $collision 'agents') -Target (Join-Path $repo 'team') | Out-Null + Run-Installer $collision | Out-Null + Assert-Link (Join-Path $collision 'agents') (Join-Path $repo 'agents') + Assert ([IO.File]::ReadAllText((Join-Path $collision 'skills/team-build/marker')) -eq 'keep') 'local skill must survive' + Run-Installer $collision @('-Check') | Out-Null + Run-Installer $collision @('-Uninstall') | Out-Null + Assert (Test-Path (Join-Path $collision 'skills/team-build/marker')) 'local skill must survive uninstall' + + Write-Host 'external link protection' + $external = Join-Path $work 'external' + Write-Text (Join-Path $external 'marker') 'keep' + $protected = Join-Path $work 'protected' + [IO.Directory]::CreateDirectory($protected) | Out-Null + New-Item -ItemType SymbolicLink -Path (Join-Path $protected 'agents') -Target $external | Out-Null + Run-Installer $protected @() $false | Out-Null + Run-Installer $protected @('-Uninstall') | Out-Null + Assert-Link (Join-Path $protected 'agents') $external + Assert (Test-Path (Join-Path $external 'marker')) 'external target must survive' + Assert (-not (Test-Path (Join-Path $protected 'commands'))) 'collision preflight must prevent partial installation' + + Write-Host 'ordinary core files and linked parent containers' + $ordinary = Join-Path $work 'ordinary' + Write-Text (Join-Path $ordinary 'agents/marker') 'keep' + Run-Installer $ordinary @() $false | Out-Null + Assert (Test-Path (Join-Path $ordinary 'agents/marker')) 'ordinary core directory must survive' + $parentConfig = Join-Path $work 'linked-parent' + [IO.Directory]::CreateDirectory($parentConfig) | Out-Null + New-Item -ItemType SymbolicLink -Path (Join-Path $parentConfig 'skills') -Target $external | Out-Null + Run-Installer $parentConfig @() $false | Out-Null + Run-Installer $parentConfig @('-Uninstall') $false | Out-Null + Assert (-not (Test-Path (Join-Path $external 'team-build'))) 'must not install through linked parent' + + Write-Host 'relative external links and dangling links' + $relative = Join-Path $work 'relative' + [IO.Directory]::CreateDirectory($relative) | Out-Null + # mklink supports relative directory targets in Windows PowerShell 5.1 too. + & $env:ComSpec /d /c "mklink /D `"$relative\agents`" `"..\external`"" | Out-Null + Assert ($LASTEXITCODE -eq 0) 'relative link fixture must exist' + Run-Installer $relative @() $false | Out-Null + Run-Installer $relative @('-Uninstall') | Out-Null + Assert ((Get-Item -LiteralPath (Join-Path $relative 'agents')).LinkType -eq 'SymbolicLink') 'relative external link must survive' + $dangling = Join-Path $work 'dangling' + [IO.Directory]::CreateDirectory($dangling) | Out-Null + Write-Text (Join-Path $work 'missing.yaml') 'temporary target' + New-Item -ItemType SymbolicLink -Path (Join-Path $dangling 'team.yaml') -Target (Join-Path $work 'missing.yaml') | Out-Null + [IO.File]::Delete((Join-Path $work 'missing.yaml')) + Run-Installer $dangling @() $false | Out-Null + Run-Installer $dangling @('-Uninstall') | Out-Null + Assert ($null -ne (Get-Item -LiteralPath (Join-Path $dangling 'team.yaml') -Force)) 'dangling external link must survive' + + $sibling = Join-Path $work 'prefix-sibling' + [IO.Directory]::CreateDirectory($sibling) | Out-Null + & $env:ComSpec /d /c "mklink /D `"$sibling\agents`" `"$repo-other\agents`"" | Out-Null + Assert ($LASTEXITCODE -eq 0) 'prefix sibling fixture must exist' + Run-Installer $sibling @() $false | Out-Null + Run-Installer $sibling @('-Uninstall') | Out-Null + Assert ($null -ne (Get-Item -LiteralPath (Join-Path $sibling 'agents') -Force)) 'repository-prefix sibling link must survive' + + Write-Host 'version requirements and explicit bootstrap' + $versionConfig = Join-Path $work 'versions' + foreach ($version in @('2.0.0','unparseable')) { + Fake-Version $version + Run-Installer $versionConfig @() $false | Out-Null + Run-Installer $versionConfig @('-Check') $false | Out-Null + Assert (-not (Test-Path $versionConfig)) 'bad version must not create config' + } + Fake-Version '1.19.4' 7 + Run-Installer $versionConfig @() $false | Out-Null + [IO.File]::Delete((Join-Path $work 'bin/opencode.cmd')) + Run-Installer $versionConfig @() $false | Out-Null + Run-Installer $versionConfig @('-Uninstall') | Out-Null + Run-Installer $versionConfig @('-InstallOpenCode') $false | Out-Null + Fake-Version + [IO.File]::Copy((Join-Path $work 'bin/opencode.cmd'), (Join-Path $work 'bin/installed.cmd')) + $npm = "@echo off`r`necho %* > `"%~dp0npm-args.txt`"`r`ncopy /y `"%~dp0installed.cmd`" `"%~dp0opencode.cmd`" >nul`r`nexit /b 0`r`n" + Write-Text (Join-Path $work 'bin/npm.cmd') $npm + Run-Installer $versionConfig @('-InstallOpenCode') | Out-Null + Assert (-not (Test-Path (Join-Path $work 'bin/npm-args.txt'))) 'valid 1.x must not invoke npm' + [IO.File]::Delete((Join-Path $work 'bin/opencode.cmd')) + Run-Installer $versionConfig @('-InstallOpenCode') | Out-Null + Assert (([IO.File]::ReadAllText((Join-Path $work 'bin/npm-args.txt'))).Trim() -eq 'install --global opencode-ai@1') 'bootstrap must constrain OpenCode major version' + [IO.File]::Delete((Join-Path $work 'bin/opencode.cmd')) + Write-Text (Join-Path $work 'bin/npm.cmd') "@echo off`r`nexit /b 9`r`n" + Run-Installer $versionConfig @('-InstallOpenCode') $false | Out-Null + Write-Text (Join-Path $work 'bin/npm.cmd') "@echo off`r`nexit /b 0`r`n" + Run-Installer $versionConfig @('-InstallOpenCode') $false | Out-Null + Fake-Version + Run-Installer $versionConfig @('-Check','-Uninstall') $false | Out-Null + Run-Installer $versionConfig @('-Check','-InstallOpenCode') $false | Out-Null + + Write-Host 'JSONC hint and caller working directory' + $jsoncConfig = Join-Path $work 'jsonc' + $jsonc = "{ // user comment`n `"custom`": true`n}" + Write-Text (Join-Path $jsoncConfig 'opencode.json') $jsonc + Push-Location $work + try { $hint = Run-Installer $jsoncConfig } finally { Pop-Location } + Assert ($hint -match 'default_agent') 'missing setting must print a configuration hint' + Assert ([IO.File]::ReadAllText((Join-Path $jsoncConfig 'opencode.json')) -ceq $jsonc) 'JSONC must remain unchanged' + + Write-Host "installer smoke: OK ($script:assertions assertions)" +} finally { + $env:PATH = $oldPath + $env:OPENCODE_CONFIG_DIR = $oldConfig + if (Test-Path -LiteralPath $work) { Remove-Fixture $work } +} From 3514218a72d0bcd9505fa4a66a9b227d273627fa Mon Sep 17 00:00:00 2001 From: Onur Date: Tue, 27 Oct 2026 23:35:18 +0300 Subject: [PATCH 3/8] feat: support memory compaction in PowerShell --- scripts/test-memory-compact.ps1 | 135 ++++++++++++++++++++++++++ skills/team-memory/memory-compact.ps1 | 94 ++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 scripts/test-memory-compact.ps1 create mode 100644 skills/team-memory/memory-compact.ps1 diff --git a/scripts/test-memory-compact.ps1 b/scripts/test-memory-compact.ps1 new file mode 100644 index 0000000..9652312 --- /dev/null +++ b/scripts/test-memory-compact.ps1 @@ -0,0 +1,135 @@ +# Dependency-free native memory regression tests. No user memory is touched. +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +$repo = Split-Path $PSScriptRoot -Parent +$helper = Join-Path $repo 'skills/team-memory/memory-compact.ps1' +$shell = (Get-Process -Id $PID).Path +$work = Join-Path ([IO.Path]::GetTempPath()) ('opencode-team-memory-' + [guid]::NewGuid().ToString('N')) +$oldMemory = $env:MEMORY_DIR +$utf8 = New-Object Text.UTF8Encoding($false) +$script:assertions = 0 +function Assert($Condition, [string]$Message) { + if (-not $Condition) { throw "FAIL: $Message" } + $script:assertions++ +} +function Write-Lines([string]$Path, [string[]]$Lines) { + [IO.Directory]::CreateDirectory((Split-Path $Path -Parent)) | Out-Null + [IO.File]::WriteAllLines($Path, $Lines, $utf8) +} +function Entries([int]$Start, [int]$End, [string]$Month = '2026-10') { + for ($i = $Start; $i -le $End; $i++) { '- {0}-{1:00} | research | fact | entry {2} | artifact' -f $Month, (($i - 1) % 28 + 1), $i } +} +function Run-Compactor([string]$Memory, [string]$Role = '', [bool]$Success = $true) { + $env:MEMORY_DIR = $Memory + $start = New-Object Diagnostics.ProcessStartInfo + $start.FileName = $shell + $start.WorkingDirectory = (Get-Location).Path + $start.Arguments = '-NoProfile -ExecutionPolicy Bypass -File "' + $helper + '"' + if ($Role) { $start.Arguments += ' -Role "' + $Role + '"' } + $start.UseShellExecute = $false + $start.RedirectStandardOutput = $true + $start.RedirectStandardError = $true + $process = [Diagnostics.Process]::Start($start) + $stdout = $process.StandardOutput.ReadToEndAsync() + $stderr = $process.StandardError.ReadToEndAsync() + $process.WaitForExit() + $output = $stdout.Result + $stderr.Result + $code = $process.ExitCode + $process.Dispose() + Assert (($code -eq 0) -eq $Success) "compactor exit $code (expected success=$Success): $output" +} +function Count-Entries([string]$Path) { return @([IO.File]::ReadAllLines($Path) | Where-Object { $_ -match '^-\s' }).Count } +function Remove-Fixture([string]$Path) { + $item = Get-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue + if ($null -eq $item) { return } + if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { + if ($item.PSIsContainer) { [IO.Directory]::Delete($Path) } else { [IO.File]::Delete($Path) } + } elseif ($item.PSIsContainer) { + foreach ($child in Get-ChildItem -LiteralPath $Path -Force) { Remove-Fixture $child.FullName } + [IO.Directory]::Delete($Path) + } else { [IO.File]::Delete($Path) } +} +try { + Assert (Test-Path -LiteralPath $helper) 'memory-compact.ps1 must exist' + $memory = Join-Path $work ('project space ' + [char]0x00F6) + $log = Join-Path $memory 'log/analyst.md' + Write-Host 'empty logs and 40-entry boundary' + Write-Lines $log @('# analyst log', '') + Run-Compactor $memory 'analyst' + Write-Lines $log (@('# analyst log', '') + @(Entries 1 40)) + $before = [IO.File]::ReadAllText($log) + Run-Compactor $memory 'analyst' + Assert ([IO.File]::ReadAllText($log) -ceq $before) '40 entries must remain unchanged' + + Write-Host 'archive 35 / retain 10 and repeated monthly archive' + Write-Lines $log (@('# analyst log', '') + @(Entries 1 45)) + Run-Compactor $memory 'analyst' + $archive = Join-Path $memory 'archive/analyst-2026-10.md' + Assert ((Count-Entries $log) -eq 10) '45 entries must retain last 10' + Assert ((Count-Entries $archive) -eq 35) '45 entries must archive first 35' + Assert (([IO.File]::ReadAllLines($log))[-1] -match 'entry 45 \|') 'log order must be retained' + [IO.File]::AppendAllLines($log, [string[]]@(Entries 46 80), $utf8) + Run-Compactor $memory 'analyst' + Assert ((Count-Entries $archive) -eq 70) 'second run must append 35 entries' + Assert (@([IO.File]::ReadAllLines($archive) | Where-Object { $_ -match '^# analyst archive' }).Count -eq 1) 'archive header must not duplicate' + Run-Compactor $memory 'analyst' + Assert ((Count-Entries $archive) -eq 70) 'no-op rerun must not duplicate entries' + + Write-Host 'multiple months, CRLF, Unicode, role filter' + $unicode = 'T' + [char]0x00FC + 'rk' + [char]0x00E7 + 'e' + $lines = @('# analyst log', '') + @(Entries 1 20 '2026-09') + @(Entries 21 41) + $lines[2] += ' ' + $unicode + [IO.File]::WriteAllText($log, ($lines -join "`r`n"), $utf8) + Write-Lines (Join-Path $memory 'log/pm.md') (@('# pm log', '') + @(Entries 1 41)) + Run-Compactor $memory 'analyst' + Assert ((Count-Entries (Join-Path $memory 'archive/analyst-2026-09.md')) -eq 20) 'September entries must use September archive' + Assert ([IO.File]::ReadAllText((Join-Path $memory 'archive/analyst-2026-09.md')).Contains($unicode)) 'Unicode must survive compaction' + Assert ((Count-Entries (Join-Path $memory 'log/pm.md')) -eq 41) 'explicit role must not compact another role' + Run-Compactor $memory + Assert ((Count-Entries (Join-Path $memory 'log/pm.md')) -eq 10) 'no role must compact all known roles' + + Write-Host 'index cap, stable exact deduplication, unchanged failure' + $index = Join-Path $memory 'INDEX.md' + Write-Lines $index ([string[]](1..60 | ForEach-Object { "fact $_" })) + $before = [IO.File]::ReadAllText($index) + Run-Compactor $memory + Assert ([IO.File]::ReadAllText($index) -ceq $before) '60-line index must remain unchanged' + $lines = @('# Memory Index', 'Fact', 'fact', $unicode) + @((1..65) | ForEach-Object { 'duplicate' }) + Write-Lines $index $lines + Run-Compactor $memory + Assert ([IO.File]::ReadAllLines($index).Count -eq 5) 'only exact duplicates must be removed' + Assert (([IO.File]::ReadAllLines($index))[1] -ceq 'Fact') 'first occurrence order must remain' + Write-Lines $index ([string[]](1..61 | ForEach-Object { "unique fact $_" })) + $before = [IO.File]::ReadAllText($index) + Run-Compactor $memory '' $false + Assert ([IO.File]::ReadAllText($index) -ceq $before) 'unique oversized index must remain unchanged' + [IO.File]::Delete($index) + + Write-Host 'default current-project directory and custom memory root' + $project = Join-Path $work 'cwd' + $cwdLog = Join-Path $project '.team/memory/log/analyst.md' + Write-Lines $cwdLog (@('# analyst log', '') + @(Entries 1 41)) + Push-Location $project + try { Run-Compactor '' 'analyst' } finally { Pop-Location } + Assert ((Count-Entries $cwdLog) -eq 10) 'default memory path must use target project CWD' + + Write-Host 'unsafe role/date and linked memory protection' + Run-Compactor $memory '../outside' $false + $badLines = @('# analyst log', '') + @(Entries 1 41) + $badLines[2] = '- ../escape | research | fact | bad date' + Write-Lines $log $badLines + $before = [IO.File]::ReadAllText($log) + Run-Compactor $memory 'analyst' $false + Assert ([IO.File]::ReadAllText($log) -ceq $before) 'unsafe archive date must not change log' + $outside = Join-Path $work 'outside' + Write-Lines (Join-Path $outside 'analyst.md') (@('# analyst log', '') + @(Entries 1 41)) + $linkedMemory = Join-Path $work 'linked' + [IO.Directory]::CreateDirectory($linkedMemory) | Out-Null + New-Item -ItemType SymbolicLink -Path (Join-Path $linkedMemory 'log') -Target $outside | Out-Null + Run-Compactor $linkedMemory 'analyst' $false + Assert ((Count-Entries (Join-Path $outside 'analyst.md')) -eq 41) 'linked log container must not change outside memory' + Write-Host "memory-compact: OK ($script:assertions assertions)" +} finally { + $env:MEMORY_DIR = $oldMemory + if (Test-Path -LiteralPath $work) { Remove-Fixture $work } +} diff --git a/skills/team-memory/memory-compact.ps1 b/skills/team-memory/memory-compact.ps1 new file mode 100644 index 0000000..0c1f16a --- /dev/null +++ b/skills/team-memory/memory-compact.ps1 @@ -0,0 +1,94 @@ +#Requires -Version 5.1 +# Compact project memory from the target project's working directory. +[CmdletBinding()] +param([Parameter(Position = 0)][string]$Role) +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +$roles = @('analyst','pm','po','ux','brand','architect','fe','be','qa','security','docs','devops','marketing','orchestrator') +$memory = if ($env:MEMORY_DIR) { $env:MEMORY_DIR } else { Join-Path (Get-Location).Path '.team/memory' } +$memory = [IO.Path]::GetFullPath($memory).TrimEnd('\', '/') +$utf8 = New-Object Text.UTF8Encoding($false) + +function Assert-SafePath([string]$Path) { + $full = [IO.Path]::GetFullPath($Path) + if ($full -ne $memory -and -not $full.StartsWith($memory + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { + throw "Path escapes the memory directory: $Path" + } + # Reparse points could redirect writes outside the selected memory tree. + $current = $full + while ($current) { + $item = Get-Item -LiteralPath $current -Force -ErrorAction SilentlyContinue + if ($null -ne $item -and ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { + throw "Linked memory paths are not supported: $current" + } + $parent = [IO.Path]::GetDirectoryName($current) + if ($parent -eq $current) { break } + $current = $parent + } +} +function Write-MemoryLines([string]$Path, [string[]]$Lines) { + Assert-SafePath $Path + $temporary = $Path + '.' + [guid]::NewGuid().ToString('N') + '.tmp' + try { + [IO.File]::WriteAllLines($temporary, $Lines, $utf8) + if ([IO.File]::Exists($Path)) { [IO.File]::Replace($temporary, $Path, [NullString]::Value) } + else { [IO.File]::Move($temporary, $Path) } + } finally { if ([IO.File]::Exists($temporary)) { [IO.File]::Delete($temporary) } } +} +function Compact-Log([string]$Name) { + $log = Join-Path $memory ('log/' + $Name + '.md') + Assert-SafePath $log + if (-not [IO.File]::Exists($log)) { return } + $lines = [IO.File]::ReadAllLines($log) + $entries = @($lines | Where-Object { $_ -match '^-\s' }) + if ($entries.Count -le 40) { return } + $archiveCount = $entries.Count - 10 + $groups = [ordered]@{} + for ($i = 0; $i -lt $archiveCount; $i++) { + $entry = $entries[$i] + $date = [datetime]::MinValue + if ($entry -notmatch '^-\s+(\d{4}-\d{2}-\d{2})\s+\|' -or + -not [datetime]::TryParseExact($Matches[1], 'yyyy-MM-dd', [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::None, [ref]$date)) { + throw "Invalid archive date in $Name log; log was left unchanged." + } + $month = $date.ToString('yyyy-MM', [Globalization.CultureInfo]::InvariantCulture) + if (-not $groups.Contains($month)) { $groups[$month] = New-Object 'Collections.Generic.List[string]' } + $groups[$month].Add($entry) + } + $archiveDir = Join-Path $memory 'archive' + Assert-SafePath $archiveDir + foreach ($month in $groups.Keys) { Assert-SafePath (Join-Path $archiveDir ("$Name-$month.md")) } + [IO.Directory]::CreateDirectory($archiveDir) | Out-Null + foreach ($month in $groups.Keys) { + $archive = Join-Path $archiveDir ("$Name-$month.md") + $archived = if ([IO.File]::Exists($archive)) { @([IO.File]::ReadAllLines($archive)) } else { @("# $Name archive $([char]0x2014) $month", '') } + Write-MemoryLines $archive ([string[]]($archived + $groups[$month].ToArray())) + } + $retained = @($lines | Select-Object -First 2) + @($entries[$archiveCount..($entries.Count - 1)]) + Write-MemoryLines $log ([string[]]$retained) + Write-Host "compacted ${Name}: archived $archiveCount entries, kept 10" +} +function Compact-Index { + $index = Join-Path $memory 'INDEX.md' + Assert-SafePath $index + if (-not [IO.File]::Exists($index)) { return } + $lines = [IO.File]::ReadAllLines($index) + if ($lines.Count -le 60) { return } + $seen = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) + $unique = @($lines | Where-Object { $seen.Add($_) }) + if ($unique.Count -gt 60) { + throw "INDEX.md is $($unique.Count) lines after removing duplicates; merge facts, move detail to a decision file, or shorten open questions, then rerun. Index was left unchanged." + } + Write-MemoryLines $index ([string[]]$unique) + Write-Host "compacted INDEX.md: $($lines.Count) -> $($unique.Count) lines" +} +try { + if ($Role -and $roles -cnotcontains $Role) { throw "Unknown role: $Role. Use a role from team.yaml." } + Assert-SafePath $memory + $selected = if ($Role) { @($Role) } else { $roles } + foreach ($name in $selected) { Compact-Log $name } + Compact-Index +} catch { + [Console]::Error.WriteLine($_.Exception.Message) + exit 1 +} From 95b47dfad64601939040a6d999bd1b8d01c3f3b6 Mon Sep 17 00:00:00 2001 From: Onur Date: Tue, 27 Oct 2026 23:39:27 +0300 Subject: [PATCH 4/8] fix: validate skill targets and allow safe custom memory roles --- install.ps1 | 13 ++++++++++++- scripts/smoke-install.ps1 | 19 +++++++++++++++++++ scripts/test-memory-compact.ps1 | 6 ++++++ skills/team-memory/memory-compact.ps1 | 4 +++- 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/install.ps1 b/install.ps1 index 21c12cf..45ba1c1 100644 --- a/install.ps1 +++ b/install.ps1 @@ -39,6 +39,17 @@ function Test-Owned($Entry) { $target = Get-LinkTarget $Entry return $null -ne $target -and $target.StartsWith($repo + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) } +function Test-AvailableEntry($Entry) { + # Test-Path may report a dangling directory link as present on Windows. + $seen = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) + $current = $Entry + while ($null -ne $current -and (Test-Link $current)) { + $target = Get-LinkTarget $current + if (-not $target -or -not $seen.Add($target)) { return $false } + $current = Get-Entry $target + } + return $null -ne $current +} function Assert-RealParents([string]$Path) { $current = [IO.Path]::GetFullPath($Path) while ($current) { @@ -145,7 +156,7 @@ try { foreach ($skill in $skills) { $name = 'skills/' + $skill.Name $entry = Get-Entry (Join-Path $config $name) - if ($null -eq $entry) { Write-Host " MISSING $name"; $ok = $false } + if (-not (Test-AvailableEntry $entry)) { Write-Host " MISSING $name"; $ok = $false } elseif ((Test-Owned $entry) -and ((Get-LinkTarget $entry) -ne $skill.FullName -or -not (Test-Path -LiteralPath $entry.FullName))) { Write-Host " WRONG target: $name"; $ok = $false } else { Write-Host " OK $name (local/external entries are preserved)" } diff --git a/scripts/smoke-install.ps1 b/scripts/smoke-install.ps1 index 63675f0..a0e2b7d 100644 --- a/scripts/smoke-install.ps1 +++ b/scripts/smoke-install.ps1 @@ -7,6 +7,7 @@ $shell = (Get-Process -Id $PID).Path $work = Join-Path ([IO.Path]::GetTempPath()) ('opencode-team-install-' + [guid]::NewGuid().ToString('N')) $oldPath = $env:PATH $oldConfig = $env:OPENCODE_CONFIG_DIR +$oldProfile = $env:USERPROFILE $utf8 = New-Object Text.UTF8Encoding($false) $script:assertions = 0 @@ -183,9 +184,27 @@ try { Assert ($hint -match 'default_agent') 'missing setting must print a configuration hint' Assert ([IO.File]::ReadAllText((Join-Path $jsoncConfig 'opencode.json')) -ceq $jsonc) 'JSONC must remain unchanged' + Write-Host 'default user profile and wrong or unavailable skill targets' + $env:USERPROFILE = Join-Path $work 'user profile' + Run-Installer '' | Out-Null + Assert-Link (Join-Path $env:USERPROFILE '.config/opencode/agents') (Join-Path $repo 'agents') + $wrong = Join-Path $jsoncConfig 'team.yaml' + [IO.File]::Delete($wrong) + New-Item -ItemType SymbolicLink -Path $wrong -Target (Join-Path $repo 'README.md') | Out-Null + Run-Installer $jsoncConfig @('-Check') $false | Out-Null + Run-Installer $jsoncConfig | Out-Null + $brokenSkill = Join-Path $jsoncConfig 'skills/team-build' + [IO.Directory]::Delete($brokenSkill) + $skillTarget = Join-Path $work 'external-skill' + [IO.Directory]::CreateDirectory($skillTarget) | Out-Null + New-Item -ItemType SymbolicLink -Path $brokenSkill -Target $skillTarget | Out-Null + [IO.Directory]::Delete($skillTarget) + Run-Installer $jsoncConfig @('-Check') $false | Out-Null + Write-Host "installer smoke: OK ($script:assertions assertions)" } finally { $env:PATH = $oldPath $env:OPENCODE_CONFIG_DIR = $oldConfig + $env:USERPROFILE = $oldProfile if (Test-Path -LiteralPath $work) { Remove-Fixture $work } } diff --git a/scripts/test-memory-compact.ps1 b/scripts/test-memory-compact.ps1 index 9652312..8724f8b 100644 --- a/scripts/test-memory-compact.ps1 +++ b/scripts/test-memory-compact.ps1 @@ -113,6 +113,12 @@ try { try { Run-Compactor '' 'analyst' } finally { Pop-Location } Assert ((Count-Entries $cwdLog) -eq 10) 'default memory path must use target project CWD' + Write-Host 'explicit custom role' + $customLog = Join-Path $memory 'log/researcher.md' + Write-Lines $customLog (@('# researcher log', '') + @(Entries 1 41)) + Run-Compactor $memory 'researcher' + Assert ((Count-Entries $customLog) -eq 10) 'explicit safe custom roles must work like the Bash helper' + Write-Host 'unsafe role/date and linked memory protection' Run-Compactor $memory '../outside' $false $badLines = @('# analyst log', '') + @(Entries 1 41) diff --git a/skills/team-memory/memory-compact.ps1 b/skills/team-memory/memory-compact.ps1 index 0c1f16a..6b0d505 100644 --- a/skills/team-memory/memory-compact.ps1 +++ b/skills/team-memory/memory-compact.ps1 @@ -83,7 +83,9 @@ function Compact-Index { Write-Host "compacted INDEX.md: $($lines.Count) -> $($unique.Count) lines" } try { - if ($Role -and $roles -cnotcontains $Role) { throw "Unknown role: $Role. Use a role from team.yaml." } + if ($Role -and ($Role -cnotmatch '^[A-Za-z0-9][A-Za-z0-9_-]*$' -or $Role -match '^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$')) { + throw "Unsafe role name: $Role. Use letters, digits, underscores or hyphens and avoid Windows device names." + } Assert-SafePath $memory $selected = if ($Role) { @($Role) } else { $roles } foreach ($name in $selected) { Compact-Log $name } From e91024d3b67d948568d6d8f333ad870e91f34e56 Mon Sep 17 00:00:00 2001 From: Onur Date: Tue, 27 Oct 2026 23:39:31 +0300 Subject: [PATCH 5/8] docs: enable and document native Windows support --- .github/workflows/checks.yml | 19 +++++++ CONTRIBUTING.md | 11 ++++ README.md | 23 +++++++- README.tr.md | 23 +++++++- agents/orchestrator.md | 2 +- docs/faq.md | 6 +- docs/install.md | 55 +++++++++++++++++-- docs/memory.md | 11 ++++ .../plans/2026-10-27-native-windows.md | 36 ++++++------ skills/team-memory/SKILL.md | 13 ++++- team/AGENTS.md | 4 +- 11 files changed, 171 insertions(+), 32 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index cab131e..a030393 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -79,3 +79,22 @@ jobs: - name: install / check / uninstall shell: bash run: ./scripts/smoke-install.sh + + smoke-windows: + name: Windows smoke (${{ matrix.shell }}) + runs-on: windows-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + shell: [powershell, pwsh] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: install / check / uninstall + shell: ${{ matrix.shell }} + run: ./scripts/smoke-install.ps1 + - name: Native memory compaction + shell: ${{ matrix.shell }} + run: ./scripts/test-memory-compact.ps1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ffc9e3d..d495082 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,6 +22,8 @@ npm run docs:dev # http://localhost:5173/opencode-team/ `install.sh` never edits `opencode.json`. Set `default_agent` and `instructions` yourself (see README). +On native Windows, use `install.ps1` instead of `install.sh`, with `-Check`, `-Uninstall`, or `-InstallOpenCode` as needed. Run in Administrator PowerShell (Windows PowerShell 5.1 or PowerShell 7), or use PowerShell 7 with Developer Mode enabled. If local execution policy blocks scripts, invoke `powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\install.ps1`. Neither installer changes your JSON configuration or machine policy. See [Windows installation](docs/install.md#windows). + ## What you can change | Area | Path | Frontmatter | @@ -81,6 +83,15 @@ The validator checks frontmatter, filename/name matches, agent and command refer CI also runs `shellcheck --severity=error`. It is preinstalled on `ubuntu-latest`; locally, install it with your package manager if you want the same result. +Native Windows tests need symbolic-link privileges and run offline against temporary directories, with fake OpenCode/npm commands: + +```powershell +.\scripts\smoke-install.ps1 +.\scripts\test-memory-compact.ps1 +``` + +Run both under Windows PowerShell 5.1 and PowerShell 7 when changing native scripts. The `windows-latest` job tests both shells. Ruby is still needed for manifest validation/reference generation, and Node 22+ for the docs build; these are contributor tools, not dependencies of the team installer. + Build the docs before opening a PR that touches `docs/`: ```bash diff --git a/README.md b/README.md index b0ed6c4..ba19cda 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,9 @@ The orchestrator is the **default agent**. Open `opencode` anywhere, describe an Requires OpenCode 1.x (tested on 1.19.0). OpenCode 2 support is planned as a separate release. There is no tagged release yet; `main` is the development branch. -Supported on macOS and Linux. Native Windows is **not supported yet**: `install.sh` is Bash and uses symlinks. WSL is untested. +Supported on macOS, Linux, and native Windows (Windows PowerShell 5.1 or PowerShell 7; verified with OpenCode 1.19.4). WSL is untested. + +On macOS or Linux: ```bash git clone https://github.com/sametcelikbicak/opencode-team.git @@ -78,6 +80,16 @@ cd opencode-team ./install.sh ``` +On Windows, clone the repository and run the PowerShell installer from an **Administrator PowerShell**: + +```powershell +git clone https://github.com/sametcelikbicak/opencode-team.git +cd opencode-team +powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\install.ps1 +``` + +PowerShell 7 with Windows Developer Mode enabled can create the symlinks without elevation. The process-scoped `-ExecutionPolicy Bypass` does not change your machine policy. See the [Windows installation guide](docs/install.md#windows) for checks, optional OpenCode installation, and troubleshooting. + If OpenCode is not on your PATH, install it first, or let the installer run the official OpenCode 1.x installer: ```bash @@ -100,7 +112,7 @@ Restart opencode. That's it — the setup is global, so it works in every projec The repo stays in git; the config directory holds symlinks. Edit a file in the repo and it takes effect immediately. -The installer never modifies `opencode.json`. If `default_agent` is not set to `orchestrator`, it prints the recommended snippet and leaves the file alone. +Neither installer modifies `opencode.json`. If `default_agent` is not set to `orchestrator`, it prints the recommended snippet and leaves the file alone. On Windows the default directory is `%USERPROFILE%\.config\opencode`; both installers honor `OPENCODE_CONFIG_DIR`. Check or remove the install: @@ -109,6 +121,13 @@ Check or remove the install: ./install.sh --uninstall ``` +Windows equivalents (use the same elevated shell or Developer Mode setup): + +```powershell +.\install.ps1 -Check +.\install.ps1 -Uninstall +``` + `--check` exits non-zero when a symlink is missing. A missing `default_agent` setting is shown as a hint, not a failure. `--uninstall` removes only symlinks that point into this repo. ### Manual setup diff --git a/README.tr.md b/README.tr.md index 6ce287c..97040f7 100644 --- a/README.tr.md +++ b/README.tr.md @@ -70,7 +70,9 @@ Orkestratör **varsayılan ajandır**. Herhangi bir dizinde `opencode` açın, f OpenCode 1.x gerektirir (1.19.0 ile test edildi). OpenCode 2 desteği ayrı bir sürüm olarak planlanıyor. Henüz etiketli sürüm yok; geliştirme `main` üzerinde devam ediyor. -macOS ve Linux desteklenir. Yerel Windows **henüz desteklenmiyor**: `install.sh` Bash kullanır ve symlink oluşturur. WSL test edilmedi. +macOS, Linux ve yerel Windows desteklenir (Windows PowerShell 5.1 veya PowerShell 7; OpenCode 1.19.4 ile doğrulandı). WSL test edilmedi. + +macOS veya Linux: ```bash git clone https://github.com/sametcelikbicak/opencode-team @@ -78,6 +80,16 @@ cd opencode-team ./install.sh ``` +Windows'ta repoyu klonlayın ve kurucuyu **yönetici PowerShell** üzerinden çalıştırın: + +```powershell +git clone https://github.com/sametcelikbicak/opencode-team.git +cd opencode-team +powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\install.ps1 +``` + +Windows Geliştirici Modu açıkken PowerShell 7 ile yönetici yetkisi olmadan da symlink oluşturulabilir. `-ExecutionPolicy Bypass` yalnızca bu işlem için geçerlidir; makinenin kalıcı politikasını değiştirmez. Kontrol, isteğe bağlı OpenCode kurulumu ve sorun giderme için [Windows kurulum kılavuzuna](docs/install.md#windows) bakın. + OpenCode PATH'te yoksa önce kurun veya kurulum betiğinin resmî OpenCode 1.x kurucusunu çalıştırmasına izin verin: ```bash @@ -100,7 +112,7 @@ opencode'u yeniden başlatın. Hepsi bu — kurulum global'dir, her proje dizini Repo git'te kalır, yapılandırma dizininde symlink bulunur. Repoda bir dosyayı düzenlediğinizde etkisi anında olur. -Kurulum betiği `opencode.json` dosyasını hiçbir zaman değiştirmez. `default_agent` `orchestrator` olarak ayarlı değilse önerilen snippet'i yazdırır, dosyayı olduğu gibi bırakır. +İki kurucu da `opencode.json` dosyasını hiçbir zaman değiştirmez. `default_agent` `orchestrator` olarak ayarlı değilse önerilen snippet'i yazdırır, dosyayı olduğu gibi bırakır. Windows'ta varsayılan dizin `%USERPROFILE%\.config\opencode` yoludur; iki kurucu da `OPENCODE_CONFIG_DIR` değişkenini kullanabilir. Kurulumu kontrol etmek veya kaldırmak için: @@ -109,6 +121,13 @@ Kurulumu kontrol etmek veya kaldırmak için: ./install.sh --uninstall ``` +Windows karşılıkları (aynı yönetici veya Geliştirici Modu oturumunda): + +```powershell +.\install.ps1 -Check +.\install.ps1 -Uninstall +``` + `--check`, bir symlink eksikse sıfır dışı kodla çıkar. Eksik `default_agent` ayarı hata değil, öneri olarak gösterilir. `--uninstall` yalnızca bu repoya işaret eden symlink'leri kaldırır. ### Manuel kurulum diff --git a/agents/orchestrator.md b/agents/orchestrator.md index c77ebf6..864a0da 100644 --- a/agents/orchestrator.md +++ b/agents/orchestrator.md @@ -80,7 +80,7 @@ Read `skills/team-memory/SKILL.md` before seeding, dispatching or compacting mem 1. Confirm no subagents are running. 2. Read the logs changed during the stage. 3. Promote durable facts to `INDEX.md`; create a decision file when the stage made a real decision. -4. Run the installed `~/.config/opencode/skills/team-memory/memory-compact.sh` from the project root to archive old log entries (over 40) and compact `INDEX.md` (over 60 lines). It reads `$PWD/.team/memory` by default. +4. Run the installed memory compactor from the project root to archive old log entries (over 40) and compact `INDEX.md` (over 60 lines). Resolve the global config root from `OPENCODE_CONFIG_DIR`, otherwise `~/.config/opencode`. On native Windows invoke `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "/skills/team-memory/memory-compact.ps1"`; on macOS/Linux invoke `/skills/team-memory/memory-compact.sh`. Both default to the current project's `.team/memory`; when `memory_dir` is customized, set `MEMORY_DIR` to that project-resolved path for the helper. See the memory skill for shell-specific examples. 5. Write the stage transition to `log/orchestrator.md`. 6. Update `stage` in `INDEX.md` when the user approves the gate and the next stage starts. diff --git a/docs/faq.md b/docs/faq.md index 39333b4..c059aba 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -12,9 +12,11 @@ cd opencode-team Restart opencode after installation. +On native Windows, use an Administrator PowerShell and run `powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\install.ps1` instead of `./install.sh`. See the [Windows guide](install.md#windows). + ### Does it work on Windows? -Not natively yet. macOS and Linux are supported. `install.sh` is Bash and creates symlinks; native Windows needs a separate installer and CI coverage. WSL is untested, so treat it as experimental. +Yes. Use `install.ps1` with Windows PowerShell 5.1 or PowerShell 7. Symbolic links require an Administrator PowerShell, or PowerShell 7 with Windows Developer Mode enabled. Configuration defaults to `%USERPROFILE%\.config\opencode` and supports `OPENCODE_CONFIG_DIR`. Installation and memory maintenance have native PowerShell tests and Windows CI coverage. WSL is untested. See [Windows installation](install.md#windows). ### Do I need OpenCode 1.x or 2.x? @@ -28,6 +30,8 @@ OpenCode Team is tested on OpenCode 1.x (1.19.0). OpenCode 2 support is planned This removes only symlinks that point into this repo. `opencode.json` is never modified. +On Windows, use `.\install.ps1 -Uninstall`; `.\install.ps1 -Check` verifies the installation. + ## Memory ### Where is memory stored? diff --git a/docs/install.md b/docs/install.md index 0f019dd..0584a99 100644 --- a/docs/install.md +++ b/docs/install.md @@ -4,7 +4,7 @@ Install OpenCode Team into your OpenCode configuration so you can use it in any ## Requirements -- [OpenCode](https://opencode.ai) 1.x (tested on 1.19.0) +- [OpenCode](https://opencode.ai) 1.x (tested on 1.19.0; native Windows verified on 1.19.4) - An OpenCode Zen account or any provider configured in opencode — the defaults use free-tier models ## Platform support @@ -13,13 +13,15 @@ Install OpenCode Team into your OpenCode configuration so you can use it in any |----------|--------| | macOS | Supported | | Linux | Supported | -| Windows (native) | Not supported yet | +| Windows (native) | Supported: Windows PowerShell 5.1 and PowerShell 7 | | Windows (WSL) | Untested | -`install.sh` is Bash and creates symlinks. Native Windows needs a separate installer and CI coverage; neither exists yet. +Use `install.sh` on macOS/Linux and `install.ps1` on native Windows. Both create symbolic links to the repository. ## Install +### macOS and Linux + Clone the repository and run the installer: ```bash @@ -38,9 +40,52 @@ If OpenCode is not on your PATH, install it first: Restart opencode after installation. The setup is global, so it works in every project directory. +### Windows + +Open an **Administrator PowerShell**, then run: + +```powershell +git clone https://github.com/sametcelikbicak/opencode-team.git +cd opencode-team +powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\install.ps1 +``` + +Windows PowerShell 5.1 is included with Windows. PowerShell 7 (`pwsh`) is also supported; with Windows Developer Mode enabled, it can create symbolic links without elevation. The installer never elevates itself or changes Developer Mode. Without link privileges, it stops before changing installation links and explains how to proceed. + +`-ExecutionPolicy Bypass` applies only to the new PowerShell process. It does not change the machine's execution policy; an organization policy can still restrict script execution. In a shell that already permits scripts, `.\install.ps1` is sufficient. + +OpenCode 1.x must already be on PATH. To explicitly install it when missing or incompatible, use: + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\install.ps1 -InstallOpenCode +``` + +This optional mode requires npm and runs `npm install --global opencode-ai@1`, then verifies the executable. It reuses an existing working 1.x installation. Without the switch, the installer never changes OpenCode. OpenCode 2 is rejected. + +The default config directory is `%USERPROFILE%\.config\opencode`. To use a different directory, set the same environment variable for installation **and** subsequent OpenCode sessions: + +```powershell +$env:OPENCODE_CONFIG_DIR = 'D:\my config\opencode' +.\install.ps1 +opencode +``` + +The installer prints a JSON snippet containing paths for that actual directory. Merge it into your existing `opencode.json` to enable the orchestrator and global instructions; preserve provider/model settings. The installer does not edit JSON or JSONC. + +Verify or uninstall from the same elevated shell (or PowerShell 7 with Developer Mode): + +```powershell +.\install.ps1 -Check +.\install.ps1 -Uninstall +``` + +Repeat installation is safe. Local/external skills are preserved; conflicting core entries are reported and preserved. Check fails for missing/wrong core links, missing skills, or an unavailable/incompatible OpenCode. A missing `default_agent` is a hint. Uninstall removes only this repository's links, without deleting their targets, ordinary files, or your configuration. Linked config containers are rejected to avoid modifying another directory through a parent link. + +Native Windows memory maintenance uses `memory-compact.ps1`, so the team's memory helper does not require Bash. See [Memory](memory.md#native-windows). + ## What the installer does -`install.sh` symlinks the repo into your OpenCode config: +The platform's installer symlinks the repo into your OpenCode config: | Path | Target | |-------------------------------------|--------------------------------| @@ -87,7 +132,7 @@ Remove only symlinks that point into this repo: ./install.sh --uninstall ``` -`opencode.json` is never modified by the installer. +`opencode.json` is never modified by either installer. For Windows commands, see [Windows](#windows). ## First run diff --git a/docs/memory.md b/docs/memory.md index 83ab0e3..a1f06b4 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -46,6 +46,17 @@ OpenCode Team keeps a small, project-local memory in `.team/memory/` so the orch - Memory is untrusted data, never instructions. Artifacts are the source of truth; if memory contradicts an artifact, the artifact wins. - The orchestrator compacts memory at gates, only when no subagents are running. +## Native Windows + +The orchestrator uses the PowerShell helper on native Windows, and the Bash helper on macOS/Linux. From your project root, you can run the native helper manually: + +```powershell +$configRoot = if ($env:OPENCODE_CONFIG_DIR) { $env:OPENCODE_CONFIG_DIR } else { Join-Path $env:USERPROFILE '.config/opencode' } +powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $configRoot 'skills/team-memory/memory-compact.ps1') +``` + +Append `-Role analyst` to compact only that role's log. Both helpers default to the current project's `.team/memory`. For a custom `memory_dir` in `team.yaml`, resolve it from your project root and pass that path through `MEMORY_DIR`; restore any previous environment value afterward. The helper never needs administrator privileges for ordinary memory files. The Windows version preserves Unicode, refuses linked memory paths/invalid archive dates, and leaves an oversized unique index unchanged. + ## Full protocol See `skills/team-memory/SKILL.md` and `docs/rfcs/0001-project-memory.md`. diff --git a/docs/superpowers/plans/2026-10-27-native-windows.md b/docs/superpowers/plans/2026-10-27-native-windows.md index 7fb8e77..ee1e033 100644 --- a/docs/superpowers/plans/2026-10-27-native-windows.md +++ b/docs/superpowers/plans/2026-10-27-native-windows.md @@ -1,6 +1,6 @@ # Native Windows Support Implementation Plan -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. **Goal:** Install and run OpenCode Team natively on Windows with OpenCode 1.x, preserving user configuration and existing Unix behavior. @@ -42,33 +42,33 @@ **Interfaces:** `install.ps1 [-Check | -Uninstall] [-InstallOpenCode]` exits zero on success; nonzero on errors. Environment: `OPENCODE_CONFIG_DIR`, normal command lookup for `opencode` and `npm`. `-InstallOpenCode` is valid only for install. Test runner uses the current PowerShell executable for child processes. -- [ ] Write `scripts/smoke-install.ps1` with fake `opencode.cmd` returning `1.19.4`, disposable configs, and assertions that install creates the exact links, reinstall succeeds, check succeeds, uninstall preserves repository targets, and check then fails. Run before `install.ps1` exists and confirm the expected missing-script failure. -- [ ] Implement `install.ps1` using `[CmdletBinding(DefaultParameterSetName='Install')]`, switch parameters, strict error handling, normalized paths, and explicit native exit-code handling. Keep filesystem helpers private to the script; use .NET `Directory.Delete(path)` / `File.Delete(path)` for link-only deletion. -- [ ] Extend the smoke tests for unchanged JSON/JSONC, missing settings as a hint, spaces/Unicode, invocation from another working directory, and a custom config directory. -- [ ] Add collision fixtures: local skills survive and check succeeds; core local entries fail unchanged; internal wrong links are repaired; relative external/dangling links and prefix-sibling links survive; linked config child containers cannot redirect writes or uninstall. -- [ ] Add version/bootstrap fixtures: missing/unparseable/2.x/failing executable fail install/check; uninstall works without OpenCode; existing 1.x skips npm; explicit bootstrap calls `npm install --global opencode-ai@1`; missing/failing npm and failed post-install verification return nonzero. Invalid mode combinations fail. -- [ ] Implement link preflight, cleanup probes, readable actionable errors, accurate config snippet paths, and the explicit npm bootstrap needed by those fixtures. Run all installer tests under both shells; verify fixture cleanup never traverses link targets. -- [ ] Commit installer and tests: `feat: add native Windows installer`. +- [x] Write `scripts/smoke-install.ps1` with fake `opencode.cmd` returning `1.19.4`, disposable configs, and assertions that install creates the exact links, reinstall succeeds, check succeeds, uninstall preserves repository targets, and check then fails. Run before `install.ps1` exists and confirm the expected missing-script failure. +- [x] Implement `install.ps1` using `[CmdletBinding(DefaultParameterSetName='Install')]`, switch parameters, strict error handling, normalized paths, and explicit native exit-code handling. Keep filesystem helpers private to the script; use .NET `Directory.Delete(path)` / `File.Delete(path)` for link-only deletion. +- [x] Extend the smoke tests for unchanged JSON/JSONC, missing settings as a hint, spaces/Unicode, invocation from another working directory, and a custom config directory. +- [x] Add collision fixtures: local skills survive and check succeeds; core local entries fail unchanged; internal wrong links are repaired; relative external/dangling links and prefix-sibling links survive; linked config child containers cannot redirect writes or uninstall. +- [x] Add version/bootstrap fixtures: missing/unparseable/2.x/failing executable fail install/check; uninstall works without OpenCode; existing 1.x skips npm; explicit bootstrap calls `npm install --global opencode-ai@1`; missing/failing npm and failed post-install verification return nonzero. Invalid mode combinations fail. +- [x] Implement link preflight, cleanup probes, readable actionable errors, accurate config snippet paths, and the explicit npm bootstrap needed by those fixtures. Run all installer tests under both shells; verify fixture cleanup never traverses link targets. +- [x] Commit installer and tests: `feat: add native Windows installer`. ### Task 2: Native memory helper and regression tests **Interfaces:** `memory-compact.ps1 [[-Role] ]` exits zero for successful/no-op compaction, nonzero for unsafe input or unique indexes above 60 lines. Environment: `MEMORY_DIR` or `$PWD/.team/memory`. With no role, process the same 14 roles as the Bash helper, then index; with a role, process only it, then index. -- [ ] Write `scripts/test-memory-compact.ps1` using the current shell in child processes. Assert an empty log succeeds, 40 entries remain unchanged, and 45 entries yield exactly 35 archived + 10 retained. Run before the helper exists and confirm the expected failure. -- [ ] Implement `skills/team-memory/memory-compact.ps1`: UTF-8 reads/writes, stable exact comparisons, monthly append archives, unique headers, validation before writes, and safe temporary replacements. Index threshold is 60 lines; exact duplicate removal preserves first occurrence; unique overflow fails with unchanged index. -- [ ] Add tests for repeated compaction (70 archive entries and one header), multiple months, Unicode/case-sensitive facts, 60/61-line indexes, role filtering, default CWD, custom memory path, malformed dates, unsafe role paths, and linked paths that escape the memory directory. Add no-trailing-newline and CRLF inputs. -- [ ] Run native memory tests under both shells and existing Bash memory regression tests. Confirm retained/archive entries match the intended ordering and failure fixtures preserve data. -- [ ] Commit helper and tests: `feat: support memory compaction in PowerShell`. +- [x] Write `scripts/test-memory-compact.ps1` using the current shell in child processes. Assert an empty log succeeds, 40 entries remain unchanged, and 45 entries yield exactly 35 archived + 10 retained. Run before the helper exists and confirm the expected failure. +- [x] Implement `skills/team-memory/memory-compact.ps1`: UTF-8 reads/writes, stable exact comparisons, monthly append archives, unique headers, validation before writes, and safe temporary replacements. Index threshold is 60 lines; exact duplicate removal preserves first occurrence; unique overflow fails with unchanged index. +- [x] Add tests for repeated compaction (70 archive entries and one header), multiple months, Unicode/case-sensitive facts, 60/61-line indexes, role filtering, default CWD, custom memory path, malformed dates, unsafe role paths, and linked paths that escape the memory directory. Add no-trailing-newline and CRLF inputs. +- [x] Run native memory tests under both shells and existing Bash memory regression tests. Confirm retained/archive entries match the intended ordering and failure fixtures preserve data. +- [x] Commit helper and tests: `feat: support memory compaction in PowerShell`. ### Task 3: Runtime instructions, docs, and Windows CI **Interfaces:** Windows invocation uses `powershell.exe -NoProfile -ExecutionPolicy Bypass -File ` from the target project root, passing `MEMORY_DIR` when configured. Unix invocation remains the existing `.sh`. Config-root precedence is `OPENCODE_CONFIG_DIR`, then the documented default. -- [ ] Update orchestrator and memory skill instructions with concrete native Windows helper invocation and custom config/memory behavior. Update `team/AGENTS.md` installation/config precedence without changing pipeline gates or role behavior. -- [ ] Add a `windows-latest` CI job with a shell matrix (`powershell`, `pwsh`) running `scripts/smoke-install.ps1` and `scripts/test-memory-compact.ps1`. Keep existing jobs and pinned checkout action. -- [ ] Update `README.md`, `README.tr.md`, `docs/install.md`, `docs/faq.md`, `CONTRIBUTING.md`, and `docs/memory.md` with native commands, supported shells, Developer Mode/elevation, execution-policy workaround, custom config, optional npm bootstrap, and user-merged JSON snippet. Do not suggest changing machine policy. -- [ ] Regenerate `docs/reference/*` using the existing Ruby generator. Validate manifests, Bash syntax, existing install/memory tests, reference freshness, and VitePress docs build. If Ruby is absent, locate an available/portable runtime without changing machine installation; report any genuine limits. -- [ ] Commit runtime instructions, docs and CI: `docs: enable and document native Windows support`. +- [x] Update orchestrator and memory skill instructions with concrete native Windows helper invocation and custom config/memory behavior. Update `team/AGENTS.md` installation/config precedence without changing pipeline gates or role behavior. +- [x] Add a `windows-latest` CI job with a shell matrix (`powershell`, `pwsh`) running `scripts/smoke-install.ps1` and `scripts/test-memory-compact.ps1`. Keep existing jobs and pinned checkout action. +- [x] Update `README.md`, `README.tr.md`, `docs/install.md`, `docs/faq.md`, `CONTRIBUTING.md`, and `docs/memory.md` with native commands, supported shells, Developer Mode/elevation, execution-policy workaround, custom config, optional npm bootstrap, and user-merged JSON snippet. Do not suggest changing machine policy. +- [x] Regenerate `docs/reference/*` using the existing Ruby generator. Validate manifests, Bash syntax, existing install/memory tests, reference freshness, and VitePress docs build. If Ruby is absent, locate an available/portable runtime without changing machine installation; report any genuine limits. +- [x] Commit runtime instructions, docs and CI: `docs: enable and document native Windows support`. ### Task 4: Actual binary verification and final review diff --git a/skills/team-memory/SKILL.md b/skills/team-memory/SKILL.md index 0c46db8..dcaf38c 100644 --- a/skills/team-memory/SKILL.md +++ b/skills/team-memory/SKILL.md @@ -231,7 +231,16 @@ Example: ## Compaction -Run at a gate, with no subagents running. From the project root, the orchestrator runs the installed `~/.config/opencode/skills/team-memory/memory-compact.sh` for the mechanical parts (repo contributors can run `skills/team-memory/memory-compact.sh`). +Run at a gate, with no subagents running, from the target project root. Resolve the installed config root from `OPENCODE_CONFIG_DIR`, otherwise `~/.config/opencode`. Native Windows uses `skills/team-memory/memory-compact.ps1` under that config root; macOS/Linux use `skills/team-memory/memory-compact.sh`. Repo contributors can use the corresponding helper directly from this checkout. + +Native Windows, from a PowerShell session: + +```powershell +$configRoot = if ($env:OPENCODE_CONFIG_DIR) { $env:OPENCODE_CONFIG_DIR } else { Join-Path $env:USERPROFILE '.config/opencode' } +powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $configRoot 'skills/team-memory/memory-compact.ps1') +``` + +Both helpers default to the current project's `.team/memory`, not the installation repo. If `memory_dir` in the project/global `team.yaml` is customized, resolve it from the project root and pass it via `MEMORY_DIR` (PowerShell: `$env:MEMORY_DIR = ''`; Bash: `MEMORY_DIR='' `). Preserve/restore any previous environment value after invocation. An optional role argument restricts log compaction to that role while still checking the index. The Windows helper rejects linked memory paths and malformed archive dates instead of writing through them. - **Log over 40 entries:** the script moves old entries to `archive/-YYYY-MM.md` and keeps the last 10 entries. A compacted log is normally 10 entries; it grows again as new entries are appended. Do not pad it back to 40. The script does not promote facts; the orchestrator does that. - **What goes where:** the archive keeps the original line as the historical record; `INDEX.md` gets a concise fact, not a copy of the whole line. @@ -240,7 +249,7 @@ Run at a gate, with no subagents running. From the project root, the orchestrato - **Archive:** never read automatically; search only when needed. - **Another session:** do not compact while another session may be writing memory. The orchestrator cannot detect a direct invocation in another terminal. -`scripts/test-memory-compact.sh` covers the empty-log, repeated-archive, index-cap and default-project-path cases. +`scripts/test-memory-compact.sh` and the native Windows `scripts/test-memory-compact.ps1` cover empty logs, repeated archives, index caps and default project paths. The Windows suite also checks role filtering, Unicode, monthly archives and unsafe input/link protection. ## Content rules diff --git a/team/AGENTS.md b/team/AGENTS.md index 7cb166b..b8763b9 100644 --- a/team/AGENTS.md +++ b/team/AGENTS.md @@ -66,7 +66,7 @@ Every stage writes one markdown file into `.team/`: ## Precedence 1. `./team.yaml` in the project root (project-specific override) -2. `~/.config/opencode/team.yaml` (global default) +2. `$OPENCODE_CONFIG_DIR/team.yaml` when configured; otherwise `~/.config/opencode/team.yaml` (global default, including native Windows) ## Conventions @@ -79,4 +79,6 @@ Every stage writes one markdown file into `.team/`: `./install.sh` links the global directories to this repo. It never modifies `~/.config/opencode/opencode.json`; when the recommended settings are missing it prints the snippet to add. `./install.sh --install-opencode` optionally installs OpenCode 1.x first. +On native Windows, use `.\install.ps1` with Windows PowerShell 5.1 or PowerShell 7. Run an Administrator PowerShell, or use PowerShell 7 with Developer Mode enabled for symbolic-link privileges. `-Check`, `-Uninstall`, and `-InstallOpenCode` are the Windows switches. The config directory is `%USERPROFILE%\.config\opencode` unless `OPENCODE_CONFIG_DIR` is set. Neither installer edits `opencode.json`. + All stage playbooks are local under `skills/team-*` and referenced by `team.yaml`; no external skill is required. `team-memory` is a support skill, not a pipeline stage. `team-build`, `team-backlog` and `team-brand` are adapted from MIT-licensed upstream skills — see `THIRD_PARTY_NOTICES.md`. From dcf510d11f63747bfbea74dd25a78bb3b36e1812 Mon Sep 17 00:00:00 2001 From: Onur Date: Tue, 27 Oct 2026 23:47:10 +0300 Subject: [PATCH 6/8] fix: resolve Windows overrides from the PowerShell location --- TESTING-WINDOWS.md | 40 +++++++++++++++++++ docs/install.md | 22 ++++++++++ .../plans/2026-10-27-native-windows.md | 8 ++-- install.ps1 | 1 + scripts/smoke-install.ps1 | 16 +++++++- scripts/test-memory-compact.ps1 | 22 +++++++++- skills/team-memory/memory-compact.ps1 | 1 + 7 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 TESTING-WINDOWS.md diff --git a/TESTING-WINDOWS.md b/TESTING-WINDOWS.md new file mode 100644 index 0000000..1f51316 --- /dev/null +++ b/TESTING-WINDOWS.md @@ -0,0 +1,40 @@ +# Native Windows verification + +Date: 2026-10-27 (Europe/Istanbul) + +Issue: #42 + +## Automated checks + +| Check | Result | +| --- | --- | +| `scripts/smoke-install.ps1`, Windows PowerShell 5.1 | Passed, 102 assertions | +| `scripts/smoke-install.ps1`, PowerShell 7.6.5 | Passed, 102 assertions | +| `scripts/test-memory-compact.ps1`, Windows PowerShell 5.1 | Passed, 38 assertions | +| `scripts/test-memory-compact.ps1`, PowerShell 7.6.5 | Passed, 38 assertions | +| `ruby scripts/validate.rb` | Passed, 47 files | +| `ruby scripts/generate-reference.rb` | Passed; generated reference unchanged | +| `bash -n install.sh scripts/*.sh skills/team-memory/*.sh` | Passed | +| Existing `scripts/smoke-install.sh` | Passed under elevated Git Bash with `MSYS=winsymlinks:nativestrict` | +| Existing `scripts/test-memory-compact.sh` | Passed | +| `npm run docs:build` | Passed | + +Native tests ran elevated because Developer Mode was disabled. They use disposable directories and fake OpenCode/npm commands; they never install packages or modify actual user configuration. Existing Unix scripts were also checked, but this is a Windows run, not a new macOS/Linux run. A Windows GitHub Actions job for both shells is configured; remote CI has not been run during this work. + +## Actual OpenCode 1.19.4 + +An isolated config/data environment using the real installed binary discovered all 14 team agents, 19 commands, and 13 local skills. A disposable `opencode.json` resolved `orchestrator` as the default and both team instruction paths. The PowerShell install/check succeeded. No provider/model calls were made. + +Installation into the actual user config also passed `-Check`. The preexisting `opencode.json` SHA256 was identical before/after. The installer printed the recommended settings instead of changing provider/model/default-agent settings. + +The existing user database had a v2 schema (`session_v2` and `session_message`, no `session`) and caused OpenCode 1.19.4 to fail before agent discovery. A process-local `XDG_DATA_HOME` pointing at a separate v1 data directory allowed the real user configuration to discover all 14 team agents. The original database was not reset or migrated. See [Windows database troubleshooting](docs/install.md#windows-database-is-not-empty-and-has-no-session-table). + +Provider authentication, model responses, and a full idea-to-launch pipeline were not exercised. Those depend on the user's configured provider and may incur usage. + +## Independent review + +A fresh reviewer found one important issue: relative `OPENCODE_CONFIG_DIR`/`MEMORY_DIR` values were resolved against the process-start directory instead of PowerShell's current location after `Set-Location`. Both regressions failed before the fix. Both helpers now resolve through the PowerShell filesystem provider before normalizing with .NET; the complete native suites passed after the fix. + +Review scope decisions: memory compaction still requires no concurrent writers; the existing single-line log format remains the contract; v2 session migration remains outside this Windows installer change. The separate-data-directory workaround preserves those v2 sessions rather than converting them. + +The process-only `OPENCODE_CONFIG_CONTENT` startup alternative was also verified with the user's actual config: `orchestrator` resolved as the default, both team instruction paths were present, and all 14 team agents were discovered, without editing `opencode.json`. diff --git a/docs/install.md b/docs/install.md index 0584a99..99b1338 100644 --- a/docs/install.md +++ b/docs/install.md @@ -72,6 +72,15 @@ opencode The installer prints a JSON snippet containing paths for that actual directory. Merge it into your existing `opencode.json` to enable the orchestrator and global instructions; preserve provider/model settings. The installer does not edit JSON or JSONC. +Alternatively, for the default config directory, you can supply the team settings only to the current terminal without editing your configuration file: + +```powershell +$env:OPENCODE_CONFIG_CONTENT = '{"default_agent":"orchestrator","instructions":["~/.config/opencode/team/AGENTS.md","~/.config/opencode/team.yaml"]}' +opencode +``` + +For a custom config directory, use the instruction paths printed by the installer. If you already use `OPENCODE_CONFIG_CONTENT`, merge these settings into its existing value instead of replacing it. + Verify or uninstall from the same elevated shell (or PowerShell 7 with Developer Mode): ```powershell @@ -146,6 +155,19 @@ The orchestrator will start the pipeline and ask for your approval at each gate. ## Troubleshooting +### Windows: `Database is not empty and has no session table` + +This can occur when OpenCode 1.x opens a data directory previously migrated by OpenCode 2. It is separate from the team installer: a v2 database has `session_v2` instead of the v1 `session` table. See the [upstream report](https://github.com/anomalyco/opencode/issues/56764). + +Keep that database and use a separate data directory for your 1.x session: + +```powershell +$env:XDG_DATA_HOME = Join-Path $env:USERPROFILE '.local/share/opencode-v1' +opencode --agent orchestrator +``` + +This environment setting applies to the current terminal and its child processes. It preserves the original database and uses fresh v1 session/authentication data; connect your provider in the new environment if needed. The global team configuration remains in `%USERPROFILE%\.config\opencode` (or `OPENCODE_CONFIG_DIR`). It does not migrate v2 sessions. Do not delete the old database to fix this error. + ### `opencode: command not found` The installer only links files when OpenCode is already on your PATH. To install OpenCode 1.x first: diff --git a/docs/superpowers/plans/2026-10-27-native-windows.md b/docs/superpowers/plans/2026-10-27-native-windows.md index ee1e033..182e9fa 100644 --- a/docs/superpowers/plans/2026-10-27-native-windows.md +++ b/docs/superpowers/plans/2026-10-27-native-windows.md @@ -74,7 +74,7 @@ **Interfaces:** Use installed OpenCode `1.19.4`; discovery only (`opencode agent list`, `opencode debug ...`), with no provider calls. Do not print provider credentials or whole user configuration. -- [ ] Install into a temporary isolated config with the actual binary and verify the 14 team agents, 19 commands, and 13 team skills are discovered using bounded output/parsed JSON. Verify default-agent/instruction resolution with a disposable `opencode.json`. -- [ ] Inspect only paths/collisions in the actual user config, run the installer there when permitted, and run `-Check`. Preserve all existing entries; never edit user JSON. Report any necessary snippet or collision without overwriting it. -- [ ] Use `requesting-code-review` for a fresh whole-branch review. Correct actionable issues and rerun the tests affected by fixes. -- [ ] Use `verification-before-completion`, inspect the final diff/status, and record exactly which checks passed or could not run. Preserve implementation in the task branch and show the runnable Windows command, any remaining manual config step, and verification limits. Do not publish or merge without a request. +- [x] Install into a temporary isolated config with the actual binary and verify the 14 team agents, 19 commands, and 13 team skills are discovered using bounded output/parsed JSON. Verify default-agent/instruction resolution with a disposable `opencode.json`. +- [x] Inspect only paths/collisions in the actual user config, run the installer there when permitted, and run `-Check`. Preserve all existing entries; never edit user JSON. Report any necessary snippet or collision without overwriting it. +- [x] Use `requesting-code-review` for a fresh whole-branch review. Correct actionable issues and rerun the tests affected by fixes. +- [x] Use `verification-before-completion`, inspect the final diff/status, and record exactly which checks passed or could not run. Preserve implementation in the task branch and show the runnable Windows command, any remaining manual config step, and verification limits. Do not publish or merge without a request. diff --git a/install.ps1 b/install.ps1 index 45ba1c1..daa296d 100644 --- a/install.ps1 +++ b/install.ps1 @@ -18,6 +18,7 @@ $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest $repo = [IO.Path]::GetFullPath($PSScriptRoot).TrimEnd('\', '/') $config = if ($env:OPENCODE_CONFIG_DIR) { $env:OPENCODE_CONFIG_DIR } else { Join-Path $env:USERPROFILE '.config/opencode' } +$config = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($config) $config = [IO.Path]::GetFullPath($config).TrimEnd('\', '/') $core = @('agents', 'commands', 'team.yaml', 'team/AGENTS.md') $skills = @(Get-ChildItem -LiteralPath (Join-Path $repo 'skills') -Directory | Where-Object { $_.Name -like 'team-*' }) diff --git a/scripts/smoke-install.ps1 b/scripts/smoke-install.ps1 index a0e2b7d..1c99cbd 100644 --- a/scripts/smoke-install.ps1 +++ b/scripts/smoke-install.ps1 @@ -29,11 +29,16 @@ function Remove-Fixture([string]$Path) { [IO.Directory]::Delete($Path) } else { [IO.File]::Delete($Path) } } -function Run-Installer([string]$Config, [string[]]$Arguments = @(), [bool]$Success = $true) { +function Run-Installer([string]$Config, [string[]]$Arguments = @(), [bool]$Success = $true, [string]$StartDirectory = '') { $env:OPENCODE_CONFIG_DIR = $Config $start = New-Object Diagnostics.ProcessStartInfo $start.FileName = $shell $start.Arguments = '-NoProfile -ExecutionPolicy Bypass -File "' + $installer + '" ' + ($Arguments -join ' ') + if ($StartDirectory) { + $start.WorkingDirectory = $StartDirectory + $command = "Set-Location -LiteralPath '" + (Get-Location).Path.Replace("'", "''") + "'; `$global:LASTEXITCODE = 0; & '" + $installer.Replace("'", "''") + "' " + ($Arguments -join ' ') + '; exit $LASTEXITCODE' + $start.Arguments = '-NoProfile -ExecutionPolicy Bypass -EncodedCommand ' + [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command)) + } $start.UseShellExecute = $false $start.RedirectStandardOutput = $true $start.RedirectStandardError = $true @@ -201,6 +206,15 @@ try { [IO.Directory]::Delete($skillTarget) Run-Installer $jsoncConfig @('-Check') $false | Out-Null + Write-Host 'relative config override after Set-Location' + $relativeRoot = Join-Path $work 'changed-location' + $relativeProject = Join-Path $relativeRoot 'project' + Write-Text (Join-Path $relativeRoot 'config/skills') 'outside marker' + [IO.Directory]::CreateDirectory((Join-Path $relativeProject 'config')) | Out-Null + Push-Location $relativeProject + try { Run-Installer 'config' @('-Uninstall') $true $relativeRoot | Out-Null } finally { Pop-Location } + Assert ([IO.File]::ReadAllText((Join-Path $relativeRoot 'config/skills')) -eq 'outside marker') 'relative override must not use process-start config' + Write-Host "installer smoke: OK ($script:assertions assertions)" } finally { $env:PATH = $oldPath diff --git a/scripts/test-memory-compact.ps1 b/scripts/test-memory-compact.ps1 index 8724f8b..b4f15d2 100644 --- a/scripts/test-memory-compact.ps1 +++ b/scripts/test-memory-compact.ps1 @@ -19,13 +19,20 @@ function Write-Lines([string]$Path, [string[]]$Lines) { function Entries([int]$Start, [int]$End, [string]$Month = '2026-10') { for ($i = $Start; $i -le $End; $i++) { '- {0}-{1:00} | research | fact | entry {2} | artifact' -f $Month, (($i - 1) % 28 + 1), $i } } -function Run-Compactor([string]$Memory, [string]$Role = '', [bool]$Success = $true) { +function Run-Compactor([string]$Memory, [string]$Role = '', [bool]$Success = $true, [string]$StartDirectory = '') { $env:MEMORY_DIR = $Memory $start = New-Object Diagnostics.ProcessStartInfo $start.FileName = $shell $start.WorkingDirectory = (Get-Location).Path $start.Arguments = '-NoProfile -ExecutionPolicy Bypass -File "' + $helper + '"' if ($Role) { $start.Arguments += ' -Role "' + $Role + '"' } + if ($StartDirectory) { + $start.WorkingDirectory = $StartDirectory + $command = "Set-Location -LiteralPath '" + (Get-Location).Path.Replace("'", "''") + "'; `$global:LASTEXITCODE = 0; & '" + $helper.Replace("'", "''") + "'" + if ($Role) { $command += " -Role '" + $Role.Replace("'", "''") + "'" } + $command += '; exit $LASTEXITCODE' + $start.Arguments = '-NoProfile -ExecutionPolicy Bypass -EncodedCommand ' + [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command)) + } $start.UseShellExecute = $false $start.RedirectStandardOutput = $true $start.RedirectStandardError = $true @@ -119,6 +126,19 @@ try { Run-Compactor $memory 'researcher' Assert ((Count-Entries $customLog) -eq 10) 'explicit safe custom roles must work like the Bash helper' + Write-Host 'relative memory override after Set-Location' + $relativeRoot = Join-Path $work 'changed-location' + $relativeProject = Join-Path $relativeRoot 'project' + $outsideIndex = Join-Path $relativeRoot 'memory/INDEX.md' + Write-Lines $outsideIndex ([string[]](1..61 | ForEach-Object { "outside fact $_" })) + $relativeLog = Join-Path $relativeProject 'memory/log/analyst.md' + Write-Lines $relativeLog (@('# analyst log', '') + @(Entries 1 41)) + Write-Lines (Join-Path $relativeProject 'memory/INDEX.md') @('current project') + Push-Location $relativeProject + try { Run-Compactor 'memory' '' $true $relativeRoot } finally { Pop-Location } + Assert ((Count-Entries $relativeLog) -eq 10) 'relative memory must use current PowerShell location' + Assert ([IO.File]::ReadAllLines($outsideIndex).Count -eq 61) 'process-start memory must remain untouched' + Write-Host 'unsafe role/date and linked memory protection' Run-Compactor $memory '../outside' $false $badLines = @('# analyst log', '') + @(Entries 1 41) diff --git a/skills/team-memory/memory-compact.ps1 b/skills/team-memory/memory-compact.ps1 index 6b0d505..d9dc0b5 100644 --- a/skills/team-memory/memory-compact.ps1 +++ b/skills/team-memory/memory-compact.ps1 @@ -6,6 +6,7 @@ $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest $roles = @('analyst','pm','po','ux','brand','architect','fe','be','qa','security','docs','devops','marketing','orchestrator') $memory = if ($env:MEMORY_DIR) { $env:MEMORY_DIR } else { Join-Path (Get-Location).Path '.team/memory' } +$memory = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($memory) $memory = [IO.Path]::GetFullPath($memory).TrimEnd('\', '/') $utf8 = New-Object Text.UTF8Encoding($false) From 653050c65a4deca3bbb6158a1f810e908f15063f Mon Sep 17 00:00:00 2001 From: Onur Date: Wed, 28 Oct 2026 01:01:14 +0300 Subject: [PATCH 7/8] chore: remove local planning documents --- .../plans/2026-10-27-native-windows.md | 80 ------------------- .../specs/2026-10-27-native-windows-design.md | 51 ------------ 2 files changed, 131 deletions(-) delete mode 100644 docs/superpowers/plans/2026-10-27-native-windows.md delete mode 100644 docs/superpowers/specs/2026-10-27-native-windows-design.md diff --git a/docs/superpowers/plans/2026-10-27-native-windows.md b/docs/superpowers/plans/2026-10-27-native-windows.md deleted file mode 100644 index 182e9fa..0000000 --- a/docs/superpowers/plans/2026-10-27-native-windows.md +++ /dev/null @@ -1,80 +0,0 @@ -# Native Windows Support Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. - -**Goal:** Install and run OpenCode Team natively on Windows with OpenCode 1.x, preserving user configuration and existing Unix behavior. - -**Architecture:** A standalone PowerShell installer manages symbolic links and version checks. A separate PowerShell memory helper mirrors the Bash helper. Dependency-free subprocess smoke tests exercise each public script; Windows CI runs them under Windows PowerShell 5.1 and PowerShell 7. - -**Tech Stack:** PowerShell 5.1/7, .NET filesystem APIs, GitHub Actions, existing Bash/Ruby/VitePress checks. - -**Spec:** `docs/superpowers/specs/2026-10-27-native-windows-design.md` (approved by the user). - -## Global Constraints - -- Support Windows PowerShell 5.1 and PowerShell 7. -- Require a successful, parseable OpenCode 1.x version for install and check. -- Never modify `opencode.json` (including JSONC). -- No new test framework, automatic elevation, machine setting changes, or paid agent executions. -- Use `OPENCODE_CONFIG_DIR` when set, otherwise `%USERPROFILE%\.config\opencode`. -- Use real symbolic links; do not add a copy, junction, or hard-link fallback. -- Preserve Bash behavior, user entries, memory entry order, and Unicode. - -## Review Focus - -1. Linked parent containers must not redirect writes/deletes into unrelated user directories (installer negative fixtures). -2. Relative link targets, case differences, and a repository-prefix sibling must not confuse link ownership (installer ownership fixtures). -3. Native-command failures must propagate even on Windows PowerShell 5.1 (version/npm failure fixtures). -4. Oversized unique indexes and malformed archive dates must preserve existing memory (memory failure fixtures). -5. Paths with spaces/Unicode and an alternate caller working directory must work without changing the target project (both script fixtures). - -## File Responsibilities - -- `install.ps1`: public install/check/uninstall/version/bootstrap interface and safe link management. -- `scripts/smoke-install.ps1`: disposable fixtures, fake commands, subprocess execution, installer assertions. -- `skills/team-memory/memory-compact.ps1`: native memory maintenance. -- `scripts/test-memory-compact.ps1`: native memory behavior and data-preservation assertions. -- `.github/workflows/checks.yml`: Windows PowerShell matrix alongside existing Unix checks. -- READMEs, install/FAQ/contribution/memory guides, `team/AGENTS.md`, `agents/orchestrator.md`, `skills/team-memory/SKILL.md`: supported commands, constraints, and runtime helper selection. -- `docs/reference/*`: regenerated from source after instruction changes. - -### Task 1: PowerShell installer and lifecycle tests - -**Interfaces:** `install.ps1 [-Check | -Uninstall] [-InstallOpenCode]` exits zero on success; nonzero on errors. Environment: `OPENCODE_CONFIG_DIR`, normal command lookup for `opencode` and `npm`. `-InstallOpenCode` is valid only for install. Test runner uses the current PowerShell executable for child processes. - -- [x] Write `scripts/smoke-install.ps1` with fake `opencode.cmd` returning `1.19.4`, disposable configs, and assertions that install creates the exact links, reinstall succeeds, check succeeds, uninstall preserves repository targets, and check then fails. Run before `install.ps1` exists and confirm the expected missing-script failure. -- [x] Implement `install.ps1` using `[CmdletBinding(DefaultParameterSetName='Install')]`, switch parameters, strict error handling, normalized paths, and explicit native exit-code handling. Keep filesystem helpers private to the script; use .NET `Directory.Delete(path)` / `File.Delete(path)` for link-only deletion. -- [x] Extend the smoke tests for unchanged JSON/JSONC, missing settings as a hint, spaces/Unicode, invocation from another working directory, and a custom config directory. -- [x] Add collision fixtures: local skills survive and check succeeds; core local entries fail unchanged; internal wrong links are repaired; relative external/dangling links and prefix-sibling links survive; linked config child containers cannot redirect writes or uninstall. -- [x] Add version/bootstrap fixtures: missing/unparseable/2.x/failing executable fail install/check; uninstall works without OpenCode; existing 1.x skips npm; explicit bootstrap calls `npm install --global opencode-ai@1`; missing/failing npm and failed post-install verification return nonzero. Invalid mode combinations fail. -- [x] Implement link preflight, cleanup probes, readable actionable errors, accurate config snippet paths, and the explicit npm bootstrap needed by those fixtures. Run all installer tests under both shells; verify fixture cleanup never traverses link targets. -- [x] Commit installer and tests: `feat: add native Windows installer`. - -### Task 2: Native memory helper and regression tests - -**Interfaces:** `memory-compact.ps1 [[-Role] ]` exits zero for successful/no-op compaction, nonzero for unsafe input or unique indexes above 60 lines. Environment: `MEMORY_DIR` or `$PWD/.team/memory`. With no role, process the same 14 roles as the Bash helper, then index; with a role, process only it, then index. - -- [x] Write `scripts/test-memory-compact.ps1` using the current shell in child processes. Assert an empty log succeeds, 40 entries remain unchanged, and 45 entries yield exactly 35 archived + 10 retained. Run before the helper exists and confirm the expected failure. -- [x] Implement `skills/team-memory/memory-compact.ps1`: UTF-8 reads/writes, stable exact comparisons, monthly append archives, unique headers, validation before writes, and safe temporary replacements. Index threshold is 60 lines; exact duplicate removal preserves first occurrence; unique overflow fails with unchanged index. -- [x] Add tests for repeated compaction (70 archive entries and one header), multiple months, Unicode/case-sensitive facts, 60/61-line indexes, role filtering, default CWD, custom memory path, malformed dates, unsafe role paths, and linked paths that escape the memory directory. Add no-trailing-newline and CRLF inputs. -- [x] Run native memory tests under both shells and existing Bash memory regression tests. Confirm retained/archive entries match the intended ordering and failure fixtures preserve data. -- [x] Commit helper and tests: `feat: support memory compaction in PowerShell`. - -### Task 3: Runtime instructions, docs, and Windows CI - -**Interfaces:** Windows invocation uses `powershell.exe -NoProfile -ExecutionPolicy Bypass -File ` from the target project root, passing `MEMORY_DIR` when configured. Unix invocation remains the existing `.sh`. Config-root precedence is `OPENCODE_CONFIG_DIR`, then the documented default. - -- [x] Update orchestrator and memory skill instructions with concrete native Windows helper invocation and custom config/memory behavior. Update `team/AGENTS.md` installation/config precedence without changing pipeline gates or role behavior. -- [x] Add a `windows-latest` CI job with a shell matrix (`powershell`, `pwsh`) running `scripts/smoke-install.ps1` and `scripts/test-memory-compact.ps1`. Keep existing jobs and pinned checkout action. -- [x] Update `README.md`, `README.tr.md`, `docs/install.md`, `docs/faq.md`, `CONTRIBUTING.md`, and `docs/memory.md` with native commands, supported shells, Developer Mode/elevation, execution-policy workaround, custom config, optional npm bootstrap, and user-merged JSON snippet. Do not suggest changing machine policy. -- [x] Regenerate `docs/reference/*` using the existing Ruby generator. Validate manifests, Bash syntax, existing install/memory tests, reference freshness, and VitePress docs build. If Ruby is absent, locate an available/portable runtime without changing machine installation; report any genuine limits. -- [x] Commit runtime instructions, docs and CI: `docs: enable and document native Windows support`. - -### Task 4: Actual binary verification and final review - -**Interfaces:** Use installed OpenCode `1.19.4`; discovery only (`opencode agent list`, `opencode debug ...`), with no provider calls. Do not print provider credentials or whole user configuration. - -- [x] Install into a temporary isolated config with the actual binary and verify the 14 team agents, 19 commands, and 13 team skills are discovered using bounded output/parsed JSON. Verify default-agent/instruction resolution with a disposable `opencode.json`. -- [x] Inspect only paths/collisions in the actual user config, run the installer there when permitted, and run `-Check`. Preserve all existing entries; never edit user JSON. Report any necessary snippet or collision without overwriting it. -- [x] Use `requesting-code-review` for a fresh whole-branch review. Correct actionable issues and rerun the tests affected by fixes. -- [x] Use `verification-before-completion`, inspect the final diff/status, and record exactly which checks passed or could not run. Preserve implementation in the task branch and show the runnable Windows command, any remaining manual config step, and verification limits. Do not publish or merge without a request. diff --git a/docs/superpowers/specs/2026-10-27-native-windows-design.md b/docs/superpowers/specs/2026-10-27-native-windows-design.md deleted file mode 100644 index e0d8d19..0000000 --- a/docs/superpowers/specs/2026-10-27-native-windows-design.md +++ /dev/null @@ -1,51 +0,0 @@ -# Native Windows support (issue #42) - -## Intent and decisions - -A Windows user with OpenCode 1.x must be able to install and use OpenCode Team from PowerShell. The user is blocked at the documented `./install.sh` step, not by a demonstrated OpenCode runtime failure. Their installed `opencode --version` was independently confirmed as `1.19.4`. - -The user accepts enabling Windows Developer Mode or running an elevated PowerShell to create symbolic links. Use real symbolic links, matching the existing Bash installer, so repository edits take effect without reinstalling. Do not add a copy, junction, or hard-link fallback. - -The following are proposed implementation choices, not additional user requirements: support Windows PowerShell 5.1 and PowerShell 7; add a PowerShell memory compactor because the orchestrator currently invokes a Bash script at every gate; use npm only for the explicitly requested optional OpenCode installation. These choices are subject to approval of this design. - -## Installer behavior - -Add `install.ps1` at the repository root with mutually exclusive `-Check` and `-Uninstall` modes, default installation, and an optional `-InstallOpenCode` switch for installation mode. Document commands that work from a freshly cloned repository, including a process-scoped execution-policy workaround when needed; never change machine execution policy or enable Developer Mode automatically. - -Use `OPENCODE_CONFIG_DIR` when set, otherwise `%USERPROFILE%\.config\opencode`. Resolve paths independently of the caller's working directory and handle spaces, Unicode and Windows path separators. Link `agents`, `commands`, `team.yaml`, `team/AGENTS.md`, and each repository-local `skills/team-*` directory to their corresponding repository targets. Keep the containing `skills` and `team` directories as real directories. - -Require a successful, parseable OpenCode 1.x version for install and check. An existing valid 1.x installation is reused. If installation is requested and OpenCode is missing or incompatible, run the officially documented npm distribution with a major-version constraint (`npm install --global opencode-ai@1`), check its exit status, and verify the resulting executable/version. Without that switch, never install or replace OpenCode. Explain missing npm, unsupported versions, and missing symlink privileges with actionable errors. Uninstall does not require OpenCode. - -Never modify `opencode.json` (including JSONC). Print the recommended `default_agent` and `instructions` snippet when needed, using paths for the actual config directory. The user merges that snippet into their own configuration; this work does not change provider or model settings. - -## Existing files and ownership - -Repeated installation succeeds when expected links already exist. A link pointing elsewhere inside this repository may be corrected. An ordinary existing core file/directory or a core link outside the repository causes a clear failure and is preserved. Existing local or externally managed `skills/team-*` entries are skipped and preserved, matching the Bash installer. - -Check validates that core links resolve to the exact expected targets, required targets exist, and required local skills are available; local/external skill collisions remain informational. A missing recommended JSON setting is informational, not a failed check. - -Uninstall removes only links managed by this repository, never their targets, external links, ordinary files, or user configuration. Determine ownership with normalized absolute paths and directory boundaries, including relative link targets and case-insensitive Windows comparisons. Reject unexpected linked parent containers before writing through them. Preflight predictable collisions and link-creation capability before changing installation entries, and clean up any preflight probe. No recursive deletion of installed directory links. - -## Native memory operation - -Add `skills/team-memory/memory-compact.ps1` with the existing script's contract: optional role; `MEMORY_DIR` override or current project's `.team/memory`; logs above 40 entries archive all but the newest 10 into monthly role archives; append without repeating archive headers. Remove exact duplicate index lines only when the index exceeds 60 lines. If it still exceeds 60 unique lines, fail without changing the index. Preserve Unicode and original entry order. Validate role/path-derived names before writes and keep writes within the selected memory directory. - -Update the orchestrator and memory skill to select the PowerShell helper on native Windows and the existing Bash helper on macOS/Linux. Respect the configured global config directory and the existing project memory-directory setting when describing invocation. Preserve the pipeline, roles, memory format, and human approval gates. - -## Tests and documentation - -Add dependency-free PowerShell smoke tests using disposable config directories and fake OpenCode/npm commands, with no network or real user configuration writes. Cover installation, repeat installation, check, uninstall, missing/bad versions, optional installation, unchanged JSON, custom config paths, spaces, Unicode, local skill collisions, in-repository relinking, external links and parent-container protection. Include memory tests for empty logs, thresholds, repeated/monthly archives, Unicode, current-project paths, custom memory directories, exact duplicate removal, and an unchanged oversized unique index. - -Add a `windows-latest` smoke job to `.github/workflows/checks.yml`; exercise the PowerShell installer and memory helper under the supported PowerShell versions. Keep Ubuntu/macOS jobs. Update both READMEs, install guide, FAQ, contribution instructions, and affected memory/runtime instructions. Regenerate reference pages if source manifests change. Describe symlink privileges, config paths, execution policy, and manual JSON configuration accurately. - -## Verification and completion - -Run the Windows smoke and memory tests locally under the available PowerShell versions. Use the actual OpenCode 1.19.4 binary with an isolated configuration to verify agent/config discovery without paid model requests. Run `ruby scripts/validate.rb`, Bash syntax checks and existing shell/memory smoke tests where their required runtimes are available, and build the documentation. Report any unavailable runtime/check explicitly instead of claiming it passed. A configured Windows CI job is not evidence of a completed remote CI run. - -After isolated tests, install into the user's actual configuration within the authorized scope and run the installer check, preserving existing entries and JSON. If local collisions or permissions prevent installation, retain all existing data and report the precise remaining step. Provide the concrete JSON snippet if the orchestrator default/instructions need manual configuration. - -## Scope and builder discretion - -No OpenCode 2 migration, WSL requirement, new test framework, provider authentication changes, paid agent executions, automatic elevation, machine setting changes, publishing, issue comments, or PR merging. Existing Bash behavior stays compatible. This is a Windows support change, not a rewrite of the pipeline or documentation toolchain. - -The builder chooses function organization, test fixture structure, safe .NET/PowerShell filesystem APIs, and the exact failure messages. Do not broaden scope solely to install contributor tools; use available or portable runtimes and document genuine verification limits. From 61003dda968a93888ee74b4e4d3abd4b8596c35e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samet=20=C3=87EL=C4=B0KBI=C3=87AK?= Date: Wed, 28 Oct 2026 11:35:58 +0300 Subject: [PATCH 8/8] ci: set Windows smoke shell from matrix via defaults.run --- .github/workflows/checks.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index a030393..add10ce 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -88,13 +88,14 @@ jobs: fail-fast: false matrix: shell: [powershell, pwsh] + defaults: + run: + shell: ${{ matrix.shell }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: install / check / uninstall - shell: ${{ matrix.shell }} run: ./scripts/smoke-install.ps1 - name: Native memory compaction - shell: ${{ matrix.shell }} run: ./scripts/test-memory-compact.ps1