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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/claude-plugin/hooks/companion-instructions.mjs +9 -0
  2. package/claude-plugin/hooks/hooks.json +12 -2
  3. package/opencode-plugin/src/add-dir.ts +76 -10
  4. package/opencode-plugin/src/companion.ts +1 -8
  5. package/opencode-plugin/src/server.ts +2 -15
  6. package/package.json +10 -2
  7. package/src/brand/art.generated.ts +69 -0
  8. package/src/brand/index.ts +12 -0
  9. package/src/brand/palette.ts +19 -0
  10. package/src/brand/svg.generated.ts +8 -0
  11. package/src/brand/terminal.ts +83 -0
  12. package/src/brand/types.ts +33 -0
  13. package/src/cli/commands/artifact/artifact.ts +8 -0
  14. package/src/cli/commands/artifact/delivery/archive.ts +117 -0
  15. package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
  16. package/src/cli/commands/artifact/delivery/environment.ts +104 -0
  17. package/src/cli/commands/artifact/delivery/link.ts +154 -0
  18. package/src/cli/commands/artifact/delivery/output.ts +58 -0
  19. package/src/cli/commands/artifact/finish/command.ts +33 -3
  20. package/src/cli/commands/artifact/finish/engine.ts +47 -3
  21. package/src/cli/commands/artifact/finish/finisher.ts +10 -0
  22. package/src/cli/commands/artifact/finish/openspec.ts +200 -129
  23. package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
  24. package/src/cli/commands/artifact/pending/command.ts +31 -3
  25. package/src/cli/commands/artifact/pending/discovery.ts +21 -57
  26. package/src/cli/commands/cap/graphify.ts +52 -3
  27. package/src/cli/commands/cap/index-cmd.ts +8 -1
  28. package/src/cli/commands/cap/openspec.ts +13 -25
  29. package/src/cli/commands/companion/open.ts +56 -39
  30. package/src/cli/commands/companion/update.ts +75 -42
  31. package/src/cli/commands/doctor.ts +257 -78
  32. package/src/cli/commands/install.ts +0 -8
  33. package/src/cli/commands/plugin/install.ts +9 -4
  34. package/src/cli/commands/plugin/plugin.ts +3 -2
  35. package/src/cli/commands/report/adapter.ts +11 -2
  36. package/src/cli/commands/report/contract.ts +21 -0
  37. package/src/cli/commands/report/highlight.ts +34 -4
  38. package/src/cli/commands/report/markdown.ts +110 -0
  39. package/src/cli/commands/report/palette.ts +118 -0
  40. package/src/cli/commands/report/renderer.ts +456 -153
  41. package/src/cli/commands/report/types.ts +16 -0
  42. package/src/cli/commands/setup.ts +7 -15
  43. package/src/cli/commands/studio/client-asset.ts +16 -0
  44. package/src/cli/commands/studio/openspec-cli.ts +10 -58
  45. package/src/cli/commands/studio/routes.ts +2 -0
  46. package/src/cli/commands/studio/selection.ts +19 -1
  47. package/src/cli/commands/studio/server.ts +42 -1
  48. package/src/cli/commands/studio/terminal-launch.ts +7 -16
  49. package/src/cli/commands/studio/views/client.ts +6 -616
  50. package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
  51. package/src/cli/commands/studio/views/document.tsx +137 -69
  52. package/src/cli/commands/studio/views/markdown.tsx +250 -0
  53. package/src/cli/commands/studio/views/palette.ts +134 -0
  54. package/src/cli/commands/studio/views/reports/index.tsx +2 -7
  55. package/src/cli/commands/studio/views/skills/index.tsx +43 -62
  56. package/src/cli/commands/studio/views/specs/index.tsx +7 -15
  57. package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
  58. package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
  59. package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
  60. package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
  61. package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
  62. package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
  63. package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
  64. package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
  65. package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
  66. package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
  67. package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
  68. package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
  69. package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
  70. package/src/cli/commands/studio/views/styles.ts +217 -206
  71. package/src/cli/commands/studio/views/vault/state.ts +3 -5
  72. package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
  73. package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
  74. package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
  75. package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
  76. package/src/cli/commands/wrap.ts +9 -42
  77. package/src/cli/main.ts +4 -1
  78. package/src/cli/parse-flags.ts +25 -7
  79. package/src/cli/setup-selector.tsx +38 -162
  80. package/src/cli/usage.ts +5 -2
  81. package/src/hooks/companion-instructions.ts +190 -0
  82. package/src/hooks/hook-io.ts +63 -0
  83. package/src/hooks/session-banner.ts +21 -29
  84. package/src/hooks/session-guidance.ts +10 -36
  85. package/src/hooks/session-prepare.ts +115 -28
  86. package/src/hooks/validate-artifact-path.ts +82 -24
  87. package/src/index.ts +4 -0
  88. package/src/lib/change-layout.ts +154 -0
  89. package/src/lib/components/wizard-header.tsx +3 -1
  90. package/src/lib/context-mode-package.ts +24 -11
  91. package/src/lib/delivery/archive-change.ts +154 -0
  92. package/src/lib/delivery/branches.ts +108 -0
  93. package/src/lib/delivery/environment.ts +29 -0
  94. package/src/lib/delivery/forge.ts +279 -0
  95. package/src/lib/delivery/overlap.ts +202 -0
  96. package/src/lib/delivery/record-delivery.ts +196 -0
  97. package/src/lib/delivery/record.ts +343 -0
  98. package/src/lib/delivery/refresh.ts +174 -0
  99. package/src/lib/delivery/repository-access.ts +97 -0
  100. package/src/lib/delivery/stages.ts +111 -0
  101. package/src/lib/delivery/token.ts +58 -0
  102. package/src/lib/frontmatter.ts +18 -0
  103. package/src/lib/opencode-plugin-package.ts +5 -5
  104. package/src/lib/openspec-cli.ts +116 -0
  105. package/src/lib/openspec-scopes.ts +86 -0
  106. package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
  107. package/src/lib/orchestrator/adapters/claude.ts +60 -162
  108. package/src/lib/orchestrator/adapters/opencode.ts +9 -24
  109. package/src/lib/orchestrator/config-store.ts +2 -70
  110. package/src/lib/orchestrator/editor.ts +9 -9
  111. package/src/lib/orchestrator/openspec-store.ts +14 -54
  112. package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
  113. package/src/lib/orchestrator/projection-claude-links.ts +159 -0
  114. package/src/lib/orchestrator/projection-claude-skills.ts +19 -190
  115. package/src/lib/orchestrator/projection-entries.ts +45 -15
  116. package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
  117. package/src/lib/orchestrator/projection-types.ts +3 -2
  118. package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
  119. package/src/lib/orchestrator/session-prepare.ts +49 -87
  120. package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
  121. package/src/lib/orchestrator/types.ts +2 -2
  122. package/src/lib/orchestrator/wrap-pass.ts +22 -88
  123. package/src/lib/package-paths.ts +8 -0
  124. package/src/opencode/companion-hooks.ts +64 -5
  125. package/src/opencode/companion-policy.ts +37 -18
  126. package/src/opencode/projected-guidance.ts +4 -1
  127. package/src/opencode/session-prepare.ts +2 -1
  128. package/src/opencode/tui.tsx +4 -2
  129. package/src/runtime/artifact-refusal.ts +63 -0
  130. package/src/runtime/claude-delivery-plan.ts +89 -7
  131. package/src/runtime/commit-attribution-guard.ts +181 -0
  132. package/src/runtime/companion-guidance.ts +15 -3
  133. package/src/runtime/companion-instructions.ts +340 -0
  134. package/src/runtime/error-message.ts +4 -0
  135. package/src/runtime/guidance.ts +0 -17
  136. package/src/runtime/index.ts +5 -0
  137. package/src/runtime/managed-block.ts +117 -0
  138. package/src/runtime/markdown-scan.ts +266 -0
  139. package/src/runtime/policy.ts +16 -15
  140. package/src/runtime/projected-flag.ts +7 -0
  141. package/src/runtime/projected-guidance.ts +1 -0
  142. package/src/runtime/runtime-document-manifest.ts +62 -28
  143. package/src/runtime/secret-guard.ts +148 -0
  144. package/src/runtime/secret-patterns.ts +164 -0
  145. package/src/runtime/secret-scan.ts +90 -0
  146. package/src/runtime/session-kind.ts +13 -0
  147. package/src/runtime/shell-heredoc.ts +81 -0
  148. package/src/runtime/wrap-state.ts +36 -6
  149. package/src/runtime/write-atomic.ts +64 -0
  150. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
  151. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
  152. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
  153. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  154. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
  155. package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
  156. package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
  157. package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
  158. package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
  159. package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
  160. package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
  161. package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
  162. package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
  163. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
  164. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
  165. package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
  166. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
  167. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
  168. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
  169. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  170. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
  171. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
  172. package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
  173. package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
  174. package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
  175. package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
  176. package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
  177. package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
  178. package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
  179. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
  180. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
  181. package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
  182. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
  183. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
  184. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
  185. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  186. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
  187. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
  188. package/src/templates/mate-skills/openspec-references.ts +140 -0
  189. package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
  190. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
  191. package/src/tools/setup/capabilities/context-mode.ts +25 -5
  192. package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
  193. package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
  194. package/src/tools/setup/capabilities/graphify.ts +12 -0
  195. package/src/tools/setup/capabilities/openspec.ts +273 -270
  196. package/src/tools/setup/capabilities/tokensave.ts +36 -3
  197. package/src/tools/setup/context-services.ts +9 -17
  198. package/src/tools/setup/dynamic-plugins/audiences.ts +7 -39
  199. package/src/tools/setup/dynamic-plugins/declarations.ts +1 -1
  200. package/src/tools/setup/dynamic-plugins/hydrate.ts +4 -7
  201. package/src/tools/setup/dynamic-plugins/install.ts +24 -28
  202. package/src/tools/setup/engine.ts +93 -148
  203. package/src/tools/setup/managed-output.ts +51 -0
  204. package/src/tools/setup/mate.ts +86 -11
  205. package/src/tools/setup/plugin.ts +37 -17
  206. package/src/tools/setup/plugins/gitignore.ts +1 -7
  207. package/src/tools/setup/plugins/guidance.ts +7 -4
  208. package/src/tools/setup/providers/agent-file-sections.ts +4 -2
  209. package/src/tools/setup/providers/claude-attribution.ts +65 -0
  210. package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
  211. package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
  212. package/src/tools/setup/providers/claude.ts +285 -34
  213. package/src/tools/setup/providers/opencode.ts +180 -11
  214. package/src/tools/setup/registry.ts +2 -24
  215. package/src/tools/setup/surface-target.ts +4 -13
  216. package/src/tools/setup/utils.ts +14 -4
  217. package/src/tools/setup.ts +131 -18
  218. package/wrappers/bin/graphify +52 -4
  219. package/wrappers/bin/openspec +33 -9
  220. package/wrappers/lib/graphify-gate.mjs +8 -0
  221. package/opencode-plugin/src/audience-layer.ts +0 -91
  222. package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
  223. package/src/runtime/audience-layer.ts +0 -84
  224. package/src/runtime/opencode-audience-layer.ts +0 -176
  225. package/src/tools/setup/audience-isolation.fixture.ts +0 -325
  226. package/src/tools/setup/audience-layer.ts +0 -207
  227. package/src/tools/setup/audience-registrations.ts +0 -52
  228. package/src/tools/setup/contribution-ledger.ts +0 -207
  229. package/src/tools/setup/isolation-diagnostics.ts +0 -139
  230. package/src/tools/setup/runtime-documents.ts +0 -291
  231. package/src/tools/setup/upgrade-sweep.ts +0 -153
@@ -1,8 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
- import { PROJECTED_BANNER_FLAG } from "../../../hooks/session-banner";
5
4
  import { getClaudePluginRoot } from "../../../lib/package-paths";
5
+ import { PROJECTED_FLAG } from "../../../runtime/projected-flag";
6
6
  import { runtimeClaudePluginRoot } from "../../../runtime/runtime-directory";
7
7
  import { hookGroupKey, type ClaudeHookGroup } from "./claude-format";
8
8
 
@@ -12,12 +12,11 @@ import { hookGroupKey, type ClaudeHookGroup } from "./claude-format";
12
12
  * all, so the working-target settings document has to carry it instead.
13
13
  *
14
14
  * A managed session loads the Working Repository's settings as its `local`
15
- * source *alongside* the plugin, so every group carried over runs twice there.
16
- * All three survive that: the guard, whose verdict is a function of the tool
17
- * input alone; the guidance hook, which yields nothing when a launch
18
- * environment is present and so cannot double the prompt the launch already
19
- * appended; and the banner, whose carried copy is marked so it defers to the
20
- * launch's.
15
+ * source *alongside* the plugin, so every group carried over runs twice there;
16
+ * a plain session may load both too, through `--plugin-dir`. The guard is
17
+ * input-pure, the guidance hook yields under a launch environment, and the
18
+ * banner, prepare and SubagentStart instruction copies carried here hold
19
+ * `PROJECTED_FLAG`, so one copy of each acts.
21
20
  *
22
21
  * The wiring is read from the installed plugin, but every command names
23
22
  * `<mate home>/runtime/claude-plugin`, so a reinstall or Node switch does not
@@ -28,6 +27,7 @@ const GUARD_HOOK_SHIM = "validate-artifact-path.mjs";
28
27
  const GUIDANCE_HOOK_SHIM = "session-guidance.mjs";
29
28
  const BANNER_HOOK_SHIM = "session-banner.mjs";
30
29
  const PREPARE_HOOK_SHIM = "session-prepare.mjs";
30
+ const INSTRUCTIONS_HOOK_SHIM = "companion-instructions.mjs";
31
31
  const PLUGIN_ROOT_TOKEN = "${CLAUDE_PLUGIN_ROOT}";
32
32
 
33
33
  interface PluginHookWiring {
@@ -46,16 +46,23 @@ function readPluginHookWiring(pluginRoot: string): PluginHookWiring {
46
46
  return {};
47
47
  }
48
48
 
49
- function resolveCommands(group: ClaudeHookGroup, pluginRoot: string): ClaudeHookGroup {
49
+ function mapCommands(
50
+ group: ClaudeHookGroup,
51
+ rewrite: (command: string) => string,
52
+ ): ClaudeHookGroup {
50
53
  return {
51
54
  ...group,
52
55
  hooks: (group.hooks ?? []).map((hook) => ({
53
56
  ...hook,
54
- ...(hook.command ? { command: hook.command.split(PLUGIN_ROOT_TOKEN).join(pluginRoot) } : {}),
57
+ ...(hook.command ? { command: rewrite(hook.command) } : {}),
55
58
  })),
56
59
  };
57
60
  }
58
61
 
62
+ function resolveCommands(group: ClaudeHookGroup, pluginRoot: string): ClaudeHookGroup {
63
+ return mapCommands(group, (command) => command.split(PLUGIN_ROOT_TOKEN).join(pluginRoot));
64
+ }
65
+
59
66
  function runsShim(group: ClaudeHookGroup, shim: string): boolean {
60
67
  return (group.hooks ?? []).some((hook) => (hook.command ?? "").includes(shim));
61
68
  }
@@ -90,31 +97,43 @@ export function mateGuidanceHookGroups(
90
97
 
91
98
  /**
92
99
  * The banner's `SessionStart` group, resolved the same way and carrying
93
- * `PROJECTED_BANNER_FLAG`. Without the marker the carried command would be
100
+ * `PROJECTED_FLAG`. Without the marker the carried command would be
94
101
  * byte-identical to the plugin's, and a managed session — which loads both —
95
102
  * would print the banner twice.
96
103
  */
97
104
  export function mateBannerHookGroups(
98
105
  commandRoot: string = runtimeClaudePluginRoot(),
99
106
  ): ClaudeHookGroup[] {
100
- return groupsFor("SessionStart", BANNER_HOOK_SHIM, commandRoot).map((group) => ({
101
- ...group,
102
- hooks: (group.hooks ?? []).map((hook) => ({
103
- ...hook,
104
- ...(hook.command ? { command: `${hook.command} ${PROJECTED_BANNER_FLAG}` } : {}),
105
- })),
106
- }));
107
+ return withProjectedFlag(groupsFor("SessionStart", BANNER_HOOK_SHIM, commandRoot));
108
+ }
109
+
110
+ function withProjectedFlag(groups: ClaudeHookGroup[]): ClaudeHookGroup[] {
111
+ return groups.map((group) => mapCommands(group, (command) => `${command} ${PROJECTED_FLAG}`));
112
+ }
113
+
114
+ /**
115
+ * The `SubagentStart` instruction group, resolved the same way and carrying
116
+ * `PROJECTED_FLAG`: a session that loads both this copy and the plugin's,
117
+ * through a launch or `--plugin-dir`, would otherwise deliver the instructions
118
+ * twice.
119
+ */
120
+ export function mateInstructionHookGroups(
121
+ commandRoot: string = runtimeClaudePluginRoot(),
122
+ ): ClaudeHookGroup[] {
123
+ return withProjectedFlag(groupsFor("SubagentStart", INSTRUCTIONS_HOOK_SHIM, commandRoot));
107
124
  }
108
125
 
109
126
  /**
110
127
  * The session-prepare `SessionStart` group, resolved the same way. Its declared
111
128
  * `timeout` sits above the shim's own bound, so Claude Code never kills a
112
- * prepare the shim has not given up on. Inert under a launch environment.
129
+ * prepare the shim has not given up on. It carries `PROJECTED_FLAG`: this copy
130
+ * alone prepares, which it skips under a launch environment, and then delivers
131
+ * the main thread's instructions; the plugin's copy stays inert.
113
132
  */
114
133
  export function mateSessionPrepareHookGroups(
115
134
  commandRoot: string = runtimeClaudePluginRoot(),
116
135
  ): ClaudeHookGroup[] {
117
- return groupsFor("SessionStart", PREPARE_HOOK_SHIM, commandRoot);
136
+ return withProjectedFlag(groupsFor("SessionStart", PREPARE_HOOK_SHIM, commandRoot));
118
137
  }
119
138
 
120
139
  function mateOwnHookGroups(commandRoot: string): ClaudeHookGroup[] {
@@ -123,6 +142,7 @@ function mateOwnHookGroups(commandRoot: string): ClaudeHookGroup[] {
123
142
  ...mateGuidanceHookGroups(commandRoot),
124
143
  ...mateBannerHookGroups(commandRoot),
125
144
  ...mateSessionPrepareHookGroups(commandRoot),
145
+ ...mateInstructionHookGroups(commandRoot),
126
146
  ];
127
147
  }
128
148
 
@@ -4,6 +4,17 @@ import path from "node:path";
4
4
 
5
5
  import { FRAMEWORK_NAME } from "../../../framework";
6
6
  import type { FrameworkConfig } from "../../../lib/orchestrator/types";
7
+ import { getClaudePluginRoot } from "../../../lib/package-paths";
8
+ import { writeAtomicThroughLink } from "../../../runtime/write-atomic";
9
+ import { resolveSecretPolicy } from "../../../runtime/policy";
10
+ import {
11
+ CLAUDE_LOCAL_CONFIG_DOCUMENT,
12
+ CLAUDE_SETTINGS_DOCUMENT,
13
+ renderedDocument,
14
+ type ManagedRegion,
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 {
9
20
  instructionBlockKey,
@@ -11,9 +22,19 @@ import {
11
22
  upsertManagedBlock,
12
23
  } from "../context-services";
13
24
  import type { CapabilityContributionInput, ProviderPlugin, SetupContext } from "../plugin";
14
- import { projectionTarget, surfaceRoot } from "../surface-target";
25
+ import { projectionRepoRoot, surfaceRoot } from "../surface-target";
15
26
  import { mergeDir, pruneEmptyAncestors } from "../utils";
16
27
  import { stripSectionFromFile, type RemoveHeadingSectionOptions } from "./agent-file-sections";
28
+ import {
29
+ claudeUserConfigDir,
30
+ definesUserClaudeAttribution,
31
+ isManagedClaudeAttribution,
32
+ MANAGED_CLAUDE_ATTRIBUTION,
33
+ } from "./claude-attribution";
34
+ import {
35
+ inspectClaudePluginFolders,
36
+ type PublishedClaudePluginFolder,
37
+ } from "./claude-plugin-folders";
17
38
  import { patchSkillTreeMarkdownFiles } from "./skill-tree";
18
39
  import {
19
40
  filterClaudeHookGroups,
@@ -28,6 +49,13 @@ import {
28
49
  type ClaudeHookGroup,
29
50
  type ClaudeSettings,
30
51
  } from "./claude-format";
52
+ import {
53
+ mateBannerHookGroups,
54
+ mateGuardHookGroups,
55
+ mateGuidanceHookGroups,
56
+ mateInstructionHookGroups,
57
+ mateSessionPrepareHookGroups,
58
+ } from "./claude-plugin-hooks";
31
59
  import { getSetupRootTemplates } from "./utils";
32
60
 
33
61
  async function configureClaudeGuidance(companionPath: string): Promise<void> {
@@ -106,6 +134,54 @@ function getAllManagedPermissionEntries(companionPath: string): Set<string> {
106
134
  ]);
107
135
  }
108
136
 
137
+ /**
138
+ * Claude Code features Mate turns off because they compete with its own
139
+ * workflow: plan mode writes plans outside the Companion Repository, the
140
+ * Artifact tool competes with `mate report`, and DesignSync has no role in a
141
+ * Mate session. `ExitPlanMode` stays allowed so a user who enters plan mode
142
+ * by hand can still leave it. Fixed, not configurable.
143
+ */
144
+ export const MANAGED_CLAUDE_DENY_ENTRIES: readonly string[] = ["EnterPlanMode", "DesignSync"];
145
+
146
+ const SECRET_DENY_TOOLS = ["Read", "Edit"] as const;
147
+
148
+ /**
149
+ * Claude deny rules for the default secret files, at any depth. Claude checks
150
+ * deny before allow, so only patterns that can't match an allow-listed file are
151
+ * rendered; the hook enforces the rest of the pattern set. Claude applies
152
+ * `Edit(...)` rules to every built-in file-editing tool, `Write` included, so
153
+ * no `Write(...)` rule is rendered: Claude never matches one and warns about it.
154
+ */
155
+ export function renderSecretDenyEntries(): string[] {
156
+ return projectableSecretDenyPatterns().flatMap((pattern) =>
157
+ SECRET_DENY_TOOLS.map((tool) => `${tool}(**/${pattern})`),
158
+ );
159
+ }
160
+
161
+ /** The managed `permissions.deny` list: fixed tool denies, plus secret files while guardrails are on. */
162
+ function renderManagedClaudeDenyEntries(companionPath: string): string[] {
163
+ return [
164
+ ...MANAGED_CLAUDE_DENY_ENTRIES,
165
+ ...(resolveSecretPolicy(companionPath).guardrails ? renderSecretDenyEntries() : []),
166
+ ];
167
+ }
168
+
169
+ /**
170
+ * Every deny entry Mate may have written, whatever the current config says, so
171
+ * an opt-out strips the secret entries instead of keeping them as user entries.
172
+ */
173
+ function getAllManagedDenyEntries(): Set<string> {
174
+ return new Set([
175
+ ...MANAGED_CLAUDE_DENY_ENTRIES,
176
+ ...renderSecretDenyEntries(),
177
+ // Legacy secret entries: Claude Code never matched Write() rules for file
178
+ // permission checks and warns about them, so setup no longer emits them.
179
+ // Keep them managed so re-running setup strips them from existing installs.
180
+ ...projectableSecretDenyPatterns().map((pattern) => `Write(**/${pattern})`),
181
+ ]);
182
+ }
183
+ const MANAGED_CLAUDE_FORCED_SETTINGS = { enableArtifact: false } as const;
184
+
109
185
  /** Companion-owned Claude settings: sole source of Mate-managed hooks, permissions and MCP servers. */
110
186
  export function getCompanionClaudeSettingsPath(companionPath: string): string {
111
187
  return path.join(companionPath, ".claude", "settings.local.json");
@@ -121,7 +197,7 @@ export function getCompanionClaudeMcpConfigPath(companionPath: string): string {
121
197
  * Rendered separately from the settings document so a working-target pass can
122
198
  * obtain them without a write.
123
199
  */
124
- export function renderManagedClaudePermissionEntries(
200
+ function renderManagedClaudePermissionEntries(
125
201
  companionPath: string,
126
202
  contributions: CapabilityContributionInput[] = [],
127
203
  ): string[] {
@@ -134,7 +210,7 @@ export function renderManagedClaudePermissionEntries(
134
210
  }
135
211
 
136
212
  /** The Mate-managed `.mcp.json` server map of one pass, as a value. */
137
- export function renderManagedClaudeMcpServers(
213
+ function renderManagedClaudeMcpServers(
138
214
  contributions: CapabilityContributionInput[] = [],
139
215
  ): Record<string, unknown> {
140
216
  const servers: Record<string, unknown> = {};
@@ -151,7 +227,7 @@ export function renderManagedClaudeMcpServers(
151
227
  * Enabled Capabilities' Claude hook groups per event, in declared order, as a
152
228
  * value: what a Claude wrap carries into the Working Repository.
153
229
  */
154
- export function renderManagedClaudeHookGroups(
230
+ function renderManagedClaudeHookGroups(
155
231
  contributions: CapabilityContributionInput[] = [],
156
232
  ): Record<string, ClaudeHookGroup[]> {
157
233
  const hooks: Record<string, ClaudeHookGroup[]> = {};
@@ -164,23 +240,6 @@ export function renderManagedClaudeHookGroups(
164
240
  return hooks;
165
241
  }
166
242
 
167
- /**
168
- * A layer's Claude settings: the enabled inputs' hook groups and permission
169
- * entries, nothing of Mate's own. The launch merges them over its base settings.
170
- */
171
- export function renderLayerClaudeSettings(
172
- contributions: CapabilityContributionInput[] = [],
173
- ): ClaudeSettings {
174
- const hooks = renderManagedClaudeHookGroups(contributions);
175
- const allow = contributions
176
- .filter((input) => input.enabled)
177
- .flatMap((input) => input.contributions.permissionEntries ?? []);
178
- return {
179
- ...(Object.keys(hooks).length > 0 ? { hooks } : {}),
180
- ...(allow.length > 0 ? { permissions: { allow } } : {}),
181
- };
182
- }
183
-
184
243
  // Reconcile the Mate-managed Claude settings shape (hooks, permissions, MCP
185
244
  // servers) on top of whatever already exists in the settings document. Managed
186
245
  // groups/entries always lead their arrays so the emitted shape stays stable
@@ -190,6 +249,7 @@ function renderManagedClaudeSettings(
190
249
  companionPath: string,
191
250
  config: FrameworkConfig,
192
251
  contributions: CapabilityContributionInput[] = [],
252
+ userDefinesAttribution: boolean,
193
253
  ): ClaudeSettings {
194
254
  // Declared markers and permission entries widen the managed strip set for
195
255
  // every registered Capability, enabled or not, so deselection tears down.
@@ -238,6 +298,13 @@ function renderManagedClaudeSettings(
238
298
  } else {
239
299
  delete permissions.allow;
240
300
  }
301
+ // permissions.deny: same reconciliation as allow, against the fixed set.
302
+ const managedDeny = getAllManagedDenyEntries();
303
+ const existingDeny = Array.isArray(existingPermissions.deny) ? existingPermissions.deny : [];
304
+ permissions.deny = [
305
+ ...existingDeny.filter((entry) => !managedDeny.has(entry)),
306
+ ...renderManagedClaudeDenyEntries(companionPath),
307
+ ];
241
308
 
242
309
  const mcpServers = { ...existing.mcpServers };
243
310
  // tokensave (and any future Mate-managed MCP server) now lives in the
@@ -245,7 +312,12 @@ function renderManagedClaudeSettings(
245
312
  // entry is removed so it cannot shadow the `.mcp.json` definition.
246
313
  delete mcpServers.tokensave;
247
314
 
248
- const settings: ClaudeSettings = { ...existing, hooks, autoMemoryEnabled: false };
315
+ const settings: ClaudeSettings = {
316
+ ...existing,
317
+ hooks,
318
+ autoMemoryEnabled: false,
319
+ ...MANAGED_CLAUDE_FORCED_SETTINGS,
320
+ };
249
321
  if (Object.keys(hooks).length === 0) {
250
322
  delete settings.hooks;
251
323
  }
@@ -259,10 +331,190 @@ function renderManagedClaudeSettings(
259
331
  } else {
260
332
  delete settings.mcpServers;
261
333
  }
334
+ /** The document's own value is kept; Mate's yields to one the user scope sets. */
335
+ if (!("attribution" in existing) || isManagedClaudeAttribution(existing.attribution)) {
336
+ if (userDefinesAttribution) delete settings.attribution;
337
+ else settings.attribution = { ...MANAGED_CLAUDE_ATTRIBUTION };
338
+ }
262
339
 
263
340
  return settings;
264
341
  }
265
342
 
343
+ /**
344
+ * Loaded by a managed session beside the launch's plugin, so each Mate hook
345
+ * here runs twice there: the guard is input-pure, guidance yields under a
346
+ * launch env, and the banner, prepare and instruction copies carry
347
+ * `--projected`, so one copy of each acts. Capability groups run once: the
348
+ * launch omits them from `--settings`.
349
+ *
350
+ * `permissions.additionalDirectories` grants companion file access only; it
351
+ * loads no `CLAUDE.md`.
352
+ *
353
+ * Unlike {@link renderManagedClaudeSettings}, this carries no
354
+ * `autoMemoryEnabled: false`. Whether a Wrap should leave the user's choice
355
+ * alone there is an open question, not settled policy.
356
+ */
357
+ function claudeSettingsRegions(
358
+ ctx: SetupContext,
359
+ inputs: CapabilityContributionInput[],
360
+ repoRoot: string,
361
+ pluginFolders: readonly PublishedClaudePluginFolder[],
362
+ ): ManagedRegion[] {
363
+ const capabilityHooks = renderManagedClaudeHookGroups(inputs);
364
+ const mateHooks: Record<string, unknown[]> = {
365
+ PreToolUse: mateGuardHookGroups(),
366
+ SessionStart: [
367
+ ...mateBannerHookGroups(),
368
+ ...mateGuidanceHookGroups(),
369
+ ...mateSessionPrepareHookGroups(),
370
+ ],
371
+ SubagentStart: mateInstructionHookGroups(),
372
+ };
373
+ const events = [...new Set([...Object.keys(mateHooks), ...Object.keys(capabilityHooks)])];
374
+ return [
375
+ ...events.map((event): ManagedRegion => ({
376
+ at: ["hooks", event],
377
+ kind: "list",
378
+ values: [...(mateHooks[event] ?? []), ...(capabilityHooks[event] ?? [])],
379
+ })),
380
+ /**
381
+ * `stripPrefix` heals a repository an older Mate wrapped. That version wrote
382
+ * the whole launch environment here, and Claude Code applies `env` to the
383
+ * session, so every hook in a bare session inherited `MATE_*` and read the
384
+ * repository as a Managed Session — silently, since looking managed is
385
+ * exactly what makes the guidance hook emit nothing. Guidance must be the
386
+ * projection's to give in an Unmanaged Session, so no `MATE_*` key may
387
+ * survive here. It sets no entry of its own: the
388
+ * `CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD` an earlier wrap set is
389
+ * reverted from the manifest, so a user's own value stays.
390
+ */
391
+ {
392
+ at: ["env"],
393
+ kind: "map",
394
+ entries: {},
395
+ stripPrefix: "MATE_",
396
+ },
397
+ {
398
+ at: ["permissions", "allow"],
399
+ kind: "list",
400
+ values: renderManagedClaudePermissionEntries(ctx.companionPath, inputs),
401
+ },
402
+ {
403
+ at: ["permissions", "deny"],
404
+ kind: "list",
405
+ values: renderManagedClaudeDenyEntries(ctx.companionPath),
406
+ },
407
+ ...claudePluginRegions(pluginFolders),
408
+ /**
409
+ * Yields: the document is the user's, so their own `enableArtifact` wins,
410
+ * and unwrapping restores it instead of deleting a key Mate overwrote.
411
+ */
412
+ ...Object.entries(MANAGED_CLAUDE_FORCED_SETTINGS).map(([key, value]): ManagedRegion => ({
413
+ at: [key],
414
+ kind: "value",
415
+ value,
416
+ yields: true,
417
+ })),
418
+ /**
419
+ * Placed only while no scope sets attribution: a project-scope value would
420
+ * lose to this local one, yielding or not. Mate's own earlier value does
421
+ * not count as the user's.
422
+ */
423
+ ...(definesUserClaudeAttribution({ repoRoot })
424
+ ? []
425
+ : [
426
+ {
427
+ at: ["attribution"],
428
+ kind: "value",
429
+ value: { ...MANAGED_CLAUDE_ATTRIBUTION },
430
+ yields: true,
431
+ } satisfies ManagedRegion,
432
+ ]),
433
+ ];
434
+ }
435
+
436
+ /**
437
+ * Each published plugin folder as a local `directory` marketplace and its one
438
+ * enabled plugin, so a plain session in a trusted folder loads it in place. The
439
+ * path is the folder as the launch passes it, so the wrap state can name what
440
+ * the launch must leave to the wrap.
441
+ */
442
+ function claudePluginRegions(published: readonly PublishedClaudePluginFolder[]): ManagedRegion[] {
443
+ return [
444
+ {
445
+ at: ["extraKnownMarketplaces"],
446
+ kind: "map",
447
+ entries: Object.fromEntries(
448
+ published.map(({ path: folder, marketplace }) => [
449
+ marketplace,
450
+ { source: { source: "directory", path: folder } },
451
+ ]),
452
+ ),
453
+ },
454
+ {
455
+ at: ["enabledPlugins"],
456
+ kind: "map",
457
+ entries: Object.fromEntries(
458
+ published.map(({ plugin, marketplace }) => [`${plugin}@${marketplace}`, true]),
459
+ ),
460
+ },
461
+ ];
462
+ }
463
+
464
+ /**
465
+ * The working target's Claude documents, rendered as values. Each is a document
466
+ * the runtime finds by walking up from the current directory, and every path
467
+ * inside one still names the Companion Repository — only where the document is
468
+ * placed differs from the companion target.
469
+ */
470
+ export async function renderClaudeWorkingDocuments(
471
+ ctx: SetupContext,
472
+ inputs: CapabilityContributionInput[],
473
+ ): Promise<RenderedRuntimeDocuments> {
474
+ const repoRoot = projectionRepoRoot(ctx);
475
+ /** Reads the folders only; the pass published them before this render. */
476
+ const pluginFolders = (
477
+ await inspectClaudePluginFolders(inputs, { bundledPluginRoot: getClaudePluginRoot() })
478
+ ).published;
479
+ return {
480
+ documents: [
481
+ ...renderedDocument(
482
+ CLAUDE_SETTINGS_DOCUMENT,
483
+ claudeSettingsRegions(ctx, inputs, repoRoot, pluginFolders),
484
+ ),
485
+ /**
486
+ * Local scope, not `.mcp.json`: a project-scoped server is inert until a
487
+ * human approves it in a session, which an Unmanaged Session is exactly the
488
+ * case that cannot rely on. The repo path is the key Claude Code files a
489
+ * project under, resolved so a symlinked working directory still matches.
490
+ *
491
+ * Confirmed against a real `~/.claude.json`: the keys there are the
492
+ * directories sessions started in, not repository roots — sibling
493
+ * directories of one checkout each hold their own entry, as do temporary
494
+ * directories. So this reaches exactly one session: the one started at the
495
+ * repository root. A session started in a subdirectory of a wrapped
496
+ * repository has its own key, finds no `mcpServers` under it, and sees none
497
+ * of these servers — the Unmanaged Session case this document exists for,
498
+ * missed. That limit is deliberate. The directories a session might start
499
+ * in are unbounded, and every one of them would be a fresh entry in the
500
+ * user's global configuration that nothing later reclaims; keying the
501
+ * repository root only keeps the projection's footprint there to a single
502
+ * key. A subdirectory session still gets the rest of the projection — the
503
+ * documents inside the repository are found by the runtime's own upward
504
+ * walk — it loses only the MCP servers.
505
+ */
506
+ ...renderedDocument(CLAUDE_LOCAL_CONFIG_DOCUMENT, [
507
+ {
508
+ at: ["projects", repoRoot, "mcpServers"],
509
+ kind: "map",
510
+ entries: renderManagedClaudeMcpServers(inputs),
511
+ },
512
+ ]),
513
+ ],
514
+ warnings: [],
515
+ };
516
+ }
517
+
266
518
  // Write the Mate-managed Claude settings into the companion repo. This is the
267
519
  // config surface Mate owns and loads at launch via `--settings`, so hooks,
268
520
  // permissions, and MCP servers no longer need to live in the working repo.
@@ -270,10 +522,17 @@ export async function syncCompanionClaudeSettings(
270
522
  companionPath: string,
271
523
  config: FrameworkConfig,
272
524
  contributions: CapabilityContributionInput[] = [],
525
+ env: Record<string, string | undefined> = process.env,
273
526
  ): Promise<void> {
274
527
  const settingsPath = getCompanionClaudeSettingsPath(companionPath);
275
528
  const existing = await readClaudeSettings(settingsPath);
276
- const settings = renderManagedClaudeSettings(existing, companionPath, config, contributions);
529
+ const settings = renderManagedClaudeSettings(
530
+ existing,
531
+ companionPath,
532
+ config,
533
+ contributions,
534
+ definesUserClaudeAttribution({ userConfigDir: claudeUserConfigDir(env) }),
535
+ );
277
536
 
278
537
  await writeClaudeSettings(settingsPath, settings);
279
538
 
@@ -304,15 +563,7 @@ export async function reconcileClaudeContributions(
304
563
  // The settings sync (re)creates the managed settings document, so it only
305
564
  // runs while the Claude runtime is active; a deactivated runtime's pass is
306
565
  // teardown-only and must not resurrect files the provider teardown removed.
307
- // A layer carries its plugins' hooks and permission entries only, for the
308
- // launch to merge into its one `--settings` file.
309
- if (projectionTarget(ctx) === "audience-layer") {
310
- await writeClaudeSettings(
311
- getCompanionClaudeSettingsPath(root),
312
- renderLayerClaudeSettings(inputs),
313
- );
314
- await syncCompanionClaudeMcpConfig(root);
315
- } else if (ctx.activeProviders.includes("claude")) {
566
+ if (ctx.activeProviders.includes("claude")) {
316
567
  await syncCompanionClaudeSettings(root, config, inputs);
317
568
  }
318
569
 
@@ -420,7 +671,7 @@ async function configureClaude(companionPath: string): Promise<void> {
420
671
  try {
421
672
  await fs.access(agentsMdDest);
422
673
  } catch {
423
- await fs.copyFile(agentsMdSrc, agentsMdDest);
674
+ await writeAtomicThroughLink(agentsMdDest, await fs.readFile(agentsMdSrc));
424
675
  }
425
676
 
426
677
  const claudeMdSrc = path.join(getSetupRootTemplates(), "TEMPLATE_CLAUDE.md");
@@ -428,7 +679,7 @@ async function configureClaude(companionPath: string): Promise<void> {
428
679
  try {
429
680
  await fs.access(claudeMdDest);
430
681
  } catch {
431
- await fs.copyFile(claudeMdSrc, claudeMdDest);
682
+ await writeAtomicThroughLink(claudeMdDest, await fs.readFile(claudeMdSrc));
432
683
  }
433
684
  }
434
685