feat: add openspec init --global for tool global directory installation#753
feat: add openspec init --global for tool global directory installation#753askpatrickw wants to merge 3 commits intoFission-AI:mainfrom
openspec init --global for tool global directory installation#753Conversation
📝 WalkthroughWalkthroughAdds global-install support: ToolCommandAdapter gains optional getGlobalRoot(), many adapters implement it (mostly null; Claude/OpenCode/Codex return paths), a resolveGlobalRoot helper and registry filtering are added, and init/update CLI flows gain a --global mode with executeGlobal() paths and extensive tests/specs updated. Changes
Sequence Diagram(s)sequenceDiagram
participant CLI as CLI (src/cli/index.ts)
participant Init as InitCommand (src/core/init.ts)
participant Registry as CommandAdapterRegistry
participant Adapter as ToolCommandAdapter
participant FS as Filesystem
CLI->>Init: run `openspec init --global --tools claude,opencode`
Init->>Registry: resolve requested tools / getGlobalAdapters()
Registry-->>Init: adapters for requested tools
Init->>Adapter: resolveGlobalRoot(adapter)
Adapter-->>Init: absolute global root (or null)
Init->>FS: write generated skill/command files to resolved global paths
FS-->>Init: write results
Init-->>CLI: summary (installed / skipped)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…tion Install skills and commands to tool global directories (~/.claude/, ~/.config/opencode/, ~/.codex/) instead of project directories. This lets consultants and multi-repo developers install once globally rather than per-project. - Add optional `getGlobalRoot()` method to ToolCommandAdapter interface - Implement for Claude (~/.claude/), OpenCode (XDG-aware), Codex (~/.codex/) - Return null from all 20 remaining adapters (Cursor, Windsurf, etc.) - Migrate Codex adapter: getFilePath() now returns project-relative path - Add `--global` flag to `openspec init` (requires `--tools`) - Add `--global` flag to `openspec update` - Add `resolveGlobalRoot()` helper with OPENSPEC_GLOBAL_ROOT env var override - Add `getGlobalAdapters()` registry method Closes Fission-AI#752 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
68e2c9c to
00e058f
Compare
Greptile SummaryThis PR adds global installation support for OpenSpec skills and commands, allowing consultants and multi-repo developers to install once globally rather than per-project. The implementation introduces an optional Key Changes:
Architecture: Minor Consideration: Confidence Score: 4/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
CLI["CLI: openspec init --global --tools X"]
InitCmd["InitCommand.executeGlobal()"]
Registry["CommandAdapterRegistry"]
Resolver["resolveGlobalRoot()"]
CLI --> InitCmd
InitCmd --> Registry
Registry --> |"getGlobalAdapters()"| FilterAdapters["Filter adapters with getGlobalRoot() != null"]
InitCmd --> Resolver
Resolver --> |"Check OPENSPEC_GLOBAL_ROOT"| EnvCheck{Env var set?}
EnvCheck --> |Yes| EnvPath["Use $OPENSPEC_GLOBAL_ROOT/{toolId}"]
EnvCheck --> |No| AdapterPath["Use adapter.getGlobalRoot()"]
EnvPath --> GlobalRoot["Global Root Path"]
AdapterPath --> GlobalRoot
GlobalRoot --> GenSkills["Generate Skills\n{globalRoot}/skills/openspec-X/"]
GlobalRoot --> GenCommands["Generate Commands\n{globalRoot}/commands/opsx/"]
GenCommands --> PathManip["Strip first dir segment\nfrom relative path"]
PathManip --> WriteFiles["Write to global directories"]
GenSkills --> WriteFiles
subgraph "Adapter Implementations"
Claude["claude: ~/.claude/"]
OpenCode["opencode: $XDG_CONFIG_HOME/opencode/"]
Codex["codex: $CODEX_HOME or ~/.codex/"]
Cursor["cursor: null"]
Others["20 other adapters: null"]
end
FilterAdapters --> Claude
FilterAdapters --> OpenCode
FilterAdapters --> Codex
Last reviewed commit: 00e058f |
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/cli/index.ts (1)
90-100:⚠️ Potential issue | 🟡 MinorDocument which tools support
--globalin help output.The CLI spec calls for listing supported global tools in help text; the current flag description doesn’t include that list.
✍️ Suggested update
const availableToolIds = AI_TOOLS.filter((tool) => tool.skillsDir).map((tool) => tool.value); const toolsOptionDescription = `Configure AI tools non-interactively. Use "all", "none", or a comma-separated list of: ${availableToolIds.join(', ')}`; +const globalToolIds = ['claude', 'opencode', 'codex']; +const globalToolsHint = `Supported global tools: ${globalToolIds.join(', ')}`; @@ - .option('--global', 'Install skills and commands to tool global directories (requires --tools)') + .option('--global', `Install skills and commands to tool global directories (requires --tools). ${globalToolsHint}`)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cli/index.ts` around lines 90 - 100, The help text for the --global flag doesn't list which tools support global installs; update the CLI to compute the supported-global tool ids from AI_TOOLS (similar to availableToolIds) by filtering for the property that indicates global support (e.g., a field like globalInstall/globalDir/supportsGlobal on each tool) and then include that comma-separated list in the --global option description. Modify the code around availableToolIds/toolsOptionDescription and the program.command(...).option('--global', ...) call so the option text explicitly names the tools that support --global.
♻️ Duplicate comments (13)
src/core/command-generation/adapters/costrict.ts (1)
32-34: LGTM.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/command-generation/adapters/costrict.ts` around lines 32 - 34, getGlobalRoot currently returns null in the Costrict adapter (function getGlobalRoot), and the reviewer approved the change; no code modification is required—leave the getGlobalRoot(): null { return null; } implementation as-is in src/core/command-generation/adapters/costrict.ts.src/core/command-generation/adapters/crush.ts (1)
35-37: LGTM.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/command-generation/adapters/crush.ts` around lines 35 - 37, getGlobalRoot() in the crush adapter is approved as-is; no changes required—leave the method getGlobalRoot() returning null in src/core/command-generation/adapters/crush.ts unchanged.src/core/command-generation/adapters/pi.ts (1)
47-49: LGTM.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/command-generation/adapters/pi.ts` around lines 47 - 49, The review approves the current implementation—no change required: leave the getGlobalRoot() method as implemented (returning null) in the adapter (function getGlobalRoot) and proceed with the PR as-is.src/core/command-generation/adapters/continue.ts (1)
33-35: LGTM. Same pattern asgeminiAdapter; the DRY refactor suggestion ingemini.tsapplies here too.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/command-generation/adapters/continue.ts` around lines 33 - 35, The getGlobalRoot() implementation in continue.ts is identical to geminiAdapter's pattern; refactor by extracting the shared logic into a common helper or base class (e.g., move the null-returning getGlobalRoot behavior into a shared utility used by both continueAdapter and geminiAdapter) and update continue.ts to call that shared function or extend the base so the duplicate getGlobalRoot() method is removed; ensure you reference and reuse the same symbol/name used for the DRY helper in gemini.ts so both adapters share one implementation.src/core/command-generation/adapters/factory.ts (1)
32-34: LGTM.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/command-generation/adapters/factory.ts` around lines 32 - 34, No change required: the getGlobalRoot() method in factory.ts intentionally returns null and is approved as-is; if you later need it to return a real path, update the method implementation and return type from getGlobalRoot(): null to something like getGlobalRoot(): string | null and implement logic there.src/core/command-generation/adapters/windsurf.ts (1)
58-60: LGTM.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/command-generation/adapters/windsurf.ts` around lines 58 - 60, No change required: the reviewer approved the implementation of getGlobalRoot(), so leave the getGlobalRoot(): null { return null; } method as-is (no modifications or refactors needed) and proceed to merge; ensure you do not add duplicate review comments or edits related to this method.src/core/command-generation/adapters/auggie.ts (1)
32-34: Same pattern asgithub-copilot.ts.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/command-generation/adapters/auggie.ts` around lines 32 - 34, getGlobalRoot currently returns null in auggie.ts, duplicating the same stub pattern used in github-copilot.ts; replace the placeholder with the correct implementation or a shared helper. Locate the getGlobalRoot method in the adapter (function getGlobalRoot) and either implement the proper logic that returns the adapter’s global root path or delegate to a common utility (extract a shared getGlobalRoot helper and call it from both auggie.ts and github-copilot.ts) so the behavior is consistent and not duplicated.src/core/command-generation/adapters/cursor.ts (1)
50-52: Same pattern asgithub-copilot.ts.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/command-generation/adapters/cursor.ts` around lines 50 - 52, getGlobalRoot() currently returns null as a stub (same issue as in github-copilot.ts); replace this with the same implementation used in github-copilot.ts so the adapter returns the correct global root value (or delegates to the shared helper used there). Locate getGlobalRoot in the Cursor adapter and copy the logic from the github-copilot adapter (or call the common helper function used by that file) so the method returns the actual global root rather than null.src/core/command-generation/adapters/cline.ts (1)
32-34: Same pattern asgithub-copilot.ts.See the comment on
github-copilot.tslines 31–33 — the optional-method boilerplate concern applies here identically.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/command-generation/adapters/cline.ts` around lines 32 - 34, The no-op getGlobalRoot() method in the Cline adapter is redundant boilerplate like in github-copilot.ts; remove the explicit getGlobalRoot(): null { return null; } from the class so the adapter doesn't define an unnecessary optional-method stub (or implement the actual behavior if required), ensuring the class relies on the interface's optional method semantics instead of a null-returning placeholder.src/core/command-generation/adapters/iflow.ts (1)
34-36: Same pattern asgithub-copilot.ts.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/command-generation/adapters/iflow.ts` around lines 34 - 36, The getGlobalRoot() method in iflow.ts is a stub that returns null and duplicates the same pattern from github-copilot.ts; update getGlobalRoot() in iflow.ts to match the real implementation used in github-copilot.ts (replace the null stub with the same logic/return type and behavior), ensuring the function name getGlobalRoot and any helper calls used in github-copilot.ts are mirrored so both adapters behave consistently.src/core/command-generation/adapters/kiro.ts (1)
31-33: Same pattern asgithub-copilot.ts.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/command-generation/adapters/kiro.ts` around lines 31 - 33, The getGlobalRoot() implementation in kiro.ts simply returns null and duplicates the same pattern found in github-copilot.ts; refactor to avoid duplication by either implementing the shared logic used by github-copilot.ts or extracting a common helper used by both adapters (e.g., move the real getGlobalRoot behavior into a shared utility and have kiro.ts and github-copilot.ts call that), then update kiro.ts's getGlobalRoot() to call the shared helper instead of returning null.src/core/command-generation/adapters/antigravity.ts (1)
31-33: Same pattern asgithub-copilot.ts.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/command-generation/adapters/antigravity.ts` around lines 31 - 33, getGlobalRoot currently returns null in antigravity.ts which duplicates the pattern in github-copilot.ts; update getGlobalRoot to match the github-copilot.ts implementation by either delegating to the shared helper used there (e.g., call the common getGlobalRoot helper) or copying that same logic into antigravity.ts so both adapters behave identically, keeping the method signature and return type unchanged and removing the duplicated null-return behavior.src/core/command-generation/adapters/kilocode.ts (1)
28-30: Same pattern asgithub-copilot.ts.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/command-generation/adapters/kilocode.ts` around lines 28 - 30, The getGlobalRoot() override in the kilocode adapter duplicates the trivial implementation from github-copilot.ts; remove this redundant method from the Kilocode adapter (or replace it with the same consolidated implementation used by github-copilot.ts) so the adapter relies on the shared/base behavior instead of repeating the identical getGlobalRoot() { return null; } stub.
🧹 Nitpick comments (7)
src/core/command-generation/adapters/gemini.ts (1)
31-33: Consider extracting a sharednullGlobalRootmixin to reduce boilerplate across all null-returning adapters.This identical 3-line block is duplicated across ~20 adapter files in this PR. A single shared object literal would centralize it:
♻️ Proposed extraction (applies to all null-returning adapters)
Create a shared file (e.g.,
src/core/command-generation/adapters/shared.ts):+// Shared mixin for adapters that have no global installation root +export const nullGlobalRoot = { + getGlobalRoot(): null { + return null; + }, +} as const;Then spread it into each null-returning adapter:
export const geminiAdapter: ToolCommandAdapter = { toolId: 'gemini', + ...nullGlobalRoot, getFilePath(commandId: string): string { return path.join('.gemini', 'commands', 'opsx', `${commandId}.toml`); }, formatFile(content: CommandContent): string { // ... }, - - getGlobalRoot(): null { - return null; - }, };Repeat for every other null-returning adapter in this PR (continue, windsurf, factory, costrict, pi, crush, and the ~13 others not shown here).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/command-generation/adapters/gemini.ts` around lines 31 - 33, Extract a shared object (e.g., export const nullGlobalRoot) that implements getGlobalRoot(): null and return null, place it in a new module (suggested name: src/core/command-generation/adapters/shared.ts), then import and spread that object into each null-returning adapter (including the adapter object in src/core/command-generation/adapters/gemini.ts) instead of defining the three-line getGlobalRoot() { return null; } block in every file; ensure the exported symbol name nullGlobalRoot and the method name getGlobalRoot are used so existing adapter shapes remain unchanged.src/core/command-generation/adapters/github-copilot.ts (1)
31-33: Optional refactoring: Remove redundantgetGlobalRoot(): nullstubs from non-global adapters.Since
getGlobalRoot()is optional onToolCommandAdapter(declared asgetGlobalRoot?()), adapters without global support can simply omit the method. ThegetGlobalAdapters()filter already uses optional chaining (adapter.getGlobalRoot?.() != null), which correctly excludes both adapters without the method (returningundefined) and those returningnull. The explicit null-returning stubs across ~19 adapters (includinggithub-copilot.ts) are unnecessary boilerplate.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/command-generation/adapters/github-copilot.ts` around lines 31 - 33, Remove the redundant getGlobalRoot(): null stub from non-global adapters (e.g., in github-copilot.ts) since ToolCommandAdapter declares getGlobalRoot?() as optional; simply delete the getGlobalRoot method from the adapter class so it relies on optional chaining in getGlobalAdapters() (which uses adapter.getGlobalRoot?.() != null) to filter adapters that provide a global root.src/core/command-generation/types.ts (1)
48-50: Clarify the absolute-path example for getGlobalRoot docs.The description says “absolute path” but the example uses
~, which is shell expansion. Consider updating the example to an explicit absolute path or a<home>placeholder to avoid confusion.💡 Suggested doc tweak
- * `@returns` Absolute path to the global root (e.g., '~/.claude/'), or null + * `@returns` Absolute path to the global root (e.g., '<home>/.claude/'), or null🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/command-generation/types.ts` around lines 48 - 50, Update the JSDoc for getGlobalRoot in types.ts to avoid using shell-tilde; change the example from '~/.claude/' to an explicit absolute-path or a placeholder like '/home/username/.claude/' or '<home>/.claude/' so the docs accurately reflect an absolute path and don't imply shell expansion.src/core/command-generation/adapters/opencode.ts (1)
24-31: Normalize XDG_CONFIG_HOME to an absolute path.This keeps the adapter aligned with the “absolute path” contract even if the env var is relative.
♻️ Suggested tweak
- return xdgConfig - ? path.join(xdgConfig, 'opencode') + return xdgConfig + ? path.resolve(xdgConfig, 'opencode') : path.join(os.homedir(), '.config', 'opencode');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/command-generation/adapters/opencode.ts` around lines 24 - 31, The getGlobalRoot() function should normalize XDG_CONFIG_HOME to an absolute path before joining with 'opencode'; update the branch that reads process.env.XDG_CONFIG_HOME in getGlobalRoot to call path.resolve(...) (or otherwise convert the possibly-relative env value to an absolute path) and then use that resolved path in path.join(..., 'opencode') so the adapter always returns an absolute path even when XDG_CONFIG_HOME is relative.openspec/specs/cli-init/spec.md (1)
5-12: Clarify that installed commands are slash commands.To avoid confusion with terminal CLI commands, spell out “slash commands” (or “/opsx:*”).
Based on learnings: In the OpenSpec codebase, distinguish between CLI commands (terminal-based, e.g.,openspec status) and slash commands (agent interface commands, e.g.,/opsx:clarify).✏️ Suggested wording
-The `openspec init` command SHALL support a `--global` flag that installs skills and commands to tool global directories instead of project directories. +The `openspec init` command SHALL support a `--global` flag that installs skills and slash commands to tool global directories instead of project directories.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@openspec/specs/cli-init/spec.md` around lines 5 - 12, Update the wording in the scenario for `openspec init` to disambiguate "commands" as agent slash commands: change the phrase "the system SHALL write skills and commands to Claude Code's global directory" to explicitly say "the system SHALL write skills and slash commands (e.g., /opsx:...) to Claude Code's global directory (~/.claude/)". Also ensure any other occurrences in this block referencing "commands" (including the `openspec/` directory and `--global` behavior) are clarified to distinguish terminal CLI commands (e.g., `openspec status`) from slash commands used by agents.openspec/specs/command-generation/spec.md (1)
38-61:resolveGlobalRoot()utility has no specification.
resolveGlobalRoot()is the central routing function for all global CLI operations (init, update), but it is only referenced intasks.mdwith no corresponding requirement or scenario inspec.md. Its contract — specifically the precedence logic betweenOPENSPEC_GLOBAL_ROOTandadapter.getGlobalRoot(), and its return value when the adapter has a null global root — should be specified here.📝 Suggested addition
+### Requirement: Global-root resolution utility + +The system SHALL provide a `resolveGlobalRoot(adapter: ToolCommandAdapter)` helper for determining the effective global installation root. + +#### Scenario: Resolve global root with env var override + +- **WHEN** calling `resolveGlobalRoot(adapter)` and `OPENSPEC_GLOBAL_ROOT` is set +- **THEN** it SHALL return the value of `OPENSPEC_GLOBAL_ROOT` as an absolute path +- **AND** it SHALL NOT call `adapter.getGlobalRoot()` + +#### Scenario: Resolve global root from adapter + +- **WHEN** calling `resolveGlobalRoot(adapter)` and `OPENSPEC_GLOBAL_ROOT` is not set +- **THEN** it SHALL return `adapter.getGlobalRoot()` (which may be `null`)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@openspec/specs/command-generation/spec.md` around lines 38 - 61, Add a new requirement and scenarios for the resolveGlobalRoot() utility: specify that resolveGlobalRoot() SHALL prefer an explicit OPENSPEC_GLOBAL_ROOT environment variable over adapter.getGlobalRoot(), SHALL return adapter.getGlobalRoot() when OPENSPEC_GLOBAL_ROOT is unset and adapter.getGlobalRoot() is non-null, and SHALL return null (or an explicit error/empty result as chosen) when both OPENSPEC_GLOBAL_ROOT is unset and adapter.getGlobalRoot() is null; include scenarios for (1) OPENSPEC_GLOBAL_ROOT set (returns that value), (2) OPENSPEC_GLOBAL_ROOT unset but adapter.getGlobalRoot() non-null (returns adapter path), and (3) both unset/null (define returned value/behavior), referencing the resolveGlobalRoot() utility and adapter.getGlobalRoot() to locate the code.openspec/changes/archive/2026-02-23-init-global/tasks.md (1)
40-43: Mixed-support behavior (skip vs. error) is undocumented in any spec scenario.Tasks 6.6 and 6.9 imply different CLI behaviors:
--tools cursor→ error (all requested tools are globally unsupported)--tools claude,cursor→ skip cursor, install claude (partially unsupported)This distinction is not captured in any spec scenario visible in this PR. Without a spec requirement, this branching behavior risks inconsistent implementation or regression. Consider adding a CLI scenario in
openspec/specs/cli-init/spec.md(or equivalent) that explicitly documents when the command errors vs. when it silently skips.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@openspec/changes/archive/2026-02-23-init-global/tasks.md` around lines 40 - 43, Add a CLI spec scenario that documents the mixed-support behavior for the --tools flag: one case specifying that when all requested tools are globally unsupported (matching test 6.6, e.g., --tools cursor) the command must return an error, and another case showing that when some tools are supported and some are not (matching test 6.9, e.g., --tools claude,cursor) the command should install supported tools and skip unsupported ones without failing; update the cli-init spec (spec.md) to list which tools are considered globally-supported and state the exact error vs. skip semantics for --tools so implementations and tests are unambiguous.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@openspec/changes/archive/2026-02-23-init-global/.openspec.yaml`:
- Line 2: The `created` date in the file's frontmatter (the created field in
.openspec.yaml) does not match the parent directory name
`2026-02-23-init-global`; pick the canonical date for this change and make them
consistent by either updating the created field in .openspec.yaml to
`2026-02-23` or renaming the directory to `2026-02-24-init-global`, and ensure
the change is applied consistently across this entry.
In `@openspec/changes/archive/2026-02-23-init-global/specs/cli-update/spec.md`:
- Around line 1-26: Add a short explicit sentence to the "Global update mode"
requirement stating that this delta is applied independently and that OpenSpec
archive changes are not transactional across multiple deltas; reference the
"Global update mode" heading and the scenario blocks (e.g., "Scenario: Global
update", "Scenario: Global update with no globally-installed files", "Scenario:
Non-global update unchanged") so readers know the per-delta partial-sync
atomicity model applies and that no cross-delta transaction is implied.
In `@openspec/changes/archive/2026-02-23-init-global/tasks.md`:
- Around line 24-25: Task 4.2 is ambiguous about the dual patterns `openspec-*`
vs `opsx-*`; clarify which artifact type each pattern represents and ensure the
global scan covers both; update the Task 4.2 description to state explicitly
"scan global adapter roots for opsx-<id>.md (commands) and openspec-<id>.md
(skills/other artifacts)" (referencing the adapter.getFilePath() scenarios), and
update Task 6.11 to add tests that assert the global update scan finds and
regenerates files matching both `opsx-*` and `openspec-*` patterns so nothing is
silently skipped.
In `@openspec/specs/command-generation/spec.md`:
- Around line 38-61: Add a new scenario for the OpenCode adapter documenting its
global-root contract: call OpenCodeAdapter.getGlobalRoot() and assert it returns
the adapter's XDG-aware absolute home directory (use ~/.config/opencode/ on
macOS/Linux, %APPDATA%\opencode\ on Windows, and respect XDG/APPDATA environment
overrides), so the spec explicitly documents OpenCodeAdapter.getGlobalRoot()’s
expected behavior alongside Claude and Codex.
- Around line 44-47: Clarify and implement the intended filtering contract for
CommandAdapterRegistry.getGlobalAdapters(): decide whether it should include
adapters based on their declared getGlobalRoot() (Option 1) or based on the
effective path returned by resolveGlobalRoot() (Option 2). Update the spec text
and the implementation for getGlobalAdapters() accordingly: if you choose Option
1, state that OPENSPEC_GLOBAL_ROOT only overrides adapters that already return a
non-null getGlobalRoot() and keep getGlobalAdapters() filtering on
getGlobalRoot(); if you choose Option 2, state that getGlobalAdapters() uses
resolveGlobalRoot() (which checks OPENSPEC_GLOBAL_ROOT first) and change
getGlobalAdapters() to call resolveGlobalRoot(adapter) instead of
adapter.getGlobalRoot() so adapters with null getGlobalRoot() can be included
when OPENSPEC_GLOBAL_ROOT makes them resolvable.
- Line 22: The spec's Windows path for getGlobalRoot() is incorrect; change the
documented Windows return value of getGlobalRoot() from "%APPDATA%\Claude\" to
"%USERPROFILE%\.claude\" (e.g., C:\Users\<username>\.claude\) so it matches
Claude Code's actual global configuration directory; update the spec line that
currently states getGlobalRoot() SHALL return "~/.claude/ (macOS/Linux) or
%APPDATA%\Claude\ (Windows)" to instead specify "%USERPROFILE%\.claude\
(Windows)" while keeping the macOS/Linux value unchanged.
In `@src/core/command-generation/adapters/codex.ts`:
- Around line 31-37: The adapter documentation/header is inconsistent:
getFilePath(commandId) now returns a project‑relative path
(".codex/prompts/opsx-<commandId>.md") but the doc text still claims a
global/absolute home; update the adapter docs to state that prompt files are
stored in a project‑relative ".codex" directory under the project root and
explain how getGlobalRoot() (which calls getCodexHome()) differs or when a
global home is used; update any mention of "global home directory / absolute" to
clearly describe both behaviors and reference getFilePath and getGlobalRoot by
name so readers can find the code.
In `@src/core/init.ts`:
- Around line 217-266: The global init currently only aggregates totalFiles;
update the loop over toolResults to track per-tool file counts and emit per-tool
summary lines including the global path: inside the for (const { toolId,
globalRoot, name } of toolResults) block create a local counter (e.g.,
filesForTool) and increment it whenever you write a file (both in the
shouldGenerateSkills branch where you call FileSystemUtils.writeFile and in the
shouldGenerateCommands branch where you write commandFile), still increment
totalFiles as before, then collect a summary entry (e.g., `${name}:
${filesForTool} files → ${globalRoot}`) into an array or map; after the loop
print those per-tool lines (instead of only the aggregated total) alongside the
existing overall Installed/totalFiles output so callers see per-tool counts and
directories.
In `@src/core/update.ts`:
- Around line 737-773: The code currently detects existing global commands by
checking adapter.getFilePath('explore'), which can miss active workflows;
instead call generateCommands(commandContents, adapter) early, set hasCommands =
generatedCommands.length > 0, and reuse that generatedCommands list later when
writing files (replace the current second call to generateCommands with the
already-built generatedCommands); update the block around
shouldGenerateCommands/hasCommands to use generatedCommands so command detection
and writes reflect the active workflow set.
In `@test/core/global-init.test.ts`:
- Around line 114-128: The test may write to the real Claude global root because
OPENSPEC_GLOBAL_ROOT is deleted; before calling InitCommand.executeGlobal() stub
or mock claudeAdapter.getGlobalRoot (the function used to locate Claude's global
root) to return a temporary directory (or set OPENSPEC_GLOBAL_ROOT to a temp
path) so the test operates on an isolated temp folder; do this setup inside the
test (or a before/after hook) and ensure teardown/cleanup of the temp directory
after the test finishes.
---
Outside diff comments:
In `@src/cli/index.ts`:
- Around line 90-100: The help text for the --global flag doesn't list which
tools support global installs; update the CLI to compute the supported-global
tool ids from AI_TOOLS (similar to availableToolIds) by filtering for the
property that indicates global support (e.g., a field like
globalInstall/globalDir/supportsGlobal on each tool) and then include that
comma-separated list in the --global option description. Modify the code around
availableToolIds/toolsOptionDescription and the
program.command(...).option('--global', ...) call so the option text explicitly
names the tools that support --global.
---
Duplicate comments:
In `@src/core/command-generation/adapters/antigravity.ts`:
- Around line 31-33: getGlobalRoot currently returns null in antigravity.ts
which duplicates the pattern in github-copilot.ts; update getGlobalRoot to match
the github-copilot.ts implementation by either delegating to the shared helper
used there (e.g., call the common getGlobalRoot helper) or copying that same
logic into antigravity.ts so both adapters behave identically, keeping the
method signature and return type unchanged and removing the duplicated
null-return behavior.
In `@src/core/command-generation/adapters/auggie.ts`:
- Around line 32-34: getGlobalRoot currently returns null in auggie.ts,
duplicating the same stub pattern used in github-copilot.ts; replace the
placeholder with the correct implementation or a shared helper. Locate the
getGlobalRoot method in the adapter (function getGlobalRoot) and either
implement the proper logic that returns the adapter’s global root path or
delegate to a common utility (extract a shared getGlobalRoot helper and call it
from both auggie.ts and github-copilot.ts) so the behavior is consistent and not
duplicated.
In `@src/core/command-generation/adapters/cline.ts`:
- Around line 32-34: The no-op getGlobalRoot() method in the Cline adapter is
redundant boilerplate like in github-copilot.ts; remove the explicit
getGlobalRoot(): null { return null; } from the class so the adapter doesn't
define an unnecessary optional-method stub (or implement the actual behavior if
required), ensuring the class relies on the interface's optional method
semantics instead of a null-returning placeholder.
In `@src/core/command-generation/adapters/continue.ts`:
- Around line 33-35: The getGlobalRoot() implementation in continue.ts is
identical to geminiAdapter's pattern; refactor by extracting the shared logic
into a common helper or base class (e.g., move the null-returning getGlobalRoot
behavior into a shared utility used by both continueAdapter and geminiAdapter)
and update continue.ts to call that shared function or extend the base so the
duplicate getGlobalRoot() method is removed; ensure you reference and reuse the
same symbol/name used for the DRY helper in gemini.ts so both adapters share one
implementation.
In `@src/core/command-generation/adapters/costrict.ts`:
- Around line 32-34: getGlobalRoot currently returns null in the Costrict
adapter (function getGlobalRoot), and the reviewer approved the change; no code
modification is required—leave the getGlobalRoot(): null { return null; }
implementation as-is in src/core/command-generation/adapters/costrict.ts.
In `@src/core/command-generation/adapters/crush.ts`:
- Around line 35-37: getGlobalRoot() in the crush adapter is approved as-is; no
changes required—leave the method getGlobalRoot() returning null in
src/core/command-generation/adapters/crush.ts unchanged.
In `@src/core/command-generation/adapters/cursor.ts`:
- Around line 50-52: getGlobalRoot() currently returns null as a stub (same
issue as in github-copilot.ts); replace this with the same implementation used
in github-copilot.ts so the adapter returns the correct global root value (or
delegates to the shared helper used there). Locate getGlobalRoot in the Cursor
adapter and copy the logic from the github-copilot adapter (or call the common
helper function used by that file) so the method returns the actual global root
rather than null.
In `@src/core/command-generation/adapters/factory.ts`:
- Around line 32-34: No change required: the getGlobalRoot() method in
factory.ts intentionally returns null and is approved as-is; if you later need
it to return a real path, update the method implementation and return type from
getGlobalRoot(): null to something like getGlobalRoot(): string | null and
implement logic there.
In `@src/core/command-generation/adapters/iflow.ts`:
- Around line 34-36: The getGlobalRoot() method in iflow.ts is a stub that
returns null and duplicates the same pattern from github-copilot.ts; update
getGlobalRoot() in iflow.ts to match the real implementation used in
github-copilot.ts (replace the null stub with the same logic/return type and
behavior), ensuring the function name getGlobalRoot and any helper calls used in
github-copilot.ts are mirrored so both adapters behave consistently.
In `@src/core/command-generation/adapters/kilocode.ts`:
- Around line 28-30: The getGlobalRoot() override in the kilocode adapter
duplicates the trivial implementation from github-copilot.ts; remove this
redundant method from the Kilocode adapter (or replace it with the same
consolidated implementation used by github-copilot.ts) so the adapter relies on
the shared/base behavior instead of repeating the identical getGlobalRoot() {
return null; } stub.
In `@src/core/command-generation/adapters/kiro.ts`:
- Around line 31-33: The getGlobalRoot() implementation in kiro.ts simply
returns null and duplicates the same pattern found in github-copilot.ts;
refactor to avoid duplication by either implementing the shared logic used by
github-copilot.ts or extracting a common helper used by both adapters (e.g.,
move the real getGlobalRoot behavior into a shared utility and have kiro.ts and
github-copilot.ts call that), then update kiro.ts's getGlobalRoot() to call the
shared helper instead of returning null.
In `@src/core/command-generation/adapters/pi.ts`:
- Around line 47-49: The review approves the current implementation—no change
required: leave the getGlobalRoot() method as implemented (returning null) in
the adapter (function getGlobalRoot) and proceed with the PR as-is.
In `@src/core/command-generation/adapters/windsurf.ts`:
- Around line 58-60: No change required: the reviewer approved the
implementation of getGlobalRoot(), so leave the getGlobalRoot(): null { return
null; } method as-is (no modifications or refactors needed) and proceed to
merge; ensure you do not add duplicate review comments or edits related to this
method.
---
Nitpick comments:
In `@openspec/changes/archive/2026-02-23-init-global/tasks.md`:
- Around line 40-43: Add a CLI spec scenario that documents the mixed-support
behavior for the --tools flag: one case specifying that when all requested tools
are globally unsupported (matching test 6.6, e.g., --tools cursor) the command
must return an error, and another case showing that when some tools are
supported and some are not (matching test 6.9, e.g., --tools claude,cursor) the
command should install supported tools and skip unsupported ones without
failing; update the cli-init spec (spec.md) to list which tools are considered
globally-supported and state the exact error vs. skip semantics for --tools so
implementations and tests are unambiguous.
In `@openspec/specs/cli-init/spec.md`:
- Around line 5-12: Update the wording in the scenario for `openspec init` to
disambiguate "commands" as agent slash commands: change the phrase "the system
SHALL write skills and commands to Claude Code's global directory" to explicitly
say "the system SHALL write skills and slash commands (e.g., /opsx:...) to
Claude Code's global directory (~/.claude/)". Also ensure any other occurrences
in this block referencing "commands" (including the `openspec/` directory and
`--global` behavior) are clarified to distinguish terminal CLI commands (e.g.,
`openspec status`) from slash commands used by agents.
In `@openspec/specs/command-generation/spec.md`:
- Around line 38-61: Add a new requirement and scenarios for the
resolveGlobalRoot() utility: specify that resolveGlobalRoot() SHALL prefer an
explicit OPENSPEC_GLOBAL_ROOT environment variable over adapter.getGlobalRoot(),
SHALL return adapter.getGlobalRoot() when OPENSPEC_GLOBAL_ROOT is unset and
adapter.getGlobalRoot() is non-null, and SHALL return null (or an explicit
error/empty result as chosen) when both OPENSPEC_GLOBAL_ROOT is unset and
adapter.getGlobalRoot() is null; include scenarios for (1) OPENSPEC_GLOBAL_ROOT
set (returns that value), (2) OPENSPEC_GLOBAL_ROOT unset but
adapter.getGlobalRoot() non-null (returns adapter path), and (3) both unset/null
(define returned value/behavior), referencing the resolveGlobalRoot() utility
and adapter.getGlobalRoot() to locate the code.
In `@src/core/command-generation/adapters/gemini.ts`:
- Around line 31-33: Extract a shared object (e.g., export const nullGlobalRoot)
that implements getGlobalRoot(): null and return null, place it in a new module
(suggested name: src/core/command-generation/adapters/shared.ts), then import
and spread that object into each null-returning adapter (including the adapter
object in src/core/command-generation/adapters/gemini.ts) instead of defining
the three-line getGlobalRoot() { return null; } block in every file; ensure the
exported symbol name nullGlobalRoot and the method name getGlobalRoot are used
so existing adapter shapes remain unchanged.
In `@src/core/command-generation/adapters/github-copilot.ts`:
- Around line 31-33: Remove the redundant getGlobalRoot(): null stub from
non-global adapters (e.g., in github-copilot.ts) since ToolCommandAdapter
declares getGlobalRoot?() as optional; simply delete the getGlobalRoot method
from the adapter class so it relies on optional chaining in getGlobalAdapters()
(which uses adapter.getGlobalRoot?.() != null) to filter adapters that provide a
global root.
In `@src/core/command-generation/adapters/opencode.ts`:
- Around line 24-31: The getGlobalRoot() function should normalize
XDG_CONFIG_HOME to an absolute path before joining with 'opencode'; update the
branch that reads process.env.XDG_CONFIG_HOME in getGlobalRoot to call
path.resolve(...) (or otherwise convert the possibly-relative env value to an
absolute path) and then use that resolved path in path.join(..., 'opencode') so
the adapter always returns an absolute path even when XDG_CONFIG_HOME is
relative.
In `@src/core/command-generation/types.ts`:
- Around line 48-50: Update the JSDoc for getGlobalRoot in types.ts to avoid
using shell-tilde; change the example from '~/.claude/' to an explicit
absolute-path or a placeholder like '/home/username/.claude/' or
'<home>/.claude/' so the docs accurately reflect an absolute path and don't
imply shell expansion.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (45)
openspec/changes/archive/2026-02-23-init-global/.openspec.yamlopenspec/changes/archive/2026-02-23-init-global/design.mdopenspec/changes/archive/2026-02-23-init-global/proposal.mdopenspec/changes/archive/2026-02-23-init-global/specs/cli-init/spec.mdopenspec/changes/archive/2026-02-23-init-global/specs/cli-update/spec.mdopenspec/changes/archive/2026-02-23-init-global/specs/command-generation/spec.mdopenspec/changes/archive/2026-02-23-init-global/specs/global-install/spec.mdopenspec/changes/archive/2026-02-23-init-global/tasks.mdopenspec/specs/cli-init/spec.mdopenspec/specs/cli-update/spec.mdopenspec/specs/command-generation/spec.mdopenspec/specs/global-install/spec.mdsrc/cli/index.tssrc/core/command-generation/adapters/amazon-q.tssrc/core/command-generation/adapters/antigravity.tssrc/core/command-generation/adapters/auggie.tssrc/core/command-generation/adapters/claude.tssrc/core/command-generation/adapters/cline.tssrc/core/command-generation/adapters/codebuddy.tssrc/core/command-generation/adapters/codex.tssrc/core/command-generation/adapters/continue.tssrc/core/command-generation/adapters/costrict.tssrc/core/command-generation/adapters/crush.tssrc/core/command-generation/adapters/cursor.tssrc/core/command-generation/adapters/factory.tssrc/core/command-generation/adapters/gemini.tssrc/core/command-generation/adapters/github-copilot.tssrc/core/command-generation/adapters/iflow.tssrc/core/command-generation/adapters/kilocode.tssrc/core/command-generation/adapters/kiro.tssrc/core/command-generation/adapters/opencode.tssrc/core/command-generation/adapters/pi.tssrc/core/command-generation/adapters/qoder.tssrc/core/command-generation/adapters/qwen.tssrc/core/command-generation/adapters/roocode.tssrc/core/command-generation/adapters/windsurf.tssrc/core/command-generation/global-root.tssrc/core/command-generation/index.tssrc/core/command-generation/registry.tssrc/core/command-generation/types.tssrc/core/init.tssrc/core/update.tstest/core/command-generation/adapters.test.tstest/core/command-generation/global-install.test.tstest/core/global-init.test.ts
openspec/changes/archive/2026-02-23-init-global/specs/cli-update/spec.md
Show resolved
Hide resolved
- Use generated command list for global command detection instead of hard-coded 'explore' probe, ensuring all active workflows are checked - Mock claudeAdapter.getGlobalRoot() in test to avoid writing to real ~/.claude directory during test execution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
test/core/global-init.test.ts (1)
114-131: Properly isolates from the real filesystem by mockingclaudeAdapter.getGlobalRoot().This was flagged in a prior review and is now correctly addressed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/core/global-init.test.ts` around lines 114 - 131, Test properly isolates from the real filesystem by deleting process.env.OPENSPEC_GLOBAL_ROOT and mocking claudeAdapter.getGlobalRoot to return a tempClaudeRoot; ensure the InitCommand.executeGlobal call uses that mocked root and assertions check the tempClaudeRoot/skills directory (e.g., path.join(tempClaudeRoot, 'skills')) for installed openspec packages—keep the vi.spyOn(claudeAdapter, 'getGlobalRoot').mockReturnValue(tempClaudeRoot) and the delete process.env line so no real ~/.claude is touched.
🧹 Nitpick comments (3)
src/core/update.ts (2)
738-744: Duplicated path-stripping regex — consider extracting a helper.The regex
cmd.path.replace(/^\.?[^/\\]+[/\\]/, '')appears on both Line 742 and Line 770 to strip the tool-directory prefix from project-relative command paths. Extracting this into a small helper (e.g.,stripToolDirPrefix(cmdPath: string): string) would reduce the risk of the two diverging and make the intent self-documenting.Example helper
+/** Strips the leading tool-directory segment from a project-relative command path. */ +function stripToolDirPrefix(cmdPath: string): string { + return cmdPath.replace(/^\.?[^/\\]+[/\\]/, ''); +}Then use
path.join(globalRoot, stripToolDirPrefix(cmd.path))at both sites.Also applies to: 767-773
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/update.ts` around lines 738 - 744, Extract the repeated regex cmd.path.replace(/^\.?[^/\\]+[/\\]/, '') into a small helper (e.g., stripToolDirPrefix(cmdPath: string): string) and use it where the project-relative command path is normalized (currently in the generation/checking logic around generatedCommands and hasCommands and the other occurrence near command installation checks). Update calls like path.join(globalRoot, stripToolDirPrefix(cmd.path)) and any other places that perform the same replacement to ensure a single, well-named implementation and avoid divergence.
80-84:globalModeis assigned but never read.
this.globalModeis set in the constructor but never referenced anywhere in the class. The CLI callsexecuteGlobal()directly, so this field is dead code. Consider removing it, or if it's intended for future routing insideexecute(), add a comment explaining the intent.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/update.ts` around lines 80 - 84, The field this.globalMode assigned in the constructor of the class (via UpdateCommandOptions) is never read; either remove the globalMode property and its assignment from the constructor to eliminate dead code, or if you intend it for future routing inside execute(), keep the property but add a brief comment above the declaration explaining its planned use and ensure execute() will consult globalMode (or wire execute() to call executeGlobal() when true) so the field is actually used; update references around the constructor, the globalMode declaration, and any call sites of execute()/executeGlobal() accordingly.test/core/global-init.test.ts (1)
184-189: Consider asserting the expected console output for the "no global files" scenario.This test only verifies that
executeGlobal()doesn't throw. Sinceconsole.logis already spied on, you could assert the guidance message was printed, which would strengthen the test.Example assertion
it('should show message when no global files exist', async () => { const updateCommand = new UpdateCommand({ global: true }); await updateCommand.executeGlobal(); - // Should not throw, just show a message + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining('No globally-installed OpenSpec files found') + ); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/core/global-init.test.ts` around lines 184 - 189, Add an assertion that the guidance message is printed when no global files exist by checking the existing console.log spy after calling UpdateCommand.executeGlobal(); specifically, after creating const updateCommand = new UpdateCommand({ global: true }) and awaiting updateCommand.executeGlobal(), assert that console.log was called with the expected guidance string (the user-facing message produced by executeGlobal) so the test verifies output as well as absence of exceptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/core/update.ts`:
- Around line 778-780: The catch block in update.ts currently uses an unsafe
cast `(error as Error).message`; change it to the guarded extraction pattern
used in the local execute() method: compute a message = error instanceof Error ?
error.message : String(error) (and optionally include error.stack when
available) and pass that message to spinner.fail so non-Error throws (strings,
objects) won't cause a secondary exception; update the catch surrounding the
update logic that references toolName to use this guarded message extraction.
In `@test/core/global-init.test.ts`:
- Around line 104-111: resolveGlobalRoot() currently applies
OPENSPEC_GLOBAL_ROOT unconditionally, letting environment overrides enable
global roots for adapters that should be null; update resolveGlobalRoot() to
first call adapter.getGlobalRoot() and if that call returns null, return null
(ignore OPENSPEC_GLOBAL_ROOT), otherwise allow the environment override
(process.env.OPENSPEC_GLOBAL_ROOT) to replace the adapter path; reference
resolveGlobalRoot() and adapter.getGlobalRoot() so the fix ensures null-adapters
remain excluded from global installs regardless of OPENSPEC_GLOBAL_ROOT.
---
Duplicate comments:
In `@test/core/global-init.test.ts`:
- Around line 114-131: Test properly isolates from the real filesystem by
deleting process.env.OPENSPEC_GLOBAL_ROOT and mocking
claudeAdapter.getGlobalRoot to return a tempClaudeRoot; ensure the
InitCommand.executeGlobal call uses that mocked root and assertions check the
tempClaudeRoot/skills directory (e.g., path.join(tempClaudeRoot, 'skills')) for
installed openspec packages—keep the vi.spyOn(claudeAdapter,
'getGlobalRoot').mockReturnValue(tempClaudeRoot) and the delete process.env line
so no real ~/.claude is touched.
---
Nitpick comments:
In `@src/core/update.ts`:
- Around line 738-744: Extract the repeated regex
cmd.path.replace(/^\.?[^/\\]+[/\\]/, '') into a small helper (e.g.,
stripToolDirPrefix(cmdPath: string): string) and use it where the
project-relative command path is normalized (currently in the
generation/checking logic around generatedCommands and hasCommands and the other
occurrence near command installation checks). Update calls like
path.join(globalRoot, stripToolDirPrefix(cmd.path)) and any other places that
perform the same replacement to ensure a single, well-named implementation and
avoid divergence.
- Around line 80-84: The field this.globalMode assigned in the constructor of
the class (via UpdateCommandOptions) is never read; either remove the globalMode
property and its assignment from the constructor to eliminate dead code, or if
you intend it for future routing inside execute(), keep the property but add a
brief comment above the declaration explaining its planned use and ensure
execute() will consult globalMode (or wire execute() to call executeGlobal()
when true) so the field is actually used; update references around the
constructor, the globalMode declaration, and any call sites of
execute()/executeGlobal() accordingly.
In `@test/core/global-init.test.ts`:
- Around line 184-189: Add an assertion that the guidance message is printed
when no global files exist by checking the existing console.log spy after
calling UpdateCommand.executeGlobal(); specifically, after creating const
updateCommand = new UpdateCommand({ global: true }) and awaiting
updateCommand.executeGlobal(), assert that console.log was called with the
expected guidance string (the user-facing message produced by executeGlobal) so
the test verifies output as well as absence of exceptions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/core/update.ts (1)
723-774: Consider cleanup/rehydration when delivery or workflow set changes.
Global update only rewrites files that already exist (hasSkills/hasCommands) and never removes deselected artifacts. If profiles/delivery change, stale global skills/commands remain and newly selected ones won’t be created. Consider mirroring local update’s cleanup logic underglobalRoot(remove deselected, drop commands on skills-only, etc.).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/update.ts` around lines 723 - 774, The global update loop (using globalAdapters, resolveGlobalRoot) only writes files that already exist via hasSkills/hasCommands and never removes stale artifacts, so stale global skills/commands persist when profiles/delivery change; update the logic to mirror the local update cleanup/rehydration: compute the desired set from generatedCommands and skillTemplates (respecting shouldGenerateCommands/shouldGenerateSkills), remove any existing global artifacts under skillsDir that do not match the current skillTemplates (e.g., openspec-* dirs and SKILL.md files), delete global generated command files that are not present in generatedCommands, and create any newly selected skills/commands even if they didn’t previously exist before calling FileSystemUtils.writeFile; ensure this cleanup happens per adapter/globalRoot before writing new files so stale items are removed and new selections are created.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/core/update.ts`:
- Around line 713-721: The global update path is passing unfiltered profile
workflows into generators; ensure you filter to known workflow IDs like in
execute(). After computing workflows (from getProfileWorkflows),
intersect/filter that array against ALL_WORKFLOWS before calling
getSkillTemplates and getCommandContents (used in executeGlobal), so change the
code around workflows -> shouldGenerateSkills/getSkillTemplates and
shouldGenerateCommands/getCommandContents to use the filtered list (reference
symbols: executeGlobal, workflows, ALL_WORKFLOWS, getSkillTemplates,
getCommandContents, getProfileWorkflows).
---
Nitpick comments:
In `@src/core/update.ts`:
- Around line 723-774: The global update loop (using globalAdapters,
resolveGlobalRoot) only writes files that already exist via
hasSkills/hasCommands and never removes stale artifacts, so stale global
skills/commands persist when profiles/delivery change; update the logic to
mirror the local update cleanup/rehydration: compute the desired set from
generatedCommands and skillTemplates (respecting
shouldGenerateCommands/shouldGenerateSkills), remove any existing global
artifacts under skillsDir that do not match the current skillTemplates (e.g.,
openspec-* dirs and SKILL.md files), delete global generated command files that
are not present in generatedCommands, and create any newly selected
skills/commands even if they didn’t previously exist before calling
FileSystemUtils.writeFile; ensure this cleanup happens per adapter/globalRoot
before writing new files so stale items are removed and new selections are
created.
Install skills and commands to tool global directories (~/.claude/, ~/.config/opencode/, ~/.codex/) instead of project directories. This lets consultants and multi-repo developers install once globally rather than per-project.
getGlobalRoot()method to ToolCommandAdapter interface/.claude/), OpenCode (XDG-aware), Codex (/.codex/)--globalflag toopenspec init(requires--tools)--globalflag toopenspec updateresolveGlobalRoot()helper with OPENSPEC_GLOBAL_ROOT env var overridegetGlobalAdapters()registry methodI tested OpenCode and Claude manually locally and I can use them in repos with no openspec folders within. I was able to use /openspec-propose from within an existing repo with had never had
openspec initrun and OpenSpec generated the files in the correct places.Closes #752
Summary by CodeRabbit
New Features
Documentation
Tests