@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.
- package/claude-plugin/.claude-plugin/plugin.json +4 -0
- package/claude-plugin/hooks/artifact-finish-nudge.mjs +8 -0
- package/claude-plugin/hooks/hooks.json +37 -0
- package/claude-plugin/hooks/session-banner.mjs +8 -0
- package/claude-plugin/hooks/ts-loader.mjs +28 -0
- package/claude-plugin/hooks/validate-artifact-path.mjs +8 -0
- package/package.json +4 -2
- package/src/cli/commands/artifact/finish/openspec.ts +1 -5
- package/src/cli/commands/companion/hub.ts +99 -0
- package/src/cli/commands/companion/link.ts +0 -5
- package/src/cli/commands/companion/tui.ts +3 -2
- package/src/cli/commands/doctor.ts +254 -170
- package/src/cli/commands/install.ts +18 -14
- package/src/cli/commands/launch/shared.ts +4 -4
- package/src/cli/commands/plugin/install.ts +94 -0
- package/src/cli/commands/plugin/plugin.ts +17 -0
- package/src/cli/commands/setup.ts +4 -6
- package/src/cli/commands/update.ts +9 -11
- package/src/cli/companion-link-wizard.tsx +22 -78
- package/src/cli/main.ts +57 -13
- package/src/cli/plugin-commands.ts +4 -4
- package/src/cli/repo-list-table.tsx +2 -7
- package/src/cli/usage.ts +7 -3
- package/src/distribution.ts +3 -5
- package/src/framework.ts +4 -36
- package/src/hooks/artifact-finish-nudge.ts +212 -0
- package/src/hooks/session-banner.ts +25 -0
- package/src/hooks/validate-artifact-path.ts +236 -0
- package/src/index.ts +4 -0
- package/src/lib/context-mode-package.ts +5 -3
- package/src/lib/install.ts +31 -43
- package/src/lib/orchestrator/adapters/base.ts +7 -8
- package/src/lib/orchestrator/adapters/claude.ts +16 -0
- package/src/lib/orchestrator/adapters/opencode.ts +10 -60
- package/src/lib/orchestrator/companion-hub.ts +380 -0
- package/src/lib/orchestrator/companion-store.ts +1 -35
- package/src/lib/orchestrator/config-store.ts +94 -7
- package/src/lib/orchestrator/editor.ts +4 -20
- package/src/lib/orchestrator/framework-context.ts +119 -24
- package/src/lib/orchestrator/global-config-store.ts +1 -5
- package/src/lib/orchestrator/launcher.ts +3 -9
- package/src/lib/orchestrator/migration.ts +0 -23
- package/src/lib/orchestrator/opencode-guidance.ts +1 -2
- package/src/lib/orchestrator/repo-local-registry.ts +3 -1
- package/src/lib/orchestrator/root-context.ts +117 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
- package/src/lib/orchestrator/setup-preflight.ts +5 -18
- package/src/lib/orchestrator/types.ts +27 -19
- package/src/lib/orchestrator/working-repo-store.ts +9 -3
- package/src/lib/package-paths.ts +37 -0
- package/src/lib/update-checker.ts +5 -5
- package/src/playbooks/companion-guidance.ts +7 -7
- package/src/runtime/env.ts +0 -4
- package/src/templates/capabilities/openspec-cap/mate-skills/{mate-artifact-finish → agents/mate-artifact-finish}/SKILL.md +8 -8
- package/src/templates/capabilities/openspec-cap/mate-skills/{mate-artifact-finish → agents/mate-artifact-finish}/references/openspec.md +3 -3
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +58 -0
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +139 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +6 -5
- package/src/templates/root/TEMPLATE_AGENTS.md +2 -0
- package/src/templates/root/TEMPLATE_CLAUDE.md +2 -0
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3541 -0
- package/src/tools/setup/capabilities/context-mode.ts +57 -83
- package/src/tools/setup/capabilities/graphify-shared.ts +27 -0
- package/src/tools/setup/capabilities/graphify.ts +86 -295
- package/src/tools/setup/capabilities/openspec.ts +27 -30
- package/src/tools/setup/capabilities/react-doctor.ts +37 -37
- package/src/tools/setup/capabilities/rtk.ts +4 -1
- package/src/tools/setup/capabilities/tokensave-shared.ts +6 -0
- package/src/tools/setup/capabilities/tokensave.ts +50 -71
- package/src/tools/setup/context-services.ts +29 -0
- package/src/tools/setup/dynamic-plugins/host.ts +2 -1
- package/src/tools/setup/dynamic-plugins/hydrate.ts +3 -13
- package/src/tools/setup/dynamic-plugins/install.ts +124 -109
- package/src/tools/setup/dynamic-plugins/loader.ts +23 -58
- package/src/tools/setup/dynamic-plugins/paths.ts +8 -19
- package/src/tools/setup/dynamic-plugins/registry-hint.ts +14 -0
- package/src/tools/setup/engine.ts +65 -1
- package/src/tools/setup/mate.ts +28 -31
- package/src/tools/setup/plugin.ts +81 -0
- package/src/tools/setup/plugins/gitignore.ts +19 -9
- package/src/tools/setup/plugins/guidance.ts +2 -6
- package/src/tools/setup/policy.ts +4 -7
- package/src/tools/setup/providers/agent-file-sections.ts +78 -0
- package/src/tools/setup/providers/claude-format.ts +159 -0
- package/src/tools/setup/providers/claude.ts +283 -292
- package/src/tools/setup/providers/opencode-format.ts +146 -0
- package/src/tools/setup/providers/opencode.ts +171 -66
- package/src/tools/setup/providers/skill-tree.ts +38 -0
- package/src/tools/setup.ts +12 -7
- package/src/tui.ts +5 -0
- package/src/templates/capabilities/openspec-cap/claude/hooks/mate-artifact-finish.sh +0 -161
- package/src/templates/providers/claude/.claude/hooks/mate-session-banner +0 -28
- package/src/templates/providers/claude/.claude/hooks/validate-artifact-path +0 -242
- 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
|
-
}
|