@uniqbit/mate-core 0.15.5 → 0.16.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 (183) 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 +6 -4
  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/companion/companion.ts +5 -1
  19. package/src/cli/commands/companion/link.ts +2 -2
  20. package/src/cli/commands/companion/sync.ts +92 -0
  21. package/src/cli/commands/doctor.ts +0 -3
  22. package/src/cli/commands/launch/shared.ts +23 -5
  23. package/src/cli/commands/report/collector.ts +72 -78
  24. package/src/cli/commands/report/contract.ts +40 -1
  25. package/src/cli/commands/report/highlight.ts +27 -0
  26. package/src/cli/commands/report/index.ts +11 -18
  27. package/src/cli/commands/report/renderer.ts +199 -2
  28. package/src/cli/commands/report/types.ts +26 -1
  29. package/src/cli/commands/shared/companion-selection.ts +107 -10
  30. package/src/cli/commands/studio/areas.ts +68 -0
  31. package/src/cli/commands/studio/index.ts +69 -0
  32. package/src/cli/commands/studio/inventory.ts +55 -0
  33. package/src/cli/commands/studio/mate-inventory.ts +43 -0
  34. package/src/cli/commands/studio/openspec-cli.ts +198 -0
  35. package/src/cli/commands/studio/payload.ts +184 -0
  36. package/src/cli/commands/studio/routes.ts +2 -0
  37. package/src/cli/commands/studio/selection.ts +61 -0
  38. package/src/cli/commands/studio/server.ts +201 -0
  39. package/src/cli/commands/studio/snapshot.ts +63 -0
  40. package/src/cli/commands/studio/topology.ts +199 -0
  41. package/src/cli/commands/studio/views/client.ts +197 -0
  42. package/src/cli/commands/studio/views/companion-picker.tsx +91 -0
  43. package/src/cli/commands/studio/views/companion-selector.tsx +90 -0
  44. package/src/cli/commands/studio/views/dashboard/changes.tsx +107 -0
  45. package/src/cli/commands/studio/views/dashboard/index.tsx +19 -0
  46. package/src/cli/commands/studio/views/document.tsx +256 -0
  47. package/src/cli/commands/studio/views/error.tsx +20 -0
  48. package/src/cli/commands/studio/views/model.ts +34 -0
  49. package/src/cli/commands/studio/views/pairings.tsx +38 -0
  50. package/src/cli/commands/studio/views/skills/index.tsx +87 -0
  51. package/src/cli/commands/studio/views/specs/index.tsx +101 -0
  52. package/src/cli/commands/studio/views/styles.ts +389 -0
  53. package/src/cli/commands/studio/views/warnings.tsx +21 -0
  54. package/src/cli/commands/studio/views/workflow/index.tsx +21 -0
  55. package/src/cli/commands/studio/views/workflow/steps.ts +329 -0
  56. package/src/cli/commands/studio/views/workflow/transcript.tsx +190 -0
  57. package/src/cli/commands/unwrap.ts +70 -0
  58. package/src/cli/commands/wrap.ts +164 -0
  59. package/src/cli/main.ts +68 -19
  60. package/src/cli/parse-flags.ts +36 -11
  61. package/src/cli/usage.ts +12 -3
  62. package/src/framework.ts +1 -7
  63. package/src/hooks/session-banner.ts +64 -11
  64. package/src/hooks/session-guidance.ts +40 -0
  65. package/src/hooks/validate-artifact-path.ts +108 -35
  66. package/src/lib/fs-utils.ts +9 -0
  67. package/src/lib/install.ts +33 -0
  68. package/src/lib/orchestrator/adapters/base.ts +14 -125
  69. package/src/lib/orchestrator/adapters/claude.ts +0 -11
  70. package/src/lib/orchestrator/adapters/opencode.ts +2 -32
  71. package/src/lib/orchestrator/companion-git-sync.ts +94 -84
  72. package/src/lib/orchestrator/config-store.ts +2 -21
  73. package/src/lib/orchestrator/editor.ts +12 -22
  74. package/src/lib/orchestrator/framework-context.ts +17 -6
  75. package/src/lib/orchestrator/global-config-store.ts +1 -1
  76. package/src/lib/orchestrator/launcher.ts +97 -7
  77. package/src/lib/orchestrator/opencode-guidance.ts +4 -56
  78. package/src/lib/orchestrator/projection-claude-entry.ts +198 -0
  79. package/src/lib/orchestrator/projection-claude-skills.ts +120 -0
  80. package/src/lib/orchestrator/projection-companion-link.ts +62 -0
  81. package/src/lib/orchestrator/projection-entries.ts +377 -0
  82. package/src/lib/orchestrator/projection-record.ts +56 -0
  83. package/src/lib/orchestrator/projection-runtime-documents.ts +424 -0
  84. package/src/lib/orchestrator/projection-types.ts +169 -0
  85. package/src/lib/orchestrator/repo-local-registry.ts +37 -133
  86. package/src/lib/orchestrator/repo-local-store.ts +96 -0
  87. package/src/lib/orchestrator/setup-compatibilities.ts +1 -9
  88. package/src/lib/orchestrator/types.ts +1 -0
  89. package/src/lib/orchestrator/working-repo-projection.ts +366 -0
  90. package/src/lib/orchestrator/workspace-inventory.ts +1 -1
  91. package/src/lib/package-paths.ts +11 -1
  92. package/src/opencode/companion-hooks.ts +89 -245
  93. package/src/opencode/companion-policy.ts +35 -10
  94. package/src/opencode/index.ts +1 -0
  95. package/src/opencode/projected-guidance.ts +56 -0
  96. package/src/opencode/tui.tsx +13 -4
  97. package/src/playbooks/companion-guidance.ts +32 -116
  98. package/src/plugins.ts +0 -1
  99. package/src/runtime/companion-git-state.ts +156 -0
  100. package/src/runtime/companion-git.ts +203 -0
  101. package/src/runtime/companion-guidance.ts +222 -0
  102. package/src/runtime/companion-sync.ts +298 -0
  103. package/src/runtime/env-names.ts +30 -0
  104. package/src/runtime/env.ts +67 -35
  105. package/src/runtime/framework.ts +10 -0
  106. package/src/runtime/freshness.ts +58 -0
  107. package/src/runtime/index.ts +104 -0
  108. package/src/runtime/install.ts +30 -0
  109. package/src/runtime/policy.ts +66 -0
  110. package/src/runtime/projected-guidance.ts +45 -0
  111. package/src/runtime/projection.ts +224 -0
  112. package/src/runtime/repo-local.ts +64 -0
  113. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +56 -0
  114. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +48 -0
  115. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/tasks.md +22 -0
  116. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +31 -90
  117. package/src/templates/capabilities/openspec-cap/mate-v1/templates/design.md +3 -0
  118. package/src/templates/capabilities/openspec-cap/mate-v1/templates/explore-brief.md +6 -6
  119. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  120. package/src/templates/capabilities/openspec-cap/mate-v1/templates/tasks.md +3 -0
  121. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +30 -0
  122. package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +2 -2
  123. package/src/templates/mate-skills/agents/mate-artifact-publish/SKILL.md +185 -0
  124. package/src/templates/mate-skills/agents/mate-artifact-publish/references/openspec.md +227 -0
  125. package/src/templates/mate-skills/agents/mate-domain-modeling/SKILL.md +68 -0
  126. package/src/templates/mate-skills/agents/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
  127. package/src/templates/mate-skills/agents/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
  128. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +14 -0
  129. package/src/templates/mate-skills/agents/mate-grill-with-docs/SKILL.md +29 -0
  130. package/src/templates/mate-skills/agents/mate-grilling/SKILL.md +49 -0
  131. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +147 -0
  132. package/src/templates/{capabilities/openspec-cap/mate-skills → mate-skills}/agents/mate-openspec-backfill/SKILL.md +4 -2
  133. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +139 -0
  134. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +503 -0
  135. package/src/templates/mate-skills/claude/mate-artifact-publish/SKILL.md +185 -0
  136. package/src/templates/mate-skills/claude/mate-artifact-publish/references/openspec.md +227 -0
  137. package/src/templates/mate-skills/claude/mate-domain-modeling/SKILL.md +68 -0
  138. package/src/templates/mate-skills/claude/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
  139. package/src/templates/mate-skills/claude/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
  140. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +14 -0
  141. package/src/templates/mate-skills/claude/mate-grill-with-docs/SKILL.md +29 -0
  142. package/src/templates/mate-skills/claude/mate-grilling/SKILL.md +49 -0
  143. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +147 -0
  144. package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +67 -0
  145. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +139 -0
  146. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +503 -0
  147. package/src/templates/report-assets/README.md +32 -0
  148. package/src/templates/report-assets/mermaid.LICENSE +21 -0
  149. package/src/templates/report-assets/mermaid.min.js +4376 -0
  150. package/src/templates/root/TEMPLATE_CLAUDE.md +1 -9
  151. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +1476 -197
  152. package/src/tools/setup/capabilities/graphify.ts +16 -7
  153. package/src/tools/setup/capabilities/openspec.ts +63 -56
  154. package/src/tools/setup/capabilities/tokensave.ts +47 -0
  155. package/src/tools/setup/engine.ts +34 -6
  156. package/src/tools/setup/mate.ts +42 -13
  157. package/src/tools/setup/plugin.ts +9 -0
  158. package/src/tools/setup/plugins/guidance.ts +11 -1
  159. package/src/tools/setup/providers/claude-format.ts +49 -4
  160. package/src/tools/setup/providers/claude-plugin-hooks.ts +117 -0
  161. package/src/tools/setup/providers/claude.ts +55 -220
  162. package/src/tools/setup/providers/opencode.ts +41 -14
  163. package/src/tools/setup/runtime-documents.ts +174 -0
  164. package/src/tools/setup/surface-target.ts +50 -0
  165. package/src/tools/setup/working-repo-cleanup.ts +33 -26
  166. package/src/tools/setup/working-repo-local-state.ts +21 -1
  167. package/src/tools/setup.ts +25 -3
  168. package/wrappers/bin/graphify +57 -8
  169. package/wrappers/bin/openspec +50 -3
  170. package/claude-plugin/hooks/artifact-finish-nudge.mjs +0 -8
  171. package/src/cli/commands/cap/headroom.ts +0 -52
  172. package/src/cli/commands/workspace/list.ts +0 -25
  173. package/src/cli/commands/workspace/materialize.ts +0 -46
  174. package/src/cli/commands/workspace/workspace.ts +0 -22
  175. package/src/hooks/artifact-finish-nudge.ts +0 -244
  176. package/src/lib/orchestrator/headroom/proxy.ts +0 -116
  177. package/src/lib/orchestrator/workspace-materialize.ts +0 -80
  178. package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/SKILL.md +0 -51
  179. package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/references/openspec.md +0 -134
  180. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +0 -58
  181. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +0 -139
  182. package/src/tools/setup/capabilities/headroom.ts +0 -57
  183. /package/src/cli/commands/{workspace → companion}/open.ts +0 -0
@@ -0,0 +1,174 @@
1
+ import {
2
+ CLAUDE_LOCAL_CONFIG_DOCUMENT,
3
+ CLAUDE_MCP_DOCUMENT,
4
+ CLAUDE_SETTINGS_DOCUMENT,
5
+ OPENCODE_CONFIG_DOCUMENT,
6
+ } from "../../lib/orchestrator/projection-runtime-documents";
7
+ import type {
8
+ ManagedRegion,
9
+ RenderedRuntimeDocument,
10
+ } from "../../lib/orchestrator/projection-types";
11
+ import { getOpenCodePluginPackageReference } from "../../lib/opencode-plugin-package";
12
+ import type { CapabilityContributionInput, SetupContext } from "./plugin";
13
+ import {
14
+ mateBannerHookGroups,
15
+ mateGuardHookGroups,
16
+ mateGuidanceHookGroups,
17
+ } from "./providers/claude-plugin-hooks";
18
+ import {
19
+ renderManagedClaudeMcpServers,
20
+ renderManagedClaudePermissionEntries,
21
+ } from "./providers/claude";
22
+ import {
23
+ renderCompanionExternalDirectoryPermissions,
24
+ renderManagedOpenCodeMcpServers,
25
+ } from "./providers/opencode";
26
+ import { projectionRepoRoot } from "./surface-target";
27
+
28
+ /**
29
+ * The working target's documents, rendered as values. Each is a document the
30
+ * runtime finds by walking up from the current directory, and every path inside
31
+ * one still names the Companion Repository — only where the document is placed
32
+ * differs from the companion target.
33
+ */
34
+
35
+ export {
36
+ CLAUDE_LOCAL_CONFIG_DOCUMENT,
37
+ CLAUDE_MCP_DOCUMENT,
38
+ CLAUDE_SETTINGS_DOCUMENT,
39
+ OPENCODE_CONFIG_DOCUMENT,
40
+ };
41
+
42
+ function nonEmpty(region: ManagedRegion): boolean {
43
+ if (region.kind === "list") return region.values.length > 0;
44
+ if (region.kind === "map") return Object.keys(region.entries).length > 0;
45
+ return true;
46
+ }
47
+
48
+ function document(documentPath: string, regions: ManagedRegion[]): RenderedRuntimeDocument[] {
49
+ const kept = regions.filter(nonEmpty);
50
+ return kept.length > 0 ? [{ path: documentPath, regions: kept }] : [];
51
+ }
52
+
53
+ /**
54
+ * The Mate plugin reference is what makes an Unmanaged OpenCode session load
55
+ * the companion guidance at all: OpenCode has no config channel for a generated
56
+ * system prompt, so the plugin is the only injection point, and a bare session
57
+ * loads no companion config to find it in.
58
+ *
59
+ * A managed launch does load the plugin twice — its `OPENCODE_CONFIG_DIR` names
60
+ * the companion, whose config carries the same reference. That is inert rather
61
+ * than doubled: the plugin skips its own emission when the companion-policy
62
+ * marker is already in the prompt.
63
+ */
64
+
65
+ /**
66
+ * Capability hook groups stay companion-only. A managed session loads this
67
+ * document as its `local` source *alongside* the companion's, so anything here
68
+ * that the launch also delivers runs twice; the guard, the guidance hook and
69
+ * the banner are the contributions whose absence is a correctness hole and
70
+ * whose double run is inert — the guard because its verdict depends only on the
71
+ * tool input, the guidance hook because it yields nothing under a launch
72
+ * environment, and the banner because the copy carried here is flagged
73
+ * `--projected` and defers to the one the launch's own plugin prints.
74
+ *
75
+ * `CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD` is what a managed launch sets
76
+ * beside `--add-dir`. Without it the companion directories this document
77
+ * already permits are reachable but their `CLAUDE.md` is not loaded, so the
78
+ * permission alone delivers no guidance.
79
+ */
80
+ function claudeSettingsRegions(
81
+ ctx: SetupContext,
82
+ inputs: CapabilityContributionInput[],
83
+ ): ManagedRegion[] {
84
+ return [
85
+ { at: ["hooks", "PreToolUse"], kind: "list", values: mateGuardHookGroups() },
86
+ {
87
+ at: ["hooks", "SessionStart"],
88
+ kind: "list",
89
+ values: [...mateBannerHookGroups(), ...mateGuidanceHookGroups()],
90
+ },
91
+ /**
92
+ * `stripPrefix` heals a repository an older Mate wrapped. That version wrote
93
+ * the whole launch environment here, and Claude Code applies `env` to the
94
+ * session, so every hook in a bare session inherited `MATE_*` and read the
95
+ * repository as a Managed Session — silently, since looking managed is
96
+ * exactly what makes the guidance hook emit nothing. Guidance must be the
97
+ * projection's to give in an Unmanaged Session, so no `MATE_*` key may
98
+ * survive here.
99
+ */
100
+ {
101
+ at: ["env"],
102
+ kind: "map",
103
+ entries: { CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: "1" },
104
+ stripPrefix: "MATE_",
105
+ },
106
+ {
107
+ at: ["permissions", "allow"],
108
+ kind: "list",
109
+ values: renderManagedClaudePermissionEntries(ctx.companionPath, inputs),
110
+ },
111
+ ];
112
+ }
113
+
114
+ export function renderDocumentsForTarget(
115
+ ctx: SetupContext,
116
+ inputsByRuntime: Map<string, CapabilityContributionInput[]>,
117
+ ): RenderedRuntimeDocument[] {
118
+ const repoRoot = projectionRepoRoot(ctx);
119
+
120
+ const documents: RenderedRuntimeDocument[] = [];
121
+ if (ctx.activeProviders.includes("claude")) {
122
+ const inputs = inputsByRuntime.get("claude") ?? [];
123
+ documents.push(...document(CLAUDE_SETTINGS_DOCUMENT, claudeSettingsRegions(ctx, inputs)));
124
+ /**
125
+ * Local scope, not `.mcp.json`: a project-scoped server is inert until a
126
+ * human approves it in a session, which an Unmanaged Session is exactly the
127
+ * case that cannot rely on. The repo path is the key Claude Code files a
128
+ * project under, resolved so a symlinked working directory still matches.
129
+ *
130
+ * Confirmed against a real `~/.claude.json`: the keys there are the
131
+ * directories sessions started in, not repository roots — sibling
132
+ * directories of one checkout each hold their own entry, as do temporary
133
+ * directories. So this reaches exactly one session: the one started at the
134
+ * repository root. A session started in a subdirectory of a wrapped
135
+ * repository has its own key, finds no `mcpServers` under it, and sees none
136
+ * of these servers — the Unmanaged Session case this document exists for,
137
+ * missed. That limit is deliberate. The directories a session might start
138
+ * in are unbounded, and every one of them would be a fresh entry in the
139
+ * user's global configuration that nothing later reclaims; keying the
140
+ * repository root only keeps the projection's footprint there to a single
141
+ * key. A subdirectory session still gets the rest of the projection — the
142
+ * documents inside the repository are found by the runtime's own upward
143
+ * walk — it loses only the MCP servers.
144
+ */
145
+ documents.push(
146
+ ...document(CLAUDE_LOCAL_CONFIG_DOCUMENT, [
147
+ {
148
+ at: ["projects", repoRoot, "mcpServers"],
149
+ kind: "map",
150
+ entries: renderManagedClaudeMcpServers(inputs),
151
+ },
152
+ ]),
153
+ );
154
+ }
155
+ if (ctx.activeProviders.includes("opencode")) {
156
+ const inputs = inputsByRuntime.get("opencode") ?? [];
157
+ documents.push(
158
+ ...document(OPENCODE_CONFIG_DOCUMENT, [
159
+ {
160
+ at: ["plugin"],
161
+ kind: "list",
162
+ values: [getOpenCodePluginPackageReference()],
163
+ },
164
+ { at: ["mcp"], kind: "map", entries: renderManagedOpenCodeMcpServers(inputs) },
165
+ {
166
+ at: ["permission", "external_directory"],
167
+ kind: "map",
168
+ entries: renderCompanionExternalDirectoryPermissions(ctx.companionPath),
169
+ },
170
+ ]),
171
+ );
172
+ }
173
+ return documents;
174
+ }
@@ -0,0 +1,50 @@
1
+ import path from "node:path";
2
+
3
+ import type { ProjectionTarget, SetupContext } from "./plugin";
4
+
5
+ /**
6
+ * The projection target of a reconcile pass, resolved once. A `working` pass
7
+ * without a Working Repository in scope refuses rather than falling back to the
8
+ * companion: writing the other root is the one outcome the caller cannot detect.
9
+ */
10
+ export function projectionTarget(ctx: SetupContext): ProjectionTarget {
11
+ const target = ctx.target ?? "companion";
12
+ if (target === "working" && !ctx.repoPath) {
13
+ throw new Error(
14
+ "a working-target reconcile needs a Working Repository in scope; none was supplied",
15
+ );
16
+ }
17
+ return target;
18
+ }
19
+
20
+ /**
21
+ * The Working Repository a rendered document is addressed by, resolved so a
22
+ * symlinked working directory still matches what the runtime reports. Every
23
+ * document a reconcile pass renders is for a Working Repository, so a pass with
24
+ * none in scope refuses here rather than resolving an absent path: `path.resolve`
25
+ * answers the current working directory for an empty one, and a document keyed
26
+ * by wherever the process happened to start would file a Runtime Surface's MCP
27
+ * servers under a stranger's project in the user's global Claude config — a
28
+ * write no caller asked for and none would notice.
29
+ */
30
+ export function projectionRepoRoot(ctx: SetupContext): string {
31
+ const target = projectionTarget(ctx);
32
+ if (!ctx.repoPath) {
33
+ throw new Error(
34
+ `a ${target}-target render needs a Working Repository in scope to address its documents by; none was supplied`,
35
+ );
36
+ }
37
+ return path.resolve(ctx.repoPath);
38
+ }
39
+
40
+ /**
41
+ * The root a Runtime Surface writes into. Defined for the companion target
42
+ * only: a working-target pass renders its documents for the Projection Root to
43
+ * place, so no Runtime Surface ever computes a path inside a Working Repository.
44
+ */
45
+ export function surfaceRoot(ctx: SetupContext): string {
46
+ if (projectionTarget(ctx) === "working") {
47
+ throw new Error("a working-target reconcile renders its documents; it writes no surface root");
48
+ }
49
+ return ctx.companionPath;
50
+ }
@@ -1,40 +1,47 @@
1
- import fs from "node:fs/promises";
2
-
3
- import { repoLocalDirPath } from "../../lib/orchestrator/repo-local-registry";
4
- import { cleanupWorkingRepoClaudeSettings } from "./providers/claude";
5
- import { removeWorkingRepoLocalExcludes } from "./working-repo-local-state";
1
+ import { unproject } from "../../lib/orchestrator/working-repo-projection";
2
+ import type { ProjectionEntryId } from "../../lib/orchestrator/projection-types";
6
3
 
7
4
  export interface WorkingRepoCleanupResult {
8
5
  changed: boolean;
9
6
  removed: string[];
10
7
  updated: string[];
8
+ /** Entries `unproject` deliberately left in place, named rather than omitted. */
9
+ retained: string[];
11
10
  }
12
11
 
13
- async function pathExists(candidatePath: string): Promise<boolean> {
14
- try {
15
- await fs.access(candidatePath);
16
- return true;
17
- } catch {
18
- return false;
19
- }
20
- }
12
+ /**
13
+ * How an entry is reported. Presentation only — cleanup holds no list of paths,
14
+ * so a newly declared entry is removed without touching this map.
15
+ */
16
+ const REPORTED_AS: Partial<
17
+ Record<ProjectionEntryId, { as: "removed" | "updated"; label: string }>
18
+ > = {
19
+ "projection-root": { as: "removed", label: ".mate" },
20
+ "git-excludes": { as: "removed", label: "git-excludes" },
21
+ "claude-working-settings": { as: "updated", label: "claude-settings" },
22
+ "legacy-tokensave-claude-md": { as: "updated", label: "claude-settings" },
23
+ };
21
24
 
22
25
  export async function cleanupWorkingRepository(
23
26
  repoPath: string,
24
- registeredCompanionPaths: string[],
27
+ registeredCompanionPaths: string[] = [],
25
28
  ): Promise<WorkingRepoCleanupResult> {
26
- const updated: string[] = [];
29
+ const { outcomes } = await unproject({ repoPath, registeredCompanionPaths });
27
30
  const removed: string[] = [];
28
- if (await cleanupWorkingRepoClaudeSettings(repoPath, registeredCompanionPaths)) {
29
- updated.push("claude-settings");
30
- }
31
- if (await removeWorkingRepoLocalExcludes(repoPath)) {
32
- removed.push("git-excludes");
33
- }
34
- const localDir = repoLocalDirPath(repoPath);
35
- if (await pathExists(localDir)) {
36
- await fs.rm(localDir, { recursive: true, force: true });
37
- removed.unshift(".mate");
31
+ const updated: string[] = [];
32
+ const retained: string[] = [];
33
+
34
+ for (const entry of outcomes) {
35
+ if (entry.state === "retained") {
36
+ retained.push(entry.id);
37
+ continue;
38
+ }
39
+ if (entry.state !== "removed") continue;
40
+ const reported = REPORTED_AS[entry.id];
41
+ if (!reported) continue;
42
+ const bucket = reported.as === "removed" ? removed : updated;
43
+ if (!bucket.includes(reported.label)) bucket.push(reported.label);
38
44
  }
39
- return { changed: removed.length > 0 || updated.length > 0, removed, updated };
45
+
46
+ return { changed: removed.length > 0 || updated.length > 0, removed, updated, retained };
40
47
  }
@@ -5,7 +5,8 @@ import { resolveGitInfoExcludePath } from "./git-utils";
5
5
 
6
6
  const MANAGED_BLOCK_START = "# mate managed: start";
7
7
  const MANAGED_BLOCK_END = "# mate managed: end";
8
- const CORE_EXCLUDE_ENTRIES = ["/.mate/", "/.claude/", "/.opencode/", "/.agents/"];
8
+ /** `.mcp.json` is the one working-target document no directory entry covers. */
9
+ const CORE_EXCLUDE_ENTRIES = ["/.mate/", "/.claude/", "/.opencode/", "/.agents/", "/.mcp.json"];
9
10
  const LEGACY_CORE_EXCLUDE_ENTRIES = new Set([".mate/", ".claude/settings.local.json"]);
10
11
 
11
12
  async function readExclude(excludePath: string): Promise<string> {
@@ -90,6 +91,25 @@ export async function reconcileWorkingRepoCapabilityExcludes(
90
91
  return writeExclude(excludePath, existing, lines);
91
92
  }
92
93
 
94
+ /** Whether the managed block is on disk; the block, not the exclude file. */
95
+ export async function managedWorkingRepoExcludesPresent(repoPath: string): Promise<boolean> {
96
+ const excludePath = await resolveGitInfoExcludePath(repoPath);
97
+ if (!excludePath) return false;
98
+ const lines = contentLines(await readExclude(excludePath));
99
+ return lines.some((line) => line.trim() === MANAGED_BLOCK_START);
100
+ }
101
+
102
+ /** Whether any Capability-owned exclusion is present outside the managed block. */
103
+ export async function workingRepoCapabilityExcludesPresent(
104
+ repoPath: string,
105
+ candidateEntries: string[],
106
+ ): Promise<boolean> {
107
+ const excludePath = await resolveGitInfoExcludePath(repoPath);
108
+ if (!excludePath) return false;
109
+ const lines = withoutManagedBlocks(contentLines(await readExclude(excludePath)));
110
+ return candidateEntries.some((entry) => lines.some((line) => line.trim() === entry));
111
+ }
112
+
93
113
  export async function removeWorkingRepoLocalExcludes(repoPath: string): Promise<boolean> {
94
114
  const excludePath = await resolveGitInfoExcludePath(repoPath);
95
115
  if (!excludePath) return false;
@@ -19,7 +19,6 @@ import {
19
19
  type CapabilityConfig,
20
20
  type FrameworkConfig,
21
21
  } from "../lib/orchestrator/types";
22
- import { syncWorkingRepoClaudeSettings } from "./setup/providers/claude";
23
22
  import {
24
23
  collectManagedGitignoreEntries,
25
24
  writeManagedGitignoreBlock,
@@ -35,10 +34,10 @@ import { installDeclaredPlugins } from "./setup/dynamic-plugins/install";
35
34
  import {
36
35
  buildSetupInstallationPlan,
37
36
  executeSetupInstallationPlan,
37
+ renderRuntimeDocuments,
38
38
  type SetupInstallationOutcome,
39
39
  } from "./setup/engine";
40
-
41
- export { syncWorkingRepoClaudeSettings };
40
+ import type { RenderedRuntimeDocument } from "../lib/orchestrator/projection-types";
42
41
 
43
42
  export interface SetupInput {
44
43
  allowedAgents?: string[];
@@ -67,6 +66,29 @@ export async function applySetupCompatibilities(
67
66
  return executeSetupInstallationPlan(ctx, plugins, plan);
68
67
  }
69
68
 
69
+ /**
70
+ * The active Agent Runtimes' documents for a Working Repository, rendered
71
+ * without a write. Runs no plugin phase: the working target reconciles
72
+ * declarations only, so nothing is written to the companion on its behalf.
73
+ */
74
+ export async function renderWorkingRuntimeDocuments(
75
+ companionPath: string,
76
+ config: FrameworkConfig,
77
+ repoPath: string,
78
+ plugins: PluginRegistration[] = getActiveDistribution().registry.getAll(),
79
+ ): Promise<RenderedRuntimeDocument[]> {
80
+ const plan = buildSetupInstallationPlan(config, plugins);
81
+ const ctx: SetupContext = {
82
+ companionPath,
83
+ config,
84
+ mode: "sync",
85
+ activeProviders: plan.activeProviders,
86
+ repoPath,
87
+ target: "working",
88
+ };
89
+ return renderRuntimeDocuments(ctx, plugins, plan);
90
+ }
91
+
70
92
  export async function updateProjectGitignore(
71
93
  companionPath: string,
72
94
  config: FrameworkConfig,
@@ -1,8 +1,59 @@
1
1
  #!/usr/bin/env bash
2
- [ -n "$MATE_ARTIFACT_PATH" ] || { echo "graphify: must be run via mate claude or mate opencode (MATE_ARTIFACT_PATH not set)" >&2; exit 1; }
3
- [ -n "$MATE_REPO_PATH" ] || { echo "graphify: must be run via mate claude or mate opencode (MATE_REPO_PATH not set)" >&2; exit 1; }
4
-
5
2
  _this_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
3
+
4
+ # Mate context: the launch environment first, the Working Repository's
5
+ # Projection Root second. Projected values stay in private shell variables and
6
+ # no MATE_-prefixed variable is exported: the projection carries paths and
7
+ # never predicates, so a partial MATE_* environment would read to any child as
8
+ # authoritative with every predicate disabled.
9
+ _mate_projection_file() {
10
+ local dir="$PWD"
11
+ while :; do
12
+ if [ -f "$dir/.mate/config/registry.yaml" ]; then
13
+ [ -f "$dir/.mate/projection.env" ] && printf '%s' "$dir/.mate/projection.env"
14
+ return
15
+ fi
16
+ [ "$dir" = "/" ] && return
17
+ dir="$(dirname "$dir")"
18
+ done
19
+ }
20
+
21
+ _mate_projected() {
22
+ sed -n "s/^$2='\\(.*\\)'\$/\\1/p" "$1" | head -n 1 | sed "s/'\\\\''/'/g"
23
+ }
24
+
25
+ _mate_same_dir() {
26
+ local left="$1" right="$2"
27
+ [ -d "$left" ] && left="$(cd "$left" && pwd)"
28
+ [ -d "$right" ] && right="$(cd "$right" && pwd)"
29
+ [ "$left" = "$right" ]
30
+ }
31
+
32
+ _companion="$MATE_ARTIFACT_PATH"
33
+ _repo_path="$MATE_REPO_PATH"
34
+ _graphify_out="$GRAPHIFY_OUT"
35
+ if [ -z "$_companion" ]; then
36
+ _projection="$(_mate_projection_file)"
37
+ if [ -n "$_projection" ]; then
38
+ _companion="$(_mate_projected "$_projection" MATE_ARTIFACT_PATH)"
39
+ _repo_path="$(_mate_projected "$_projection" MATE_REPO_PATH)"
40
+ _graphify_out="$(_mate_projected "$_projection" GRAPHIFY_OUT)"
41
+ _projected_wrapper_dir="$(_mate_projected "$_projection" MATE_WRAPPER_BIN_PATH)"
42
+ if [ -n "$_companion" ] && ! _mate_same_dir "$_projected_wrapper_dir" "$_this_dir"; then
43
+ {
44
+ echo "graphify: this repository was wrapped by a different mate install."
45
+ echo " projected wrappers: $_projected_wrapper_dir"
46
+ echo " running wrapper: $_this_dir"
47
+ echo " re-run \`mate wrap\` with this install."
48
+ } >&2
49
+ exit 1
50
+ fi
51
+ fi
52
+ fi
53
+ [ -n "$_companion" ] || { echo "graphify: no mate context here — run \`mate wrap\` in this repository" >&2; exit 1; }
54
+ [ -n "$_repo_path" ] || { echo "graphify: no working repository in the mate context — run \`mate wrap\` in this repository" >&2; exit 1; }
55
+ [ -n "$_graphify_out" ] || { echo "graphify: no graphify output path in the mate context — run \`mate wrap\` in this repository" >&2; exit 1; }
56
+
6
57
  _real=""
7
58
  IFS=:
8
59
  for _dir in $PATH; do
@@ -14,8 +65,6 @@ done
14
65
  unset IFS
15
66
  [ -n "$_real" ] || { echo "graphify wrapper: real graphify binary not found in PATH" >&2; exit 1; }
16
67
 
17
- _repo_id="${MATE_REPO_ID:-unknown}"
18
- _graphify_out="$MATE_ARTIFACT_PATH/.graphify/$_repo_id/graphify-out"
19
68
  _graphify_root="$(dirname "$_graphify_out")"
20
69
  _graph_json="$_graphify_out/graph.json"
21
70
  export GRAPHIFY_OUT="$_graphify_out"
@@ -32,18 +81,18 @@ case "$_cmd" in
32
81
  *) _rest+=("$_a") ;;
33
82
  esac
34
83
  done
35
- exec "$_real" extract "$MATE_REPO_PATH" --out "$_graphify_root" "${_rest[@]}"
84
+ exec "$_real" extract "$_repo_path" --out "$_graphify_root" "${_rest[@]}"
36
85
  ;;
37
86
  update)
38
87
  if [ "$#" -eq 1 ] || [[ "$2" == -* ]]; then
39
- exec "$_real" "$MATE_REPO_PATH" --update "${@:2}"
88
+ exec "$_real" "$_repo_path" --update "${@:2}"
40
89
  else
41
90
  exec "$_real" "$2" --update "${@:3}"
42
91
  fi
43
92
  ;;
44
93
  extract|watch|cluster-only|label|check-update)
45
94
  if [ "$#" -eq 1 ] || [[ "$2" == -* ]]; then
46
- exec "$_real" "$_cmd" "$MATE_REPO_PATH" "${@:2}"
95
+ exec "$_real" "$_cmd" "$_repo_path" "${@:2}"
47
96
  else
48
97
  exec "$_real" "$@"
49
98
  fi
@@ -1,6 +1,53 @@
1
1
  #!/usr/bin/env bash
2
- [ -n "$MATE_ARTIFACT_PATH" ] || { echo "openspec: must be run via mate claude or mate opencode" >&2; exit 1; }
3
2
  _this_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
3
+
4
+ # Mate context: the launch environment first, the Working Repository's
5
+ # Projection Root second. Projected values stay in private shell variables and
6
+ # no MATE_-prefixed variable is exported: the projection carries paths and
7
+ # never predicates, so a partial MATE_* environment would read to any child as
8
+ # authoritative with every predicate disabled.
9
+ _mate_projection_file() {
10
+ local dir="$PWD"
11
+ while :; do
12
+ if [ -f "$dir/.mate/config/registry.yaml" ]; then
13
+ [ -f "$dir/.mate/projection.env" ] && printf '%s' "$dir/.mate/projection.env"
14
+ return
15
+ fi
16
+ [ "$dir" = "/" ] && return
17
+ dir="$(dirname "$dir")"
18
+ done
19
+ }
20
+
21
+ _mate_projected() {
22
+ sed -n "s/^$2='\\(.*\\)'\$/\\1/p" "$1" | head -n 1 | sed "s/'\\\\''/'/g"
23
+ }
24
+
25
+ _mate_same_dir() {
26
+ local left="$1" right="$2"
27
+ [ -d "$left" ] && left="$(cd "$left" && pwd)"
28
+ [ -d "$right" ] && right="$(cd "$right" && pwd)"
29
+ [ "$left" = "$right" ]
30
+ }
31
+
32
+ _companion="$MATE_ARTIFACT_PATH"
33
+ if [ -z "$_companion" ]; then
34
+ _projection="$(_mate_projection_file)"
35
+ if [ -n "$_projection" ]; then
36
+ _companion="$(_mate_projected "$_projection" MATE_ARTIFACT_PATH)"
37
+ _projected_wrapper_dir="$(_mate_projected "$_projection" MATE_WRAPPER_BIN_PATH)"
38
+ if [ -n "$_companion" ] && ! _mate_same_dir "$_projected_wrapper_dir" "$_this_dir"; then
39
+ {
40
+ echo "openspec: this repository was wrapped by a different mate install."
41
+ echo " projected wrappers: $_projected_wrapper_dir"
42
+ echo " running wrapper: $_this_dir"
43
+ echo " re-run \`mate wrap\` with this install."
44
+ } >&2
45
+ exit 1
46
+ fi
47
+ fi
48
+ fi
49
+ [ -n "$_companion" ] || { echo "openspec: no mate context here — run \`mate wrap\` in this repository" >&2; exit 1; }
50
+
4
51
  _real=""
5
52
  IFS=:
6
53
  for _dir in $PATH; do
@@ -22,7 +69,7 @@ if [ "$command" = "templates" ]; then
22
69
  done
23
70
  if [ "$has_schema" -eq 0 ]; then
24
71
  _schema=""
25
- for _cfg in "$MATE_ARTIFACT_PATH/openspec/config.yaml" "$MATE_ARTIFACT_PATH/openspec/config.yml"; do
72
+ for _cfg in "$_companion/openspec/config.yaml" "$_companion/openspec/config.yml"; do
26
73
  [ -f "$_cfg" ] || continue
27
74
  while IFS= read -r _line; do
28
75
  case "$_line" in
@@ -45,4 +92,4 @@ if [ "$command" = "templates" ]; then
45
92
  fi
46
93
  fi
47
94
  fi
48
- exec env -C "$MATE_ARTIFACT_PATH" "$_real" "$@"
95
+ exec env -C "$_companion" "$_real" "$@"
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env node
2
- // Thin shim: hook logic lives in src/hooks/artifact-finish-nudge.ts, loaded
3
- // through ts-loader.mjs because the installed plugin sits inside node_modules
4
- // where node's native type stripping is disabled.
5
- import "./ts-loader.mjs";
6
-
7
- const { run } = await import("../../src/hooks/artifact-finish-nudge.ts");
8
- process.exitCode = await run();
@@ -1,52 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
-
3
- import { resolveForCapability } from "../../../lib/orchestrator/framework-context";
4
- import type { CapabilityConfig } from "../../../lib/orchestrator/types";
5
- import { ensureUnambiguousCompanion } from "../shared/companion-selection";
6
-
7
- export interface HeadroomCapDeps {
8
- ensureUnambiguousCompanion?: (cwd: string) => Promise<boolean>;
9
- }
10
-
11
- /**
12
- * @command mate cap headroom [...args]
13
- * @description Forwards `[...args]` to the `headroom` CLI, run from the
14
- * companion directory. Requires the `headroom` capability to be enabled in
15
- * `.mate/config/framework.yaml`.
16
- * @remarks The child process's exit code is propagated via `process.exitCode`.
17
- */
18
- export async function runHeadroomCapCommand(
19
- args: string[],
20
- deps: HeadroomCapDeps = {},
21
- ): Promise<void> {
22
- const ensureCompanion = deps.ensureUnambiguousCompanion ?? ensureUnambiguousCompanion;
23
- if (!(await ensureCompanion(process.cwd()))) {
24
- process.exitCode = 1;
25
- return;
26
- }
27
-
28
- const { companionPath, configStore } = await resolveForCapability(process.cwd());
29
- const config = await configStore.load();
30
-
31
- const capability = (config.capabilities ?? []).find(
32
- (entry: CapabilityConfig) => entry.name === "headroom",
33
- );
34
-
35
- if (!capability) {
36
- process.stderr.write("headroom capability is not enabled in .mate/config/framework.yaml\n");
37
- process.exitCode = 1;
38
- return;
39
- }
40
-
41
- const result = spawnSync("headroom", args, { cwd: companionPath, stdio: "inherit" });
42
-
43
- if (result.error) {
44
- process.stderr.write(`Failed to run headroom: ${result.error.message}\n`);
45
- process.exitCode = 1;
46
- return;
47
- }
48
-
49
- if (result.status !== null) {
50
- process.exitCode = result.status;
51
- }
52
- }
@@ -1,25 +0,0 @@
1
- import { collectWorkspaceInventory } from "../../../lib/orchestrator/workspace-inventory";
2
- import { writeJsonStdout } from "../../write-json-stdout";
3
-
4
- export const workspaceListCommandDeps = {
5
- collectWorkspaceInventory: () => collectWorkspaceInventory(),
6
- };
7
-
8
- /**
9
- * @command mate workspace list
10
- * @description Prints the aggregate inventory of registered companions and
11
- * their linked working-repository pairings as one JSON document. Runs
12
- * without a linked working repository or any active companion context.
13
- * @flags
14
- * - `--json` — required; this command only supports JSON output.
15
- */
16
- export async function runWorkspaceListCommand(argv: string[] = []): Promise<void> {
17
- if (!argv.includes("--json")) {
18
- process.stderr.write("mate: `workspace list` requires --json.\n");
19
- process.exitCode = 1;
20
- return;
21
- }
22
-
23
- const inventory = await workspaceListCommandDeps.collectWorkspaceInventory();
24
- await writeJsonStdout(inventory);
25
- }