@uniqbit/mate-core 0.18.4 → 1.0.0-canary.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 (231) hide show
  1. package/claude-plugin/hooks/companion-instructions.mjs +9 -0
  2. package/claude-plugin/hooks/hooks.json +12 -2
  3. package/opencode-plugin/src/add-dir.ts +76 -10
  4. package/opencode-plugin/src/companion.ts +1 -8
  5. package/opencode-plugin/src/server.ts +2 -15
  6. package/package.json +10 -2
  7. package/src/brand/art.generated.ts +69 -0
  8. package/src/brand/index.ts +12 -0
  9. package/src/brand/palette.ts +19 -0
  10. package/src/brand/svg.generated.ts +8 -0
  11. package/src/brand/terminal.ts +83 -0
  12. package/src/brand/types.ts +33 -0
  13. package/src/cli/commands/artifact/artifact.ts +8 -0
  14. package/src/cli/commands/artifact/delivery/archive.ts +117 -0
  15. package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
  16. package/src/cli/commands/artifact/delivery/environment.ts +104 -0
  17. package/src/cli/commands/artifact/delivery/link.ts +154 -0
  18. package/src/cli/commands/artifact/delivery/output.ts +58 -0
  19. package/src/cli/commands/artifact/finish/command.ts +33 -3
  20. package/src/cli/commands/artifact/finish/engine.ts +47 -3
  21. package/src/cli/commands/artifact/finish/finisher.ts +10 -0
  22. package/src/cli/commands/artifact/finish/openspec.ts +200 -129
  23. package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
  24. package/src/cli/commands/artifact/pending/command.ts +31 -3
  25. package/src/cli/commands/artifact/pending/discovery.ts +21 -57
  26. package/src/cli/commands/cap/graphify.ts +52 -3
  27. package/src/cli/commands/cap/index-cmd.ts +8 -1
  28. package/src/cli/commands/cap/openspec.ts +13 -25
  29. package/src/cli/commands/companion/open.ts +56 -39
  30. package/src/cli/commands/companion/update.ts +75 -42
  31. package/src/cli/commands/doctor.ts +257 -78
  32. package/src/cli/commands/install.ts +0 -8
  33. package/src/cli/commands/plugin/install.ts +9 -4
  34. package/src/cli/commands/plugin/plugin.ts +3 -2
  35. package/src/cli/commands/report/adapter.ts +11 -2
  36. package/src/cli/commands/report/contract.ts +21 -0
  37. package/src/cli/commands/report/highlight.ts +34 -4
  38. package/src/cli/commands/report/markdown.ts +110 -0
  39. package/src/cli/commands/report/palette.ts +118 -0
  40. package/src/cli/commands/report/renderer.ts +456 -153
  41. package/src/cli/commands/report/types.ts +16 -0
  42. package/src/cli/commands/setup.ts +7 -15
  43. package/src/cli/commands/studio/client-asset.ts +16 -0
  44. package/src/cli/commands/studio/openspec-cli.ts +10 -58
  45. package/src/cli/commands/studio/routes.ts +2 -0
  46. package/src/cli/commands/studio/selection.ts +19 -1
  47. package/src/cli/commands/studio/server.ts +42 -1
  48. package/src/cli/commands/studio/terminal-launch.ts +7 -16
  49. package/src/cli/commands/studio/views/client.ts +6 -616
  50. package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
  51. package/src/cli/commands/studio/views/document.tsx +137 -69
  52. package/src/cli/commands/studio/views/markdown.tsx +250 -0
  53. package/src/cli/commands/studio/views/palette.ts +134 -0
  54. package/src/cli/commands/studio/views/reports/index.tsx +2 -7
  55. package/src/cli/commands/studio/views/skills/index.tsx +43 -62
  56. package/src/cli/commands/studio/views/specs/index.tsx +7 -15
  57. package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
  58. package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
  59. package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
  60. package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
  61. package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
  62. package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
  63. package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
  64. package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
  65. package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
  66. package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
  67. package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
  68. package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
  69. package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
  70. package/src/cli/commands/studio/views/styles.ts +217 -206
  71. package/src/cli/commands/studio/views/vault/state.ts +3 -5
  72. package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
  73. package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
  74. package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
  75. package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
  76. package/src/cli/commands/wrap.ts +9 -42
  77. package/src/cli/main.ts +4 -1
  78. package/src/cli/parse-flags.ts +25 -7
  79. package/src/cli/setup-selector.tsx +38 -162
  80. package/src/cli/usage.ts +5 -2
  81. package/src/hooks/companion-instructions.ts +190 -0
  82. package/src/hooks/hook-io.ts +63 -0
  83. package/src/hooks/session-banner.ts +21 -29
  84. package/src/hooks/session-guidance.ts +10 -36
  85. package/src/hooks/session-prepare.ts +115 -28
  86. package/src/hooks/validate-artifact-path.ts +82 -24
  87. package/src/index.ts +4 -0
  88. package/src/lib/change-layout.ts +154 -0
  89. package/src/lib/components/wizard-header.tsx +3 -1
  90. package/src/lib/context-mode-package.ts +24 -11
  91. package/src/lib/delivery/archive-change.ts +154 -0
  92. package/src/lib/delivery/branches.ts +108 -0
  93. package/src/lib/delivery/environment.ts +29 -0
  94. package/src/lib/delivery/forge.ts +279 -0
  95. package/src/lib/delivery/overlap.ts +202 -0
  96. package/src/lib/delivery/record-delivery.ts +196 -0
  97. package/src/lib/delivery/record.ts +343 -0
  98. package/src/lib/delivery/refresh.ts +174 -0
  99. package/src/lib/delivery/repository-access.ts +97 -0
  100. package/src/lib/delivery/stages.ts +111 -0
  101. package/src/lib/delivery/token.ts +58 -0
  102. package/src/lib/frontmatter.ts +18 -0
  103. package/src/lib/opencode-plugin-package.ts +5 -5
  104. package/src/lib/openspec-cli.ts +116 -0
  105. package/src/lib/openspec-scopes.ts +86 -0
  106. package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
  107. package/src/lib/orchestrator/adapters/claude.ts +60 -162
  108. package/src/lib/orchestrator/adapters/opencode.ts +9 -24
  109. package/src/lib/orchestrator/config-store.ts +2 -70
  110. package/src/lib/orchestrator/editor.ts +9 -9
  111. package/src/lib/orchestrator/openspec-store.ts +14 -54
  112. package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
  113. package/src/lib/orchestrator/projection-claude-links.ts +159 -0
  114. package/src/lib/orchestrator/projection-claude-skills.ts +19 -190
  115. package/src/lib/orchestrator/projection-entries.ts +45 -15
  116. package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
  117. package/src/lib/orchestrator/projection-types.ts +3 -2
  118. package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
  119. package/src/lib/orchestrator/session-prepare.ts +49 -87
  120. package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
  121. package/src/lib/orchestrator/types.ts +2 -2
  122. package/src/lib/orchestrator/wrap-pass.ts +22 -88
  123. package/src/lib/package-paths.ts +8 -0
  124. package/src/opencode/companion-hooks.ts +64 -5
  125. package/src/opencode/companion-policy.ts +37 -18
  126. package/src/opencode/projected-guidance.ts +4 -1
  127. package/src/opencode/session-prepare.ts +2 -1
  128. package/src/opencode/tui.tsx +4 -2
  129. package/src/runtime/artifact-refusal.ts +63 -0
  130. package/src/runtime/claude-delivery-plan.ts +89 -7
  131. package/src/runtime/commit-attribution-guard.ts +181 -0
  132. package/src/runtime/companion-guidance.ts +15 -3
  133. package/src/runtime/companion-instructions.ts +340 -0
  134. package/src/runtime/error-message.ts +4 -0
  135. package/src/runtime/guidance.ts +0 -17
  136. package/src/runtime/index.ts +5 -0
  137. package/src/runtime/managed-block.ts +117 -0
  138. package/src/runtime/markdown-scan.ts +266 -0
  139. package/src/runtime/policy.ts +16 -15
  140. package/src/runtime/projected-flag.ts +7 -0
  141. package/src/runtime/projected-guidance.ts +1 -0
  142. package/src/runtime/runtime-document-manifest.ts +62 -28
  143. package/src/runtime/secret-guard.ts +148 -0
  144. package/src/runtime/secret-patterns.ts +164 -0
  145. package/src/runtime/secret-scan.ts +90 -0
  146. package/src/runtime/session-kind.ts +13 -0
  147. package/src/runtime/shell-heredoc.ts +81 -0
  148. package/src/runtime/wrap-state.ts +36 -6
  149. package/src/runtime/write-atomic.ts +64 -0
  150. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
  151. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
  152. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
  153. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  154. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
  155. package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
  156. package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
  157. package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
  158. package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
  159. package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
  160. package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
  161. package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
  162. package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
  163. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
  164. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
  165. package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
  166. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
  167. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
  168. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
  169. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  170. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
  171. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
  172. package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
  173. package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
  174. package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
  175. package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
  176. package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
  177. package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
  178. package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
  179. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
  180. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
  181. package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
  182. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
  183. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
  184. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
  185. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  186. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
  187. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
  188. package/src/templates/mate-skills/openspec-references.ts +140 -0
  189. package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
  190. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
  191. package/src/tools/setup/capabilities/context-mode.ts +25 -5
  192. package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
  193. package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
  194. package/src/tools/setup/capabilities/graphify.ts +12 -0
  195. package/src/tools/setup/capabilities/openspec.ts +273 -270
  196. package/src/tools/setup/capabilities/tokensave.ts +36 -3
  197. package/src/tools/setup/context-services.ts +9 -17
  198. package/src/tools/setup/dynamic-plugins/audiences.ts +7 -39
  199. package/src/tools/setup/dynamic-plugins/declarations.ts +1 -1
  200. package/src/tools/setup/dynamic-plugins/hydrate.ts +4 -7
  201. package/src/tools/setup/dynamic-plugins/install.ts +24 -28
  202. package/src/tools/setup/engine.ts +93 -148
  203. package/src/tools/setup/managed-output.ts +51 -0
  204. package/src/tools/setup/mate.ts +86 -11
  205. package/src/tools/setup/plugin.ts +37 -17
  206. package/src/tools/setup/plugins/gitignore.ts +1 -7
  207. package/src/tools/setup/plugins/guidance.ts +7 -4
  208. package/src/tools/setup/providers/agent-file-sections.ts +4 -2
  209. package/src/tools/setup/providers/claude-attribution.ts +65 -0
  210. package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
  211. package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
  212. package/src/tools/setup/providers/claude.ts +285 -34
  213. package/src/tools/setup/providers/opencode.ts +180 -11
  214. package/src/tools/setup/registry.ts +2 -24
  215. package/src/tools/setup/surface-target.ts +4 -13
  216. package/src/tools/setup/utils.ts +14 -4
  217. package/src/tools/setup.ts +131 -18
  218. package/wrappers/bin/graphify +52 -4
  219. package/wrappers/bin/openspec +33 -9
  220. package/wrappers/lib/graphify-gate.mjs +8 -0
  221. package/opencode-plugin/src/audience-layer.ts +0 -91
  222. package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
  223. package/src/runtime/audience-layer.ts +0 -84
  224. package/src/runtime/opencode-audience-layer.ts +0 -176
  225. package/src/tools/setup/audience-isolation.fixture.ts +0 -325
  226. package/src/tools/setup/audience-layer.ts +0 -207
  227. package/src/tools/setup/audience-registrations.ts +0 -52
  228. package/src/tools/setup/contribution-ledger.ts +0 -207
  229. package/src/tools/setup/isolation-diagnostics.ts +0 -139
  230. package/src/tools/setup/runtime-documents.ts +0 -291
  231. package/src/tools/setup/upgrade-sweep.ts +0 -153
@@ -1,35 +1,28 @@
1
- import { execFile as execFileCb } from "node:child_process";
2
1
  import fs from "node:fs/promises";
3
2
  import path from "node:path";
4
- import { promisify } from "node:util";
5
3
 
6
- import { getOpenSpecSchemaSelection } from "../../../lib/orchestrator/setup-compatibilities";
4
+ import { Document, isMap, isScalar, isSeq, parseDocument, type YAMLMap, type YAMLSeq } from "yaml";
5
+
6
+ import {
7
+ locateOpenSpec,
8
+ runOpenSpec,
9
+ type OpenSpecLocation,
10
+ type RunOpenSpec,
11
+ } from "../../../lib/openspec-cli";
7
12
  import { getWrapperBinPath } from "../../../lib/package-paths";
8
- import { fetchPublicPackageVersion } from "../../../lib/public-npm";
9
- import { isNewer, isPinnedDeployment } from "../../../lib/update-checker";
10
13
  import { FRAMEWORK_NAME } from "../../../framework";
11
- import { confirm } from "../../../cli/confirm";
12
- import type { CapabilityPlugin, RuntimeContributionsByRuntime, SetupContext } from "../plugin";
13
- import type { InstallRequirement } from "../install-contract";
14
- import {
15
- isCommandOnPath,
16
- mergeDir,
17
- pruneEmptyAncestors,
18
- resolveCommandOnPath,
19
- runCommand,
20
- runCommandSilently,
21
- } from "../utils";
22
- import { applyMateSkills, MANAGED_MATE_SKILL_NAMES, teardownMateSkills } from "../mate";
23
- import {
24
- getSharedSkillsDir,
25
- isSharedSkillRootActive,
26
- removeLegacyOpenCodeSkills,
27
- removeSharedSkills,
28
- } from "../providers/agents-skill-root";
29
-
30
- export { MATE_ARTIFACT_SKILLS, MATE_SKILLS } from "../mate";
31
-
32
- const execFileAsync = promisify(execFileCb);
14
+ import { removeRecorded } from "../managed-output";
15
+ import type {
16
+ CapabilityPlugin,
17
+ ManagedOutputRecorder,
18
+ RuntimeContributionsByRuntime,
19
+ SetupContext,
20
+ } from "../plugin";
21
+ import { mergeDir, pruneEmptyAncestors } from "../utils";
22
+ import { applyMateSkills, MATE_SKILLS, readSkillAuthor, teardownMateSkills } from "../mate";
23
+ import { getSharedSkillsDir, isSharedSkillRootActive } from "../providers/agents-skill-root";
24
+
25
+ export { MATE_SKILLS } from "../mate";
33
26
 
34
27
  export const OPENSPEC_SKILLS = [
35
28
  "openspec-explore",
@@ -40,11 +33,30 @@ export const OPENSPEC_SKILLS = [
40
33
  "openspec-update-change",
41
34
  ] as const;
42
35
 
36
+ /** Mate skills without a pre-seeded `Skill(...)` permission. */
37
+ const UNPERMITTED_MATE_SKILLS: ReadonlySet<string> = new Set(["mate-create-report"]);
38
+
43
39
  /** Written by the OpenSpec `agents` tool beside its skills in the shared root. */
44
40
  const OPENSPEC_AGENTS_MARKER = ".openspec-target";
45
41
 
46
- /** Skill names the capability owns in any skill root. */
47
- const MANAGED_SKILL_NAMES = [...OPENSPEC_SKILLS, ...MANAGED_MATE_SKILL_NAMES];
42
+ /** Command ids the OpenSpec CLI writes as `.claude/commands/opsx/<id>.md` and `.opencode/commands/opsx-<id>.md`. */
43
+ const OPENSPEC_COMMAND_IDS = [
44
+ "explore",
45
+ "new",
46
+ "continue",
47
+ "apply",
48
+ "update",
49
+ "ff",
50
+ "sync",
51
+ "archive",
52
+ "bulk-archive",
53
+ "verify",
54
+ "onboard",
55
+ "propose",
56
+ ] as const;
57
+
58
+ /** Skill roots earlier Mate versions let the OpenSpec CLI generate workflow skills into. */
59
+ const GENERATED_SKILL_ROOTS = [".claude", ".agents", ".opencode"] as const;
48
60
 
49
61
  /** Every Mate-owned schema is installed for availability; only one is ever activated as the companion default. */
50
62
  const MATE_SCHEMA_NAMES = ["mate-v1", "mate-minimal"] as const;
@@ -56,15 +68,26 @@ const MATE_SCHEMA_SOURCE_ROOT = path.join(
56
68
  const MATE_CONVENTIONS_SOURCE = path.join(MATE_SCHEMA_SOURCE_ROOT, "openspec-conventions.yaml");
57
69
 
58
70
  /**
59
- * Runtime → OpenSpec CLI tools. OpenCode reads workflow skills from the shared
60
- * `agents` root; its own tool only contributes the `/opsx-*` commands.
71
+ * Identifies Mate's own `rules.specs` entry, so a later release can replace its
72
+ * text instead of adding a second entry.
61
73
  */
62
- const OPENSPEC_TOOLS_BY_RUNTIME = {
63
- claude: ["claude"],
64
- opencode: ["agents", "opencode"],
65
- } as const;
74
+ export const MATE_SPECS_RULE_MARKER = "[mate] ";
75
+
76
+ /**
77
+ * Delivered through `rules.specs`, the one config hook OpenSpec's archive and
78
+ * sync workflows read when writing main specs. Propose reads it for delta specs
79
+ * too, so the text binds main specs only.
80
+ */
81
+ export const MATE_SPECS_RULE = `${MATE_SPECS_RULE_MARKER}A main spec at openspec/specs/<capability>/spec.md MUST begin with flat frontmatter: type: spec, capability: <capability>, repository: <org/repository> (one scalar), areas: [<area>, ...] (a flat list in the delta's scope order), and tags: [openspec/spec]. Project repository and areas from the delta's paired scopes; never copy scopes, change, or delta tags onto a main spec. Delta specs under openspec/changes/<name>/specs/ keep their \`mate:delta\` comment. Full rules: openspec/mate-conventions.yaml.`;
82
+
83
+ /** What Mate writes besides its entry; anything beyond the two is user content. */
84
+ const MANAGED_CONFIG_SCHEMA_ONLY = "schema: mate-v1";
85
+
86
+ /** The schema a fresh `openspec init` writes into config.yaml. */
87
+ const OPENSPEC_INIT_SCHEMA = "spec-driven";
66
88
 
67
- const OPENSPEC_INSTALL_COMMAND = "npm install -g @fission-ai/openspec@latest";
89
+ /** Unfolded output keeps the Mate entry on one line and user scalars unwrapped. */
90
+ const CONFIG_TO_STRING = { lineWidth: 0 } as const;
68
91
 
69
92
  /**
70
93
  * Store metadata `openspec store register` writes beside the companion. Its id
@@ -73,47 +96,10 @@ const OPENSPEC_INSTALL_COMMAND = "npm install -g @fission-ai/openspec@latest";
73
96
  */
74
97
  const OPENSPEC_GITIGNORE_ENTRIES = [".openspec-store/"];
75
98
 
76
- type OpenSpecTool =
77
- (typeof OPENSPEC_TOOLS_BY_RUNTIME)[keyof typeof OPENSPEC_TOOLS_BY_RUNTIME][number];
78
- type OpenSpecRunCommand = typeof runCommandSilently;
79
-
80
99
  export interface OpenSpecPluginDeps {
81
- runCommand?: OpenSpecRunCommand;
82
- installCommand?: typeof runCommand;
83
- isCommandOnPath?: typeof isCommandOnPath;
84
- confirm?: typeof confirm;
85
- pathValue?: () => string;
100
+ runOpenSpec?: RunOpenSpec;
101
+ locateOpenSpec?: () => OpenSpecLocation;
86
102
  wrapperBinPath?: () => string;
87
- getInstalledVersion?: (pathValue: string) => Promise<string | undefined>;
88
- fetchLatestVersion?: () => Promise<string | undefined>;
89
- isPinnedDeployment?: () => boolean;
90
- }
91
-
92
- async function defaultGetInstalledVersion(pathValue: string): Promise<string | undefined> {
93
- const bin = resolveCommandOnPath("openspec", pathValue);
94
- if (!bin) return undefined;
95
-
96
- try {
97
- const { stdout } = await execFileAsync(bin, ["--version"], { timeout: 10_000 });
98
- return stdout.match(/\d+\.\d+\.\d+/)?.[0];
99
- } catch {
100
- return undefined;
101
- }
102
- }
103
-
104
- async function defaultFetchLatestVersion(): Promise<string | undefined> {
105
- try {
106
- return await fetchPublicPackageVersion("@fission-ai/openspec");
107
- } catch {
108
- return undefined;
109
- }
110
- }
111
-
112
- export function deriveOpenSpecTools(activeProviders: string[]): OpenSpecTool[] {
113
- const active = new Set(activeProviders);
114
- return Object.entries(OPENSPEC_TOOLS_BY_RUNTIME).flatMap(([runtime, tools]) =>
115
- active.has(runtime) ? tools : [],
116
- );
117
103
  }
118
104
 
119
105
  function getClaudeSkillsDir(companionPath: string): string {
@@ -132,179 +118,241 @@ function getMateConventionsPath(companionPath: string): string {
132
118
  return path.join(companionPath, "openspec", "mate-conventions.yaml");
133
119
  }
134
120
 
135
- async function teardownClaudeSkillRoot(companionPath: string): Promise<void> {
121
+ async function teardownClaudeSkillRoot(
122
+ companionPath: string,
123
+ recorder?: ManagedOutputRecorder,
124
+ ): Promise<void> {
136
125
  const skillsDir = getClaudeSkillsDir(companionPath);
137
126
  for (const skill of OPENSPEC_SKILLS) {
138
127
  try {
139
- await fs.rm(path.join(skillsDir, skill), { recursive: true, force: true });
128
+ await removeRecorded(path.join(skillsDir, skill), "openspec", recorder);
140
129
  } catch {
141
130
  /* not present */
142
131
  }
143
132
  }
144
- await teardownMateSkills(skillsDir, companionPath);
133
+ await teardownMateSkills(skillsDir, companionPath, recorder);
145
134
  await pruneEmptyAncestors(skillsDir, companionPath);
146
135
  }
147
136
 
148
- async function teardownSharedSkillRoot(companionPath: string): Promise<void> {
149
- await removeSharedSkills(companionPath, [...MANAGED_SKILL_NAMES, OPENSPEC_AGENTS_MARKER]);
150
- }
151
-
152
- /** Converges earlier layouts and drops the `opencode` tool's duplicate workflow skills. */
153
- async function removeLegacyOpenCodeCopies(companionPath: string): Promise<void> {
154
- await removeLegacyOpenCodeSkills(companionPath, MANAGED_SKILL_NAMES);
155
- }
156
-
157
- async function applyMateOpenspecSkills(
137
+ async function teardownSharedSkillRoot(
158
138
  companionPath: string,
159
- activeProviders: string[],
139
+ recorder?: ManagedOutputRecorder,
160
140
  ): Promise<void> {
161
- if (activeProviders.includes("claude")) {
162
- await applyMateSkills(getClaudeSkillsDir(companionPath), "claude");
163
- }
164
- if (isSharedSkillRootActive(activeProviders)) {
165
- await applyMateSkills(getSharedSkillsDir(companionPath), "agents");
141
+ const skillsDir = getSharedSkillsDir(companionPath);
142
+ await teardownMateSkills(skillsDir, companionPath, recorder);
143
+ for (const name of [...OPENSPEC_SKILLS, OPENSPEC_AGENTS_MARKER]) {
144
+ await removeRecorded(path.join(skillsDir, name), "openspec", recorder);
166
145
  }
146
+ await pruneEmptyAncestors(skillsDir, companionPath);
167
147
  }
168
148
 
169
- async function reconcileOpenSpecTools(
170
- ctx: SetupContext,
171
- runCommand: OpenSpecRunCommand,
149
+ /** Converges the earlier layout that deployed Mate skills into `.opencode/skills`. */
150
+ async function removeLegacyOpenCodeCopies(
151
+ companionPath: string,
152
+ recorder?: ManagedOutputRecorder,
172
153
  ): Promise<void> {
173
- const tools = deriveOpenSpecTools(ctx.activeProviders);
174
- if (tools.length === 0) return;
175
- /** Best-effort: `config reset` is missing on older openspec releases. */
176
- try {
177
- await runCommand("openspec", ["config", "reset", "--all", "-y"], {
178
- cwd: ctx.companionPath,
179
- });
180
- } catch (error) {
181
- process.stderr.write(
182
- `openspec config reset skipped: ${error instanceof Error ? error.message : String(error)}\n`,
183
- );
184
- }
185
-
186
- await runCommand("openspec", ["init", "--tools", tools.join(","), "--force", ctx.companionPath], {
187
- cwd: ctx.companionPath,
188
- });
189
- await runCommand("openspec", ["update", "--force", ctx.companionPath], {
190
- cwd: ctx.companionPath,
191
- });
154
+ await teardownMateSkills(
155
+ path.join(companionPath, ".opencode", "skills"),
156
+ companionPath,
157
+ recorder,
158
+ );
159
+ }
192
160
 
193
- await removeLegacyOpenCodeCopies(ctx.companionPath);
194
- await applyMateOpenspecSkills(ctx.companionPath, ctx.activeProviders);
161
+ /** The `openspec-*` skills in `skillsDir` OpenSpec wrote: the known names, or `author: openspec`. */
162
+ async function generatedOpenSpecSkills(skillsDir: string): Promise<string[]> {
163
+ const names = (await fs.readdir(skillsDir).catch(() => [])).filter((name) =>
164
+ name.startsWith("openspec-"),
165
+ );
166
+ const generated = await Promise.all(
167
+ names.map(async (name) => {
168
+ const dir = path.join(skillsDir, name);
169
+ const known = (OPENSPEC_SKILLS as readonly string[]).includes(name);
170
+ return known || (await readSkillAuthor(dir)) === "openspec" ? [dir] : [];
171
+ }),
172
+ );
173
+ return generated.flat();
195
174
  }
196
175
 
197
- async function updateOpenSpecIfOutdated(
198
- ctx: SetupContext,
199
- deps: Required<
200
- Pick<
201
- OpenSpecPluginDeps,
202
- | "installCommand"
203
- | "pathValue"
204
- | "getInstalledVersion"
205
- | "fetchLatestVersion"
206
- | "isPinnedDeployment"
207
- >
208
- >,
176
+ /** Removes OpenSpec-generated output by name or authorship; user and third-party entries stay. */
177
+ async function removeGeneratedOpenSpecOutput(
178
+ companionPath: string,
179
+ recorder?: ManagedOutputRecorder,
209
180
  ): Promise<void> {
210
- /** Pinned artifacts change tool versions by replacement, never reconciliation. */
211
- if (deps.isPinnedDeployment()) return;
181
+ const skills = await Promise.all(
182
+ GENERATED_SKILL_ROOTS.map((root) =>
183
+ generatedOpenSpecSkills(path.join(companionPath, root, "skills")),
184
+ ),
185
+ );
186
+ const targets = [
187
+ ...skills.flat(),
188
+ path.join(getSharedSkillsDir(companionPath), OPENSPEC_AGENTS_MARKER),
189
+ ...OPENSPEC_COMMAND_IDS.flatMap((id) => [
190
+ path.join(companionPath, ".claude", "commands", "opsx", `${id}.md`),
191
+ path.join(companionPath, ".opencode", "commands", `opsx-${id}.md`),
192
+ ]),
193
+ ];
194
+ await Promise.all(targets.map((target) => removeRecorded(target, "openspec", recorder)));
195
+ // Sequential: the parent directories share ancestors.
196
+ for (const dir of new Set(targets.map((target) => path.dirname(target)))) {
197
+ // oxlint-disable-next-line no-await-in-loop
198
+ await pruneEmptyAncestors(dir, companionPath);
199
+ }
200
+ }
212
201
 
213
- const installed = await deps.getInstalledVersion(deps.pathValue());
214
- if (!installed) return;
202
+ async function applyMateOpenspecSkills(ctx: SetupContext): Promise<void> {
203
+ if (ctx.activeProviders.includes("claude")) {
204
+ await applyMateSkills(getClaudeSkillsDir(ctx.companionPath), "claude", ctx.managedOutput);
205
+ }
206
+ if (isSharedSkillRootActive(ctx.activeProviders)) {
207
+ await applyMateSkills(getSharedSkillsDir(ctx.companionPath), "agents", ctx.managedOutput);
208
+ }
209
+ }
215
210
 
216
- const latest = await deps.fetchLatestVersion();
217
- if (!latest || !isNewer(latest, installed)) return;
211
+ /**
212
+ * Initializes OpenSpec without tool output, so it writes only its `openspec/`
213
+ * scaffolding: Mate carries the workflow text in its own skills.
214
+ */
215
+ async function initializeOpenSpec(ctx: SetupContext, run: RunOpenSpec): Promise<void> {
216
+ const args = ["init", "--tools", "none", "--force", ctx.companionPath];
217
+ const result = await run(args, { cwd: ctx.companionPath });
218
+ if (result.code !== 0) {
219
+ // A missing bundled CLI answers its reinstall message as stderr.
220
+ const detail = result.stderr.trim() || result.stdout.trim();
221
+ throw new Error(`openspec ${args.join(" ")} exited with code ${result.code}: ${detail}`);
222
+ }
223
+ await removeGeneratedOpenSpecOutput(ctx.companionPath, ctx.managedOutput);
224
+ }
218
225
 
219
- process.stdout.write(
220
- `openspec: installed version ${installed} is outdated (latest ${latest}). Updating now:\n ${OPENSPEC_INSTALL_COMMAND}\n`,
226
+ function isMateSpecsRule(item: unknown): boolean {
227
+ return (
228
+ isScalar(item) &&
229
+ typeof item.value === "string" &&
230
+ item.value.startsWith(MATE_SPECS_RULE_MARKER)
221
231
  );
232
+ }
222
233
 
223
- try {
224
- await deps.installCommand("npm", ["install", "-g", "@fission-ai/openspec@latest"], {
225
- cwd: ctx.companionPath,
226
- });
227
- } catch {
228
- process.stderr.write("openspec: upgrade failed - continuing with the installed version\n");
229
- }
234
+ /** The config's `rules.specs` sequence, `null` when absent, or `undefined` when a shape is foreign. */
235
+ function readSpecsRules(doc: Document): YAMLSeq | null | undefined {
236
+ if (doc.contents !== null && !isMap(doc.contents)) return undefined;
237
+ const rules = doc.get("rules", true);
238
+ if (rules === undefined || rules === null) return null;
239
+ if (!isMap(rules)) return undefined;
240
+ const specs = rules.get("specs", true);
241
+ if (specs === undefined || specs === null) return null;
242
+ return isSeq(specs) ? specs : undefined;
243
+ }
244
+
245
+ /**
246
+ * Removes Mate's `rules.specs` entry and any `rules.specs` list or `rules` map
247
+ * that leaves empty. Returns whether the document changed.
248
+ */
249
+ function removeMateSpecsRule(doc: Document): boolean {
250
+ const specs = readSpecsRules(doc);
251
+ if (!specs) return false;
252
+ const before = specs.items.length;
253
+ specs.items = specs.items.filter((item) => !isMateSpecsRule(item));
254
+ if (specs.items.length === before) return false;
255
+ if (specs.items.length === 0) doc.deleteIn(["rules", "specs"]);
256
+ const rules = doc.get("rules", true);
257
+ if (isMap(rules) && rules.items.length === 0) doc.delete("rules");
258
+ return true;
230
259
  }
231
260
 
232
- async function ensureOpenSpecInstalled(
233
- ctx: SetupContext,
234
- deps: Required<
235
- Pick<
236
- OpenSpecPluginDeps,
237
- | "installCommand"
238
- | "isCommandOnPath"
239
- | "confirm"
240
- | "pathValue"
241
- | "getInstalledVersion"
242
- | "fetchLatestVersion"
243
- | "isPinnedDeployment"
244
- >
245
- >,
246
- ): Promise<boolean> {
247
- if (deps.isCommandOnPath("openspec", deps.pathValue())) {
248
- await updateOpenSpecIfOutdated(ctx, deps);
261
+ /**
262
+ * Ensures exactly one current Mate entry in `rules.specs`: appended when
263
+ * missing, replaced in place when outdated. Returns whether the document changed.
264
+ */
265
+ function upsertMateSpecsRule(doc: Document): boolean {
266
+ if (doc.contents === null) doc.contents = doc.createNode({}) as YAMLMap;
267
+ const specs = readSpecsRules(doc);
268
+ if (specs === null) {
269
+ doc.setIn(["rules", "specs"], doc.createNode([MATE_SPECS_RULE]));
249
270
  return true;
250
271
  }
272
+ if (!specs) return false;
251
273
 
252
- if (ctx.mode !== "setup") {
253
- process.stderr.write(
254
- `openspec binary not found. Install it manually with \`${OPENSPEC_INSTALL_COMMAND}\`\n`,
255
- );
256
- return false;
274
+ const index = specs.items.findIndex(isMateSpecsRule);
275
+ if (index === -1) {
276
+ specs.add(doc.createNode(MATE_SPECS_RULE));
277
+ return true;
257
278
  }
258
-
259
- if (!deps.isCommandOnPath("npm", deps.pathValue())) {
260
- process.stderr.write(
261
- `openspec binary not found and npm is not available on PATH. Install it manually with \`${OPENSPEC_INSTALL_COMMAND}\`\n`,
262
- );
263
- return false;
279
+ let changed = false;
280
+ const current = specs.items[index];
281
+ if (isScalar(current) && current.value !== MATE_SPECS_RULE) {
282
+ current.value = MATE_SPECS_RULE;
283
+ changed = true;
264
284
  }
265
-
266
- process.stdout.write(`openspec binary not found. To install:\n ${OPENSPEC_INSTALL_COMMAND}\n`);
267
- const ok = await deps.confirm("Run this install command now?");
268
- if (!ok) {
269
- process.stderr.write(
270
- `openspec: install skipped - install manually with \`${OPENSPEC_INSTALL_COMMAND}\`\n`,
271
- );
272
- return false;
285
+ const deduplicated = specs.items.filter((item, i) => i === index || !isMateSpecsRule(item));
286
+ if (deduplicated.length !== specs.items.length) {
287
+ specs.items = deduplicated;
288
+ changed = true;
273
289
  }
290
+ return changed;
291
+ }
274
292
 
293
+ /**
294
+ * Removes Mate's own config content: its `rules.specs` entry always, and the
295
+ * file only when what remains is exactly what Mate writes. User content stays.
296
+ */
297
+ async function removeManagedOpenSpecConfig(companionPath: string): Promise<void> {
298
+ const configPath = getOpenSpecConfigPath(companionPath);
299
+ let content: string;
275
300
  try {
276
- await deps.installCommand("npm", ["install", "-g", "@fission-ai/openspec@latest"], {
277
- cwd: ctx.companionPath,
278
- });
301
+ content = await fs.readFile(configPath, "utf8");
279
302
  } catch {
280
- process.stderr.write(
281
- `openspec: install failed - install manually with \`${OPENSPEC_INSTALL_COMMAND}\`\n`,
282
- );
283
- return false;
303
+ return;
284
304
  }
285
305
 
286
- if (!deps.isCommandOnPath("openspec", deps.pathValue())) {
287
- process.stderr.write(
288
- "openspec install completed but the binary is still not available on PATH\n",
289
- );
290
- return false;
306
+ let remaining = content;
307
+ if (content.trim() !== MANAGED_CONFIG_SCHEMA_ONLY) {
308
+ const doc = parseDocument(content);
309
+ if (doc.errors.length > 0 || !removeMateSpecsRule(doc)) return;
310
+ remaining = doc.toString(CONFIG_TO_STRING);
291
311
  }
292
312
 
293
- return true;
313
+ if (remaining.trim() === MANAGED_CONFIG_SCHEMA_ONLY) {
314
+ await fs.rm(configPath, { force: true });
315
+ await pruneEmptyAncestors(path.dirname(configPath), companionPath);
316
+ } else {
317
+ await fs.writeFile(configPath, remaining, "utf8");
318
+ }
294
319
  }
295
320
 
296
- /** Only the config Mate itself wrote is removed; a user-edited config.yaml is left alone. */
297
- async function removeManagedOpenSpecConfig(companionPath: string): Promise<void> {
321
+ /** Writes a fresh Mate config, or merges Mate's entry into an existing one; writes only on change. */
322
+ async function ensureManagedOpenSpecConfig(companionPath: string): Promise<void> {
298
323
  const configPath = getOpenSpecConfigPath(companionPath);
324
+ let content: string | undefined;
299
325
  try {
300
- const content = await fs.readFile(configPath, "utf8");
301
- if (content.trim() === "schema: mate-v1") {
302
- await fs.rm(configPath, { force: true });
303
- await pruneEmptyAncestors(path.dirname(configPath), companionPath);
304
- }
326
+ content = await fs.readFile(configPath, "utf8");
305
327
  } catch {
306
- /* no managed config present */
328
+ /* no config yet */
307
329
  }
330
+
331
+ if (content === undefined) {
332
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
333
+ const fresh = new Document({ schema: "mate-v1", rules: { specs: [MATE_SPECS_RULE] } });
334
+ await fs.writeFile(configPath, fresh.toString(CONFIG_TO_STRING), "utf8");
335
+ return;
336
+ }
337
+
338
+ // config.yaml is user-editable once created; only Mate's entry and a default `schema` are touched.
339
+ const doc = parseDocument(content);
340
+ if (doc.errors.length > 0) {
341
+ process.stderr.write(
342
+ `openspec: left openspec/config.yaml unchanged, it is not valid YAML: ${doc.errors[0].message.split("\n")[0]}\n`,
343
+ );
344
+ return;
345
+ }
346
+ const ruleChanged = upsertMateSpecsRule(doc);
347
+ // Mate's `mate-v1` replaces a missing schema or the one `openspec init` writes; any other is a user choice.
348
+ const current = isMap(doc.contents) ? doc.get("schema") : undefined;
349
+ const schemaChanged =
350
+ isMap(doc.contents) &&
351
+ (current === undefined || current === null || current === OPENSPEC_INIT_SCHEMA);
352
+ if (schemaChanged) doc.set("schema", "mate-v1");
353
+ if (!schemaChanged && !ruleChanged) return;
354
+ const merged = doc.toString(CONFIG_TO_STRING);
355
+ if (merged !== content) await fs.writeFile(configPath, merged, "utf8");
308
356
  }
309
357
 
310
358
  async function teardownOpenSpecSchema(companionPath: string): Promise<void> {
@@ -333,37 +381,14 @@ async function reconcileOpenSpecSchema(ctx: SetupContext): Promise<void> {
333
381
  );
334
382
  await fs.copyFile(MATE_CONVENTIONS_SOURCE, getMateConventionsPath(ctx.companionPath));
335
383
 
336
- // Availability is unconditional so any change may select a Mate schema;
337
- // activation stays opt-in through the companion-level setup selection.
338
- if (getOpenSpecSchemaSelection(ctx.config.capabilities ?? []) !== "mate-v1") {
339
- await removeManagedOpenSpecConfig(ctx.companionPath);
340
- return;
341
- }
342
-
343
- const configPath = getOpenSpecConfigPath(ctx.companionPath);
344
- await fs.mkdir(path.dirname(configPath), { recursive: true });
345
-
346
- // config.yaml is user-editable once created; never overwrite it on re-sync.
347
- const configExists = await fs
348
- .access(configPath)
349
- .then(() => true)
350
- .catch(() => false);
351
- if (configExists) return;
352
-
353
- await fs.writeFile(configPath, "schema: mate-v1\n", "utf8");
384
+ await ensureManagedOpenSpecConfig(ctx.companionPath);
354
385
  }
355
386
 
356
387
  export function createOpenspecPlugin(deps: OpenSpecPluginDeps = {}): CapabilityPlugin {
357
388
  const depsWithDefaults = {
358
- runCommand: deps.runCommand ?? runCommandSilently,
359
- installCommand: deps.installCommand ?? runCommand,
360
- isCommandOnPath: deps.isCommandOnPath ?? isCommandOnPath,
361
- confirm: deps.confirm ?? confirm,
362
- pathValue: deps.pathValue ?? (() => process.env.PATH ?? ""),
389
+ runOpenSpec: deps.runOpenSpec ?? runOpenSpec,
390
+ locateOpenSpec: deps.locateOpenSpec ?? locateOpenSpec,
363
391
  wrapperBinPath: deps.wrapperBinPath ?? getWrapperBinPath,
364
- getInstalledVersion: deps.getInstalledVersion ?? defaultGetInstalledVersion,
365
- fetchLatestVersion: deps.fetchLatestVersion ?? defaultFetchLatestVersion,
366
- isPinnedDeployment: deps.isPinnedDeployment ?? (() => isPinnedDeployment()),
367
392
  };
368
393
 
369
394
  return {
@@ -381,19 +406,9 @@ export function createOpenspecPlugin(deps: OpenSpecPluginDeps = {}): CapabilityP
381
406
  return {
382
407
  claude: {
383
408
  permissionEntries: [
384
- "Skill(openspec-explore)",
385
- "Skill(openspec-propose)",
386
- "Skill(openspec-apply-change)",
387
- "Skill(openspec-archive-change)",
388
- "Skill(mate-artifact-publish)",
389
- "Skill(mate-openspec-backfill)",
390
- "Skill(mate-interview-me)",
391
- "Skill(mate-grill-me)",
392
- "Skill(mate-grilling)",
393
- "Skill(mate-grill-with-docs)",
394
- "Skill(mate-domain-modeling)",
395
- "Skill(mate-show-me)",
396
- "Skill(mate-simplify-code)",
409
+ ...MATE_SKILLS.filter((skill) => !UNPERMITTED_MATE_SKILLS.has(skill)).map(
410
+ (skill) => `Skill(${skill})`,
411
+ ),
397
412
  "Bash(openspec:*)",
398
413
  `Bash(${FRAMEWORK_NAME} cap graphify:*)`,
399
414
  `Bash(${path.join(depsWithDefaults.wrapperBinPath(), "openspec")}:*)`,
@@ -403,54 +418,42 @@ export function createOpenspecPlugin(deps: OpenSpecPluginDeps = {}): CapabilityP
403
418
  },
404
419
  gitignoreEntries: () => OPENSPEC_GITIGNORE_ENTRIES,
405
420
  persistGitignoreEntries: true,
406
- getInstallRequirements: (): InstallRequirement[] => [
407
- {
408
- id: "capability:openspec",
409
- label: "OpenSpec CLI",
410
- group: "companion",
411
- source: "OpenSpec capability",
412
- command: OPENSPEC_INSTALL_COMMAND,
413
- fingerprint: `openspec:${OPENSPEC_INSTALL_COMMAND}`,
414
- detect: () => depsWithDefaults.isCommandOnPath("openspec", depsWithDefaults.pathValue()),
415
- install: () =>
416
- depsWithDefaults.installCommand("npm", ["install", "-g", "@fission-ai/openspec@latest"]),
417
- verify: () => depsWithDefaults.isCommandOnPath("openspec", depsWithDefaults.pathValue()),
418
- },
419
- ],
420
421
  async apply(ctx) {
421
- const tools = deriveOpenSpecTools(ctx.activeProviders);
422
- if (ctx.mode === "setup" || tools.length > 0) {
423
- if (await ensureOpenSpecInstalled(ctx, depsWithDefaults)) {
424
- if (tools.length > 0) {
425
- await reconcileOpenSpecTools(ctx, depsWithDefaults.runCommand);
426
- }
427
- }
422
+ // OpenSpec output is committed in the companion, so only setup and
423
+ // `mate companion update` touch it; a launch must leave it alone.
424
+ if (ctx.mode === "setup" || ctx.regenerateOpenSpec === true) {
425
+ await initializeOpenSpec(ctx, depsWithDefaults.runOpenSpec);
426
+ } else {
427
+ const location = depsWithDefaults.locateOpenSpec();
428
+ if ("missing" in location) process.stderr.write(`openspec: ${location.missing}\n`);
428
429
  }
429
- await removeLegacyOpenCodeCopies(ctx.companionPath);
430
+ await applyMateOpenspecSkills(ctx);
431
+ await removeLegacyOpenCodeCopies(ctx.companionPath, ctx.managedOutput);
430
432
  await reconcileOpenSpecSchema(ctx);
431
433
  },
432
434
  async teardown(ctx) {
433
- await teardownClaudeSkillRoot(ctx.companionPath);
434
- await teardownSharedSkillRoot(ctx.companionPath);
435
- await removeLegacyOpenCodeCopies(ctx.companionPath);
435
+ await teardownClaudeSkillRoot(ctx.companionPath, ctx.managedOutput);
436
+ await teardownSharedSkillRoot(ctx.companionPath, ctx.managedOutput);
437
+ await removeLegacyOpenCodeCopies(ctx.companionPath, ctx.managedOutput);
438
+ await removeGeneratedOpenSpecOutput(ctx.companionPath, ctx.managedOutput);
436
439
  await teardownOpenSpecSchema(ctx.companionPath);
437
440
  },
438
441
  forProvider: {
439
442
  claude: {
440
- // Publication is explicit (`mate-artifact-publish`), so openspec
443
+ // Publication is explicit (`mate-publish-artifact`), so openspec
441
444
  // contributes no archive-triggered Claude hook and no hook state.
442
445
  async apply() {},
443
446
  async teardown(ctx: SetupContext) {
444
- await teardownClaudeSkillRoot(ctx.companionPath);
447
+ await teardownClaudeSkillRoot(ctx.companionPath, ctx.managedOutput);
445
448
  },
446
449
  },
447
450
  opencode: {
448
451
  async apply() {},
449
452
  async teardown(ctx: SetupContext) {
450
453
  if (!isSharedSkillRootActive(ctx.activeProviders)) {
451
- await teardownSharedSkillRoot(ctx.companionPath);
454
+ await teardownSharedSkillRoot(ctx.companionPath, ctx.managedOutput);
452
455
  }
453
- await removeLegacyOpenCodeCopies(ctx.companionPath);
456
+ await removeLegacyOpenCodeCopies(ctx.companionPath, ctx.managedOutput);
454
457
  },
455
458
  },
456
459
  },