@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
@@ -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";
@@ -502,7 +513,7 @@ async function reconcileOpenCodeMcpContributions(
502
513
  }
503
514
 
504
515
  /** The Mate-managed V2 `mcp.servers` map of one pass, as a value. */
505
- export function renderManagedOpenCodeMcpServers(
516
+ function renderManagedOpenCodeMcpServers(
506
517
  contributions: CapabilityContributionInput[] = [],
507
518
  ): Record<string, unknown> {
508
519
  const servers: Record<string, unknown> = {};
@@ -529,6 +540,174 @@ export function renderCompanionExternalDirectoryPermissions(
529
540
  ];
530
541
  }
531
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
+
532
711
  /**
533
712
  * Reconcile declared agent definition files under `.opencode/agents/`. Runs
534
713
  * in both companion and hub scope — an agent definition is fully
@@ -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,6 +73,16 @@ 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,
@@ -64,11 +90,23 @@ export async function applySetupCompatibilities(
64
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
  /**
@@ -82,16 +120,83 @@ export async function renderWorkingRuntimeDocuments(
82
120
  repoPath: string,
83
121
  plugins: PluginRegistration[] = getActiveDistribution().registry.getAll(),
84
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 } {
85
154
  const plan = buildSetupInstallationPlan(config, plugins);
86
155
  const ctx: SetupContext = {
87
156
  companionPath,
88
157
  config,
89
158
  mode: "sync",
90
159
  activeProviders: plan.activeProviders,
91
- repoPath,
92
- 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,
93
199
  };
94
- return renderRuntimeDocuments(ctx, plugins, plan);
95
200
  }
96
201
 
97
202
  export async function updateProjectGitignore(
@@ -104,18 +209,29 @@ export async function updateProjectGitignore(
104
209
  await writeManagedGitignoreBlock(path.join(companionPath, ".gitignore"), FRAMEWORK_NAME, entries);
105
210
  }
106
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
+
107
219
  export async function syncCompanionFiles(
108
220
  companionPath: string,
109
221
  config: FrameworkConfig,
110
222
  repoPath?: string,
111
- ): Promise<void> {
112
- await applySetupCompatibilities(
223
+ options: CompanionSyncOptions = {},
224
+ ): Promise<CompanionSyncResult> {
225
+ const outcome = await applySetupCompatibilities(
113
226
  companionPath,
114
227
  config,
115
228
  "sync",
116
229
  getActiveDistribution().registry.getAll(),
117
230
  repoPath,
231
+ undefined,
232
+ options,
118
233
  );
234
+ return { claudePluginFolders: outcome.claudePluginFolders, managedOutput: outcome.managedOutput };
119
235
  }
120
236
 
121
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
@@ -50,16 +50,25 @@ if [ -z "$_companion" ]; then
50
50
  fi
51
51
  [ -n "$_companion" ] || { echo "openspec: no mate context here — run \`mate wrap\` in this repository" >&2; exit 1; }
52
52
 
53
+ # The OpenSpec CLI Mate ships with, found the way Node resolves it: upward from
54
+ # the package root, hoisted or nested under mate-core (distribution packages).
55
+ # Never an openspec on PATH, so a global install of another version is ignored.
56
+ # Physical path: the runtime directory reaches wrappers/ through a symlink.
53
57
  _real=""
54
- IFS=:
55
- for _dir in $PATH; do
56
- [ "$_dir" = "$_this_dir" ] && continue
57
- [ -x "$_dir/openspec" ] || continue
58
- _real="$_dir/openspec"
59
- break
58
+ _dir="$(cd -P "$_this_dir" && cd ../.. && pwd -P)"
59
+ while [ -z "$_real" ]; do
60
+ for _candidate in \
61
+ "$_dir/node_modules/@fission-ai/openspec/bin/openspec.js" \
62
+ "$_dir/node_modules/@uniqbit/mate-core/node_modules/@fission-ai/openspec/bin/openspec.js"; do
63
+ if [ -f "$_candidate" ]; then
64
+ _real="$_candidate"
65
+ break 2
66
+ fi
67
+ done
68
+ [ "$_dir" = "/" ] && break
69
+ _dir="$(dirname "$_dir")"
60
70
  done
61
- unset IFS
62
- [ -n "$_real" ] || { echo "openspec wrapper: real binary not found in PATH" >&2; exit 1; }
71
+ [ -n "$_real" ] || { echo "openspec: bundled OpenSpec CLI is missing. Reinstall Mate to restore it." >&2; exit 1; }
63
72
  command="${1-}"
64
73
  if [ "$command" = "templates" ]; then
65
74
  has_schema=0
@@ -133,4 +142,19 @@ case "$command" in
133
142
  _mate_use_store_pointer
134
143
  ;;
135
144
  esac
136
- exec env -C "$_cwd" "$_real" "$@"
145
+ # A JS entry's `#!/usr/bin/env node` shebang needs an executable bit and `node`
146
+ # on PATH; without either (Bun-only installs) it runs on an explicit runtime.
147
+ _shebang=""
148
+ [ "${_real%.js}" != "$_real" ] && { IFS= read -r _shebang < "$_real" || true; }
149
+ if [ -n "$_shebang" ] && {
150
+ [ ! -x "$_real" ] ||
151
+ { [ "$_shebang" = "#!/usr/bin/env node" ] && ! command -v node >/dev/null 2>&1; }
152
+ }; then
153
+ _runtime="$(command -v node || command -v bun || true)"
154
+ [ -n "$_runtime" ] || { echo "openspec: neither node nor bun is on PATH to run $_real" >&2; exit 1; }
155
+ set -- "$_real" "$@"
156
+ _real="$_runtime"
157
+ fi
158
+ # Mate decides the OpenSpec version, so OpenSpec's update hint is wrong advice
159
+ # here; both variables reach the delegated binary only.
160
+ exec env -C "$_cwd" OPENSPEC_TELEMETRY=0 OPENSPEC_NO_UPDATE_CHECK=1 "$_real" "$@"
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ // Thin shim: the gate lives in src/tools/setup/capabilities/graphify-gate.ts,
3
+ // loaded through the plugin's ts-loader because the installed package sits
4
+ // inside node_modules where node's native type stripping is disabled.
5
+ import "../../claude-plugin/hooks/ts-loader.mjs";
6
+
7
+ const { runGraphifyGate } = await import("../../src/tools/setup/capabilities/graphify-gate.ts");
8
+ process.exitCode = await runGraphifyGate(process.argv.slice(2));