@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
@@ -4,6 +4,17 @@ import path from "node:path";
4
4
 
5
5
  import { FRAMEWORK_NAME } from "../../../framework";
6
6
  import { isLegacyMateOpenCodePluginReference } from "../../../lib/opencode-plugin-package";
7
+ import { resolveSecretPolicy } from "../../../runtime/policy";
8
+ import { runtimeOpenCodePluginRoot } from "../../../runtime/runtime-directory";
9
+ import {
10
+ OPENCODE_CONFIG_DOCUMENT,
11
+ OPENCODE_MCP_SERVERS_AT,
12
+ renderedDocument,
13
+ type ManagedRegion,
14
+ type RenderedRuntimeDocument,
15
+ type RenderedRuntimeDocuments,
16
+ } from "../../../runtime/runtime-document-manifest";
17
+ import { projectableSecretDenyPatterns } from "../../../runtime/secret-patterns";
7
18
  import { refreshFromTemplate, stripGuidanceBlock } from "../plugins/guidance";
8
19
  import { stripSectionFromFile, type RemoveHeadingSectionOptions } from "./agent-file-sections";
9
20
  import { reconcileSharedSkillTree } from "./agents-skill-root";
@@ -416,16 +427,6 @@ export async function removeOpenCodeForeignPluginReferences(
416
427
 
417
428
  const OPENCODE_CONTRIBUTION_CONFIG_FILES = ["opencode.json"];
418
429
 
419
- /**
420
- * The surface's reader for an audience layer it wrote: what the Mate OpenCode
421
- * plugin applies. Defined under `runtime/` so the import-isolated plugin can
422
- * reach it; this surface owns the format it reads.
423
- */
424
- export {
425
- readOpenCodeAudienceLayer,
426
- type OpenCodeLayerContents,
427
- } from "../../../runtime/opencode-audience-layer";
428
-
429
430
  export async function reconcileOpenCodeContributions(
430
431
  ctx: SetupContext,
431
432
  inputs: CapabilityContributionInput[],
@@ -512,7 +513,7 @@ async function reconcileOpenCodeMcpContributions(
512
513
  }
513
514
 
514
515
  /** The Mate-managed V2 `mcp.servers` map of one pass, as a value. */
515
- export function renderManagedOpenCodeMcpServers(
516
+ function renderManagedOpenCodeMcpServers(
516
517
  contributions: CapabilityContributionInput[] = [],
517
518
  ): Record<string, unknown> {
518
519
  const servers: Record<string, unknown> = {};
@@ -539,6 +540,174 @@ export function renderCompanionExternalDirectoryPermissions(
539
540
  ];
540
541
  }
541
542
 
543
+ /**
544
+ * V2 deny rules for the default secret files: `read`, and `edit`, which covers
545
+ * write and patch. Only patterns that can't match an allow-listed file are
546
+ * rendered, as for Claude; the middleware enforces the rest of the set.
547
+ */
548
+ export function renderSecretDenyPermissions(
549
+ companionPath: string,
550
+ ): Array<{ action: string; resource: string; effect: string }> {
551
+ if (!resolveSecretPolicy(companionPath).guardrails) return [];
552
+ return projectableSecretDenyPatterns().flatMap((pattern) =>
553
+ ["read", "edit"].map((action) => ({ action, resource: `**/${pattern}`, effect: "deny" })),
554
+ );
555
+ }
556
+
557
+ /**
558
+ * The working target's OpenCode document, rendered as a value, with the
559
+ * companion's `.opencode/opencode.json` folded in.
560
+ *
561
+ * The Mate plugin root is what makes an Unmanaged OpenCode session load the
562
+ * companion guidance at all: OpenCode has no config channel for a generated
563
+ * system prompt, so the plugin is the only injection point. It is named through
564
+ * the runtime directory, so a reinstall does not strand a wrapped repository.
565
+ * The managed launch carries no overlay of its own, so this document is the one
566
+ * place every OpenCode session in the repository loads it from.
567
+ */
568
+ export async function renderOpenCodeWorkingDocuments(
569
+ ctx: SetupContext,
570
+ inputs: CapabilityContributionInput[],
571
+ ): Promise<RenderedRuntimeDocuments> {
572
+ const documents = renderedDocument(OPENCODE_CONFIG_DOCUMENT, [
573
+ {
574
+ at: ["plugins"],
575
+ kind: "list",
576
+ values: [runtimeOpenCodePluginRoot()],
577
+ },
578
+ {
579
+ at: [...OPENCODE_MCP_SERVERS_AT],
580
+ kind: "map",
581
+ entries: renderManagedOpenCodeMcpServers(inputs),
582
+ },
583
+ {
584
+ at: ["permissions"],
585
+ kind: "list",
586
+ values: [
587
+ ...renderCompanionExternalDirectoryPermissions(ctx.companionPath),
588
+ ...renderSecretDenyPermissions(ctx.companionPath),
589
+ ],
590
+ },
591
+ {
592
+ at: ["skills"],
593
+ kind: "list",
594
+ values: [path.join(ctx.companionPath, ".agents", "skills")],
595
+ },
596
+ { at: ["references"], kind: "map", entries: { mate: ctx.companionPath } },
597
+ ]);
598
+ if (ctx.target !== "working") return { documents, warnings: [] };
599
+ return mergeCompanionOpenCodeConfig(ctx.companionPath, documents);
600
+ }
601
+
602
+ /** Keys Mate renders its own regions for; the companion's values join those regions. */
603
+ const MATE_OPENCODE_KEYS = new Set(["plugins", "permissions", "skills", "mcp", "references"]);
604
+
605
+ function relativePathValues(value: unknown, at: string[]): string[] {
606
+ if (typeof value === "string") {
607
+ return value.startsWith("./") || value.startsWith("../") ? [at.join(".")] : [];
608
+ }
609
+ if (Array.isArray(value)) {
610
+ return value.flatMap((entry, index) => relativePathValues(entry, [...at, String(index)]));
611
+ }
612
+ if (isRecord(value)) {
613
+ return Object.entries(value).flatMap(([key, entry]) => relativePathValues(entry, [...at, key]));
614
+ }
615
+ return [];
616
+ }
617
+
618
+ /** Replaces the region at `at` with what `merge` builds from it, or appends one. */
619
+ function upsertRegion(
620
+ regions: ManagedRegion[],
621
+ at: string[],
622
+ merge: (found: ManagedRegion | undefined) => ManagedRegion,
623
+ ): void {
624
+ const key = JSON.stringify(at);
625
+ const index = regions.findIndex((region) => JSON.stringify(region.at) === key);
626
+ if (index === -1) regions.push(merge(undefined));
627
+ else regions[index] = merge(regions[index]);
628
+ }
629
+
630
+ /** Mate's own values stay; companion values it does not already list go first. */
631
+ function mergeListRegion(regions: ManagedRegion[], at: string[], extra: unknown[]): void {
632
+ upsertRegion(regions, at, (found) => {
633
+ const own = found?.kind === "list" ? found.values : [];
634
+ const seen = new Set(own.map((value) => JSON.stringify(value)));
635
+ const values = [...extra.filter((value) => !seen.has(JSON.stringify(value))), ...own];
636
+ return { at, kind: "list", values };
637
+ });
638
+ }
639
+
640
+ /** Mate's own entries win over companion entries of the same name. */
641
+ function mergeMapRegion(
642
+ regions: ManagedRegion[],
643
+ at: string[],
644
+ extra: Record<string, unknown>,
645
+ ): void {
646
+ upsertRegion(regions, at, (found) => {
647
+ const own = found?.kind === "map" ? found.entries : {};
648
+ return { at, kind: "map", entries: { ...extra, ...own } };
649
+ });
650
+ }
651
+
652
+ /** Relative reference paths resolved against the companion's `.opencode/`. */
653
+ function resolveReferences(
654
+ references: Record<string, unknown>,
655
+ companionDir: string,
656
+ ): Record<string, unknown> {
657
+ return Object.fromEntries(
658
+ Object.entries(references).map(([name, reference]) => [
659
+ name,
660
+ typeof reference === "string" && !path.isAbsolute(reference)
661
+ ? path.resolve(companionDir, reference)
662
+ : reference,
663
+ ]),
664
+ );
665
+ }
666
+
667
+ /**
668
+ * Folds the companion's `.opencode/opencode.json` into the wrapped document.
669
+ * Mate's regions beat companion values; other top-level keys yield to the
670
+ * repository's. `references` resolve against the companion's `.opencode/`;
671
+ * other relative paths are reported, not rewritten.
672
+ */
673
+ async function mergeCompanionOpenCodeConfig(
674
+ companionPath: string,
675
+ documents: RenderedRuntimeDocument[],
676
+ ): Promise<RenderedRuntimeDocuments> {
677
+ const [rendered] = documents;
678
+ const companionConfigPath = path.join(companionPath, ".opencode", "opencode.json");
679
+ const companion = rendered ? await readOpenCodeConfig(companionConfigPath) : null;
680
+ if (!rendered || !companion?.present) return { documents, warnings: [] };
681
+
682
+ const regions = [...rendered.regions] as ManagedRegion[];
683
+ const warnings: string[] = [];
684
+
685
+ const companionDir = path.dirname(companionConfigPath);
686
+ for (const [key, value] of Object.entries(companion.config)) {
687
+ if (key === "$schema") continue;
688
+ if (key === "references" && isRecord(value)) {
689
+ mergeMapRegion(regions, ["references"], resolveReferences(value, companionDir));
690
+ continue;
691
+ }
692
+ for (const at of relativePathValues(value, [key])) {
693
+ warnings.push(
694
+ `${companionConfigPath}: ${at} is a relative path, which resolves against the working repository once merged into ${OPENCODE_CONFIG_DOCUMENT}; use an absolute path.`,
695
+ );
696
+ }
697
+ if (MATE_OPENCODE_KEYS.has(key)) {
698
+ if (key === "mcp" && isRecord(value) && isRecord(value.servers)) {
699
+ mergeMapRegion(regions, [...OPENCODE_MCP_SERVERS_AT], value.servers);
700
+ } else if (Array.isArray(value)) {
701
+ mergeListRegion(regions, [key], value);
702
+ }
703
+ continue;
704
+ }
705
+ regions.push({ at: [key], kind: "value", value, yields: true });
706
+ }
707
+
708
+ return { documents: [{ path: OPENCODE_CONFIG_DOCUMENT, regions }], warnings };
709
+ }
710
+
542
711
  /**
543
712
  * Reconcile declared agent definition files under `.opencode/agents/`. Runs
544
713
  * in both companion and hub scope — an agent definition is fully
@@ -3,22 +3,13 @@ import type { Plugin, PluginPolicy, PluginRegistration } from "./plugin";
3
3
  export interface NormalizedRegistration {
4
4
  plugin: Plugin;
5
5
  policy: PluginPolicy;
6
- /** The audience that declared the plugin; `null` for base. */
7
- audience: string | null;
8
- /** The declared package a dynamic plugin was loaded from; absent for compiled-in plugins. */
9
- packageName?: string;
10
6
  }
11
7
 
12
8
  export function normalizeRegistration(entry: PluginRegistration): NormalizedRegistration {
13
9
  if ("plugin" in entry && "policy" in entry) {
14
- return {
15
- plugin: entry.plugin,
16
- policy: entry.policy,
17
- audience: entry.audience ?? null,
18
- ...(entry.packageName ? { packageName: entry.packageName } : {}),
19
- };
10
+ return { plugin: entry.plugin, policy: entry.policy };
20
11
  }
21
- return { plugin: entry, policy: entry.defaultSelected ? "default" : "optional", audience: null };
12
+ return { plugin: entry, policy: entry.defaultSelected ? "default" : "optional" };
22
13
  }
23
14
 
24
15
  export class PluginRegistry {
@@ -41,19 +32,6 @@ export class PluginRegistry {
41
32
  return this.entries.map((entry) => ({ ...entry }));
42
33
  }
43
34
 
44
- /**
45
- * Compiled-in plugins and top-level declarations: the only registrations a
46
- * pass that writes a persisted surface may reconcile.
47
- */
48
- baseRegistrations(): NormalizedRegistration[] {
49
- return this.getEntries().filter((entry) => entry.audience === null);
50
- }
51
-
52
- /** The registrations one audience declared; only its launch layer renders them. */
53
- audienceRegistrations(audience: string): NormalizedRegistration[] {
54
- return this.getEntries().filter((entry) => entry.audience === audience);
55
- }
56
-
57
35
  getPolicy(pluginId: string): PluginPolicy | undefined {
58
36
  return this.entries.find((entry) => entry.plugin.id === pluginId)?.policy;
59
37
  }
@@ -1,6 +1,5 @@
1
1
  import path from "node:path";
2
2
 
3
- import { audienceLayerDir } from "../../runtime/audience-layer";
4
3
  import type { ProjectionTarget, SetupContext } from "./plugin";
5
4
 
6
5
  /**
@@ -15,9 +14,6 @@ export function projectionTarget(ctx: SetupContext): ProjectionTarget {
15
14
  "a working-target reconcile needs a Working Repository in scope; none was supplied",
16
15
  );
17
16
  }
18
- if (target === "audience-layer" && !ctx.audience) {
19
- throw new Error("an audience-layer reconcile needs an audience; none was supplied");
20
- }
21
17
  return target;
22
18
  }
23
19
 
@@ -42,18 +38,13 @@ export function projectionRepoRoot(ctx: SetupContext): string {
42
38
  }
43
39
 
44
40
  /**
45
- * The root a Runtime Surface writes into: the companion, or one audience's
46
- * layer outside every repository. Never a Working Repository: a working-target
47
- * pass renders its documents for the Projection Root to place, so no Runtime
48
- * Surface ever computes a path inside one.
41
+ * The root a Runtime Surface writes into. Defined for the companion target
42
+ * only: a working-target pass renders its documents for the Projection Root to
43
+ * place, so no Runtime Surface ever computes a path inside a Working Repository.
49
44
  */
50
45
  export function surfaceRoot(ctx: SetupContext): string {
51
- const target = projectionTarget(ctx);
52
- if (target === "working") {
46
+ if (projectionTarget(ctx) === "working") {
53
47
  throw new Error("a working-target reconcile renders its documents; it writes no surface root");
54
48
  }
55
- if (target === "audience-layer") {
56
- return ctx.layerRoot ?? audienceLayerDir(ctx.companionPath, ctx.audience!);
57
- }
58
49
  return ctx.companionPath;
59
50
  }
@@ -65,21 +65,31 @@ function waitForClose(child: ChildProcess, formatError: (code: number) => string
65
65
  });
66
66
  }
67
67
 
68
+ export interface RunCommandOptions {
69
+ cwd?: string;
70
+ /** Merged over `process.env` for the child only; Mate's own env is untouched. */
71
+ env?: Record<string, string>;
72
+ }
73
+
74
+ function childEnv(options: RunCommandOptions): { env?: NodeJS.ProcessEnv } {
75
+ return options.env ? { env: { ...process.env, ...options.env } } : {};
76
+ }
77
+
68
78
  export async function runCommand(
69
79
  command: string,
70
80
  args: string[],
71
- options: { cwd?: string } = {},
81
+ options: RunCommandOptions = {},
72
82
  ): Promise<void> {
73
- const child = spawn(command, args, { cwd: options.cwd, stdio: "inherit" });
83
+ const child = spawn(command, args, { cwd: options.cwd, stdio: "inherit", ...childEnv(options) });
74
84
  await waitForClose(child, (code) => `${command} ${args.join(" ")} exited with code ${code}`);
75
85
  }
76
86
 
77
87
  export async function runCommandSilently(
78
88
  command: string,
79
89
  args: string[],
80
- options: { cwd?: string } = {},
90
+ options: RunCommandOptions = {},
81
91
  ): Promise<void> {
82
- const child = spawn(command, args, { cwd: options.cwd, stdio: "pipe" });
92
+ const child = spawn(command, args, { cwd: options.cwd, stdio: "pipe", ...childEnv(options) });
83
93
  await waitForClose(child, (code) => `${command} ${args.join(" ")} exited with code ${code}`);
84
94
  }
85
95
 
@@ -18,11 +18,25 @@ import {
18
18
  type CapabilityConfig,
19
19
  type FrameworkConfig,
20
20
  } from "../lib/orchestrator/types";
21
+ import { getClaudePluginRoot } from "../lib/package-paths";
22
+ import type { RenderedRuntimeDocuments } from "../runtime/runtime-document-manifest";
21
23
  import {
22
24
  collectManagedGitignoreEntries,
23
25
  writeManagedGitignoreBlock,
24
26
  } from "./setup/plugins/gitignore";
25
- import type { PluginRegistration, SetupContext, SetupScope } from "./setup/plugin";
27
+ import { createManagedOutputCollector } from "./setup/managed-output";
28
+ import type {
29
+ ManagedOutputReport,
30
+ PluginRegistration,
31
+ SetupContext,
32
+ SetupScope,
33
+ } from "./setup/plugin";
34
+ import {
35
+ inspectClaudePluginFolders,
36
+ type ClaudePluginContributionFailure,
37
+ type ClaudePluginDirectoryReport,
38
+ type ClaudePluginFolders,
39
+ } from "./setup/providers/claude-plugin-folders";
26
40
  import { getActiveDistribution } from "../distribution";
27
41
  import { createUvPlugin } from "./setup/package-managers/uv";
28
42
  import type { PackageManagerSetupDeps } from "./setup/package-managers/uv";
@@ -38,10 +52,12 @@ import { hydrateDynamicPlugins } from "./setup/dynamic-plugins/hydrate";
38
52
  import { installDeclaredPlugins } from "./setup/dynamic-plugins/install";
39
53
  import {
40
54
  buildSetupInstallationPlan,
55
+ collectRuntimeContributionInputs,
41
56
  executeSetupInstallationPlan,
57
+ publishRuntimeClaudePluginMarketplaces,
42
58
  renderRuntimeDocuments,
43
- type RenderedRuntimeDocuments,
44
59
  type SetupInstallationOutcome,
60
+ type SetupInstallationPlan,
45
61
  } from "./setup/engine";
46
62
 
47
63
  export interface SetupInput {
@@ -57,42 +73,130 @@ export interface SetupDependencies {
57
73
  globalConfigStore?: GlobalConfigStore;
58
74
  }
59
75
 
76
+ export interface CompanionSyncOptions {
77
+ /** Regenerates OpenSpec CLI output; set only by `mate companion update`. */
78
+ regenerateOpenSpec?: boolean;
79
+ }
80
+
81
+ export interface CompanionSyncOutcome extends SetupInstallationOutcome {
82
+ /** The managed output this pass wrote or removed. */
83
+ managedOutput: ManagedOutputReport;
84
+ }
85
+
60
86
  export async function applySetupCompatibilities(
61
87
  companionPath: string,
62
88
  config: FrameworkConfig,
63
89
  mode: "setup" | "sync",
64
- plugins: PluginRegistration[] = getActiveDistribution().registry.baseRegistrations(),
90
+ plugins: PluginRegistration[] = getActiveDistribution().registry.getEntries(),
65
91
  repoPath?: string,
66
92
  scope: SetupScope = config.type === "hub" ? "hub" : "companion",
67
- ): Promise<SetupInstallationOutcome> {
93
+ options: CompanionSyncOptions = {},
94
+ ): Promise<CompanionSyncOutcome> {
68
95
  const plan = buildSetupInstallationPlan(config, plugins);
69
96
  const activeProviders = plan.activeProviders;
70
- const ctx: SetupContext = { companionPath, config, mode, activeProviders, repoPath, scope };
71
- return executeSetupInstallationPlan(ctx, plugins, plan);
97
+ const managedOutput = createManagedOutputCollector(companionPath);
98
+ const ctx: SetupContext = {
99
+ companionPath,
100
+ config,
101
+ mode,
102
+ activeProviders,
103
+ repoPath,
104
+ scope,
105
+ ...(options.regenerateOpenSpec ? { regenerateOpenSpec: true } : {}),
106
+ managedOutput: managedOutput.recorder,
107
+ };
108
+ const outcome = await executeSetupInstallationPlan(ctx, plugins, plan);
109
+ return { ...outcome, managedOutput: managedOutput.report() };
72
110
  }
73
111
 
74
112
  /**
75
113
  * The active Agent Runtimes' documents for a Working Repository, rendered
76
- * without a write: base, plus the audience the checkout is wrapped for. Runs no plugin phase: the working target reconciles
114
+ * without a write. Runs no plugin phase: the working target reconciles
77
115
  * declarations only, so nothing is written to the companion on its behalf.
78
116
  */
79
117
  export async function renderWorkingRuntimeDocuments(
80
118
  companionPath: string,
81
119
  config: FrameworkConfig,
82
120
  repoPath: string,
83
- plugins: PluginRegistration[] = getActiveDistribution().registry.baseRegistrations(),
84
- wrapped?: { audience: string; registrations: PluginRegistration[] },
121
+ plugins: PluginRegistration[] = getActiveDistribution().registry.getAll(),
85
122
  ): Promise<RenderedRuntimeDocuments> {
123
+ const { plan, ctx } = planSyncPass(companionPath, config, plugins, {
124
+ repoPath,
125
+ target: "working",
126
+ });
127
+ return renderRuntimeDocuments(ctx, plugins, plan);
128
+ }
129
+
130
+ /**
131
+ * Publishes the enabled Claude plugin folders for a Working Repository's wrap
132
+ * pass, before it renders; see {@link publishRuntimeClaudePluginMarketplaces}.
133
+ */
134
+ export async function publishWorkingClaudePluginMarketplaces(
135
+ companionPath: string,
136
+ config: FrameworkConfig,
137
+ repoPath: string,
138
+ plugins: PluginRegistration[] = getActiveDistribution().registry.getAll(),
139
+ ): Promise<ClaudePluginFolders> {
140
+ const { plan, ctx } = planSyncPass(companionPath, config, plugins, {
141
+ repoPath,
142
+ target: "working",
143
+ });
144
+ return publishRuntimeClaudePluginMarketplaces(ctx, plugins, plan);
145
+ }
146
+
147
+ /** A sync pass's plan and context, for a caller that runs no plugin phase. */
148
+ function planSyncPass(
149
+ companionPath: string,
150
+ config: FrameworkConfig,
151
+ plugins: PluginRegistration[],
152
+ extra: Pick<SetupContext, "repoPath" | "target"> = {},
153
+ ): { plan: SetupInstallationPlan; ctx: SetupContext } {
86
154
  const plan = buildSetupInstallationPlan(config, plugins);
87
155
  const ctx: SetupContext = {
88
156
  companionPath,
89
157
  config,
90
158
  mode: "sync",
91
159
  activeProviders: plan.activeProviders,
92
- repoPath,
93
- target: "working",
160
+ ...extra,
161
+ };
162
+ return { plan, ctx };
163
+ }
164
+
165
+ /** What an operator sees of the enabled Capabilities' Claude plugin folders. */
166
+ export interface ClaudePluginDirectoryInspection {
167
+ plugins: ClaudePluginDirectoryReport[];
168
+ /** Capabilities whose contributions rejected; the other Capabilities are still listed. */
169
+ failures: ClaudePluginContributionFailure[];
170
+ }
171
+
172
+ /**
173
+ * Enabled Capabilities' declared Claude plugin folders and whether `mate claude`
174
+ * passes each, checked against the running install's bundled plugin. Read-only:
175
+ * runs no plugin phase and reconciles nothing.
176
+ */
177
+ export async function inspectClaudePluginDirectories(
178
+ companionPath: string,
179
+ config: FrameworkConfig,
180
+ plugins: PluginRegistration[] = getActiveDistribution().registry.getEntries(),
181
+ ): Promise<ClaudePluginDirectoryInspection> {
182
+ const { plan, ctx } = planSyncPass(companionPath, config, plugins);
183
+ const failures: ClaudePluginContributionFailure[] = [];
184
+ const inputs = await collectRuntimeContributionInputs(ctx, plugins, plan, (capability, error) =>
185
+ failures.push({
186
+ capability,
187
+ reason: error instanceof Error ? error.message : String(error),
188
+ }),
189
+ );
190
+ return {
191
+ plugins: [
192
+ ...(
193
+ await inspectClaudePluginFolders(inputs.get("claude") ?? [], {
194
+ bundledPluginRoot: getClaudePluginRoot(),
195
+ })
196
+ ).report,
197
+ ],
198
+ failures,
94
199
  };
95
- return renderRuntimeDocuments(ctx, plugins, plan, wrapped);
96
200
  }
97
201
 
98
202
  export async function updateProjectGitignore(
@@ -100,25 +204,34 @@ export async function updateProjectGitignore(
100
204
  config: FrameworkConfig,
101
205
  ): Promise<void> {
102
206
  const ctx: SetupContext = { companionPath, config, mode: "sync", activeProviders: [] };
103
- const plugins = getActiveDistribution()
104
- .registry.baseRegistrations()
105
- .map((entry) => entry.plugin);
207
+ const plugins = getActiveDistribution().registry.getAll();
106
208
  const entries = collectManagedGitignoreEntries(ctx, plugins);
107
209
  await writeManagedGitignoreBlock(path.join(companionPath, ".gitignore"), FRAMEWORK_NAME, entries);
108
210
  }
109
211
 
212
+ export interface CompanionSyncResult {
213
+ /** For a launch to pass and report; every other caller ignores them. */
214
+ claudePluginFolders: ClaudePluginFolders;
215
+ /** For `mate companion update` to sort changed paths; a launch ignores it. */
216
+ managedOutput: ManagedOutputReport;
217
+ }
218
+
110
219
  export async function syncCompanionFiles(
111
220
  companionPath: string,
112
221
  config: FrameworkConfig,
113
222
  repoPath?: string,
114
- ): Promise<void> {
115
- await applySetupCompatibilities(
223
+ options: CompanionSyncOptions = {},
224
+ ): Promise<CompanionSyncResult> {
225
+ const outcome = await applySetupCompatibilities(
116
226
  companionPath,
117
227
  config,
118
228
  "sync",
119
- getActiveDistribution().registry.baseRegistrations(),
229
+ getActiveDistribution().registry.getAll(),
120
230
  repoPath,
231
+ undefined,
232
+ options,
121
233
  );
234
+ return { claudePluginFolders: outcome.claudePluginFolders, managedOutput: outcome.managedOutput };
122
235
  }
123
236
 
124
237
  export const setupToolDeps = {
@@ -69,6 +69,34 @@ _graphify_root="$(dirname "$_graphify_out")"
69
69
  _graph_json="$_graphify_out/graph.json"
70
70
  export GRAPHIFY_OUT="$_graphify_out"
71
71
 
72
+ # Secret gate: leaves secret files out of every extraction and prunes any
73
+ # secret that still reaches the output. It never stops graphify: when the gate
74
+ # can't run, the wrapper warns and runs graphify anyway.
75
+ _gate="$_this_dir/../lib/graphify-gate.mjs"
76
+ _secret_excludes=()
77
+ _secret_pre() {
78
+ local _out _p
79
+ if ! _out="$(node "$_gate" pre "$_companion" "$_repo_path")"; then
80
+ echo "graphify: warning: the secret gate did not run; secret files may be indexed" >&2
81
+ return
82
+ fi
83
+ while IFS= read -r _p; do
84
+ [ -n "$_p" ] && _secret_excludes+=(--exclude "$_p")
85
+ done <<< "$_out"
86
+ }
87
+ _secret_post() {
88
+ node "$_gate" post "$_companion" "$_graphify_out" ||
89
+ echo "graphify: warning: the graphify output was not checked for secrets" >&2
90
+ }
91
+ # Runs an extraction that accepts --exclude, then prunes its output.
92
+ _extract() {
93
+ _secret_pre
94
+ "$_real" "$@" "${_secret_excludes[@]}"
95
+ local _status=$?
96
+ _secret_post
97
+ exit "$_status"
98
+ }
99
+
72
100
  _cmd="${1:-}"
73
101
  case "$_cmd" in
74
102
  build)
@@ -81,16 +109,32 @@ case "$_cmd" in
81
109
  *) _rest+=("$_a") ;;
82
110
  esac
83
111
  done
84
- exec "$_real" extract "$_repo_path" --out "$_graphify_root" "${_rest[@]}"
112
+ _extract extract "$_repo_path" --out "$_graphify_root" "${_rest[@]}"
85
113
  ;;
86
114
  update)
87
115
  if [ "$#" -eq 1 ] || [[ "$2" == -* ]]; then
88
- exec "$_real" "$_repo_path" --update "${@:2}"
116
+ _extract "$_repo_path" --update "${@:2}"
117
+ else
118
+ _extract "$2" --update "${@:3}"
119
+ fi
120
+ ;;
121
+ extract)
122
+ if [ "$#" -eq 1 ] || [[ "$2" == -* ]]; then
123
+ _extract extract "$_repo_path" "${@:2}"
124
+ else
125
+ _extract "$@"
126
+ fi
127
+ ;;
128
+ watch)
129
+ # Long-running: only the ignore region applies; there is no output to prune.
130
+ _secret_pre
131
+ if [ "$#" -eq 1 ] || [[ "$2" == -* ]]; then
132
+ exec "$_real" watch "$_repo_path" "${@:2}"
89
133
  else
90
- exec "$_real" "$2" --update "${@:3}"
134
+ exec "$_real" "$@"
91
135
  fi
92
136
  ;;
93
- extract|watch|cluster-only|label|check-update)
137
+ cluster-only|label|check-update)
94
138
  if [ "$#" -eq 1 ] || [[ "$2" == -* ]]; then
95
139
  exec "$_real" "$_cmd" "$_repo_path" "${@:2}"
96
140
  else
@@ -120,6 +164,10 @@ case "$_cmd" in
120
164
  fi
121
165
  ;;
122
166
  *)
167
+ # A direct `graphify <path> --update` is an extraction too.
168
+ for _a in "$@"; do
169
+ [ "$_a" = "--update" ] && _extract "$@"
170
+ done
123
171
  exec "$_real" "$@"
124
172
  ;;
125
173
  esac