@uniqbit/mate-core 0.15.5 → 0.16.0-canary.1

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 (186) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +2 -2
  2. package/claude-plugin/hooks/hooks.json +3 -6
  3. package/claude-plugin/hooks/session-guidance.mjs +8 -0
  4. package/claude-plugin/hooks/ts-loader.mjs +19 -0
  5. package/package.json +7 -5
  6. package/src/cli/commands/artifact/artifact.ts +19 -3
  7. package/src/cli/commands/artifact/finish/command.ts +183 -57
  8. package/src/cli/commands/artifact/finish/engine.ts +80 -91
  9. package/src/cli/commands/artifact/finish/finisher.ts +26 -33
  10. package/src/cli/commands/artifact/finish/git.ts +34 -23
  11. package/src/cli/commands/artifact/finish/index.ts +9 -3
  12. package/src/cli/commands/artifact/finish/openspec.ts +225 -97
  13. package/src/cli/commands/artifact/pending/command.ts +175 -0
  14. package/src/cli/commands/artifact/pending/discovery.ts +249 -0
  15. package/src/cli/commands/artifact/pending/index.ts +17 -0
  16. package/src/cli/commands/cap/index-cmd.ts +9 -1
  17. package/src/cli/commands/cap/index.ts +2 -6
  18. package/src/cli/commands/cap/tokensave.ts +4 -4
  19. package/src/cli/commands/companion/companion.ts +5 -1
  20. package/src/cli/commands/companion/link.ts +2 -2
  21. package/src/cli/commands/companion/sync.ts +92 -0
  22. package/src/cli/commands/doctor.ts +0 -3
  23. package/src/cli/commands/launch/shared.ts +23 -5
  24. package/src/cli/commands/report/collector.ts +72 -78
  25. package/src/cli/commands/report/contract.ts +40 -1
  26. package/src/cli/commands/report/highlight.ts +27 -0
  27. package/src/cli/commands/report/index.ts +11 -18
  28. package/src/cli/commands/report/renderer.ts +199 -2
  29. package/src/cli/commands/report/types.ts +26 -1
  30. package/src/cli/commands/shared/companion-selection.ts +107 -10
  31. package/src/cli/commands/studio/areas.ts +68 -0
  32. package/src/cli/commands/studio/index.ts +69 -0
  33. package/src/cli/commands/studio/inventory.ts +55 -0
  34. package/src/cli/commands/studio/mate-inventory.ts +43 -0
  35. package/src/cli/commands/studio/openspec-cli.ts +198 -0
  36. package/src/cli/commands/studio/payload.ts +184 -0
  37. package/src/cli/commands/studio/routes.ts +2 -0
  38. package/src/cli/commands/studio/selection.ts +61 -0
  39. package/src/cli/commands/studio/server.ts +201 -0
  40. package/src/cli/commands/studio/snapshot.ts +63 -0
  41. package/src/cli/commands/studio/topology.ts +199 -0
  42. package/src/cli/commands/studio/views/client.ts +197 -0
  43. package/src/cli/commands/studio/views/companion-picker.tsx +91 -0
  44. package/src/cli/commands/studio/views/companion-selector.tsx +90 -0
  45. package/src/cli/commands/studio/views/dashboard/changes.tsx +107 -0
  46. package/src/cli/commands/studio/views/dashboard/index.tsx +19 -0
  47. package/src/cli/commands/studio/views/document.tsx +256 -0
  48. package/src/cli/commands/studio/views/error.tsx +20 -0
  49. package/src/cli/commands/studio/views/model.ts +34 -0
  50. package/src/cli/commands/studio/views/pairings.tsx +38 -0
  51. package/src/cli/commands/studio/views/skills/index.tsx +87 -0
  52. package/src/cli/commands/studio/views/specs/index.tsx +101 -0
  53. package/src/cli/commands/studio/views/styles.ts +389 -0
  54. package/src/cli/commands/studio/views/warnings.tsx +21 -0
  55. package/src/cli/commands/studio/views/workflow/index.tsx +21 -0
  56. package/src/cli/commands/studio/views/workflow/steps.ts +329 -0
  57. package/src/cli/commands/studio/views/workflow/transcript.tsx +190 -0
  58. package/src/cli/commands/unwrap.ts +70 -0
  59. package/src/cli/commands/wrap.ts +164 -0
  60. package/src/cli/main.ts +68 -19
  61. package/src/cli/parse-flags.ts +36 -11
  62. package/src/cli/usage.ts +12 -3
  63. package/src/framework.ts +1 -7
  64. package/src/hooks/session-banner.ts +64 -11
  65. package/src/hooks/session-guidance.ts +40 -0
  66. package/src/hooks/validate-artifact-path.ts +108 -35
  67. package/src/lib/fs-utils.ts +9 -0
  68. package/src/lib/install.ts +33 -0
  69. package/src/lib/orchestrator/adapters/base.ts +14 -125
  70. package/src/lib/orchestrator/adapters/claude.ts +0 -11
  71. package/src/lib/orchestrator/adapters/opencode.ts +2 -32
  72. package/src/lib/orchestrator/companion-git-sync.ts +94 -84
  73. package/src/lib/orchestrator/config-store.ts +2 -21
  74. package/src/lib/orchestrator/editor.ts +12 -22
  75. package/src/lib/orchestrator/framework-context.ts +17 -6
  76. package/src/lib/orchestrator/global-config-store.ts +1 -1
  77. package/src/lib/orchestrator/launcher.ts +97 -7
  78. package/src/lib/orchestrator/opencode-guidance.ts +4 -56
  79. package/src/lib/orchestrator/projection-claude-entry.ts +198 -0
  80. package/src/lib/orchestrator/projection-claude-skills.ts +120 -0
  81. package/src/lib/orchestrator/projection-companion-link.ts +62 -0
  82. package/src/lib/orchestrator/projection-entries.ts +377 -0
  83. package/src/lib/orchestrator/projection-record.ts +56 -0
  84. package/src/lib/orchestrator/projection-runtime-documents.ts +424 -0
  85. package/src/lib/orchestrator/projection-types.ts +169 -0
  86. package/src/lib/orchestrator/repo-local-registry.ts +37 -133
  87. package/src/lib/orchestrator/repo-local-store.ts +96 -0
  88. package/src/lib/orchestrator/setup-compatibilities.ts +1 -9
  89. package/src/lib/orchestrator/types.ts +1 -0
  90. package/src/lib/orchestrator/working-repo-projection.ts +366 -0
  91. package/src/lib/orchestrator/workspace-inventory.ts +1 -1
  92. package/src/lib/package-paths.ts +11 -1
  93. package/src/lib/public-npm.ts +2 -1
  94. package/src/lib/update-checker.ts +15 -9
  95. package/src/opencode/companion-hooks.ts +89 -245
  96. package/src/opencode/companion-policy.ts +35 -10
  97. package/src/opencode/index.ts +1 -0
  98. package/src/opencode/projected-guidance.ts +56 -0
  99. package/src/opencode/tui.tsx +13 -4
  100. package/src/playbooks/companion-guidance.ts +32 -116
  101. package/src/plugins.ts +0 -1
  102. package/src/runtime/companion-git-state.ts +156 -0
  103. package/src/runtime/companion-git.ts +203 -0
  104. package/src/runtime/companion-guidance.ts +222 -0
  105. package/src/runtime/companion-sync.ts +298 -0
  106. package/src/runtime/env-names.ts +30 -0
  107. package/src/runtime/env.ts +67 -35
  108. package/src/runtime/framework.ts +10 -0
  109. package/src/runtime/freshness.ts +58 -0
  110. package/src/runtime/index.ts +104 -0
  111. package/src/runtime/install.ts +30 -0
  112. package/src/runtime/policy.ts +66 -0
  113. package/src/runtime/projected-guidance.ts +45 -0
  114. package/src/runtime/projection.ts +224 -0
  115. package/src/runtime/repo-local.ts +64 -0
  116. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +56 -0
  117. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +48 -0
  118. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/tasks.md +22 -0
  119. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +31 -90
  120. package/src/templates/capabilities/openspec-cap/mate-v1/templates/design.md +3 -0
  121. package/src/templates/capabilities/openspec-cap/mate-v1/templates/explore-brief.md +6 -6
  122. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  123. package/src/templates/capabilities/openspec-cap/mate-v1/templates/tasks.md +3 -0
  124. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +30 -0
  125. package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +2 -2
  126. package/src/templates/mate-skills/agents/mate-artifact-publish/SKILL.md +185 -0
  127. package/src/templates/mate-skills/agents/mate-artifact-publish/references/openspec.md +227 -0
  128. package/src/templates/mate-skills/agents/mate-domain-modeling/SKILL.md +68 -0
  129. package/src/templates/mate-skills/agents/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
  130. package/src/templates/mate-skills/agents/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
  131. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +14 -0
  132. package/src/templates/mate-skills/agents/mate-grill-with-docs/SKILL.md +29 -0
  133. package/src/templates/mate-skills/agents/mate-grilling/SKILL.md +49 -0
  134. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +156 -0
  135. package/src/templates/{capabilities/openspec-cap/mate-skills → mate-skills}/agents/mate-openspec-backfill/SKILL.md +4 -2
  136. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +139 -0
  137. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +503 -0
  138. package/src/templates/mate-skills/claude/mate-artifact-publish/SKILL.md +185 -0
  139. package/src/templates/mate-skills/claude/mate-artifact-publish/references/openspec.md +227 -0
  140. package/src/templates/mate-skills/claude/mate-domain-modeling/SKILL.md +68 -0
  141. package/src/templates/mate-skills/claude/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
  142. package/src/templates/mate-skills/claude/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
  143. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +14 -0
  144. package/src/templates/mate-skills/claude/mate-grill-with-docs/SKILL.md +29 -0
  145. package/src/templates/mate-skills/claude/mate-grilling/SKILL.md +49 -0
  146. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +156 -0
  147. package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +67 -0
  148. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +139 -0
  149. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +503 -0
  150. package/src/templates/report-assets/README.md +32 -0
  151. package/src/templates/report-assets/mermaid.LICENSE +21 -0
  152. package/src/templates/report-assets/mermaid.min.js +4376 -0
  153. package/src/templates/root/TEMPLATE_CLAUDE.md +1 -9
  154. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +1476 -197
  155. package/src/tools/setup/capabilities/graphify.ts +16 -7
  156. package/src/tools/setup/capabilities/openspec.ts +63 -56
  157. package/src/tools/setup/capabilities/tokensave.ts +116 -2
  158. package/src/tools/setup/engine.ts +34 -6
  159. package/src/tools/setup/mate.ts +42 -13
  160. package/src/tools/setup/plugin.ts +9 -0
  161. package/src/tools/setup/plugins/guidance.ts +11 -1
  162. package/src/tools/setup/providers/claude-format.ts +49 -4
  163. package/src/tools/setup/providers/claude-plugin-hooks.ts +117 -0
  164. package/src/tools/setup/providers/claude.ts +55 -220
  165. package/src/tools/setup/providers/opencode.ts +41 -14
  166. package/src/tools/setup/runtime-documents.ts +174 -0
  167. package/src/tools/setup/surface-target.ts +50 -0
  168. package/src/tools/setup/working-repo-cleanup.ts +33 -26
  169. package/src/tools/setup/working-repo-local-state.ts +21 -1
  170. package/src/tools/setup.ts +25 -3
  171. package/wrappers/bin/graphify +57 -8
  172. package/wrappers/bin/openspec +50 -3
  173. package/claude-plugin/hooks/artifact-finish-nudge.mjs +0 -8
  174. package/src/cli/commands/cap/headroom.ts +0 -52
  175. package/src/cli/commands/workspace/list.ts +0 -25
  176. package/src/cli/commands/workspace/materialize.ts +0 -46
  177. package/src/cli/commands/workspace/workspace.ts +0 -22
  178. package/src/hooks/artifact-finish-nudge.ts +0 -244
  179. package/src/lib/orchestrator/headroom/proxy.ts +0 -116
  180. package/src/lib/orchestrator/workspace-materialize.ts +0 -80
  181. package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/SKILL.md +0 -51
  182. package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/references/openspec.md +0 -134
  183. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +0 -58
  184. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +0 -139
  185. package/src/tools/setup/capabilities/headroom.ts +0 -57
  186. /package/src/cli/commands/{workspace → companion}/open.ts +0 -0
@@ -31,6 +31,9 @@ import {
31
31
 
32
32
  // Storage contract: <companionPath>/.graphify/<repositoryId>/graphify-out/
33
33
  export const GRAPHIFY_STORE_SEGMENT = ".graphify";
34
+
35
+ /** Skill tree the external `graphify install` CLI writes into a runtime directory. */
36
+ export const GRAPHIFY_SKILL_NAME = "graphify";
34
37
  export const GRAPHIFY_OUTPUT_SUBDIR = "graphify-out";
35
38
 
36
39
  const GRAPHIFY_INSTALL_CMD = `uv tool install graphifyy`;
@@ -271,9 +274,12 @@ export function createGraphifyPlugin(deps: GraphifyPluginDeps = {}): CapabilityP
271
274
  // every setup/sync so it survives graphifyy regenerating the skill.
272
275
  const patchSkillTree =
273
276
  providerId === "claude" ? patchClaudeSkillTree : patchOpenCodeSkillTree;
274
- await patchSkillTree(ctx.companionPath, "graphify", rewriteGraphifySkillOutputPaths, {
275
- excludeFiles: GRAPHIFY_SKILL_REWRITE_EXCLUDE,
276
- });
277
+ await patchSkillTree(
278
+ ctx.companionPath,
279
+ GRAPHIFY_SKILL_NAME,
280
+ rewriteGraphifySkillOutputPaths,
281
+ { excludeFiles: GRAPHIFY_SKILL_REWRITE_EXCLUDE },
282
+ );
277
283
  },
278
284
 
279
285
  async teardown(ctx: SetupContext) {
@@ -282,10 +288,13 @@ export function createGraphifyPlugin(deps: GraphifyPluginDeps = {}): CapabilityP
282
288
  // Remove the Graphify-managed skills directory (written by the
283
289
  // external CLI, so removed here rather than via a declaration).
284
290
  try {
285
- await fs.rm(path.join(ctx.companionPath, providerDir, "skills", "graphify"), {
286
- recursive: true,
287
- force: true,
288
- });
291
+ await fs.rm(
292
+ path.join(ctx.companionPath, providerDir, "skills", GRAPHIFY_SKILL_NAME),
293
+ {
294
+ recursive: true,
295
+ force: true,
296
+ },
297
+ );
289
298
  } catch {
290
299
  /* not present */
291
300
  }
@@ -32,14 +32,17 @@ export const OPENSPEC_SKILLS = [
32
32
  "openspec-archive-change",
33
33
  ] as const;
34
34
 
35
- // Mate-authored skills (not generated by the openspec CLI) that ship alongside the
36
- // openspec workflow skills and are installed per active provider.
37
- const LEGACY_CLAUDE_STATE_PATTERN = /^mate-artifact-finish\..+\.json$/;
35
+ // Mate-authored skills (not generated by the openspec CLI) are sourced from the
36
+ // shared Mate skill tree and installed per active provider while OpenSpec is enabled.
38
37
 
39
- const MATE_V1_SCHEMA_SOURCE = path.join(
38
+ /** Every Mate-owned schema is installed for availability; only one is ever activated as the companion default. */
39
+ const MATE_SCHEMA_NAMES = ["mate-v1", "mate-minimal"] as const;
40
+
41
+ const MATE_SCHEMA_SOURCE_ROOT = path.join(
40
42
  import.meta.dirname,
41
- "../../../templates/capabilities/openspec-cap/mate-v1",
43
+ "../../../templates/capabilities/openspec-cap",
42
44
  );
45
+ const MATE_CONVENTIONS_SOURCE = path.join(MATE_SCHEMA_SOURCE_ROOT, "openspec-conventions.yaml");
43
46
 
44
47
  const OPENSPEC_TOOL_DIRS = {
45
48
  claude: ".claude",
@@ -57,6 +60,7 @@ export interface OpenSpecPluginDeps {
57
60
  isCommandOnPath?: typeof isCommandOnPath;
58
61
  confirm?: typeof confirm;
59
62
  pathValue?: () => string;
63
+ wrapperBinPath?: () => string;
60
64
  getInstalledVersion?: (pathValue: string) => Promise<string | undefined>;
61
65
  fetchLatestVersion?: () => Promise<string | undefined>;
62
66
  }
@@ -95,8 +99,12 @@ function getOpenSpecConfigPath(companionPath: string): string {
95
99
  return path.join(companionPath, "openspec", "config.yaml");
96
100
  }
97
101
 
98
- function getMateV1SchemaDir(companionPath: string): string {
99
- return path.join(companionPath, "openspec", "schemas", "mate-v1");
102
+ function getMateSchemaDir(companionPath: string, schemaName: string): string {
103
+ return path.join(companionPath, "openspec", "schemas", schemaName);
104
+ }
105
+
106
+ function getMateConventionsPath(companionPath: string): string {
107
+ return path.join(companionPath, "openspec", "mate-conventions.yaml");
100
108
  }
101
109
 
102
110
  export async function teardownOpenspecSkills(
@@ -154,21 +162,15 @@ async function reconcileOpenSpecTools(
154
162
 
155
163
  // The openspec CLI generates its own workflow skills above; layer the
156
164
  // mate-authored skills on top for the same active providers.
157
- if (ctx.config.git === "auto") {
158
- await applyMateOpenspecSkills(ctx.companionPath, tools);
159
- } else {
160
- for (const tool of tools) {
161
- await teardownMateSkills(getSkillsDir(ctx.companionPath, tool), ctx.companionPath);
162
- }
163
- }
165
+ await applyMateOpenspecSkills(ctx.companionPath, tools);
164
166
  }
165
167
 
166
- async function warnIfOpenSpecOutdated(
168
+ async function updateOpenSpecIfOutdated(
167
169
  ctx: SetupContext,
168
170
  deps: Required<
169
171
  Pick<
170
172
  OpenSpecPluginDeps,
171
- "installCommand" | "confirm" | "pathValue" | "getInstalledVersion" | "fetchLatestVersion"
173
+ "installCommand" | "pathValue" | "getInstalledVersion" | "fetchLatestVersion"
172
174
  >
173
175
  >,
174
176
  ): Promise<void> {
@@ -178,21 +180,9 @@ async function warnIfOpenSpecOutdated(
178
180
  const latest = await deps.fetchLatestVersion();
179
181
  if (!latest || !isNewer(latest, installed)) return;
180
182
 
181
- if (ctx.mode !== "setup") {
182
- process.stderr.write(
183
- `openspec: installed version ${installed} is outdated (latest ${latest}). Run \`${OPENSPEC_INSTALL_COMMAND}\` to upgrade.\n`,
184
- );
185
- return;
186
- }
187
-
188
183
  process.stdout.write(
189
- `openspec: installed version ${installed} is outdated (latest ${latest}).\n ${OPENSPEC_INSTALL_COMMAND}\n`,
184
+ `openspec: installed version ${installed} is outdated (latest ${latest}). Updating now:\n ${OPENSPEC_INSTALL_COMMAND}\n`,
190
185
  );
191
- const ok = await deps.confirm("Upgrade openspec now?");
192
- if (!ok) {
193
- process.stderr.write("openspec: upgrade skipped - continuing with the installed version\n");
194
- return;
195
- }
196
186
 
197
187
  try {
198
188
  await deps.installCommand("npm", ["install", "-g", "@fission-ai/openspec@latest"], {
@@ -218,7 +208,7 @@ async function ensureOpenSpecInstalled(
218
208
  >,
219
209
  ): Promise<boolean> {
220
210
  if (deps.isCommandOnPath("openspec", deps.pathValue())) {
221
- await warnIfOpenSpecOutdated(ctx, deps);
211
+ await updateOpenSpecIfOutdated(ctx, deps);
222
212
  return true;
223
213
  }
224
214
 
@@ -266,11 +256,8 @@ async function ensureOpenSpecInstalled(
266
256
  return true;
267
257
  }
268
258
 
269
- async function teardownOpenSpecSchema(companionPath: string): Promise<void> {
270
- const schemaDir = getMateV1SchemaDir(companionPath);
271
- await fs.rm(schemaDir, { recursive: true, force: true });
272
- await pruneEmptyAncestors(schemaDir, companionPath);
273
-
259
+ /** Only the config Mate itself wrote is removed; a user-edited config.yaml is left alone. */
260
+ async function removeManagedOpenSpecConfig(companionPath: string): Promise<void> {
274
261
  const configPath = getOpenSpecConfigPath(companionPath);
275
262
  try {
276
263
  const content = await fs.readFile(configPath, "utf8");
@@ -283,13 +270,39 @@ async function teardownOpenSpecSchema(companionPath: string): Promise<void> {
283
270
  }
284
271
  }
285
272
 
273
+ async function teardownOpenSpecSchema(companionPath: string): Promise<void> {
274
+ for (const schemaName of MATE_SCHEMA_NAMES) {
275
+ const schemaDir = getMateSchemaDir(companionPath, schemaName);
276
+ await fs.rm(schemaDir, { recursive: true, force: true });
277
+ await pruneEmptyAncestors(schemaDir, companionPath);
278
+ }
279
+
280
+ await fs.rm(getMateConventionsPath(companionPath), { force: true });
281
+ await removeManagedOpenSpecConfig(companionPath);
282
+ }
283
+
286
284
  async function reconcileOpenSpecSchema(ctx: SetupContext): Promise<void> {
285
+ await Promise.all(
286
+ MATE_SCHEMA_NAMES.map((schemaName) =>
287
+ mergeDir(
288
+ path.join(MATE_SCHEMA_SOURCE_ROOT, schemaName),
289
+ getMateSchemaDir(ctx.companionPath, schemaName),
290
+ ),
291
+ ),
292
+ );
293
+ await fs.rm(
294
+ path.join(getMateSchemaDir(ctx.companionPath, "mate-v1"), "templates", "explore-brief.md"),
295
+ { force: true },
296
+ );
297
+ await fs.copyFile(MATE_CONVENTIONS_SOURCE, getMateConventionsPath(ctx.companionPath));
298
+
299
+ // Availability is unconditional so any change may select a Mate schema;
300
+ // activation stays opt-in through the companion-level setup selection.
287
301
  if (getOpenSpecSchemaSelection(ctx.config.capabilities ?? []) !== "mate-v1") {
288
- await teardownOpenSpecSchema(ctx.companionPath);
302
+ await removeManagedOpenSpecConfig(ctx.companionPath);
289
303
  return;
290
304
  }
291
305
 
292
- await mergeDir(MATE_V1_SCHEMA_SOURCE, getMateV1SchemaDir(ctx.companionPath));
293
306
  const configPath = getOpenSpecConfigPath(ctx.companionPath);
294
307
  await fs.mkdir(path.dirname(configPath), { recursive: true });
295
308
 
@@ -310,6 +323,7 @@ export function createOpenspecPlugin(deps: OpenSpecPluginDeps = {}): CapabilityP
310
323
  isCommandOnPath: deps.isCommandOnPath ?? isCommandOnPath,
311
324
  confirm: deps.confirm ?? confirm,
312
325
  pathValue: deps.pathValue ?? (() => process.env.PATH ?? ""),
326
+ wrapperBinPath: deps.wrapperBinPath ?? getWrapperBinPath,
313
327
  getInstalledVersion: deps.getInstalledVersion ?? defaultGetInstalledVersion,
314
328
  fetchLatestVersion: deps.fetchLatestVersion ?? defaultFetchLatestVersion,
315
329
  };
@@ -333,11 +347,18 @@ export function createOpenspecPlugin(deps: OpenSpecPluginDeps = {}): CapabilityP
333
347
  "Skill(openspec-propose)",
334
348
  "Skill(openspec-apply-change)",
335
349
  "Skill(openspec-archive-change)",
336
- "Skill(mate-artifact-finish)",
350
+ "Skill(mate-artifact-publish)",
337
351
  "Skill(mate-openspec-backfill)",
352
+ "Skill(mate-interview-me)",
353
+ "Skill(mate-grill-me)",
354
+ "Skill(mate-grilling)",
355
+ "Skill(mate-grill-with-docs)",
356
+ "Skill(mate-domain-modeling)",
357
+ "Skill(mate-show-me)",
358
+ "Skill(mate-simplify-code)",
338
359
  "Bash(openspec:*)",
339
360
  `Bash(${FRAMEWORK_NAME} cap graphify:*)`,
340
- `Bash(${path.join(getWrapperBinPath(), "openspec")}:*)`,
361
+ `Bash(${path.join(depsWithDefaults.wrapperBinPath(), "openspec")}:*)`,
341
362
  ],
342
363
  },
343
364
  };
@@ -375,23 +396,9 @@ export function createOpenspecPlugin(deps: OpenSpecPluginDeps = {}): CapabilityP
375
396
  },
376
397
  forProvider: {
377
398
  claude: {
378
- // The archive-finish nudge hook ships in the bundled mate Claude
379
- // plugin and self-gates at runtime; openspec no longer contributes a
380
- // Claude hook file. Only legacy per-session nudge state is migrated.
381
- async apply(ctx: SetupContext) {
382
- const stateDir = path.join(ctx.companionPath, ".claude", "state");
383
- try {
384
- const entries = await fs.readdir(stateDir);
385
- await Promise.all(
386
- entries
387
- .filter((entry) => LEGACY_CLAUDE_STATE_PATTERN.test(entry))
388
- .map((entry) => fs.unlink(path.join(stateDir, entry))),
389
- );
390
- await pruneEmptyAncestors(stateDir, ctx.companionPath);
391
- } catch {
392
- /* no legacy state */
393
- }
394
- },
399
+ // Publication is explicit (`mate-artifact-publish`), so openspec
400
+ // contributes no archive-triggered Claude hook and no hook state.
401
+ async apply() {},
395
402
  async teardown(ctx: SetupContext) {
396
403
  await teardownToolRuntime(ctx.companionPath, "claude");
397
404
  },
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { constants as fsConstants } from "node:fs";
2
3
  import fs from "node:fs/promises";
3
4
  import path from "node:path";
4
5
 
@@ -10,7 +11,12 @@ export { TOKENSAVE_WORKING_REPO_EXCLUDE_ENTRIES } from "./tokensave-shared";
10
11
 
11
12
  export const TOKENSAVE_SUPPORTED_AGENTS = new Set(["claude", "opencode"]);
12
13
  export const TOKENSAVE_STORE_DIR = ".tokensave";
14
+ export const TOKENSAVE_STORE_CONFIG_FILE = "config.json";
13
15
  export const TOKENSAVE_MIN_RUST_VERSION = "1.91.0";
16
+ /** Single-graph posture Mate owns in the per-project store config; every other key is left as found. */
17
+ const TOKENSAVE_OWNED_POSTURE = { auto_track: false, suppress_scope_warning: true } as const;
18
+ const TOKENSAVE_BRANCH_META_FILE = "branch-meta.json";
19
+ const TOKENSAVE_DISABLED_BRANCH_META_FILE = "branch-meta.json.mate-disabled";
14
20
  const TOKENSAVE_STORE_EXCLUDE_ENTRY = `${TOKENSAVE_STORE_DIR}/`;
15
21
  const TOKENSAVE_BREW_INSTALL_CMD = "brew install aovestdipaperino/tap/tokensave";
16
22
  const TOKENSAVE_CARGO_INSTALL_CMD = "cargo install --locked tokensave";
@@ -101,6 +107,21 @@ async function tokensaveInstalled(repoPath: string): Promise<boolean> {
101
107
  return result.ok;
102
108
  }
103
109
 
110
+ /**
111
+ * Upgrades the installed binary without making setup depend on network
112
+ * availability. `--kill` keeps the non-interactive setup path from hanging on
113
+ * a running Tokensave MCP process.
114
+ */
115
+ async function upgradeTokensave(repoPath: string): Promise<void> {
116
+ const result = tokensaveDeps.run(["upgrade", "--kill"], repoPath);
117
+ if (result.ok) return;
118
+
119
+ const detail = result.stderr.trim();
120
+ process.stderr.write(
121
+ `tokensave: automatic upgrade failed${detail ? `: ${detail}` : ""} - continuing with the installed version\n`,
122
+ );
123
+ }
124
+
104
125
  // Global agent integration (MCP entry, session hooks, wildcard permission grant, and
105
126
  // tokensave's own config bookkeeping) is owned by tokensave's installer — Mate never
106
127
  // hand-edits ~/.tokensave/config.toml. Runs in setup mode only; the installer is
@@ -169,8 +190,12 @@ function getTokensaveInstallPlan(): { command: string; run: () => Promise<void>
169
190
  return undefined;
170
191
  }
171
192
 
172
- export async function ensureTokensaveInstalled(repoPath: string): Promise<boolean> {
193
+ export async function ensureTokensaveInstalled(
194
+ repoPath: string,
195
+ options: { upgrade?: boolean } = {},
196
+ ): Promise<boolean> {
173
197
  if (await tokensaveInstalled(repoPath)) {
198
+ if (options.upgrade) await upgradeTokensave(repoPath);
174
199
  return true;
175
200
  }
176
201
 
@@ -230,6 +255,90 @@ export async function ensureTokensaveStoreExcluded(repoPath: string): Promise<vo
230
255
  await fs.writeFile(excludePath, lines.join("\n") + "\n", "utf8");
231
256
  }
232
257
 
258
+ /**
259
+ * Deactivates Tokensave's multi-branch mode without deleting branch databases.
260
+ * The metadata is preserved in a Mate-owned backup so the migration is reversible.
261
+ */
262
+ async function deactivateTokensaveBranchMetadata(repoPath: string): Promise<void> {
263
+ const storePath = path.join(repoPath, TOKENSAVE_STORE_DIR);
264
+ const branchMetaPath = path.join(storePath, TOKENSAVE_BRANCH_META_FILE);
265
+ const disabledBranchMetaPath = path.join(storePath, TOKENSAVE_DISABLED_BRANCH_META_FILE);
266
+
267
+ let branchMeta: string;
268
+ try {
269
+ branchMeta = await fs.readFile(branchMetaPath, "utf8");
270
+ } catch {
271
+ return;
272
+ }
273
+
274
+ let removeActiveMetadata = false;
275
+ try {
276
+ await fs.copyFile(branchMetaPath, disabledBranchMetaPath, fsConstants.COPYFILE_EXCL);
277
+ removeActiveMetadata = true;
278
+ } catch (error) {
279
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
280
+ return;
281
+ }
282
+
283
+ try {
284
+ removeActiveMetadata = (await fs.readFile(disabledBranchMetaPath, "utf8")) === branchMeta;
285
+ } catch {
286
+ return;
287
+ }
288
+ }
289
+
290
+ if (removeActiveMetadata) {
291
+ await fs.unlink(branchMetaPath).catch(() => {});
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Reconciles Mate's single-graph branching posture into the working repo's
297
+ * git-excluded `.tokensave/config.json` and deactivates existing branch
298
+ * metadata. Best-effort by design: the file is created by `tokensave init`,
299
+ * so an absent, unreadable, or unparseable config is left untouched rather
300
+ * than treated as an error. Existing branch databases are never removed.
301
+ */
302
+ export async function ensureTokensaveBranchingPosture(repoPath: string): Promise<void> {
303
+ const configPath = path.join(repoPath, TOKENSAVE_STORE_DIR, TOKENSAVE_STORE_CONFIG_FILE);
304
+
305
+ let raw: string;
306
+ try {
307
+ raw = await fs.readFile(configPath, "utf8");
308
+ } catch {
309
+ return;
310
+ }
311
+
312
+ let parsed: unknown;
313
+ try {
314
+ parsed = JSON.parse(raw);
315
+ } catch {
316
+ return;
317
+ }
318
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
319
+ return;
320
+ }
321
+
322
+ const next = JSON.stringify(
323
+ { ...(parsed as Record<string, unknown>), ...TOKENSAVE_OWNED_POSTURE },
324
+ null,
325
+ 2,
326
+ );
327
+ const serialized = `${next}\n`;
328
+ if (serialized === raw) {
329
+ await deactivateTokensaveBranchMetadata(repoPath);
330
+ return;
331
+ }
332
+
333
+ try {
334
+ await fs.writeFile(configPath, serialized, "utf8");
335
+ } catch {
336
+ /** Best-effort: the store config is not Mate's to guarantee. */
337
+ }
338
+
339
+ await deactivateTokensaveBranchMetadata(repoPath);
340
+ }
341
+
233
342
  async function teardownDriver(repoPath: string, providers: string[]) {
234
343
  const agents = providers.filter((p) => TOKENSAVE_SUPPORTED_AGENTS.has(p));
235
344
  if (agents.length === 0) return;
@@ -325,9 +434,14 @@ export function createTokensavePlugin(): CapabilityPlugin {
325
434
  if (ctx.activeProviders.filter((p) => TOKENSAVE_SUPPORTED_AGENTS.has(p)).length === 0) return;
326
435
 
327
436
  const targetPath = ctx.repoPath ?? ctx.companionPath;
328
- if (!(await ensureTokensaveInstalled(targetPath))) {
437
+ if (
438
+ !(await ensureTokensaveInstalled(targetPath, {
439
+ upgrade: ctx.mode === "setup",
440
+ }))
441
+ ) {
329
442
  return;
330
443
  }
444
+ await ensureTokensaveBranchingPosture(targetPath);
331
445
  if (ctx.mode === "setup") {
332
446
  installTokensaveAgentIntegrations(ctx.activeProviders, targetPath);
333
447
  }
@@ -1,6 +1,7 @@
1
1
  // oxlint-disable no-await-in-loop
2
2
  import { getActiveDistribution } from "../../distribution";
3
3
  import { FRAMEWORK_NAME } from "../../framework";
4
+ import type { RenderedRuntimeDocument } from "../../lib/orchestrator/projection-types";
4
5
  import type { FrameworkConfig } from "../../lib/orchestrator/types";
5
6
  import { collectHostingProviders, ContextServiceMediator } from "./context-services";
6
7
  import type {
@@ -12,6 +13,7 @@ import type {
12
13
  } from "./plugin";
13
14
  import { reconcileClaudeContributions } from "./providers/claude";
14
15
  import { reconcileOpenCodeContributions } from "./providers/opencode";
16
+ import { renderDocumentsForTarget } from "./runtime-documents";
15
17
  import { normalizeRegistration, type NormalizedRegistration } from "./registry";
16
18
 
17
19
  export interface SetupInstallationPlanAction {
@@ -189,16 +191,14 @@ const RUNTIME_SURFACE_RECONCILERS: Record<
189
191
  };
190
192
 
191
193
  /**
192
- * Reconcile declared Capability contributions through every active runtime's
193
- * Runtime Surface (spec: plugin-engine). Runs after all plugin phases, so
194
- * providers are always done first. Disabled capabilities participate with
195
- * `enabled: false` and drive teardown of their managed entries.
194
+ * The declared Capability contributions of one pass, keyed by runtime. Shared
195
+ * by both targets: what a companion pass writes is what a working pass renders.
196
196
  */
197
- async function reconcileCapabilityContributions(
197
+ async function collectContributionInputs(
198
198
  ctx: SetupContext,
199
199
  plugins: Plugin[],
200
200
  plan: SetupInstallationPlan,
201
- ): Promise<void> {
201
+ ): Promise<Map<string, CapabilityContributionInput[]>> {
202
202
  // The plan already resolved enablement (policy, saved selection, package
203
203
  // manager requirements); mirror it instead of recomputing.
204
204
  const enabledByPluginId = new Map(
@@ -225,10 +225,38 @@ async function reconcileCapabilityContributions(
225
225
  inputsByRuntime.set(runtimeId, inputs);
226
226
  }
227
227
  }
228
+ return inputsByRuntime;
229
+ }
228
230
 
231
+ /**
232
+ * Reconcile declared Capability contributions through every active runtime's
233
+ * Runtime Surface (spec: plugin-engine). Runs after all plugin phases, so
234
+ * providers are always done first. Disabled capabilities participate with
235
+ * `enabled: false` and drive teardown of their managed entries.
236
+ */
237
+ async function reconcileCapabilityContributions(
238
+ ctx: SetupContext,
239
+ plugins: Plugin[],
240
+ plan: SetupInstallationPlan,
241
+ ): Promise<void> {
242
+ const inputsByRuntime = await collectContributionInputs(ctx, plugins, plan);
229
243
  for (const [runtimeId, inputs] of inputsByRuntime) {
230
244
  const reconcile = RUNTIME_SURFACE_RECONCILERS[runtimeId];
231
245
  if (!reconcile || inputs.length === 0) continue;
232
246
  await reconcile(ctx, inputs);
233
247
  }
234
248
  }
249
+
250
+ /**
251
+ * The same declarations, rendered rather than written: a working-target pass
252
+ * produces the documents and the Projection Root places them, so no Runtime
253
+ * Surface reaches inside a Working Repository.
254
+ */
255
+ export async function renderRuntimeDocuments(
256
+ ctx: SetupContext,
257
+ registrations: PluginRegistration[],
258
+ plan: SetupInstallationPlan,
259
+ ): Promise<RenderedRuntimeDocument[]> {
260
+ const plugins = registrations.map((entry) => normalizeRegistration(entry).plugin);
261
+ return renderDocumentsForTarget(ctx, await collectContributionInputs(ctx, plugins, plan));
262
+ }
@@ -3,18 +3,22 @@ import path from "node:path";
3
3
 
4
4
  import { pruneEmptyAncestors } from "./utils";
5
5
 
6
- export const MATE_ARTIFACT_SKILLS = ["mate-artifact-finish"] as const;
6
+ export const MATE_ARTIFACT_SKILLS = ["mate-artifact-publish"] as const;
7
7
  export const MATE_SKILLS = [
8
- "mate-artifact-finish",
8
+ "mate-artifact-publish",
9
9
  "mate-create-report",
10
10
  "mate-openspec-backfill",
11
+ "mate-interview-me",
12
+ "mate-grill-me",
13
+ "mate-grilling",
14
+ "mate-grill-with-docs",
15
+ "mate-domain-modeling",
16
+ "mate-show-me",
17
+ "mate-simplify-code",
11
18
  ] as const;
12
- const LEGACY_MATE_SKILLS = ["mate-openspec-artifact-finish"] as const;
19
+ const LEGACY_MATE_SKILLS = ["mate-artifact-finish", "mate-openspec-artifact-finish"] as const;
13
20
 
14
- const MATE_SKILLS_SOURCE = path.join(
15
- import.meta.dirname,
16
- "../../templates/capabilities/openspec-cap/mate-skills",
17
- );
21
+ const MATE_SKILLS_SOURCE = path.join(import.meta.dirname, "../../templates/mate-skills");
18
22
 
19
23
  const MATE_CREATE_REPORT_SKILL = `---
20
24
  name: mate-create-report
@@ -55,7 +59,27 @@ Write a JSON document with this shape:
55
59
  }
56
60
  \`\`\`
57
61
 
58
- Supported section types are \`metadata\`, \`metrics\`, \`key-value\`, \`table\`, \`statuses\`, and \`text\`. Use only strings, numbers, booleans, and null values. Section IDs must be unique. Validate required fields before invoking the CLI.
62
+ Supported section types are \`metadata\`, \`metrics\`, \`key-value\`, \`table\`, \`statuses\`, \`text\`, \`diagram\`, and \`diff\`. Use only strings, numbers, booleans, and null values. Section IDs must be unique. Validate required fields before invoking the CLI.
63
+
64
+ ## Visual Sections
65
+
66
+ A \`diagram\` section carries exactly one payload. \`mermaid\` holds diagram source the report draws as a picture; \`text\` holds a monospace ASCII sketch or pseudocode block, kept with its line breaks and indentation. Supplying both payloads or neither fails validation.
67
+
68
+ \`\`\`json
69
+ { "id": "structure", "title": "Structure", "type": "diagram", "mermaid": "classDiagram\\n Report --> Renderer" }
70
+ \`\`\`
71
+
72
+ \`\`\`json
73
+ { "id": "sketch", "title": "Sketch", "type": "diagram", "text": "root\\n child" }
74
+ \`\`\`
75
+
76
+ A \`diff\` section carries a unified diff patch as produced by \`git diff\`. The report groups it by file, marks added, removed, and context lines, and keeps those markings legible in print. An empty patch renders an explicit no-data message.
77
+
78
+ \`\`\`json
79
+ { "id": "changes", "title": "Changes", "type": "diff", "patch": "diff --git a/src/acme.ts b/src/acme.ts\\n..." }
80
+ \`\`\`
81
+
82
+ A report carrying a mermaid payload inlines the diagram runtime; a report without one does not. Either way the page stays a single self-contained file that makes no network request.
59
83
 
60
84
  ## Invocation
61
85
 
@@ -87,10 +111,8 @@ const DEFAULT_MATE_SKILLS_BUCKET = "agents";
87
111
  /**
88
112
  * A skill's source lives under a provider bucket so its folder name always
89
113
  * matches its `name:` frontmatter: `<tool>/<skill>/` when that tool needs its
90
- * own behavior (e.g. Claude Code always confirms before `mate artifact
91
- * finish` pushes, since a push is a shared-state action), falling back to
92
- * `agents/<skill>/` — the shared default every other tool (e.g. opencode,
93
- * which pushes automatically) uses.
114
+ * own behavior, falling back to `agents/<skill>/` — the shared default every
115
+ * other tool uses.
94
116
  */
95
117
  async function resolveMateSkillSource(skill: string, tool: string): Promise<string> {
96
118
  const providerDir = path.join(MATE_SKILLS_SOURCE, tool, skill);
@@ -98,8 +120,15 @@ async function resolveMateSkillSource(skill: string, tool: string): Promise<stri
98
120
  await fs.access(providerDir);
99
121
  return providerDir;
100
122
  } catch {
101
- return path.join(MATE_SKILLS_SOURCE, DEFAULT_MATE_SKILLS_BUCKET, skill);
123
+ const sharedDir = path.join(MATE_SKILLS_SOURCE, DEFAULT_MATE_SKILLS_BUCKET, skill);
124
+ try {
125
+ await fs.access(sharedDir);
126
+ return sharedDir;
127
+ } catch {
128
+ /* source is missing */
129
+ }
102
130
  }
131
+ throw new Error(`Mate skill source not found: ${skill}`);
103
132
  }
104
133
 
105
134
  export async function applyMateSkills(skillsDir: string, tool: string): Promise<void> {
@@ -4,6 +4,13 @@ import type { ClaudeHookGroup } from "./providers/claude-format";
4
4
 
5
5
  export type SetupScope = "companion" | "hub";
6
6
 
7
+ /**
8
+ * Where a reconcile pass lands its documents. `companion` writes the material's
9
+ * own repository; `working` renders the documents an Agent Runtime discovers by
10
+ * its own upward walk and leaves the placing to the Projection Root.
11
+ */
12
+ export type ProjectionTarget = "companion" | "working";
13
+
7
14
  export interface SetupContext {
8
15
  companionPath: string;
9
16
  config: FrameworkConfig;
@@ -13,6 +20,8 @@ export interface SetupContext {
13
20
  scope?: SetupScope;
14
21
  /** Working repository path when syncing from a linked repo (e.g. during launch). */
15
22
  repoPath?: string;
23
+ /** Defaults to `companion`, so a caller that names no target is unchanged. */
24
+ target?: ProjectionTarget;
16
25
  /**
17
26
  * Provider-mediated context services. Always populated when the engine
18
27
  * executes a plugin; optional so hand-built contexts (tests, direct calls)
@@ -104,8 +104,18 @@ export async function refreshFromTemplate(
104
104
 
105
105
  remaining = removeGraphifySection(remaining).trimEnd();
106
106
 
107
+ const template = templateContent.trimEnd();
108
+ /**
109
+ * A template carrying no managed markers cannot be stripped by marker above,
110
+ * so without this it would be prepended again on every refresh. Consuming
111
+ * every leading copy also heals a file an earlier refresh already grew.
112
+ */
113
+ while (template && remaining.startsWith(template)) {
114
+ remaining = remaining.slice(template.length).trimStart();
115
+ }
116
+
107
117
  const sections = [
108
- templateContent.trimEnd(),
118
+ template,
109
119
  ...(normalizedOptions.appendSections ?? []).map((section) => section.trim()).filter(Boolean),
110
120
  ];
111
121
  if (remaining) {