@uniqbit/mate-core 0.15.3 → 0.15.4-canary.10

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 (94) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +4 -0
  2. package/claude-plugin/hooks/artifact-finish-nudge.mjs +8 -0
  3. package/claude-plugin/hooks/hooks.json +37 -0
  4. package/claude-plugin/hooks/session-banner.mjs +8 -0
  5. package/claude-plugin/hooks/ts-loader.mjs +28 -0
  6. package/claude-plugin/hooks/validate-artifact-path.mjs +8 -0
  7. package/package.json +4 -2
  8. package/src/cli/commands/artifact/finish/openspec.ts +1 -5
  9. package/src/cli/commands/companion/hub.ts +99 -0
  10. package/src/cli/commands/companion/link.ts +0 -5
  11. package/src/cli/commands/companion/tui.ts +3 -2
  12. package/src/cli/commands/doctor.ts +254 -170
  13. package/src/cli/commands/install.ts +18 -14
  14. package/src/cli/commands/launch/shared.ts +4 -4
  15. package/src/cli/commands/plugin/install.ts +94 -0
  16. package/src/cli/commands/plugin/plugin.ts +17 -0
  17. package/src/cli/commands/setup.ts +4 -6
  18. package/src/cli/commands/update.ts +9 -11
  19. package/src/cli/companion-link-wizard.tsx +22 -78
  20. package/src/cli/main.ts +57 -13
  21. package/src/cli/plugin-commands.ts +4 -4
  22. package/src/cli/repo-list-table.tsx +2 -7
  23. package/src/cli/usage.ts +7 -3
  24. package/src/distribution.ts +3 -5
  25. package/src/framework.ts +4 -36
  26. package/src/hooks/artifact-finish-nudge.ts +212 -0
  27. package/src/hooks/session-banner.ts +25 -0
  28. package/src/hooks/validate-artifact-path.ts +236 -0
  29. package/src/index.ts +4 -0
  30. package/src/lib/context-mode-package.ts +5 -3
  31. package/src/lib/install.ts +31 -43
  32. package/src/lib/orchestrator/adapters/base.ts +7 -8
  33. package/src/lib/orchestrator/adapters/claude.ts +16 -0
  34. package/src/lib/orchestrator/adapters/opencode.ts +10 -60
  35. package/src/lib/orchestrator/companion-hub.ts +380 -0
  36. package/src/lib/orchestrator/companion-store.ts +1 -35
  37. package/src/lib/orchestrator/config-store.ts +94 -7
  38. package/src/lib/orchestrator/editor.ts +4 -20
  39. package/src/lib/orchestrator/framework-context.ts +119 -24
  40. package/src/lib/orchestrator/global-config-store.ts +1 -5
  41. package/src/lib/orchestrator/launcher.ts +3 -9
  42. package/src/lib/orchestrator/migration.ts +0 -23
  43. package/src/lib/orchestrator/opencode-guidance.ts +1 -2
  44. package/src/lib/orchestrator/repo-local-registry.ts +3 -1
  45. package/src/lib/orchestrator/root-context.ts +117 -0
  46. package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
  47. package/src/lib/orchestrator/setup-preflight.ts +5 -18
  48. package/src/lib/orchestrator/types.ts +27 -19
  49. package/src/lib/orchestrator/working-repo-store.ts +9 -3
  50. package/src/lib/package-paths.ts +37 -0
  51. package/src/lib/update-checker.ts +5 -5
  52. package/src/playbooks/companion-guidance.ts +7 -7
  53. package/src/runtime/env.ts +0 -4
  54. package/src/templates/capabilities/openspec-cap/mate-skills/{mate-artifact-finish → agents/mate-artifact-finish}/SKILL.md +8 -8
  55. package/src/templates/capabilities/openspec-cap/mate-skills/{mate-artifact-finish → agents/mate-artifact-finish}/references/openspec.md +3 -3
  56. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +58 -0
  57. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +139 -0
  58. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +6 -5
  59. package/src/templates/root/TEMPLATE_AGENTS.md +2 -0
  60. package/src/templates/root/TEMPLATE_CLAUDE.md +2 -0
  61. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3541 -0
  62. package/src/tools/setup/capabilities/context-mode.ts +57 -83
  63. package/src/tools/setup/capabilities/graphify-shared.ts +27 -0
  64. package/src/tools/setup/capabilities/graphify.ts +86 -295
  65. package/src/tools/setup/capabilities/openspec.ts +27 -30
  66. package/src/tools/setup/capabilities/react-doctor.ts +37 -37
  67. package/src/tools/setup/capabilities/rtk.ts +4 -1
  68. package/src/tools/setup/capabilities/tokensave-shared.ts +6 -0
  69. package/src/tools/setup/capabilities/tokensave.ts +50 -71
  70. package/src/tools/setup/context-services.ts +29 -0
  71. package/src/tools/setup/dynamic-plugins/host.ts +2 -1
  72. package/src/tools/setup/dynamic-plugins/hydrate.ts +3 -13
  73. package/src/tools/setup/dynamic-plugins/install.ts +124 -109
  74. package/src/tools/setup/dynamic-plugins/loader.ts +23 -58
  75. package/src/tools/setup/dynamic-plugins/paths.ts +8 -19
  76. package/src/tools/setup/dynamic-plugins/registry-hint.ts +14 -0
  77. package/src/tools/setup/engine.ts +65 -1
  78. package/src/tools/setup/mate.ts +28 -31
  79. package/src/tools/setup/plugin.ts +81 -0
  80. package/src/tools/setup/plugins/gitignore.ts +19 -9
  81. package/src/tools/setup/plugins/guidance.ts +2 -6
  82. package/src/tools/setup/policy.ts +4 -7
  83. package/src/tools/setup/providers/agent-file-sections.ts +78 -0
  84. package/src/tools/setup/providers/claude-format.ts +159 -0
  85. package/src/tools/setup/providers/claude.ts +283 -292
  86. package/src/tools/setup/providers/opencode-format.ts +146 -0
  87. package/src/tools/setup/providers/opencode.ts +171 -66
  88. package/src/tools/setup/providers/skill-tree.ts +38 -0
  89. package/src/tools/setup.ts +12 -7
  90. package/src/tui.ts +5 -0
  91. package/src/templates/capabilities/openspec-cap/claude/hooks/mate-artifact-finish.sh +0 -161
  92. package/src/templates/providers/claude/.claude/hooks/mate-session-banner +0 -28
  93. package/src/templates/providers/claude/.claude/hooks/validate-artifact-path +0 -242
  94. package/src/tools/setup/dynamic-plugins/pin-store.ts +0 -30
@@ -1,161 +0,0 @@
1
- #!/bin/sh
2
- set -u
3
-
4
- input_file=$(mktemp "${TMPDIR:-/tmp}/mate-artifact-finish.XXXXXX")
5
- trap 'rm -f "$input_file"' EXIT
6
- cat >"$input_file"
7
-
8
- node -e '
9
- const fs = require("node:fs");
10
-
11
- const inputFile = process.argv[1];
12
- const archivePathPattern = /(?:^|[\s\/"\x27`,(])openspec\/changes\/archive\/(\d{4}-\d{2}-\d{2}-[^\/\s"\x27`,)]+)/;
13
-
14
- function shellSplit(command) {
15
- const parts = [];
16
- const singleQuote = String.fromCharCode(39);
17
- let current = "";
18
- let quote = null;
19
- let escaping = false;
20
- for (let index = 0; index < command.length; index += 1) {
21
- const char = command[index];
22
- if (escaping) {
23
- current += char;
24
- escaping = false;
25
- continue;
26
- }
27
- if (char === "\\" && quote !== singleQuote) {
28
- const next = command[index + 1] || "";
29
- if (next === "\\" || /\s/.test(next) || next === singleQuote || next === String.fromCharCode(34)) {
30
- escaping = true;
31
- continue;
32
- }
33
- }
34
- if ((char === singleQuote || char === String.fromCharCode(34)) && !quote) {
35
- quote = char;
36
- continue;
37
- }
38
- if (quote === char) {
39
- quote = null;
40
- continue;
41
- }
42
- const next = command[index + 1] || "";
43
- const previous = command[index - 1] || "";
44
- const isControlOperator = !quote && (
45
- char === ";" || char === "|" ||
46
- (char === "&" && next !== ">" && previous !== ">" && previous !== "<")
47
- );
48
- if (isControlOperator) {
49
- if (current) parts.push(current);
50
- current = "";
51
- if ((char === "|" || char === "&") && next === char) {
52
- parts.push(char + next);
53
- index += 1;
54
- } else {
55
- parts.push(char);
56
- }
57
- continue;
58
- }
59
- if (!quote && /\s/.test(char)) {
60
- if (current) parts.push(current);
61
- current = "";
62
- continue;
63
- }
64
- current += char;
65
- }
66
- if (current) parts.push(current);
67
- return parts;
68
- }
69
-
70
- function extractChangeFromPath(value) {
71
- const match = value.replace(/\\/g, "/").match(archivePathPattern);
72
- return match ? match[1].replace(/^\d{4}-\d{2}-\d{2}-/, "") : null;
73
- }
74
-
75
- // Shell syntax tokens are never change-name positionals: separators end the
76
- // archive invocation, redirections are skipped (a bare operator also consumes
77
- // its target token).
78
- const separatorPattern = /^(\|\|?|&&?|;)$/;
79
- const redirectionPattern = /^(\d*(>>?|<)|&>>?)/;
80
- const bareRedirectionPattern = /^(\d*(>>?|<)|&>>?)$/;
81
-
82
- function extractArchiveCommand(command) {
83
- const parts = shellSplit(command);
84
- const index = parts.findIndex((part, position) =>
85
- part === "archive" && position > 0 &&
86
- (parts[position - 1] === "openspec" || parts[position - 1].endsWith("/openspec")));
87
- if (index < 0) return null;
88
- for (let position = index + 1; position < parts.length; position += 1) {
89
- const part = parts[position];
90
- if (!part || part === "--") continue;
91
- if (separatorPattern.test(part)) return null;
92
- if (redirectionPattern.test(part)) {
93
- if (bareRedirectionPattern.test(part)) position += 1;
94
- continue;
95
- }
96
- if (part.startsWith("-")) {
97
- if (part === "--store") position += 1;
98
- continue;
99
- }
100
- return part;
101
- }
102
- return null;
103
- }
104
-
105
- function extractMoveCommand(command) {
106
- const parts = shellSplit(command);
107
- const moveCommands = new Set([
108
- "mv", "move", "move-item", "cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh", "pwsh.exe",
109
- ]);
110
- const isPython = (part) => /^python(?:\d(?:\.\d+)*)?(?:\.exe)?$/i.test(part.split(/[\\/]/).pop() || part);
111
- for (let index = 0; index < parts.length; index += 1) {
112
- if (moveCommands.has(parts[index].toLowerCase())) {
113
- for (const part of parts.slice(index + 1)) {
114
- const change = extractChangeFromPath(part);
115
- if (change) return change;
116
- }
117
- }
118
- if (isPython(parts[index]) && /(?:shutil\.move|os\.rename|\.rename\s*\()/.test(command)) {
119
- return extractChangeFromPath(command);
120
- }
121
- }
122
- return null;
123
- }
124
-
125
- // Nudge only on apparent success: stay silent on a clear failure indication,
126
- // nudge when success is indicated or indeterminate.
127
- function isClearFailure(response) {
128
- if (!response || typeof response !== "object") return false;
129
- if (response.success === false || response.is_error === true) return true;
130
- if (typeof response.error === "string" && response.error) return true;
131
- if (response.interrupted === true) return true;
132
- const exitCode = response.exit_code ?? response.exitCode;
133
- return typeof exitCode === "number" && exitCode !== 0;
134
- }
135
-
136
- let input = {};
137
- try {
138
- input = JSON.parse(fs.readFileSync(inputFile, "utf8") || "{}");
139
- } catch {}
140
-
141
- if (input.hook_event_name !== "PostToolUse" || input.tool_name !== "Bash") process.exit(0);
142
- const command = input.tool_input && typeof input.tool_input.command === "string"
143
- ? input.tool_input.command
144
- : "";
145
- const change = extractArchiveCommand(command) || extractMoveCommand(command);
146
- if (!change) process.exit(0);
147
- if (isClearFailure(input.tool_response)) process.exit(0);
148
-
149
- const mateCommand = process.env.MATE_COMMAND || "mate";
150
- const context =
151
- "OpenSpec change " + change + " was just archived. Invoke the mate-artifact-finish " +
152
- "skill, then run `" + mateCommand + " artifact finish \"" + change + "\" --json` to complete the " +
153
- "finish workflow (commit, tag, and push).";
154
- process.stdout.write(JSON.stringify({
155
- hookSpecificOutput: {
156
- hookEventName: "PostToolUse",
157
- additionalContext: context,
158
- },
159
- }));
160
- ' "$input_file"
161
- exit 0
@@ -1,28 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Show the active Mate companion paths in Claude Code."""
3
-
4
- from __future__ import annotations
5
-
6
- import json
7
- import os
8
- import sys
9
-
10
-
11
- def main() -> int:
12
- repo_path = os.environ.get("MATE_REPO_PATH")
13
- artifact_path = os.environ.get("MATE_ARTIFACT_PATH")
14
- if not repo_path or not artifact_path:
15
- return 0
16
-
17
- mate_version = os.environ.get("MATE_VERSION", "unknown")
18
- message = (
19
- f"mate v{mate_version}\n"
20
- f" repo: {repo_path}\n"
21
- f" mate: {artifact_path}"
22
- )
23
- print(json.dumps({"systemMessage": message}))
24
- return 0
25
-
26
-
27
- if __name__ == "__main__":
28
- raise SystemExit(main())
@@ -1,242 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Block artifact writes outside the companion framework path.
3
-
4
- Artifact-like files may be written in the working repository only when the
5
- target path is already ignored by git. That keeps local-only scratch files
6
- possible without weakening the default repo split.
7
-
8
- Editing a file that already exists is always allowed: the guard exists to
9
- stop new agent artifacts from landing in the working repo, not to freeze
10
- files that are already part of it.
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- import json
16
- import os
17
- import pathlib
18
- import re
19
- import subprocess
20
- import sys
21
- from typing import Any
22
-
23
-
24
- KNOWN_ARTIFACT_BASENAMES = {
25
- "CLAUDE.md",
26
- "CONTEXT.md",
27
- "design.md",
28
- "explore-brief.md",
29
- "proposal.md",
30
- "spec.md",
31
- "tasks.md",
32
- }
33
-
34
- ARTIFACT_PATH_MARKERS = (
35
- "/changes/",
36
- "/openspec/",
37
- "/specs/",
38
- "/docs/adr/",
39
- "/docs/adrs/",
40
- "/docs/decisions/",
41
- "/docs/prd/",
42
- )
43
- MD_REDIRECT_RE = re.compile(r">{1,2}\s*([^\s;|&<>]+\.md)\b")
44
- MD_TEE_RE = re.compile(r"\btee\s+(?:-a\s+)?([^\s;|&<>]+\.md)\b")
45
-
46
-
47
- def read_payload() -> dict[str, Any]:
48
- try:
49
- return json.load(sys.stdin)
50
- except json.JSONDecodeError:
51
- return {}
52
-
53
-
54
- def artifact_like_path(file_path: str) -> bool:
55
- if not file_path:
56
- return False
57
-
58
- basename = os.path.basename(file_path)
59
- normalized = "/" + file_path.replace("\\", "/").lstrip("/")
60
- if basename in KNOWN_ARTIFACT_BASENAMES:
61
- return True
62
-
63
- if any(marker in normalized for marker in ARTIFACT_PATH_MARKERS):
64
- return True
65
-
66
- if basename.endswith(".md") and basename != "README.md":
67
- return True
68
-
69
- return False
70
-
71
-
72
- def repo_root() -> str:
73
- return os.environ.get("MATE_REPO_PATH") or os.getcwd()
74
-
75
-
76
- def companion_root() -> str | None:
77
- companion = os.environ.get("MATE_ARTIFACT_PATH")
78
- return os.path.normpath(companion) if companion else None
79
-
80
-
81
- def claude_config_root() -> str:
82
- """Claude Code's own config/state directory (plan files, settings, todos).
83
- Writes there are agent-runtime state, never Mate artifacts."""
84
- configured = os.environ.get("CLAUDE_CONFIG_DIR")
85
- if configured:
86
- return os.path.normpath(configured)
87
- return os.path.normpath(os.path.join(os.path.expanduser("~"), ".claude"))
88
-
89
-
90
- def normalize_path(file_path: str) -> str:
91
- if os.path.isabs(file_path):
92
- return os.path.normpath(file_path)
93
- return os.path.normpath(os.path.join(repo_root(), file_path))
94
-
95
-
96
- def is_under(path: str, root: str) -> bool:
97
- try:
98
- return os.path.commonpath([os.path.normpath(path), os.path.normpath(root)]) == os.path.normpath(root)
99
- except ValueError:
100
- return False
101
-
102
-
103
- def is_gitignored(path: str) -> bool:
104
- root = repo_root()
105
- if not is_under(path, root):
106
- return False
107
-
108
- rel_path = os.path.relpath(path, root)
109
- try:
110
- result = subprocess.run(
111
- ["git", "-C", root, "check-ignore", "--no-index", "-q", "--", rel_path],
112
- stdout=subprocess.DEVNULL,
113
- stderr=subprocess.DEVNULL,
114
- check=False,
115
- )
116
- except FileNotFoundError:
117
- return False
118
- return result.returncode == 0
119
-
120
-
121
- def is_git_tracked(path: str) -> bool:
122
- """A file already tracked in the working repo is product code being edited,
123
- not a new agent artifact (e.g. packaged SKILL.md/spec.md template sources)."""
124
- root = repo_root()
125
- if not is_under(path, root):
126
- return False
127
-
128
- rel_path = os.path.relpath(path, root)
129
- try:
130
- result = subprocess.run(
131
- ["git", "-C", root, "ls-files", "--error-unmatch", "--", rel_path],
132
- stdout=subprocess.DEVNULL,
133
- stderr=subprocess.DEVNULL,
134
- check=False,
135
- )
136
- except FileNotFoundError:
137
- return False
138
- return result.returncode == 0
139
-
140
-
141
- def is_product_documentation_path(path: str) -> bool:
142
- root = repo_root()
143
- if not is_under(path, root):
144
- return False
145
-
146
- rel_path = os.path.relpath(path, root)
147
- parts = pathlib.Path(rel_path).parts
148
- for index, part in enumerate(parts):
149
- if part in {".storybook", "storybook"}:
150
- return True
151
-
152
- if part != "docs":
153
- continue
154
-
155
- docs_root = os.path.join(root, *parts[: index + 1])
156
- if os.path.exists(os.path.join(docs_root, "package.json")):
157
- return True
158
-
159
- return False
160
-
161
-
162
- def block_write(target: str, companion: str) -> None:
163
- print("Mate guardrail: artifact writes must go to the companion framework path.", file=sys.stderr)
164
- print(f" target: {target}", file=sys.stderr)
165
- print(f" companion: {companion}", file=sys.stderr)
166
- print("Use an absolute path under MATE_ARTIFACT_PATH instead.", file=sys.stderr)
167
-
168
- if os.path.basename(target) == "CLAUDE.md":
169
- print(
170
- f"Note: CLAUDE.md already lives in the companion repo at {companion}/CLAUDE.md.",
171
- file=sys.stderr,
172
- )
173
-
174
- raise SystemExit(2)
175
-
176
-
177
- def check_file_path(file_path: str, companion: str | None, allow_existing: bool = False) -> None:
178
- if not file_path or not companion:
179
- return
180
-
181
- normalized = normalize_path(file_path)
182
- if normalized.startswith(companion):
183
- return
184
-
185
- if is_under(normalized, claude_config_root()):
186
- return
187
-
188
- if allow_existing and os.path.isfile(normalized):
189
- return
190
-
191
- if is_product_documentation_path(normalized):
192
- return
193
-
194
- if not artifact_like_path(file_path):
195
- return
196
-
197
- if is_under(normalized, repo_root()) and (
198
- is_gitignored(normalized) or is_git_tracked(normalized)
199
- ):
200
- return
201
-
202
- block_write(file_path, companion)
203
-
204
-
205
- def command_matches(command: str) -> list[str]:
206
- targets: list[str] = []
207
- for pattern in (MD_REDIRECT_RE, MD_TEE_RE):
208
- for match in pattern.finditer(command):
209
- target = match.group(1).strip().strip('"\'')
210
- if target:
211
- targets.append(target)
212
- return targets
213
-
214
-
215
- def main() -> int:
216
- companion = companion_root()
217
- if not companion:
218
- return 0
219
-
220
- payload = read_payload()
221
- tool_name = payload.get("tool_name", "")
222
- tool_input = payload.get("tool_input", {})
223
-
224
- if tool_name in {"Write", "Edit", "MultiEdit"}:
225
- check_file_path(
226
- str(tool_input.get("file_path", "")),
227
- companion,
228
- allow_existing=tool_name in {"Edit", "MultiEdit"},
229
- )
230
- return 0
231
-
232
- if tool_name == "Bash":
233
- command = str(tool_input.get("command", ""))
234
- for target in command_matches(command):
235
- check_file_path(target, companion)
236
- return 0
237
-
238
- return 0
239
-
240
-
241
- if __name__ == "__main__":
242
- raise SystemExit(main())
@@ -1,30 +0,0 @@
1
- import { YamlFileStore } from "../../../lib/orchestrator/yaml-file-store";
2
- import { pluginPinFilePath } from "./paths";
3
-
4
- /** Committed record of one declared plugin's resolved install. */
5
- export interface PluginPin {
6
- package: string;
7
- declaredVersion: string;
8
- resolvedVersion: string;
9
- integrity?: string;
10
- }
11
-
12
- export interface PluginPinFile {
13
- plugins: PluginPin[];
14
- }
15
-
16
- export class PluginPinStore extends YamlFileStore<PluginPinFile> {
17
- constructor(companionPath: string) {
18
- super(pluginPinFilePath(companionPath));
19
- }
20
-
21
- override async load(): Promise<PluginPinFile> {
22
- const parsed = await super.load();
23
- return { plugins: Array.isArray(parsed?.plugins) ? parsed.plugins : [] };
24
- }
25
-
26
- protected onMissing(): Promise<PluginPinFile> {
27
- // The pin file appears only once install records a resolved plugin.
28
- return Promise.resolve({ plugins: [] });
29
- }
30
- }