@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
@@ -12,12 +12,15 @@ import fs from "node:fs";
12
12
  import os from "node:os";
13
13
  import path from "node:path";
14
14
 
15
+ import { artifactRefusal, resolveArtifactTarget } from "../runtime/artifact-refusal";
16
+ import { checkCommitAttribution } from "../runtime/commit-attribution-guard";
15
17
  import { gitEnvironment } from "../runtime/companion-git";
16
18
  import { companionForkRefusal } from "../runtime/companion-sync";
17
19
  import { readCompanionRuntimeContext } from "../runtime/env";
18
20
  import { companionLinkPath } from "../runtime/repo-local";
19
-
20
- export type HookEnv = Record<string, string | undefined>;
21
+ import { checkSecretAccess, type SecretToolCall } from "../runtime/secret-guard";
22
+ import { stripHeredocBodies } from "../runtime/shell-heredoc";
23
+ import type { HookEnv } from "./hook-io";
21
24
 
22
25
  export interface HookOutcome {
23
26
  exitCode: number;
@@ -44,8 +47,9 @@ const ARTIFACT_PATH_MARKERS = [
44
47
  "/docs/prd/",
45
48
  ];
46
49
 
47
- const MD_REDIRECT_PATTERN = />{1,2}\s*([^\s;|&<>]+\.md)\b/g;
48
- const MD_TEE_PATTERN = /\btee\s+(?:-a\s+)?([^\s;|&<>]+\.md)\b/g;
50
+ /** `[ \t]`, not `\s`: a redirect or `tee` names its target on its own line. */
51
+ const MD_REDIRECT_PATTERN = />{1,2}[ \t]*([^\s;|&<>]+\.md)\b/g;
52
+ const MD_TEE_PATTERN = /\btee[ \t]+(?:-a[ \t]+)?([^\s;|&<>]+\.md)\b/g;
49
53
 
50
54
  export function artifactLikePath(filePath: string): boolean {
51
55
  if (!filePath) return false;
@@ -118,11 +122,6 @@ function claudeConfigRoot(env: HookEnv): string {
118
122
  return path.normalize(path.join(env.HOME || os.homedir(), ".claude"));
119
123
  }
120
124
 
121
- function normalizePath(filePath: string, ctx: GuardContext): string {
122
- if (path.isAbsolute(filePath)) return path.normalize(filePath);
123
- return path.normalize(path.join(ctx.repoRoot, filePath));
124
- }
125
-
126
125
  function isUnder(target: string, root: string): boolean {
127
126
  const relative = path.relative(path.normalize(root), path.normalize(target));
128
127
  return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
@@ -181,17 +180,14 @@ function isProductDocumentationPath(target: string, ctx: GuardContext): boolean
181
180
  return false;
182
181
  }
183
182
 
184
- function blockWrite(target: string, companion: string): HookOutcome {
185
- const lines = [
186
- "Mate guardrail: artifact writes must go to the companion framework path.",
187
- ` target: ${target}`,
188
- ` companion: ${companion}`,
189
- "Use an absolute path under MATE_ARTIFACT_PATH instead.",
190
- ];
191
- if (path.basename(target) === "CLAUDE.md") {
192
- lines.push(`Note: CLAUDE.md already lives in the companion repo at ${companion}/CLAUDE.md.`);
193
- }
194
- return { exitCode: 2, stderr: lines.join("\n") + "\n" };
183
+ function blockWrite(target: string, resolved: string, ctx: GuardContext): HookOutcome {
184
+ const refusal = artifactRefusal({
185
+ target,
186
+ resolved,
187
+ companion: ctx.companion,
188
+ repoRoot: ctx.repoRoot,
189
+ });
190
+ return { exitCode: 2, stderr: `${refusal}\n` };
195
191
  }
196
192
 
197
193
  function checkFilePath(
@@ -201,7 +197,7 @@ function checkFilePath(
201
197
  ): HookOutcome | null {
202
198
  if (!filePath) return null;
203
199
 
204
- const normalized = normalizePath(filePath, ctx);
200
+ const normalized = resolveArtifactTarget(filePath, ctx.repoRoot);
205
201
  if (isCompanionPath(normalized, ctx)) return null;
206
202
 
207
203
  if (isUnder(normalized, claudeConfigRoot(ctx.env))) return null;
@@ -219,7 +215,7 @@ function checkFilePath(
219
215
  return null;
220
216
  }
221
217
 
222
- return blockWrite(filePath, ctx.companion);
218
+ return blockWrite(filePath, normalized, ctx);
223
219
  }
224
220
 
225
221
  /**
@@ -234,17 +230,19 @@ async function checkForkedCompanion(
234
230
  ctx: GuardContext,
235
231
  ): Promise<HookOutcome | null> {
236
232
  if (!filePath || !artifactLikePath(filePath)) return null;
237
- if (!isCompanionPath(normalizePath(filePath, ctx), ctx)) return null;
233
+ if (!isCompanionPath(resolveArtifactTarget(filePath, ctx.repoRoot), ctx)) return null;
238
234
 
239
235
  const refusal = await companionForkRefusal(ctx.env, ctx.companion);
240
236
  return refusal ? { exitCode: 2, stderr: `${refusal}\n` } : null;
241
237
  }
242
238
 
239
+ /** Heredoc bodies are what a command writes, not what it runs: they are not scanned. */
243
240
  export function commandMatches(command: string): string[] {
244
241
  const targets: string[] = [];
242
+ const commandText = stripHeredocBodies(command);
245
243
  for (const pattern of [MD_REDIRECT_PATTERN, MD_TEE_PATTERN]) {
246
244
  const matcher = new RegExp(pattern.source, pattern.flags);
247
- for (const match of command.matchAll(matcher)) {
245
+ for (const match of commandText.matchAll(matcher)) {
248
246
  const target = match[1].trim().replace(/^["']+|["']+$/g, "");
249
247
  if (target) targets.push(target);
250
248
  }
@@ -252,6 +250,53 @@ export function commandMatches(command: string): string[] {
252
250
  return targets;
253
251
  }
254
252
 
253
+ /** Claude tool payloads in the shape the shared secret guard reads. */
254
+ function secretToolCall(
255
+ toolName: string,
256
+ toolInput: Record<string, unknown>,
257
+ ctx: GuardContext,
258
+ ): SecretToolCall | null {
259
+ const text = (value: unknown) => (typeof value === "string" ? value : "");
260
+ const target = (value: unknown) =>
261
+ text(value) ? resolveArtifactTarget(text(value), ctx.repoRoot) : "";
262
+ switch (toolName) {
263
+ case "Read":
264
+ return { kind: "read", paths: [target(toolInput.file_path)] };
265
+ case "Write":
266
+ return {
267
+ kind: "write",
268
+ writes: [{ path: target(toolInput.file_path), content: text(toolInput.content) }],
269
+ };
270
+ case "Edit":
271
+ return {
272
+ kind: "write",
273
+ writes: [{ path: target(toolInput.file_path), content: text(toolInput.new_string) }],
274
+ };
275
+ case "MultiEdit": {
276
+ const edits = Array.isArray(toolInput.edits) ? toolInput.edits : [];
277
+ const content = edits
278
+ .map((edit) => (edit && typeof edit === "object" ? text(edit.new_string) : ""))
279
+ .join("\n");
280
+ return { kind: "write", writes: [{ path: target(toolInput.file_path), content }] };
281
+ }
282
+ case "NotebookEdit":
283
+ return {
284
+ kind: "write",
285
+ writes: [{ path: target(toolInput.notebook_path), content: text(toolInput.new_source) }],
286
+ };
287
+ case "Bash":
288
+ return { kind: "shell", command: text(toolInput.command) };
289
+ case "Grep":
290
+ return {
291
+ kind: "search",
292
+ paths: [target(toolInput.path)],
293
+ globs: [text(toolInput.glob)],
294
+ };
295
+ default:
296
+ return null;
297
+ }
298
+ }
299
+
255
300
  /**
256
301
  * Fails open only when no Mate context resolves from either source; in a
257
302
  * wrapped Working Repository the guard runs without a launch.
@@ -273,6 +318,16 @@ export async function evaluate(
273
318
  ? (input.tool_input as Record<string, unknown>)
274
319
  : {};
275
320
 
321
+ /** Secrets first: a secret refusal wins over every artifact-placement verdict. */
322
+ const secretCall = secretToolCall(toolName, toolInput, ctx);
323
+ if (secretCall) {
324
+ const refusal = await checkSecretAccess(secretCall, {
325
+ companionPath: ctx.companion,
326
+ isCompanionPath: (target) => isCompanionPath(target, ctx),
327
+ });
328
+ if (refusal) return { exitCode: 2, stderr: `${refusal}\n` };
329
+ }
330
+
276
331
  if (toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit") {
277
332
  const filePath = String(toolInput.file_path ?? "");
278
333
  /** The path refusal wins when both apply: its message is not replaced. */
@@ -282,6 +337,9 @@ export async function evaluate(
282
337
 
283
338
  if (toolName === "Bash") {
284
339
  const command = String(toolInput.command ?? "");
340
+ const attribution = checkCommitAttribution(command);
341
+ if (attribution) return { exitCode: 2, stderr: `${attribution}\n` };
342
+
285
343
  const targets = commandMatches(command);
286
344
  for (const target of targets) {
287
345
  const outcome = checkFilePath(target, ctx);
package/src/index.ts CHANGED
@@ -39,13 +39,17 @@ export type {
39
39
  McpService,
40
40
  Plugin,
41
41
  PluginCliCommand,
42
+ PluginDirectoryContribution,
42
43
  PluginPolicy,
43
44
  PluginRegistration,
44
45
  ProviderHosting,
45
46
  ProviderPlugin,
47
+ RuntimeContributions,
48
+ RuntimeContributionsByRuntime,
46
49
  SetupContext,
47
50
  TemplatesService,
48
51
  } from "./tools/setup/plugin";
52
+ export type { SessionKind } from "./runtime/session-kind";
49
53
  export type {
50
54
  FrameworkType,
51
55
  FrameworkConfig,
@@ -0,0 +1,154 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { pathIsDirectory } from "./fs-utils";
5
+
6
+ /**
7
+ * Where a change lives in a Companion Repository: the active `openspec/changes/<name>/`
8
+ * directory, its dated archive `openspec/changes/archive/YYYY-MM-DD-<name>/`, and the
9
+ * canonical specs its delta specs map to. Only directories count, and only names are read.
10
+ */
11
+
12
+ /** Companion-relative POSIX paths. */
13
+ export const CHANGES_RELATIVE_DIR = "openspec/changes";
14
+ export const ARCHIVE_RELATIVE_DIR = "openspec/changes/archive";
15
+ export const SPECS_RELATIVE_DIR = "openspec/specs";
16
+
17
+ const ARCHIVE_DIR_NAME = "archive";
18
+
19
+ /**
20
+ * One path segment: no separator, NUL, or leading dot (so never `.`, `..`, or hidden).
21
+ * Wider than OpenSpec's kebab-case, so archives made under older rules still resolve.
22
+ */
23
+ const SEGMENT = /^[^./\\\0][^/\\\0]*$/;
24
+
25
+ /** The `YYYY-MM-DD-<name>` anchor `openspec archive` gives an archived change. */
26
+ const ANCHOR = /^(\d{4}-\d{2}-\d{2})-(.+)$/;
27
+
28
+ export function changesDirOf(companionPath: string): string {
29
+ return path.join(companionPath, ...CHANGES_RELATIVE_DIR.split("/"));
30
+ }
31
+
32
+ export function archiveDirOf(companionPath: string): string {
33
+ return path.join(companionPath, ...ARCHIVE_RELATIVE_DIR.split("/"));
34
+ }
35
+
36
+ /** A change name or archive anchor: one path segment that is not the archive directory. */
37
+ export function isChangeName(candidate: string): boolean {
38
+ return SEGMENT.test(candidate) && candidate !== ARCHIVE_DIR_NAME;
39
+ }
40
+
41
+ export interface Anchor {
42
+ date: string;
43
+ name: string;
44
+ }
45
+
46
+ /** `2026-01-02-acme` → `{date: "2026-01-02", name: "acme"}`; null for anything else, `2026-01-02-` included. */
47
+ export function parseAnchor(candidate: string): Anchor | null {
48
+ const match = ANCHOR.exec(candidate);
49
+ if (!match || !isChangeName(match[2])) return null;
50
+ return { date: match[1], name: match[2] };
51
+ }
52
+
53
+ /** The change name of an archive anchor (`2026-01-02-acme` → `acme`); other names pass through. */
54
+ export function changeNameOf(anchorOrName: string): string {
55
+ return parseAnchor(anchorOrName)?.name ?? anchorOrName;
56
+ }
57
+
58
+ async function directoryNames(dir: string): Promise<string[]> {
59
+ try {
60
+ const entries = await fs.readdir(dir, { withFileTypes: true });
61
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
62
+ } catch {
63
+ return [];
64
+ }
65
+ }
66
+
67
+ /** Active change names, sorted. */
68
+ export async function activeChanges(companionPath: string): Promise<string[]> {
69
+ return (await directoryNames(changesDirOf(companionPath))).filter(isChangeName).toSorted();
70
+ }
71
+
72
+ /** Archive anchors, optionally only those of change `name`, sorted oldest first. */
73
+ export async function archiveAnchors(companionPath: string, name?: string): Promise<string[]> {
74
+ return (await directoryNames(archiveDirOf(companionPath)))
75
+ .filter((anchor) => {
76
+ const parsed = parseAnchor(anchor);
77
+ return parsed !== null && (name === undefined || parsed.name === name);
78
+ })
79
+ .toSorted();
80
+ }
81
+
82
+ export interface ChangeLocation {
83
+ dir: string;
84
+ archived: boolean;
85
+ }
86
+
87
+ /**
88
+ * Resolves a change name or anchor to its directory: the archive an anchor names exactly,
89
+ * else the active change, else the newest archive of that name. Anything that is not a
90
+ * single change-name segment resolves to nothing.
91
+ */
92
+ export async function locateChange(
93
+ companionPath: string,
94
+ target: string,
95
+ ): Promise<ChangeLocation | null> {
96
+ if (!isChangeName(target)) return null;
97
+ const archiveDir = archiveDirOf(companionPath);
98
+ if (parseAnchor(target) && (await pathIsDirectory(path.join(archiveDir, target)))) {
99
+ return { dir: path.join(archiveDir, target), archived: true };
100
+ }
101
+ const active = path.join(changesDirOf(companionPath), target);
102
+ if (await pathIsDirectory(active)) return { dir: active, archived: false };
103
+ const newest = (await archiveAnchors(companionPath, target)).at(-1);
104
+ return newest ? { dir: path.join(archiveDir, newest), archived: true } : null;
105
+ }
106
+
107
+ export interface DeltaSpec {
108
+ /** POSIX path under the change's `specs/`, e.g. `identity/user-auth/spec.md`. */
109
+ file: string;
110
+ /** The companion-relative canonical path the file maps to. */
111
+ canonical: string;
112
+ /** The capability path of a `spec.md` below `specs/`; null for any other file. */
113
+ capability: string | null;
114
+ }
115
+
116
+ /**
117
+ * Every file below `dir` at any depth, as sorted POSIX paths relative to it. Reads directory
118
+ * entry names only, never contents; an unreadable directory contributes nothing.
119
+ */
120
+ export async function filesBelow(dir: string): Promise<string[]> {
121
+ const walk = async (current: string, relative: string): Promise<string[]> => {
122
+ let entries;
123
+ try {
124
+ entries = await fs.readdir(current, { withFileTypes: true });
125
+ } catch {
126
+ return [];
127
+ }
128
+ const nested = await Promise.all(
129
+ entries.map(async (entry) => {
130
+ const entryRelative = relative ? `${relative}/${entry.name}` : entry.name;
131
+ if (entry.isDirectory()) return walk(path.join(current, entry.name), entryRelative);
132
+ return entry.isFile() ? [entryRelative] : [];
133
+ }),
134
+ );
135
+ return nested.flat();
136
+ };
137
+ return (await walk(dir, "")).toSorted();
138
+ }
139
+
140
+ /**
141
+ * Every file under the change's `specs/`, at any depth, sorted. Reads directory entry
142
+ * names only, never contents. A change without `specs/` has none.
143
+ */
144
+ export async function deltaSpecs(changeDir: string): Promise<DeltaSpec[]> {
145
+ const files = await filesBelow(path.join(changeDir, "specs"));
146
+ return files.map((file) => {
147
+ const capability = path.posix.dirname(file);
148
+ return {
149
+ file,
150
+ canonical: `${SPECS_RELATIVE_DIR}/${file}`,
151
+ capability: path.posix.basename(file) === "spec.md" && capability !== "." ? capability : null,
152
+ };
153
+ });
154
+ }
@@ -1,5 +1,7 @@
1
1
  import { Box, Text } from "ink";
2
2
 
3
+ import { BRAND_ACCENT } from "../../brand/palette";
4
+
3
5
  interface WizardHeaderProps {
4
6
  title: string;
5
7
  step: number;
@@ -12,7 +14,7 @@ export function WizardHeader({ title, step, totalSteps }: WizardHeaderProps) {
12
14
  return (
13
15
  <Box borderStyle="round" width="100%">
14
16
  <Box flexDirection="column" flexGrow={1} marginLeft={1}>
15
- <Text bold color="green">
17
+ <Text bold color={BRAND_ACCENT}>
16
18
  mate framework
17
19
  </Text>
18
20
  <Box justifyContent="space-between">
@@ -54,9 +54,17 @@ function validateContextModeNodeRuntime(): void {
54
54
  export async function validateContextModePackage(companionPath: string): Promise<void> {
55
55
  validateContextModeNodeRuntime();
56
56
  const root = getContextModePackageRoot(companionPath);
57
- const manifest = JSON.parse(await fs.readFile(path.join(root, "package.json"), "utf8")) as {
58
- version?: string;
59
- };
57
+ let manifestText: string;
58
+ try {
59
+ manifestText = await fs.readFile(path.join(root, "package.json"), "utf8");
60
+ } catch (error) {
61
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
62
+ throw new Error(
63
+ `Expected ${getContextModePackageReference()} at ${root}; it is not installed. Re-run \`mate companion setup\`.`,
64
+ { cause: error },
65
+ );
66
+ }
67
+ const manifest = JSON.parse(manifestText) as { version?: string };
60
68
  if (manifest.version !== CONTEXT_MODE_VERSION) {
61
69
  throw new Error(
62
70
  `Expected ${getContextModePackageReference()} at ${root}; found ${manifest.version ?? "an unknown version"}. Re-run \`mate companion setup\`.`,
@@ -72,6 +80,15 @@ export async function validateContextModePackage(companionPath: string): Promise
72
80
  );
73
81
  }
74
82
 
83
+ export const contextModePackageDeps = {
84
+ runInstall: (cwd: string) =>
85
+ spawnSync(
86
+ "npm",
87
+ ["install", "--no-audit", "--no-fund", "--loglevel=error", "--registry", PUBLIC_NPM_REGISTRY],
88
+ { cwd, encoding: "utf8" as const, env: process.env },
89
+ ),
90
+ };
91
+
75
92
  export async function installContextModePackage(companionPath: string): Promise<void> {
76
93
  validateContextModeNodeRuntime();
77
94
  const installDir = getContextModeInstallDir(companionPath);
@@ -85,15 +102,11 @@ export async function installContextModePackage(companionPath: string): Promise<
85
102
  ) + "\n",
86
103
  "utf8",
87
104
  );
88
- const result = spawnSync(
89
- "npm",
90
- ["install", "--no-audit", "--no-fund", "--silent", "--registry", PUBLIC_NPM_REGISTRY],
91
- { cwd: installDir, encoding: "utf8", env: process.env },
92
- );
105
+ const result = contextModePackageDeps.runInstall(installDir);
93
106
  if (result.error || result.status !== 0) {
94
- throw new Error(
95
- `Could not install ${getContextModePackageReference()}: ${result.error?.message ?? result.stderr?.trim() ?? `npm exited with ${result.status}`}`,
96
- );
107
+ const cause =
108
+ result.error?.message ?? (result.stderr?.trim() || `npm exited with ${result.status}`);
109
+ throw new Error(`Could not install ${getContextModePackageReference()}: ${cause}`);
97
110
  }
98
111
  await validateContextModePackage(companionPath);
99
112
  }
@@ -0,0 +1,154 @@
1
+ import { locateChange } from "../change-layout";
2
+ import { runOpenSpec, type RunOpenSpec } from "../openspec-cli";
3
+ import { companionRelative, type DeliveryEnvironment } from "./environment";
4
+ import { archiveWarnings, planningWarning, type ArchiveWarning } from "./overlap";
5
+ import {
6
+ isPublishable,
7
+ settleDelivery,
8
+ type DeliveryRecord,
9
+ type DeliverySettlement,
10
+ type UnscopedDelivery,
11
+ } from "./record";
12
+ import {
13
+ recordDelivery,
14
+ type DeliveryEdit,
15
+ type DeliveryRefusal,
16
+ type RecordDeliveryDeps,
17
+ } from "./record-delivery";
18
+ import type { BranchOutcome } from "./refresh";
19
+
20
+ export type ArchiveStatus = "archived" | "needs-branch" | "failed";
21
+
22
+ export interface ArchiveResult {
23
+ status: ArchiveStatus;
24
+ change: string;
25
+ /** Companion-relative archive directory, once archived. */
26
+ path: string | null;
27
+ record: DeliveryRecord;
28
+ settlement: DeliverySettlement;
29
+ outcomes: BranchOutcome[];
30
+ warnings: ArchiveWarning[];
31
+ /** `needs-branch` only: the branch to suggest, when it exists. */
32
+ suggestedBranch?: string | null;
33
+ message: string;
34
+ }
35
+
36
+ export interface ArchiveRequest {
37
+ change: string;
38
+ /** The repository `delivery` is recorded for; defaults to the Working Repository. */
39
+ repository?: string;
40
+ /** Recorded before the refresh, as `artifact link` would. */
41
+ delivery?: DeliveryEdit["delivery"];
42
+ /** Archive with an unlinked record instead of answering `needs-branch`. */
43
+ skipBranch?: boolean;
44
+ }
45
+
46
+ export type ArchiveOutcome = { refusal: DeliveryRefusal } | { result: ArchiveResult };
47
+
48
+ export interface ArchiveChangeDeps extends RecordDeliveryDeps {
49
+ runOpenSpec?: RunOpenSpec;
50
+ /**
51
+ * Called once before `openspec archive` starts, so warnings are visible even when the
52
+ * archive fails or hangs. Never called for `needs-branch` or a refusal.
53
+ */
54
+ onWarnings?: (warnings: ArchiveWarning[], unscoped: UnscopedDelivery[]) => void;
55
+ }
56
+
57
+ /**
58
+ * The Archive change stage: records and refreshes the delivery, collects overlap,
59
+ * dependency, and incomplete-planning warnings, then runs `openspec archive`. Without a
60
+ * recorded delivery and no `skipBranch`, nothing is archived and the result is
61
+ * `needs-branch`. Merge state is reported, never a refusal.
62
+ */
63
+ export async function archiveChange(
64
+ environment: DeliveryEnvironment,
65
+ request: ArchiveRequest,
66
+ deps: ArchiveChangeDeps = {},
67
+ ): Promise<ArchiveOutcome> {
68
+ const { change } = request;
69
+ const outcome = await recordDelivery(
70
+ environment,
71
+ {
72
+ changes: [change],
73
+ ...(request.repository ? { repository: request.repository } : {}),
74
+ ...(request.delivery ? { edit: { delivery: request.delivery } } : {}),
75
+ refresh: true,
76
+ // The settlement covers every repository, so each delivery's merge state is refreshed.
77
+ refreshAll: true,
78
+ requireDelivery: !request.skipBranch,
79
+ },
80
+ deps,
81
+ );
82
+ if ("refusal" in outcome) return outcome;
83
+
84
+ const runner = deps.runOpenSpec ?? runOpenSpec;
85
+ const cwd = environment.companionPath;
86
+ const [overlap, status] = await Promise.all([
87
+ archiveWarnings(environment.companionPath, change),
88
+ runner(["status", "--change", change, "--json"], { cwd }),
89
+ ]);
90
+ const planning = status.code === 0 ? planningWarning(change, status.stdout) : null;
91
+ const warnings = [...overlap, ...(planning ? [planning] : [])];
92
+
93
+ if ("needsBranch" in outcome) {
94
+ const { record, suggestedBranch } = outcome.needsBranch;
95
+ return {
96
+ result: {
97
+ status: "needs-branch",
98
+ change,
99
+ path: null,
100
+ record,
101
+ settlement: settleDelivery(record),
102
+ outcomes: [],
103
+ warnings,
104
+ suggestedBranch,
105
+ message: `No delivery branch is recorded for ${change}. Re-run with --branch <branch>, --not-applicable <reason>, or --skip-branch.`,
106
+ },
107
+ };
108
+ }
109
+
110
+ const [{ record, settlement, outcomes }] = outcome.results;
111
+ deps.onWarnings?.(warnings, settlement.unscoped);
112
+
113
+ const run = await runner(["archive", change, "--yes"], { cwd });
114
+ if (run.code !== 0) {
115
+ const detail = (run.stderr.trim() || run.stdout.trim()).split("\n").slice(-5).join("\n");
116
+ return {
117
+ result: {
118
+ status: "failed",
119
+ change,
120
+ path: null,
121
+ record,
122
+ settlement,
123
+ outcomes,
124
+ warnings,
125
+ message: `openspec archive ${change} failed (exit ${run.code ?? "unknown"}): ${detail}`,
126
+ },
127
+ };
128
+ }
129
+
130
+ const archived = await locateChange(environment.companionPath, change);
131
+ return {
132
+ result: {
133
+ status: "archived",
134
+ change,
135
+ path: archived?.archived ? companionRelative(environment, archived.dir) : null,
136
+ record,
137
+ settlement,
138
+ outcomes,
139
+ warnings,
140
+ message: archivedMessage(change, settlement),
141
+ },
142
+ };
143
+ }
144
+
145
+ function archivedMessage(change: string, settlement: DeliverySettlement): string {
146
+ if (isPublishable(settlement.status)) {
147
+ return `Archived ${change}; it can be published with mate-publish-artifact.`;
148
+ }
149
+ const missing =
150
+ settlement.missing.length > 0
151
+ ? ` No delivery is recorded for ${settlement.missing.join(", ")}, which its scopes name.`
152
+ : "";
153
+ return `Archived ${change}; publication waits until its delivery is merged (${settlement.status}).${missing}`;
154
+ }