@uniqbit/mate-core 0.18.5 → 1.0.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/claude-plugin/hooks/companion-instructions.mjs +9 -0
  2. package/claude-plugin/hooks/hooks.json +12 -2
  3. package/package.json +10 -2
  4. package/src/brand/art.generated.ts +69 -0
  5. package/src/brand/index.ts +12 -0
  6. package/src/brand/palette.ts +19 -0
  7. package/src/brand/svg.generated.ts +8 -0
  8. package/src/brand/terminal.ts +83 -0
  9. package/src/brand/types.ts +33 -0
  10. package/src/cli/commands/artifact/artifact.ts +8 -0
  11. package/src/cli/commands/artifact/delivery/archive.ts +117 -0
  12. package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
  13. package/src/cli/commands/artifact/delivery/environment.ts +104 -0
  14. package/src/cli/commands/artifact/delivery/link.ts +154 -0
  15. package/src/cli/commands/artifact/delivery/output.ts +58 -0
  16. package/src/cli/commands/artifact/finish/command.ts +33 -3
  17. package/src/cli/commands/artifact/finish/engine.ts +47 -3
  18. package/src/cli/commands/artifact/finish/finisher.ts +10 -0
  19. package/src/cli/commands/artifact/finish/openspec.ts +177 -138
  20. package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
  21. package/src/cli/commands/artifact/pending/command.ts +31 -3
  22. package/src/cli/commands/artifact/pending/discovery.ts +21 -57
  23. package/src/cli/commands/cap/graphify.ts +52 -3
  24. package/src/cli/commands/cap/index-cmd.ts +8 -1
  25. package/src/cli/commands/cap/openspec.ts +13 -25
  26. package/src/cli/commands/companion/open.ts +56 -39
  27. package/src/cli/commands/companion/update.ts +75 -42
  28. package/src/cli/commands/doctor.ts +176 -3
  29. package/src/cli/commands/plugin/install.ts +9 -4
  30. package/src/cli/commands/plugin/plugin.ts +3 -2
  31. package/src/cli/commands/report/adapter.ts +11 -2
  32. package/src/cli/commands/report/contract.ts +21 -0
  33. package/src/cli/commands/report/highlight.ts +34 -4
  34. package/src/cli/commands/report/markdown.ts +110 -0
  35. package/src/cli/commands/report/palette.ts +118 -0
  36. package/src/cli/commands/report/renderer.ts +456 -153
  37. package/src/cli/commands/report/types.ts +16 -0
  38. package/src/cli/commands/setup.ts +7 -15
  39. package/src/cli/commands/studio/client-asset.ts +16 -0
  40. package/src/cli/commands/studio/openspec-cli.ts +10 -58
  41. package/src/cli/commands/studio/routes.ts +2 -0
  42. package/src/cli/commands/studio/selection.ts +19 -1
  43. package/src/cli/commands/studio/server.ts +42 -1
  44. package/src/cli/commands/studio/views/client.ts +6 -616
  45. package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
  46. package/src/cli/commands/studio/views/document.tsx +137 -69
  47. package/src/cli/commands/studio/views/markdown.tsx +250 -0
  48. package/src/cli/commands/studio/views/palette.ts +134 -0
  49. package/src/cli/commands/studio/views/reports/index.tsx +2 -7
  50. package/src/cli/commands/studio/views/skills/index.tsx +43 -62
  51. package/src/cli/commands/studio/views/specs/index.tsx +7 -15
  52. package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
  53. package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
  54. package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
  55. package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
  56. package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
  57. package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
  58. package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
  59. package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
  60. package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
  61. package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
  62. package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
  63. package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
  64. package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
  65. package/src/cli/commands/studio/views/styles.ts +217 -206
  66. package/src/cli/commands/studio/views/vault/state.ts +3 -5
  67. package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
  68. package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
  69. package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
  70. package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
  71. package/src/cli/commands/wrap.ts +5 -13
  72. package/src/cli/main.ts +4 -1
  73. package/src/cli/parse-flags.ts +25 -7
  74. package/src/cli/setup-selector.tsx +38 -162
  75. package/src/cli/usage.ts +4 -0
  76. package/src/hooks/companion-instructions.ts +190 -0
  77. package/src/hooks/hook-io.ts +63 -0
  78. package/src/hooks/session-banner.ts +21 -29
  79. package/src/hooks/session-guidance.ts +9 -17
  80. package/src/hooks/session-prepare.ts +115 -28
  81. package/src/hooks/validate-artifact-path.ts +82 -24
  82. package/src/index.ts +4 -0
  83. package/src/lib/change-layout.ts +154 -0
  84. package/src/lib/components/wizard-header.tsx +3 -1
  85. package/src/lib/context-mode-package.ts +24 -11
  86. package/src/lib/delivery/archive-change.ts +154 -0
  87. package/src/lib/delivery/branches.ts +108 -0
  88. package/src/lib/delivery/environment.ts +29 -0
  89. package/src/lib/delivery/forge.ts +279 -0
  90. package/src/lib/delivery/overlap.ts +202 -0
  91. package/src/lib/delivery/record-delivery.ts +196 -0
  92. package/src/lib/delivery/record.ts +343 -0
  93. package/src/lib/delivery/refresh.ts +174 -0
  94. package/src/lib/delivery/repository-access.ts +97 -0
  95. package/src/lib/delivery/stages.ts +111 -0
  96. package/src/lib/delivery/token.ts +58 -0
  97. package/src/lib/frontmatter.ts +18 -0
  98. package/src/lib/opencode-plugin-package.ts +5 -5
  99. package/src/lib/openspec-cli.ts +116 -0
  100. package/src/lib/openspec-scopes.ts +86 -0
  101. package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
  102. package/src/lib/orchestrator/adapters/claude.ts +48 -40
  103. package/src/lib/orchestrator/adapters/opencode.ts +2 -0
  104. package/src/lib/orchestrator/editor.ts +9 -9
  105. package/src/lib/orchestrator/openspec-store.ts +14 -54
  106. package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
  107. package/src/lib/orchestrator/projection-claude-links.ts +159 -0
  108. package/src/lib/orchestrator/projection-claude-skills.ts +19 -137
  109. package/src/lib/orchestrator/projection-entries.ts +45 -14
  110. package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
  111. package/src/lib/orchestrator/projection-types.ts +3 -0
  112. package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
  113. package/src/lib/orchestrator/session-prepare.ts +45 -5
  114. package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
  115. package/src/lib/orchestrator/types.ts +2 -2
  116. package/src/lib/orchestrator/wrap-pass.ts +22 -1
  117. package/src/lib/package-paths.ts +8 -0
  118. package/src/opencode/companion-hooks.ts +64 -5
  119. package/src/opencode/companion-policy.ts +37 -18
  120. package/src/opencode/projected-guidance.ts +4 -1
  121. package/src/opencode/session-prepare.ts +2 -1
  122. package/src/opencode/tui.tsx +4 -2
  123. package/src/runtime/artifact-refusal.ts +63 -0
  124. package/src/runtime/claude-delivery-plan.ts +89 -7
  125. package/src/runtime/commit-attribution-guard.ts +181 -0
  126. package/src/runtime/companion-guidance.ts +15 -3
  127. package/src/runtime/companion-instructions.ts +340 -0
  128. package/src/runtime/error-message.ts +4 -0
  129. package/src/runtime/index.ts +5 -0
  130. package/src/runtime/managed-block.ts +117 -0
  131. package/src/runtime/markdown-scan.ts +266 -0
  132. package/src/runtime/policy.ts +15 -1
  133. package/src/runtime/projected-flag.ts +7 -0
  134. package/src/runtime/projected-guidance.ts +1 -0
  135. package/src/runtime/runtime-document-manifest.ts +62 -11
  136. package/src/runtime/secret-guard.ts +148 -0
  137. package/src/runtime/secret-patterns.ts +164 -0
  138. package/src/runtime/secret-scan.ts +90 -0
  139. package/src/runtime/session-kind.ts +13 -0
  140. package/src/runtime/shell-heredoc.ts +81 -0
  141. package/src/runtime/wrap-state.ts +36 -2
  142. package/src/runtime/write-atomic.ts +64 -0
  143. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
  144. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
  145. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
  146. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  147. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
  148. package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
  149. package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
  150. package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
  151. package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
  152. package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
  153. package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
  154. package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
  155. package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
  156. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
  157. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
  158. package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
  159. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
  160. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
  161. package/src/templates/mate-skills/{claude/mate-artifact-publish → agents/mate-publish-artifact}/SKILL.md +91 -23
  162. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  163. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
  164. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
  165. package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
  166. package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
  167. package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
  168. package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
  169. package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
  170. package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
  171. package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
  172. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
  173. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
  174. package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
  175. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
  176. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
  177. package/src/templates/mate-skills/{agents/mate-artifact-publish → claude/mate-publish-artifact}/SKILL.md +91 -23
  178. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  179. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
  180. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
  181. package/src/templates/mate-skills/openspec-references.ts +140 -0
  182. package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
  183. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
  184. package/src/tools/setup/capabilities/context-mode.ts +25 -5
  185. package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
  186. package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
  187. package/src/tools/setup/capabilities/graphify.ts +12 -0
  188. package/src/tools/setup/capabilities/openspec.ts +151 -270
  189. package/src/tools/setup/capabilities/tokensave.ts +36 -3
  190. package/src/tools/setup/context-services.ts +9 -17
  191. package/src/tools/setup/dynamic-plugins/install.ts +24 -28
  192. package/src/tools/setup/engine.ts +97 -21
  193. package/src/tools/setup/managed-output.ts +51 -0
  194. package/src/tools/setup/mate.ts +86 -11
  195. package/src/tools/setup/plugin.ts +35 -0
  196. package/src/tools/setup/plugins/guidance.ts +7 -4
  197. package/src/tools/setup/providers/agent-file-sections.ts +4 -2
  198. package/src/tools/setup/providers/claude-attribution.ts +65 -0
  199. package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
  200. package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
  201. package/src/tools/setup/providers/claude.ts +284 -8
  202. package/src/tools/setup/providers/opencode.ts +180 -1
  203. package/src/tools/setup/utils.ts +14 -4
  204. package/src/tools/setup.ts +126 -10
  205. package/wrappers/bin/graphify +52 -4
  206. package/wrappers/bin/openspec +33 -9
  207. package/wrappers/lib/graphify-gate.mjs +8 -0
  208. package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
  209. package/src/tools/setup/runtime-documents.ts +0 -291
@@ -1,190 +0,0 @@
1
- /** @jsxImportSource hono/jsx */
2
-
3
- import type { StudioCompanionPayload } from "../../payload";
4
- import {
5
- workflowPlan,
6
- type WorkflowBranch,
7
- type WorkflowStep,
8
- type WorkflowStepBadge,
9
- } from "./steps";
10
-
11
- type WorkflowPlan = ReturnType<typeof workflowPlan>;
12
-
13
- export function WorkflowTranscript({ payload }: { payload: StudioCompanionPayload }) {
14
- return <CommandTranscript plan={workflowPlan(payload.skills)} />;
15
- }
16
-
17
- function CommandTranscript({ plan }: { plan: WorkflowPlan }) {
18
- return (
19
- <section className="workflow-console" aria-label="Workflow execution map">
20
- <header className="workflow-console-header">
21
- <div className="workflow-console-header-title">
22
- <span>Workflow map</span>
23
- <strong>Companion delivery path</strong>
24
- </div>
25
- <span>Read-only guide</span>
26
- </header>
27
- <div className="workflow-console-body">
28
- <ConsoleLine step={plan.start} marker="01" />
29
- <div className="workflow-console-context">
30
- <span>precondition</span>
31
- <div>
32
- <p>The desired change, its scope, and the risk involved are clear.</p>
33
- <small>If any of that is unclear, pre-explore above resolves it first.</small>
34
- </div>
35
- </div>
36
- <div className="workflow-console-schema" data-workflow-schema-root>
37
- <div
38
- className="workflow-console-tabs"
39
- data-workflow-schema-switch
40
- role="tablist"
41
- aria-label="Planning depth"
42
- >
43
- {plan.branches.map((branch, index) => (
44
- <button
45
- key={branch.profile}
46
- type="button"
47
- role="tab"
48
- data-workflow-schema-profile={branch.profile}
49
- aria-selected={index === 0}
50
- >
51
- <strong>{branch.label}</strong>
52
- <small>{index === 0 ? "full planning" : "minimal planning"}</small>
53
- </button>
54
- ))}
55
- </div>
56
- <div className="workflow-console-panels">
57
- {plan.branches.map((branch, branchIndex) => (
58
- <section
59
- key={branch.profile}
60
- data-workflow-schema-panel={branch.profile}
61
- data-active={branchIndex === 0 ? "true" : "false"}
62
- role="tabpanel"
63
- >
64
- <div className="workflow-console-branch-rule">
65
- <p className="workflow-console-branch-rule-description">
66
- {planningRule(branch.profile)}
67
- </p>
68
- </div>
69
- {branch.steps.map((step, index) => (
70
- <div key={step.id}>
71
- {step.sessionBreakBefore ? <WorkflowSessionBreak /> : null}
72
- <ConsoleLine
73
- step={step}
74
- marker={`${branchIndex === 0 ? "F" : "M"}${String(index + 1).padStart(2, "0")}`}
75
- />
76
- </div>
77
- ))}
78
- </section>
79
- ))}
80
- </div>
81
- </div>
82
- <div className="workflow-console-divider">paths rejoin</div>
83
- {plan.shared.map((step) => (
84
- <div key={step.id}>
85
- {step.sessionBreakBefore ? <WorkflowSessionBreak /> : null}
86
- <ConsoleLine step={step} marker="+" />
87
- </div>
88
- ))}
89
- <div className="workflow-console-divider">feature is on production</div>
90
- <ConsoleLine step={plan.finish} marker="END" />
91
- </div>
92
- </section>
93
- );
94
- }
95
-
96
- function planningRule(profile: WorkflowBranch["profile"]): string {
97
- return profile === "mate-v1"
98
- ? "Use when intent is clear, but the solution, repository evidence, or risk still needs exploration."
99
- : "Use only when intent and scope are clear, the change is low-risk, and it stays within one Area.";
100
- }
101
-
102
- function ConsoleLine({ step, marker }: { step: WorkflowStep; marker: string }) {
103
- const hasAlternatives = Boolean(step.alternatives?.length);
104
-
105
- return (
106
- <article className="workflow-console-line">
107
- <span className="workflow-console-gutter">{marker}</span>
108
- <div className="workflow-console-line-body">
109
- <div className="workflow-console-step-title">
110
- <strong>{step.title}</strong>
111
- <WorkflowBadges badges={step.badges} />
112
- </div>
113
- {!hasAlternatives ? (
114
- step.kind === "review" ? (
115
- <div className="workflow-console-command workflow-console-review">
116
- <span className="workflow-console-command-label">Review</span>
117
- <code>{step.prompt}</code>
118
- </div>
119
- ) : (
120
- <div className="workflow-console-command">
121
- <span className="workflow-console-command-label">Run</span>
122
- <code>{step.prompt}</code>
123
- <WorkflowCopy text={step.copyPrompt} label={`${step.title} prompt`} />
124
- </div>
125
- )
126
- ) : null}
127
- {hasAlternatives ? <WorkflowOptions step={step} /> : null}
128
- <div className="workflow-console-explanation">
129
- <div className="workflow-console-detail">
130
- <span>What</span>
131
- <p>{step.what}</p>
132
- </div>
133
- <div className="workflow-console-detail">
134
- <span>Why</span>
135
- <small>{step.why}</small>
136
- </div>
137
- </div>
138
- {!hasAlternatives ? <WorkflowOptions step={step} /> : null}
139
- </div>
140
- </article>
141
- );
142
- }
143
-
144
- function WorkflowOptions({ step }: { step: WorkflowStep }) {
145
- if (!step.alternatives?.length) return null;
146
- return (
147
- <div className="workflow-option-list">
148
- {step.alternatives.map((alternative) => (
149
- <div key={alternative.name} className="workflow-option">
150
- <div className="workflow-console-command">
151
- <span className="workflow-console-command-label">Option</span>
152
- <code>{alternative.prompt}</code>
153
- <WorkflowCopy text={alternative.copyPrompt} label={`${alternative.name} prompt`} />
154
- </div>
155
- <small>{alternative.description}</small>
156
- </div>
157
- ))}
158
- </div>
159
- );
160
- }
161
-
162
- function WorkflowCopy({ text, label }: { text: string; label: string }) {
163
- return (
164
- <button type="button" className="runway-step-copy" data-copy={text} data-copy-label={label}>
165
- Copy
166
- </button>
167
- );
168
- }
169
-
170
- function WorkflowSessionBreak() {
171
- return <div className="workflow-session-break">New session / clean chat</div>;
172
- }
173
-
174
- function WorkflowBadges({ badges }: { badges: WorkflowStepBadge[] }) {
175
- return (
176
- <span className="workflow-badges">
177
- {badges.map((badge) => (
178
- <em key={badge} className={`runway-step-badge runway-step-badge-${badge}`}>
179
- {badgeLabel(badge)}
180
- </em>
181
- ))}
182
- </span>
183
- );
184
- }
185
-
186
- function badgeLabel(badge: WorkflowStepBadge): string {
187
- if (badge === "human-in-loop") return "Review";
188
- if (badge === "openspec") return "OpenSpec";
189
- return badge;
190
- }
@@ -1,291 +0,0 @@
1
- import path from "node:path";
2
-
3
- import { runtimeOpenCodePluginRoot } from "../../runtime/runtime-directory";
4
- import {
5
- CLAUDE_LOCAL_CONFIG_DOCUMENT,
6
- CLAUDE_SETTINGS_DOCUMENT,
7
- OPENCODE_CONFIG_DOCUMENT,
8
- OPENCODE_MCP_SERVERS_AT,
9
- type ManagedRegion,
10
- type RenderedRuntimeDocument,
11
- } from "../../runtime/runtime-document-manifest";
12
- import type { CapabilityContributionInput, SetupContext } from "./plugin";
13
- import {
14
- mateBannerHookGroups,
15
- mateGuardHookGroups,
16
- mateGuidanceHookGroups,
17
- mateSessionPrepareHookGroups,
18
- } from "./providers/claude-plugin-hooks";
19
- import {
20
- renderManagedClaudeHookGroups,
21
- renderManagedClaudeMcpServers,
22
- renderManagedClaudePermissionEntries,
23
- } from "./providers/claude";
24
- import {
25
- renderCompanionExternalDirectoryPermissions,
26
- renderManagedOpenCodeMcpServers,
27
- } from "./providers/opencode";
28
- import { isRecord, readOpenCodeConfig } from "./providers/opencode-format";
29
- import { projectionRepoRoot } from "./surface-target";
30
-
31
- /**
32
- * The working target's documents, rendered as values. Each is a document the
33
- * runtime finds by walking up from the current directory, and every path inside
34
- * one still names the Companion Repository — only where the document is placed
35
- * differs from the companion target.
36
- */
37
-
38
- export { CLAUDE_LOCAL_CONFIG_DOCUMENT };
39
-
40
- function nonEmpty(region: ManagedRegion): boolean {
41
- if (region.kind === "list") return region.values.length > 0;
42
- if (region.kind === "map") return Object.keys(region.entries).length > 0;
43
- return true;
44
- }
45
-
46
- function document(documentPath: string, regions: ManagedRegion[]): RenderedRuntimeDocument[] {
47
- const kept = regions.filter(nonEmpty);
48
- return kept.length > 0 ? [{ path: documentPath, regions: kept }] : [];
49
- }
50
-
51
- /**
52
- * The Mate plugin root is what makes an Unmanaged OpenCode session load the
53
- * companion guidance at all: OpenCode has no config channel for a generated
54
- * system prompt, so the plugin is the only injection point. It is named through
55
- * the runtime directory, so a reinstall does not strand a wrapped repository.
56
- * The managed launch carries no overlay of its own, so this document is the one
57
- * place every OpenCode session in the repository loads it from.
58
- */
59
-
60
- /**
61
- * Loaded by a managed session beside the launch's plugin. Mate hooks here run
62
- * twice there, inertly: guard is input-pure, guidance/prepare yield under a
63
- * launch env, banner is `--projected`. Capability groups run once: the launch
64
- * omits them from `--settings`. A launch passes no `--add-dir`; it relies on
65
- * `CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD` for companion `CLAUDE.md`.
66
- */
67
- function claudeSettingsRegions(
68
- ctx: SetupContext,
69
- inputs: CapabilityContributionInput[],
70
- ): ManagedRegion[] {
71
- const capabilityHooks = renderManagedClaudeHookGroups(inputs);
72
- const mateHooks: Record<string, unknown[]> = {
73
- PreToolUse: mateGuardHookGroups(),
74
- SessionStart: [
75
- ...mateBannerHookGroups(),
76
- ...mateGuidanceHookGroups(),
77
- ...mateSessionPrepareHookGroups(),
78
- ],
79
- };
80
- const events = [...new Set([...Object.keys(mateHooks), ...Object.keys(capabilityHooks)])];
81
- return [
82
- ...events.map((event): ManagedRegion => ({
83
- at: ["hooks", event],
84
- kind: "list",
85
- values: [...(mateHooks[event] ?? []), ...(capabilityHooks[event] ?? [])],
86
- })),
87
- /**
88
- * `stripPrefix` heals a repository an older Mate wrapped. That version wrote
89
- * the whole launch environment here, and Claude Code applies `env` to the
90
- * session, so every hook in a bare session inherited `MATE_*` and read the
91
- * repository as a Managed Session — silently, since looking managed is
92
- * exactly what makes the guidance hook emit nothing. Guidance must be the
93
- * projection's to give in an Unmanaged Session, so no `MATE_*` key may
94
- * survive here.
95
- */
96
- {
97
- at: ["env"],
98
- kind: "map",
99
- entries: { CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: "1" },
100
- stripPrefix: "MATE_",
101
- },
102
- {
103
- at: ["permissions", "allow"],
104
- kind: "list",
105
- values: renderManagedClaudePermissionEntries(ctx.companionPath, inputs),
106
- },
107
- ];
108
- }
109
-
110
- export function renderDocumentsForTarget(
111
- ctx: SetupContext,
112
- inputsByRuntime: Map<string, CapabilityContributionInput[]>,
113
- ): RenderedRuntimeDocument[] {
114
- const repoRoot = projectionRepoRoot(ctx);
115
-
116
- const documents: RenderedRuntimeDocument[] = [];
117
- if (ctx.activeProviders.includes("claude")) {
118
- const inputs = inputsByRuntime.get("claude") ?? [];
119
- documents.push(...document(CLAUDE_SETTINGS_DOCUMENT, claudeSettingsRegions(ctx, inputs)));
120
- /**
121
- * Local scope, not `.mcp.json`: a project-scoped server is inert until a
122
- * human approves it in a session, which an Unmanaged Session is exactly the
123
- * case that cannot rely on. The repo path is the key Claude Code files a
124
- * project under, resolved so a symlinked working directory still matches.
125
- *
126
- * Confirmed against a real `~/.claude.json`: the keys there are the
127
- * directories sessions started in, not repository roots — sibling
128
- * directories of one checkout each hold their own entry, as do temporary
129
- * directories. So this reaches exactly one session: the one started at the
130
- * repository root. A session started in a subdirectory of a wrapped
131
- * repository has its own key, finds no `mcpServers` under it, and sees none
132
- * of these servers — the Unmanaged Session case this document exists for,
133
- * missed. That limit is deliberate. The directories a session might start
134
- * in are unbounded, and every one of them would be a fresh entry in the
135
- * user's global configuration that nothing later reclaims; keying the
136
- * repository root only keeps the projection's footprint there to a single
137
- * key. A subdirectory session still gets the rest of the projection — the
138
- * documents inside the repository are found by the runtime's own upward
139
- * walk — it loses only the MCP servers.
140
- */
141
- documents.push(
142
- ...document(CLAUDE_LOCAL_CONFIG_DOCUMENT, [
143
- {
144
- at: ["projects", repoRoot, "mcpServers"],
145
- kind: "map",
146
- entries: renderManagedClaudeMcpServers(inputs),
147
- },
148
- ]),
149
- );
150
- }
151
- if (ctx.activeProviders.includes("opencode")) {
152
- const inputs = inputsByRuntime.get("opencode") ?? [];
153
- documents.push(
154
- ...document(OPENCODE_CONFIG_DOCUMENT, [
155
- {
156
- at: ["plugins"],
157
- kind: "list",
158
- values: [runtimeOpenCodePluginRoot()],
159
- },
160
- {
161
- at: [...OPENCODE_MCP_SERVERS_AT],
162
- kind: "map",
163
- entries: renderManagedOpenCodeMcpServers(inputs),
164
- },
165
- {
166
- at: ["permissions"],
167
- kind: "list",
168
- values: renderCompanionExternalDirectoryPermissions(ctx.companionPath),
169
- },
170
- {
171
- at: ["skills"],
172
- kind: "list",
173
- values: [path.join(ctx.companionPath, ".agents", "skills")],
174
- },
175
- { at: ["references"], kind: "map", entries: { mate: ctx.companionPath } },
176
- ]),
177
- );
178
- }
179
- return documents;
180
- }
181
-
182
- /** Keys Mate renders its own regions for; the companion's values join those regions. */
183
- const MATE_OPENCODE_KEYS = new Set(["plugins", "permissions", "skills", "mcp", "references"]);
184
-
185
- function relativePathValues(value: unknown, at: string[]): string[] {
186
- if (typeof value === "string") {
187
- return value.startsWith("./") || value.startsWith("../") ? [at.join(".")] : [];
188
- }
189
- if (Array.isArray(value)) {
190
- return value.flatMap((entry, index) => relativePathValues(entry, [...at, String(index)]));
191
- }
192
- if (isRecord(value)) {
193
- return Object.entries(value).flatMap(([key, entry]) => relativePathValues(entry, [...at, key]));
194
- }
195
- return [];
196
- }
197
-
198
- /** Replaces the region at `at` with what `merge` builds from it, or appends one. */
199
- function upsertRegion(
200
- regions: ManagedRegion[],
201
- at: string[],
202
- merge: (found: ManagedRegion | undefined) => ManagedRegion,
203
- ): void {
204
- const key = JSON.stringify(at);
205
- const index = regions.findIndex((region) => JSON.stringify(region.at) === key);
206
- if (index === -1) regions.push(merge(undefined));
207
- else regions[index] = merge(regions[index]);
208
- }
209
-
210
- /** Mate's own values stay; companion values it does not already list go first. */
211
- function mergeListRegion(regions: ManagedRegion[], at: string[], extra: unknown[]): void {
212
- upsertRegion(regions, at, (found) => {
213
- const own = found?.kind === "list" ? found.values : [];
214
- const seen = new Set(own.map((value) => JSON.stringify(value)));
215
- const values = [...extra.filter((value) => !seen.has(JSON.stringify(value))), ...own];
216
- return { at, kind: "list", values };
217
- });
218
- }
219
-
220
- /** Mate's own entries win over companion entries of the same name. */
221
- function mergeMapRegion(
222
- regions: ManagedRegion[],
223
- at: string[],
224
- extra: Record<string, unknown>,
225
- ): void {
226
- upsertRegion(regions, at, (found) => {
227
- const own = found?.kind === "map" ? found.entries : {};
228
- return { at, kind: "map", entries: { ...extra, ...own } };
229
- });
230
- }
231
-
232
- /** Relative reference paths resolved against the companion's `.opencode/`. */
233
- function resolveReferences(
234
- references: Record<string, unknown>,
235
- companionDir: string,
236
- ): Record<string, unknown> {
237
- return Object.fromEntries(
238
- Object.entries(references).map(([name, reference]) => [
239
- name,
240
- typeof reference === "string" && !path.isAbsolute(reference)
241
- ? path.resolve(companionDir, reference)
242
- : reference,
243
- ]),
244
- );
245
- }
246
-
247
- /**
248
- * Folds the companion's `.opencode/opencode.json` into the wrapped document.
249
- * Mate's regions beat companion values; other top-level keys yield to the
250
- * repository's. `references` resolve against the companion's `.opencode/`;
251
- * other relative paths are reported, not rewritten.
252
- */
253
- export async function mergeCompanionOpenCodeConfig(
254
- companionPath: string,
255
- documents: RenderedRuntimeDocument[],
256
- ): Promise<{ documents: RenderedRuntimeDocument[]; warnings: string[] }> {
257
- const index = documents.findIndex((document) => document.path === OPENCODE_CONFIG_DOCUMENT);
258
- const companionConfigPath = path.join(companionPath, ".opencode", "opencode.json");
259
- const companion = index === -1 ? null : await readOpenCodeConfig(companionConfigPath);
260
- if (!companion?.present) return { documents, warnings: [] };
261
-
262
- const regions = [...documents[index]!.regions] as ManagedRegion[];
263
- const warnings: string[] = [];
264
-
265
- const companionDir = path.dirname(companionConfigPath);
266
- for (const [key, value] of Object.entries(companion.config)) {
267
- if (key === "$schema") continue;
268
- if (key === "references" && isRecord(value)) {
269
- mergeMapRegion(regions, ["references"], resolveReferences(value, companionDir));
270
- continue;
271
- }
272
- for (const at of relativePathValues(value, [key])) {
273
- warnings.push(
274
- `${companionConfigPath}: ${at} is a relative path, which resolves against the working repository once merged into ${OPENCODE_CONFIG_DOCUMENT}; use an absolute path.`,
275
- );
276
- }
277
- if (MATE_OPENCODE_KEYS.has(key)) {
278
- if (key === "mcp" && isRecord(value) && isRecord(value.servers)) {
279
- mergeMapRegion(regions, [...OPENCODE_MCP_SERVERS_AT], value.servers);
280
- } else if (Array.isArray(value)) {
281
- mergeListRegion(regions, [key], value);
282
- }
283
- continue;
284
- }
285
- regions.push({ at: [key], kind: "value", value, yields: true });
286
- }
287
-
288
- const next = [...documents];
289
- next[index] = { path: OPENCODE_CONFIG_DOCUMENT, regions };
290
- return { documents: next, warnings };
291
- }