@uniqbit/mate-core 0.18.5 → 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 (209) hide show
  1. package/claude-plugin/hooks/companion-instructions.mjs +9 -0
  2. package/claude-plugin/hooks/hooks.json +12 -2
  3. package/package.json +10 -2
  4. package/src/brand/art.generated.ts +69 -0
  5. package/src/brand/index.ts +12 -0
  6. package/src/brand/palette.ts +19 -0
  7. package/src/brand/svg.generated.ts +8 -0
  8. package/src/brand/terminal.ts +83 -0
  9. package/src/brand/types.ts +33 -0
  10. package/src/cli/commands/artifact/artifact.ts +8 -0
  11. package/src/cli/commands/artifact/delivery/archive.ts +117 -0
  12. package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
  13. package/src/cli/commands/artifact/delivery/environment.ts +104 -0
  14. package/src/cli/commands/artifact/delivery/link.ts +154 -0
  15. package/src/cli/commands/artifact/delivery/output.ts +58 -0
  16. package/src/cli/commands/artifact/finish/command.ts +33 -3
  17. package/src/cli/commands/artifact/finish/engine.ts +47 -3
  18. package/src/cli/commands/artifact/finish/finisher.ts +10 -0
  19. package/src/cli/commands/artifact/finish/openspec.ts +177 -138
  20. package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
  21. package/src/cli/commands/artifact/pending/command.ts +31 -3
  22. package/src/cli/commands/artifact/pending/discovery.ts +21 -57
  23. package/src/cli/commands/cap/graphify.ts +52 -3
  24. package/src/cli/commands/cap/index-cmd.ts +8 -1
  25. package/src/cli/commands/cap/openspec.ts +13 -25
  26. package/src/cli/commands/companion/open.ts +56 -39
  27. package/src/cli/commands/companion/update.ts +75 -42
  28. package/src/cli/commands/doctor.ts +176 -3
  29. package/src/cli/commands/plugin/install.ts +9 -4
  30. package/src/cli/commands/plugin/plugin.ts +3 -2
  31. package/src/cli/commands/report/adapter.ts +11 -2
  32. package/src/cli/commands/report/contract.ts +21 -0
  33. package/src/cli/commands/report/highlight.ts +34 -4
  34. package/src/cli/commands/report/markdown.ts +110 -0
  35. package/src/cli/commands/report/palette.ts +118 -0
  36. package/src/cli/commands/report/renderer.ts +456 -153
  37. package/src/cli/commands/report/types.ts +16 -0
  38. package/src/cli/commands/setup.ts +7 -15
  39. package/src/cli/commands/studio/client-asset.ts +16 -0
  40. package/src/cli/commands/studio/openspec-cli.ts +10 -58
  41. package/src/cli/commands/studio/routes.ts +2 -0
  42. package/src/cli/commands/studio/selection.ts +19 -1
  43. package/src/cli/commands/studio/server.ts +42 -1
  44. package/src/cli/commands/studio/views/client.ts +6 -616
  45. package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
  46. package/src/cli/commands/studio/views/document.tsx +137 -69
  47. package/src/cli/commands/studio/views/markdown.tsx +250 -0
  48. package/src/cli/commands/studio/views/palette.ts +134 -0
  49. package/src/cli/commands/studio/views/reports/index.tsx +2 -7
  50. package/src/cli/commands/studio/views/skills/index.tsx +43 -62
  51. package/src/cli/commands/studio/views/specs/index.tsx +7 -15
  52. package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
  53. package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
  54. package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
  55. package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
  56. package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
  57. package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
  58. package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
  59. package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
  60. package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
  61. package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
  62. package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
  63. package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
  64. package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
  65. package/src/cli/commands/studio/views/styles.ts +217 -206
  66. package/src/cli/commands/studio/views/vault/state.ts +3 -5
  67. package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
  68. package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
  69. package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
  70. package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
  71. package/src/cli/commands/wrap.ts +5 -13
  72. package/src/cli/main.ts +4 -1
  73. package/src/cli/parse-flags.ts +25 -7
  74. package/src/cli/setup-selector.tsx +38 -162
  75. package/src/cli/usage.ts +4 -0
  76. package/src/hooks/companion-instructions.ts +190 -0
  77. package/src/hooks/hook-io.ts +63 -0
  78. package/src/hooks/session-banner.ts +21 -29
  79. package/src/hooks/session-guidance.ts +9 -17
  80. package/src/hooks/session-prepare.ts +115 -28
  81. package/src/hooks/validate-artifact-path.ts +82 -24
  82. package/src/index.ts +4 -0
  83. package/src/lib/change-layout.ts +154 -0
  84. package/src/lib/components/wizard-header.tsx +3 -1
  85. package/src/lib/context-mode-package.ts +24 -11
  86. package/src/lib/delivery/archive-change.ts +154 -0
  87. package/src/lib/delivery/branches.ts +108 -0
  88. package/src/lib/delivery/environment.ts +29 -0
  89. package/src/lib/delivery/forge.ts +279 -0
  90. package/src/lib/delivery/overlap.ts +202 -0
  91. package/src/lib/delivery/record-delivery.ts +196 -0
  92. package/src/lib/delivery/record.ts +343 -0
  93. package/src/lib/delivery/refresh.ts +174 -0
  94. package/src/lib/delivery/repository-access.ts +97 -0
  95. package/src/lib/delivery/stages.ts +111 -0
  96. package/src/lib/delivery/token.ts +58 -0
  97. package/src/lib/frontmatter.ts +18 -0
  98. package/src/lib/opencode-plugin-package.ts +5 -5
  99. package/src/lib/openspec-cli.ts +116 -0
  100. package/src/lib/openspec-scopes.ts +86 -0
  101. package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
  102. package/src/lib/orchestrator/adapters/claude.ts +48 -40
  103. package/src/lib/orchestrator/adapters/opencode.ts +2 -0
  104. package/src/lib/orchestrator/editor.ts +9 -9
  105. package/src/lib/orchestrator/openspec-store.ts +14 -54
  106. package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
  107. package/src/lib/orchestrator/projection-claude-links.ts +159 -0
  108. package/src/lib/orchestrator/projection-claude-skills.ts +19 -137
  109. package/src/lib/orchestrator/projection-entries.ts +45 -14
  110. package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
  111. package/src/lib/orchestrator/projection-types.ts +3 -0
  112. package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
  113. package/src/lib/orchestrator/session-prepare.ts +45 -5
  114. package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
  115. package/src/lib/orchestrator/types.ts +2 -2
  116. package/src/lib/orchestrator/wrap-pass.ts +22 -1
  117. package/src/lib/package-paths.ts +8 -0
  118. package/src/opencode/companion-hooks.ts +64 -5
  119. package/src/opencode/companion-policy.ts +37 -18
  120. package/src/opencode/projected-guidance.ts +4 -1
  121. package/src/opencode/session-prepare.ts +2 -1
  122. package/src/opencode/tui.tsx +4 -2
  123. package/src/runtime/artifact-refusal.ts +63 -0
  124. package/src/runtime/claude-delivery-plan.ts +89 -7
  125. package/src/runtime/commit-attribution-guard.ts +181 -0
  126. package/src/runtime/companion-guidance.ts +15 -3
  127. package/src/runtime/companion-instructions.ts +340 -0
  128. package/src/runtime/error-message.ts +4 -0
  129. package/src/runtime/index.ts +5 -0
  130. package/src/runtime/managed-block.ts +117 -0
  131. package/src/runtime/markdown-scan.ts +266 -0
  132. package/src/runtime/policy.ts +15 -1
  133. package/src/runtime/projected-flag.ts +7 -0
  134. package/src/runtime/projected-guidance.ts +1 -0
  135. package/src/runtime/runtime-document-manifest.ts +62 -11
  136. package/src/runtime/secret-guard.ts +148 -0
  137. package/src/runtime/secret-patterns.ts +164 -0
  138. package/src/runtime/secret-scan.ts +90 -0
  139. package/src/runtime/session-kind.ts +13 -0
  140. package/src/runtime/shell-heredoc.ts +81 -0
  141. package/src/runtime/wrap-state.ts +36 -2
  142. package/src/runtime/write-atomic.ts +64 -0
  143. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
  144. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
  145. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
  146. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  147. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
  148. package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
  149. package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
  150. package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
  151. package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
  152. package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
  153. package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
  154. package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
  155. package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
  156. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
  157. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
  158. package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
  159. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
  160. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
  161. package/src/templates/mate-skills/{claude/mate-artifact-publish → agents/mate-publish-artifact}/SKILL.md +91 -23
  162. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  163. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
  164. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
  165. package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
  166. package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
  167. package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
  168. package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
  169. package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
  170. package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
  171. package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
  172. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
  173. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
  174. package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
  175. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
  176. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
  177. package/src/templates/mate-skills/{agents/mate-artifact-publish → claude/mate-publish-artifact}/SKILL.md +91 -23
  178. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  179. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
  180. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
  181. package/src/templates/mate-skills/openspec-references.ts +140 -0
  182. package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
  183. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
  184. package/src/tools/setup/capabilities/context-mode.ts +25 -5
  185. package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
  186. package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
  187. package/src/tools/setup/capabilities/graphify.ts +12 -0
  188. package/src/tools/setup/capabilities/openspec.ts +151 -270
  189. package/src/tools/setup/capabilities/tokensave.ts +36 -3
  190. package/src/tools/setup/context-services.ts +9 -17
  191. package/src/tools/setup/dynamic-plugins/install.ts +24 -28
  192. package/src/tools/setup/engine.ts +97 -21
  193. package/src/tools/setup/managed-output.ts +51 -0
  194. package/src/tools/setup/mate.ts +86 -11
  195. package/src/tools/setup/plugin.ts +35 -0
  196. package/src/tools/setup/plugins/guidance.ts +7 -4
  197. package/src/tools/setup/providers/agent-file-sections.ts +4 -2
  198. package/src/tools/setup/providers/claude-attribution.ts +65 -0
  199. package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
  200. package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
  201. package/src/tools/setup/providers/claude.ts +284 -8
  202. package/src/tools/setup/providers/opencode.ts +180 -1
  203. package/src/tools/setup/utils.ts +14 -4
  204. package/src/tools/setup.ts +126 -10
  205. package/wrappers/bin/graphify +52 -4
  206. package/wrappers/bin/openspec +33 -9
  207. package/wrappers/lib/graphify-gate.mjs +8 -0
  208. package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
  209. package/src/tools/setup/runtime-documents.ts +0 -291
@@ -1,37 +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
4
  import { Document, isMap, isScalar, isSeq, parseDocument, type YAMLMap, type YAMLSeq } from "yaml";
7
5
 
8
- import { getOpenSpecSchemaSelection } from "../../../lib/orchestrator/setup-compatibilities";
6
+ import {
7
+ locateOpenSpec,
8
+ runOpenSpec,
9
+ type OpenSpecLocation,
10
+ type RunOpenSpec,
11
+ } from "../../../lib/openspec-cli";
9
12
  import { getWrapperBinPath } from "../../../lib/package-paths";
10
- import { fetchPublicPackageVersion } from "../../../lib/public-npm";
11
- import { isNewer, isPinnedDeployment } from "../../../lib/update-checker";
12
13
  import { FRAMEWORK_NAME } from "../../../framework";
13
- import { confirm } from "../../../cli/confirm";
14
- import type { CapabilityPlugin, RuntimeContributionsByRuntime, SetupContext } from "../plugin";
15
- import type { InstallRequirement } from "../install-contract";
16
- import {
17
- isCommandOnPath,
18
- mergeDir,
19
- pruneEmptyAncestors,
20
- resolveCommandOnPath,
21
- runCommand,
22
- runCommandSilently,
23
- } from "../utils";
24
- import { applyMateSkills, MANAGED_MATE_SKILL_NAMES, teardownMateSkills } from "../mate";
25
- import {
26
- getSharedSkillsDir,
27
- isSharedSkillRootActive,
28
- removeLegacyOpenCodeSkills,
29
- removeSharedSkills,
30
- } from "../providers/agents-skill-root";
31
-
32
- export { MATE_ARTIFACT_SKILLS, MATE_SKILLS } from "../mate";
33
-
34
- 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";
35
26
 
36
27
  export const OPENSPEC_SKILLS = [
37
28
  "openspec-explore",
@@ -42,11 +33,30 @@ export const OPENSPEC_SKILLS = [
42
33
  "openspec-update-change",
43
34
  ] as const;
44
35
 
36
+ /** Mate skills without a pre-seeded `Skill(...)` permission. */
37
+ const UNPERMITTED_MATE_SKILLS: ReadonlySet<string> = new Set(["mate-create-report"]);
38
+
45
39
  /** Written by the OpenSpec `agents` tool beside its skills in the shared root. */
46
40
  const OPENSPEC_AGENTS_MARKER = ".openspec-target";
47
41
 
48
- /** Skill names the capability owns in any skill root. */
49
- 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;
50
60
 
51
61
  /** Every Mate-owned schema is installed for availability; only one is ever activated as the companion default. */
52
62
  const MATE_SCHEMA_NAMES = ["mate-v1", "mate-minimal"] as const;
@@ -68,25 +78,17 @@ export const MATE_SPECS_RULE_MARKER = "[mate] ";
68
78
  * sync workflows read when writing main specs. Propose reads it for delta specs
69
79
  * too, so the text binds main specs only.
70
80
  */
71
- 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 delta frontmatter. Full rules: openspec/mate-conventions.yaml.`;
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.`;
72
82
 
73
83
  /** What Mate writes besides its entry; anything beyond the two is user content. */
74
84
  const MANAGED_CONFIG_SCHEMA_ONLY = "schema: mate-v1";
75
85
 
86
+ /** The schema a fresh `openspec init` writes into config.yaml. */
87
+ const OPENSPEC_INIT_SCHEMA = "spec-driven";
88
+
76
89
  /** Unfolded output keeps the Mate entry on one line and user scalars unwrapped. */
77
90
  const CONFIG_TO_STRING = { lineWidth: 0 } as const;
78
91
 
79
- /**
80
- * Runtime → OpenSpec CLI tools. OpenCode reads workflow skills from the shared
81
- * `agents` root; its own tool only contributes the `/opsx-*` commands.
82
- */
83
- const OPENSPEC_TOOLS_BY_RUNTIME = {
84
- claude: ["claude"],
85
- opencode: ["agents", "opencode"],
86
- } as const;
87
-
88
- const OPENSPEC_INSTALL_COMMAND = "npm install -g @fission-ai/openspec@latest";
89
-
90
92
  /**
91
93
  * Store metadata `openspec store register` writes beside the companion. Its id
92
94
  * is machine-local, and companion Git sync would otherwise stash it on every
@@ -94,47 +96,10 @@ const OPENSPEC_INSTALL_COMMAND = "npm install -g @fission-ai/openspec@latest";
94
96
  */
95
97
  const OPENSPEC_GITIGNORE_ENTRIES = [".openspec-store/"];
96
98
 
97
- type OpenSpecTool =
98
- (typeof OPENSPEC_TOOLS_BY_RUNTIME)[keyof typeof OPENSPEC_TOOLS_BY_RUNTIME][number];
99
- type OpenSpecRunCommand = typeof runCommandSilently;
100
-
101
99
  export interface OpenSpecPluginDeps {
102
- runCommand?: OpenSpecRunCommand;
103
- installCommand?: typeof runCommand;
104
- isCommandOnPath?: typeof isCommandOnPath;
105
- confirm?: typeof confirm;
106
- pathValue?: () => string;
100
+ runOpenSpec?: RunOpenSpec;
101
+ locateOpenSpec?: () => OpenSpecLocation;
107
102
  wrapperBinPath?: () => string;
108
- getInstalledVersion?: (pathValue: string) => Promise<string | undefined>;
109
- fetchLatestVersion?: () => Promise<string | undefined>;
110
- isPinnedDeployment?: () => boolean;
111
- }
112
-
113
- async function defaultGetInstalledVersion(pathValue: string): Promise<string | undefined> {
114
- const bin = resolveCommandOnPath("openspec", pathValue);
115
- if (!bin) return undefined;
116
-
117
- try {
118
- const { stdout } = await execFileAsync(bin, ["--version"], { timeout: 10_000 });
119
- return stdout.match(/\d+\.\d+\.\d+/)?.[0];
120
- } catch {
121
- return undefined;
122
- }
123
- }
124
-
125
- async function defaultFetchLatestVersion(): Promise<string | undefined> {
126
- try {
127
- return await fetchPublicPackageVersion("@fission-ai/openspec");
128
- } catch {
129
- return undefined;
130
- }
131
- }
132
-
133
- export function deriveOpenSpecTools(activeProviders: string[]): OpenSpecTool[] {
134
- const active = new Set(activeProviders);
135
- return Object.entries(OPENSPEC_TOOLS_BY_RUNTIME).flatMap(([runtime, tools]) =>
136
- active.has(runtime) ? tools : [],
137
- );
138
103
  }
139
104
 
140
105
  function getClaudeSkillsDir(companionPath: string): string {
@@ -153,165 +118,109 @@ function getMateConventionsPath(companionPath: string): string {
153
118
  return path.join(companionPath, "openspec", "mate-conventions.yaml");
154
119
  }
155
120
 
156
- async function teardownClaudeSkillRoot(companionPath: string): Promise<void> {
121
+ async function teardownClaudeSkillRoot(
122
+ companionPath: string,
123
+ recorder?: ManagedOutputRecorder,
124
+ ): Promise<void> {
157
125
  const skillsDir = getClaudeSkillsDir(companionPath);
158
126
  for (const skill of OPENSPEC_SKILLS) {
159
127
  try {
160
- await fs.rm(path.join(skillsDir, skill), { recursive: true, force: true });
128
+ await removeRecorded(path.join(skillsDir, skill), "openspec", recorder);
161
129
  } catch {
162
130
  /* not present */
163
131
  }
164
132
  }
165
- await teardownMateSkills(skillsDir, companionPath);
133
+ await teardownMateSkills(skillsDir, companionPath, recorder);
166
134
  await pruneEmptyAncestors(skillsDir, companionPath);
167
135
  }
168
136
 
169
- async function teardownSharedSkillRoot(companionPath: string): Promise<void> {
170
- await removeSharedSkills(companionPath, [...MANAGED_SKILL_NAMES, OPENSPEC_AGENTS_MARKER]);
171
- }
172
-
173
- /** Converges earlier layouts and drops the `opencode` tool's duplicate workflow skills. */
174
- async function removeLegacyOpenCodeCopies(companionPath: string): Promise<void> {
175
- await removeLegacyOpenCodeSkills(companionPath, MANAGED_SKILL_NAMES);
176
- }
177
-
178
- async function applyMateOpenspecSkills(
137
+ async function teardownSharedSkillRoot(
179
138
  companionPath: string,
180
- activeProviders: string[],
139
+ recorder?: ManagedOutputRecorder,
181
140
  ): Promise<void> {
182
- if (activeProviders.includes("claude")) {
183
- await applyMateSkills(getClaudeSkillsDir(companionPath), "claude");
184
- }
185
- if (isSharedSkillRootActive(activeProviders)) {
186
- 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);
187
145
  }
146
+ await pruneEmptyAncestors(skillsDir, companionPath);
188
147
  }
189
148
 
190
- async function reconcileOpenSpecTools(
191
- ctx: SetupContext,
192
- runCommand: OpenSpecRunCommand,
149
+ /** Converges the earlier layout that deployed Mate skills into `.opencode/skills`. */
150
+ async function removeLegacyOpenCodeCopies(
151
+ companionPath: string,
152
+ recorder?: ManagedOutputRecorder,
193
153
  ): Promise<void> {
194
- const tools = deriveOpenSpecTools(ctx.activeProviders);
195
- if (tools.length === 0) return;
196
- /** Best-effort: `config reset` is missing on older openspec releases. */
197
- try {
198
- await runCommand("openspec", ["config", "reset", "--all", "-y"], {
199
- cwd: ctx.companionPath,
200
- });
201
- } catch (error) {
202
- process.stderr.write(
203
- `openspec config reset skipped: ${error instanceof Error ? error.message : String(error)}\n`,
204
- );
205
- }
206
-
207
- await runCommand("openspec", ["init", "--tools", tools.join(","), "--force", ctx.companionPath], {
208
- cwd: ctx.companionPath,
209
- });
210
- await runCommand("openspec", ["update", "--force", ctx.companionPath], {
211
- cwd: ctx.companionPath,
212
- });
154
+ await teardownMateSkills(
155
+ path.join(companionPath, ".opencode", "skills"),
156
+ companionPath,
157
+ recorder,
158
+ );
159
+ }
213
160
 
214
- await removeLegacyOpenCodeCopies(ctx.companionPath);
215
- 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();
216
174
  }
217
175
 
218
- async function updateOpenSpecIfOutdated(
219
- ctx: SetupContext,
220
- deps: Required<
221
- Pick<
222
- OpenSpecPluginDeps,
223
- | "installCommand"
224
- | "pathValue"
225
- | "getInstalledVersion"
226
- | "fetchLatestVersion"
227
- | "isPinnedDeployment"
228
- >
229
- >,
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,
230
180
  ): Promise<void> {
231
- /** Pinned artifacts change tool versions by replacement, never reconciliation. */
232
- if (deps.isPinnedDeployment()) return;
233
-
234
- const installed = await deps.getInstalledVersion(deps.pathValue());
235
- if (!installed) return;
236
-
237
- const latest = await deps.fetchLatestVersion();
238
- if (!latest || !isNewer(latest, installed)) return;
239
-
240
- process.stdout.write(
241
- `openspec: installed version ${installed} is outdated (latest ${latest}). Updating now:\n ${OPENSPEC_INSTALL_COMMAND}\n`,
181
+ const skills = await Promise.all(
182
+ GENERATED_SKILL_ROOTS.map((root) =>
183
+ generatedOpenSpecSkills(path.join(companionPath, root, "skills")),
184
+ ),
242
185
  );
243
-
244
- try {
245
- await deps.installCommand("npm", ["install", "-g", "@fission-ai/openspec@latest"], {
246
- cwd: ctx.companionPath,
247
- });
248
- } catch {
249
- process.stderr.write("openspec: upgrade failed - continuing with the installed version\n");
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);
250
199
  }
251
200
  }
252
201
 
253
- async function ensureOpenSpecInstalled(
254
- ctx: SetupContext,
255
- deps: Required<
256
- Pick<
257
- OpenSpecPluginDeps,
258
- | "installCommand"
259
- | "isCommandOnPath"
260
- | "confirm"
261
- | "pathValue"
262
- | "getInstalledVersion"
263
- | "fetchLatestVersion"
264
- | "isPinnedDeployment"
265
- >
266
- >,
267
- ): Promise<boolean> {
268
- if (deps.isCommandOnPath("openspec", deps.pathValue())) {
269
- await updateOpenSpecIfOutdated(ctx, deps);
270
- return true;
271
- }
272
-
273
- if (ctx.mode !== "setup") {
274
- process.stderr.write(
275
- `openspec binary not found. Install it manually with \`${OPENSPEC_INSTALL_COMMAND}\`\n`,
276
- );
277
- return false;
278
- }
279
-
280
- if (!deps.isCommandOnPath("npm", deps.pathValue())) {
281
- process.stderr.write(
282
- `openspec binary not found and npm is not available on PATH. Install it manually with \`${OPENSPEC_INSTALL_COMMAND}\`\n`,
283
- );
284
- return false;
285
- }
286
-
287
- process.stdout.write(`openspec binary not found. To install:\n ${OPENSPEC_INSTALL_COMMAND}\n`);
288
- const ok = await deps.confirm("Run this install command now?");
289
- if (!ok) {
290
- process.stderr.write(
291
- `openspec: install skipped - install manually with \`${OPENSPEC_INSTALL_COMMAND}\`\n`,
292
- );
293
- return false;
202
+ async function applyMateOpenspecSkills(ctx: SetupContext): Promise<void> {
203
+ if (ctx.activeProviders.includes("claude")) {
204
+ await applyMateSkills(getClaudeSkillsDir(ctx.companionPath), "claude", ctx.managedOutput);
294
205
  }
295
-
296
- try {
297
- await deps.installCommand("npm", ["install", "-g", "@fission-ai/openspec@latest"], {
298
- cwd: ctx.companionPath,
299
- });
300
- } catch {
301
- process.stderr.write(
302
- `openspec: install failed - install manually with \`${OPENSPEC_INSTALL_COMMAND}\`\n`,
303
- );
304
- return false;
206
+ if (isSharedSkillRootActive(ctx.activeProviders)) {
207
+ await applyMateSkills(getSharedSkillsDir(ctx.companionPath), "agents", ctx.managedOutput);
305
208
  }
209
+ }
306
210
 
307
- if (!deps.isCommandOnPath("openspec", deps.pathValue())) {
308
- process.stderr.write(
309
- "openspec install completed but the binary is still not available on PATH\n",
310
- );
311
- return false;
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}`);
312
222
  }
313
-
314
- return true;
223
+ await removeGeneratedOpenSpecOutput(ctx.companionPath, ctx.managedOutput);
315
224
  }
316
225
 
317
226
  function isMateSpecsRule(item: unknown): boolean {
@@ -426,7 +335,7 @@ async function ensureManagedOpenSpecConfig(companionPath: string): Promise<void>
426
335
  return;
427
336
  }
428
337
 
429
- // config.yaml is user-editable once created; only Mate's own entry is touched.
338
+ // config.yaml is user-editable once created; only Mate's entry and a default `schema` are touched.
430
339
  const doc = parseDocument(content);
431
340
  if (doc.errors.length > 0) {
432
341
  process.stderr.write(
@@ -434,7 +343,14 @@ async function ensureManagedOpenSpecConfig(companionPath: string): Promise<void>
434
343
  );
435
344
  return;
436
345
  }
437
- if (!upsertMateSpecsRule(doc)) return;
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;
438
354
  const merged = doc.toString(CONFIG_TO_STRING);
439
355
  if (merged !== content) await fs.writeFile(configPath, merged, "utf8");
440
356
  }
@@ -465,27 +381,14 @@ async function reconcileOpenSpecSchema(ctx: SetupContext): Promise<void> {
465
381
  );
466
382
  await fs.copyFile(MATE_CONVENTIONS_SOURCE, getMateConventionsPath(ctx.companionPath));
467
383
 
468
- // Availability is unconditional so any change may select a Mate schema;
469
- // activation stays opt-in through the companion-level setup selection.
470
- if (getOpenSpecSchemaSelection(ctx.config.capabilities ?? []) !== "mate-v1") {
471
- await removeManagedOpenSpecConfig(ctx.companionPath);
472
- return;
473
- }
474
-
475
384
  await ensureManagedOpenSpecConfig(ctx.companionPath);
476
385
  }
477
386
 
478
387
  export function createOpenspecPlugin(deps: OpenSpecPluginDeps = {}): CapabilityPlugin {
479
388
  const depsWithDefaults = {
480
- runCommand: deps.runCommand ?? runCommandSilently,
481
- installCommand: deps.installCommand ?? runCommand,
482
- isCommandOnPath: deps.isCommandOnPath ?? isCommandOnPath,
483
- confirm: deps.confirm ?? confirm,
484
- pathValue: deps.pathValue ?? (() => process.env.PATH ?? ""),
389
+ runOpenSpec: deps.runOpenSpec ?? runOpenSpec,
390
+ locateOpenSpec: deps.locateOpenSpec ?? locateOpenSpec,
485
391
  wrapperBinPath: deps.wrapperBinPath ?? getWrapperBinPath,
486
- getInstalledVersion: deps.getInstalledVersion ?? defaultGetInstalledVersion,
487
- fetchLatestVersion: deps.fetchLatestVersion ?? defaultFetchLatestVersion,
488
- isPinnedDeployment: deps.isPinnedDeployment ?? (() => isPinnedDeployment()),
489
392
  };
490
393
 
491
394
  return {
@@ -503,19 +406,9 @@ export function createOpenspecPlugin(deps: OpenSpecPluginDeps = {}): CapabilityP
503
406
  return {
504
407
  claude: {
505
408
  permissionEntries: [
506
- "Skill(openspec-explore)",
507
- "Skill(openspec-propose)",
508
- "Skill(openspec-apply-change)",
509
- "Skill(openspec-archive-change)",
510
- "Skill(mate-artifact-publish)",
511
- "Skill(mate-openspec-backfill)",
512
- "Skill(mate-interview-me)",
513
- "Skill(mate-grill-me)",
514
- "Skill(mate-grilling)",
515
- "Skill(mate-grill-with-docs)",
516
- "Skill(mate-domain-modeling)",
517
- "Skill(mate-show-me)",
518
- "Skill(mate-simplify-code)",
409
+ ...MATE_SKILLS.filter((skill) => !UNPERMITTED_MATE_SKILLS.has(skill)).map(
410
+ (skill) => `Skill(${skill})`,
411
+ ),
519
412
  "Bash(openspec:*)",
520
413
  `Bash(${FRAMEWORK_NAME} cap graphify:*)`,
521
414
  `Bash(${path.join(depsWithDefaults.wrapperBinPath(), "openspec")}:*)`,
@@ -525,54 +418,42 @@ export function createOpenspecPlugin(deps: OpenSpecPluginDeps = {}): CapabilityP
525
418
  },
526
419
  gitignoreEntries: () => OPENSPEC_GITIGNORE_ENTRIES,
527
420
  persistGitignoreEntries: true,
528
- getInstallRequirements: (): InstallRequirement[] => [
529
- {
530
- id: "capability:openspec",
531
- label: "OpenSpec CLI",
532
- group: "companion",
533
- source: "OpenSpec capability",
534
- command: OPENSPEC_INSTALL_COMMAND,
535
- fingerprint: `openspec:${OPENSPEC_INSTALL_COMMAND}`,
536
- detect: () => depsWithDefaults.isCommandOnPath("openspec", depsWithDefaults.pathValue()),
537
- install: () =>
538
- depsWithDefaults.installCommand("npm", ["install", "-g", "@fission-ai/openspec@latest"]),
539
- verify: () => depsWithDefaults.isCommandOnPath("openspec", depsWithDefaults.pathValue()),
540
- },
541
- ],
542
421
  async apply(ctx) {
543
- const tools = deriveOpenSpecTools(ctx.activeProviders);
544
- if (ctx.mode === "setup" || tools.length > 0) {
545
- if (await ensureOpenSpecInstalled(ctx, depsWithDefaults)) {
546
- if (tools.length > 0) {
547
- await reconcileOpenSpecTools(ctx, depsWithDefaults.runCommand);
548
- }
549
- }
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`);
550
429
  }
551
- await removeLegacyOpenCodeCopies(ctx.companionPath);
430
+ await applyMateOpenspecSkills(ctx);
431
+ await removeLegacyOpenCodeCopies(ctx.companionPath, ctx.managedOutput);
552
432
  await reconcileOpenSpecSchema(ctx);
553
433
  },
554
434
  async teardown(ctx) {
555
- await teardownClaudeSkillRoot(ctx.companionPath);
556
- await teardownSharedSkillRoot(ctx.companionPath);
557
- 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);
558
439
  await teardownOpenSpecSchema(ctx.companionPath);
559
440
  },
560
441
  forProvider: {
561
442
  claude: {
562
- // Publication is explicit (`mate-artifact-publish`), so openspec
443
+ // Publication is explicit (`mate-publish-artifact`), so openspec
563
444
  // contributes no archive-triggered Claude hook and no hook state.
564
445
  async apply() {},
565
446
  async teardown(ctx: SetupContext) {
566
- await teardownClaudeSkillRoot(ctx.companionPath);
447
+ await teardownClaudeSkillRoot(ctx.companionPath, ctx.managedOutput);
567
448
  },
568
449
  },
569
450
  opencode: {
570
451
  async apply() {},
571
452
  async teardown(ctx: SetupContext) {
572
453
  if (!isSharedSkillRootActive(ctx.activeProviders)) {
573
- await teardownSharedSkillRoot(ctx.companionPath);
454
+ await teardownSharedSkillRoot(ctx.companionPath, ctx.managedOutput);
574
455
  }
575
- await removeLegacyOpenCodeCopies(ctx.companionPath);
456
+ await removeLegacyOpenCodeCopies(ctx.companionPath, ctx.managedOutput);
576
457
  },
577
458
  },
578
459
  },
@@ -4,6 +4,8 @@ import fs from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
 
7
+ import { resolveSecretPolicy } from "../../../runtime/policy";
8
+ import { DEFAULT_SECRET_DENY_PATTERNS, type SecretPolicy } from "../../../runtime/secret-patterns";
7
9
  import { resolveGitInfoExcludePath } from "../git-utils";
8
10
  import type { InstallRequirement } from "../install-contract";
9
11
  import type { CapabilityPlugin, RuntimeContributionsByRuntime } from "../plugin";
@@ -368,14 +370,40 @@ export async function isTokensaveIndexed(repoPath: string): Promise<boolean> {
368
370
  return fileExists(path.join(repoPath, TOKENSAVE_STORE_DIR, TOKENSAVE_STORE_DATABASE_FILE));
369
371
  }
370
372
 
373
+ function secretExclude(pattern: string): string {
374
+ return `**/${pattern.replace(/^\/+/, "")}`;
375
+ }
376
+
377
+ /**
378
+ * Mate owns the excludes derived from the defaults and from the companion's
379
+ * current extra patterns. An extra removed from config is no longer
380
+ * recognizable, so its exclude stays: over-excluding from an index is harmless.
381
+ */
382
+ function reconcileSecretExcludes(existing: unknown, secrets: SecretPolicy): string[] {
383
+ const owned = new Set([...DEFAULT_SECRET_DENY_PATTERNS, ...secrets.deny].map(secretExclude));
384
+ const kept = (Array.isArray(existing) ? existing : []).filter(
385
+ (entry): entry is string => typeof entry === "string" && !owned.has(entry),
386
+ );
387
+ const managed = secrets.guardrails ? [...new Set(secrets.deny.map(secretExclude))] : [];
388
+ return [...kept, ...managed];
389
+ }
390
+
371
391
  /**
372
392
  * Reconciles Mate's single-graph branching posture into the working repo's
373
393
  * git-excluded `.tokensave/config.json` and deactivates existing branch
374
394
  * metadata. Best-effort by design: the file is created by `tokensave init`,
375
395
  * so an absent, unreadable, or unparseable config is left untouched rather
376
396
  * than treated as an error. Existing branch databases are never removed.
397
+ *
398
+ * With a secret policy, also reconciles the secret-file excludes: Mate's
399
+ * `**\/<pattern>` entries are appended while guardrails are on and removed
400
+ * when they are off, by exact value, so user-authored excludes keep their
401
+ * place. Without one, `exclude` is left as it is.
377
402
  */
378
- export async function ensureTokensaveBranchingPosture(repoPath: string): Promise<void> {
403
+ export async function ensureTokensaveBranchingPosture(
404
+ repoPath: string,
405
+ secrets?: SecretPolicy,
406
+ ): Promise<void> {
379
407
  const configPath = path.join(repoPath, TOKENSAVE_STORE_DIR, TOKENSAVE_STORE_CONFIG_FILE);
380
408
 
381
409
  let raw: string;
@@ -395,8 +423,13 @@ export async function ensureTokensaveBranchingPosture(repoPath: string): Promise
395
423
  return;
396
424
  }
397
425
 
426
+ const config = parsed as Record<string, unknown>;
398
427
  const next = JSON.stringify(
399
- { ...(parsed as Record<string, unknown>), ...TOKENSAVE_OWNED_POSTURE },
428
+ {
429
+ ...config,
430
+ ...TOKENSAVE_OWNED_POSTURE,
431
+ ...(secrets ? { exclude: reconcileSecretExcludes(config.exclude, secrets) } : {}),
432
+ },
400
433
  null,
401
434
  2,
402
435
  );
@@ -539,7 +572,7 @@ export function createTokensavePlugin(): CapabilityPlugin {
539
572
  ) {
540
573
  return;
541
574
  }
542
- await ensureTokensaveBranchingPosture(targetPath);
575
+ await ensureTokensaveBranchingPosture(targetPath, resolveSecretPolicy(ctx.companionPath));
543
576
  if (ctx.mode === "setup") {
544
577
  installTokensaveAgentIntegrations(ctx.activeProviders, targetPath);
545
578
  }