sauron-cli 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (671) hide show
  1. package/.agents/rules/memory.md +156 -0
  2. package/.agents/skills/wiki/SKILL.md +172 -0
  3. package/.aider.instructions.md +152 -0
  4. package/.cursor/rules/sauron-memory.mdc +157 -0
  5. package/.genesis/prompts/prompt-onboarding-inteligente.md +45 -0
  6. package/.genesis/prompts/prompt-sauron-vs-openspec-arquitetura.md +106 -0
  7. package/.github/workflows/ci.yml +27 -0
  8. package/.sauron/.manifest.json +2 -1
  9. package/.sauron/wiki/history/implementacao-scanner-inteligente.md +29 -0
  10. package/.sauron/wiki/history/reestruturacao-arquitetural-core.md +29 -0
  11. package/.sauron/wiki/history/resiliencia-caminho-templates.md +29 -0
  12. package/.sauron/wiki/standards/typescript.rules.md +21 -0
  13. package/.sauron/wiki/summary.json +81 -0
  14. package/.windsurfrules +160 -0
  15. package/55ea973d-e255-4f08-a313-a5d68243bd2d.png +0 -0
  16. package/Evolu/303/247/303/243o Arquitetural do Sauron CLI.md" +97 -0
  17. package/OpenSpec-main/.actrc +1 -0
  18. package/OpenSpec-main/.changeset/README.md +97 -0
  19. package/OpenSpec-main/.changeset/config.json +15 -0
  20. package/OpenSpec-main/.coderabbit.yaml +11 -0
  21. package/OpenSpec-main/.devcontainer/README.md +92 -0
  22. package/OpenSpec-main/.devcontainer/devcontainer.json +68 -0
  23. package/OpenSpec-main/.github/CODEOWNERS +2 -0
  24. package/OpenSpec-main/.github/workflows/README.md +20 -0
  25. package/OpenSpec-main/.github/workflows/ci.yml +346 -0
  26. package/OpenSpec-main/.github/workflows/release-prepare.yml +60 -0
  27. package/OpenSpec-main/CHANGELOG.md +594 -0
  28. package/OpenSpec-main/LICENSE +22 -0
  29. package/OpenSpec-main/MAINTAINERS.md +17 -0
  30. package/OpenSpec-main/README.md +206 -0
  31. package/OpenSpec-main/README_OLD.md +475 -0
  32. package/OpenSpec-main/assets/openspec_bg.png +0 -0
  33. package/OpenSpec-main/assets/openspec_dashboard.png +0 -0
  34. package/OpenSpec-main/assets/openspec_pixel_dark.svg +89 -0
  35. package/OpenSpec-main/assets/openspec_pixel_light.svg +89 -0
  36. package/OpenSpec-main/bin/openspec.js +5 -0
  37. package/OpenSpec-main/build.js +31 -0
  38. package/OpenSpec-main/eslint.config.js +42 -0
  39. package/OpenSpec-main/flake.lock +27 -0
  40. package/OpenSpec-main/flake.nix +114 -0
  41. package/OpenSpec-main/openspec/changes/IMPLEMENTATION_ORDER.md +68 -0
  42. package/OpenSpec-main/openspec/changes/add-artifact-regeneration-support/proposal.md +136 -0
  43. package/OpenSpec-main/openspec/changes/add-change-stacking-awareness/.openspec.yaml +2 -0
  44. package/OpenSpec-main/openspec/changes/add-change-stacking-awareness/proposal.md +93 -0
  45. package/OpenSpec-main/openspec/changes/add-change-stacking-awareness/specs/change-creation/spec.md +15 -0
  46. package/OpenSpec-main/openspec/changes/add-change-stacking-awareness/specs/change-stacking-workflow/spec.md +65 -0
  47. package/OpenSpec-main/openspec/changes/add-change-stacking-awareness/specs/cli-change/spec.md +27 -0
  48. package/OpenSpec-main/openspec/changes/add-change-stacking-awareness/specs/openspec-conventions/spec.md +29 -0
  49. package/OpenSpec-main/openspec/changes/add-change-stacking-awareness/tasks.md +39 -0
  50. package/OpenSpec-main/openspec/changes/add-global-install-scope/.openspec.yaml +2 -0
  51. package/OpenSpec-main/openspec/changes/add-global-install-scope/design.md +161 -0
  52. package/OpenSpec-main/openspec/changes/add-global-install-scope/proposal.md +101 -0
  53. package/OpenSpec-main/openspec/changes/add-global-install-scope/specs/ai-tool-paths/spec.md +35 -0
  54. package/OpenSpec-main/openspec/changes/add-global-install-scope/specs/cli-config/spec.md +21 -0
  55. package/OpenSpec-main/openspec/changes/add-global-install-scope/specs/cli-init/spec.md +28 -0
  56. package/OpenSpec-main/openspec/changes/add-global-install-scope/specs/cli-update/spec.md +34 -0
  57. package/OpenSpec-main/openspec/changes/add-global-install-scope/specs/command-generation/spec.md +22 -0
  58. package/OpenSpec-main/openspec/changes/add-global-install-scope/specs/global-config/spec.md +24 -0
  59. package/OpenSpec-main/openspec/changes/add-global-install-scope/specs/installation-scope/spec.md +71 -0
  60. package/OpenSpec-main/openspec/changes/add-global-install-scope/tasks.md +61 -0
  61. package/OpenSpec-main/openspec/changes/add-qa-smoke-harness/.openspec.yaml +2 -0
  62. package/OpenSpec-main/openspec/changes/add-qa-smoke-harness/proposal.md +45 -0
  63. package/OpenSpec-main/openspec/changes/add-qa-smoke-harness/specs/developer-qa-workflow/spec.md +49 -0
  64. package/OpenSpec-main/openspec/changes/add-tool-command-surface-capabilities/.openspec.yaml +2 -0
  65. package/OpenSpec-main/openspec/changes/add-tool-command-surface-capabilities/proposal.md +111 -0
  66. package/OpenSpec-main/openspec/changes/add-tool-command-surface-capabilities/specs/cli-init/spec.md +121 -0
  67. package/OpenSpec-main/openspec/changes/add-tool-command-surface-capabilities/specs/cli-update/spec.md +48 -0
  68. package/OpenSpec-main/openspec/changes/add-tool-command-surface-capabilities/tasks.md +53 -0
  69. package/OpenSpec-main/openspec/changes/archive/2025-01-11-add-update-command/design.md +86 -0
  70. package/OpenSpec-main/openspec/changes/archive/2025-01-11-add-update-command/proposal.md +29 -0
  71. package/OpenSpec-main/openspec/changes/archive/2025-01-11-add-update-command/specs/cli-update/spec.md +59 -0
  72. package/OpenSpec-main/openspec/changes/archive/2025-01-11-add-update-command/tasks.md +20 -0
  73. package/OpenSpec-main/openspec/changes/archive/2025-01-13-add-list-command/proposal.md +20 -0
  74. package/OpenSpec-main/openspec/changes/archive/2025-01-13-add-list-command/specs/cli-list/spec.md +69 -0
  75. package/OpenSpec-main/openspec/changes/archive/2025-01-13-add-list-command/tasks.md +26 -0
  76. package/OpenSpec-main/openspec/changes/archive/2025-08-05-initialize-typescript-project/design.md +64 -0
  77. package/OpenSpec-main/openspec/changes/archive/2025-08-05-initialize-typescript-project/proposal.md +18 -0
  78. package/OpenSpec-main/openspec/changes/archive/2025-08-05-initialize-typescript-project/tasks.md +25 -0
  79. package/OpenSpec-main/openspec/changes/archive/2025-08-06-add-init-command/design.md +104 -0
  80. package/OpenSpec-main/openspec/changes/archive/2025-08-06-add-init-command/proposal.md +30 -0
  81. package/OpenSpec-main/openspec/changes/archive/2025-08-06-add-init-command/specs/cli-init/spec.md +148 -0
  82. package/OpenSpec-main/openspec/changes/archive/2025-08-06-add-init-command/tasks.md +38 -0
  83. package/OpenSpec-main/openspec/changes/archive/2025-08-06-adopt-future-state-storage/proposal.md +24 -0
  84. package/OpenSpec-main/openspec/changes/archive/2025-08-06-adopt-future-state-storage/specs/openspec-conventions/spec.md +120 -0
  85. package/OpenSpec-main/openspec/changes/archive/2025-08-06-adopt-future-state-storage/tasks.md +38 -0
  86. package/OpenSpec-main/openspec/changes/archive/2025-08-11-add-complexity-guidelines/proposal.md +13 -0
  87. package/OpenSpec-main/openspec/changes/archive/2025-08-11-add-complexity-guidelines/specs/openspec-docs/README.md +472 -0
  88. package/OpenSpec-main/openspec/changes/archive/2025-08-11-add-complexity-guidelines/tasks.md +9 -0
  89. package/OpenSpec-main/openspec/changes/archive/2025-08-13-add-archive-command/proposal.md +15 -0
  90. package/OpenSpec-main/openspec/changes/archive/2025-08-13-add-archive-command/specs/cli-archive/spec.md +111 -0
  91. package/OpenSpec-main/openspec/changes/archive/2025-08-13-add-archive-command/tasks.md +44 -0
  92. package/OpenSpec-main/openspec/changes/archive/2025-08-13-add-diff-command/proposal.md +19 -0
  93. package/OpenSpec-main/openspec/changes/archive/2025-08-13-add-diff-command/specs/cli-diff/spec.md +77 -0
  94. package/OpenSpec-main/openspec/changes/archive/2025-08-13-add-diff-command/tasks.md +23 -0
  95. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-change-commands/design.md +56 -0
  96. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-change-commands/proposal.md +17 -0
  97. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-change-commands/specs/cli-change/spec.md +48 -0
  98. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-change-commands/specs/cli-list/spec.md +12 -0
  99. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-change-commands/tasks.md +34 -0
  100. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-interactive-show-command/proposal.md +20 -0
  101. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-interactive-show-command/specs/cli-change/spec.md +23 -0
  102. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-interactive-show-command/specs/cli-show/spec.md +83 -0
  103. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-interactive-show-command/specs/cli-spec/spec.md +23 -0
  104. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-interactive-show-command/tasks.md +142 -0
  105. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-skip-specs-archive-option/proposal.md +13 -0
  106. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-skip-specs-archive-option/specs/cli-archive/spec.md +191 -0
  107. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-skip-specs-archive-option/tasks.md +57 -0
  108. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-spec-commands/design.md +45 -0
  109. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-spec-commands/proposal.md +19 -0
  110. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-spec-commands/specs/cli-spec/spec.md +43 -0
  111. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-spec-commands/tasks.md +22 -0
  112. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-zod-validation/design.md +104 -0
  113. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-zod-validation/proposal.md +22 -0
  114. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-zod-validation/specs/cli-archive/spec.md +18 -0
  115. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-zod-validation/specs/cli-diff/spec.md +12 -0
  116. package/OpenSpec-main/openspec/changes/archive/2025-08-19-add-zod-validation/tasks.md +59 -0
  117. package/OpenSpec-main/openspec/changes/archive/2025-08-19-adopt-delta-based-changes/proposal.md +93 -0
  118. package/OpenSpec-main/openspec/changes/archive/2025-08-19-adopt-delta-based-changes/specs/cli-archive/spec.md +48 -0
  119. package/OpenSpec-main/openspec/changes/archive/2025-08-19-adopt-delta-based-changes/specs/cli-diff/spec.md +45 -0
  120. package/OpenSpec-main/openspec/changes/archive/2025-08-19-adopt-delta-based-changes/specs/openspec-conventions/spec.md +101 -0
  121. package/OpenSpec-main/openspec/changes/archive/2025-08-19-adopt-delta-based-changes/tasks.md +55 -0
  122. package/OpenSpec-main/openspec/changes/archive/2025-08-19-adopt-verb-noun-cli-structure/design.md +19 -0
  123. package/OpenSpec-main/openspec/changes/archive/2025-08-19-adopt-verb-noun-cli-structure/proposal.md +67 -0
  124. package/OpenSpec-main/openspec/changes/archive/2025-08-19-adopt-verb-noun-cli-structure/specs/cli-list/spec.md +57 -0
  125. package/OpenSpec-main/openspec/changes/archive/2025-08-19-adopt-verb-noun-cli-structure/specs/openspec-conventions/spec.md +23 -0
  126. package/OpenSpec-main/openspec/changes/archive/2025-08-19-adopt-verb-noun-cli-structure/tasks.md +27 -0
  127. package/OpenSpec-main/openspec/changes/archive/2025-08-19-bulk-validation-interactive-selection/proposal.md +20 -0
  128. package/OpenSpec-main/openspec/changes/archive/2025-08-19-bulk-validation-interactive-selection/specs/cli-change/spec.md +22 -0
  129. package/OpenSpec-main/openspec/changes/archive/2025-08-19-bulk-validation-interactive-selection/specs/cli-spec/spec.md +23 -0
  130. package/OpenSpec-main/openspec/changes/archive/2025-08-19-bulk-validation-interactive-selection/specs/cli-validate/spec.md +149 -0
  131. package/OpenSpec-main/openspec/changes/archive/2025-08-19-bulk-validation-interactive-selection/tasks.md +81 -0
  132. package/OpenSpec-main/openspec/changes/archive/2025-08-19-fix-update-tool-selection/proposal.md +40 -0
  133. package/OpenSpec-main/openspec/changes/archive/2025-08-19-fix-update-tool-selection/specs/cli-update/spec.md +23 -0
  134. package/OpenSpec-main/openspec/changes/archive/2025-08-19-fix-update-tool-selection/tasks.md +21 -0
  135. package/OpenSpec-main/openspec/changes/archive/2025-08-19-improve-validate-error-messages/proposal.md +25 -0
  136. package/OpenSpec-main/openspec/changes/archive/2025-08-19-improve-validate-error-messages/specs/cli-validate/spec.md +55 -0
  137. package/OpenSpec-main/openspec/changes/archive/2025-08-19-improve-validate-error-messages/tasks.md +21 -0
  138. package/OpenSpec-main/openspec/changes/archive/2025-08-19-structured-spec-format/proposal.md +36 -0
  139. package/OpenSpec-main/openspec/changes/archive/2025-08-19-structured-spec-format/specs/openspec-conventions/spec.md +192 -0
  140. package/OpenSpec-main/openspec/changes/archive/2025-08-19-structured-spec-format/tasks.md +19 -0
  141. package/OpenSpec-main/openspec/changes/archive/2025-09-12-add-view-dashboard-command/proposal.md +38 -0
  142. package/OpenSpec-main/openspec/changes/archive/2025-09-12-add-view-dashboard-command/specs/cli-view/spec.md +109 -0
  143. package/OpenSpec-main/openspec/changes/archive/2025-09-12-add-view-dashboard-command/tasks.md +47 -0
  144. package/OpenSpec-main/openspec/changes/archive/2025-09-29-add-agents-md-config/proposal.md +28 -0
  145. package/OpenSpec-main/openspec/changes/archive/2025-09-29-add-agents-md-config/specs/cli-init/spec.md +71 -0
  146. package/OpenSpec-main/openspec/changes/archive/2025-09-29-add-agents-md-config/specs/cli-update/spec.md +41 -0
  147. package/OpenSpec-main/openspec/changes/archive/2025-09-29-add-agents-md-config/tasks.md +17 -0
  148. package/OpenSpec-main/openspec/changes/archive/2025-09-29-add-multi-agent-init/proposal.md +35 -0
  149. package/OpenSpec-main/openspec/changes/archive/2025-09-29-add-multi-agent-init/specs/cli-init/spec.md +45 -0
  150. package/OpenSpec-main/openspec/changes/archive/2025-09-29-add-multi-agent-init/tasks.md +16 -0
  151. package/OpenSpec-main/openspec/changes/archive/2025-09-29-add-slash-command-support/proposal.md +119 -0
  152. package/OpenSpec-main/openspec/changes/archive/2025-09-29-add-slash-command-support/specs/cli-init/spec.md +21 -0
  153. package/OpenSpec-main/openspec/changes/archive/2025-09-29-add-slash-command-support/specs/cli-update/spec.md +22 -0
  154. package/OpenSpec-main/openspec/changes/archive/2025-09-29-add-slash-command-support/tasks.md +20 -0
  155. package/OpenSpec-main/openspec/changes/archive/2025-09-29-improve-cli-e2e-plan/proposal.md +19 -0
  156. package/OpenSpec-main/openspec/changes/archive/2025-09-29-improve-cli-e2e-plan/tasks.md +9 -0
  157. package/OpenSpec-main/openspec/changes/archive/2025-09-29-improve-deterministic-tests/proposal.md +78 -0
  158. package/OpenSpec-main/openspec/changes/archive/2025-09-29-improve-deterministic-tests/tasks.md +25 -0
  159. package/OpenSpec-main/openspec/changes/archive/2025-09-29-improve-init-onboarding/proposal.md +13 -0
  160. package/OpenSpec-main/openspec/changes/archive/2025-09-29-improve-init-onboarding/specs/cli-init/spec.md +92 -0
  161. package/OpenSpec-main/openspec/changes/archive/2025-09-29-improve-init-onboarding/tasks.md +12 -0
  162. package/OpenSpec-main/openspec/changes/archive/2025-09-29-remove-diff-command/proposal.md +81 -0
  163. package/OpenSpec-main/openspec/changes/archive/2025-09-29-remove-diff-command/tasks.md +37 -0
  164. package/OpenSpec-main/openspec/changes/archive/2025-09-29-sort-active-changes-by-progress/proposal.md +25 -0
  165. package/OpenSpec-main/openspec/changes/archive/2025-09-29-sort-active-changes-by-progress/specs/cli-view/spec.md +9 -0
  166. package/OpenSpec-main/openspec/changes/archive/2025-09-29-sort-active-changes-by-progress/tasks.md +8 -0
  167. package/OpenSpec-main/openspec/changes/archive/2025-09-29-update-agent-file-name/proposal.md +29 -0
  168. package/OpenSpec-main/openspec/changes/archive/2025-09-29-update-agent-file-name/specs/cli-init/spec.md +40 -0
  169. package/OpenSpec-main/openspec/changes/archive/2025-09-29-update-agent-file-name/specs/cli-update/spec.md +22 -0
  170. package/OpenSpec-main/openspec/changes/archive/2025-09-29-update-agent-file-name/specs/openspec-conventions/spec.md +27 -0
  171. package/OpenSpec-main/openspec/changes/archive/2025-09-29-update-agent-file-name/tasks.md +22 -0
  172. package/OpenSpec-main/openspec/changes/archive/2025-09-29-update-agent-instructions/design.md +130 -0
  173. package/OpenSpec-main/openspec/changes/archive/2025-09-29-update-agent-instructions/proposal.md +117 -0
  174. package/OpenSpec-main/openspec/changes/archive/2025-09-29-update-agent-instructions/tasks.md +69 -0
  175. package/OpenSpec-main/openspec/changes/archive/2025-09-29-update-markdown-parser-crlf/proposal.md +19 -0
  176. package/OpenSpec-main/openspec/changes/archive/2025-09-29-update-markdown-parser-crlf/specs/cli-validate/spec.md +9 -0
  177. package/OpenSpec-main/openspec/changes/archive/2025-09-29-update-markdown-parser-crlf/tasks.md +11 -0
  178. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-codex-slash-command-support/proposal.md +25 -0
  179. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-codex-slash-command-support/specs/cli-init/spec.md +56 -0
  180. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-codex-slash-command-support/specs/cli-update/spec.md +41 -0
  181. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-codex-slash-command-support/tasks.md +19 -0
  182. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-github-copilot-prompts/proposal.md +25 -0
  183. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-github-copilot-prompts/specs/cli-init/spec.md +48 -0
  184. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-github-copilot-prompts/specs/cli-update/spec.md +48 -0
  185. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-github-copilot-prompts/tasks.md +30 -0
  186. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-kilocode-workflows/proposal.md +17 -0
  187. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-kilocode-workflows/specs/cli-init/spec.md +43 -0
  188. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-kilocode-workflows/specs/cli-update/spec.md +27 -0
  189. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-kilocode-workflows/tasks.md +15 -0
  190. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-non-interactive-init-options/proposal.md +12 -0
  191. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-non-interactive-init-options/specs/cli-init/spec.md +39 -0
  192. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-non-interactive-init-options/tasks.md +17 -0
  193. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-windsurf-workflows/proposal.md +17 -0
  194. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-windsurf-workflows/specs/cli-init/spec.md +42 -0
  195. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-windsurf-workflows/specs/cli-update/spec.md +27 -0
  196. package/OpenSpec-main/openspec/changes/archive/2025-10-14-add-windsurf-workflows/tasks.md +17 -0
  197. package/OpenSpec-main/openspec/changes/archive/2025-10-14-enhance-validation-error-messages/proposal.md +12 -0
  198. package/OpenSpec-main/openspec/changes/archive/2025-10-14-enhance-validation-error-messages/specs/cli-validate/spec.md +39 -0
  199. package/OpenSpec-main/openspec/changes/archive/2025-10-14-enhance-validation-error-messages/tasks.md +12 -0
  200. package/OpenSpec-main/openspec/changes/archive/2025-10-14-improve-agent-instruction-usability/proposal.md +12 -0
  201. package/OpenSpec-main/openspec/changes/archive/2025-10-14-improve-agent-instruction-usability/specs/docs-agent-instructions/spec.md +33 -0
  202. package/OpenSpec-main/openspec/changes/archive/2025-10-14-improve-agent-instruction-usability/tasks.md +11 -0
  203. package/OpenSpec-main/openspec/changes/archive/2025-10-14-slim-root-agents-file/proposal.md +13 -0
  204. package/OpenSpec-main/openspec/changes/archive/2025-10-14-slim-root-agents-file/tasks.md +15 -0
  205. package/OpenSpec-main/openspec/changes/archive/2025-10-14-update-cli-init-enter-selection/proposal.md +14 -0
  206. package/OpenSpec-main/openspec/changes/archive/2025-10-14-update-cli-init-enter-selection/specs/cli-init/spec.md +10 -0
  207. package/OpenSpec-main/openspec/changes/archive/2025-10-14-update-cli-init-enter-selection/tasks.md +8 -0
  208. package/OpenSpec-main/openspec/changes/archive/2025-10-14-update-cli-init-root-agents/proposal.md +15 -0
  209. package/OpenSpec-main/openspec/changes/archive/2025-10-14-update-cli-init-root-agents/specs/cli-init/spec.md +32 -0
  210. package/OpenSpec-main/openspec/changes/archive/2025-10-14-update-cli-init-root-agents/specs/cli-update/spec.md +10 -0
  211. package/OpenSpec-main/openspec/changes/archive/2025-10-14-update-cli-init-root-agents/tasks.md +11 -0
  212. package/OpenSpec-main/openspec/changes/archive/2025-10-14-update-release-automation/proposal.md +49 -0
  213. package/OpenSpec-main/openspec/changes/archive/2025-10-14-update-release-automation/tasks.md +12 -0
  214. package/OpenSpec-main/openspec/changes/archive/2025-10-22-add-archive-command-arguments/proposal.md +17 -0
  215. package/OpenSpec-main/openspec/changes/archive/2025-10-22-add-archive-command-arguments/specs/cli-update/spec.md +32 -0
  216. package/OpenSpec-main/openspec/changes/archive/2025-10-22-add-archive-command-arguments/tasks.md +15 -0
  217. package/OpenSpec-main/openspec/changes/archive/2025-10-22-add-cline-support/proposal.md +15 -0
  218. package/OpenSpec-main/openspec/changes/archive/2025-10-22-add-cline-support/specs/cli-init/spec.md +97 -0
  219. package/OpenSpec-main/openspec/changes/archive/2025-10-22-add-cline-support/tasks.md +19 -0
  220. package/OpenSpec-main/openspec/changes/archive/2025-10-22-add-crush-support/proposal.md +13 -0
  221. package/OpenSpec-main/openspec/changes/archive/2025-10-22-add-crush-support/specs/cli-init/spec.md +67 -0
  222. package/OpenSpec-main/openspec/changes/archive/2025-10-22-add-crush-support/tasks.md +7 -0
  223. package/OpenSpec-main/openspec/changes/archive/2025-10-22-add-factory-slash-commands/proposal.md +12 -0
  224. package/OpenSpec-main/openspec/changes/archive/2025-10-22-add-factory-slash-commands/specs/cli-init/spec.md +54 -0
  225. package/OpenSpec-main/openspec/changes/archive/2025-10-22-add-factory-slash-commands/specs/cli-update/spec.md +54 -0
  226. package/OpenSpec-main/openspec/changes/archive/2025-10-22-add-factory-slash-commands/tasks.md +11 -0
  227. package/OpenSpec-main/openspec/changes/archive/2025-11-06-add-shell-completions/design.md +525 -0
  228. package/OpenSpec-main/openspec/changes/archive/2025-11-06-add-shell-completions/proposal.md +29 -0
  229. package/OpenSpec-main/openspec/changes/archive/2025-11-06-add-shell-completions/specs/cli-completion/spec.md +300 -0
  230. package/OpenSpec-main/openspec/changes/archive/2025-11-06-add-shell-completions/tasks.md +81 -0
  231. package/OpenSpec-main/openspec/changes/archive/2025-12-20-add-global-config-dir/design.md +105 -0
  232. package/OpenSpec-main/openspec/changes/archive/2025-12-20-add-global-config-dir/proposal.md +20 -0
  233. package/OpenSpec-main/openspec/changes/archive/2025-12-20-add-global-config-dir/specs/global-config/spec.md +76 -0
  234. package/OpenSpec-main/openspec/changes/archive/2025-12-20-add-global-config-dir/tasks.md +26 -0
  235. package/OpenSpec-main/openspec/changes/archive/2025-12-21-add-config-command/design.md +89 -0
  236. package/OpenSpec-main/openspec/changes/archive/2025-12-21-add-config-command/proposal.md +60 -0
  237. package/OpenSpec-main/openspec/changes/archive/2025-12-21-add-config-command/specs/cli-config/spec.md +213 -0
  238. package/OpenSpec-main/openspec/changes/archive/2025-12-21-add-config-command/tasks.md +28 -0
  239. package/OpenSpec-main/openspec/changes/archive/2025-12-23-extend-shell-completions/proposal.md +15 -0
  240. package/OpenSpec-main/openspec/changes/archive/2025-12-23-extend-shell-completions/specs/cli-completion/spec.md +328 -0
  241. package/OpenSpec-main/openspec/changes/archive/2025-12-23-extend-shell-completions/tasks.md +49 -0
  242. package/OpenSpec-main/openspec/changes/archive/2025-12-24-add-artifact-graph-core/design.md +197 -0
  243. package/OpenSpec-main/openspec/changes/archive/2025-12-24-add-artifact-graph-core/proposal.md +18 -0
  244. package/OpenSpec-main/openspec/changes/archive/2025-12-24-add-artifact-graph-core/specs/artifact-graph/spec.md +103 -0
  245. package/OpenSpec-main/openspec/changes/archive/2025-12-24-add-artifact-graph-core/tasks.md +61 -0
  246. package/OpenSpec-main/openspec/changes/archive/2025-12-25-add-change-manager/design.md +74 -0
  247. package/OpenSpec-main/openspec/changes/archive/2025-12-25-add-change-manager/proposal.md +45 -0
  248. package/OpenSpec-main/openspec/changes/archive/2025-12-25-add-change-manager/specs/change-creation/spec.md +63 -0
  249. package/OpenSpec-main/openspec/changes/archive/2025-12-25-add-change-manager/tasks.md +30 -0
  250. package/OpenSpec-main/openspec/changes/archive/2025-12-28-add-artifact-workflow-cli/design.md +112 -0
  251. package/OpenSpec-main/openspec/changes/archive/2025-12-28-add-artifact-workflow-cli/proposal.md +33 -0
  252. package/OpenSpec-main/openspec/changes/archive/2025-12-28-add-artifact-workflow-cli/specs/cli-artifact-workflow/spec.md +153 -0
  253. package/OpenSpec-main/openspec/changes/archive/2025-12-28-add-artifact-workflow-cli/tasks.md +48 -0
  254. package/OpenSpec-main/openspec/changes/archive/2025-12-28-add-instruction-loader/design.md +149 -0
  255. package/OpenSpec-main/openspec/changes/archive/2025-12-28-add-instruction-loader/proposal.md +20 -0
  256. package/OpenSpec-main/openspec/changes/archive/2025-12-28-add-instruction-loader/specs/instruction-loader/spec.md +70 -0
  257. package/OpenSpec-main/openspec/changes/archive/2025-12-28-add-instruction-loader/tasks.md +13 -0
  258. package/OpenSpec-main/openspec/changes/archive/2025-12-28-restructure-schema-directories/design.md +129 -0
  259. package/OpenSpec-main/openspec/changes/archive/2025-12-28-restructure-schema-directories/proposal.md +20 -0
  260. package/OpenSpec-main/openspec/changes/archive/2025-12-28-restructure-schema-directories/specs/artifact-graph/spec.md +49 -0
  261. package/OpenSpec-main/openspec/changes/archive/2025-12-28-restructure-schema-directories/tasks.md +32 -0
  262. package/OpenSpec-main/openspec/changes/archive/2025-12-29-unify-change-state-model/design.md +151 -0
  263. package/OpenSpec-main/openspec/changes/archive/2025-12-29-unify-change-state-model/proposal.md +101 -0
  264. package/OpenSpec-main/openspec/changes/archive/2025-12-29-unify-change-state-model/specs/cli-artifact-workflow/spec.md +109 -0
  265. package/OpenSpec-main/openspec/changes/archive/2025-12-29-unify-change-state-model/specs/cli-view/spec.md +60 -0
  266. package/OpenSpec-main/openspec/changes/archive/2025-12-29-unify-change-state-model/tasks.md +25 -0
  267. package/OpenSpec-main/openspec/changes/archive/2025-12-30-add-antigravity-support/proposal.md +11 -0
  268. package/OpenSpec-main/openspec/changes/archive/2025-12-30-add-antigravity-support/specs/cli-init/spec.md +105 -0
  269. package/OpenSpec-main/openspec/changes/archive/2025-12-30-add-antigravity-support/specs/cli-update/spec.md +92 -0
  270. package/OpenSpec-main/openspec/changes/archive/2025-12-30-add-antigravity-support/tasks.md +12 -0
  271. package/OpenSpec-main/openspec/changes/archive/2025-12-30-fix-cline-workflows-implementation/proposal.md +13 -0
  272. package/OpenSpec-main/openspec/changes/archive/2025-12-30-fix-cline-workflows-implementation/specs/cli-init/spec.md +105 -0
  273. package/OpenSpec-main/openspec/changes/archive/2025-12-30-fix-cline-workflows-implementation/specs/cli-update/spec.md +92 -0
  274. package/OpenSpec-main/openspec/changes/archive/2025-12-30-fix-cline-workflows-implementation/tasks.md +13 -0
  275. package/OpenSpec-main/openspec/changes/archive/2026-01-06-add-agent-schema-selection/proposal.md +26 -0
  276. package/OpenSpec-main/openspec/changes/archive/2026-01-06-add-agent-schema-selection/tasks.md +32 -0
  277. package/OpenSpec-main/openspec/changes/archive/2026-01-06-add-per-change-schema-metadata/design.md +147 -0
  278. package/OpenSpec-main/openspec/changes/archive/2026-01-06-add-per-change-schema-metadata/proposal.md +29 -0
  279. package/OpenSpec-main/openspec/changes/archive/2026-01-06-add-per-change-schema-metadata/specs/cli-artifact-workflow/spec.md +98 -0
  280. package/OpenSpec-main/openspec/changes/archive/2026-01-06-add-per-change-schema-metadata/tasks.md +29 -0
  281. package/OpenSpec-main/openspec/changes/archive/2026-01-06-add-specs-apply-command/.openspec.yaml +2 -0
  282. package/OpenSpec-main/openspec/changes/archive/2026-01-06-add-specs-apply-command/design.md +77 -0
  283. package/OpenSpec-main/openspec/changes/archive/2026-01-06-add-specs-apply-command/proposal.md +32 -0
  284. package/OpenSpec-main/openspec/changes/archive/2026-01-06-add-specs-apply-command/specs/specs-sync-skill/spec.md +67 -0
  285. package/OpenSpec-main/openspec/changes/archive/2026-01-06-add-specs-apply-command/tasks.md +40 -0
  286. package/OpenSpec-main/openspec/changes/archive/2026-01-06-make-apply-instructions-schema-aware/proposal.md +138 -0
  287. package/OpenSpec-main/openspec/changes/archive/2026-01-06-make-apply-instructions-schema-aware/specs/cli-artifact-workflow/spec.md +60 -0
  288. package/OpenSpec-main/openspec/changes/archive/2026-01-06-make-apply-instructions-schema-aware/tasks.md +35 -0
  289. package/OpenSpec-main/openspec/changes/archive/2026-01-06-opsx-archive-command/.openspec.yaml +2 -0
  290. package/OpenSpec-main/openspec/changes/archive/2026-01-06-opsx-archive-command/design.md +84 -0
  291. package/OpenSpec-main/openspec/changes/archive/2026-01-06-opsx-archive-command/proposal.md +28 -0
  292. package/OpenSpec-main/openspec/changes/archive/2026-01-06-opsx-archive-command/specs/opsx-archive-skill/spec.md +122 -0
  293. package/OpenSpec-main/openspec/changes/archive/2026-01-06-opsx-archive-command/tasks.md +23 -0
  294. package/OpenSpec-main/openspec/changes/archive/2026-01-07-add-nix-flake-support/.openspec.yaml +2 -0
  295. package/OpenSpec-main/openspec/changes/archive/2026-01-07-add-nix-flake-support/design.md +94 -0
  296. package/OpenSpec-main/openspec/changes/archive/2026-01-07-add-nix-flake-support/proposal.md +25 -0
  297. package/OpenSpec-main/openspec/changes/archive/2026-01-07-add-nix-flake-support/specs/nix-flake-support/spec.md +79 -0
  298. package/OpenSpec-main/openspec/changes/archive/2026-01-07-add-nix-flake-support/tasks.md +65 -0
  299. package/OpenSpec-main/openspec/changes/archive/2026-01-09-add-flake-update-script/.openspec.yaml +2 -0
  300. package/OpenSpec-main/openspec/changes/archive/2026-01-09-add-flake-update-script/design.md +117 -0
  301. package/OpenSpec-main/openspec/changes/archive/2026-01-09-add-flake-update-script/proposal.md +23 -0
  302. package/OpenSpec-main/openspec/changes/archive/2026-01-09-add-flake-update-script/specs/flake-update-script/spec.md +93 -0
  303. package/OpenSpec-main/openspec/changes/archive/2026-01-09-add-flake-update-script/tasks.md +55 -0
  304. package/OpenSpec-main/openspec/changes/archive/2026-01-09-add-posthog-analytics/.openspec.yaml +2 -0
  305. package/OpenSpec-main/openspec/changes/archive/2026-01-09-add-posthog-analytics/design.md +175 -0
  306. package/OpenSpec-main/openspec/changes/archive/2026-01-09-add-posthog-analytics/proposal.md +37 -0
  307. package/OpenSpec-main/openspec/changes/archive/2026-01-09-add-posthog-analytics/specs/global-config/spec.md +21 -0
  308. package/OpenSpec-main/openspec/changes/archive/2026-01-09-add-posthog-analytics/specs/telemetry/spec.md +116 -0
  309. package/OpenSpec-main/openspec/changes/archive/2026-01-09-add-posthog-analytics/tasks.md +47 -0
  310. package/OpenSpec-main/openspec/changes/archive/2026-01-09-fix-codebuddy-frontmatter-fields/proposal.md +16 -0
  311. package/OpenSpec-main/openspec/changes/archive/2026-01-09-fix-codebuddy-frontmatter-fields/specs/cli-init/spec.md +75 -0
  312. package/OpenSpec-main/openspec/changes/archive/2026-01-09-fix-codebuddy-frontmatter-fields/specs/cli-update/spec.md +56 -0
  313. package/OpenSpec-main/openspec/changes/archive/2026-01-09-fix-codebuddy-frontmatter-fields/tasks.md +6 -0
  314. package/OpenSpec-main/openspec/changes/archive/2026-01-15-add-nix-ci-validation/design.md +206 -0
  315. package/OpenSpec-main/openspec/changes/archive/2026-01-15-add-nix-ci-validation/proposal.md +21 -0
  316. package/OpenSpec-main/openspec/changes/archive/2026-01-15-add-nix-ci-validation/specs/ci-nix-validation/spec.md +104 -0
  317. package/OpenSpec-main/openspec/changes/archive/2026-01-15-add-nix-ci-validation/tasks.md +49 -0
  318. package/OpenSpec-main/openspec/changes/archive/2026-01-30-opencode-command-references/.openspec.yaml +2 -0
  319. package/OpenSpec-main/openspec/changes/archive/2026-01-30-opencode-command-references/README.md +3 -0
  320. package/OpenSpec-main/openspec/changes/archive/2026-01-30-opencode-command-references/design.md +70 -0
  321. package/OpenSpec-main/openspec/changes/archive/2026-01-30-opencode-command-references/proposal.md +32 -0
  322. package/OpenSpec-main/openspec/changes/archive/2026-01-30-opencode-command-references/specs/no-changes.md +9 -0
  323. package/OpenSpec-main/openspec/changes/archive/2026-01-30-opencode-command-references/tasks.md +22 -0
  324. package/OpenSpec-main/openspec/changes/archive/2026-02-17-add-feedback-command/proposal.md +20 -0
  325. package/OpenSpec-main/openspec/changes/archive/2026-02-17-add-feedback-command/specs/cli-feedback/spec.md +188 -0
  326. package/OpenSpec-main/openspec/changes/archive/2026-02-17-add-feedback-command/tasks.md +30 -0
  327. package/OpenSpec-main/openspec/changes/archive/2026-02-17-add-opsx-onboard-skill/.openspec.yaml +2 -0
  328. package/OpenSpec-main/openspec/changes/archive/2026-02-17-add-opsx-onboard-skill/design.md +115 -0
  329. package/OpenSpec-main/openspec/changes/archive/2026-02-17-add-opsx-onboard-skill/proposal.md +27 -0
  330. package/OpenSpec-main/openspec/changes/archive/2026-02-17-add-opsx-onboard-skill/specs/opsx-onboard-skill/spec.md +162 -0
  331. package/OpenSpec-main/openspec/changes/archive/2026-02-17-add-opsx-onboard-skill/tasks.md +21 -0
  332. package/OpenSpec-main/openspec/changes/archive/2026-02-17-add-verify-skill/design.md +96 -0
  333. package/OpenSpec-main/openspec/changes/archive/2026-02-17-add-verify-skill/proposal.md +48 -0
  334. package/OpenSpec-main/openspec/changes/archive/2026-02-17-add-verify-skill/specs/opsx-verify-skill/spec.md +190 -0
  335. package/OpenSpec-main/openspec/changes/archive/2026-02-17-add-verify-skill/tasks.md +15 -0
  336. package/OpenSpec-main/openspec/changes/archive/2026-02-17-merge-init-experimental/.openspec.yaml +2 -0
  337. package/OpenSpec-main/openspec/changes/archive/2026-02-17-merge-init-experimental/design.md +193 -0
  338. package/OpenSpec-main/openspec/changes/archive/2026-02-17-merge-init-experimental/proposal.md +32 -0
  339. package/OpenSpec-main/openspec/changes/archive/2026-02-17-merge-init-experimental/specs/cli-init/spec.md +176 -0
  340. package/OpenSpec-main/openspec/changes/archive/2026-02-17-merge-init-experimental/specs/legacy-cleanup/spec.md +158 -0
  341. package/OpenSpec-main/openspec/changes/archive/2026-02-17-merge-init-experimental/tasks.md +67 -0
  342. package/OpenSpec-main/openspec/changes/archive/2026-02-17-multi-provider-skill-generation/.openspec.yaml +2 -0
  343. package/OpenSpec-main/openspec/changes/archive/2026-02-17-multi-provider-skill-generation/design.md +144 -0
  344. package/OpenSpec-main/openspec/changes/archive/2026-02-17-multi-provider-skill-generation/proposal.md +36 -0
  345. package/OpenSpec-main/openspec/changes/archive/2026-02-17-multi-provider-skill-generation/specs/ai-tool-paths/spec.md +63 -0
  346. package/OpenSpec-main/openspec/changes/archive/2026-02-17-multi-provider-skill-generation/specs/cli-artifact-workflow/spec.md +60 -0
  347. package/OpenSpec-main/openspec/changes/archive/2026-02-17-multi-provider-skill-generation/specs/command-generation/spec.md +98 -0
  348. package/OpenSpec-main/openspec/changes/archive/2026-02-17-multi-provider-skill-generation/tasks.md +55 -0
  349. package/OpenSpec-main/openspec/changes/archive/2026-02-17-project-config/.openspec.yaml +2 -0
  350. package/OpenSpec-main/openspec/changes/archive/2026-02-17-project-config/design.md +665 -0
  351. package/OpenSpec-main/openspec/changes/archive/2026-02-17-project-config/proposal.md +774 -0
  352. package/OpenSpec-main/openspec/changes/archive/2026-02-17-project-config/specs/config-loading/spec.md +119 -0
  353. package/OpenSpec-main/openspec/changes/archive/2026-02-17-project-config/specs/context-injection/spec.md +51 -0
  354. package/OpenSpec-main/openspec/changes/archive/2026-02-17-project-config/specs/rules-injection/spec.md +99 -0
  355. package/OpenSpec-main/openspec/changes/archive/2026-02-17-project-config/specs/schema-resolution/spec.md +83 -0
  356. package/OpenSpec-main/openspec/changes/archive/2026-02-17-project-config/tasks.md +72 -0
  357. package/OpenSpec-main/openspec/changes/archive/2026-02-17-project-local-schemas/.openspec.yaml +2 -0
  358. package/OpenSpec-main/openspec/changes/archive/2026-02-17-project-local-schemas/design.md +117 -0
  359. package/OpenSpec-main/openspec/changes/archive/2026-02-17-project-local-schemas/proposal.md +167 -0
  360. package/OpenSpec-main/openspec/changes/archive/2026-02-17-project-local-schemas/specs/schema-resolution/spec.md +88 -0
  361. package/OpenSpec-main/openspec/changes/archive/2026-02-17-project-local-schemas/tasks.md +28 -0
  362. package/OpenSpec-main/openspec/changes/archive/2026-02-17-schema-management-cli/.openspec.yaml +2 -0
  363. package/OpenSpec-main/openspec/changes/archive/2026-02-17-schema-management-cli/design.md +113 -0
  364. package/OpenSpec-main/openspec/changes/archive/2026-02-17-schema-management-cli/proposal.md +55 -0
  365. package/OpenSpec-main/openspec/changes/archive/2026-02-17-schema-management-cli/specs/schema-fork-command/spec.md +66 -0
  366. package/OpenSpec-main/openspec/changes/archive/2026-02-17-schema-management-cli/specs/schema-init-command/spec.md +71 -0
  367. package/OpenSpec-main/openspec/changes/archive/2026-02-17-schema-management-cli/specs/schema-validate-command/spec.md +86 -0
  368. package/OpenSpec-main/openspec/changes/archive/2026-02-17-schema-management-cli/specs/schema-which-command/spec.md +65 -0
  369. package/OpenSpec-main/openspec/changes/archive/2026-02-17-schema-management-cli/tasks.md +67 -0
  370. package/OpenSpec-main/openspec/changes/archive/2026-04-23-add-kimi-cli-skills-only-support/.openspec.yaml +2 -0
  371. package/OpenSpec-main/openspec/changes/archive/2026-04-23-add-kimi-cli-skills-only-support/README.md +3 -0
  372. package/OpenSpec-main/openspec/changes/archive/2026-04-23-add-kimi-cli-skills-only-support/design.md +85 -0
  373. package/OpenSpec-main/openspec/changes/archive/2026-04-23-add-kimi-cli-skills-only-support/proposal.md +38 -0
  374. package/OpenSpec-main/openspec/changes/archive/2026-04-23-add-kimi-cli-skills-only-support/specs/ai-tool-paths/spec.md +12 -0
  375. package/OpenSpec-main/openspec/changes/archive/2026-04-23-add-kimi-cli-skills-only-support/specs/cli-init/spec.md +37 -0
  376. package/OpenSpec-main/openspec/changes/archive/2026-04-23-add-kimi-cli-skills-only-support/tasks.md +22 -0
  377. package/OpenSpec-main/openspec/changes/archive/2026-05-04-workspace-foundation/design.md +208 -0
  378. package/OpenSpec-main/openspec/changes/archive/2026-05-04-workspace-foundation/proposal.md +142 -0
  379. package/OpenSpec-main/openspec/changes/archive/2026-05-04-workspace-foundation/specs/openspec-conventions/spec.md +29 -0
  380. package/OpenSpec-main/openspec/changes/archive/2026-05-04-workspace-foundation/specs/workspace-foundation/spec.md +199 -0
  381. package/OpenSpec-main/openspec/changes/archive/2026-05-04-workspace-foundation/tasks.md +56 -0
  382. package/OpenSpec-main/openspec/changes/archive/2026-05-06-workspace-create-and-register-repos/design.md +356 -0
  383. package/OpenSpec-main/openspec/changes/archive/2026-05-06-workspace-create-and-register-repos/proposal.md +128 -0
  384. package/OpenSpec-main/openspec/changes/archive/2026-05-06-workspace-create-and-register-repos/specs/cli-artifact-workflow/spec.md +24 -0
  385. package/OpenSpec-main/openspec/changes/archive/2026-05-06-workspace-create-and-register-repos/specs/workspace-foundation/spec.md +35 -0
  386. package/OpenSpec-main/openspec/changes/archive/2026-05-06-workspace-create-and-register-repos/specs/workspace-links/spec.md +356 -0
  387. package/OpenSpec-main/openspec/changes/archive/2026-05-06-workspace-create-and-register-repos/tasks.md +121 -0
  388. package/OpenSpec-main/openspec/changes/archive/2026-05-06-workspace-open-agent-context/design.md +266 -0
  389. package/OpenSpec-main/openspec/changes/archive/2026-05-06-workspace-open-agent-context/proposal.md +65 -0
  390. package/OpenSpec-main/openspec/changes/archive/2026-05-06-workspace-open-agent-context/specs/workspace-foundation/spec.md +76 -0
  391. package/OpenSpec-main/openspec/changes/archive/2026-05-06-workspace-open-agent-context/specs/workspace-open/spec.md +199 -0
  392. package/OpenSpec-main/openspec/changes/archive/2026-05-06-workspace-open-agent-context/tasks.md +89 -0
  393. package/OpenSpec-main/openspec/changes/archive/2026-05-14-workspace-change-planning/design.md +242 -0
  394. package/OpenSpec-main/openspec/changes/archive/2026-05-14-workspace-change-planning/proposal.md +78 -0
  395. package/OpenSpec-main/openspec/changes/archive/2026-05-14-workspace-change-planning/specs/artifact-graph/spec.md +36 -0
  396. package/OpenSpec-main/openspec/changes/archive/2026-05-14-workspace-change-planning/specs/change-creation/spec.md +42 -0
  397. package/OpenSpec-main/openspec/changes/archive/2026-05-14-workspace-change-planning/specs/cli-artifact-workflow/spec.md +100 -0
  398. package/OpenSpec-main/openspec/changes/archive/2026-05-14-workspace-change-planning/specs/cli-config/spec.md +55 -0
  399. package/OpenSpec-main/openspec/changes/archive/2026-05-14-workspace-change-planning/specs/cli-update/spec.md +21 -0
  400. package/OpenSpec-main/openspec/changes/archive/2026-05-14-workspace-change-planning/specs/openspec-conventions/spec.md +32 -0
  401. package/OpenSpec-main/openspec/changes/archive/2026-05-14-workspace-change-planning/specs/schema-resolution/spec.md +25 -0
  402. package/OpenSpec-main/openspec/changes/archive/2026-05-14-workspace-change-planning/specs/workspace-change-planning/spec.md +67 -0
  403. package/OpenSpec-main/openspec/changes/archive/2026-05-14-workspace-change-planning/specs/workspace-links/spec.md +163 -0
  404. package/OpenSpec-main/openspec/changes/archive/2026-05-14-workspace-change-planning/tasks.md +133 -0
  405. package/OpenSpec-main/openspec/changes/fix-opencode-commands-directory/.openspec.yaml +2 -0
  406. package/OpenSpec-main/openspec/changes/fix-opencode-commands-directory/design.md +48 -0
  407. package/OpenSpec-main/openspec/changes/fix-opencode-commands-directory/proposal.md +26 -0
  408. package/OpenSpec-main/openspec/changes/fix-opencode-commands-directory/specs/command-generation/spec.md +63 -0
  409. package/OpenSpec-main/openspec/changes/fix-opencode-commands-directory/tasks.md +19 -0
  410. package/OpenSpec-main/openspec/changes/graceful-status-no-changes/.openspec.yaml +2 -0
  411. package/OpenSpec-main/openspec/changes/graceful-status-no-changes/design.md +38 -0
  412. package/OpenSpec-main/openspec/changes/graceful-status-no-changes/proposal.md +25 -0
  413. package/OpenSpec-main/openspec/changes/graceful-status-no-changes/specs/graceful-status-empty/spec.md +27 -0
  414. package/OpenSpec-main/openspec/changes/graceful-status-no-changes/tasks.md +16 -0
  415. package/OpenSpec-main/openspec/changes/schema-alias-support/.openspec.yaml +2 -0
  416. package/OpenSpec-main/openspec/changes/schema-alias-support/proposal.md +28 -0
  417. package/OpenSpec-main/openspec/changes/simplify-skill-installation/.openspec.yaml +2 -0
  418. package/OpenSpec-main/openspec/changes/simplify-skill-installation/design.md +288 -0
  419. package/OpenSpec-main/openspec/changes/simplify-skill-installation/proposal.md +202 -0
  420. package/OpenSpec-main/openspec/changes/simplify-skill-installation/specs/cli-init/spec.md +199 -0
  421. package/OpenSpec-main/openspec/changes/simplify-skill-installation/specs/cli-update/spec.md +177 -0
  422. package/OpenSpec-main/openspec/changes/simplify-skill-installation/specs/profiles/spec.md +142 -0
  423. package/OpenSpec-main/openspec/changes/simplify-skill-installation/specs/propose-workflow/spec.md +42 -0
  424. package/OpenSpec-main/openspec/changes/simplify-skill-installation/tasks.md +132 -0
  425. package/OpenSpec-main/openspec/changes/unify-template-generation-pipeline/.openspec.yaml +2 -0
  426. package/OpenSpec-main/openspec/changes/unify-template-generation-pipeline/design.md +149 -0
  427. package/OpenSpec-main/openspec/changes/unify-template-generation-pipeline/proposal.md +47 -0
  428. package/OpenSpec-main/openspec/changes/unify-template-generation-pipeline/specs/template-artifact-pipeline/spec.md +89 -0
  429. package/OpenSpec-main/openspec/changes/unify-template-generation-pipeline/tasks.md +41 -0
  430. package/OpenSpec-main/openspec/changes/workspace-agent-guidance/.openspec.yaml +2 -0
  431. package/OpenSpec-main/openspec/changes/workspace-agent-guidance/proposal.md +100 -0
  432. package/OpenSpec-main/openspec/changes/workspace-apply-repo-slice/proposal.md +58 -0
  433. package/OpenSpec-main/openspec/changes/workspace-reimplementation-roadmap/HISTORICAL_DIRECTION.md +511 -0
  434. package/OpenSpec-main/openspec/changes/workspace-reimplementation-roadmap/POC_REFERENCE_GUIDE.md +266 -0
  435. package/OpenSpec-main/openspec/changes/workspace-reimplementation-roadmap/README.md +107 -0
  436. package/OpenSpec-main/openspec/changes/workspace-reimplementation-roadmap/START_HERE.md +105 -0
  437. package/OpenSpec-main/openspec/changes/workspace-reimplementation-roadmap/proposal.md +62 -0
  438. package/OpenSpec-main/openspec/changes/workspace-verify-and-archive/proposal.md +57 -0
  439. package/OpenSpec-main/openspec/config.yaml +36 -0
  440. package/OpenSpec-main/openspec/explorations/explore-workflow-ux.md +116 -0
  441. package/OpenSpec-main/openspec/explorations/workspace-architecture.md +857 -0
  442. package/OpenSpec-main/openspec/explorations/workspace-roadmap.md +367 -0
  443. package/OpenSpec-main/openspec/explorations/workspace-user-journeys.md +2259 -0
  444. package/OpenSpec-main/openspec/explorations/workspace-ux-simplification.md +491 -0
  445. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/.initiative.yaml +27 -0
  446. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/README.md +33 -0
  447. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/decisions.md +204 -0
  448. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/direction.md +447 -0
  449. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/questions.md +23 -0
  450. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/roadmap.md +759 -0
  451. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/tasks.md +308 -0
  452. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/01-lock-the-direction/evidence.md +154 -0
  453. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/01-lock-the-direction/plan.md +90 -0
  454. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/01-lock-the-direction/tasks.md +44 -0
  455. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/02-stabilize-workspace-as-local-view/evidence.md +68 -0
  456. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/02-stabilize-workspace-as-local-view/plan.md +80 -0
  457. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/02-stabilize-workspace-as-local-view/tasks.md +23 -0
  458. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/03-add-context-store-foundation/evidence.md +43 -0
  459. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/03-add-context-store-foundation/plan.md +85 -0
  460. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/03-add-context-store-foundation/tasks.md +17 -0
  461. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/04-add-collection-foundation/evidence.md +77 -0
  462. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/04-add-collection-foundation/plan.md +198 -0
  463. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/04-add-collection-foundation/tasks.md +14 -0
  464. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/05-ship-initiative-mvp/evidence.md +99 -0
  465. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/05-ship-initiative-mvp/plan.md +236 -0
  466. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/05-ship-initiative-mvp/tasks.md +21 -0
  467. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/06-add-minimal-context-store-ux/evidence.md +97 -0
  468. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/06-add-minimal-context-store-ux/plan.md +333 -0
  469. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/06-add-minimal-context-store-ux/tasks.md +29 -0
  470. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/07-add-agent-first-initiative-discovery/evidence.md +97 -0
  471. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/07-add-agent-first-initiative-discovery/plan.md +184 -0
  472. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/07-add-agent-first-initiative-discovery/tasks.md +27 -0
  473. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/08-connect-repo-local-changes-to-initiatives/evidence.md +239 -0
  474. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/08-connect-repo-local-changes-to-initiatives/plan.md +279 -0
  475. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/08-connect-repo-local-changes-to-initiatives/tasks.md +22 -0
  476. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/09-add-initiative-resolve/decision-review.md +64 -0
  477. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/09-add-initiative-resolve/evidence.md +106 -0
  478. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/09-add-initiative-resolve/plan.md +141 -0
  479. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/09-add-initiative-resolve/tasks.md +22 -0
  480. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/10-let-workspaces-open-initiatives/plan.md +430 -0
  481. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/10-let-workspaces-open-initiatives/tasks.md +43 -0
  482. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/11-manual-beta-reality-pass/notes.md +289 -0
  483. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/11-manual-beta-reality-pass/plan.md +39 -0
  484. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/11-manual-beta-reality-pass/tasks.md +8 -0
  485. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/12-context-store-first-run-and-cleanup-ux/evidence.md +45 -0
  486. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/12-context-store-first-run-and-cleanup-ux/plan.md +150 -0
  487. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/12-context-store-first-run-and-cleanup-ux/tasks.md +23 -0
  488. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/13-agent-handoff-output-and-delivery-polish/evidence.md +25 -0
  489. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/13-agent-handoff-output-and-delivery-polish/plan.md +98 -0
  490. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/13-agent-handoff-output-and-delivery-polish/tasks.md +25 -0
  491. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/14-workspaces-beta-guide-split/plan.md +37 -0
  492. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/14-workspaces-beta-guide-split/tasks.md +9 -0
  493. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/15-context-store-project-roots-and-schema-led-initiatives/evidence.md +140 -0
  494. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/15-context-store-project-roots-and-schema-led-initiatives/plan.md +344 -0
  495. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/15-context-store-project-roots-and-schema-led-initiatives/tasks.md +39 -0
  496. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/16-add-escalation-ux/plan.md +26 -0
  497. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/16-add-escalation-ux/tasks.md +7 -0
  498. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/17-harden-team-shared-coordination/plan.md +25 -0
  499. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/17-harden-team-shared-coordination/tasks.md +7 -0
  500. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/18-explore-initiative-hosted-target-bound-change-artifacts/evidence.md +397 -0
  501. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/18-explore-initiative-hosted-target-bound-change-artifacts/plan.md +180 -0
  502. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/18-explore-initiative-hosted-target-bound-change-artifacts/tasks.md +28 -0
  503. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/19-review-workspace-beta-compatibility-before-public-release/plan.md +62 -0
  504. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/19-review-workspace-beta-compatibility-before-public-release/tasks.md +16 -0
  505. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/proposed-initiative-next-agent-handoff-ux/evidence.md +47 -0
  506. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/proposed-initiative-next-agent-handoff-ux/plan.md +90 -0
  507. package/OpenSpec-main/openspec/initiatives/context-store-and-initiatives/work-items/proposed-initiative-next-agent-handoff-ux/tasks.md +18 -0
  508. package/OpenSpec-main/openspec/specs/ai-tool-paths/spec.md +63 -0
  509. package/OpenSpec-main/openspec/specs/artifact-graph/spec.md +165 -0
  510. package/OpenSpec-main/openspec/specs/change-creation/spec.md +108 -0
  511. package/OpenSpec-main/openspec/specs/ci-nix-validation/spec.md +107 -0
  512. package/OpenSpec-main/openspec/specs/cli-archive/spec.md +210 -0
  513. package/OpenSpec-main/openspec/specs/cli-artifact-workflow/spec.md +398 -0
  514. package/OpenSpec-main/openspec/specs/cli-change/spec.md +92 -0
  515. package/OpenSpec-main/openspec/specs/cli-completion/spec.md +432 -0
  516. package/OpenSpec-main/openspec/specs/cli-config/spec.md +318 -0
  517. package/OpenSpec-main/openspec/specs/cli-feedback/spec.md +193 -0
  518. package/OpenSpec-main/openspec/specs/cli-init/spec.md +269 -0
  519. package/OpenSpec-main/openspec/specs/cli-list/spec.md +103 -0
  520. package/OpenSpec-main/openspec/specs/cli-show/spec.md +86 -0
  521. package/OpenSpec-main/openspec/specs/cli-spec/spec.md +88 -0
  522. package/OpenSpec-main/openspec/specs/cli-update/spec.md +229 -0
  523. package/OpenSpec-main/openspec/specs/cli-validate/spec.md +219 -0
  524. package/OpenSpec-main/openspec/specs/cli-view/spec.md +129 -0
  525. package/OpenSpec-main/openspec/specs/command-generation/spec.md +97 -0
  526. package/OpenSpec-main/openspec/specs/config-loading/spec.md +122 -0
  527. package/OpenSpec-main/openspec/specs/context-injection/spec.md +53 -0
  528. package/OpenSpec-main/openspec/specs/docs-agent-instructions/spec.md +62 -0
  529. package/OpenSpec-main/openspec/specs/global-config/spec.md +101 -0
  530. package/OpenSpec-main/openspec/specs/instruction-loader/spec.md +70 -0
  531. package/OpenSpec-main/openspec/specs/legacy-cleanup/spec.md +163 -0
  532. package/OpenSpec-main/openspec/specs/openspec-conventions/spec.md +556 -0
  533. package/OpenSpec-main/openspec/specs/opsx-archive-skill/spec.md +128 -0
  534. package/OpenSpec-main/openspec/specs/opsx-onboard-skill/spec.md +167 -0
  535. package/OpenSpec-main/openspec/specs/opsx-verify-skill/spec.md +189 -0
  536. package/OpenSpec-main/openspec/specs/rules-injection/spec.md +102 -0
  537. package/OpenSpec-main/openspec/specs/schema-fork-command/spec.md +71 -0
  538. package/OpenSpec-main/openspec/specs/schema-init-command/spec.md +76 -0
  539. package/OpenSpec-main/openspec/specs/schema-resolution/spec.md +201 -0
  540. package/OpenSpec-main/openspec/specs/schema-validate-command/spec.md +91 -0
  541. package/OpenSpec-main/openspec/specs/schema-which-command/spec.md +70 -0
  542. package/OpenSpec-main/openspec/specs/specs-sync-skill/spec.md +72 -0
  543. package/OpenSpec-main/openspec/specs/telemetry/spec.md +122 -0
  544. package/OpenSpec-main/openspec/specs/workspace-change-planning/spec.md +71 -0
  545. package/OpenSpec-main/openspec/specs/workspace-foundation/spec.md +279 -0
  546. package/OpenSpec-main/openspec/specs/workspace-links/spec.md +529 -0
  547. package/OpenSpec-main/openspec/specs/workspace-open/spec.md +205 -0
  548. package/OpenSpec-main/openspec-parallel-merge-plan.md +98 -0
  549. package/OpenSpec-main/package-lock.json +4978 -0
  550. package/OpenSpec-main/package.json +84 -0
  551. package/OpenSpec-main/pnpm-lock.yaml +3187 -0
  552. package/OpenSpec-main/schemas/spec-driven/schema.yaml +153 -0
  553. package/OpenSpec-main/schemas/spec-driven/templates/design.md +19 -0
  554. package/OpenSpec-main/schemas/spec-driven/templates/proposal.md +23 -0
  555. package/OpenSpec-main/schemas/spec-driven/templates/spec.md +8 -0
  556. package/OpenSpec-main/schemas/spec-driven/templates/tasks.md +9 -0
  557. package/OpenSpec-main/schemas/workspace-planning/schema.yaml +72 -0
  558. package/OpenSpec-main/schemas/workspace-planning/templates/design.md +33 -0
  559. package/OpenSpec-main/schemas/workspace-planning/templates/proposal.md +28 -0
  560. package/OpenSpec-main/schemas/workspace-planning/templates/spec.md +9 -0
  561. package/OpenSpec-main/schemas/workspace-planning/templates/tasks.md +15 -0
  562. package/OpenSpec-main/scripts/README.md +37 -0
  563. package/OpenSpec-main/scripts/pack-version-check.mjs +111 -0
  564. package/OpenSpec-main/scripts/postinstall.js +83 -0
  565. package/OpenSpec-main/scripts/test-postinstall.sh +57 -0
  566. package/OpenSpec-main/scripts/update-flake.sh +128 -0
  567. package/OpenSpec-main/test/cli-e2e/basic.test.ts +205 -0
  568. package/OpenSpec-main/test/commands/artifact-workflow.test.ts +1063 -0
  569. package/OpenSpec-main/test/commands/change-initiative-link.test.ts +532 -0
  570. package/OpenSpec-main/test/commands/change.interactive-show.test.ts +45 -0
  571. package/OpenSpec-main/test/commands/change.interactive-validate.test.ts +48 -0
  572. package/OpenSpec-main/test/commands/completion.test.ts +278 -0
  573. package/OpenSpec-main/test/commands/config-profile.test.ts +532 -0
  574. package/OpenSpec-main/test/commands/config.test.ts +285 -0
  575. package/OpenSpec-main/test/commands/context-store.test.ts +692 -0
  576. package/OpenSpec-main/test/commands/feedback.test.ts +429 -0
  577. package/OpenSpec-main/test/commands/initiative.test.ts +907 -0
  578. package/OpenSpec-main/test/commands/schema.test.ts +467 -0
  579. package/OpenSpec-main/test/commands/show.test.ts +123 -0
  580. package/OpenSpec-main/test/commands/spec.interactive-show.test.ts +44 -0
  581. package/OpenSpec-main/test/commands/spec.interactive-validate.test.ts +44 -0
  582. package/OpenSpec-main/test/commands/spec.test.ts +324 -0
  583. package/OpenSpec-main/test/commands/validate.enriched-output.test.ts +49 -0
  584. package/OpenSpec-main/test/commands/validate.test.ts +147 -0
  585. package/OpenSpec-main/test/commands/workspace-initiative-open.test.ts +638 -0
  586. package/OpenSpec-main/test/commands/workspace-open.test.ts +123 -0
  587. package/OpenSpec-main/test/commands/workspace.interactive.test.ts +696 -0
  588. package/OpenSpec-main/test/commands/workspace.test.ts +1812 -0
  589. package/OpenSpec-main/test/core/archive.test.ts +869 -0
  590. package/OpenSpec-main/test/core/artifact-graph/graph.test.ts +268 -0
  591. package/OpenSpec-main/test/core/artifact-graph/instruction-loader.test.ts +609 -0
  592. package/OpenSpec-main/test/core/artifact-graph/outputs.test.ts +175 -0
  593. package/OpenSpec-main/test/core/artifact-graph/resolver.test.ts +651 -0
  594. package/OpenSpec-main/test/core/artifact-graph/schema.test.ts +207 -0
  595. package/OpenSpec-main/test/core/artifact-graph/state.test.ts +174 -0
  596. package/OpenSpec-main/test/core/artifact-graph/workflow.integration.test.ts +182 -0
  597. package/OpenSpec-main/test/core/available-tools.test.ts +167 -0
  598. package/OpenSpec-main/test/core/collections/initiatives/operations.test.ts +342 -0
  599. package/OpenSpec-main/test/core/collections/initiatives/resolution.test.ts +21 -0
  600. package/OpenSpec-main/test/core/collections/initiatives/schema.test.ts +201 -0
  601. package/OpenSpec-main/test/core/collections/initiatives/templates.test.ts +74 -0
  602. package/OpenSpec-main/test/core/collections/runtime.test.ts +214 -0
  603. package/OpenSpec-main/test/core/command-generation/adapters.test.ts +710 -0
  604. package/OpenSpec-main/test/core/command-generation/generator.test.ts +110 -0
  605. package/OpenSpec-main/test/core/command-generation/registry.test.ts +108 -0
  606. package/OpenSpec-main/test/core/command-generation/types.test.ts +79 -0
  607. package/OpenSpec-main/test/core/commands/change-command.list.test.ts +76 -0
  608. package/OpenSpec-main/test/core/commands/change-command.show-validate.test.ts +111 -0
  609. package/OpenSpec-main/test/core/completions/command-registry.test.ts +201 -0
  610. package/OpenSpec-main/test/core/completions/completion-provider.test.ts +288 -0
  611. package/OpenSpec-main/test/core/completions/generators/bash-generator.test.ts +586 -0
  612. package/OpenSpec-main/test/core/completions/generators/fish-generator.test.ts +549 -0
  613. package/OpenSpec-main/test/core/completions/generators/powershell-generator.test.ts +621 -0
  614. package/OpenSpec-main/test/core/completions/generators/zsh-generator.test.ts +425 -0
  615. package/OpenSpec-main/test/core/completions/installers/bash-installer.test.ts +484 -0
  616. package/OpenSpec-main/test/core/completions/installers/fish-installer.test.ts +321 -0
  617. package/OpenSpec-main/test/core/completions/installers/powershell-installer.test.ts +824 -0
  618. package/OpenSpec-main/test/core/completions/installers/zsh-installer.test.ts +750 -0
  619. package/OpenSpec-main/test/core/config-schema.test.ts +340 -0
  620. package/OpenSpec-main/test/core/context-store/foundation.test.ts +364 -0
  621. package/OpenSpec-main/test/core/context-store/registry.test.ts +599 -0
  622. package/OpenSpec-main/test/core/converters/json-converter.test.ts +184 -0
  623. package/OpenSpec-main/test/core/global-config.test.ts +371 -0
  624. package/OpenSpec-main/test/core/init.test.ts +786 -0
  625. package/OpenSpec-main/test/core/legacy-cleanup.test.ts +1162 -0
  626. package/OpenSpec-main/test/core/list.test.ts +165 -0
  627. package/OpenSpec-main/test/core/migration.test.ts +150 -0
  628. package/OpenSpec-main/test/core/parsers/change-parser.test.ts +52 -0
  629. package/OpenSpec-main/test/core/parsers/markdown-parser.test.ts +355 -0
  630. package/OpenSpec-main/test/core/parsers/requirement-blocks.test.ts +46 -0
  631. package/OpenSpec-main/test/core/planning-home.test.ts +120 -0
  632. package/OpenSpec-main/test/core/profile-sync-drift.test.ts +92 -0
  633. package/OpenSpec-main/test/core/profiles.test.ts +63 -0
  634. package/OpenSpec-main/test/core/project-config.test.ts +610 -0
  635. package/OpenSpec-main/test/core/shared/skill-generation.test.ts +301 -0
  636. package/OpenSpec-main/test/core/shared/tool-detection.test.ts +333 -0
  637. package/OpenSpec-main/test/core/templates/skill-templates-parity.test.ts +172 -0
  638. package/OpenSpec-main/test/core/update.test.ts +1810 -0
  639. package/OpenSpec-main/test/core/validation.enriched-messages.test.ts +74 -0
  640. package/OpenSpec-main/test/core/validation.test.ts +680 -0
  641. package/OpenSpec-main/test/core/view.test.ts +129 -0
  642. package/OpenSpec-main/test/core/workspace/foundation.test.ts +694 -0
  643. package/OpenSpec-main/test/core/workspace/legacy-state.test.ts +221 -0
  644. package/OpenSpec-main/test/core/workspace/skills.test.ts +69 -0
  645. package/OpenSpec-main/test/fixtures/tmp-init/openspec/changes/c1/proposal.md +7 -0
  646. package/OpenSpec-main/test/fixtures/tmp-init/openspec/changes/c1/specs/alpha/spec.md +8 -0
  647. package/OpenSpec-main/test/fixtures/tmp-init/openspec/specs/alpha/spec.md +12 -0
  648. package/OpenSpec-main/test/helpers/path-env.ts +26 -0
  649. package/OpenSpec-main/test/helpers/run-cli.ts +150 -0
  650. package/OpenSpec-main/test/prompts/searchable-multi-select.test.ts +220 -0
  651. package/OpenSpec-main/test/specs/source-specs-normalization.test.ts +63 -0
  652. package/OpenSpec-main/test/telemetry/config.test.ts +298 -0
  653. package/OpenSpec-main/test/telemetry/index.test.ts +219 -0
  654. package/OpenSpec-main/test/utils/change-metadata.test.ts +368 -0
  655. package/OpenSpec-main/test/utils/change-utils.test.ts +201 -0
  656. package/OpenSpec-main/test/utils/command-references.test.ts +83 -0
  657. package/OpenSpec-main/test/utils/file-system.test.ts +322 -0
  658. package/OpenSpec-main/test/utils/interactive.test.ts +125 -0
  659. package/OpenSpec-main/test/utils/marker-updates.test.ts +448 -0
  660. package/OpenSpec-main/test/utils/shell-detection.test.ts +185 -0
  661. package/OpenSpec-main/vitest.config.ts +47 -0
  662. package/OpenSpec-main/vitest.setup.ts +15 -0
  663. package/README.md +37 -4
  664. package/Scanner CLI Inteligente para Projetos.md +433 -0
  665. package/dist/index.js +1178 -139
  666. package/package.json +1 -1
  667. package/templates/wiki-recipes/nextjs.rules.md +21 -0
  668. package/templates/wiki-recipes/postgresql.rules.md +20 -0
  669. package/templates/wiki-recipes/react.rules.md +18 -0
  670. package/templates/wiki-recipes/tailwindcss.rules.md +18 -0
  671. package/templates/wiki-recipes/typescript.rules.md +21 -0
@@ -0,0 +1,1812 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+
6
+ import { COMMAND_REGISTRY } from '../../src/core/completions/command-registry.js';
7
+ import {
8
+ createManagedWorkspace,
9
+ resolveExistingDirectory,
10
+ } from '../../src/commands/workspace/operations.js';
11
+ import {
12
+ WORKSPACE_CHANGES_DIR_NAME,
13
+ WORKSPACE_GUIDANCE_END_MARKER,
14
+ WORKSPACE_GUIDANCE_START_MARKER,
15
+ WORKSPACE_METADATA_DIR_NAME,
16
+ getWorkspaceCodeWorkspacePath,
17
+ getManagedWorkspaceRoot,
18
+ getWorkspaceRegistryPath,
19
+ getWorkspaceViewStatePath,
20
+ parseWorkspaceViewState,
21
+ } from '../../src/core/workspace/index.js';
22
+ import {
23
+ WORKSPACE_LEGACY_LOCAL_STATE_FILE_NAME,
24
+ WORKSPACE_LEGACY_SHARED_STATE_FILE_NAME,
25
+ } from '../../src/core/workspace/legacy-state.js';
26
+ import { FileSystemUtils } from '../../src/utils/file-system.js';
27
+ import { withPrependedPathEnv } from '../helpers/path-env.js';
28
+ import { runCLI, type RunCLIResult } from '../helpers/run-cli.js';
29
+
30
+ describe('workspace command', () => {
31
+ let tempDir: string;
32
+ let dataHome: string;
33
+ let configHome: string;
34
+ let env: NodeJS.ProcessEnv;
35
+
36
+ beforeEach(() => {
37
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openspec-workspace-command-'));
38
+ dataHome = path.join(tempDir, 'data');
39
+ configHome = path.join(tempDir, 'config');
40
+ env = {
41
+ XDG_DATA_HOME: dataHome,
42
+ XDG_CONFIG_HOME: configHome,
43
+ OPEN_SPEC_INTERACTIVE: '0',
44
+ OPENSPEC_TELEMETRY: '0',
45
+ };
46
+ });
47
+
48
+ afterEach(() => {
49
+ fs.rmSync(tempDir, { recursive: true, force: true });
50
+ });
51
+
52
+ function mkdir(relativePath: string): string {
53
+ const dir = path.join(tempDir, relativePath);
54
+ fs.mkdirSync(dir, { recursive: true });
55
+ return dir;
56
+ }
57
+
58
+ function expectedExistingPath(existingPath: string): string {
59
+ return fs.realpathSync.native(existingPath);
60
+ }
61
+
62
+ function expectSameExistingPath(actualPath: string | null, expectedPath: string): void {
63
+ expect(actualPath).not.toBeNull();
64
+ expect(fs.realpathSync.native(actualPath as string)).toBe(fs.realpathSync.native(expectedPath));
65
+ }
66
+
67
+ function parseJson(result: RunCLIResult): any {
68
+ try {
69
+ return JSON.parse(result.stdout);
70
+ } catch (error) {
71
+ throw new Error(
72
+ `Could not parse JSON.\nCommand: ${result.command}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}\n${String(error)}`
73
+ );
74
+ }
75
+ }
76
+
77
+ function createFakeExecutable(name: string): { binDir: string; logPath: string } {
78
+ const binDir = path.join(tempDir, 'fake-bin');
79
+ const logPath = path.join(tempDir, `${name}-launch.json`);
80
+ const recorderPath = path.join(binDir, 'record-launch.cjs');
81
+ fs.mkdirSync(binDir, { recursive: true });
82
+ fs.writeFileSync(
83
+ recorderPath,
84
+ "const fs = require('node:fs');\nfs.writeFileSync(process.env.OPENSPEC_FAKE_OPEN_LOG, JSON.stringify({ cwd: process.cwd(), args: process.argv.slice(2) }));\n"
85
+ );
86
+
87
+ const posixExecutable = path.join(binDir, name);
88
+ fs.writeFileSync(posixExecutable, '#!/bin/sh\nnode "$OPENSPEC_FAKE_OPEN_RECORDER" "$@"\n');
89
+ fs.chmodSync(posixExecutable, 0o755);
90
+ fs.writeFileSync(
91
+ path.join(binDir, `${name}.cmd`),
92
+ '@echo off\r\nnode "%OPENSPEC_FAKE_OPEN_RECORDER%" %*\r\n'
93
+ );
94
+
95
+ return { binDir, logPath };
96
+ }
97
+
98
+ function envWithFakeExecutable(fake: { binDir: string; logPath: string }): NodeJS.ProcessEnv {
99
+ return {
100
+ ...withPrependedPathEnv(env, fake.binDir),
101
+ OPENSPEC_FAKE_OPEN_RECORDER: path.join(fake.binDir, 'record-launch.cjs'),
102
+ OPENSPEC_FAKE_OPEN_LOG: fake.logPath,
103
+ };
104
+ }
105
+
106
+ function readLaunchLog(logPath: string): { cwd: string; args: string[] } {
107
+ return JSON.parse(fs.readFileSync(logPath, 'utf-8'));
108
+ }
109
+
110
+ async function setupWorkspace(
111
+ name = 'platform',
112
+ links: string[] = [],
113
+ extraArgs: string[] = []
114
+ ): Promise<any> {
115
+ const result = await runCLI(
116
+ [
117
+ 'workspace',
118
+ 'setup',
119
+ '--no-interactive',
120
+ '--json',
121
+ '--name',
122
+ name,
123
+ ...links.flatMap((link) => ['--link', link]),
124
+ ...extraArgs,
125
+ ],
126
+ { cwd: tempDir, env }
127
+ );
128
+ expect(result.exitCode).toBe(0);
129
+ return parseJson(result);
130
+ }
131
+
132
+ function readWorkspaceState(workspaceRoot: string) {
133
+ return parseWorkspaceViewState(fs.readFileSync(getWorkspaceViewStatePath(workspaceRoot), 'utf-8'));
134
+ }
135
+
136
+ function writeGlobalConfig(config: Record<string, unknown>): void {
137
+ const configDir = path.join(configHome, 'openspec');
138
+ fs.mkdirSync(configDir, { recursive: true });
139
+ fs.writeFileSync(path.join(configDir, 'config.json'), `${JSON.stringify(config, null, 2)}\n`);
140
+ }
141
+
142
+ it('sets up a workspace with required links, records local state, and lists it through ls', async () => {
143
+ const api = mkdir('repos/api');
144
+ mkdir('repos/api/openspec/specs');
145
+ const checkout = mkdir('repos/platform/apps/checkout');
146
+ const expectedApi = expectedExistingPath(api);
147
+ const expectedCheckout = expectedExistingPath(checkout);
148
+
149
+ const setup = await setupWorkspace('platform', [`api=${api}`, checkout]);
150
+ const workspaceRoot = setup.workspace.root;
151
+ const expectedWorkspaceRoot = expectedExistingPath(workspaceRoot);
152
+
153
+ expect(setup.status).toEqual([]);
154
+ expect(setup.workspace.name).toBe('platform');
155
+ expect(setup.workspace.links).toEqual([
156
+ expect.objectContaining({
157
+ name: 'api',
158
+ path: expectedApi,
159
+ repo_specs_path: path.join(expectedApi, 'openspec', 'specs'),
160
+ status: [],
161
+ }),
162
+ expect.objectContaining({
163
+ name: 'checkout',
164
+ path: expectedCheckout,
165
+ repo_specs_path: null,
166
+ status: [],
167
+ }),
168
+ ]);
169
+
170
+ const workspaceState = readWorkspaceState(workspaceRoot);
171
+
172
+ expect(workspaceState).toEqual({
173
+ version: 1,
174
+ name: 'platform',
175
+ context: null,
176
+ links: {
177
+ api: expectedApi,
178
+ checkout: expectedCheckout,
179
+ },
180
+ });
181
+ expect(workspaceState.preferred_opener).toBeUndefined();
182
+ expect(fs.existsSync(getWorkspaceRegistryPath({ globalDataDir: path.join(dataHome, 'openspec') }))).toBe(false);
183
+ expect(fs.existsSync(path.join(workspaceRoot, '.gitignore'))).toBe(false);
184
+ expect(fs.existsSync(path.join(workspaceRoot, WORKSPACE_CHANGES_DIR_NAME))).toBe(false);
185
+ expect(fs.readFileSync(path.join(workspaceRoot, 'AGENTS.md'), 'utf-8')).toContain(
186
+ 'OpenSpec Workspace Guidance'
187
+ );
188
+ expect(JSON.parse(fs.readFileSync(getWorkspaceCodeWorkspacePath(workspaceRoot, 'platform'), 'utf-8')).folders).toEqual([
189
+ {
190
+ name: 'api',
191
+ path: expectedApi,
192
+ },
193
+ {
194
+ name: 'checkout',
195
+ path: expectedCheckout,
196
+ },
197
+ {
198
+ name: 'OpenSpec workspace',
199
+ path: '.',
200
+ },
201
+ ]);
202
+
203
+ const list = await runCLI(['workspace', 'ls', '--json'], { cwd: tempDir, env });
204
+ expect(list.exitCode).toBe(0);
205
+ const listPayload = parseJson(list);
206
+ expect(listPayload.workspaces).toEqual([
207
+ expect.objectContaining({
208
+ name: 'platform',
209
+ root: expectedWorkspaceRoot,
210
+ links: [
211
+ expect.objectContaining({ name: 'api', path: expectedApi, status: [] }),
212
+ expect.objectContaining({ name: 'checkout', path: expectedCheckout, status: [] }),
213
+ ],
214
+ status: [],
215
+ }),
216
+ ]);
217
+
218
+ const doctor = await runCLI(['workspace', 'doctor', '--workspace', 'platform', '--json'], {
219
+ cwd: tempDir,
220
+ env,
221
+ });
222
+ expect(doctor.exitCode).toBe(0);
223
+ expect(parseJson(doctor).workspace.links).toEqual([
224
+ expect.objectContaining({ name: 'api', path: expectedApi, status: [] }),
225
+ expect.objectContaining({ name: 'checkout', path: expectedCheckout, status: [] }),
226
+ ]);
227
+ });
228
+
229
+ it('keeps non-interactive setup compatible by skipping skills when --tools is omitted', async () => {
230
+ const api = mkdir('repos/api');
231
+ const setup = await setupWorkspace('skip-skills', [`api=${api}`]);
232
+
233
+ expect(setup.workspace_skills).toEqual(
234
+ expect.objectContaining({
235
+ selected_agents: [],
236
+ generated: [],
237
+ refreshed: [],
238
+ failed: [],
239
+ skipped: [
240
+ expect.objectContaining({
241
+ reason: 'tools_omitted',
242
+ message: expect.stringContaining('openspec workspace update --tools <ids>'),
243
+ }),
244
+ ],
245
+ })
246
+ );
247
+ expect(readWorkspaceState(setup.workspace.root).workspace_skills).toBeUndefined();
248
+ expect(fs.existsSync(path.join(setup.workspace.root, '.codex'))).toBe(false);
249
+ });
250
+
251
+ it('installs profile-selected workspace skills in the workspace root only', async () => {
252
+ const api = mkdir('repos/api');
253
+ const linkedEntriesBefore = fs.readdirSync(api).sort();
254
+ const codexHome = path.join(tempDir, 'codex-home');
255
+ writeGlobalConfig({
256
+ profile: 'custom',
257
+ delivery: 'commands',
258
+ workflows: ['apply', 'archive'],
259
+ });
260
+
261
+ const result = await runCLI(
262
+ [
263
+ 'workspace',
264
+ 'setup',
265
+ '--no-interactive',
266
+ '--json',
267
+ '--name',
268
+ 'skill-root',
269
+ '--link',
270
+ `api=${api}`,
271
+ '--opener',
272
+ 'codex',
273
+ '--tools',
274
+ 'codex',
275
+ ],
276
+ {
277
+ cwd: tempDir,
278
+ env: {
279
+ ...env,
280
+ CODEX_HOME: codexHome,
281
+ },
282
+ }
283
+ );
284
+
285
+ expect(result.exitCode).toBe(0);
286
+ const payload = parseJson(result);
287
+ const workspaceRoot = payload.workspace.root;
288
+ expect(payload.workspace_skills).toEqual(
289
+ expect.objectContaining({
290
+ profile: 'custom',
291
+ delivery: 'commands',
292
+ workflow_ids: ['apply', 'archive'],
293
+ selected_agents: ['codex'],
294
+ skills_only: true,
295
+ delivery_notice: expect.stringContaining('skills only'),
296
+ generated: [
297
+ expect.objectContaining({
298
+ tool_id: 'codex',
299
+ workflow_ids: ['apply', 'archive'],
300
+ }),
301
+ ],
302
+ refreshed: [],
303
+ failed: [],
304
+ })
305
+ );
306
+
307
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-apply-change', 'SKILL.md'))).toBe(true);
308
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-archive-change', 'SKILL.md'))).toBe(true);
309
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-propose', 'SKILL.md'))).toBe(false);
310
+ expect(fs.existsSync(path.join(codexHome, 'prompts'))).toBe(false);
311
+ expect(fs.readdirSync(api).sort()).toEqual(linkedEntriesBefore);
312
+ expect(fs.existsSync(path.join(api, '.codex'))).toBe(false);
313
+
314
+ expect(readWorkspaceState(workspaceRoot).workspace_skills).toEqual(
315
+ expect.objectContaining({
316
+ selected_agents: ['codex'],
317
+ last_applied_profile: 'custom',
318
+ last_applied_delivery: 'commands',
319
+ last_applied_workflow_ids: ['apply', 'archive'],
320
+ last_applied_at: expect.any(String),
321
+ })
322
+ );
323
+ });
324
+
325
+ it('supports --tools none and records an empty workspace skill selection', async () => {
326
+ const api = mkdir('repos/api');
327
+ const setup = await setupWorkspace('skills-none', [`api=${api}`], ['--tools', 'none']);
328
+
329
+ expect(setup.workspace_skills).toEqual(
330
+ expect.objectContaining({
331
+ selected_agents: [],
332
+ generated: [],
333
+ refreshed: [],
334
+ failed: [],
335
+ skipped: [
336
+ expect.objectContaining({
337
+ reason: 'no_agents_selected',
338
+ }),
339
+ ],
340
+ })
341
+ );
342
+ expect(readWorkspaceState(setup.workspace.root).workspace_skills).toEqual(
343
+ expect.objectContaining({
344
+ selected_agents: [],
345
+ last_applied_workflow_ids: ['propose', 'explore', 'apply', 'sync', 'archive'],
346
+ })
347
+ );
348
+ });
349
+
350
+ it('updates stored workspace skills from the current workspace and clears profile drift', async () => {
351
+ const api = mkdir('repos/api');
352
+ const linkedEntriesBefore = fs.readdirSync(api).sort();
353
+ writeGlobalConfig({
354
+ profile: 'custom',
355
+ delivery: 'commands',
356
+ workflows: ['apply', 'verify'],
357
+ });
358
+ const setup = await setupWorkspace('profile-sync', [`api=${api}`], ['--tools', 'codex']);
359
+ const workspaceRoot = setup.workspace.root;
360
+ const customSkillDir = path.join(workspaceRoot, '.codex', 'skills', 'custom-note');
361
+ fs.mkdirSync(customSkillDir, { recursive: true });
362
+ fs.writeFileSync(path.join(customSkillDir, 'README.md'), 'user-owned\n');
363
+
364
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-apply-change', 'SKILL.md'))).toBe(true);
365
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-verify-change', 'SKILL.md'))).toBe(true);
366
+
367
+ writeGlobalConfig({
368
+ profile: 'core',
369
+ delivery: 'commands',
370
+ });
371
+
372
+ const drift = await runCLI(
373
+ ['workspace', 'doctor', '--workspace', 'profile-sync', '--json'],
374
+ { cwd: tempDir, env }
375
+ );
376
+ expect(drift.exitCode).toBe(0);
377
+ expect(parseJson(drift).workspace.status).toContainEqual(
378
+ expect.objectContaining({
379
+ code: 'workspace_skills_out_of_sync',
380
+ fix: 'openspec workspace update --workspace profile-sync',
381
+ })
382
+ );
383
+
384
+ const update = await runCLI(['workspace', 'update', '--json'], {
385
+ cwd: workspaceRoot,
386
+ env,
387
+ });
388
+ expect(update.exitCode).toBe(0);
389
+ const payload = parseJson(update);
390
+
391
+ expect(payload.workspace.name).toBe('profile-sync');
392
+ expect(payload.workspace_skills).toEqual(
393
+ expect.objectContaining({
394
+ profile: 'core',
395
+ delivery: 'commands',
396
+ workflow_ids: ['propose', 'explore', 'apply', 'sync', 'archive'],
397
+ selected_agents: ['codex'],
398
+ skills_only: true,
399
+ delivery_notice: expect.stringContaining('skills only'),
400
+ refreshed: [
401
+ expect.objectContaining({
402
+ tool_id: 'codex',
403
+ workflow_ids: ['propose', 'explore', 'apply', 'sync', 'archive'],
404
+ }),
405
+ ],
406
+ removed: [
407
+ expect.objectContaining({
408
+ tool_id: 'codex',
409
+ reason: 'workflow_unselected',
410
+ workflow_ids: ['verify'],
411
+ }),
412
+ ],
413
+ failed: [],
414
+ })
415
+ );
416
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-propose', 'SKILL.md'))).toBe(true);
417
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-explore', 'SKILL.md'))).toBe(true);
418
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-sync-specs', 'SKILL.md'))).toBe(true);
419
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-archive-change', 'SKILL.md'))).toBe(true);
420
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-verify-change'))).toBe(false);
421
+ expect(fs.existsSync(path.join(customSkillDir, 'README.md'))).toBe(true);
422
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'prompts'))).toBe(false);
423
+ expect(fs.readdirSync(api).sort()).toEqual(linkedEntriesBefore);
424
+ expect(fs.existsSync(path.join(api, '.codex'))).toBe(false);
425
+ expect(readWorkspaceState(workspaceRoot).workspace_skills).toEqual(
426
+ expect.objectContaining({
427
+ selected_agents: ['codex'],
428
+ last_applied_profile: 'core',
429
+ last_applied_delivery: 'commands',
430
+ last_applied_workflow_ids: ['propose', 'explore', 'apply', 'sync', 'archive'],
431
+ })
432
+ );
433
+
434
+ const clean = await runCLI(
435
+ ['workspace', 'doctor', '--workspace', 'profile-sync', '--json'],
436
+ { cwd: tempDir, env }
437
+ );
438
+ expect(clean.exitCode).toBe(0);
439
+ expect(parseJson(clean).workspace.status).not.toContainEqual(
440
+ expect.objectContaining({
441
+ code: 'workspace_skills_out_of_sync',
442
+ })
443
+ );
444
+ });
445
+
446
+ it('does not route openspec update through workspace update from a workspace root', async () => {
447
+ const api = mkdir('repos/api');
448
+ const linkedEntriesBefore = fs.readdirSync(api).sort();
449
+ writeGlobalConfig({
450
+ profile: 'custom',
451
+ delivery: 'commands',
452
+ workflows: ['apply'],
453
+ });
454
+ const setup = await setupWorkspace('update-redirect', [`api=${api}`], ['--tools', 'codex']);
455
+ const workspaceRoot = setup.workspace.root;
456
+ const workspaceStateBefore = fs.readFileSync(getWorkspaceViewStatePath(workspaceRoot), 'utf-8');
457
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-apply-change', 'SKILL.md'))).toBe(true);
458
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-propose', 'SKILL.md'))).toBe(false);
459
+
460
+ writeGlobalConfig({
461
+ profile: 'core',
462
+ delivery: 'commands',
463
+ });
464
+
465
+ const update = await runCLI(['update'], {
466
+ cwd: workspaceRoot,
467
+ env,
468
+ });
469
+ expect(update.exitCode).toBe(1);
470
+ expect(`${update.stdout}\n${update.stderr}`).toContain('Run `openspec workspace update`');
471
+ expect(update.stdout).not.toContain('Workspace update complete');
472
+ expect(update.stdout).not.toContain('not in the managed local workspace views list');
473
+ expect(fs.readFileSync(getWorkspaceViewStatePath(workspaceRoot), 'utf-8')).toBe(workspaceStateBefore);
474
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-propose', 'SKILL.md'))).toBe(false);
475
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-sync-specs', 'SKILL.md'))).toBe(false);
476
+ expect(fs.readdirSync(api).sort()).toEqual(linkedEntriesBefore);
477
+ expect(fs.existsSync(path.join(api, '.codex'))).toBe(false);
478
+ });
479
+
480
+ it('updates repo-local project targets nested under a workspace without touching workspace state', async () => {
481
+ const api = mkdir('repos/api');
482
+ writeGlobalConfig({
483
+ profile: 'custom',
484
+ delivery: 'commands',
485
+ workflows: ['apply'],
486
+ });
487
+ const setup = await setupWorkspace('nested-update-target', [`api=${api}`], ['--tools', 'codex']);
488
+ const workspaceRoot = setup.workspace.root;
489
+ const workspaceStateBefore = fs.readFileSync(getWorkspaceViewStatePath(workspaceRoot), 'utf-8');
490
+ const nestedRepo = path.join(workspaceRoot, 'repos', 'nested-api');
491
+ fs.mkdirSync(path.join(nestedRepo, 'openspec'), { recursive: true });
492
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-apply-change', 'SKILL.md'))).toBe(true);
493
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-propose', 'SKILL.md'))).toBe(false);
494
+
495
+ writeGlobalConfig({
496
+ profile: 'core',
497
+ delivery: 'commands',
498
+ });
499
+
500
+ const update = await runCLI(['update', nestedRepo], {
501
+ cwd: tempDir,
502
+ env,
503
+ });
504
+
505
+ expect(update.exitCode).toBe(0);
506
+ expect(update.stdout).toContain('No configured tools found');
507
+ expect(`${update.stdout}\n${update.stderr}`).not.toContain('Run `openspec workspace update`');
508
+ expect(update.stdout).not.toContain('Workspace update complete');
509
+ expect(fs.readFileSync(getWorkspaceViewStatePath(workspaceRoot), 'utf-8')).toBe(workspaceStateBefore);
510
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-propose', 'SKILL.md'))).toBe(false);
511
+ });
512
+
513
+ it('does not touch workspace state when updating repo-local projects with foreign workspace.yaml', async () => {
514
+ const existingApi = mkdir('repos/existing-api');
515
+ writeGlobalConfig({
516
+ profile: 'custom',
517
+ delivery: 'commands',
518
+ workflows: ['apply'],
519
+ });
520
+ const existingWorkspace = await setupWorkspace('known-workspace', [`api=${existingApi}`], ['--tools', 'codex']);
521
+ const existingWorkspaceRoot = existingWorkspace.workspace.root;
522
+ const existingWorkspaceStateBefore = fs.readFileSync(
523
+ getWorkspaceViewStatePath(existingWorkspaceRoot),
524
+ 'utf-8'
525
+ );
526
+ expect(fs.existsSync(path.join(existingWorkspaceRoot, '.codex', 'skills', 'openspec-apply-change', 'SKILL.md'))).toBe(true);
527
+ expect(fs.existsSync(path.join(existingWorkspaceRoot, '.codex', 'skills', 'openspec-propose', 'SKILL.md'))).toBe(false);
528
+
529
+ writeGlobalConfig({
530
+ profile: 'core',
531
+ delivery: 'commands',
532
+ });
533
+
534
+ const repoRoot = mkdir('repos/foreign-tool');
535
+ fs.mkdirSync(path.join(repoRoot, 'openspec'), { recursive: true });
536
+ const foreignWorkspaceYaml = `tool_workspace:
537
+ projects:
538
+ - name: example
539
+ path: ./service
540
+ `;
541
+ fs.writeFileSync(path.join(repoRoot, 'workspace.yaml'), foreignWorkspaceYaml);
542
+
543
+ const update = await runCLI(['update'], {
544
+ cwd: repoRoot,
545
+ env,
546
+ });
547
+
548
+ expect(update.exitCode).toBe(0);
549
+ expect(update.stdout).not.toContain('Workspace update complete');
550
+ expect(update.stderr).not.toContain('Invalid workspace state');
551
+ expect(update.stdout).toContain('No configured tools found');
552
+ expect(fs.readFileSync(getWorkspaceViewStatePath(existingWorkspaceRoot), 'utf-8')).toBe(
553
+ existingWorkspaceStateBefore
554
+ );
555
+ expect(fs.existsSync(path.join(existingWorkspaceRoot, '.codex', 'skills', 'openspec-propose', 'SKILL.md'))).toBe(false);
556
+ expect(fs.readFileSync(path.join(repoRoot, 'workspace.yaml'), 'utf-8')).toBe(
557
+ foreignWorkspaceYaml
558
+ );
559
+ expect(fs.existsSync(path.join(repoRoot, WORKSPACE_METADATA_DIR_NAME))).toBe(false);
560
+ expect(fs.existsSync(path.join(repoRoot, WORKSPACE_CHANGES_DIR_NAME))).toBe(false);
561
+ expect(fs.readdirSync(repoRoot).some((entry) => entry.endsWith('.code-workspace'))).toBe(false);
562
+ expect(fs.existsSync(getWorkspaceRegistryPath({ globalDataDir: path.join(dataHome, 'openspec') }))).toBe(false);
563
+ });
564
+
565
+ it('does not update a workspace passed to openspec update even when another workspace is known', async () => {
566
+ const firstApi = mkdir('repos/first-api');
567
+ const secondApi = mkdir('repos/second-api');
568
+ writeGlobalConfig({
569
+ profile: 'custom',
570
+ delivery: 'commands',
571
+ workflows: ['apply'],
572
+ });
573
+ const first = await setupWorkspace('target-first', [`api=${firstApi}`], ['--tools', 'codex']);
574
+ const second = await setupWorkspace('target-second', [`api=${secondApi}`], ['--tools', 'codex']);
575
+ const firstWorkspaceStateBefore = fs.readFileSync(getWorkspaceViewStatePath(first.workspace.root), 'utf-8');
576
+ const secondWorkspaceStateBefore = fs.readFileSync(getWorkspaceViewStatePath(second.workspace.root), 'utf-8');
577
+
578
+ writeGlobalConfig({
579
+ profile: 'core',
580
+ delivery: 'commands',
581
+ });
582
+
583
+ const update = await runCLI(
584
+ ['update', first.workspace.root],
585
+ { cwd: tempDir, env }
586
+ );
587
+
588
+ expect(update.exitCode).toBe(1);
589
+ expect(`${update.stdout}\n${update.stderr}`).toContain('Run `openspec workspace update`');
590
+ expect(update.stdout).not.toContain('Workspace update complete');
591
+ expect(update.stdout).not.toContain('Multiple OpenSpec workspaces are known');
592
+ expect(fs.readFileSync(getWorkspaceViewStatePath(first.workspace.root), 'utf-8')).toBe(
593
+ firstWorkspaceStateBefore
594
+ );
595
+ expect(fs.readFileSync(getWorkspaceViewStatePath(second.workspace.root), 'utf-8')).toBe(
596
+ secondWorkspaceStateBefore
597
+ );
598
+ expect(fs.existsSync(path.join(first.workspace.root, '.codex', 'skills', 'openspec-propose', 'SKILL.md'))).toBe(false);
599
+ expect(fs.existsSync(path.join(second.workspace.root, '.codex', 'skills', 'openspec-propose', 'SKILL.md'))).toBe(false);
600
+ });
601
+
602
+ it('supports named and flag-selected workspace updates with explicit agent changes', async () => {
603
+ const api = mkdir('repos/api');
604
+ writeGlobalConfig({
605
+ profile: 'custom',
606
+ delivery: 'skills',
607
+ workflows: ['apply'],
608
+ });
609
+ const setup = await setupWorkspace('agent-change', [`api=${api}`], ['--tools', 'codex']);
610
+ const workspaceRoot = setup.workspace.root;
611
+ const userSkillDir = path.join(workspaceRoot, '.codex', 'skills', 'user-skill');
612
+ fs.mkdirSync(userSkillDir, { recursive: true });
613
+ fs.writeFileSync(path.join(userSkillDir, 'SKILL.md'), 'user-owned\n');
614
+
615
+ const addAgent = await runCLI(
616
+ ['workspace', 'update', 'agent-change', '--tools', 'codex,claude', '--json'],
617
+ { cwd: tempDir, env }
618
+ );
619
+ expect(addAgent.exitCode).toBe(0);
620
+ const addPayload = parseJson(addAgent);
621
+ expect(addPayload.workspace_skills.refreshed).toEqual([
622
+ expect.objectContaining({ tool_id: 'codex', workflow_ids: ['apply'] }),
623
+ ]);
624
+ expect(addPayload.workspace_skills.added).toEqual([
625
+ expect.objectContaining({ tool_id: 'claude', workflow_ids: ['apply'] }),
626
+ ]);
627
+ expect(fs.existsSync(path.join(workspaceRoot, '.claude', 'skills', 'openspec-apply-change', 'SKILL.md'))).toBe(true);
628
+ expect(readWorkspaceState(workspaceRoot).workspace_skills?.selected_agents).toEqual(['codex', 'claude']);
629
+
630
+ const removeAgent = await runCLI(
631
+ ['workspace', 'update', '--workspace', 'agent-change', '--tools', 'claude', '--json'],
632
+ { cwd: tempDir, env }
633
+ );
634
+ expect(removeAgent.exitCode).toBe(0);
635
+ const removePayload = parseJson(removeAgent);
636
+ expect(removePayload.workspace_skills.removed).toEqual([
637
+ expect.objectContaining({
638
+ tool_id: 'codex',
639
+ reason: 'agent_unselected',
640
+ workflow_ids: ['apply'],
641
+ }),
642
+ ]);
643
+ expect(removePayload.workspace_skills.refreshed).toEqual([
644
+ expect.objectContaining({ tool_id: 'claude', workflow_ids: ['apply'] }),
645
+ ]);
646
+ expect(fs.existsSync(path.join(workspaceRoot, '.codex', 'skills', 'openspec-apply-change'))).toBe(false);
647
+ expect(fs.existsSync(path.join(userSkillDir, 'SKILL.md'))).toBe(true);
648
+ expect(readWorkspaceState(workspaceRoot).workspace_skills?.selected_agents).toEqual(['claude']);
649
+ });
650
+
651
+ it('does not remove unmanaged skill directories that collide with OpenSpec workflow names', async () => {
652
+ const api = mkdir('repos/api');
653
+ writeGlobalConfig({
654
+ profile: 'custom',
655
+ delivery: 'skills',
656
+ workflows: ['verify'],
657
+ });
658
+ const setup = await setupWorkspace('unmanaged-collision', [`api=${api}`], ['--tools', 'codex']);
659
+ const workspaceRoot = setup.workspace.root;
660
+ const collidingSkillDir = path.join(workspaceRoot, '.codex', 'skills', 'openspec-verify-change');
661
+ fs.writeFileSync(path.join(collidingSkillDir, 'SKILL.md'), 'name: user-owned-verify\n');
662
+
663
+ const update = await runCLI(
664
+ ['workspace', 'update', '--workspace', 'unmanaged-collision', '--tools', 'none', '--json'],
665
+ { cwd: tempDir, env }
666
+ );
667
+
668
+ expect(update.exitCode).toBe(0);
669
+ expect(parseJson(update).workspace_skills.removed).toEqual([]);
670
+ expect(fs.existsSync(path.join(collidingSkillDir, 'SKILL.md'))).toBe(true);
671
+ expect(readWorkspaceState(workspaceRoot).workspace_skills?.selected_agents).toEqual([]);
672
+ });
673
+
674
+ it('does not record workspace skills as applied when an update fails', async () => {
675
+ const api = mkdir('repos/api');
676
+ writeGlobalConfig({
677
+ profile: 'custom',
678
+ delivery: 'skills',
679
+ workflows: ['apply'],
680
+ });
681
+ const setup = await setupWorkspace('failed-update-state', [`api=${api}`], ['--tools', 'codex']);
682
+ const workspaceRoot = setup.workspace.root;
683
+ const blockingSkillPath = path.join(workspaceRoot, '.codex', 'skills', 'openspec-propose');
684
+ fs.writeFileSync(blockingSkillPath, 'blocks generated skill directory\n');
685
+
686
+ writeGlobalConfig({
687
+ profile: 'core',
688
+ delivery: 'skills',
689
+ });
690
+
691
+ const update = await runCLI(
692
+ ['workspace', 'update', '--workspace', 'failed-update-state', '--json'],
693
+ { cwd: tempDir, env }
694
+ );
695
+
696
+ expect(update.exitCode).toBe(1);
697
+ expect(parseJson(update).workspace_skills.failed).toEqual([
698
+ expect.objectContaining({
699
+ tool_id: 'codex',
700
+ }),
701
+ ]);
702
+ expect(readWorkspaceState(workspaceRoot).workspace_skills).toEqual(
703
+ expect.objectContaining({
704
+ selected_agents: ['codex'],
705
+ last_applied_profile: 'custom',
706
+ last_applied_workflow_ids: ['apply'],
707
+ })
708
+ );
709
+ });
710
+
711
+ it('reports a no-op workspace update when no stored skill selection exists', async () => {
712
+ const api = mkdir('repos/api');
713
+ const linkedEntriesBefore = fs.readdirSync(api).sort();
714
+ const setup = await setupWorkspace('no-stored-skills', [`api=${api}`]);
715
+ const agentsPath = path.join(setup.workspace.root, 'AGENTS.md');
716
+ fs.writeFileSync(
717
+ agentsPath,
718
+ `# User Notes
719
+
720
+ ${WORKSPACE_GUIDANCE_START_MARKER}
721
+ # OpenSpec Workspace Guidance
722
+
723
+ Use \`changes/\` for workspace-level planning.
724
+ ${WORKSPACE_GUIDANCE_END_MARKER}
725
+ `
726
+ );
727
+
728
+ const update = await runCLI(
729
+ ['workspace', 'update', '--workspace', 'no-stored-skills', '--json'],
730
+ { cwd: tempDir, env }
731
+ );
732
+ expect(update.exitCode).toBe(0);
733
+ expect(parseJson(update).workspace_skills).toEqual(
734
+ expect.objectContaining({
735
+ selected_agents: [],
736
+ generated: [],
737
+ added: [],
738
+ refreshed: [],
739
+ removed: [],
740
+ failed: [],
741
+ skipped: [
742
+ expect.objectContaining({
743
+ reason: 'no_stored_agent_selection',
744
+ message: expect.stringContaining('--tools <ids>'),
745
+ }),
746
+ ],
747
+ })
748
+ );
749
+ const agentsContent = fs.readFileSync(agentsPath, 'utf-8');
750
+ expect(agentsContent).toContain('# User Notes');
751
+ expect(agentsContent).toContain(
752
+ 'Use initiatives for durable cross-team or cross-repo intent'
753
+ );
754
+ expect(agentsContent).not.toContain('Use `changes/` for workspace-level planning');
755
+ expect(fs.readdirSync(api).sort()).toEqual(linkedEntriesBefore);
756
+ expect(readWorkspaceState(setup.workspace.root).workspace_skills).toBeUndefined();
757
+ expect(fs.existsSync(path.join(setup.workspace.root, '.codex'))).toBe(false);
758
+ });
759
+
760
+ it('rejects invalid workspace setup tool IDs with structured JSON status', async () => {
761
+ const api = mkdir('repos/api');
762
+ const invalid = await runCLI(
763
+ [
764
+ 'workspace',
765
+ 'setup',
766
+ '--no-interactive',
767
+ '--json',
768
+ '--name',
769
+ 'invalid-skills',
770
+ '--link',
771
+ `api=${api}`,
772
+ '--tools',
773
+ 'codex,not-real',
774
+ ],
775
+ { cwd: tempDir, env }
776
+ );
777
+
778
+ expect(invalid.exitCode).toBe(1);
779
+ expect(parseJson(invalid).status[0]).toEqual(
780
+ expect.objectContaining({
781
+ code: 'invalid_workspace_setup_tools',
782
+ target: 'workspace.skills',
783
+ message: expect.stringContaining('not-real'),
784
+ })
785
+ );
786
+
787
+ const setup = await setupWorkspace('update-invalid-skills', [`api=${api}`]);
788
+ const invalidUpdate = await runCLI(
789
+ [
790
+ 'workspace',
791
+ 'update',
792
+ '--workspace',
793
+ 'update-invalid-skills',
794
+ '--json',
795
+ '--tools',
796
+ 'codex,not-real',
797
+ ],
798
+ { cwd: tempDir, env }
799
+ );
800
+ expect(invalidUpdate.exitCode).toBe(1);
801
+ expect(parseJson(invalidUpdate).status[0]).toEqual(
802
+ expect.objectContaining({
803
+ code: 'invalid_workspace_update_tools',
804
+ target: 'workspace.skills',
805
+ message: expect.stringContaining('not-real'),
806
+ })
807
+ );
808
+ expect(readWorkspaceState(setup.workspace.root).workspace_skills).toBeUndefined();
809
+ });
810
+
811
+ it('preserves equals signs in inferred and explicit setup link paths', async () => {
812
+ const inferred = mkdir('repos/foo=bar');
813
+ const explicit = mkdir('repos/api=service');
814
+ const expectedInferred = expectedExistingPath(inferred);
815
+ const expectedExplicit = expectedExistingPath(explicit);
816
+
817
+ const setup = await setupWorkspace('equals-paths', [inferred, `api=${explicit}`]);
818
+
819
+ expect(setup.workspace.links).toEqual([
820
+ expect.objectContaining({
821
+ name: 'api',
822
+ path: expectedExplicit,
823
+ status: [],
824
+ }),
825
+ expect.objectContaining({
826
+ name: 'foo=bar',
827
+ path: expectedInferred,
828
+ status: [],
829
+ }),
830
+ ]);
831
+
832
+ const workspaceState = readWorkspaceState(setup.workspace.root);
833
+ expect(workspaceState.links).toEqual({
834
+ api: expectedExplicit,
835
+ 'foo=bar': expectedInferred,
836
+ });
837
+ });
838
+
839
+ it('stores non-interactive preferred openers only when --opener is provided', async () => {
840
+ const api = mkdir('repos/api');
841
+ const codex = await setupWorkspace('codex-workspace', [`api=${api}`], ['--opener', 'codex-cli']);
842
+ const legacyCodex = await setupWorkspace('legacy-codex-workspace', [`api=${api}`], ['--opener', 'codex']);
843
+ const editor = await setupWorkspace('editor-workspace', [`api=${api}`], ['--opener', 'editor']);
844
+ const unset = await setupWorkspace('unset-workspace', [`api=${api}`]);
845
+
846
+ expect(readWorkspaceState(codex.workspace.root).preferred_opener).toEqual({
847
+ kind: 'agent',
848
+ id: 'codex-cli',
849
+ });
850
+ expect(readWorkspaceState(legacyCodex.workspace.root).preferred_opener).toEqual({
851
+ kind: 'agent',
852
+ id: 'codex-cli',
853
+ });
854
+ expect(readWorkspaceState(editor.workspace.root).preferred_opener).toEqual({
855
+ kind: 'editor',
856
+ id: 'vscode',
857
+ });
858
+ expect(readWorkspaceState(unset.workspace.root).preferred_opener).toBeUndefined();
859
+
860
+ const invalid = await runCLI(
861
+ [
862
+ 'workspace',
863
+ 'setup',
864
+ '--no-interactive',
865
+ '--json',
866
+ '--name',
867
+ 'invalid-opener',
868
+ '--link',
869
+ `api=${api}`,
870
+ '--opener',
871
+ 'cursor',
872
+ ],
873
+ { cwd: tempDir, env }
874
+ );
875
+ expect(invalid.exitCode).toBe(1);
876
+ expect(parseJson(invalid).status[0]).toEqual(
877
+ expect.objectContaining({
878
+ code: 'unsupported_workspace_opener',
879
+ target: 'workspace.opener',
880
+ })
881
+ );
882
+ });
883
+
884
+ it('resolves relative setup, link, and relink paths before storing local state', async () => {
885
+ const project = mkdir('project');
886
+ fs.mkdirSync(path.join(project, 'repos', 'api'), { recursive: true });
887
+ fs.mkdirSync(path.join(project, 'services', 'billing'), { recursive: true });
888
+ fs.mkdirSync(path.join(project, 'archive', 'billing'), { recursive: true });
889
+
890
+ const setup = await runCLI(
891
+ [
892
+ 'workspace',
893
+ 'setup',
894
+ '--no-interactive',
895
+ '--json',
896
+ '--name',
897
+ 'platform',
898
+ '--link',
899
+ 'repos/api',
900
+ ],
901
+ { cwd: project, env }
902
+ );
903
+ expect(setup.exitCode).toBe(0);
904
+
905
+ const setupPayload = parseJson(setup);
906
+ expectSameExistingPath(
907
+ readWorkspaceState(setupPayload.workspace.root).links.api ?? null,
908
+ path.join(project, 'repos', 'api')
909
+ );
910
+
911
+ const link = await runCLI(['workspace', 'link', 'services/billing', '--json'], {
912
+ cwd: project,
913
+ env,
914
+ });
915
+ expect(link.exitCode).toBe(0);
916
+ const linkPayload = parseJson(link).link;
917
+ expect(linkPayload).toEqual(
918
+ expect.objectContaining({
919
+ name: 'billing',
920
+ path: expect.any(String),
921
+ })
922
+ );
923
+ expectSameExistingPath(linkPayload.path, path.join(project, 'services', 'billing'));
924
+
925
+ const relink = await runCLI(
926
+ ['workspace', 'relink', 'billing', 'archive/billing', '--json'],
927
+ { cwd: project, env }
928
+ );
929
+ expect(relink.exitCode).toBe(0);
930
+ const relinkPayload = parseJson(relink).link;
931
+ expect(relinkPayload).toEqual(
932
+ expect.objectContaining({
933
+ name: 'billing',
934
+ path: expect.any(String),
935
+ })
936
+ );
937
+ expectSameExistingPath(relinkPayload.path, path.join(project, 'archive', 'billing'));
938
+
939
+ const workspaceLinks = readWorkspaceState(setupPayload.workspace.root).links;
940
+ expect(Object.keys(workspaceLinks).sort()).toEqual(['api', 'billing']);
941
+ expectSameExistingPath(workspaceLinks.api ?? null, path.join(project, 'repos', 'api'));
942
+ expectSameExistingPath(workspaceLinks.billing ?? null, path.join(project, 'archive', 'billing'));
943
+ });
944
+
945
+ it('canonicalizes existing link directories on Windows before storing local paths', async () => {
946
+ const api = mkdir('repos/api');
947
+ const canonicalApi = path.join(tempDir, 'canonical', 'api');
948
+ const originalPlatform = process.platform;
949
+ const canonicalize = vi
950
+ .spyOn(FileSystemUtils, 'canonicalizeExistingPath')
951
+ .mockImplementation((targetPath) => (targetPath === api ? canonicalApi : targetPath));
952
+
953
+ Object.defineProperty(process, 'platform', { value: 'win32' });
954
+
955
+ try {
956
+ await expect(resolveExistingDirectory(api)).resolves.toBe(canonicalApi);
957
+ expect(canonicalize).toHaveBeenCalledWith(api);
958
+ } finally {
959
+ canonicalize.mockRestore();
960
+ Object.defineProperty(process, 'platform', { value: originalPlatform });
961
+ }
962
+ });
963
+
964
+ it('rejects duplicate setup link names without creating or rewriting a workspace', async () => {
965
+ const firstApi = mkdir('repos/current/api');
966
+ const secondApi = mkdir('repos/archive/api');
967
+ const expectedFirstApi = expectedExistingPath(firstApi);
968
+
969
+ const duplicate = await runCLI(
970
+ [
971
+ 'workspace',
972
+ 'setup',
973
+ '--no-interactive',
974
+ '--json',
975
+ '--name',
976
+ 'platform',
977
+ '--link',
978
+ firstApi,
979
+ '--link',
980
+ secondApi,
981
+ ],
982
+ { cwd: tempDir, env }
983
+ );
984
+
985
+ expect(duplicate.exitCode).toBe(1);
986
+ expect(parseJson(duplicate).status[0]).toEqual(
987
+ expect.objectContaining({
988
+ code: 'duplicate_link_name',
989
+ message: expect.stringContaining(expectedFirstApi),
990
+ fix: expect.stringContaining('--link api-alt='),
991
+ })
992
+ );
993
+ expect(fs.existsSync(getWorkspaceRegistryPath({ globalDataDir: path.join(dataHome, 'openspec') }))).toBe(false);
994
+ });
995
+
996
+ it('removes a partially created workspace when setup fails after creating the root', async () => {
997
+ const api = mkdir('repos/api');
998
+ const originalDataHome = process.env.XDG_DATA_HOME;
999
+ process.env.XDG_DATA_HOME = dataHome;
1000
+ const writeFileSpy = vi
1001
+ .spyOn(FileSystemUtils, 'writeFile')
1002
+ .mockRejectedValueOnce(new Error('disk full'));
1003
+
1004
+ try {
1005
+ await expect(createManagedWorkspace('platform', { api })).rejects.toMatchObject({
1006
+ status: {
1007
+ code: 'workspace_create_failed',
1008
+ },
1009
+ });
1010
+ } finally {
1011
+ writeFileSpy.mockRestore();
1012
+ if (originalDataHome === undefined) {
1013
+ delete process.env.XDG_DATA_HOME;
1014
+ } else {
1015
+ process.env.XDG_DATA_HOME = originalDataHome;
1016
+ }
1017
+ }
1018
+
1019
+ const globalDataDir = path.join(dataHome, 'openspec');
1020
+ expect(fs.existsSync(getManagedWorkspaceRoot('platform', { globalDataDir }))).toBe(false);
1021
+ expect(fs.existsSync(getWorkspaceRegistryPath({ globalDataDir }))).toBe(false);
1022
+ });
1023
+
1024
+ it('rejects existing workspace names without overwriting workspace state', async () => {
1025
+ const api = mkdir('repos/api');
1026
+ const web = mkdir('repos/web');
1027
+ const setup = await setupWorkspace('platform', [`api=${api}`]);
1028
+ const workspaceRoot = setup.workspace.root;
1029
+ const viewBefore = fs.readFileSync(getWorkspaceViewStatePath(workspaceRoot), 'utf-8');
1030
+ const markerPath = path.join(workspaceRoot, 'sentinel.txt');
1031
+ fs.writeFileSync(markerPath, 'keep me');
1032
+
1033
+ const duplicate = await runCLI(
1034
+ [
1035
+ 'workspace',
1036
+ 'setup',
1037
+ '--no-interactive',
1038
+ '--json',
1039
+ '--name',
1040
+ 'platform',
1041
+ '--link',
1042
+ `web=${web}`,
1043
+ ],
1044
+ { cwd: tempDir, env }
1045
+ );
1046
+
1047
+ expect(duplicate.exitCode).toBe(1);
1048
+ expect(parseJson(duplicate).status[0]).toEqual(
1049
+ expect.objectContaining({
1050
+ code: 'workspace_already_exists',
1051
+ target: 'workspace.name',
1052
+ })
1053
+ );
1054
+ expect(fs.readFileSync(getWorkspaceViewStatePath(workspaceRoot), 'utf-8')).toBe(viewBefore);
1055
+ expect(fs.readFileSync(markerPath, 'utf-8')).toBe('keep me');
1056
+ });
1057
+
1058
+ it('fails setup cleanly for missing automation inputs and JSON without no-interactive', async () => {
1059
+ const api = mkdir('repos/api');
1060
+
1061
+ const noWorkspaces = await runCLI(['workspace', 'list'], { cwd: tempDir, env });
1062
+ expect(noWorkspaces.exitCode).toBe(0);
1063
+ expect(noWorkspaces.stdout).toContain("No OpenSpec workspaces found. Run 'openspec workspace setup' first.");
1064
+
1065
+ const missing = await runCLI(['workspace', 'setup', '--no-interactive', '--json'], {
1066
+ cwd: tempDir,
1067
+ env,
1068
+ });
1069
+ expect(missing.exitCode).toBe(1);
1070
+ expect(parseJson(missing).status[0]).toEqual(
1071
+ expect.objectContaining({
1072
+ code: 'missing_setup_inputs',
1073
+ severity: 'error',
1074
+ })
1075
+ );
1076
+
1077
+ const jsonInteractive = await runCLI(
1078
+ ['workspace', 'setup', '--json', '--name', 'platform', '--link', api],
1079
+ { cwd: tempDir, env }
1080
+ );
1081
+ expect(jsonInteractive.exitCode).toBe(1);
1082
+ expect(parseJson(jsonInteractive).status[0]).toEqual(
1083
+ expect.objectContaining({
1084
+ code: 'setup_json_requires_no_interactive',
1085
+ })
1086
+ );
1087
+
1088
+ const invalidName = await runCLI(
1089
+ ['workspace', 'setup', '--no-interactive', '--json', '--name', 'Bad_Name', '--link', api],
1090
+ { cwd: tempDir, env }
1091
+ );
1092
+ expect(invalidName.exitCode).toBe(1);
1093
+ expect(parseJson(invalidName).status[0]).toEqual(
1094
+ expect.objectContaining({
1095
+ code: 'invalid_workspace_name',
1096
+ message: expect.stringContaining('kebab-case'),
1097
+ })
1098
+ );
1099
+
1100
+ const noKnown = await runCLI(['workspace', 'doctor', '--json'], { cwd: tempDir, env });
1101
+ expect(noKnown.exitCode).toBe(1);
1102
+ expect(parseJson(noKnown).status[0]).toEqual(
1103
+ expect.objectContaining({
1104
+ code: 'no_known_workspaces',
1105
+ })
1106
+ );
1107
+ });
1108
+
1109
+ it('rejects missing setup, link, and relink paths with structured status', async () => {
1110
+ const api = mkdir('repos/api');
1111
+ const billing = mkdir('repos/billing');
1112
+
1113
+ const missingSetupPath = await runCLI(
1114
+ [
1115
+ 'workspace',
1116
+ 'setup',
1117
+ '--no-interactive',
1118
+ '--json',
1119
+ '--name',
1120
+ 'missing-setup-path',
1121
+ '--link',
1122
+ 'missing-api',
1123
+ ],
1124
+ { cwd: tempDir, env }
1125
+ );
1126
+ expect(missingSetupPath.exitCode).toBe(1);
1127
+ expect(parseJson(missingSetupPath).status[0]).toEqual(
1128
+ expect.objectContaining({
1129
+ code: 'linked_path_missing',
1130
+ target: 'link.path',
1131
+ })
1132
+ );
1133
+
1134
+ await setupWorkspace('platform', [`api=${api}`]);
1135
+
1136
+ const missingLinkPath = await runCLI(
1137
+ ['workspace', 'link', 'missing-service', '--json'],
1138
+ { cwd: tempDir, env }
1139
+ );
1140
+ expect(missingLinkPath.exitCode).toBe(1);
1141
+ expect(parseJson(missingLinkPath).status[0]).toEqual(
1142
+ expect.objectContaining({
1143
+ code: 'linked_path_missing',
1144
+ target: 'link.path',
1145
+ })
1146
+ );
1147
+
1148
+ const link = await runCLI(['workspace', 'link', 'billing', billing, '--json'], {
1149
+ cwd: tempDir,
1150
+ env,
1151
+ });
1152
+ expect(link.exitCode).toBe(0);
1153
+
1154
+ const missingRelinkPath = await runCLI(
1155
+ ['workspace', 'relink', 'billing', 'missing-billing', '--json'],
1156
+ { cwd: tempDir, env }
1157
+ );
1158
+ expect(missingRelinkPath.exitCode).toBe(1);
1159
+ expect(parseJson(missingRelinkPath).status[0]).toEqual(
1160
+ expect.objectContaining({
1161
+ code: 'linked_path_missing',
1162
+ target: 'link.path',
1163
+ })
1164
+ );
1165
+ });
1166
+
1167
+ it('links, rejects duplicate link names, relinks, and reports unknown relinks', async () => {
1168
+ const api = mkdir('repos/api');
1169
+ const billing = mkdir('repos/platform/services/billing');
1170
+ const billingNew = mkdir('repos/archive/billing');
1171
+ const duplicate = mkdir('repos/duplicate-billing');
1172
+ const expectedBilling = expectedExistingPath(billing);
1173
+ const expectedBillingNew = expectedExistingPath(billingNew);
1174
+
1175
+ await setupWorkspace('platform', [`api=${api}`]);
1176
+
1177
+ const link = await runCLI(['workspace', 'link', billing, '--json'], { cwd: tempDir, env });
1178
+ expect(link.exitCode).toBe(0);
1179
+ expect(parseJson(link).link).toEqual(
1180
+ expect.objectContaining({
1181
+ name: 'billing',
1182
+ path: expectedBilling,
1183
+ status: [],
1184
+ })
1185
+ );
1186
+
1187
+ const duplicateResult = await runCLI(
1188
+ ['workspace', 'link', 'billing', duplicate, '--json'],
1189
+ { cwd: tempDir, env }
1190
+ );
1191
+ expect(duplicateResult.exitCode).toBe(1);
1192
+ expect(parseJson(duplicateResult).status[0]).toEqual(
1193
+ expect.objectContaining({
1194
+ code: 'duplicate_link_name',
1195
+ message: expect.stringContaining('already uses that name'),
1196
+ })
1197
+ );
1198
+
1199
+ const relink = await runCLI(['workspace', 'relink', 'billing', billingNew, '--json'], {
1200
+ cwd: tempDir,
1201
+ env,
1202
+ });
1203
+ expect(relink.exitCode).toBe(0);
1204
+ expect(parseJson(relink).link).toEqual(
1205
+ expect.objectContaining({
1206
+ name: 'billing',
1207
+ path: expectedBillingNew,
1208
+ })
1209
+ );
1210
+
1211
+ const unknown = await runCLI(['workspace', 'relink', 'web', billingNew, '--json'], {
1212
+ cwd: tempDir,
1213
+ env,
1214
+ });
1215
+ expect(unknown.exitCode).toBe(1);
1216
+ expect(parseJson(unknown).status[0]).toEqual(
1217
+ expect.objectContaining({
1218
+ code: 'unknown_link_name',
1219
+ })
1220
+ );
1221
+ });
1222
+
1223
+ it('links monorepo folders without editing the linked folder', async () => {
1224
+ const api = mkdir('repos/api');
1225
+ const packageDir = mkdir('monorepo/apps/checkout');
1226
+ const expectedPackageDir = expectedExistingPath(packageDir);
1227
+ const sentinelPath = path.join(packageDir, 'package.json');
1228
+ fs.writeFileSync(sentinelPath, '{"name":"checkout"}\n');
1229
+ const entriesBefore = fs.readdirSync(packageDir).sort();
1230
+
1231
+ await setupWorkspace('platform', [`api=${api}`]);
1232
+
1233
+ const link = await runCLI(['workspace', 'link', packageDir, '--json'], {
1234
+ cwd: tempDir,
1235
+ env,
1236
+ });
1237
+
1238
+ expect(link.exitCode).toBe(0);
1239
+ expect(parseJson(link).link).toEqual(
1240
+ expect.objectContaining({
1241
+ name: 'checkout',
1242
+ path: expectedPackageDir,
1243
+ })
1244
+ );
1245
+ expect(fs.readFileSync(sentinelPath, 'utf-8')).toBe('{"name":"checkout"}\n');
1246
+ expect(fs.readdirSync(packageDir).sort()).toEqual(entriesBefore);
1247
+ expect(fs.existsSync(path.join(packageDir, 'openspec'))).toBe(false);
1248
+ expect(fs.existsSync(path.join(packageDir, WORKSPACE_METADATA_DIR_NAME))).toBe(false);
1249
+ });
1250
+
1251
+ it('fails link and relink without rewriting malformed workspace state', async () => {
1252
+ const api = mkdir('repos/api');
1253
+ const billing = mkdir('repos/billing');
1254
+ const setup = await setupWorkspace('broken-local', [`api=${api}`]);
1255
+ const statePath = getWorkspaceViewStatePath(setup.workspace.root);
1256
+ const malformedState = 'version: 1\npaths: []\n';
1257
+ fs.writeFileSync(statePath, malformedState);
1258
+
1259
+ const link = await runCLI(
1260
+ ['workspace', 'link', 'billing', billing, '--workspace', 'broken-local', '--json'],
1261
+ { cwd: tempDir, env }
1262
+ );
1263
+ expect(link.exitCode).toBe(1);
1264
+ expect(parseJson(link).status[0]).toEqual(
1265
+ expect.objectContaining({
1266
+ code: 'workspace_state_invalid',
1267
+ target: 'workspace.state',
1268
+ })
1269
+ );
1270
+ expect(fs.readFileSync(statePath, 'utf-8')).toBe(malformedState);
1271
+
1272
+ const relink = await runCLI(
1273
+ ['workspace', 'relink', 'api', billing, '--workspace', 'broken-local', '--json'],
1274
+ { cwd: tempDir, env }
1275
+ );
1276
+ expect(relink.exitCode).toBe(1);
1277
+ expect(parseJson(relink).status[0]).toEqual(
1278
+ expect.objectContaining({
1279
+ code: 'workspace_state_invalid',
1280
+ target: 'workspace.state',
1281
+ })
1282
+ );
1283
+ expect(fs.readFileSync(statePath, 'utf-8')).toBe(malformedState);
1284
+ });
1285
+
1286
+ it('drops deleted managed workspace roots from scanned workspace selection', async () => {
1287
+ const api = mkdir('repos/api');
1288
+ const setup = await setupWorkspace('platform', [`api=${api}`]);
1289
+ const registryPath = getWorkspaceRegistryPath({ globalDataDir: path.join(dataHome, 'openspec') });
1290
+ expect(fs.existsSync(registryPath)).toBe(false);
1291
+
1292
+ fs.rmSync(setup.workspace.root, { recursive: true, force: true });
1293
+
1294
+ const list = await runCLI(['workspace', 'list', '--json'], { cwd: tempDir, env });
1295
+ expect(list.exitCode).toBe(0);
1296
+ expect(parseJson(list).workspaces).toEqual([]);
1297
+
1298
+ const doctor = await runCLI(['workspace', 'doctor', '--workspace', 'platform', '--json'], {
1299
+ cwd: tempDir,
1300
+ env,
1301
+ });
1302
+ expect(doctor.exitCode).toBe(1);
1303
+ expect(parseJson(doctor).status[0]).toEqual(
1304
+ expect.objectContaining({
1305
+ code: 'workspace_not_found',
1306
+ })
1307
+ );
1308
+ expect(fs.existsSync(registryPath)).toBe(false);
1309
+ });
1310
+
1311
+ it('reports malformed workspace state in list and doctor without rewriting files', async () => {
1312
+ const api = mkdir('repos/api');
1313
+ const setup = await setupWorkspace('doctor-local-invalid', [`api=${api}`]);
1314
+ const statePath = getWorkspaceViewStatePath(setup.workspace.root);
1315
+ const registryPath = getWorkspaceRegistryPath({ globalDataDir: path.join(dataHome, 'openspec') });
1316
+ const malformedState = 'version: 1\npaths: []\n';
1317
+ expect(fs.existsSync(registryPath)).toBe(false);
1318
+ fs.writeFileSync(statePath, malformedState);
1319
+
1320
+ const list = await runCLI(['workspace', 'list', '--json'], { cwd: tempDir, env });
1321
+ expect(list.exitCode).toBe(0);
1322
+ expect(parseJson(list).workspaces[0].status[0]).toEqual(
1323
+ expect.objectContaining({
1324
+ code: 'workspace_state_invalid',
1325
+ })
1326
+ );
1327
+
1328
+ const humanList = await runCLI(['workspace', 'list'], { cwd: tempDir, env });
1329
+ expect(humanList.exitCode).toBe(0);
1330
+ expect(humanList.stdout).toContain('Workspace state could not be read');
1331
+
1332
+ const doctor = await runCLI(
1333
+ ['workspace', 'doctor', '--workspace', 'doctor-local-invalid', '--json'],
1334
+ { cwd: tempDir, env }
1335
+ );
1336
+ expect(doctor.exitCode).toBe(0);
1337
+ const doctorPayload = parseJson(doctor);
1338
+ expect(doctorPayload.workspace.status[0]).toEqual(
1339
+ expect.objectContaining({
1340
+ code: 'workspace_state_invalid',
1341
+ target: 'workspace.root',
1342
+ })
1343
+ );
1344
+ expect(doctorPayload.workspace.links).toEqual([]);
1345
+ expect(fs.readFileSync(statePath, 'utf-8')).toBe(malformedState);
1346
+ expect(fs.existsSync(registryPath)).toBe(false);
1347
+ });
1348
+
1349
+ it('reports missing linked paths without repairing workspace state', async () => {
1350
+ const api = mkdir('repos/api');
1351
+ const localOnly = mkdir('repos/local-only');
1352
+ const setup = await setupWorkspace('platform', [`api=${api}`]);
1353
+ const workspaceRoot = setup.workspace.root;
1354
+ const registryPath = getWorkspaceRegistryPath({ globalDataDir: path.join(dataHome, 'openspec') });
1355
+ const missingApiPath = path.join(tempDir, 'repos', 'missing-api');
1356
+ const viewState = `version: 1
1357
+ name: platform
1358
+ context: null
1359
+ links:
1360
+ api: ${missingApiPath}
1361
+ local-only: ${localOnly}
1362
+ `;
1363
+ fs.writeFileSync(getWorkspaceViewStatePath(workspaceRoot), viewState);
1364
+ expect(fs.existsSync(registryPath)).toBe(false);
1365
+
1366
+ const doctor = await runCLI(['workspace', 'doctor', '--workspace', 'platform', '--json'], {
1367
+ cwd: tempDir,
1368
+ env,
1369
+ });
1370
+
1371
+ expect(doctor.exitCode).toBe(0);
1372
+ const payload = parseJson(doctor);
1373
+ expect(payload.workspace.status).toEqual([]);
1374
+ expect(payload.workspace.links).toEqual([
1375
+ expect.objectContaining({
1376
+ name: 'api',
1377
+ path: missingApiPath,
1378
+ status: [
1379
+ expect.objectContaining({
1380
+ code: 'linked_path_missing',
1381
+ fix: expect.stringContaining('workspace relink api'),
1382
+ }),
1383
+ ],
1384
+ }),
1385
+ expect.objectContaining({
1386
+ name: 'local-only',
1387
+ path: expect.any(String),
1388
+ status: [],
1389
+ }),
1390
+ ]);
1391
+ expectSameExistingPath(
1392
+ payload.workspace.links.find((link: any) => link.name === 'local-only')?.path ?? null,
1393
+ localOnly
1394
+ );
1395
+ expect(fs.readFileSync(getWorkspaceViewStatePath(workspaceRoot), 'utf-8')).toBe(viewState);
1396
+ expect(fs.existsSync(registryPath)).toBe(false);
1397
+ });
1398
+
1399
+ it('uses current unlisted legacy workspaces for doctor and link without writing a registry', async () => {
1400
+ const manualRoot = path.join(tempDir, 'manual-workspace');
1401
+ const nested = path.join(manualRoot, WORKSPACE_CHANGES_DIR_NAME, 'add-billing');
1402
+ const api = mkdir('repos/api');
1403
+
1404
+ fs.mkdirSync(path.join(manualRoot, WORKSPACE_METADATA_DIR_NAME), { recursive: true });
1405
+ fs.mkdirSync(nested, { recursive: true });
1406
+ fs.writeFileSync(
1407
+ path.join(manualRoot, WORKSPACE_METADATA_DIR_NAME, WORKSPACE_LEGACY_SHARED_STATE_FILE_NAME),
1408
+ 'version: 1\nname: manual-workspace\nlinks: {}\n'
1409
+ );
1410
+ fs.writeFileSync(
1411
+ path.join(manualRoot, WORKSPACE_METADATA_DIR_NAME, WORKSPACE_LEGACY_LOCAL_STATE_FILE_NAME),
1412
+ 'version: 1\npaths: {}\n'
1413
+ );
1414
+
1415
+ const registryPath = getWorkspaceRegistryPath({ globalDataDir: path.join(dataHome, 'openspec') });
1416
+ const doctor = await runCLI(['workspace', 'doctor', '--json'], { cwd: nested, env });
1417
+ expect(doctor.exitCode).toBe(0);
1418
+ expect(parseJson(doctor).status[0]).toEqual(
1419
+ expect.objectContaining({
1420
+ code: 'workspace_not_in_known_views',
1421
+ severity: 'warning',
1422
+ })
1423
+ );
1424
+ expect(fs.existsSync(registryPath)).toBe(false);
1425
+
1426
+ const link = await runCLI(['workspace', 'link', 'api', api, '--json'], {
1427
+ cwd: nested,
1428
+ env,
1429
+ });
1430
+ expect(link.exitCode).toBe(0);
1431
+ expect(parseJson(link).status[0]).toEqual(
1432
+ expect.objectContaining({
1433
+ code: 'workspace_not_in_known_views',
1434
+ })
1435
+ );
1436
+
1437
+ expect(fs.existsSync(registryPath)).toBe(false);
1438
+ });
1439
+
1440
+ it('fails JSON workspace selection when multiple known workspaces are available', async () => {
1441
+ const api = mkdir('repos/api');
1442
+ const web = mkdir('repos/web');
1443
+
1444
+ await setupWorkspace('platform', [`api=${api}`]);
1445
+ await setupWorkspace('checkout-web', [`web=${web}`]);
1446
+
1447
+ const doctor = await runCLI(['workspace', 'doctor', '--json'], { cwd: tempDir, env });
1448
+ expect(doctor.exitCode).toBe(1);
1449
+ expect(parseJson(doctor).status[0]).toEqual(
1450
+ expect.objectContaining({
1451
+ code: 'workspace_selection_ambiguous',
1452
+ fix: expect.stringContaining('--workspace <name>'),
1453
+ })
1454
+ );
1455
+ });
1456
+
1457
+ it('uses --workspace for explicit selection and reports unknown workspace names', async () => {
1458
+ const api = mkdir('repos/api');
1459
+ const web = mkdir('repos/web');
1460
+
1461
+ await setupWorkspace('platform', [`api=${api}`]);
1462
+ const checkout = await setupWorkspace('checkout-web', [`web=${web}`]);
1463
+
1464
+ const doctor = await runCLI(
1465
+ ['workspace', 'doctor', '--workspace', 'checkout-web', '--json'],
1466
+ { cwd: tempDir, env }
1467
+ );
1468
+ expect(doctor.exitCode).toBe(0);
1469
+ expect(parseJson(doctor).workspace).toEqual(
1470
+ expect.objectContaining({
1471
+ name: 'checkout-web',
1472
+ root: expectedExistingPath(checkout.workspace.root),
1473
+ })
1474
+ );
1475
+
1476
+ const unknown = await runCLI(
1477
+ ['workspace', 'doctor', '--workspace', 'unknown-workspace', '--json'],
1478
+ { cwd: tempDir, env }
1479
+ );
1480
+ expect(unknown.exitCode).toBe(1);
1481
+ expect(parseJson(unknown).status[0]).toEqual(
1482
+ expect.objectContaining({
1483
+ code: 'workspace_not_found',
1484
+ target: 'workspace.name',
1485
+ })
1486
+ );
1487
+ });
1488
+
1489
+ it('fails non-interactive ambiguous workspace selection in human output mode', async () => {
1490
+ const api = mkdir('repos/api');
1491
+ const web = mkdir('repos/web');
1492
+
1493
+ await setupWorkspace('platform', [`api=${api}`]);
1494
+ await setupWorkspace('checkout-web', [`web=${web}`]);
1495
+
1496
+ const doctor = await runCLI(['workspace', 'doctor', '--no-interactive'], {
1497
+ cwd: tempDir,
1498
+ env,
1499
+ });
1500
+
1501
+ expect(doctor.exitCode).toBe(1);
1502
+ expect(doctor.stderr).toContain('Multiple OpenSpec workspaces are known.');
1503
+ expect(doctor.stderr).toContain('Pass --workspace <name>.');
1504
+ expect(doctor.stderr).toContain('openspec workspace doctor --workspace <name>');
1505
+ });
1506
+
1507
+ it('opens a workspace through VS Code editor and agent overrides without changing stored preference', async () => {
1508
+ const api = mkdir('repos/api');
1509
+ const expectedApi = expectedExistingPath(api);
1510
+ const web = mkdir('repos/web');
1511
+ const setup = await setupWorkspace('platform', [`api=${api}`, `web=${web}`], ['--opener', 'editor']);
1512
+ fs.rmSync(web, { recursive: true, force: true });
1513
+ const code = createFakeExecutable('code');
1514
+
1515
+ const editorOpen = await runCLI(['workspace', 'open', 'platform', '--no-interactive'], {
1516
+ cwd: tempDir,
1517
+ env: envWithFakeExecutable(code),
1518
+ });
1519
+
1520
+ expect(editorOpen.exitCode).toBe(0);
1521
+ expect(editorOpen.stdout).toContain('Opening workspace: platform');
1522
+ expect(editorOpen.stdout).toContain('Opener: VS Code editor');
1523
+ expect(editorOpen.stdout).toContain('web ->');
1524
+ const workspaceFolders = JSON.parse(
1525
+ fs.readFileSync(getWorkspaceCodeWorkspacePath(setup.workspace.root, 'platform'), 'utf-8')
1526
+ ).folders;
1527
+ expect(workspaceFolders).toEqual([
1528
+ {
1529
+ name: 'api',
1530
+ path: expectedApi,
1531
+ },
1532
+ {
1533
+ name: 'OpenSpec workspace',
1534
+ path: '.',
1535
+ },
1536
+ ]);
1537
+ const editorLaunch = readLaunchLog(code.logPath);
1538
+ expect(fs.realpathSync.native(editorLaunch.cwd)).toBe(
1539
+ fs.realpathSync.native(setup.workspace.root)
1540
+ );
1541
+ expect(editorLaunch.args).toEqual([
1542
+ getWorkspaceCodeWorkspacePath(expectedExistingPath(setup.workspace.root), 'platform'),
1543
+ ]);
1544
+
1545
+ const currentWorkspaceOpen = await runCLI(['workspace', 'open', '--editor', '--no-interactive'], {
1546
+ cwd: setup.workspace.root,
1547
+ env: envWithFakeExecutable(code),
1548
+ });
1549
+ expect(currentWorkspaceOpen.exitCode).toBe(0);
1550
+
1551
+ const codex = createFakeExecutable('codex');
1552
+ const codexOpen = await runCLI(
1553
+ ['workspace', 'open', '--workspace', 'platform', '--agent', 'codex', '--no-interactive'],
1554
+ {
1555
+ cwd: tempDir,
1556
+ env: envWithFakeExecutable(codex),
1557
+ }
1558
+ );
1559
+
1560
+ expect(codexOpen.exitCode).toBe(0);
1561
+ const codexLaunch = readLaunchLog(codex.logPath);
1562
+ expect(fs.realpathSync.native(codexLaunch.cwd)).toBe(
1563
+ fs.realpathSync.native(setup.workspace.root)
1564
+ );
1565
+ expect(codexLaunch.args).toEqual([
1566
+ '--sandbox',
1567
+ 'workspace-write',
1568
+ '--add-dir',
1569
+ expectedApi,
1570
+ 'Open this OpenSpec workspace.',
1571
+ ]);
1572
+ expect(readWorkspaceState(setup.workspace.root).preferred_opener).toEqual({
1573
+ kind: 'editor',
1574
+ id: 'vscode',
1575
+ });
1576
+ });
1577
+
1578
+ it('reports workspace open selection errors', async () => {
1579
+ const api = mkdir('repos/api');
1580
+ const web = mkdir('repos/web');
1581
+
1582
+ const noKnown = await runCLI(['workspace', 'open', '--no-interactive'], {
1583
+ cwd: tempDir,
1584
+ env,
1585
+ });
1586
+ expect(noKnown.exitCode).toBe(1);
1587
+ expect(noKnown.stderr).toContain("No known OpenSpec workspaces. Run 'openspec workspace setup' first.");
1588
+
1589
+ await setupWorkspace('platform', [`api=${api}`]);
1590
+ await setupWorkspace('checkout-web', [`web=${web}`]);
1591
+
1592
+ const conflict = await runCLI(
1593
+ ['workspace', 'open', 'platform', '--workspace', 'checkout-web', '--editor', '--no-interactive'],
1594
+ { cwd: tempDir, env }
1595
+ );
1596
+ expect(conflict.exitCode).toBe(1);
1597
+ expect(conflict.stderr).toContain("positional 'platform'");
1598
+ expect(conflict.stderr).toContain("--workspace 'checkout-web'");
1599
+
1600
+ const ambiguous = await runCLI(['workspace', 'open', '--no-interactive'], {
1601
+ cwd: tempDir,
1602
+ env,
1603
+ });
1604
+ expect(ambiguous.exitCode).toBe(1);
1605
+ expect(ambiguous.stderr).toContain('Known workspaces: checkout-web, platform');
1606
+
1607
+ const jsonAmbiguous = await runCLI(['workspace', 'open', '--json'], {
1608
+ cwd: tempDir,
1609
+ env,
1610
+ });
1611
+ expect(jsonAmbiguous.exitCode).toBe(1);
1612
+ expect(parseJson(jsonAmbiguous).status[0]).toEqual(
1613
+ expect.objectContaining({
1614
+ code: 'workspace_selection_ambiguous',
1615
+ })
1616
+ );
1617
+ });
1618
+
1619
+ it('reports unsupported workspace open options before workspace selection', async () => {
1620
+ const unsupported = await runCLI(['workspace', 'open', '--prepare-only'], {
1621
+ cwd: tempDir,
1622
+ env,
1623
+ });
1624
+ expect(unsupported.exitCode).toBe(1);
1625
+ expect(unsupported.stderr).toContain('future context/query surface');
1626
+
1627
+ const changeUnsupported = await runCLI(['workspace', 'open', '--change', 'add-api'], {
1628
+ cwd: tempDir,
1629
+ env,
1630
+ });
1631
+ expect(changeUnsupported.exitCode).toBe(1);
1632
+ expect(changeUnsupported.stderr).toContain('root workspace open only');
1633
+
1634
+ const openerConflict = await runCLI(
1635
+ ['workspace', 'open', 'platform', '--agent', 'codex-cli', '--editor', '--no-interactive'],
1636
+ {
1637
+ cwd: tempDir,
1638
+ env,
1639
+ }
1640
+ );
1641
+ expect(openerConflict.exitCode).toBe(1);
1642
+ expect(openerConflict.stderr).toContain('either --agent <tool> or --editor');
1643
+ });
1644
+
1645
+ it('reports unset and unavailable workspace opener errors', async () => {
1646
+ const api = mkdir('repos/api');
1647
+ const platform = await setupWorkspace('platform', [`api=${api}`]);
1648
+
1649
+ const unset = await runCLI(['workspace', 'open', 'platform', '--no-interactive'], {
1650
+ cwd: tempDir,
1651
+ env,
1652
+ });
1653
+ expect(unset.exitCode).toBe(1);
1654
+ expect(unset.stderr).toContain('does not have a preferred opener');
1655
+
1656
+ fs.writeFileSync(
1657
+ getWorkspaceViewStatePath(platform.workspace.root),
1658
+ `version: 1
1659
+ name: platform
1660
+ context: null
1661
+ links:
1662
+ api: ${api}
1663
+ preferred_opener:
1664
+ kind: editor
1665
+ id: vscode
1666
+ `
1667
+ );
1668
+ const unavailable = await runCLI(['workspace', 'open', 'platform', '--no-interactive'], {
1669
+ cwd: tempDir,
1670
+ env: {
1671
+ ...env,
1672
+ PATH: '',
1673
+ },
1674
+ });
1675
+ expect(unavailable.exitCode).toBe(1);
1676
+ expect(unavailable.stderr).toContain("'code' was not found on PATH");
1677
+ expect(unavailable.stderr).toContain(
1678
+ getWorkspaceCodeWorkspacePath(expectedExistingPath(platform.workspace.root), 'platform')
1679
+ );
1680
+ });
1681
+
1682
+ it('prints readable human output for setup, list, and doctor', async () => {
1683
+ const api = mkdir('repos/api');
1684
+ const expectedApi = expectedExistingPath(api);
1685
+
1686
+ const setup = await runCLI(
1687
+ ['workspace', 'setup', '--no-interactive', '--name', 'platform', '--link', `api=${api}`],
1688
+ { cwd: tempDir, env }
1689
+ );
1690
+ expect(setup.exitCode).toBe(0);
1691
+ expect(setup.stdout).toContain('Workspace setup complete');
1692
+ expect(setup.stdout).toContain('OpenSpec workspaces (1)');
1693
+ expect(setup.stdout).toContain('Location:');
1694
+ expect(setup.stdout).not.toContain('Root:');
1695
+ expect(setup.stdout).toContain('Linked repos or folders (1):');
1696
+ expect(setup.stdout).toContain(`api -> ${expectedApi}`);
1697
+ expect(setup.stdout).toContain('Workspace check:');
1698
+ expect(setup.stdout).toContain('No workspace issues found.');
1699
+ expect(setup.stdout).toContain('Next useful commands:');
1700
+
1701
+ const list = await runCLI(['workspace', 'list'], { cwd: tempDir, env });
1702
+ expect(list.exitCode).toBe(0);
1703
+ expect(list.stdout).toContain('OpenSpec workspaces (1)');
1704
+ expect(list.stdout).toContain('platform');
1705
+ expect(list.stdout).toContain('Location:');
1706
+ expect(list.stdout).not.toContain('Root:');
1707
+ expect(list.stdout).toContain('Linked repos or folders (1):');
1708
+ expect(list.stdout).toContain(`api -> ${expectedApi}`);
1709
+
1710
+ const doctor = await runCLI(['workspace', 'doctor', '--workspace', 'platform'], {
1711
+ cwd: tempDir,
1712
+ env,
1713
+ });
1714
+ expect(doctor.exitCode).toBe(0);
1715
+ expect(doctor.stdout).toContain('Workspace: platform');
1716
+ expect(doctor.stdout).toContain('Location:');
1717
+ expect(doctor.stdout).not.toContain('Root:');
1718
+ expect(doctor.stdout).toContain('Linked repos or folders:');
1719
+ expect(doctor.stdout).toContain('No workspace issues found.');
1720
+ });
1721
+
1722
+ it('does not expose workspace create as a public command', async () => {
1723
+ const help = await runCLI(['workspace', '--help'], { cwd: tempDir, env });
1724
+ expect(help.exitCode).toBe(0);
1725
+ expect(help.stdout).toContain('setup');
1726
+ expect(help.stdout).toContain('update');
1727
+ expect(help.stdout).toContain('link');
1728
+ expect(help.stdout).toContain('relink');
1729
+ expect(help.stdout).not.toMatch(/\bcreate\b/u);
1730
+
1731
+ const updateHelp = await runCLI(['workspace', 'update', '--help'], { cwd: tempDir, env });
1732
+ expect(updateHelp.exitCode).toBe(0);
1733
+ expect(updateHelp.stdout).toContain('guidance and agent skills');
1734
+ expect(updateHelp.stdout).toContain('--workspace');
1735
+ expect(updateHelp.stdout).toContain('--tools');
1736
+ expect(updateHelp.stdout).toMatch(/Global\s+profile\s+selects workflows/u);
1737
+ });
1738
+
1739
+ it('registers workspace subcommands for shell completions', () => {
1740
+ const workspace = COMMAND_REGISTRY.find((command) => command.name === 'workspace');
1741
+ const setup = workspace?.subcommands?.find((command) => command.name === 'setup');
1742
+ const link = workspace?.subcommands?.find((command) => command.name === 'link');
1743
+ const relink = workspace?.subcommands?.find((command) => command.name === 'relink');
1744
+ const update = workspace?.subcommands?.find((command) => command.name === 'update');
1745
+ const open = workspace?.subcommands?.find((command) => command.name === 'open');
1746
+
1747
+ expect(workspace?.subcommands?.map((command) => command.name)).toEqual([
1748
+ 'setup',
1749
+ 'list',
1750
+ 'ls',
1751
+ 'link',
1752
+ 'relink',
1753
+ 'doctor',
1754
+ 'update',
1755
+ 'open',
1756
+ ]);
1757
+ expect(setup?.flags?.some((flag) => flag.name === 'opener')).toBe(true);
1758
+ expect(setup?.flags?.find((flag) => flag.name === 'tools')?.description).toContain(
1759
+ 'Install OpenSpec skills'
1760
+ );
1761
+ expect(setup?.flags?.find((flag) => flag.name === 'opener')?.values).toEqual([
1762
+ 'codex-cli',
1763
+ 'claude',
1764
+ 'github-copilot',
1765
+ 'editor',
1766
+ ]);
1767
+ expect(link?.positionals).toEqual([
1768
+ { name: 'name-or-path', type: 'path', optional: true },
1769
+ { name: 'path', type: 'path', optional: true },
1770
+ ]);
1771
+ expect(relink?.positionals).toEqual([
1772
+ { name: 'name' },
1773
+ { name: 'path', type: 'path' },
1774
+ ]);
1775
+ expect(update?.positionals).toEqual([
1776
+ { name: 'name', optional: true },
1777
+ ]);
1778
+ expect(update?.flags?.map((flag) => flag.name)).toEqual([
1779
+ 'workspace',
1780
+ 'tools',
1781
+ 'json',
1782
+ 'no-interactive',
1783
+ ]);
1784
+ expect(update?.description).toContain('guidance and agent skills');
1785
+ expect(update?.flags?.find((flag) => flag.name === 'tools')?.description).toContain(
1786
+ 'global profile selects workflows'
1787
+ );
1788
+ expect(update?.flags?.find((flag) => flag.name === 'tools')?.description).toContain(
1789
+ 'skills-only'
1790
+ );
1791
+ expect(open?.positionals).toEqual([
1792
+ { name: 'name', optional: true },
1793
+ ]);
1794
+ expect(open?.flags?.find((flag) => flag.name === 'agent')?.values).toEqual([
1795
+ 'codex-cli',
1796
+ 'claude',
1797
+ 'github-copilot',
1798
+ ]);
1799
+ expect(open?.flags?.map((flag) => flag.name)).toEqual([
1800
+ 'workspace',
1801
+ 'initiative',
1802
+ 'store',
1803
+ 'store-path',
1804
+ 'agent',
1805
+ 'editor',
1806
+ 'prepare-only',
1807
+ 'json',
1808
+ 'change',
1809
+ 'no-interactive',
1810
+ ]);
1811
+ });
1812
+ });