@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
@@ -4,6 +4,8 @@ import fs from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
 
7
+ import { resolveSecretPolicy } from "../../../runtime/policy";
8
+ import { DEFAULT_SECRET_DENY_PATTERNS, type SecretPolicy } from "../../../runtime/secret-patterns";
7
9
  import { resolveGitInfoExcludePath } from "../git-utils";
8
10
  import type { InstallRequirement } from "../install-contract";
9
11
  import type { CapabilityPlugin, RuntimeContributionsByRuntime } from "../plugin";
@@ -368,14 +370,40 @@ export async function isTokensaveIndexed(repoPath: string): Promise<boolean> {
368
370
  return fileExists(path.join(repoPath, TOKENSAVE_STORE_DIR, TOKENSAVE_STORE_DATABASE_FILE));
369
371
  }
370
372
 
373
+ function secretExclude(pattern: string): string {
374
+ return `**/${pattern.replace(/^\/+/, "")}`;
375
+ }
376
+
377
+ /**
378
+ * Mate owns the excludes derived from the defaults and from the companion's
379
+ * current extra patterns. An extra removed from config is no longer
380
+ * recognizable, so its exclude stays: over-excluding from an index is harmless.
381
+ */
382
+ function reconcileSecretExcludes(existing: unknown, secrets: SecretPolicy): string[] {
383
+ const owned = new Set([...DEFAULT_SECRET_DENY_PATTERNS, ...secrets.deny].map(secretExclude));
384
+ const kept = (Array.isArray(existing) ? existing : []).filter(
385
+ (entry): entry is string => typeof entry === "string" && !owned.has(entry),
386
+ );
387
+ const managed = secrets.guardrails ? [...new Set(secrets.deny.map(secretExclude))] : [];
388
+ return [...kept, ...managed];
389
+ }
390
+
371
391
  /**
372
392
  * Reconciles Mate's single-graph branching posture into the working repo's
373
393
  * git-excluded `.tokensave/config.json` and deactivates existing branch
374
394
  * metadata. Best-effort by design: the file is created by `tokensave init`,
375
395
  * so an absent, unreadable, or unparseable config is left untouched rather
376
396
  * than treated as an error. Existing branch databases are never removed.
397
+ *
398
+ * With a secret policy, also reconciles the secret-file excludes: Mate's
399
+ * `**\/<pattern>` entries are appended while guardrails are on and removed
400
+ * when they are off, by exact value, so user-authored excludes keep their
401
+ * place. Without one, `exclude` is left as it is.
377
402
  */
378
- export async function ensureTokensaveBranchingPosture(repoPath: string): Promise<void> {
403
+ export async function ensureTokensaveBranchingPosture(
404
+ repoPath: string,
405
+ secrets?: SecretPolicy,
406
+ ): Promise<void> {
379
407
  const configPath = path.join(repoPath, TOKENSAVE_STORE_DIR, TOKENSAVE_STORE_CONFIG_FILE);
380
408
 
381
409
  let raw: string;
@@ -395,8 +423,13 @@ export async function ensureTokensaveBranchingPosture(repoPath: string): Promise
395
423
  return;
396
424
  }
397
425
 
426
+ const config = parsed as Record<string, unknown>;
398
427
  const next = JSON.stringify(
399
- { ...(parsed as Record<string, unknown>), ...TOKENSAVE_OWNED_POSTURE },
428
+ {
429
+ ...config,
430
+ ...TOKENSAVE_OWNED_POSTURE,
431
+ ...(secrets ? { exclude: reconcileSecretExcludes(config.exclude, secrets) } : {}),
432
+ },
400
433
  null,
401
434
  2,
402
435
  );
@@ -539,7 +572,7 @@ export function createTokensavePlugin(): CapabilityPlugin {
539
572
  ) {
540
573
  return;
541
574
  }
542
- await ensureTokensaveBranchingPosture(targetPath);
575
+ await ensureTokensaveBranchingPosture(targetPath, resolveSecretPolicy(ctx.companionPath));
543
576
  if (ctx.mode === "setup") {
544
577
  installTokensaveAgentIntegrations(ctx.activeProviders, targetPath);
545
578
  }
@@ -3,6 +3,8 @@ import crypto from "node:crypto";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
 
6
+ import { managedBlockMarkers, managedBlockStartPattern } from "../../runtime/managed-block";
7
+ import { removeEmptiedFile, writeAtomicThroughLink } from "../../runtime/write-atomic";
6
8
  import type {
7
9
  InstructionsService,
8
10
  McpServerDescriptor,
@@ -63,13 +65,6 @@ async function saveManifest(
63
65
  // Managed instruction blocks
64
66
  // ---------------------------------------------------------------------------
65
67
 
66
- function blockMarkers(frameworkName: string, blockKey: string): { start: string; end: string } {
67
- return {
68
- start: `<!-- ${frameworkName}:managed:${blockKey} start -->`,
69
- end: `<!-- ${frameworkName}:managed:${blockKey} end -->`,
70
- };
71
- }
72
-
73
68
  export function instructionBlockKey(pluginId: string, content: string): string {
74
69
  const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 12);
75
70
  return `${pluginId}:${hash}`;
@@ -82,7 +77,7 @@ export async function upsertManagedBlock(
82
77
  blockKey: string,
83
78
  content: string,
84
79
  ): Promise<void> {
85
- const { start, end } = blockMarkers(frameworkName, blockKey);
80
+ const { start, end } = managedBlockMarkers(frameworkName, blockKey);
86
81
  let existing = "";
87
82
  try {
88
83
  existing = await fs.readFile(filePath, "utf8");
@@ -93,7 +88,8 @@ export async function upsertManagedBlock(
93
88
  if (existing.includes(start)) return;
94
89
  const separator = existing.length === 0 || existing.endsWith("\n\n") ? "" : "\n";
95
90
  await fs.mkdir(path.dirname(filePath), { recursive: true });
96
- await fs.writeFile(filePath, existing + (existing ? separator : "") + block, "utf8");
91
+ /** A session-start hook may read the file while another session's prepare rewrites it. */
92
+ await writeAtomicThroughLink(filePath, existing + (existing ? separator : "") + block);
97
93
  }
98
94
 
99
95
  /**
@@ -113,11 +109,7 @@ export async function removeManagedBlocksForPlugin(
113
109
  } catch {
114
110
  return;
115
111
  }
116
- const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
117
- const keyPattern = new RegExp(
118
- `<!-- ${escape(frameworkName)}:managed:(${escape(pluginId)}:[0-9a-f]+) start -->`,
119
- "g",
120
- );
112
+ const keyPattern = managedBlockStartPattern(frameworkName, pluginId);
121
113
  const keys = [...existing.matchAll(keyPattern)].map((match) => match[1]);
122
114
  for (const key of keys) {
123
115
  if (keepKeys.has(key)) continue;
@@ -131,7 +123,7 @@ async function removeManagedBlock(
131
123
  frameworkName: string,
132
124
  blockKey: string,
133
125
  ): Promise<void> {
134
- const { start, end } = blockMarkers(frameworkName, blockKey);
126
+ const { start, end } = managedBlockMarkers(frameworkName, blockKey);
135
127
  let existing: string;
136
128
  try {
137
129
  existing = await fs.readFile(filePath, "utf8");
@@ -146,10 +138,10 @@ async function removeManagedBlock(
146
138
  const after = existing.slice(endIdx + end.length).replace(/^\n+/, "\n");
147
139
  const next = (before + after).replace(/^\n+/, "");
148
140
  if (!next.trim()) {
149
- await fs.unlink(filePath).catch(() => {});
141
+ await removeEmptiedFile(filePath).catch(() => {});
150
142
  return;
151
143
  }
152
- await fs.writeFile(filePath, next, "utf8");
144
+ await writeAtomicThroughLink(filePath, next);
153
145
  }
154
146
 
155
147
  // ---------------------------------------------------------------------------
@@ -8,10 +8,13 @@ import { ConfigError, type PluginDeclaration } from "../../../lib/orchestrator/t
8
8
 
9
9
  export const AUDIENCE_ENV = "MATE_AUDIENCE";
10
10
 
11
- /** The reserved audience holding the base set: always applied, never selected or listed. */
11
+ /**
12
+ * Mate 0.18.4 saved the base set under `audiences.base`. That shape is read
13
+ * back for compatibility, never written: the next save uses the root shape.
14
+ */
12
15
  export const BASE_AUDIENCE = "base";
13
16
 
14
- /** The base set's audience-scoped keys; companion-wide keys stay at the root. */
17
+ /** The keys 0.18.4 moved under `audiences.base`; companion-wide keys stayed at the root. */
15
18
  export const BASE_AUDIENCE_KEYS = ["capabilities", "plugins", "studio"] as const;
16
19
 
17
20
  export type AudienceResolution =
@@ -23,13 +26,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
23
26
  }
24
27
 
25
28
  function pluginList(value: unknown): unknown[] {
26
- if (
27
- isRecord(value) &&
28
- isRecord(value.audiences) &&
29
- Object.hasOwn(value.audiences, BASE_AUDIENCE)
30
- ) {
31
- return pluginList(normalizeBaseAudience(value));
32
- }
33
29
  return isRecord(value) && Array.isArray(value.plugins) ? value.plugins : [];
34
30
  }
35
31
 
@@ -91,7 +87,6 @@ export function declaredAudiences(raw: unknown): string[] {
91
87
  : [];
92
88
  }
93
89
 
94
- /** `MATE_AUDIENCE=base` selects the base set alone, the same as no audience. */
95
90
  export function resolveAudience(raw: unknown, audience: string | null): AudienceResolution {
96
91
  if (audience === null || audience === BASE_AUDIENCE) return { ok: true, active: null };
97
92
  const declared = declaredAudiences(raw);
@@ -104,34 +99,7 @@ export function resolveAudience(raw: unknown, audience: string | null): Audience
104
99
  * its own `policy` and `config`. An unknown audience contributes nothing.
105
100
  */
106
101
  export function effectivePluginDeclarations(raw: unknown, audience: string | null): unknown[] {
107
- return [...pluginList(raw), ...pluginList(audienceEntry(raw, audience))];
108
- }
109
-
110
- /** One effective declaration and its source: `null` for base, else the audience name. */
111
- export interface SourcedDeclaration {
112
- entry: unknown;
113
- audience: string | null;
114
- }
115
-
116
- /** `effectivePluginDeclarations`, each entry marked with the source that declared it. */
117
- export function effectivePluginDeclarationSources(
118
- raw: unknown,
119
- audience: string | null,
120
- ): SourcedDeclaration[] {
121
- return [
122
- ...pluginList(raw).map((entry) => ({ entry, audience: null })),
123
- ...pluginList(audienceEntry(raw, audience)).map((entry) => ({ entry, audience })),
124
- ];
125
- }
126
-
127
- /** The declarations of one audience only, without base. */
128
- export function audiencePluginDeclarations(raw: unknown, audience: string): unknown[] {
129
- return pluginList(audienceEntry(raw, audience));
130
- }
131
-
132
- /** The raw `audiences.<name>` entry, for fingerprinting what a layer depends on. */
133
- export function audienceDeclarationEntry(raw: unknown, audience: string): unknown {
134
- return audienceEntry(raw, audience);
102
+ return [...pluginList(normalizeBaseAudience(raw)), ...pluginList(audienceEntry(raw, audience))];
135
103
  }
136
104
 
137
105
  /**
@@ -140,7 +108,7 @@ export function audienceDeclarationEntry(raw: unknown, audience: string): unknow
140
108
  * version. Runtime declarations must come from `effectivePluginDeclarations`.
141
109
  */
142
110
  export function allPluginDeclarations(raw: unknown): unknown[] {
143
- const all = [...pluginList(raw)];
111
+ const all = [...pluginList(normalizeBaseAudience(raw))];
144
112
  for (const name of declaredAudiences(raw)) {
145
113
  all.push(...pluginList(audienceEntry(raw, name)));
146
114
  }
@@ -13,7 +13,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
13
13
  }
14
14
 
15
15
  /**
16
- * Raw read of the companion's base `plugins:` list, from either shape. Deliberately avoids
16
+ * Raw read of the companion's `plugins:` list. Deliberately avoids
17
17
  * `ConfigStore.load()` — hydration runs on every invocation and must never
18
18
  * create or migrate config files as a side effect.
19
19
  */
@@ -9,7 +9,7 @@ import type { Plugin } from "../plugin";
9
9
  import type { PluginRegistry } from "../registry";
10
10
  import {
11
11
  describeUnknownAudience,
12
- effectivePluginDeclarationSources,
12
+ effectivePluginDeclarations,
13
13
  readAudienceSelection,
14
14
  readFrameworkRaw,
15
15
  resolveAudience,
@@ -71,8 +71,7 @@ function activateDeclaredPlugin(plugin: Plugin): Plugin {
71
71
  /**
72
72
  * Registers the companion's effective plugins (base plus the active
73
73
  * `MATE_AUDIENCE`) into the active registry — compiled-in plugins first
74
- * (they are already registered), declared order after. Each registration is
75
- * marked with its source, so persisted passes can keep to base. Runs before cap-command detection on every invocation. No-op
74
+ * (they are already registered), declared order after. Runs before cap-command detection on every invocation. No-op
76
75
  * without a companion or `plugins:` entries; all diagnostics go to stderr;
77
76
  * never throws.
78
77
  */
@@ -90,12 +89,12 @@ export async function hydrateDynamicPlugins(deps: HydrateDynamicPluginsDeps = {}
90
89
  if (!audience.ok) {
91
90
  warn(`${describeUnknownAudience(audience)}; applying the base configuration only`);
92
91
  }
93
- const entries = effectivePluginDeclarationSources(raw, audience.ok ? audience.active : null);
92
+ const entries = effectivePluginDeclarations(raw, audience.ok ? audience.active : null);
94
93
  if (entries.length === 0) return;
95
94
 
96
95
  const registry = deps.registry ?? getActiveDistribution().registry;
97
96
 
98
- for (const { entry, audience: source } of entries) {
97
+ for (const entry of entries) {
99
98
  const { declaration, error } = validateDeclaration(entry);
100
99
  if (!declaration) {
101
100
  if (error) warn(error);
@@ -123,8 +122,6 @@ export async function hydrateDynamicPlugins(deps: HydrateDynamicPluginsDeps = {}
123
122
  registry.register({
124
123
  plugin: activateDeclaredPlugin(result.plugin),
125
124
  policy: declaration.policy ?? "optional",
126
- audience: source,
127
- packageName: declaration.package,
128
125
  });
129
126
  hydratedPackages.add(declaration.package);
130
127
  }
@@ -26,45 +26,40 @@ export interface PluginInstallResult {
26
26
  error?: string;
27
27
  }
28
28
 
29
- /** Runs `npm install` for the shared plugin workspace, honoring the user's ambient registry/auth config. */
30
- function defaultNpmInstall(workspaceRoot: string): { ok: boolean; detail?: string } {
31
- const result = spawnSync("npm", ["install", "--no-audit", "--no-fund", "--silent"], {
32
- cwd: workspaceRoot,
33
- encoding: "utf8",
34
- });
29
+ /**
30
+ * Runs `npm <command>` in the shared plugin workspace, honoring the user's ambient
31
+ * registry/auth config. `--loglevel=error` keeps npm's own error in stderr for the report.
32
+ */
33
+ function runNpm(
34
+ workspaceRoot: string,
35
+ command: string,
36
+ args: string[] = [],
37
+ ): { ok: boolean; detail?: string } {
38
+ const result = spawnSync(
39
+ "npm",
40
+ [command, "--no-audit", "--no-fund", "--loglevel=error", ...args],
41
+ { cwd: workspaceRoot, encoding: "utf8" },
42
+ );
35
43
  if (result.error || result.status !== 0) {
36
44
  return {
37
45
  ok: false,
38
46
  detail:
39
47
  result.error?.message ??
40
- result.stderr?.trim() ??
41
- `npm install exited with ${result.status}`,
48
+ (result.stderr?.trim() || `npm ${command} exited with ${result.status}`),
42
49
  };
43
50
  }
44
51
  return { ok: true };
45
52
  }
46
53
 
54
+ /** Runs `npm install` for the shared plugin workspace. */
55
+ const defaultNpmInstall: NpmInstallRunner = (workspaceRoot) => runNpm(workspaceRoot, "install");
56
+
47
57
  /** Version literals treated as moving targets: re-resolved via `npm update` on every run. */
48
58
  const MOVING_VERSION_TAGS = new Set(["latest", "canary"]);
49
59
 
50
60
  /** Runs `npm update <pkg...>` to force re-resolution of moving-tag-declared plugins every run. */
51
- function defaultNpmUpdate(
52
- workspaceRoot: string,
53
- packages: string[],
54
- ): { ok: boolean; detail?: string } {
55
- const result = spawnSync("npm", ["update", "--no-audit", "--no-fund", "--silent", ...packages], {
56
- cwd: workspaceRoot,
57
- encoding: "utf8",
58
- });
59
- if (result.error || result.status !== 0) {
60
- return {
61
- ok: false,
62
- detail:
63
- result.error?.message ?? result.stderr?.trim() ?? `npm update exited with ${result.status}`,
64
- };
65
- }
66
- return { ok: true };
67
- }
61
+ const defaultNpmUpdate: NpmUpdateRunner = (workspaceRoot, packages) =>
62
+ runNpm(workspaceRoot, "update", packages);
68
63
 
69
64
  async function readInstalledVersion(packageRoot: string): Promise<string | null> {
70
65
  try {
@@ -155,14 +150,15 @@ export async function installDeclaredPlugins(
155
150
 
156
151
  await writeWorkspaceManifest(workspaceRoot, desired);
157
152
  const installOutcome = await runNpmInstall(workspaceRoot);
153
+ // Failure is decided from `ok`, so an empty detail can never read as success.
158
154
  const installFailure = installOutcome.ok
159
155
  ? undefined
160
- : (installOutcome.detail ?? "npm install failed");
156
+ : installOutcome.detail || "npm install failed";
161
157
 
162
158
  let updateFailure: string | undefined;
163
159
  if (movingPackages.length > 0) {
164
160
  const updateOutcome = await runNpmUpdate(workspaceRoot, movingPackages);
165
- updateFailure = updateOutcome.ok ? undefined : (updateOutcome.detail ?? "npm update failed");
161
+ updateFailure = updateOutcome.ok ? undefined : updateOutcome.detail || "npm update failed";
166
162
  }
167
163
 
168
164
  const resolvedVersions = await Promise.all(
@@ -174,7 +170,7 @@ export async function installDeclaredPlugins(
174
170
  return sorted.map((declaration, index) => {
175
171
  const isMoving = MOVING_VERSION_TAGS.has(declaration.version);
176
172
  const failure = installFailure ?? (isMoving ? updateFailure : undefined);
177
- if (failure) {
173
+ if (failure !== undefined) {
178
174
  return { package: declaration.package, status: "failed", error: failure };
179
175
  }
180
176
  const resolvedVersion = resolvedVersions[index];