@uniqbit/mate-core 0.15.5 → 0.16.0-canary.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-plugin/.claude-plugin/plugin.json +2 -2
- package/claude-plugin/hooks/hooks.json +3 -6
- package/claude-plugin/hooks/session-guidance.mjs +8 -0
- package/claude-plugin/hooks/ts-loader.mjs +19 -0
- package/package.json +7 -5
- package/src/cli/commands/artifact/artifact.ts +19 -3
- package/src/cli/commands/artifact/finish/command.ts +183 -57
- package/src/cli/commands/artifact/finish/engine.ts +80 -91
- package/src/cli/commands/artifact/finish/finisher.ts +26 -33
- package/src/cli/commands/artifact/finish/git.ts +34 -23
- package/src/cli/commands/artifact/finish/index.ts +9 -3
- package/src/cli/commands/artifact/finish/openspec.ts +225 -97
- package/src/cli/commands/artifact/pending/command.ts +175 -0
- package/src/cli/commands/artifact/pending/discovery.ts +249 -0
- package/src/cli/commands/artifact/pending/index.ts +17 -0
- package/src/cli/commands/cap/index-cmd.ts +9 -1
- package/src/cli/commands/cap/index.ts +2 -6
- package/src/cli/commands/cap/tokensave.ts +4 -4
- package/src/cli/commands/companion/companion.ts +5 -1
- package/src/cli/commands/companion/link.ts +2 -2
- package/src/cli/commands/companion/sync.ts +92 -0
- package/src/cli/commands/doctor.ts +0 -3
- package/src/cli/commands/launch/shared.ts +23 -5
- package/src/cli/commands/report/collector.ts +72 -78
- package/src/cli/commands/report/contract.ts +40 -1
- package/src/cli/commands/report/highlight.ts +27 -0
- package/src/cli/commands/report/index.ts +11 -18
- package/src/cli/commands/report/renderer.ts +199 -2
- package/src/cli/commands/report/types.ts +26 -1
- package/src/cli/commands/shared/companion-selection.ts +107 -10
- package/src/cli/commands/studio/areas.ts +68 -0
- package/src/cli/commands/studio/index.ts +69 -0
- package/src/cli/commands/studio/inventory.ts +55 -0
- package/src/cli/commands/studio/mate-inventory.ts +43 -0
- package/src/cli/commands/studio/openspec-cli.ts +198 -0
- package/src/cli/commands/studio/payload.ts +184 -0
- package/src/cli/commands/studio/routes.ts +2 -0
- package/src/cli/commands/studio/selection.ts +61 -0
- package/src/cli/commands/studio/server.ts +201 -0
- package/src/cli/commands/studio/snapshot.ts +63 -0
- package/src/cli/commands/studio/topology.ts +199 -0
- package/src/cli/commands/studio/views/client.ts +197 -0
- package/src/cli/commands/studio/views/companion-picker.tsx +91 -0
- package/src/cli/commands/studio/views/companion-selector.tsx +90 -0
- package/src/cli/commands/studio/views/dashboard/changes.tsx +107 -0
- package/src/cli/commands/studio/views/dashboard/index.tsx +19 -0
- package/src/cli/commands/studio/views/document.tsx +256 -0
- package/src/cli/commands/studio/views/error.tsx +20 -0
- package/src/cli/commands/studio/views/model.ts +34 -0
- package/src/cli/commands/studio/views/pairings.tsx +38 -0
- package/src/cli/commands/studio/views/skills/index.tsx +87 -0
- package/src/cli/commands/studio/views/specs/index.tsx +101 -0
- package/src/cli/commands/studio/views/styles.ts +389 -0
- package/src/cli/commands/studio/views/warnings.tsx +21 -0
- package/src/cli/commands/studio/views/workflow/index.tsx +21 -0
- package/src/cli/commands/studio/views/workflow/steps.ts +329 -0
- package/src/cli/commands/studio/views/workflow/transcript.tsx +190 -0
- package/src/cli/commands/unwrap.ts +70 -0
- package/src/cli/commands/wrap.ts +164 -0
- package/src/cli/main.ts +68 -19
- package/src/cli/parse-flags.ts +36 -11
- package/src/cli/usage.ts +12 -3
- package/src/framework.ts +1 -7
- package/src/hooks/session-banner.ts +64 -11
- package/src/hooks/session-guidance.ts +40 -0
- package/src/hooks/validate-artifact-path.ts +108 -35
- package/src/lib/fs-utils.ts +9 -0
- package/src/lib/install.ts +33 -0
- package/src/lib/orchestrator/adapters/base.ts +14 -125
- package/src/lib/orchestrator/adapters/claude.ts +0 -11
- package/src/lib/orchestrator/adapters/opencode.ts +2 -32
- package/src/lib/orchestrator/companion-git-sync.ts +94 -84
- package/src/lib/orchestrator/config-store.ts +2 -21
- package/src/lib/orchestrator/editor.ts +12 -22
- package/src/lib/orchestrator/framework-context.ts +17 -6
- package/src/lib/orchestrator/global-config-store.ts +1 -1
- package/src/lib/orchestrator/launcher.ts +97 -7
- package/src/lib/orchestrator/opencode-guidance.ts +4 -56
- package/src/lib/orchestrator/projection-claude-entry.ts +198 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +120 -0
- package/src/lib/orchestrator/projection-companion-link.ts +62 -0
- package/src/lib/orchestrator/projection-entries.ts +377 -0
- package/src/lib/orchestrator/projection-record.ts +56 -0
- package/src/lib/orchestrator/projection-runtime-documents.ts +424 -0
- package/src/lib/orchestrator/projection-types.ts +169 -0
- package/src/lib/orchestrator/repo-local-registry.ts +37 -133
- package/src/lib/orchestrator/repo-local-store.ts +96 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -9
- package/src/lib/orchestrator/types.ts +1 -0
- package/src/lib/orchestrator/working-repo-projection.ts +366 -0
- package/src/lib/orchestrator/workspace-inventory.ts +1 -1
- package/src/lib/package-paths.ts +11 -1
- package/src/lib/public-npm.ts +2 -1
- package/src/lib/update-checker.ts +15 -9
- package/src/opencode/companion-hooks.ts +89 -245
- package/src/opencode/companion-policy.ts +35 -10
- package/src/opencode/index.ts +1 -0
- package/src/opencode/projected-guidance.ts +56 -0
- package/src/opencode/tui.tsx +13 -4
- package/src/playbooks/companion-guidance.ts +32 -116
- package/src/plugins.ts +0 -1
- package/src/runtime/companion-git-state.ts +156 -0
- package/src/runtime/companion-git.ts +203 -0
- package/src/runtime/companion-guidance.ts +222 -0
- package/src/runtime/companion-sync.ts +298 -0
- package/src/runtime/env-names.ts +30 -0
- package/src/runtime/env.ts +67 -35
- package/src/runtime/framework.ts +10 -0
- package/src/runtime/freshness.ts +58 -0
- package/src/runtime/index.ts +104 -0
- package/src/runtime/install.ts +30 -0
- package/src/runtime/policy.ts +66 -0
- package/src/runtime/projected-guidance.ts +45 -0
- package/src/runtime/projection.ts +224 -0
- package/src/runtime/repo-local.ts +64 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +56 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +48 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/tasks.md +22 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +31 -90
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/design.md +3 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/explore-brief.md +6 -6
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/tasks.md +3 -0
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +30 -0
- package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +2 -2
- package/src/templates/mate-skills/agents/mate-artifact-publish/SKILL.md +185 -0
- package/src/templates/mate-skills/agents/mate-artifact-publish/references/openspec.md +227 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/SKILL.md +68 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +14 -0
- package/src/templates/mate-skills/agents/mate-grill-with-docs/SKILL.md +29 -0
- package/src/templates/mate-skills/agents/mate-grilling/SKILL.md +49 -0
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +156 -0
- package/src/templates/{capabilities/openspec-cap/mate-skills → mate-skills}/agents/mate-openspec-backfill/SKILL.md +4 -2
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +139 -0
- package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +503 -0
- package/src/templates/mate-skills/claude/mate-artifact-publish/SKILL.md +185 -0
- package/src/templates/mate-skills/claude/mate-artifact-publish/references/openspec.md +227 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/SKILL.md +68 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +14 -0
- package/src/templates/mate-skills/claude/mate-grill-with-docs/SKILL.md +29 -0
- package/src/templates/mate-skills/claude/mate-grilling/SKILL.md +49 -0
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +156 -0
- package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +67 -0
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +139 -0
- package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +503 -0
- package/src/templates/report-assets/README.md +32 -0
- package/src/templates/report-assets/mermaid.LICENSE +21 -0
- package/src/templates/report-assets/mermaid.min.js +4376 -0
- package/src/templates/root/TEMPLATE_CLAUDE.md +1 -9
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +1476 -197
- package/src/tools/setup/capabilities/graphify.ts +16 -7
- package/src/tools/setup/capabilities/openspec.ts +63 -56
- package/src/tools/setup/capabilities/tokensave.ts +116 -2
- package/src/tools/setup/engine.ts +34 -6
- package/src/tools/setup/mate.ts +42 -13
- package/src/tools/setup/plugin.ts +9 -0
- package/src/tools/setup/plugins/guidance.ts +11 -1
- package/src/tools/setup/providers/claude-format.ts +49 -4
- package/src/tools/setup/providers/claude-plugin-hooks.ts +117 -0
- package/src/tools/setup/providers/claude.ts +55 -220
- package/src/tools/setup/providers/opencode.ts +41 -14
- package/src/tools/setup/runtime-documents.ts +174 -0
- package/src/tools/setup/surface-target.ts +50 -0
- package/src/tools/setup/working-repo-cleanup.ts +33 -26
- package/src/tools/setup/working-repo-local-state.ts +21 -1
- package/src/tools/setup.ts +25 -3
- package/wrappers/bin/graphify +57 -8
- package/wrappers/bin/openspec +50 -3
- package/claude-plugin/hooks/artifact-finish-nudge.mjs +0 -8
- package/src/cli/commands/cap/headroom.ts +0 -52
- package/src/cli/commands/workspace/list.ts +0 -25
- package/src/cli/commands/workspace/materialize.ts +0 -46
- package/src/cli/commands/workspace/workspace.ts +0 -22
- package/src/hooks/artifact-finish-nudge.ts +0 -244
- package/src/lib/orchestrator/headroom/proxy.ts +0 -116
- package/src/lib/orchestrator/workspace-materialize.ts +0 -80
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/SKILL.md +0 -51
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/references/openspec.md +0 -134
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +0 -58
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +0 -139
- package/src/tools/setup/capabilities/headroom.ts +0 -57
- /package/src/cli/commands/{workspace → companion}/open.ts +0 -0
|
@@ -12,6 +12,10 @@ import fs from "node:fs";
|
|
|
12
12
|
import os from "node:os";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
|
|
15
|
+
import { companionForkRefusal } from "../runtime/companion-sync";
|
|
16
|
+
import { readCompanionRuntimeContext } from "../runtime/env";
|
|
17
|
+
import { companionLinkPath } from "../runtime/repo-local";
|
|
18
|
+
|
|
15
19
|
export type HookEnv = Record<string, string | undefined>;
|
|
16
20
|
|
|
17
21
|
export interface HookOutcome {
|
|
@@ -56,13 +60,53 @@ export function artifactLikePath(filePath: string): boolean {
|
|
|
56
60
|
return false;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Resolved once per evaluation: the companion may come from the launch
|
|
65
|
+
* environment or from the Working Repository's Projection Root, and every
|
|
66
|
+
* path check below is relative to whichever resolved.
|
|
67
|
+
*/
|
|
68
|
+
interface GuardContext {
|
|
69
|
+
env: HookEnv;
|
|
70
|
+
/** As resolved, for the operator: the message names the path they configured. */
|
|
71
|
+
companion: string;
|
|
72
|
+
/** Followed through every link, so filesystem identity decides the verdict. */
|
|
73
|
+
companionReal: string;
|
|
74
|
+
companionLink: string;
|
|
75
|
+
repoRoot: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The target's real path, resolved as far as it exists — a path being written
|
|
80
|
+
* does not yet, so the deepest existing ancestor is resolved and the rest
|
|
81
|
+
* appended.
|
|
82
|
+
*/
|
|
83
|
+
function realPath(target: string): string {
|
|
84
|
+
const absolute = path.resolve(target);
|
|
85
|
+
const trailing: string[] = [];
|
|
86
|
+
let current = absolute;
|
|
87
|
+
for (;;) {
|
|
88
|
+
try {
|
|
89
|
+
return path.join(fs.realpathSync(current), ...trailing);
|
|
90
|
+
} catch {
|
|
91
|
+
const parent = path.dirname(current);
|
|
92
|
+
if (parent === current) return absolute;
|
|
93
|
+
trailing.unshift(path.basename(current));
|
|
94
|
+
current = parent;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
61
97
|
}
|
|
62
98
|
|
|
63
|
-
function
|
|
64
|
-
const
|
|
65
|
-
|
|
99
|
+
function guardContext(env: HookEnv, cwd: string): GuardContext | null {
|
|
100
|
+
const resolved = readCompanionRuntimeContext(env, cwd);
|
|
101
|
+
if (!resolved.companionPath) return null;
|
|
102
|
+
const repoRoot = resolved.repositoryPath || cwd;
|
|
103
|
+
return {
|
|
104
|
+
env,
|
|
105
|
+
companion: path.normalize(resolved.companionPath),
|
|
106
|
+
companionReal: realPath(resolved.companionPath),
|
|
107
|
+
companionLink: companionLinkPath(repoRoot),
|
|
108
|
+
repoRoot,
|
|
109
|
+
};
|
|
66
110
|
}
|
|
67
111
|
|
|
68
112
|
// Claude Code's own config/state directory (plan files, settings, todos).
|
|
@@ -73,9 +117,9 @@ function claudeConfigRoot(env: HookEnv): string {
|
|
|
73
117
|
return path.normalize(path.join(env.HOME || os.homedir(), ".claude"));
|
|
74
118
|
}
|
|
75
119
|
|
|
76
|
-
function normalizePath(filePath: string,
|
|
120
|
+
function normalizePath(filePath: string, ctx: GuardContext): string {
|
|
77
121
|
if (path.isAbsolute(filePath)) return path.normalize(filePath);
|
|
78
|
-
return path.normalize(path.join(repoRoot
|
|
122
|
+
return path.normalize(path.join(ctx.repoRoot, filePath));
|
|
79
123
|
}
|
|
80
124
|
|
|
81
125
|
function isUnder(target: string, root: string): boolean {
|
|
@@ -83,8 +127,17 @@ function isUnder(target: string, root: string): boolean {
|
|
|
83
127
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
84
128
|
}
|
|
85
129
|
|
|
86
|
-
|
|
87
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Under the Projection Root's companion link, or under the companion it lands
|
|
132
|
+
* in — the second answers for a link the operator made, and for the first once
|
|
133
|
+
* its target exists.
|
|
134
|
+
*/
|
|
135
|
+
function isCompanionPath(target: string, ctx: GuardContext): boolean {
|
|
136
|
+
return isUnder(target, ctx.companionLink) || isUnder(realPath(target), ctx.companionReal);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isGitignored(target: string, ctx: GuardContext): boolean {
|
|
140
|
+
const root = ctx.repoRoot;
|
|
88
141
|
if (!isUnder(target, root)) return false;
|
|
89
142
|
|
|
90
143
|
const relPath = path.relative(root, target);
|
|
@@ -96,8 +149,8 @@ function isGitignored(target: string, env: HookEnv): boolean {
|
|
|
96
149
|
|
|
97
150
|
// A file already tracked in the working repo is product code being edited,
|
|
98
151
|
// not a new agent artifact (e.g. packaged SKILL.md/spec.md template sources).
|
|
99
|
-
function isGitTracked(target: string,
|
|
100
|
-
const root = repoRoot
|
|
152
|
+
function isGitTracked(target: string, ctx: GuardContext): boolean {
|
|
153
|
+
const root = ctx.repoRoot;
|
|
101
154
|
if (!isUnder(target, root)) return false;
|
|
102
155
|
|
|
103
156
|
const relPath = path.relative(root, target);
|
|
@@ -107,8 +160,8 @@ function isGitTracked(target: string, env: HookEnv): boolean {
|
|
|
107
160
|
return result.status === 0;
|
|
108
161
|
}
|
|
109
162
|
|
|
110
|
-
function isProductDocumentationPath(target: string,
|
|
111
|
-
const root = repoRoot
|
|
163
|
+
function isProductDocumentationPath(target: string, ctx: GuardContext): boolean {
|
|
164
|
+
const root = ctx.repoRoot;
|
|
112
165
|
if (!isUnder(target, root)) return false;
|
|
113
166
|
|
|
114
167
|
const relPath = path.relative(root, target);
|
|
@@ -140,31 +193,45 @@ function blockWrite(target: string, companion: string): HookOutcome {
|
|
|
140
193
|
|
|
141
194
|
function checkFilePath(
|
|
142
195
|
filePath: string,
|
|
143
|
-
|
|
144
|
-
env: HookEnv,
|
|
196
|
+
ctx: GuardContext,
|
|
145
197
|
allowExisting = false,
|
|
146
198
|
): HookOutcome | null {
|
|
147
|
-
if (!filePath
|
|
199
|
+
if (!filePath) return null;
|
|
148
200
|
|
|
149
|
-
const normalized = normalizePath(filePath,
|
|
150
|
-
if (normalized
|
|
201
|
+
const normalized = normalizePath(filePath, ctx);
|
|
202
|
+
if (isCompanionPath(normalized, ctx)) return null;
|
|
151
203
|
|
|
152
|
-
if (isUnder(normalized, claudeConfigRoot(env))) return null;
|
|
204
|
+
if (isUnder(normalized, claudeConfigRoot(ctx.env))) return null;
|
|
153
205
|
|
|
154
206
|
if (allowExisting && fs.existsSync(normalized) && fs.statSync(normalized).isFile()) return null;
|
|
155
207
|
|
|
156
|
-
if (isProductDocumentationPath(normalized,
|
|
208
|
+
if (isProductDocumentationPath(normalized, ctx)) return null;
|
|
157
209
|
|
|
158
210
|
if (!artifactLikePath(filePath)) return null;
|
|
159
211
|
|
|
160
212
|
if (
|
|
161
|
-
isUnder(normalized, repoRoot
|
|
162
|
-
(isGitignored(normalized,
|
|
213
|
+
isUnder(normalized, ctx.repoRoot) &&
|
|
214
|
+
(isGitignored(normalized, ctx) || isGitTracked(normalized, ctx))
|
|
163
215
|
) {
|
|
164
216
|
return null;
|
|
165
217
|
}
|
|
166
218
|
|
|
167
|
-
return blockWrite(filePath, companion);
|
|
219
|
+
return blockWrite(filePath, ctx.companion);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The fork refusal, unlike the path refusal, is not a function of the tool
|
|
224
|
+
* input alone: it depends on companion Git state, which a Managed Launch may
|
|
225
|
+
* have settled on purpose (`--no-git`, a non-automatic Git policy). So it is
|
|
226
|
+
* gated on the absence of a launch environment, and the path refusal keeps its
|
|
227
|
+
* input-only verdict and its double-load tolerance untouched.
|
|
228
|
+
*/
|
|
229
|
+
function checkForkedCompanion(filePath: string, ctx: GuardContext): HookOutcome | null {
|
|
230
|
+
if (!filePath || !artifactLikePath(filePath)) return null;
|
|
231
|
+
if (!isCompanionPath(normalizePath(filePath, ctx), ctx)) return null;
|
|
232
|
+
|
|
233
|
+
const refusal = companionForkRefusal(ctx.env, ctx.companion);
|
|
234
|
+
return refusal ? { exitCode: 2, stderr: `${refusal}\n` } : null;
|
|
168
235
|
}
|
|
169
236
|
|
|
170
237
|
export function commandMatches(command: string): string[] {
|
|
@@ -179,11 +246,15 @@ export function commandMatches(command: string): string[] {
|
|
|
179
246
|
return targets;
|
|
180
247
|
}
|
|
181
248
|
|
|
182
|
-
|
|
249
|
+
/**
|
|
250
|
+
* Fails open only when no Mate context resolves from either source; in a
|
|
251
|
+
* wrapped Working Repository the guard runs without a launch.
|
|
252
|
+
*/
|
|
253
|
+
export function evaluate(payload: unknown, env: HookEnv, cwd: string = process.cwd()): HookOutcome {
|
|
183
254
|
const allow: HookOutcome = { exitCode: 0, stderr: "" };
|
|
184
255
|
|
|
185
|
-
const
|
|
186
|
-
if (!
|
|
256
|
+
const ctx = guardContext(env, cwd);
|
|
257
|
+
if (!ctx) return allow;
|
|
187
258
|
|
|
188
259
|
const input = payload && typeof payload === "object" ? (payload as Record<string, unknown>) : {};
|
|
189
260
|
const toolName = typeof input.tool_name === "string" ? input.tool_name : "";
|
|
@@ -193,19 +264,21 @@ export function evaluate(payload: unknown, env: HookEnv): HookOutcome {
|
|
|
193
264
|
: {};
|
|
194
265
|
|
|
195
266
|
if (toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit") {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
toolName === "Edit" || toolName === "MultiEdit",
|
|
201
|
-
);
|
|
202
|
-
return outcome ?? allow;
|
|
267
|
+
const filePath = String(toolInput.file_path ?? "");
|
|
268
|
+
/** The path refusal wins when both apply: its message is not replaced. */
|
|
269
|
+
const outcome = checkFilePath(filePath, ctx, toolName === "Edit" || toolName === "MultiEdit");
|
|
270
|
+
return outcome ?? checkForkedCompanion(filePath, ctx) ?? allow;
|
|
203
271
|
}
|
|
204
272
|
|
|
205
273
|
if (toolName === "Bash") {
|
|
206
274
|
const command = String(toolInput.command ?? "");
|
|
207
|
-
|
|
208
|
-
|
|
275
|
+
const targets = commandMatches(command);
|
|
276
|
+
for (const target of targets) {
|
|
277
|
+
const outcome = checkFilePath(target, ctx);
|
|
278
|
+
if (outcome) return outcome;
|
|
279
|
+
}
|
|
280
|
+
for (const target of targets) {
|
|
281
|
+
const outcome = checkForkedCompanion(target, ctx);
|
|
209
282
|
if (outcome) return outcome;
|
|
210
283
|
}
|
|
211
284
|
return allow;
|
package/src/lib/fs-utils.ts
CHANGED
|
@@ -24,3 +24,12 @@ export async function fileExists(filePath: string): Promise<boolean> {
|
|
|
24
24
|
return false;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
export async function pathIsDirectory(candidatePath: string): Promise<boolean> {
|
|
29
|
+
try {
|
|
30
|
+
const stats = await fs.stat(candidatePath);
|
|
31
|
+
return stats.isDirectory();
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/lib/install.ts
CHANGED
|
@@ -278,6 +278,15 @@ export interface InstallPreflightResult {
|
|
|
278
278
|
plan?: InstallPlan;
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
export function isRepairableInstallPreflight(result: InstallPreflightResult): boolean {
|
|
282
|
+
return Boolean(
|
|
283
|
+
!result.ok &&
|
|
284
|
+
result.plan &&
|
|
285
|
+
result.plan.context.kind !== "ambiguous" &&
|
|
286
|
+
result.plan.requirements.every((requirement) => requirement.satisfied),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
281
290
|
export async function inspectInstallPreflight(
|
|
282
291
|
cwd = process.cwd(),
|
|
283
292
|
deps: { stateStore?: InstallStateStore; globalConfigStore?: GlobalConfigStore } = {},
|
|
@@ -328,3 +337,27 @@ export async function reconcileInstalledCompanion(plan: InstallPlan): Promise<vo
|
|
|
328
337
|
const { syncCompanionFiles } = await import("../tools/setup");
|
|
329
338
|
await syncCompanionFiles(plan.context.companionPath, plan.context.config);
|
|
330
339
|
}
|
|
340
|
+
|
|
341
|
+
export async function repairInstallState(plan: InstallPlan): Promise<void> {
|
|
342
|
+
const results = plan.requirements.map((requirement) => ({
|
|
343
|
+
id: requirement.id,
|
|
344
|
+
status: "skipped" as const,
|
|
345
|
+
verified: true,
|
|
346
|
+
}));
|
|
347
|
+
await reconcileInstalledCompanion(plan);
|
|
348
|
+
|
|
349
|
+
const store = installStateStoreForContext(plan.context);
|
|
350
|
+
const state = await readInstallState(store);
|
|
351
|
+
const version = getActiveDistribution().config.version;
|
|
352
|
+
if (
|
|
353
|
+
state?.contractRevision === INSTALL_CONTRACT_REVISION &&
|
|
354
|
+
state.mateVersion === version &&
|
|
355
|
+
state.contextFingerprint === plan.context.fingerprint &&
|
|
356
|
+
state.requirementFingerprint === plan.fingerprint &&
|
|
357
|
+
JSON.stringify(state.requirements) === JSON.stringify(results)
|
|
358
|
+
) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
await saveCompleteInstallState(plan, results, store);
|
|
363
|
+
}
|
|
@@ -1,22 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import { version } from "../../../../package.json";
|
|
5
|
-
import { isCommandOnPath } from "../../fs-utils";
|
|
6
4
|
import { FRAMEWORK_NAME } from "../../../framework";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
GRAPHIFY_OUTPUT_SUBDIR,
|
|
10
|
-
GRAPHIFY_STORE_SEGMENT,
|
|
11
|
-
} from "../../../tools/setup/capabilities/graphify";
|
|
12
|
-
import {
|
|
13
|
-
type SpawnProxyOpts,
|
|
14
|
-
getProxyStderr,
|
|
15
|
-
isProxyReachable as defaultIsProxyReachable,
|
|
16
|
-
resolveHeadroomPort,
|
|
17
|
-
spawnProxyBackground as defaultSpawnProxyBackground,
|
|
18
|
-
waitForProxyReachable as defaultWaitForProxyReachable,
|
|
19
|
-
} from "../headroom/proxy";
|
|
5
|
+
import { MATE_ENV } from "../../../runtime/env-names";
|
|
6
|
+
import { buildProjection, projectionEnvironment } from "../projection-record";
|
|
20
7
|
import type { CapabilityConfig, GitModeProfile, LinkedRepository } from "../types";
|
|
21
8
|
|
|
22
9
|
export interface AdapterContext {
|
|
@@ -37,13 +24,6 @@ export interface PreparedLaunch {
|
|
|
37
24
|
command: string;
|
|
38
25
|
args: string[];
|
|
39
26
|
env: NodeJS.ProcessEnv;
|
|
40
|
-
warning?: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface ProxyDeps {
|
|
44
|
-
isProxyReachable?: (port: number) => Promise<boolean>;
|
|
45
|
-
spawnProxyBackground?: (opts: SpawnProxyOpts) => void;
|
|
46
|
-
waitForProxyReachable?: (port: number) => Promise<boolean>;
|
|
47
27
|
}
|
|
48
28
|
|
|
49
29
|
function prependPathEntry(pathValue: string | undefined, entry: string): string {
|
|
@@ -63,63 +43,33 @@ export abstract class LaunchAdapter {
|
|
|
63
43
|
return {};
|
|
64
44
|
}
|
|
65
45
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
): NodeJS.ProcessEnv {
|
|
72
|
-
return {};
|
|
73
|
-
}
|
|
74
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The projected paths, materialized, plus the predicates the projection
|
|
48
|
+
* refuses to hold. Nothing here derives a path: a managed session and an
|
|
49
|
+
* Unmanaged Session read the same record, so they cannot disagree.
|
|
50
|
+
*/
|
|
75
51
|
environment(context: AdapterContext): NodeJS.ProcessEnv {
|
|
76
52
|
const reactDoctorEnabled = context.capabilities.some((c) => c.name === "react-doctor");
|
|
77
|
-
const
|
|
53
|
+
const projection = buildProjection(context.companionPath, context.repository);
|
|
78
54
|
const env: NodeJS.ProcessEnv = {
|
|
79
55
|
...process.env,
|
|
56
|
+
...projectionEnvironment(projection),
|
|
80
57
|
MATE_NAME: FRAMEWORK_NAME,
|
|
81
|
-
|
|
82
|
-
MATE_ARTIFACT_PATH: context.companionPath,
|
|
83
|
-
MATE_WRAPPER_BIN_PATH: wrapperBinPath,
|
|
84
|
-
PATH: prependPathEntry(process.env.PATH, wrapperBinPath),
|
|
58
|
+
PATH: prependPathEntry(process.env.PATH, projection.wrapperBinPath),
|
|
85
59
|
MATE_GRAPHIFY_ENABLED: context.capabilities.some((c) => c.name === "graphify") ? "1" : "0",
|
|
86
60
|
MATE_OPENSPEC_ENABLED: context.capabilities.some((c) => c.name === "openspec") ? "1" : "0",
|
|
87
61
|
MATE_REACT_DOCTOR_ENABLED: reactDoctorEnabled ? "1" : "0",
|
|
88
62
|
MATE_GIT_AUTO_MODE: context.git === "auto" ? "1" : "0",
|
|
89
|
-
MATE_REPO_ID: context.repository.id,
|
|
90
|
-
MATE_REPO_PATH: context.repository.path,
|
|
91
63
|
MATE_POLICY_JSON: JSON.stringify({ allowedAgents: context.allowedAgents }),
|
|
92
64
|
};
|
|
93
65
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
} else {
|
|
97
|
-
delete env.MATE_REACT_DOCTOR_BIN_PATH;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Route launch-session graphify output to the companion store. graphifyy resolves its
|
|
101
|
-
// output dir from GRAPHIFY_OUT at import time (graphify/paths.py) and every
|
|
102
|
-
// reader honours it, so injecting it into the launch session env keeps the
|
|
103
|
-
// skill's per-shell `$GRAPHIFY_OUT` references and graphifyy's own defaults
|
|
104
|
-
// pointed at the companion store instead of leaking graphify-out/ into the
|
|
105
|
-
// working repo. Absolute value, matching the wrapper and deriveGraphifyPaths contract.
|
|
106
|
-
if (context.capabilities.some((c) => c.name === "graphify")) {
|
|
107
|
-
env.GRAPHIFY_OUT = path.join(
|
|
108
|
-
context.companionPath,
|
|
109
|
-
GRAPHIFY_STORE_SEGMENT,
|
|
110
|
-
context.repository.id,
|
|
111
|
-
GRAPHIFY_OUTPUT_SUBDIR,
|
|
112
|
-
);
|
|
113
|
-
}
|
|
66
|
+
/** The one field where a launch still narrows the projection rather than materializing it. */
|
|
67
|
+
if (!reactDoctorEnabled) delete env[MATE_ENV.reactDoctorBinPath];
|
|
114
68
|
|
|
115
69
|
return env;
|
|
116
70
|
}
|
|
117
71
|
|
|
118
|
-
async prepareLaunch(
|
|
119
|
-
context: AdapterContext,
|
|
120
|
-
args: string[],
|
|
121
|
-
proxyDeps: ProxyDeps = {},
|
|
122
|
-
): Promise<PreparedLaunch> {
|
|
72
|
+
async prepareLaunch(context: AdapterContext, args: string[]): Promise<PreparedLaunch> {
|
|
123
73
|
const builtArgs = this.buildArgs(context, args);
|
|
124
74
|
const baseEnv = this.environment(context);
|
|
125
75
|
const extendedEnv = this.extendEnvironment(context);
|
|
@@ -134,72 +84,11 @@ export abstract class LaunchAdapter {
|
|
|
134
84
|
}
|
|
135
85
|
}
|
|
136
86
|
|
|
137
|
-
|
|
138
|
-
// is routed through the proxy, with no per-launch opt-in. It only falls back
|
|
139
|
-
// to a direct launch if the `headroom` binary is missing or the proxy fails
|
|
140
|
-
// to become ready (handled below).
|
|
141
|
-
const headroomCap = context.capabilities.find((c) => c.name === "headroom");
|
|
142
|
-
if (!headroomCap) {
|
|
143
|
-
return { command: this.toolName, args: builtArgs, env };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (!isCommandOnPath("headroom", env.PATH ?? process.env.PATH ?? "")) {
|
|
147
|
-
return {
|
|
148
|
-
command: this.toolName,
|
|
149
|
-
args: builtArgs,
|
|
150
|
-
env,
|
|
151
|
-
warning: `${FRAMEWORK_NAME}: headroom capability enabled but \`headroom\` was not found on PATH; install with \`uv tool install "headroom-ai[all]"\`; launching ${this.toolName} directly\n`,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const probeProxy = proxyDeps.isProxyReachable ?? defaultIsProxyReachable;
|
|
156
|
-
const spawnProxy = proxyDeps.spawnProxyBackground ?? defaultSpawnProxyBackground;
|
|
157
|
-
const waitForProxy = proxyDeps.waitForProxyReachable ?? defaultWaitForProxyReachable;
|
|
158
|
-
|
|
159
|
-
const port = resolveHeadroomPort();
|
|
160
|
-
const reachable = await probeProxy(port);
|
|
161
|
-
if (!reachable) {
|
|
162
|
-
const maxProxyStartAttempts = 3;
|
|
163
|
-
let ready = false;
|
|
164
|
-
|
|
165
|
-
for (let attempt = 1; attempt <= maxProxyStartAttempts; attempt += 1) {
|
|
166
|
-
spawnProxy({ port });
|
|
167
|
-
if (await waitForProxy(port)) {
|
|
168
|
-
ready = true;
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (!ready) {
|
|
174
|
-
const proxyErr = getProxyStderr(port).trim();
|
|
175
|
-
const errorDetail = proxyErr
|
|
176
|
-
? `\nProxy error:\n${proxyErr
|
|
177
|
-
.split("\n")
|
|
178
|
-
.map((line) => ` ${line}`)
|
|
179
|
-
.join("\n")}`
|
|
180
|
-
: "";
|
|
181
|
-
return {
|
|
182
|
-
command: this.toolName,
|
|
183
|
-
args: builtArgs,
|
|
184
|
-
env,
|
|
185
|
-
warning: `${FRAMEWORK_NAME}: headroom proxy failed to become ready on port ${port} after ${maxProxyStartAttempts} attempts; launching ${this.toolName} directly${errorDetail}\n`,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return {
|
|
191
|
-
command: this.toolName,
|
|
192
|
-
args: builtArgs,
|
|
193
|
-
env: {
|
|
194
|
-
...env,
|
|
195
|
-
...this.headroomEnv(context, port, context.companionPath, env),
|
|
196
|
-
},
|
|
197
|
-
};
|
|
87
|
+
return { command: this.toolName, args: builtArgs, env };
|
|
198
88
|
}
|
|
199
89
|
|
|
200
90
|
async run(context: AdapterContext, args: string[]): Promise<AdapterResult> {
|
|
201
91
|
const launch = await this.prepareLaunch(context, args);
|
|
202
|
-
if (launch.warning) process.stderr.write(launch.warning);
|
|
203
92
|
|
|
204
93
|
return new Promise((resolve, reject) => {
|
|
205
94
|
const child = spawn(launch.command, launch.args, {
|
|
@@ -82,15 +82,4 @@ export class ClaudeAdapter extends LaunchAdapter {
|
|
|
82
82
|
CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: "1",
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
|
-
|
|
86
|
-
protected headroomEnv(
|
|
87
|
-
context: AdapterContext,
|
|
88
|
-
port: number,
|
|
89
|
-
_companionPath: string,
|
|
90
|
-
): NodeJS.ProcessEnv {
|
|
91
|
-
const project = encodeURIComponent(context.repository.id);
|
|
92
|
-
return {
|
|
93
|
-
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}/p/${project}`,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
85
|
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
mergeOpenCodeConfigContent,
|
|
15
15
|
readOpenCodeConfig,
|
|
16
16
|
} from "../../../tools/setup/providers/opencode-format";
|
|
17
|
+
import { renderCompanionExternalDirectoryPermissions } from "../../../tools/setup/providers/opencode";
|
|
17
18
|
import { buildOpenCodeGuidance } from "../opencode-guidance";
|
|
18
19
|
import { LaunchPreflightError } from "../types";
|
|
19
20
|
import { type AdapterContext, LaunchAdapter } from "./base";
|
|
@@ -38,10 +39,7 @@ export class OpenCodeAdapter extends LaunchAdapter {
|
|
|
38
39
|
OPENCODE_CONFIG_CONTENT: mergeOpenCodeConfigContent(
|
|
39
40
|
{
|
|
40
41
|
permission: {
|
|
41
|
-
external_directory:
|
|
42
|
-
[context.companionPath]: "allow",
|
|
43
|
-
[`${context.companionPath}/**`]: "allow",
|
|
44
|
-
},
|
|
42
|
+
external_directory: renderCompanionExternalDirectoryPermissions(context.companionPath),
|
|
45
43
|
},
|
|
46
44
|
references: {
|
|
47
45
|
mate: context.companionPath,
|
|
@@ -60,34 +58,6 @@ export class OpenCodeAdapter extends LaunchAdapter {
|
|
|
60
58
|
return buildOpenCodeGuidance(context.capabilities);
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
protected headroomEnv(
|
|
64
|
-
context: AdapterContext,
|
|
65
|
-
port: number,
|
|
66
|
-
_companionPath: string,
|
|
67
|
-
env: NodeJS.ProcessEnv,
|
|
68
|
-
): NodeJS.ProcessEnv {
|
|
69
|
-
const project = encodeURIComponent(context.repository.id);
|
|
70
|
-
const baseUrl = `http://127.0.0.1:${port}/p/${project}`;
|
|
71
|
-
const configContent = mergeOpenCodeConfigContent(
|
|
72
|
-
{
|
|
73
|
-
provider: {
|
|
74
|
-
anthropic: {
|
|
75
|
-
options: { baseURL: baseUrl },
|
|
76
|
-
},
|
|
77
|
-
openai: {
|
|
78
|
-
options: { baseURL: `${baseUrl}/v1` },
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
env,
|
|
83
|
-
);
|
|
84
|
-
return {
|
|
85
|
-
ANTHROPIC_BASE_URL: baseUrl,
|
|
86
|
-
OPENAI_BASE_URL: `${baseUrl}/v1`,
|
|
87
|
-
OPENCODE_CONFIG_CONTENT: configContent,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
61
|
async validateLaunch(context: AdapterContext): Promise<void> {
|
|
92
62
|
const missingAssets = await Promise.all(
|
|
93
63
|
this.requiredRuntimeAssets.map(async (asset) => {
|