@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
@@ -28,6 +28,7 @@ export type ProjectionEntryId =
28
28
  | "openspec-store-exclude"
29
29
  | "openspec-store-pointer"
30
30
  | "claude-skill-links"
31
+ | "claude-command-links"
31
32
  | "workspace-document"
32
33
  | "capability-excludes"
33
34
  | "claude-working-settings"
@@ -41,6 +42,8 @@ export type ProjectionEntryId =
41
42
  export interface ProjectionInput {
42
43
  repoPath: string;
43
44
  companionPath?: string;
45
+ /** Every companion folder the workspace document lists, in order; `[companionPath]` when absent. */
46
+ workspaceCompanionPaths?: string[];
44
47
  repository?: LinkedRepository;
45
48
  source?: CompanionSource;
46
49
  config?: FrameworkConfig;
@@ -0,0 +1,87 @@
1
+ import { readWrapState } from "../../runtime/wrap-state";
2
+ import { GlobalConfigStore } from "./global-config-store";
3
+ import { reconcileWorkingRepoClaudeCommandLinks } from "./projection-claude-commands";
4
+ import { reconcileWorkingRepoClaudeSkillLinks } from "./projection-claude-skills";
5
+ import { readCompanionRegistry } from "./repository-link";
6
+ import type { FrameworkConfig, LinkedRepository } from "./types";
7
+ import { errorMessage } from "../../runtime/error-message";
8
+
9
+ export interface LinkRefreshResult {
10
+ /** Null when the companion's list of linked repositories could not be read. */
11
+ repository: LinkedRepository | null;
12
+ outcome: "written" | "current" | "failed";
13
+ error?: string;
14
+ }
15
+
16
+ export interface LinkRefreshDeps {
17
+ readRegistry: (companionPath: string) => Promise<LinkedRepository[]>;
18
+ isWrappedForClaude: (repoPath: string) => Promise<boolean>;
19
+ registeredCompanionPaths: () => Promise<string[]>;
20
+ reconcileSkillLinks: typeof reconcileWorkingRepoClaudeSkillLinks;
21
+ reconcileCommandLinks: typeof reconcileWorkingRepoClaudeCommandLinks;
22
+ }
23
+
24
+ const linkRefreshDeps: LinkRefreshDeps = {
25
+ readRegistry: async (companionPath) => {
26
+ try {
27
+ return (await readCompanionRegistry(companionPath)).repos;
28
+ } catch (error) {
29
+ // No registry file means no linked repositories.
30
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
31
+ throw error;
32
+ }
33
+ },
34
+ isWrappedForClaude: async (repoPath) => (await readWrapState(repoPath)).runtimes.has("claude"),
35
+ registeredCompanionPaths: () => new GlobalConfigStore().list(),
36
+ reconcileSkillLinks: reconcileWorkingRepoClaudeSkillLinks,
37
+ reconcileCommandLinks: reconcileWorkingRepoClaudeCommandLinks,
38
+ };
39
+
40
+ /**
41
+ * Re-links the companion's Claude skills and commands into every linked Claude-wrapped
42
+ * Working Repository. Links only; runtime documents wait for the wrap pass. Failures
43
+ * are reported per repository, never thrown.
44
+ */
45
+ export async function refreshWorkingClaudeLinks(
46
+ companionPath: string,
47
+ config: FrameworkConfig,
48
+ deps: LinkRefreshDeps = linkRefreshDeps,
49
+ ): Promise<LinkRefreshResult[]> {
50
+ const enabled = config.allowedAgents?.includes("claude") ?? false;
51
+ const results: LinkRefreshResult[] = [];
52
+ let registered: string[] | undefined;
53
+ let repositories: LinkedRepository[];
54
+ try {
55
+ repositories = await deps.readRegistry(companionPath);
56
+ } catch (error) {
57
+ return [{ repository: null, outcome: "failed", error: errorMessage(error) }];
58
+ }
59
+
60
+ for (const repository of repositories) {
61
+ try {
62
+ if (!(await deps.isWrappedForClaude(repository.path))) continue;
63
+ registered ??= await deps.registeredCompanionPaths();
64
+ const skills = await deps.reconcileSkillLinks(
65
+ repository.path,
66
+ companionPath,
67
+ enabled,
68
+ registered,
69
+ );
70
+ const commands = await deps.reconcileCommandLinks(
71
+ repository.path,
72
+ companionPath,
73
+ enabled,
74
+ registered,
75
+ );
76
+ const written = skills === "written" || commands === "written";
77
+ results.push({ repository, outcome: written ? "written" : "current" });
78
+ } catch (error) {
79
+ results.push({
80
+ repository,
81
+ outcome: "failed",
82
+ error: errorMessage(error),
83
+ });
84
+ }
85
+ }
86
+ return results;
87
+ }
@@ -1,5 +1,6 @@
1
1
  import { getActiveDistribution } from "../../distribution";
2
2
  import { FRAMEWORK_NAME } from "../../framework";
3
+ import { launchPassesClaudePluginFolders } from "../../runtime/claude-delivery-plan";
3
4
  import { recordCompanionSyncUnfinished } from "../../runtime/companion-git-state";
4
5
  import { withCompanionLock, type CompanionLockOptions } from "../../runtime/companion-lock";
5
6
  import {
@@ -7,9 +8,11 @@ import {
7
8
  syncCompanionUnattended,
8
9
  type UnattendedSyncOutcome,
9
10
  } from "../../runtime/companion-sync";
11
+ import { managedSessionKind, type SessionKind } from "../../runtime/session-kind";
10
12
  import type { WrapState } from "../../runtime/wrap-state";
11
13
  import { syncCompanionFiles } from "../../tools/setup";
12
14
  import type { CapabilityPlugin, LaunchPreflightContext } from "../../tools/setup/plugin";
15
+ import type { ClaudePluginFolders } from "../../tools/setup/providers/claude-plugin-folders";
13
16
  import { attendedLockOptions, syncCompanionGit } from "./companion-git-sync";
14
17
  import { readCompanionConfig } from "./config-store";
15
18
  import { entryFailureMessage, firstFailure, type ProjectionChangedPaths } from "./projection-types";
@@ -61,6 +64,8 @@ export interface PrepareResult {
61
64
  failure?: unknown;
62
65
  /** The wrap state the `working-target` step's pass left; a launch reads no manifest itself. */
63
66
  wrapState?: WrapState;
67
+ /** The Claude plugin folders the `file-sync` step gathered; a launch gathers none itself. */
68
+ claudePluginDirectories?: string[];
64
69
  }
65
70
 
66
71
  export interface PrepareSessionInput {
@@ -123,7 +128,8 @@ export const sessionPrepareDeps = {
123
128
  syncCompanionGit,
124
129
  syncCompanionUnattended,
125
130
  recordCompanionSyncUnfinished,
126
- syncCompanionFiles,
131
+ syncCompanionFiles: async (...args: Parameters<typeof syncCompanionFiles>) =>
132
+ (await syncCompanionFiles(...args)).claudePluginFolders,
127
133
  projectLaunch: (input: Parameters<typeof project>[1]) => project("launch", input),
128
134
  /**
129
135
  * The Projection Root a Claude launch guarantees for itself. Companion
@@ -149,6 +155,7 @@ class StepLog {
149
155
  readonly warnings: string[] = [];
150
156
  failure: unknown;
151
157
  wrapState: WrapState | undefined;
158
+ claudePluginDirectories: string[] | undefined;
152
159
  stopped = false;
153
160
  companionChanged = false;
154
161
 
@@ -176,10 +183,18 @@ async function runFileSync(
176
183
  ): Promise<void> {
177
184
  const deps = sessionPrepareDeps;
178
185
  try {
179
- if (input.repository) {
180
- await deps.syncCompanionFiles(input.companionPath, config, input.repository.path);
181
- } else {
182
- await deps.syncCompanionFiles(input.companionPath, config);
186
+ const folders = input.repository
187
+ ? await deps.syncCompanionFiles(input.companionPath, config, input.repository.path)
188
+ : await deps.syncCompanionFiles(input.companionPath, config);
189
+ run.claudePluginDirectories = [...folders.launch];
190
+ /**
191
+ * The Claude plan speaks for Claude sessions only; OpenCode loads no plugin
192
+ * folder. With a Working Repository the wrap pass reports publication;
193
+ * without one no wrap pass runs, so this step does.
194
+ */
195
+ if (reportsClaudePluginFolders(input)) {
196
+ run.warnings.push(...folders.loadWarnings);
197
+ if (!input.repository) run.warnings.push(...publicationWarnings(folders));
183
198
  }
184
199
  run.push({ step: "file-sync", outcome: "ok" });
185
200
  } catch (error) {
@@ -199,6 +214,24 @@ function wrapDetail(wrapped: {
199
214
  return parts.length > 0 ? `updated (${parts.join("; ")})` : "updated";
200
215
  }
201
216
 
217
+ /** A managed Claude launch; plain sessions' prepares and OpenCode report no plugin-folder problem. */
218
+ function reportsClaudePluginFolders(input: PrepareSessionInput): boolean {
219
+ return input.runtime === "claude" && launchPassesClaudePluginFolders(sessionKindOf(input));
220
+ }
221
+
222
+ /** What a publication left undone, one sentence per folder. */
223
+ function publicationWarnings(folders: ClaudePluginFolders | undefined): string[] {
224
+ const publication = folders?.publication;
225
+ if (!publication) return [];
226
+ return [...publication.unpublished.map((entry) => entry.warning), ...publication.warnings];
227
+ }
228
+
229
+ /** Attended is a managed launch, with or without a Working Repository; the rest serve plain sessions. */
230
+ function sessionKindOf(input: PrepareSessionInput): SessionKind {
231
+ if (input.mode !== "attended") return "unmanaged";
232
+ return managedSessionKind(Boolean(input.repository));
233
+ }
234
+
202
235
  function runtimesOf(input: PrepareSessionInput): PrepareRuntime[] {
203
236
  return input.mode !== "attended" && input.runtimes?.length
204
237
  ? [...input.runtimes]
@@ -225,6 +258,9 @@ async function reconcileWrap(
225
258
  return;
226
259
  }
227
260
  run.warnings.push(...wrapped.warnings);
261
+ if (reportsClaudePluginFolders(input)) {
262
+ run.warnings.push(...publicationWarnings(wrapped.claudePluginFolders));
263
+ }
228
264
  run.wrapState = wrapped.wrapState;
229
265
  run.push({
230
266
  step: "working-target",
@@ -308,6 +344,7 @@ async function runLocal(
308
344
  config,
309
345
  repository: input.repository,
310
346
  providerId: runtime,
347
+ sessionKind: sessionKindOf(input),
311
348
  }),
312
349
  ),
313
350
  );
@@ -401,6 +438,9 @@ export async function prepareSession(input: PrepareSessionInput): Promise<Prepar
401
438
  ...(run.companionChanged ? { companionChanged: true as const } : {}),
402
439
  ...(run.failure === undefined ? {} : { failure: run.failure }),
403
440
  ...(run.wrapState ? { wrapState: run.wrapState } : {}),
441
+ ...(run.claudePluginDirectories
442
+ ? { claudePluginDirectories: run.claudePluginDirectories }
443
+ : {}),
404
444
  });
405
445
 
406
446
  const locked = await deps.withCompanionLock(input.companionPath, lockOptions(input), async () => {
@@ -1,23 +1,8 @@
1
- import type {
2
- CapabilityConfig,
3
- CliToolConfig,
4
- FrameworkConfig,
5
- GitModeProfile,
6
- OpenSpecSchemaProfile,
7
- } from "./types";
1
+ import type { CapabilityConfig, CliToolConfig, FrameworkConfig, GitModeProfile } from "./types";
8
2
 
9
- export type OpenSpecSchemaSelection = OpenSpecSchemaProfile | "default";
10
3
  export type GitModeSelection = GitModeProfile | "default";
11
- const DEFAULT_OPENSPEC_SCHEMA_SELECTION: OpenSpecSchemaSelection = "default";
12
4
  export const DEFAULT_GIT_MODE_SELECTION: GitModeSelection = "default";
13
5
 
14
- export interface OpenSpecSchemaCompatibility {
15
- id: OpenSpecSchemaSelection;
16
- label: string;
17
- description: string;
18
- defaultSelected: boolean;
19
- }
20
-
21
6
  export interface GitModeCompatibility {
22
7
  id: GitModeSelection;
23
8
  label: string;
@@ -25,21 +10,6 @@ export interface GitModeCompatibility {
25
10
  defaultSelected: boolean;
26
11
  }
27
12
 
28
- const BUILTIN_OPENSPEC_SCHEMA_COMPATIBILITIES: OpenSpecSchemaCompatibility[] = [
29
- {
30
- id: "default",
31
- label: "Default (vanilla OpenSpec)",
32
- description: "Use OpenSpec's built-in spec-driven workflow.",
33
- defaultSelected: true,
34
- },
35
- {
36
- id: "mate-v1",
37
- label: "Mate v1",
38
- description: "Use Mate's schema with proposal, design, spec, and task artifacts.",
39
- defaultSelected: false,
40
- },
41
- ];
42
-
43
13
  const BUILTIN_GIT_MODE_COMPATIBILITIES: GitModeCompatibility[] = [
44
14
  {
45
15
  id: "default",
@@ -59,33 +29,9 @@ export interface SetupSelections {
59
29
  allowedAgents: string[];
60
30
  packageManagers: string[];
61
31
  capabilities: CapabilityConfig[];
62
- selectedOpenSpecSchema: OpenSpecSchemaSelection;
63
32
  selectedGitMode: GitModeSelection;
64
33
  }
65
34
 
66
- export function getOpenSpecSchemaSelection(
67
- capabilities: CapabilityConfig[],
68
- ): OpenSpecSchemaSelection {
69
- const openspec = capabilities.find((capability) => capability.name === "openspec");
70
- return openspec?.schemaProfile ?? DEFAULT_OPENSPEC_SCHEMA_SELECTION;
71
- }
72
-
73
- export function applyOpenSpecSchemaSelection(
74
- capabilities: CapabilityConfig[],
75
- schemaSelection: OpenSpecSchemaSelection | undefined,
76
- ): CapabilityConfig[] {
77
- if (schemaSelection === undefined) return capabilities;
78
-
79
- return capabilities.map((capability) => {
80
- if (capability.name !== "openspec") return capability;
81
- if (schemaSelection === "default") {
82
- const { schemaProfile: _schemaProfile, ...rest } = capability;
83
- return rest;
84
- }
85
- return { ...capability, schemaProfile: schemaSelection };
86
- });
87
- }
88
-
89
35
  interface SetupCompatibilityBase {
90
36
  id: string;
91
37
  label: string;
@@ -222,24 +168,6 @@ export function listSetupPackageManagerCompatibilities(): SetupPackageManagerCom
222
168
  );
223
169
  }
224
170
 
225
- export function listSetupOpenSpecSchemaCompatibilities(
226
- activeSchemaProfile?: OpenSpecSchemaSelection,
227
- ): OpenSpecSchemaCompatibility[] {
228
- const compatibilities = [...BUILTIN_OPENSPEC_SCHEMA_COMPATIBILITIES];
229
- if (
230
- activeSchemaProfile !== undefined &&
231
- !BUILTIN_OPENSPEC_SCHEMA_COMPATIBILITIES.some((entry) => entry.id === activeSchemaProfile)
232
- ) {
233
- compatibilities.push({
234
- id: activeSchemaProfile,
235
- label: "Custom",
236
- description: `Keep the project's current custom schema profile ("${activeSchemaProfile}"). Selecting Default or Mate v1 replaces it.`,
237
- defaultSelected: false,
238
- });
239
- }
240
- return compatibilities;
241
- }
242
-
243
171
  export function listSetupGitModeCompatibilities(): GitModeCompatibility[] {
244
172
  return [...BUILTIN_GIT_MODE_COMPATIBILITIES];
245
173
  }
@@ -272,7 +200,6 @@ export function getDefaultSetupSelections(): SetupSelections {
272
200
  allowedAgents,
273
201
  packageManagers,
274
202
  capabilities,
275
- selectedOpenSpecSchema: DEFAULT_OPENSPEC_SCHEMA_SELECTION,
276
203
  selectedGitMode: DEFAULT_GIT_MODE_SELECTION,
277
204
  };
278
205
  }
@@ -284,11 +211,27 @@ export function getSetupSelectionsFromConfig(config: FrameworkConfig): SetupSele
284
211
  ...new Set([...getLockedPackageManagers(), ...(config.packageManagers ?? [])]),
285
212
  ],
286
213
  capabilities: dedupeCapabilities(config.capabilities ?? []),
287
- selectedOpenSpecSchema: getOpenSpecSchemaSelection(config.capabilities ?? []),
288
214
  selectedGitMode: config.git ?? DEFAULT_GIT_MODE_SELECTION,
289
215
  };
290
216
  }
291
217
 
218
+ /**
219
+ * Re-selected capabilities keep their saved settings (such as openspec's
220
+ * `protectedBranches`); pickers and flags only name the capabilities.
221
+ */
222
+ export function keepCapabilitySettings(
223
+ selected: CapabilityConfig[],
224
+ saved: CapabilityConfig[],
225
+ ): CapabilityConfig[] {
226
+ const byName = new Map(saved.map((capability) => [capability.name, capability]));
227
+ return selected.map((capability) => {
228
+ // `schemaProfile` is a removed setting; a re-save drops it.
229
+ const { schemaProfile: _removed, ...settings } = (byName.get(capability.name) ??
230
+ {}) as CapabilityConfig & { schemaProfile?: unknown };
231
+ return { ...capability, ...settings };
232
+ });
233
+ }
234
+
292
235
  export function dedupeCapabilities(capabilities: CapabilityConfig[]): CapabilityConfig[] {
293
236
  const seen = new Set<string>();
294
237
  const result: CapabilityConfig[] = [];
@@ -58,12 +58,12 @@ export interface CliToolConfig {
58
58
  redirectCwd: "companionRoot" | false;
59
59
  }
60
60
 
61
- export type OpenSpecSchemaProfile = "mate-v1" | (string & {});
62
61
  export type GitModeProfile = "auto";
63
62
 
64
63
  export interface CapabilityConfig {
65
64
  name: string;
66
- schemaProfile?: OpenSpecSchemaProfile;
65
+ /** openspec only: branch glob patterns (`release/*`) treated as protected besides the default branch. */
66
+ protectedBranches?: string[];
67
67
  }
68
68
 
69
69
  export type FrameworkType = "working" | "companion" | "hub";
@@ -1,6 +1,10 @@
1
1
  import path from "node:path";
2
2
 
3
- import { renderWorkingRuntimeDocuments } from "../../tools/setup";
3
+ import {
4
+ publishWorkingClaudePluginMarketplaces,
5
+ renderWorkingRuntimeDocuments,
6
+ } from "../../tools/setup";
7
+ import type { ClaudePluginFolders } from "../../tools/setup/providers/claude-plugin-folders";
4
8
  import { readCompanionConfig } from "./config-store";
5
9
  import { GlobalConfigStore } from "./global-config-store";
6
10
  import { entryFailureMessage, firstFailure, type ProjectionChangedPaths } from "./projection-types";
@@ -10,6 +14,7 @@ import type { FrameworkConfig, LinkedRepository } from "./types";
10
14
  import { project } from "./working-repo-projection";
11
15
 
12
16
  export interface WrapPassDependencies {
17
+ publishWorkingClaudePluginMarketplaces: typeof publishWorkingClaudePluginMarketplaces;
13
18
  renderWorkingRuntimeDocuments: typeof renderWorkingRuntimeDocuments;
14
19
  readCompanionConfig: typeof readCompanionConfig;
15
20
  project: typeof project;
@@ -18,6 +23,7 @@ export interface WrapPassDependencies {
18
23
  }
19
24
 
20
25
  export const wrapPassDeps: WrapPassDependencies = {
26
+ publishWorkingClaudePluginMarketplaces,
21
27
  renderWorkingRuntimeDocuments,
22
28
  readCompanionConfig,
23
29
  project,
@@ -47,6 +53,11 @@ export type WrapPassResult =
47
53
  runtimes: ReadonlySet<WrapRuntime>;
48
54
  /** The wrap state as this pass left it. */
49
55
  wrapState: WrapState;
56
+ /**
57
+ * The Claude plugin folders this pass published, when it covered Claude;
58
+ * what publication left undone is the caller's to present, never in `warnings`.
59
+ */
60
+ claudePluginFolders?: ClaudePluginFolders;
50
61
  }
51
62
  | { kind: "failed"; message: string };
52
63
 
@@ -63,6 +74,15 @@ export async function runWrapPass(
63
74
  ): Promise<WrapPassResult> {
64
75
  const resolvedCompanionPath = path.resolve(companionPath);
65
76
  const config = options.config ?? (await deps.readCompanionConfig(resolvedCompanionPath));
77
+ /** The render reads the marketplace files, so they are written first. */
78
+ const published =
79
+ !options.runtimes || options.runtimes.includes("claude")
80
+ ? await deps.publishWorkingClaudePluginMarketplaces(
81
+ resolvedCompanionPath,
82
+ config,
83
+ repository.path,
84
+ )
85
+ : undefined;
66
86
  const rendered = await deps.renderWorkingRuntimeDocuments(
67
87
  resolvedCompanionPath,
68
88
  config,
@@ -131,5 +151,6 @@ export async function runWrapPass(
131
151
  ],
132
152
  runtimes,
133
153
  wrapState,
154
+ ...(published ? { claudePluginFolders: published } : {}),
134
155
  };
135
156
  }
@@ -32,10 +32,18 @@ export function getClaudePluginRoot(): string {
32
32
  return path.resolve(import.meta.dirname, "../../claude-plugin");
33
33
  }
34
34
 
35
+ /**
36
+ * The `name` in the bundled Claude plugin's manifest. Claude Code identifies a
37
+ * plugin by its name, so a contributed folder using this one could take the
38
+ * place of Mate's plugin and its guardrail hooks.
39
+ */
40
+ export const MATE_CLAUDE_PLUGIN_NAME = "mate-claude-plugin";
41
+
35
42
  const CLAUDE_PLUGIN_HOOK_SHIMS = [
36
43
  "validate-artifact-path.mjs",
37
44
  "session-banner.mjs",
38
45
  "session-guidance.mjs",
46
+ "companion-instructions.mjs",
39
47
  ] as const;
40
48
 
41
49
  /**
@@ -2,6 +2,8 @@ import path from "node:path";
2
2
 
3
3
  import type { Context } from "@opencode/plugin/promise/plugin";
4
4
 
5
+ import { resolveArtifactTarget } from "../runtime/artifact-refusal";
6
+ import { checkCommitAttribution } from "../runtime/commit-attribution-guard";
5
7
  import {
6
8
  companionForkRefusal,
7
9
  syncCompanionUnattended,
@@ -10,12 +12,14 @@ import {
10
12
  import { hasLaunchEnvironment } from "../runtime/env";
11
13
  import { MATE_ENV } from "../runtime/env-names";
12
14
  import { resolveProjection } from "../runtime/projection";
15
+ import { checkSecretAccess, type SecretToolCall } from "../runtime/secret-guard";
13
16
  import { readWrapStateSync } from "../runtime/wrap-state";
14
17
  import {
15
18
  buildArtifactError,
19
+ extractPatchAdditions,
16
20
  extractPatchPaths,
17
21
  isArtifactPath,
18
- normalizeTargetPath,
22
+ isCompanionTarget,
19
23
  shouldBlockArtifactWrite,
20
24
  type CompanionContext,
21
25
  } from "./companion-policy";
@@ -87,14 +91,55 @@ export async function refuseForkedCompanionWrite(
87
91
  env: Record<string, string | undefined> = process.env,
88
92
  ): Promise<void> {
89
93
  if (!filePath || !isArtifactPath(filePath)) return;
90
- if (!normalizeTargetPath(context, filePath).startsWith(path.normalize(context.companionPath))) {
91
- return;
92
- }
94
+ if (!isCompanionTarget(context, filePath)) return;
93
95
  const refusal = await companionForkRefusal(env, context.companionPath);
94
96
  if (refusal) throw new Error(refusal);
95
97
  }
96
98
 
97
- /** Rejects when the tool call would write an artifact outside the companion. */
99
+ /** OpenCode tool inputs in the shape the shared secret guard reads. */
100
+ function secretToolCall(
101
+ context: CompanionContext,
102
+ tool: string,
103
+ args: Record<string, unknown>,
104
+ ): SecretToolCall | null {
105
+ const text = (value: unknown) => (typeof value === "string" ? value : "");
106
+ const target = (value: unknown) =>
107
+ text(value) ? resolveArtifactTarget(text(value), context.repositoryPath) : "";
108
+ switch (tool) {
109
+ case "read":
110
+ return { kind: "read", paths: [target(args.filePath)] };
111
+ case "write":
112
+ return {
113
+ kind: "write",
114
+ writes: [{ path: target(args.filePath), content: text(args.content) }],
115
+ };
116
+ case "edit":
117
+ return {
118
+ kind: "write",
119
+ writes: [{ path: target(args.filePath), content: text(args.newString) }],
120
+ };
121
+ case "apply_patch":
122
+ return {
123
+ kind: "write",
124
+ writes: extractPatchAdditions(text(args.patchText)).map((addition) => ({
125
+ path: target(addition.path),
126
+ content: addition.content,
127
+ })),
128
+ };
129
+ case "bash":
130
+ return { kind: "shell", command: text(args.command) };
131
+ case "grep":
132
+ return { kind: "search", paths: [target(args.path)], globs: [text(args.include)] };
133
+ default:
134
+ return null;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Rejects when the tool call touches a secret, commits with a model co-author
140
+ * trailer, or would write an artifact outside the companion. Checked in that
141
+ * order, as in the Claude hook.
142
+ */
98
143
  export async function guardToolInput(
99
144
  context: CompanionContext,
100
145
  tool: string,
@@ -102,6 +147,20 @@ export async function guardToolInput(
102
147
  env: Record<string, string | undefined> = process.env,
103
148
  ): Promise<void> {
104
149
  const args = (input ?? {}) as Record<string, unknown>;
150
+
151
+ const secretCall = context.companionPath ? secretToolCall(context, tool, args) : null;
152
+ if (secretCall) {
153
+ const companionRoot = path.normalize(context.companionPath);
154
+ const refusal = await checkSecretAccess(secretCall, {
155
+ companionPath: context.companionPath,
156
+ isCompanionPath: (filePath) => filePath.startsWith(companionRoot),
157
+ });
158
+ if (refusal) throw new Error(refusal);
159
+ }
160
+ if (tool === "bash") {
161
+ const refusal = checkCommitAttribution(String(args.command ?? ""));
162
+ if (refusal) throw new Error(refusal);
163
+ }
105
164
  const filePaths =
106
165
  tool === "write" || tool === "edit"
107
166
  ? [String(args.filePath ?? "")]
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
 
5
+ import { artifactRefusal, resolveArtifactTarget } from "../runtime/artifact-refusal";
5
6
  import { persistedCompanionGitStalenessLines } from "../runtime/companion-sync";
6
7
  import { resolveCompanionRuntime, type CompanionRuntimeContext } from "../runtime/env";
7
8
  import { projectionFreshness, projectionStalenessLines } from "../runtime/freshness";
@@ -60,7 +61,7 @@ export function isArtifactPath(filePath: string): boolean {
60
61
  }
61
62
 
62
63
  export function isProductDocumentationPath(context: CompanionContext, filePath: string): boolean {
63
- const normalized = normalizeTargetPath(context, filePath);
64
+ const normalized = resolveArtifactTarget(filePath, context.repositoryPath);
64
65
  const relativePath = path.relative(context.repositoryPath, normalized);
65
66
  if (!relativePath || relativePath.startsWith("..")) {
66
67
  return false;
@@ -86,12 +87,10 @@ export function isProductDocumentationPath(context: CompanionContext, filePath:
86
87
  return false;
87
88
  }
88
89
 
89
- export function normalizeTargetPath(context: CompanionContext, filePath: string): string {
90
- if (path.isAbsolute(filePath)) {
91
- return path.normalize(filePath);
92
- }
93
-
94
- return path.normalize(path.join(context.repositoryPath, filePath));
90
+ export function isCompanionTarget(context: CompanionContext, filePath: string): boolean {
91
+ return resolveArtifactTarget(filePath, context.repositoryPath).startsWith(
92
+ path.normalize(context.companionPath),
93
+ );
95
94
  }
96
95
 
97
96
  export function isGitignored(context: CompanionContext, filePath: string): boolean {
@@ -99,7 +98,7 @@ export function isGitignored(context: CompanionContext, filePath: string): boole
99
98
  return false;
100
99
  }
101
100
 
102
- const normalized = normalizeTargetPath(context, filePath);
101
+ const normalized = resolveArtifactTarget(filePath, context.repositoryPath);
103
102
  if (!normalized.startsWith(path.normalize(context.repositoryPath))) {
104
103
  return false;
105
104
  }
@@ -126,7 +125,7 @@ export function shouldBlockArtifactWrite(context: CompanionContext, filePath: st
126
125
  return false;
127
126
  }
128
127
 
129
- if (normalizeTargetPath(context, filePath).startsWith(path.normalize(context.companionPath))) {
128
+ if (isCompanionTarget(context, filePath)) {
130
129
  return false;
131
130
  }
132
131
 
@@ -155,16 +154,36 @@ export function extractPatchPaths(patchText: string): string[] {
155
154
  return paths;
156
155
  }
157
156
 
158
- export function buildArtifactError(context: CompanionContext, filePath: string): string {
159
- const rel = path.relative(context.repositoryPath, filePath);
160
- const suggested = path.join(context.companionPath, rel);
157
+ /**
158
+ * What a patch would add to each file it touches: the `+` lines of every
159
+ * section, keyed by the section's target (a move's destination when present).
160
+ */
161
+ export function extractPatchAdditions(patchText: string): { path: string; content: string }[] {
162
+ const additions: { path: string; content: string[] }[] = [];
163
+ let current: { path: string; content: string[] } | undefined;
164
+ for (const line of patchText.split("\n")) {
165
+ const marker = /^\*\*\* (Add|Update|Delete|Move to) File:\s*(.+)$/.exec(line);
166
+ if (marker) {
167
+ if (marker[1] === "Move to" && current) {
168
+ current.path = marker[2].trim();
169
+ } else {
170
+ current = { path: marker[2].trim(), content: [] };
171
+ additions.push(current);
172
+ }
173
+ continue;
174
+ }
175
+ if (current && line.startsWith("+")) current.content.push(line.slice(1));
176
+ }
177
+ return additions.map((entry) => ({ path: entry.path, content: entry.content.join("\n") }));
178
+ }
161
179
 
162
- return [
163
- `${context.frameworkName} guardrail: artifact writes must go to the companion framework path.`,
164
- ` target: ${filePath}`,
165
- ` suggested: ${suggested}`,
166
- `Rewrite to ${suggested} and retry.`,
167
- ].join("\n");
180
+ export function buildArtifactError(context: CompanionContext, filePath: string): string {
181
+ return artifactRefusal({
182
+ target: filePath,
183
+ resolved: resolveArtifactTarget(filePath, context.repositoryPath),
184
+ companion: context.companionPath,
185
+ repoRoot: context.repositoryPath,
186
+ });
168
187
  }
169
188
 
170
189
  /**
@@ -44,6 +44,9 @@ export function resolveOpenCodeGuidance(
44
44
  if (!projection) return { guidance: null, errors: [] };
45
45
 
46
46
  const policy = readCompanionPolicy(projection.companionPath);
47
- const guidance = buildOpenCodeGuidance(policy.enabledCapabilities.map((name) => ({ name })));
47
+ const guidance = buildOpenCodeGuidance(
48
+ policy.enabledCapabilities.map((name) => ({ name })),
49
+ { secretGuardrails: policy.secrets.guardrails },
50
+ );
48
51
  return { guidance: guidance.errors.length > 0 ? null : guidance, errors: guidance.errors };
49
52
  }