@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,164 @@
1
+ import path from "node:path";
2
+
3
+ import { FRAMEWORK_NAME } from "../../framework";
4
+ import { ConfigStore, mergeWithDefaults } from "../../lib/orchestrator/config-store";
5
+ import { findRepoLocalLinkedRepository } from "../../lib/orchestrator/repo-local-registry";
6
+ import {
7
+ projectWorkingRepository,
8
+ projectWorkingRuntimeDocuments,
9
+ } from "../../lib/orchestrator/working-repo-projection";
10
+ import { renderWorkingRuntimeDocuments } from "../../tools/setup";
11
+ import { launchAmbiguityDeps } from "./shared/companion-selection";
12
+
13
+ export interface WrapCommandDependencies {
14
+ findRepoLocalLinkedRepository?: typeof findRepoLocalLinkedRepository;
15
+ projectWorkingRepository?: typeof projectWorkingRepository;
16
+ projectWorkingRuntimeDocuments?: typeof projectWorkingRuntimeDocuments;
17
+ renderWorkingRuntimeDocuments?: typeof renderWorkingRuntimeDocuments;
18
+ resolveLinkedCompanions?: typeof launchAmbiguityDeps.resolveLinkedCompanions;
19
+ }
20
+
21
+ export const wrapCommandDeps: Required<WrapCommandDependencies> = {
22
+ findRepoLocalLinkedRepository,
23
+ projectWorkingRepository,
24
+ projectWorkingRuntimeDocuments,
25
+ renderWorkingRuntimeDocuments,
26
+ resolveLinkedCompanions: (cwd: string) => launchAmbiguityDeps.resolveLinkedCompanions(cwd),
27
+ };
28
+
29
+ /** The companion's own selection, read from files: wrapping runs no setup pass. */
30
+ async function readCompanionConfig(companionPath: string) {
31
+ return mergeWithDefaults(
32
+ await new ConfigStore(
33
+ path.join(companionPath, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"),
34
+ ).load(),
35
+ );
36
+ }
37
+
38
+ /**
39
+ * `--companion` is consumed by the dispatch gate before the command runs, so it
40
+ * is parsed there too; this only rejects anything else.
41
+ */
42
+ export function parseWrapArgs(argv: string[]): { companion?: string } | { error: string } {
43
+ let companion: string | undefined;
44
+ for (let index = 0; index < argv.length; index += 1) {
45
+ const arg = argv[index]!;
46
+ if (arg === "--companion") {
47
+ const value = argv[index + 1];
48
+ if (!value || value.startsWith("--")) return { error: "--companion requires a path" };
49
+ companion = value;
50
+ index += 1;
51
+ continue;
52
+ }
53
+ if (arg.startsWith("--companion=")) {
54
+ const value = arg.slice("--companion=".length);
55
+ if (!value) return { error: "--companion requires a path" };
56
+ companion = value;
57
+ continue;
58
+ }
59
+ return { error: `unknown wrap option: ${arg}` };
60
+ }
61
+ return companion === undefined ? {} : { companion };
62
+ }
63
+
64
+ /**
65
+ * Writes the Working Repository's Projection Root and the documents the Agent
66
+ * Runtimes discover by themselves, so an Unmanaged Session both resolves its
67
+ * Repository Link and loads the readers that consult it. The companion has
68
+ * already been settled by the dispatch gate, which honors `--companion`, the
69
+ * launch environment, and the picker in that order; wrapping only makes the
70
+ * answer durable.
71
+ */
72
+ export async function runWrapCommand(
73
+ argv: string[] = [],
74
+ cwd: string = process.cwd(),
75
+ deps: WrapCommandDependencies = {},
76
+ ): Promise<void> {
77
+ const findRepository =
78
+ deps.findRepoLocalLinkedRepository ?? wrapCommandDeps.findRepoLocalLinkedRepository;
79
+ const project = deps.projectWorkingRepository ?? wrapCommandDeps.projectWorkingRepository;
80
+ const projectDocuments =
81
+ deps.projectWorkingRuntimeDocuments ?? wrapCommandDeps.projectWorkingRuntimeDocuments;
82
+ const renderDocuments =
83
+ deps.renderWorkingRuntimeDocuments ?? wrapCommandDeps.renderWorkingRuntimeDocuments;
84
+ const resolveLinked = deps.resolveLinkedCompanions ?? wrapCommandDeps.resolveLinkedCompanions;
85
+
86
+ const parsed = parseWrapArgs(argv);
87
+ if ("error" in parsed) {
88
+ console.error(`${FRAMEWORK_NAME}: ${parsed.error}`);
89
+ process.exitCode = 1;
90
+ return;
91
+ }
92
+
93
+ const repository = await findRepository(cwd);
94
+ if (!repository) {
95
+ console.error(
96
+ `${FRAMEWORK_NAME}: this directory is not a working repository linked to a companion.`,
97
+ );
98
+ console.error(`Run \`${FRAMEWORK_NAME} companion link\` first.`);
99
+ process.exitCode = 1;
100
+ return;
101
+ }
102
+
103
+ /**
104
+ * The gate pins only on ambiguity, so the unambiguous case still resolves
105
+ * here — a single Linked Companion needs no answer to have been recorded.
106
+ */
107
+ const companionPath =
108
+ (parsed.companion && path.resolve(parsed.companion)) ??
109
+ process.env.MATE_ARTIFACT_PATH ??
110
+ (await resolveLinked(cwd))[0]?.companionPath;
111
+ if (!companionPath) {
112
+ console.error(`${FRAMEWORK_NAME}: no registered companion links ${repository.id}.`);
113
+ console.error(`Run \`${FRAMEWORK_NAME} companion link\` first.`);
114
+ process.exitCode = 1;
115
+ return;
116
+ }
117
+
118
+ const result = await project(companionPath, repository);
119
+ if (result.kind === "failed") {
120
+ console.error(
121
+ `${FRAMEWORK_NAME}: failed to write the projection for ${repository.id}: ${result.error.message}`,
122
+ );
123
+ process.exitCode = 1;
124
+ return;
125
+ }
126
+
127
+ /**
128
+ * After the projection, never instead of it: a runtime document that cannot
129
+ * be written fails the command while the projection already written stays.
130
+ */
131
+ const config = await readCompanionConfig(result.companionPath);
132
+ const documents = await projectDocuments(
133
+ result.companionPath,
134
+ repository,
135
+ config,
136
+ await renderDocuments(result.companionPath, config, repository.path),
137
+ );
138
+ if (documents.kind === "failed") {
139
+ console.error(
140
+ `${FRAMEWORK_NAME}: failed to reconcile ${documents.document} for ${repository.id}: ${documents.error.message}`,
141
+ );
142
+ process.exitCode = 1;
143
+ return;
144
+ }
145
+
146
+ const changed = result.kind === "written" || documents.kind === "written";
147
+ console.log(
148
+ changed
149
+ ? `Wrapped ${repository.id} at ${result.projectionRoot}`
150
+ : `Already wrapped: ${result.projectionRoot}`,
151
+ );
152
+ console.log(` Companion: ${result.companionPath}`);
153
+ for (const document of documents.documents) {
154
+ console.log(` Runtime: ${document}`);
155
+ }
156
+ /**
157
+ * Said at the moment the mode is switched, not left to be discovered at the
158
+ * next launch: wrapping makes `mate claude` and `mate opencode` refuse here,
159
+ * and the operator has no other way to learn that from a successful command.
160
+ */
161
+ console.log(
162
+ `Start sessions yourself now: \`claude\` or \`opencode\`. \`${FRAMEWORK_NAME} claude\` and \`${FRAMEWORK_NAME} opencode\` do not run in a wrapped repository — \`${FRAMEWORK_NAME} unwrap\` reverses this.`,
163
+ );
164
+ }
package/src/cli/main.ts CHANGED
@@ -16,13 +16,22 @@ import { runLaunchClaudeCommand } from "./commands/launch/claude";
16
16
  import { runLaunchOpenCodeCommand } from "./commands/launch/opencode";
17
17
  import { runPluginCommand } from "./commands/plugin/plugin";
18
18
  import { runReportCommand } from "./commands/report";
19
+ import { runStudioCommand } from "./commands/studio";
19
20
  import { runUpdateCommand } from "./commands/update";
20
- import { runWorkspaceCommand } from "./commands/workspace/workspace";
21
+ import { runUnwrapCommand } from "./commands/unwrap";
22
+ import { parseWrapArgs, runWrapCommand } from "./commands/wrap";
21
23
  import { runWorkingCommand } from "./commands/working/working";
22
24
  import { runInstallCommand } from "./commands/install";
23
- import { inspectInstallPreflight } from "../lib/install";
25
+ import {
26
+ inspectInstallPreflight,
27
+ isRepairableInstallPreflight,
28
+ repairInstallState,
29
+ } from "../lib/install";
24
30
  import { resolveRootContext } from "../lib/orchestrator/root-context";
25
- import { ensureUnambiguousCompanion } from "./commands/shared/companion-selection";
31
+ import {
32
+ ensureUnambiguousCompanion,
33
+ type CompanionSelectionOptions,
34
+ } from "./commands/shared/companion-selection";
26
35
  import { hydrateDynamicPlugins } from "../tools/setup/dynamic-plugins/hydrate";
27
36
  import { findPluginCliCommand } from "./plugin-commands";
28
37
  import { usage } from "./usage";
@@ -34,8 +43,12 @@ export interface GateNeeds {
34
43
  notHubRoot?: boolean;
35
44
  /** Block the command when the current directory resolves to a companion root. */
36
45
  notCompanionRoot?: boolean;
37
- /** Require an unambiguous companion (selection wizard on ambiguity) before dispatch. */
38
- companion?: boolean;
46
+ /**
47
+ * Require an unambiguous companion (selection wizard on ambiguity) before
48
+ * dispatch. Options select a companion explicitly or force the wizard past a
49
+ * recorded answer.
50
+ */
51
+ companion?: boolean | CompanionSelectionOptions;
39
52
  /** Require a complete installation (install preflight) before dispatch. */
40
53
  install?: boolean;
41
54
  }
@@ -43,6 +56,7 @@ export interface GateNeeds {
43
56
  export interface MainDeps {
44
57
  ensureUnambiguousCompanion: typeof ensureUnambiguousCompanion;
45
58
  inspectInstallPreflight: typeof inspectInstallPreflight;
59
+ repairInstallState?: typeof repairInstallState;
46
60
  hydrateDynamicPlugins: typeof hydrateDynamicPlugins;
47
61
  resolveRootContext: typeof resolveRootContext;
48
62
  }
@@ -50,6 +64,7 @@ export interface MainDeps {
50
64
  const mainDeps: MainDeps = {
51
65
  ensureUnambiguousCompanion,
52
66
  inspectInstallPreflight,
67
+ repairInstallState,
53
68
  hydrateDynamicPlugins,
54
69
  resolveRootContext,
55
70
  };
@@ -80,17 +95,16 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
80
95
  const isPluginCommand =
81
96
  command === "cap" && findPluginCliCommand(subcommand, rest[0]) !== undefined;
82
97
 
83
- // `workspace list`/`workspace materialize` are machine-JSON contracts an
84
- // editor extension may poll frequently; their stdout must be pure JSON,
85
- // and the un-awaited scheduleBackgroundCheck() has been observed to race
86
- // a large final stdout write against process exit, truncating it. Both
87
- // reasons put them in the same stdout-owning bucket as plugin commands.
88
- const ownsStdout = isPluginCommand || command === "workspace";
98
+ // Plugin commands are the only stdout owners: the un-awaited
99
+ // scheduleBackgroundCheck() has been observed to race a large final stdout
100
+ // write against process exit, truncating it.
101
+ const ownsStdout = isPluginCommand;
89
102
 
90
103
  // Every dispatch case opens with a gate() call declaring what the command
91
104
  // needs before it may run. Declared gates run in fixed order — update
92
105
  // enforcement, companion selection, install preflight — and a blocked gate
93
106
  // sets the exit code, so callers only need `if (!(await gate(...))) return`.
107
+ let installRepairAttempted = false;
94
108
  const gate = async (needs: GateNeeds): Promise<boolean> => {
95
109
  if (!ownsStdout) {
96
110
  const updateStore = new UpdateStateStore();
@@ -120,7 +134,11 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
120
134
  }
121
135
  }
122
136
 
123
- if (needs.companion && !(await deps.ensureUnambiguousCompanion())) {
137
+ const companionOptions = typeof needs.companion === "object" ? needs.companion : {};
138
+ if (
139
+ needs.companion &&
140
+ !(await deps.ensureUnambiguousCompanion(process.cwd(), companionOptions))
141
+ ) {
124
142
  process.exitCode = 1;
125
143
  return false;
126
144
  }
@@ -128,6 +146,14 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
128
146
  if (needs.install) {
129
147
  const preflight = await deps.inspectInstallPreflight();
130
148
  if (!preflight.ok) {
149
+ if (!installRepairAttempted && isRepairableInstallPreflight(preflight)) {
150
+ installRepairAttempted = true;
151
+ try {
152
+ await (deps.repairInstallState ?? repairInstallState)(preflight.plan!);
153
+ process.stderr.write(`${FRAMEWORK_NAME}: repaired installation state; continuing.\n`);
154
+ if ((await deps.inspectInstallPreflight()).ok) return true;
155
+ } catch {}
156
+ }
131
157
  console.error(`${FRAMEWORK_NAME}: ${preflight.reason ?? "installation is incomplete"}`);
132
158
  console.error(`Run \`${FRAMEWORK_NAME} install\` before continuing.`);
133
159
  process.exitCode = 1;
@@ -171,9 +197,10 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
171
197
  case "hub":
172
198
  if (!(await gate({ updateGuard: true, notCompanionRoot: true }))) return;
173
199
  break;
174
- // open/tui consume a companion context.
200
+ // open/tui/sync consume a companion context.
175
201
  case "open":
176
202
  case "tui":
203
+ case "sync":
177
204
  if (!(await gate({ updateGuard: true, notHubRoot: true, companion: true }))) return;
178
205
  break;
179
206
  // list and unknown subcommands (which fail inside the command).
@@ -199,6 +226,14 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
199
226
  if (!(await gate({ updateGuard: true, install: true }))) return;
200
227
  await runReportCommand(argv.slice(3));
201
228
  return;
229
+ case "studio":
230
+ /**
231
+ * Serves the machine-wide inventory read-only, so it resolves no
232
+ * companion context and stays runnable from any directory.
233
+ */
234
+ if (!(await gate({ updateGuard: true }))) return;
235
+ await runStudioCommand(argv.slice(3));
236
+ return;
202
237
  case "config":
203
238
  if (!(await gate({ updateGuard: true }))) return;
204
239
  await runConfigCommand(argv.slice(3));
@@ -215,16 +250,30 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
215
250
  if (!(await gate({}))) return;
216
251
  await runUpdateCommand(argv.slice(3));
217
252
  return;
218
- case "workspace":
219
- // list/materialize are context-independent: no companion, install, or
220
- // update gating — an editor extension may call these frequently.
221
- if (!(await gate({}))) return;
222
- await runWorkspaceCommand(subcommand, rest);
223
- return;
224
253
  case "working":
225
254
  if (!(await gate({}))) return;
226
255
  await runWorkingCommand(subcommand, rest);
227
256
  return;
257
+ case "wrap": {
258
+ // Recording the answer is the point, so a recorded one must not suppress
259
+ // the wizard; a bad flag is reported without prompting first.
260
+ const wrapArgs = argv.slice(3);
261
+ const parsedWrap = parseWrapArgs(wrapArgs);
262
+ if ("error" in parsedWrap) {
263
+ await runWrapCommand(wrapArgs);
264
+ return;
265
+ }
266
+ const needs = { ignoreProjection: true, companion: parsedWrap.companion };
267
+ if (!(await gate({ updateGuard: true, companion: needs, install: true }))) return;
268
+ await runWrapCommand(wrapArgs);
269
+ return;
270
+ }
271
+ case "unwrap":
272
+ // A recovery path: it withdraws what a wrap placed, so it must never be
273
+ // gated on a companion or an installation it does not consult.
274
+ if (!(await gate({}))) return;
275
+ await runUnwrapCommand(argv.slice(3));
276
+ return;
228
277
  default:
229
278
  // Unknown commands fail fast: no case matched, so no gate ever ran.
230
279
  console.error(`Unknown command: ${command}`);
@@ -7,31 +7,56 @@ import type { CapabilityConfig } from "../lib/orchestrator/types";
7
7
 
8
8
  export type CliFlags = Record<string, string | string[] | boolean>;
9
9
 
10
- export function parseFlags(argv: string[]): CliFlags {
10
+ /**
11
+ * Flags whose presence is the whole value. A flag not listed here consumes the next
12
+ * non-`--` token, so an unlisted boolean silently swallows a positional and then reads
13
+ * as absent — e.g. `--no-push my-change` yielding `{"no-push": "my-change"}`.
14
+ */
15
+ export type BooleanFlagSet = ReadonlySet<string>;
16
+
17
+ const NO_BOOLEAN_FLAGS: BooleanFlagSet = new Set<string>();
18
+
19
+ export function parseFlags(
20
+ argv: string[],
21
+ booleanFlags: BooleanFlagSet = NO_BOOLEAN_FLAGS,
22
+ ): CliFlags {
11
23
  const flags: CliFlags = {};
12
24
 
25
+ const assign = (key: string, value: string): void => {
26
+ const current = flags[key];
27
+ if (current === undefined) flags[key] = value;
28
+ else if (Array.isArray(current)) current.push(value);
29
+ else flags[key] = [current as string, value];
30
+ };
31
+
13
32
  for (let index = 0; index < argv.length; index += 1) {
14
33
  const token = argv[index];
15
34
  if (!token.startsWith("--")) {
16
35
  continue;
17
36
  }
18
37
 
19
- const key = token.slice(2);
20
- const next = argv[index + 1];
38
+ const body = token.slice(2);
39
+ const equals = body.indexOf("=");
40
+ if (equals !== -1) {
41
+ const key = body.slice(0, equals);
42
+ const value = body.slice(equals + 1);
43
+ if (booleanFlags.has(key)) flags[key] = value !== "false";
44
+ else assign(key, value);
45
+ continue;
46
+ }
21
47
 
22
- if (!next || next.startsWith("--")) {
23
- flags[key] = true;
48
+ if (booleanFlags.has(body)) {
49
+ flags[body] = true;
24
50
  continue;
25
51
  }
26
52
 
27
- if (flags[key] === undefined) {
28
- flags[key] = next;
29
- } else if (Array.isArray(flags[key])) {
30
- flags[key].push(next);
31
- } else {
32
- flags[key] = [flags[key] as string, next];
53
+ const next = argv[index + 1];
54
+ if (!next || next.startsWith("--")) {
55
+ flags[body] = true;
56
+ continue;
33
57
  }
34
58
 
59
+ assign(body, next);
35
60
  index += 1;
36
61
  }
37
62
 
package/src/cli/usage.ts CHANGED
@@ -14,17 +14,21 @@ export function usage(): string {
14
14
  ` ${n} companion link`,
15
15
  ` ${n} companion setup`,
16
16
  ` ${n} companion list`,
17
+ ` ${n} companion sync`,
17
18
  ` ${n} companion open`,
18
19
  ` ${n} companion tui`,
19
- ` ${n} workspace list --json`,
20
- ` ${n} workspace materialize --repository ID --companion PATH --json`,
21
20
  ` ${n} working cleanup`,
21
+ ` ${n} wrap [--companion PATH]`,
22
+ ` ${n} unwrap`,
22
23
  ` ${n} hub init [folder]`,
23
24
  ` ${n} hub add [source] [--id ID] [--path PATH]`,
24
25
  ` ${n} hub sync [--json] (companions + hub plugins)`,
25
- ` ${n} artifact finish <change-name> [--type openspec] [--force] [--no-push] [--json]`,
26
+ ` ${n} artifact publish <anchor|change-name> [--type openspec] [--no-push] [--json]`,
27
+ " target: a dated archive anchor, or a change name matching exactly one archive",
28
+ ` ${n} artifact pending [--json]`,
26
29
  ` ${n} doctor`,
27
30
  ` ${n} report [--days N] [--input FILE|-] [--json]`,
31
+ ` ${n} studio`,
28
32
  ` ${n} config`,
29
33
  ` ${n} claude [args...] (use -- --no-git to bypass companion Git sync)`,
30
34
  ` ${n} opencode [args...] (use -- --no-git to bypass companion Git sync)`,
@@ -41,6 +45,11 @@ export function usage(): string {
41
45
  ` ${n} working cleanup removes Mate-owned local integration without resetting product work`,
42
46
  " tracked files and capability data remain untouched; link or launch recreates required state",
43
47
  "",
48
+ "Wrapped repositories:",
49
+ ` ${n} wrap configures the repository so sessions you start yourself load the companion`,
50
+ ` ${n} claude and ${n} opencode do not run while a repository is wrapped`,
51
+ ` ${n} unwrap withdraws the wrap and restores managed launches; the link is kept`,
52
+ "",
44
53
  "Doctor states:",
45
54
  " linked-working-repository — cwd is inside a registered working repository",
46
55
  " companion-repository — cwd is the companion repository itself",
package/src/framework.ts CHANGED
@@ -1,7 +1 @@
1
- /**
2
- * Framework identity: names everything identity-shaped — state directories
3
- * (`~/.mate`, `.mate/`), managed-block markers, `MATE_*` env values, usage
4
- * output, command hints, error prefixes, agent guidance, and permission
5
- * entries. The distribution is always invoked as `mate`.
6
- */
7
- export const FRAMEWORK_NAME = "mate";
1
+ export { FRAMEWORK_NAME } from "./runtime/framework";
@@ -1,4 +1,13 @@
1
- // Show the active Mate companion paths in Claude Code.
1
+ // Show the active Mate companion paths in Claude Code, and repair the
2
+ // companion's Git state before the session reads it.
3
+ import {
4
+ syncCompanionUnattended,
5
+ unattendedSyncStalenessLines,
6
+ type UnattendedSyncOptions,
7
+ } from "../runtime/companion-sync";
8
+ import { hasLaunchEnvironment, resolveCompanionRuntime } from "../runtime/env";
9
+ import { projectionFreshness, projectionStalenessLines } from "../runtime/freshness";
10
+ import { mateVersion } from "../runtime/install";
2
11
  import type { HookEnv } from "./validate-artifact-path";
3
12
 
4
13
  export interface BannerOutcome {
@@ -6,20 +15,64 @@ export interface BannerOutcome {
6
15
  stdout: string;
7
16
  }
8
17
 
9
- // Fail-soft outside managed sessions: no MATE_* env, no banner.
10
- export function buildBanner(env: HookEnv): BannerOutcome {
11
- const repoPath = env.MATE_REPO_PATH;
12
- const artifactPath = env.MATE_ARTIFACT_PATH;
13
- if (!repoPath || !artifactPath) return { exitCode: 0, stdout: "" };
18
+ /**
19
+ * Marks the copy a wrap inlines into the Working Repository's settings. A
20
+ * managed launch loads the plugin's own copy *and* that document, and both
21
+ * registrations resolve to the same command against the same environment, so
22
+ * nothing inside the process tells them apart. The flag is what keeps the
23
+ * banner from printing twice in exactly the session that is configured
24
+ * correctly — the projected copy defers, the launch's own copy prints.
25
+ */
26
+ export const PROJECTED_BANNER_FLAG = "--projected";
14
27
 
15
- const mateVersion = env.MATE_VERSION || "unknown";
16
- const message = `mate v${mateVersion}\n repo: ${repoPath}\n mate: ${artifactPath}`;
17
- return { exitCode: 0, stdout: JSON.stringify({ systemMessage: message }) + "\n" };
28
+ /**
29
+ * Resolves in-process from either source — no subprocess in either branch.
30
+ * With `mate doctor` out of scope this is one of only two surfaces where a
31
+ * drifted wrap becomes visible, so staleness is reported, never suppressed.
32
+ */
33
+ export function buildBanner(
34
+ env: HookEnv,
35
+ cwd: string = process.cwd(),
36
+ argv: readonly string[] = [],
37
+ syncOptions: UnattendedSyncOptions = {},
38
+ ): BannerOutcome {
39
+ if (argv.includes(PROJECTED_BANNER_FLAG) && hasLaunchEnvironment(env)) {
40
+ return { exitCode: 0, stdout: "" };
41
+ }
42
+
43
+ const { context, projection } = resolveCompanionRuntime(env, cwd);
44
+ if (!context.repositoryPath || !context.companionPath) return { exitCode: 0, stdout: "" };
45
+
46
+ /**
47
+ * Session start is the only moment at which the companion may move without
48
+ * invalidating a read the session has already taken, so the repair happens
49
+ * here and nowhere later. The launch-environment check comes first, so a
50
+ * Managed Session pays nothing at all — not the freshness read, not the
51
+ * policy read; and the consent gate is the operation's own.
52
+ */
53
+ const gitNotes = hasLaunchEnvironment(env)
54
+ ? []
55
+ : unattendedSyncStalenessLines(syncCompanionUnattended(context.companionPath, syncOptions));
56
+
57
+ const lines = [
58
+ `mate v${env.MATE_VERSION || mateVersion()}`,
59
+ ` repo: ${context.repositoryPath}`,
60
+ ` mate: ${context.companionPath}`,
61
+ ];
62
+ if (projection) {
63
+ for (const note of projectionStalenessLines(projection, projectionFreshness(projection))) {
64
+ lines.push(` ${note}`);
65
+ }
66
+ }
67
+ /** `systemMessage` only: the model must never be told to run the command. */
68
+ for (const note of gitNotes) lines.push(` ${note}`);
69
+
70
+ return { exitCode: 0, stdout: JSON.stringify({ systemMessage: lines.join("\n") }) + "\n" };
18
71
  }
19
72
 
20
73
  // Plugin-shim entry.
21
- export function run(): number {
22
- const outcome = buildBanner(process.env);
74
+ export function run(argv: readonly string[] = process.argv.slice(2)): number {
75
+ const outcome = buildBanner(process.env, process.cwd(), argv);
23
76
  if (outcome.stdout) process.stdout.write(outcome.stdout);
24
77
  return outcome.exitCode;
25
78
  }
@@ -0,0 +1,40 @@
1
+ // Deliver the companion guidance to a Claude session Mate did not launch.
2
+ import { buildProjectedGuidance } from "../runtime/projected-guidance";
3
+ import type { HookEnv } from "./validate-artifact-path";
4
+
5
+ export interface GuidanceOutcome {
6
+ exitCode: number;
7
+ stdout: string;
8
+ }
9
+
10
+ /**
11
+ * `SessionStart.additionalContext` is the file-based form of the managed
12
+ * launch's `--append-system-prompt`: a settings document can declare a hook,
13
+ * and there is no settings key for the flag. The two never both fire —
14
+ * `buildProjectedGuidance` yields nothing when a launch environment is present
15
+ * — so the plugin may carry this group alongside the projected copy.
16
+ *
17
+ * Emits nothing rather than a note when no companion resolves: an unwrapped
18
+ * repository must read exactly as it did before the hook existed.
19
+ */
20
+ export function buildSessionGuidance(env: HookEnv, cwd: string = process.cwd()): GuidanceOutcome {
21
+ const guidance = buildProjectedGuidance(env, cwd);
22
+ if (!guidance) return { exitCode: 0, stdout: "" };
23
+
24
+ return {
25
+ exitCode: 0,
26
+ stdout: `${JSON.stringify({
27
+ hookSpecificOutput: {
28
+ hookEventName: "SessionStart",
29
+ additionalContext: guidance,
30
+ },
31
+ })}\n`,
32
+ };
33
+ }
34
+
35
+ // Plugin-shim entry.
36
+ export function run(): number {
37
+ const outcome = buildSessionGuidance(process.env);
38
+ if (outcome.stdout) process.stdout.write(outcome.stdout);
39
+ return outcome.exitCode;
40
+ }