@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,43 +1,32 @@
1
1
  import path from "node:path";
2
2
 
3
- import { renderWorkingRuntimeDocuments } from "../../tools/setup";
4
- import { renderAudienceLayer } from "../../tools/setup/audience-layer";
5
- import { loadAudienceRegistrations } from "../../tools/setup/audience-registrations";
6
- import { declaredAudiences } from "../../tools/setup/dynamic-plugins/audiences";
7
- import { buildSetupInstallationPlan } from "../../tools/setup/engine";
8
- import { getActiveDistribution } from "../../distribution";
9
- import { audienceLayerDir } from "../../runtime/audience-layer";
10
3
  import {
11
- readManifest,
12
- recordedAudience,
13
- updateManifest,
14
- withAudience,
15
- } from "../../runtime/runtime-document-manifest";
4
+ publishWorkingClaudePluginMarketplaces,
5
+ renderWorkingRuntimeDocuments,
6
+ } from "../../tools/setup";
7
+ import type { ClaudePluginFolders } from "../../tools/setup/providers/claude-plugin-folders";
16
8
  import { readCompanionConfig } from "./config-store";
17
9
  import { GlobalConfigStore } from "./global-config-store";
18
10
  import { entryFailureMessage, firstFailure, type ProjectionChangedPaths } from "./projection-types";
19
11
  import type { WrapRuntime } from "../../runtime/runtime-document-manifest";
20
12
  import { readWrapState, type WrapState } from "../../runtime/wrap-state";
21
- import type { NormalizedRegistration } from "../../tools/setup/registry";
22
13
  import type { FrameworkConfig, LinkedRepository } from "./types";
23
14
  import { project } from "./working-repo-projection";
24
15
 
25
16
  export interface WrapPassDependencies {
17
+ publishWorkingClaudePluginMarketplaces: typeof publishWorkingClaudePluginMarketplaces;
26
18
  renderWorkingRuntimeDocuments: typeof renderWorkingRuntimeDocuments;
27
19
  readCompanionConfig: typeof readCompanionConfig;
28
20
  project: typeof project;
29
- loadAudienceRegistrations: typeof loadAudienceRegistrations;
30
- renderAudienceLayer: typeof renderAudienceLayer;
31
21
  /** The registered companions, whose stale skill links a re-pin withdraws; the user's registry when absent. */
32
22
  globalConfigStore?: GlobalConfigStore;
33
23
  }
34
24
 
35
25
  export const wrapPassDeps: WrapPassDependencies = {
26
+ publishWorkingClaudePluginMarketplaces,
36
27
  renderWorkingRuntimeDocuments,
37
28
  readCompanionConfig,
38
29
  project,
39
- loadAudienceRegistrations,
40
- renderAudienceLayer,
41
30
  };
42
31
 
43
32
  export interface WrapPassOptions {
@@ -45,12 +34,6 @@ export interface WrapPassOptions {
45
34
  runtimes?: readonly WrapRuntime[];
46
35
  /** The companion config the caller already read; read from files otherwise. */
47
36
  config?: FrameworkConfig;
48
- /**
49
- * The audience to wrap the checkout for: a name records it, `null` withdraws
50
- * it, absent keeps the one the manifest records. Never read from the
51
- * environment: it is the only source of a bare session's audience.
52
- */
53
- audience?: string | null;
54
37
  }
55
38
 
56
39
  export type WrapPassResult =
@@ -70,21 +53,14 @@ export type WrapPassResult =
70
53
  runtimes: ReadonlySet<WrapRuntime>;
71
54
  /** The wrap state as this pass left it. */
72
55
  wrapState: WrapState;
73
- /** The audience the checkout is wrapped for after this pass. */
74
- audience: string | null;
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;
75
61
  }
76
62
  | { kind: "failed"; message: string };
77
63
 
78
- /** An audience a checkout may be wrapped for: declared by the companion, never the reserved base. */
79
- function wrappableAudience(config: FrameworkConfig, audience: string): boolean {
80
- return audience !== "base" && declaredAudiences(config).includes(audience);
81
- }
82
-
83
- function describeUnwrappable(config: FrameworkConfig, audience: string): string {
84
- const declared = declaredAudiences(config).filter((name) => name !== "base");
85
- return `audience "${audience}" cannot be wrapped: the companion declares ${declared.length > 0 ? declared.join(", ") : "no audiences"}.`;
86
- }
87
-
88
64
  /**
89
65
  * One pass of the `wrap` scope, restricted to the requested runtimes by each
90
66
  * entry's runtime tag. Best-effort per entry like every projection: a failed
@@ -98,50 +74,19 @@ export async function runWrapPass(
98
74
  ): Promise<WrapPassResult> {
99
75
  const resolvedCompanionPath = path.resolve(companionPath);
100
76
  const config = options.config ?? (await deps.readCompanionConfig(resolvedCompanionPath));
101
-
102
- const audienceWarnings: string[] = [];
103
- let audience: string | null;
104
- if (options.audience === undefined) {
105
- const recorded = recordedAudience(await readManifest(repository.path));
106
- /** A recorded audience the companion no longer declares falls back to base, never fails a prepare. */
107
- if (recorded && !wrappableAudience(config, recorded)) {
108
- audienceWarnings.push(
109
- `${describeUnwrappable(config, recorded)} ${repository.id} is wrapped for base only.`,
110
- );
111
- audience = null;
112
- } else {
113
- audience = recorded;
114
- }
115
- } else {
116
- audience = options.audience;
117
- if (audience && !wrappableAudience(config, audience)) {
118
- return { kind: "failed", message: describeUnwrappable(config, audience) };
119
- }
120
- }
121
-
122
- let wrapped: { audience: string; registrations: NormalizedRegistration[] } | undefined;
123
- if (audience) {
124
- const loaded = await deps.loadAudienceRegistrations(resolvedCompanionPath, config, audience);
125
- audienceWarnings.push(...loaded.warnings);
126
- await deps.renderAudienceLayer({
127
- companionPath: resolvedCompanionPath,
128
- config,
129
- audience,
130
- activeProviders: buildSetupInstallationPlan(
131
- config,
132
- getActiveDistribution().registry.baseRegistrations(),
133
- ).activeProviders,
134
- registrations: loaded.registrations,
135
- });
136
- wrapped = { audience, registrations: loaded.registrations };
137
- }
138
-
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;
139
86
  const rendered = await deps.renderWorkingRuntimeDocuments(
140
87
  resolvedCompanionPath,
141
88
  config,
142
89
  repository.path,
143
- undefined,
144
- wrapped,
145
90
  );
146
91
  const result = await deps.project("wrap", {
147
92
  repoPath: repository.path,
@@ -149,7 +94,6 @@ export async function runWrapPass(
149
94
  repository,
150
95
  config,
151
96
  runtimeDocuments: rendered.documents,
152
- audienceLayerDir: audience ? audienceLayerDir(resolvedCompanionPath, audience) : null,
153
97
  globalConfigStore: deps.globalConfigStore ?? new GlobalConfigStore(),
154
98
  ...(options.runtimes ? { runtimes: options.runtimes } : {}),
155
99
  });
@@ -157,12 +101,6 @@ export async function runWrapPass(
157
101
  const failure = firstFailure(result.outcomes);
158
102
  if (failure) return { kind: "failed", message: entryFailureMessage(failure, repository.id) };
159
103
 
160
- const before = recordedAudience(await readManifest(repository.path));
161
- const audienceChanged = before !== audience;
162
- if (audienceChanged) {
163
- await updateManifest(repository.path, (manifest) => withAudience(manifest, audience));
164
- }
165
-
166
104
  /** Read once under the caller's lock, so it is exactly what the pass recorded. */
167
105
  let wrapState: WrapState;
168
106
  try {
@@ -194,10 +132,7 @@ export async function runWrapPass(
194
132
  .map((outcome) => outcome.path);
195
133
 
196
134
  return {
197
- kind:
198
- audienceChanged || result.outcomes.some((outcome) => outcome.state === "written")
199
- ? "written"
200
- : "current",
135
+ kind: result.outcomes.some((outcome) => outcome.state === "written") ? "written" : "current",
201
136
  projectionRoot: result.root,
202
137
  companionPath: resolvedCompanionPath,
203
138
  documents: [...new Set(documents)],
@@ -207,7 +142,6 @@ export async function runWrapPass(
207
142
  removed: result.outcomes.flatMap((outcome) => outcome.changed?.removed ?? []),
208
143
  },
209
144
  warnings: [
210
- ...audienceWarnings,
211
145
  ...rendered.warnings,
212
146
  ...result.outcomes.flatMap((outcome) => outcome.warnings ?? []),
213
147
  /** A degradable failure does not fail the pass, but it is never silent. */
@@ -217,6 +151,6 @@ export async function runWrapPass(
217
151
  ],
218
152
  runtimes,
219
153
  wrapState,
220
- audience,
154
+ ...(published ? { claudePluginFolders: published } : {}),
221
155
  };
222
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
  }
@@ -1,5 +1,6 @@
1
1
  import type { Cleanup, Context } from "@opencode/plugin/promise/plugin";
2
2
 
3
+ import { errorMessage } from "../runtime/error-message";
3
4
  import {
4
5
  PREPARE_TIMEOUT_MS,
5
6
  type PrepareRun,
@@ -52,7 +53,7 @@ async function report(
52
53
  }
53
54
 
54
55
  function failedRun(error: unknown): PrepareRun {
55
- return { ok: false, notes: [error instanceof Error ? error.message : String(error)] };
56
+ return { ok: false, notes: [errorMessage(error)] };
56
57
  }
57
58
 
58
59
  /** No remote: OpenCode holds MCP servers and skills until `setup` returns. */
@@ -6,6 +6,7 @@
6
6
  * does not accept it, so the jsx-key rules cannot be satisfied — only suppressed.
7
7
  * Arrays render through `.map`: Solid re-runs it whole on a store change, fine for a few lines.
8
8
  */
9
+ import { BRAND_ACCENT } from "../brand/palette";
9
10
  import { mateVersion } from "../runtime/install";
10
11
  import { readContext, readStalenessLines } from "./companion-policy";
11
12
 
@@ -47,7 +48,8 @@ export const STALENESS_POLL_MS = 5_000;
47
48
 
48
49
  type SessionNotes = { lines: string[] };
49
50
 
50
- const MIDNIGHT_PURPLE_BRIGHT = "#c084fc";
51
+ /** The `mate v<version>` line uses the brand accent, which reads on light and dark themes alike. */
52
+ export const MATE_VERSION_COLOR = BRAND_ACCENT;
51
53
  const MATE_VERSION = process.env.MATE_VERSION ?? mateVersion();
52
54
 
53
55
  function SessionContext({
@@ -72,7 +74,7 @@ function SessionContext({
72
74
  paddingBottom={sidebar ? 1 : 0}
73
75
  flexShrink={0}
74
76
  >
75
- <text fg={MIDNIGHT_PURPLE_BRIGHT}>mate v{MATE_VERSION}</text>
77
+ <text fg={MATE_VERSION_COLOR}>mate v{MATE_VERSION}</text>
76
78
  <text fg={text.muted}>repo: {context.repositoryPath}</text>
77
79
  <text fg={text.muted}>mate: {context.companionPath}</text>
78
80
  {notes.lines.map((note) => (
@@ -0,0 +1,63 @@
1
+ /** The artifact-write refusal both runtimes print; pure, so either guard can import it. */
2
+ import path from "node:path";
3
+
4
+ import { FRAMEWORK_NAME } from "./framework";
5
+
6
+ /** Matched by consumers and tests; keep byte-identical. */
7
+ export const ARTIFACT_REFUSAL_HEADLINE = "artifact writes must go to the companion framework path.";
8
+
9
+ export interface ArtifactRefusalInput {
10
+ /** As the tool supplied it, relative or absolute. */
11
+ target: string;
12
+ /** The absolute path the guard judged. */
13
+ resolved: string;
14
+ companion: string;
15
+ /** The Working Repository root relative targets resolve against. */
16
+ repoRoot: string;
17
+ }
18
+
19
+ /** The absolute path both guards judge: a relative target resolves against the repo root, never a `cd`. */
20
+ export function resolveArtifactTarget(target: string, repoRoot: string): string {
21
+ if (path.isAbsolute(target)) return path.normalize(target);
22
+ return path.normalize(path.join(repoRoot, target));
23
+ }
24
+
25
+ function relativeInside(target: string, root: string): string | null {
26
+ const relative = path.relative(path.normalize(root), path.normalize(target));
27
+ if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) return null;
28
+ return relative;
29
+ }
30
+
31
+ /** The refusal text, without a trailing newline. */
32
+ export function artifactRefusal({
33
+ target,
34
+ resolved,
35
+ companion,
36
+ repoRoot,
37
+ }: ArtifactRefusalInput): string {
38
+ const isRelative = !path.isAbsolute(target);
39
+ const inside = relativeInside(resolved, repoRoot);
40
+ const suggested = inside ? path.join(companion, inside) : null;
41
+
42
+ const lines = [
43
+ `${FRAMEWORK_NAME} guardrail: ${ARTIFACT_REFUSAL_HEADLINE}`,
44
+ ` target: ${target}`,
45
+ ];
46
+ if (isRelative) lines.push(` resolved: ${resolved}`);
47
+ lines.push(` companion: ${companion}`);
48
+ if (suggested) lines.push(` suggested: ${suggested}`);
49
+ if (isRelative) {
50
+ lines.push(
51
+ "Relative paths resolve against the working repository; a preceding `cd` is not followed.",
52
+ );
53
+ }
54
+ lines.push(
55
+ suggested
56
+ ? "Rewrite to the suggested absolute path and retry."
57
+ : `Use an absolute path under ${companion}.`,
58
+ );
59
+ if (path.basename(target) === "CLAUDE.md") {
60
+ lines.push(`Note: CLAUDE.md already lives in the companion repo at ${companion}/CLAUDE.md.`);
61
+ }
62
+ return lines.join("\n");
63
+ }
@@ -1,4 +1,7 @@
1
+ import path from "node:path";
2
+
1
3
  import type { ClaudeHookGroup } from "./claude-hook-group";
4
+ import type { SessionKind } from "./session-kind";
2
5
  import { NOT_WRAPPED, readWrapStateSync, type WrapState } from "./wrap-state";
3
6
 
4
7
  /**
@@ -7,13 +10,6 @@ import { NOT_WRAPPED, readWrapStateSync, type WrapState } from "./wrap-state";
7
10
  * may import it.
8
11
  */
9
12
 
10
- /**
11
- * `managed-repo`: `mate claude` with a Working Repository; its prepare just
12
- * reconciled the Claude wrap, which the launcher asserts. `managed-companion`:
13
- * a companion-scoped launch. `unmanaged`: a session with no launch environment.
14
- */
15
- export type SessionKind = "managed-repo" | "managed-companion" | "unmanaged";
16
-
17
13
  export interface ClaudeDeliveryPlan {
18
14
  /** Capability hook groups; `launchSettingsExclude` is what `--settings` must not repeat. */
19
15
  readonly hooks: {
@@ -28,9 +24,95 @@ export interface ClaudeDeliveryPlan {
28
24
  readonly reconcile: "attended-prepare" | "prepare-hook" | "none";
29
25
  /** Companion Git synchronization. */
30
26
  readonly gitSync: "launch-preflight" | "prepare-hook" | "banner";
27
+ /**
28
+ * Plugin folders loaded as Claude plugins. A managed launch passes Mate's
29
+ * bundled plugin and every gathered Capability folder but `launchExclude`,
30
+ * the folders the wrap carries; a Claude-wrapped plain session gets those
31
+ * from the wrap alone.
32
+ */
33
+ readonly plugins: {
34
+ readonly owner: "launch-flag" | "wrap" | "none";
35
+ readonly launchExclude: readonly string[];
36
+ };
37
+ /** Companion Instructions, per thread kind; `dirAccess` through settings loads no memory file. */
38
+ readonly instructions: ClaudeInstructionOwners;
39
+ }
40
+
41
+ export interface ClaudeInstructionOwners {
42
+ /** `wrap-session-hook`: the wrap's session-prepare hook, once it has prepared, for a launch and a plain session alike. */
43
+ readonly main: "native-memory" | "wrap-session-hook" | "none";
44
+ /** `plugin-subagent-hook`: the bundled plugin's SubagentStart copy; `wrap-subagent-hook`: the wrap's. */
45
+ readonly subagents: "plugin-subagent-hook" | "native-memory" | "wrap-subagent-hook" | "none";
46
+ }
47
+
48
+ /**
49
+ * Whether the session's launch passes `--plugin-dir`: every managed launch does, a plain
50
+ * session cannot. Not whether the session loads plugin folders: a Claude-wrapped plain
51
+ * session gets them from the wrap.
52
+ */
53
+ export function launchPassesClaudePluginFolders(kind: SessionKind): boolean {
54
+ return kind !== "unmanaged";
31
55
  }
32
56
 
33
57
  export function planClaudeDelivery(kind: SessionKind, wrap: WrapState): ClaudeDeliveryPlan {
58
+ return {
59
+ ...planChannels(kind, wrap),
60
+ plugins: planPlugins(kind, wrap),
61
+ instructions: planClaudeInstructions(kind, wrap.runtimes.has("claude")),
62
+ };
63
+ }
64
+
65
+ /**
66
+ * The instruction owners alone, for a hook that knows its wrap without reading
67
+ * one: a wrap-carried hook runs only because a Claude wrap installed it, maybe
68
+ * the main checkout's, so it must not gate on this Checkout's manifest.
69
+ */
70
+ export function planClaudeInstructions(
71
+ kind: SessionKind,
72
+ wrappedForClaude: boolean,
73
+ ): ClaudeInstructionOwners {
74
+ switch (kind) {
75
+ case "managed-repo":
76
+ return { main: "wrap-session-hook", subagents: "plugin-subagent-hook" };
77
+ case "managed-companion":
78
+ return { main: "native-memory", subagents: "native-memory" };
79
+ case "unmanaged":
80
+ return wrappedForClaude
81
+ ? { main: "wrap-session-hook", subagents: "wrap-subagent-hook" }
82
+ : { main: "none", subagents: "none" };
83
+ }
84
+ }
85
+
86
+ /**
87
+ * The folders a managed launch passes with `--plugin-dir`, in order: the
88
+ * bundled plugin, then each gathered folder the wrap does not carry, matched
89
+ * by resolved path. None when the launch flag does not own plugin folders.
90
+ */
91
+ export function launchPluginFolders(
92
+ plan: ClaudeDeliveryPlan,
93
+ gathered: readonly string[],
94
+ bundledPluginRoot: string,
95
+ ): string[] {
96
+ if (plan.plugins.owner !== "launch-flag") return [];
97
+ const carried = new Set(plan.plugins.launchExclude.map((dir) => path.resolve(dir)));
98
+ return [bundledPluginRoot, ...gathered.filter((dir) => !carried.has(path.resolve(dir)))];
99
+ }
100
+
101
+ function planPlugins(kind: SessionKind, wrap: WrapState): ClaudeDeliveryPlan["plugins"] {
102
+ switch (kind) {
103
+ case "managed-repo":
104
+ return { owner: "launch-flag", launchExclude: wrap.carriedClaudePluginDirectories };
105
+ case "managed-companion":
106
+ return { owner: "launch-flag", launchExclude: [] };
107
+ case "unmanaged":
108
+ return { owner: wrap.runtimes.has("claude") ? "wrap" : "none", launchExclude: [] };
109
+ }
110
+ }
111
+
112
+ function planChannels(
113
+ kind: SessionKind,
114
+ wrap: WrapState,
115
+ ): Omit<ClaudeDeliveryPlan, "plugins" | "instructions"> {
34
116
  switch (kind) {
35
117
  case "managed-repo":
36
118
  return {