@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
@@ -0,0 +1,190 @@
1
+ /** Delivers the Companion Instructions to a Claude main thread or subagent. */
2
+ import { errorMessage } from "../runtime/error-message";
3
+ import { planClaudeInstructions } from "../runtime/claude-delivery-plan";
4
+ import {
5
+ COMPANION_INSTRUCTIONS_BUDGET,
6
+ buildCompanionInstructions,
7
+ companionInstructionsUnavailableLine,
8
+ type CompanionInstructionsInput,
9
+ type InstructionReader,
10
+ } from "../runtime/companion-instructions";
11
+ import { MATE_ENV, hasLaunchEnvironment } from "../runtime/env";
12
+ import { FRAMEWORK_NAME } from "../runtime/framework";
13
+ import { PROJECTED_FLAG } from "../runtime/projected-flag";
14
+ import { resolveProjection } from "../runtime/projection";
15
+ import { managedSessionKind, type SessionKind } from "../runtime/session-kind";
16
+ import { hookOutcome, runHook, type HookCall, type HookEnv } from "./hook-io";
17
+
18
+ export interface InstructionHookPayload {
19
+ hookSpecificOutput?: { hookEventName: InstructionEvent; additionalContext: string };
20
+ systemMessage?: string;
21
+ }
22
+
23
+ export interface InstructionHookInput {
24
+ hook_event_name?: string;
25
+ agent_type?: string;
26
+ cwd?: string;
27
+ }
28
+
29
+ /** A hook run, with the shim arguments already read. */
30
+ export interface InstructionHookCall extends Omit<HookCall, "argv"> {
31
+ /** The copy a wrap carries, marked `--projected`. */
32
+ projected: boolean;
33
+ /** Where the projection resolves; the event's `cwd` when unset. */
34
+ directory?: string;
35
+ }
36
+
37
+ export interface InstructionHookDeps {
38
+ read?: InstructionReader;
39
+ resolveProjection?: typeof resolveProjection;
40
+ }
41
+
42
+ /**
43
+ * Agents never given a copy, since Claude Code's matchers cannot exclude them:
44
+ * Explore and Plan skip project memory by design, and a fork inherits its
45
+ * parent's context, which already holds the instructions.
46
+ */
47
+ const SKIPPED_AGENTS = new Set(["Explore", "Plan", "fork"]);
48
+
49
+ type InstructionEvent = "SessionStart" | "SubagentStart";
50
+
51
+ const INPUT_FIELDS = ["hook_event_name", "agent_type", "cwd"] as const;
52
+
53
+ function normalizeInput(raw: unknown): InstructionHookInput {
54
+ const input: InstructionHookInput = {};
55
+ if (!raw || typeof raw !== "object") return input;
56
+ for (const field of INPUT_FIELDS) {
57
+ const value = (raw as Record<string, unknown>)[field];
58
+ if (typeof value === "string") input[field] = value;
59
+ }
60
+ return input;
61
+ }
62
+
63
+ function sessionKind(env: HookEnv): SessionKind {
64
+ if (!hasLaunchEnvironment(env)) return "unmanaged";
65
+ return managedSessionKind(Boolean(env[MATE_ENV.repositoryPath]));
66
+ }
67
+
68
+ /**
69
+ * A copy runs only because the bundled plugin or a Claude wrap installed it, so
70
+ * a session with no launch environment is planned as Claude-wrapped without
71
+ * reading the Checkout's manifest: a linked worktree runs the main checkout's
72
+ * wrapped settings and has none of its own.
73
+ */
74
+ function ownsEvent(event: InstructionEvent, kind: SessionKind, projected: boolean): boolean {
75
+ const owners = planClaudeInstructions(kind, true);
76
+ if (event === "SessionStart") return owners.main === "wrap-session-hook" && projected;
77
+ if (owners.subagents === "plugin-subagent-hook") return !projected;
78
+ if (owners.subagents === "wrap-subagent-hook") return projected;
79
+ return false;
80
+ }
81
+
82
+ /** A launch sets every path; a partial one is not completed from a projection, which may name another repository. */
83
+ function launchReaderInput(env: HookEnv): CompanionInstructionsInput | null {
84
+ const companionPath = env[MATE_ENV.companionPath];
85
+ const id = env[MATE_ENV.repositoryId];
86
+ const repoPath = env[MATE_ENV.repositoryPath];
87
+ const wrapperBinPath = env[MATE_ENV.wrapperBinPath];
88
+ if (!companionPath || !id || !repoPath || !wrapperBinPath) return null;
89
+ return { companionPath, repository: { id, path: repoPath }, wrapperBinPath };
90
+ }
91
+
92
+ /** The environment always wins: a managed session never reads the projection. */
93
+ function readerInput(
94
+ env: HookEnv,
95
+ kind: SessionKind,
96
+ cwd: string,
97
+ resolve: typeof resolveProjection,
98
+ ): CompanionInstructionsInput | null {
99
+ if (kind !== "unmanaged") return launchReaderInput(env);
100
+ const projection = resolve(cwd);
101
+ if (!projection) return null;
102
+ return {
103
+ companionPath: projection.companionPath,
104
+ /** A linked worktree on a projection copied from the main checkout names itself, not that checkout. */
105
+ repository: { id: projection.repositoryId, path: projection.repoRoot },
106
+ wrapperBinPath: projection.wrapperBinPath,
107
+ };
108
+ }
109
+
110
+ function oversizeMessage(source: string): string {
111
+ const marker = FRAMEWORK_NAME.toUpperCase();
112
+ const budget = COMPANION_INSTRUCTIONS_BUDGET.toLocaleString("en-US");
113
+ return `${FRAMEWORK_NAME}: the ${marker}:COMPANION block in ${source} leaves no room within the ${budget}-character budget, so the instructions exceed it; Claude Code may deliver them as a file preview. Shorten the block.`;
114
+ }
115
+
116
+ function evaluate(
117
+ { env, cwd, projected, directory }: InstructionHookCall,
118
+ input: InstructionHookInput,
119
+ deps: InstructionHookDeps,
120
+ ): InstructionHookPayload | null {
121
+ const resolve = deps.resolveProjection ?? resolveProjection;
122
+ const read = deps.read ?? buildCompanionInstructions;
123
+ const event = input.hook_event_name;
124
+ if (event !== "SessionStart" && event !== "SubagentStart") return null;
125
+ if (event === "SubagentStart" && SKIPPED_AGENTS.has(input.agent_type ?? "")) return null;
126
+
127
+ const kind = sessionKind(env);
128
+ if (!ownsEvent(event, kind, projected)) return null;
129
+
130
+ const reader = readerInput(env, kind, directory || input.cwd || cwd, resolve);
131
+ if (!reader) return null;
132
+
133
+ const result = read(reader);
134
+ if (result.kind === "unavailable") {
135
+ const line = companionInstructionsUnavailableLine(result);
136
+ return instructionOutput(event, line, line);
137
+ }
138
+ const warning = result.oversized ? oversizeMessage(result.source) : undefined;
139
+ return instructionOutput(event, result.text, warning);
140
+ }
141
+
142
+ function instructionOutput(
143
+ event: InstructionEvent,
144
+ additionalContext: string,
145
+ systemMessage?: string,
146
+ ): InstructionHookPayload {
147
+ return {
148
+ hookSpecificOutput: { hookEventName: event, additionalContext },
149
+ ...(systemMessage ? { systemMessage } : {}),
150
+ };
151
+ }
152
+
153
+ function failure(error: unknown): InstructionHookPayload {
154
+ return {
155
+ systemMessage: `${FRAMEWORK_NAME}: companion instructions hook failed: ${errorMessage(error)}`,
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Its own hook output, apart from the Agent Guidance's: Claude Code caps each
161
+ * output at 10,000 characters. A missing `AGENTS.md` is stated as a fact, never
162
+ * as an instruction to stop: a SessionStart hook cannot block, and a pause
163
+ * would stall a `-p` or SDK session.
164
+ */
165
+ export function instructionHookPayload(
166
+ call: InstructionHookCall,
167
+ deps: InstructionHookDeps = {},
168
+ ): InstructionHookPayload | null {
169
+ try {
170
+ return evaluate(call, normalizeInput(call.input), deps);
171
+ } catch (error) {
172
+ return failure(error);
173
+ }
174
+ }
175
+
176
+ /** Plugin-shim entry; exits zero even when reading its input fails. */
177
+ export function run(argv: readonly string[] = process.argv.slice(2)): Promise<number> {
178
+ return runHook(
179
+ (input) =>
180
+ hookOutcome(
181
+ instructionHookPayload({
182
+ env: process.env,
183
+ cwd: process.cwd(),
184
+ projected: argv.includes(PROJECTED_FLAG),
185
+ input,
186
+ }),
187
+ ),
188
+ (error) => hookOutcome(failure(error)),
189
+ );
190
+ }
@@ -0,0 +1,63 @@
1
+ /** Stdin and stdout of a Claude hook shim. */
2
+
3
+ export type HookEnv = Record<string, string | undefined>;
4
+
5
+ /** One run of a hook shim: its environment, directory, shim arguments and stdin payload. */
6
+ export interface HookCall {
7
+ env: HookEnv;
8
+ cwd: string;
9
+ argv: readonly string[];
10
+ input: unknown;
11
+ }
12
+
13
+ export interface HookOutcome {
14
+ exitCode: number;
15
+ stdout: string;
16
+ }
17
+
18
+ /** A hook's JSON output line, or nothing. Always exit zero: a hook must never block the session. */
19
+ export function hookOutcome(output: object | null): HookOutcome {
20
+ return { exitCode: 0, stdout: output ? `${JSON.stringify(output)}\n` : "" };
21
+ }
22
+
23
+ async function readStdin(): Promise<string> {
24
+ if (process.stdin.isTTY) return "";
25
+ const chunks: Buffer[] = [];
26
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
27
+ return Buffer.concat(chunks).toString("utf8");
28
+ }
29
+
30
+ /** The hook payload on stdin; input that is not JSON reads as `null`, no event. */
31
+ async function readHookInput(): Promise<unknown> {
32
+ const raw = await readStdin();
33
+ try {
34
+ return JSON.parse(raw) as unknown;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * A hook shim's whole run: read the payload, build the outcome, write it.
42
+ * `onError` turns a failure in either step into an outcome, so the shim still exits zero.
43
+ */
44
+ export async function runHook(
45
+ build: (input: unknown) => HookOutcome | Promise<HookOutcome>,
46
+ onError: (error: unknown) => HookOutcome,
47
+ ): Promise<number> {
48
+ let outcome: HookOutcome;
49
+ try {
50
+ outcome = await build(await readHookInput());
51
+ } catch (error) {
52
+ outcome = onError(error);
53
+ }
54
+ writeHookStdout(outcome.stdout);
55
+ return outcome.exitCode;
56
+ }
57
+
58
+ /** Swallows stdout errors: a parent that stopped reading would otherwise end the write in an uncaught `EPIPE`. */
59
+ function writeHookStdout(stdout: string): void {
60
+ if (!stdout) return;
61
+ process.stdout.on("error", () => {});
62
+ process.stdout.write(stdout);
63
+ }
@@ -1,5 +1,8 @@
1
- // Show the active Mate companion paths in Claude Code, and repair the
2
- // companion's Git state before the session reads it.
1
+ /**
2
+ * Show the active Mate companion paths in Claude Code, and repair the
3
+ * companion's Git state before the session reads it.
4
+ */
5
+ import { errorMessage } from "../runtime/error-message";
3
6
  import { unmanagedClaudeDelivery } from "../runtime/claude-delivery-plan";
4
7
  import {
5
8
  syncCompanionUnattended,
@@ -7,24 +10,11 @@ import {
7
10
  type UnattendedSyncOptions,
8
11
  } from "../runtime/companion-sync";
9
12
  import { hasLaunchEnvironment, resolveCompanionRuntime } from "../runtime/env";
13
+ import { FRAMEWORK_NAME } from "../runtime/framework";
10
14
  import { projectionFreshness, projectionStalenessLines } from "../runtime/freshness";
11
15
  import { mateVersion } from "../runtime/install";
12
- import type { HookEnv } from "./validate-artifact-path";
13
-
14
- export interface BannerOutcome {
15
- exitCode: number;
16
- stdout: string;
17
- }
18
-
19
- /**
20
- * Marks the copy a wrap inlines into the Working Repository's settings. A
21
- * managed launch loads the plugin's own copy *and* that document, and both
22
- * registrations resolve to the same command against the same environment, so
23
- * nothing inside the process tells them apart. The flag is what keeps the
24
- * banner from printing twice in exactly the session that is configured
25
- * correctly — the projected copy defers, the launch's own copy prints.
26
- */
27
- export const PROJECTED_BANNER_FLAG = "--projected";
16
+ import { PROJECTED_FLAG } from "../runtime/projected-flag";
17
+ import { hookOutcome, runHook, type HookEnv, type HookOutcome } from "./hook-io";
28
18
 
29
19
  /**
30
20
  * Resolves in-process from either source — no subprocess in either branch.
@@ -36,13 +26,11 @@ export async function buildBanner(
36
26
  cwd: string = process.cwd(),
37
27
  argv: readonly string[] = [],
38
28
  syncOptions: UnattendedSyncOptions = {},
39
- ): Promise<BannerOutcome> {
40
- if (argv.includes(PROJECTED_BANNER_FLAG) && hasLaunchEnvironment(env)) {
41
- return { exitCode: 0, stdout: "" };
42
- }
29
+ ): Promise<HookOutcome> {
30
+ if (argv.includes(PROJECTED_FLAG) && hasLaunchEnvironment(env)) return hookOutcome(null);
43
31
 
44
32
  const { context, projection } = resolveCompanionRuntime(env, cwd);
45
- if (!context.repositoryPath || !context.companionPath) return { exitCode: 0, stdout: "" };
33
+ if (!context.repositoryPath || !context.companionPath) return hookOutcome(null);
46
34
 
47
35
  /**
48
36
  * Repaired before the session reads the companion, so its first reads are
@@ -78,12 +66,16 @@ export async function buildBanner(
78
66
  /** `systemMessage` only: the model must never be told to run the command. */
79
67
  for (const note of gitNotes) lines.push(` ${note}`);
80
68
 
81
- return { exitCode: 0, stdout: JSON.stringify({ systemMessage: lines.join("\n") }) + "\n" };
69
+ return hookOutcome({ systemMessage: lines.join("\n") });
82
70
  }
83
71
 
84
- // Plugin-shim entry.
85
- export async function run(argv: readonly string[] = process.argv.slice(2)): Promise<number> {
86
- const outcome = await buildBanner(process.env, process.cwd(), argv);
87
- if (outcome.stdout) process.stdout.write(outcome.stdout);
88
- return outcome.exitCode;
72
+ /** Plugin-shim entry; exits zero even when the banner fails. */
73
+ export function run(argv: readonly string[] = process.argv.slice(2)): Promise<number> {
74
+ return runHook(
75
+ () => buildBanner(process.env, process.cwd(), argv),
76
+ (error) =>
77
+ hookOutcome({
78
+ systemMessage: `${FRAMEWORK_NAME}: session banner failed: ${errorMessage(error)}`,
79
+ }),
80
+ );
89
81
  }
@@ -1,27 +1,6 @@
1
1
  // Deliver the companion guidance to a Claude session Mate did not launch.
2
- import { currentAudienceLayer, readLayerGuidanceSync } from "../runtime/audience-layer";
3
- import { resolveProjection } from "../runtime/projection";
4
2
  import { buildProjectedGuidance } from "../runtime/projected-guidance";
5
- import { readWrapStateSync } from "../runtime/wrap-state";
6
- import type { HookEnv } from "./validate-artifact-path";
7
-
8
- export interface GuidanceOutcome {
9
- exitCode: number;
10
- stdout: string;
11
- }
12
-
13
- /**
14
- * The guidance of the audience this checkout is wrapped for, from its layer.
15
- * The wrap is the only source: an inherited `MATE_AUDIENCE` selects nothing.
16
- */
17
- function wrappedAudienceGuidance(cwd: string): string {
18
- const projection = resolveProjection(cwd);
19
- if (!projection) return "";
20
- const audience = readWrapStateSync(projection.repoRoot).audience;
21
- if (!audience) return "";
22
- const layer = currentAudienceLayer(projection.companionPath, audience);
23
- return layer ? readLayerGuidanceSync(layer, "CLAUDE.md") : "";
24
- }
3
+ import { hookOutcome, type HookEnv, type HookOutcome } from "./hook-io";
25
4
 
26
5
  /**
27
6
  * `SessionStart.additionalContext` is the file-based form of the managed
@@ -33,21 +12,16 @@ function wrappedAudienceGuidance(cwd: string): string {
33
12
  * Emits nothing rather than a note when no companion resolves: an unwrapped
34
13
  * repository must read exactly as it did before the hook existed.
35
14
  */
36
- export function buildSessionGuidance(env: HookEnv, cwd: string = process.cwd()): GuidanceOutcome {
37
- const companionGuidance = buildProjectedGuidance(env, cwd);
38
- if (!companionGuidance) return { exitCode: 0, stdout: "" };
39
- const layer = wrappedAudienceGuidance(cwd);
40
- const guidance = layer ? `${companionGuidance}\n\n${layer}` : companionGuidance;
15
+ export function buildSessionGuidance(env: HookEnv, cwd: string = process.cwd()): HookOutcome {
16
+ const guidance = buildProjectedGuidance(env, cwd);
17
+ if (!guidance) return hookOutcome(null);
41
18
 
42
- return {
43
- exitCode: 0,
44
- stdout: `${JSON.stringify({
45
- hookSpecificOutput: {
46
- hookEventName: "SessionStart",
47
- additionalContext: guidance,
48
- },
49
- })}\n`,
50
- };
19
+ return hookOutcome({
20
+ hookSpecificOutput: {
21
+ hookEventName: "SessionStart",
22
+ additionalContext: guidance,
23
+ },
24
+ });
51
25
  }
52
26
 
53
27
  // Plugin-shim entry.
@@ -1,8 +1,11 @@
1
1
  /** Prepares a plain Claude session in a Claude-wrapped repository as `mate claude` would. */
2
2
  import fs from "node:fs";
3
3
 
4
+ import { errorMessage } from "../runtime/error-message";
4
5
  import { unmanagedClaudeDelivery } from "../runtime/claude-delivery-plan";
5
6
  import { hasLaunchEnvironment } from "../runtime/env";
7
+ import { FRAMEWORK_NAME } from "../runtime/framework";
8
+ import { PROJECTED_FLAG } from "../runtime/projected-flag";
6
9
  import { resolveProjection } from "../runtime/projection";
7
10
  import { runtimeDirectoryPath, runtimeMateBin } from "../runtime/runtime-directory";
8
11
  import {
@@ -12,12 +15,8 @@ import {
12
15
  type PrepareRun,
13
16
  type PrepareRunner,
14
17
  } from "../runtime/session-prepare-runner";
15
- import type { HookEnv } from "./validate-artifact-path";
16
-
17
- export interface SessionPrepareHookOutcome {
18
- exitCode: number;
19
- stdout: string;
20
- }
18
+ import { instructionHookPayload, type InstructionHookDeps } from "./companion-instructions";
19
+ import { hookOutcome, runHook, type HookCall, type HookEnv, type HookOutcome } from "./hook-io";
21
20
 
22
21
  export interface SessionPrepareHookDeps {
23
22
  /** The runtime-directory launcher; `null` until a `mate` run wrote it. */
@@ -25,6 +24,8 @@ export interface SessionPrepareHookDeps {
25
24
  runner: (mateBin: string, repoPath: string) => PrepareRunner;
26
25
  startSync: (mateBin: string, repoPath: string) => void;
27
26
  timeoutMs: number;
27
+ /** The main thread's instruction read, after the prepare. */
28
+ instructions?: InstructionHookDeps;
28
29
  }
29
30
 
30
31
  export const sessionPrepareHookDeps: SessionPrepareHookDeps = {
@@ -37,12 +38,9 @@ export const sessionPrepareHookDeps: SessionPrepareHookDeps = {
37
38
  timeoutMs: PREPARE_TIMEOUT_MS,
38
39
  };
39
40
 
40
- const INERT: SessionPrepareHookOutcome = { exitCode: 0, stdout: "" };
41
-
42
41
  /** `systemMessage` only: the model must never be told to repair the companion. */
43
- function operatorMessage(lines: string[]): SessionPrepareHookOutcome {
44
- if (lines.length === 0) return INERT;
45
- return { exitCode: 0, stdout: `${JSON.stringify({ systemMessage: lines.join("\n") })}\n` };
42
+ function operatorMessage(lines: string[]): HookOutcome {
43
+ return hookOutcome(lines.length > 0 ? { systemMessage: lines.join("\n") } : null);
46
44
  }
47
45
 
48
46
  async function runLocalPhase(
@@ -54,10 +52,18 @@ async function runLocalPhase(
54
52
  try {
55
53
  return await pending;
56
54
  } catch (error) {
57
- return { ok: false, notes: [error instanceof Error ? error.message : String(error)] };
55
+ return { ok: false, notes: [errorMessage(error)] };
58
56
  }
59
57
  }
60
58
 
59
+ /** The local phase's notes, and the Git sync it leaves for the caller to start. */
60
+ interface Prepared {
61
+ notes: string[];
62
+ startSync: () => void;
63
+ }
64
+
65
+ const NO_SYNC = (): void => {};
66
+
61
67
  /**
62
68
  * Inert under a launch environment or where the plan gives it nothing to own.
63
69
  * Awaits the local phase within a bound, then starts Git sync detached. Always
@@ -67,35 +73,116 @@ export async function runSessionPrepareHook(
67
73
  env: HookEnv,
68
74
  cwd: string = process.cwd(),
69
75
  deps: SessionPrepareHookDeps = sessionPrepareHookDeps,
70
- ): Promise<SessionPrepareHookOutcome> {
71
- if (hasLaunchEnvironment(env)) return INERT;
72
- const projectDir = env.CLAUDE_PROJECT_DIR || cwd;
76
+ ): Promise<HookOutcome> {
77
+ const prepared = await guardedPrepare(env, cwd, deps);
78
+ return operatorMessage([...prepared.notes, ...startSync(prepared)]);
79
+ }
80
+
81
+ /** The folder the prepare runs in, and the main thread's instructions are read from. */
82
+ function prepareDirectory(env: HookEnv, cwd: string): string {
83
+ return env.CLAUDE_PROJECT_DIR || cwd;
84
+ }
85
+
86
+ function prepareFailure(error: unknown): string {
87
+ return `${FRAMEWORK_NAME}: session prepare failed: ${errorMessage(error)}`;
88
+ }
89
+
90
+ /** A prepare that throws becomes a note, so the instructions after it still go out. */
91
+ async function guardedPrepare(
92
+ env: HookEnv,
93
+ cwd: string,
94
+ deps: SessionPrepareHookDeps,
95
+ ): Promise<Prepared> {
96
+ try {
97
+ return await prepare(env, cwd, deps);
98
+ } catch (error) {
99
+ return { notes: [prepareFailure(error)], startSync: NO_SYNC };
100
+ }
101
+ }
102
+
103
+ /** A sync that fails to start becomes a note. */
104
+ function startSync(prepared: Prepared): string[] {
105
+ try {
106
+ prepared.startSync();
107
+ return [];
108
+ } catch (error) {
109
+ return [prepareFailure(error)];
110
+ }
111
+ }
112
+
113
+ async function prepare(env: HookEnv, cwd: string, deps: SessionPrepareHookDeps): Promise<Prepared> {
114
+ const nothing = { notes: [], startSync: NO_SYNC };
115
+ if (hasLaunchEnvironment(env)) return nothing;
116
+ const projectDir = prepareDirectory(env, cwd);
73
117
  const projection = resolveProjection(projectDir);
74
- if (!projection) return INERT;
118
+ if (!projection) return nothing;
75
119
  const plan = unmanagedClaudeDelivery(projection.repoRoot);
76
120
  const reconciles = plan.reconcile === "prepare-hook";
77
121
  const syncs = plan.gitSync === "prepare-hook";
78
- if (!reconciles && !syncs) return INERT;
122
+ if (!reconciles && !syncs) return nothing;
79
123
 
80
124
  const mateBin = deps.mateBin();
81
125
  if (!mateBin) {
82
- return operatorMessage([
83
- `mate: session prepare skipped: ${runtimeDirectoryPath()} is missing; run \`mate\` once to repair it.`,
84
- ]);
126
+ return {
127
+ notes: [
128
+ `${FRAMEWORK_NAME}: session prepare skipped: ${runtimeDirectoryPath()} is missing; run \`${FRAMEWORK_NAME}\` once to repair it.`,
129
+ ],
130
+ startSync: NO_SYNC,
131
+ };
85
132
  }
86
133
 
87
134
  const local = reconciles
88
135
  ? await runLocalPhase(deps, mateBin, projectDir)
89
136
  : { ok: true, notes: [] };
90
- if (syncs) deps.startSync(mateBin, projectDir);
91
-
92
137
  const verdict = local.ok ? "warning" : "failed";
93
- return operatorMessage(local.notes.map((note) => `mate: session prepare ${verdict}: ${note}`));
138
+ return {
139
+ notes: local.notes.map((note) => `${FRAMEWORK_NAME}: session prepare ${verdict}: ${note}`),
140
+ startSync: syncs ? () => deps.startSync(mateBin, projectDir) : NO_SYNC,
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Legacy rule: wraps before `--projected` registered this copy bare, and only
146
+ * this hook re-wraps them. So a bare copy counts as the wrap's unless
147
+ * `CLAUDE_PLUGIN_ROOT`, set for plugin hooks only, marks it as the plugin's.
148
+ */
149
+ function isWrapCopy({ argv, env }: HookCall): boolean {
150
+ return argv.includes(PROJECTED_FLAG) || !env.CLAUDE_PLUGIN_ROOT;
151
+ }
152
+
153
+ /**
154
+ * The prepare, the main thread's Companion Instructions, then the Git sync, in
155
+ * one hook: Claude Code runs an event's hooks in parallel, and the prepare and
156
+ * a fast-forward both rewrite the files the instructions are read from. Only
157
+ * the wrap's copy runs any. Input that is not a JSON object is no event.
158
+ */
159
+ export async function runSessionStartHook(
160
+ call: HookCall,
161
+ deps: SessionPrepareHookDeps = sessionPrepareHookDeps,
162
+ ): Promise<HookOutcome> {
163
+ const { env, cwd, input } = call;
164
+ if (!input || typeof input !== "object" || Array.isArray(input)) return hookOutcome(null);
165
+ if (!isWrapCopy(call)) return hookOutcome(null);
166
+ const prepared = await guardedPrepare(env, cwd, deps);
167
+ const { hookSpecificOutput, systemMessage } =
168
+ instructionHookPayload(
169
+ { env, cwd, input, projected: true, directory: prepareDirectory(env, cwd) },
170
+ deps.instructions,
171
+ ) ?? {};
172
+ const notes = [...prepared.notes, ...startSync(prepared)];
173
+ const messages = systemMessage ? [...notes, systemMessage] : notes;
174
+ if (!hookSpecificOutput) return operatorMessage(messages);
175
+ return hookOutcome({
176
+ hookSpecificOutput,
177
+ ...(messages.length > 0 ? { systemMessage: messages.join("\n") } : {}),
178
+ });
94
179
  }
95
180
 
96
- /** Plugin-shim entry. */
97
- export async function run(): Promise<number> {
98
- const outcome = await runSessionPrepareHook(process.env);
99
- if (outcome.stdout) process.stdout.write(outcome.stdout);
100
- return outcome.exitCode;
181
+ /** Plugin-shim entry; exits zero even when reading its input fails. */
182
+ export function run(argv: readonly string[] = process.argv.slice(2)): Promise<number> {
183
+ return runHook(
184
+ (input) => runSessionStartHook({ env: process.env, cwd: process.cwd(), argv, input }),
185
+ (error) =>
186
+ operatorMessage([`${FRAMEWORK_NAME}: session start hook failed: ${errorMessage(error)}`]),
187
+ );
101
188
  }