@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,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 { 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[]> = {};
@@ -173,6 +249,7 @@ function renderManagedClaudeSettings(
173
249
  companionPath: string,
174
250
  config: FrameworkConfig,
175
251
  contributions: CapabilityContributionInput[] = [],
252
+ userDefinesAttribution: boolean,
176
253
  ): ClaudeSettings {
177
254
  // Declared markers and permission entries widen the managed strip set for
178
255
  // every registered Capability, enabled or not, so deselection tears down.
@@ -221,6 +298,13 @@ function renderManagedClaudeSettings(
221
298
  } else {
222
299
  delete permissions.allow;
223
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
+ ];
224
308
 
225
309
  const mcpServers = { ...existing.mcpServers };
226
310
  // tokensave (and any future Mate-managed MCP server) now lives in the
@@ -228,7 +312,12 @@ function renderManagedClaudeSettings(
228
312
  // entry is removed so it cannot shadow the `.mcp.json` definition.
229
313
  delete mcpServers.tokensave;
230
314
 
231
- const settings: ClaudeSettings = { ...existing, hooks, autoMemoryEnabled: false };
315
+ const settings: ClaudeSettings = {
316
+ ...existing,
317
+ hooks,
318
+ autoMemoryEnabled: false,
319
+ ...MANAGED_CLAUDE_FORCED_SETTINGS,
320
+ };
232
321
  if (Object.keys(hooks).length === 0) {
233
322
  delete settings.hooks;
234
323
  }
@@ -242,10 +331,190 @@ function renderManagedClaudeSettings(
242
331
  } else {
243
332
  delete settings.mcpServers;
244
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
+ }
245
339
 
246
340
  return settings;
247
341
  }
248
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
+
249
518
  // Write the Mate-managed Claude settings into the companion repo. This is the
250
519
  // config surface Mate owns and loads at launch via `--settings`, so hooks,
251
520
  // permissions, and MCP servers no longer need to live in the working repo.
@@ -253,10 +522,17 @@ export async function syncCompanionClaudeSettings(
253
522
  companionPath: string,
254
523
  config: FrameworkConfig,
255
524
  contributions: CapabilityContributionInput[] = [],
525
+ env: Record<string, string | undefined> = process.env,
256
526
  ): Promise<void> {
257
527
  const settingsPath = getCompanionClaudeSettingsPath(companionPath);
258
528
  const existing = await readClaudeSettings(settingsPath);
259
- 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
+ );
260
536
 
261
537
  await writeClaudeSettings(settingsPath, settings);
262
538
 
@@ -395,7 +671,7 @@ async function configureClaude(companionPath: string): Promise<void> {
395
671
  try {
396
672
  await fs.access(agentsMdDest);
397
673
  } catch {
398
- await fs.copyFile(agentsMdSrc, agentsMdDest);
674
+ await writeAtomicThroughLink(agentsMdDest, await fs.readFile(agentsMdSrc));
399
675
  }
400
676
 
401
677
  const claudeMdSrc = path.join(getSetupRootTemplates(), "TEMPLATE_CLAUDE.md");
@@ -403,7 +679,7 @@ async function configureClaude(companionPath: string): Promise<void> {
403
679
  try {
404
680
  await fs.access(claudeMdDest);
405
681
  } catch {
406
- await fs.copyFile(claudeMdSrc, claudeMdDest);
682
+ await writeAtomicThroughLink(claudeMdDest, await fs.readFile(claudeMdSrc));
407
683
  }
408
684
  }
409
685