@uniqbit/mate-core 0.18.3-canary.0 → 0.18.3
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/hooks/hooks.json +10 -1
- package/claude-plugin/hooks/session-banner.mjs +4 -3
- package/claude-plugin/hooks/session-prepare.mjs +9 -0
- package/opencode-plugin/src/companion.ts +47 -12
- package/opencode-plugin/src/server.ts +39 -4
- package/package.json +3 -3
- package/src/cli/commands/artifact/finish/openspec.ts +1 -1
- package/src/cli/commands/artifact/pending/discovery.ts +60 -21
- package/src/cli/commands/companion/link.ts +16 -11
- package/src/cli/commands/companion/list.ts +4 -5
- package/src/cli/commands/companion/open.ts +4 -5
- package/src/cli/commands/companion/tui.ts +1 -1
- package/src/cli/commands/doctor.ts +1 -1
- package/src/cli/commands/report/renderer.ts +9 -19
- package/src/cli/commands/session/prepare.ts +119 -0
- package/src/cli/commands/shared/companion-selection.ts +2 -2
- package/src/cli/commands/studio/reports.ts +0 -1
- package/src/cli/commands/studio/server.ts +6 -2
- package/src/cli/commands/unwrap.ts +28 -3
- package/src/cli/commands/wrap.ts +53 -59
- package/src/cli/companion-selector.tsx +1 -1
- package/src/cli/main.ts +17 -0
- package/src/cli/usage.ts +3 -1
- package/src/hooks/session-banner.ts +19 -10
- package/src/hooks/session-prepare.ts +101 -0
- package/src/lib/fs-utils.ts +13 -0
- package/src/lib/orchestrator/adapters/base.ts +43 -8
- package/src/lib/orchestrator/adapters/claude.ts +130 -29
- package/src/lib/orchestrator/adapters/opencode.ts +17 -19
- package/src/lib/orchestrator/companion-git-sync.ts +28 -1
- package/src/lib/orchestrator/companion-hub.ts +2 -1
- package/src/lib/orchestrator/config-store.ts +56 -2
- package/src/lib/orchestrator/framework-context.ts +5 -29
- package/src/lib/orchestrator/launcher.ts +36 -171
- package/src/lib/orchestrator/projection-claude-entry.ts +17 -6
- package/src/lib/orchestrator/projection-claude-skills.ts +54 -22
- package/src/lib/orchestrator/projection-entries.ts +78 -11
- package/src/lib/orchestrator/projection-opencode-copies.ts +230 -0
- package/src/lib/orchestrator/projection-record.ts +7 -1
- package/src/lib/orchestrator/projection-runtime-documents.ts +83 -130
- package/src/lib/orchestrator/projection-types.ts +38 -33
- package/src/lib/orchestrator/repository-link.ts +473 -0
- package/src/lib/orchestrator/root-context.ts +6 -2
- package/src/lib/orchestrator/session-prepare.ts +425 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
- package/src/lib/orchestrator/setup-preflight.ts +3 -1
- package/src/lib/orchestrator/types.ts +4 -1
- package/src/lib/orchestrator/working-repo-projection.ts +71 -159
- package/src/lib/orchestrator/workspace-inventory.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +131 -0
- package/src/lib/runtime-directory-refresh.ts +186 -0
- package/src/lib/update-checker.ts +6 -0
- package/src/opencode/companion-hooks.ts +24 -132
- package/src/opencode/companion-policy.ts +26 -10
- package/src/opencode/index.ts +1 -7
- package/src/opencode/session-notes.ts +76 -0
- package/src/opencode/session-prepare.ts +139 -0
- package/src/opencode/tui.tsx +56 -9
- package/src/runtime/claude-delivery-plan.ts +73 -0
- package/src/runtime/claude-hook-group.ts +21 -0
- package/src/runtime/companion-lock.ts +121 -0
- package/src/runtime/companion-sync.ts +66 -9
- package/src/runtime/runtime-directory.ts +43 -0
- package/src/runtime/runtime-document-manifest.ts +335 -0
- package/src/runtime/session-prepare-runner.ts +148 -0
- package/src/runtime/wrap-state.ts +90 -0
- package/src/runtime/write-atomic.ts +27 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +2 -1
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +2 -1
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +5 -0
- package/src/templates/capabilities/react-doctor/mate-react-doctor/SKILL.md +84 -0
- package/src/templates/capabilities/react-doctor/{skill → mate-react-doctor}/references/explain.md +18 -15
- package/src/templates/mate-skills/agents/mate-openspec-backfill/SKILL.md +45 -29
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +1 -1
- package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +45 -29
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +1 -1
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +850 -499
- package/src/tools/setup/capabilities/react-doctor.ts +34 -50
- package/src/tools/setup/dynamic-plugins/hydrate.ts +1 -1
- package/src/tools/setup/engine.ts +15 -4
- package/src/tools/setup/mate.ts +1 -1
- package/src/tools/setup/providers/claude-format.ts +11 -18
- package/src/tools/setup/providers/claude-plugin-hooks.ts +61 -24
- package/src/tools/setup/providers/claude.ts +23 -6
- package/src/tools/setup/runtime-documents.ts +152 -32
- package/src/tools/setup/working-repo-local-state.ts +82 -13
- package/src/tools/setup.ts +2 -2
- package/src/lib/orchestrator/companion-registry-reader.ts +0 -37
- package/src/lib/orchestrator/companion-resolver.ts +0 -179
- package/src/lib/orchestrator/companion-store.ts +0 -94
- package/src/lib/orchestrator/repo-local-registry.ts +0 -143
- package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +0 -144
- package/src/templates/capabilities/react-doctor/skill/SKILL.md +0 -51
- package/src/templates/report-assets/README.md +0 -32
- package/src/templates/report-assets/mermaid.LICENSE +0 -21
- package/src/templates/report-assets/mermaid.min.js +0 -4376
|
@@ -1,88 +1,72 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
import { getReactDoctorBinPath } from "../../../lib/package-paths";
|
|
4
5
|
import type { CapabilityPlugin, RuntimeContributionsByRuntime, SetupContext } from "../plugin";
|
|
5
|
-
import { removeLegacyOpenCodeSkills } from "../providers/agents-skill-root";
|
|
6
|
+
import { removeLegacyOpenCodeSkills, removeSharedSkills } from "../providers/agents-skill-root";
|
|
6
7
|
import { pruneEmptyAncestors } from "../utils";
|
|
7
8
|
|
|
8
|
-
const SKILL_NAME = "react-doctor";
|
|
9
|
+
const SKILL_NAME = "mate-react-doctor";
|
|
10
|
+
// The upstream skill earlier releases installed under its own name.
|
|
11
|
+
const LEGACY_SKILL_NAME = "react-doctor";
|
|
9
12
|
|
|
10
13
|
const SKILL_SRC = path.join(
|
|
11
14
|
import.meta.dirname,
|
|
12
|
-
"../../../templates/capabilities/react-doctor/
|
|
13
|
-
);
|
|
14
|
-
export const CLAUDE_HOOK_SRC = path.join(
|
|
15
|
-
import.meta.dirname,
|
|
16
|
-
"../../../templates/capabilities/react-doctor/claude/hooks/react-doctor.sh",
|
|
15
|
+
"../../../templates/capabilities/react-doctor/mate-react-doctor",
|
|
17
16
|
);
|
|
18
17
|
|
|
18
|
+
/** Removes the skill copies earlier releases installed under the upstream name. */
|
|
19
|
+
async function removeLegacySkills(companionPath: string): Promise<void> {
|
|
20
|
+
const claudeSkillsDir = path.join(companionPath, ".claude", "skills");
|
|
21
|
+
await fs.rm(path.join(claudeSkillsDir, LEGACY_SKILL_NAME), { recursive: true, force: true });
|
|
22
|
+
await pruneEmptyAncestors(claudeSkillsDir, companionPath);
|
|
23
|
+
await removeSharedSkills(companionPath, [LEGACY_SKILL_NAME]);
|
|
24
|
+
await removeLegacyOpenCodeSkills(companionPath, [LEGACY_SKILL_NAME]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Removes the post-edit hook script earlier releases copied into the companion. */
|
|
28
|
+
async function removeLegacyHookScript(companionPath: string): Promise<void> {
|
|
29
|
+
const hooksDir = path.join(companionPath, ".claude", "hooks");
|
|
30
|
+
await fs.rm(path.join(hooksDir, "react-doctor.sh"), { force: true });
|
|
31
|
+
await pruneEmptyAncestors(hooksDir, companionPath);
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
export function createReactDoctorPlugin(): CapabilityPlugin {
|
|
20
35
|
return {
|
|
21
36
|
id: "react-doctor",
|
|
22
37
|
kind: "capability",
|
|
23
38
|
label: "React Doctor",
|
|
24
|
-
description: "Install
|
|
39
|
+
description: "Install the mate-react-doctor skill that verifies React changes during apply.",
|
|
25
40
|
defaultSelected: true,
|
|
26
41
|
isEnabled: (config) => (config.capabilities ?? []).some((c) => c.name === "react-doctor"),
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
getRuntimeContributions(ctx: SetupContext): RuntimeContributionsByRuntime {
|
|
32
|
-
const hookCommand = `sh "${ctx.companionPath}/.claude/hooks/react-doctor.sh"`;
|
|
42
|
+
// React Doctor runs only when an agent invokes the skill (the shared Mate
|
|
43
|
+
// verification rule adds it to OpenSpec task lists), so the capability
|
|
44
|
+
// contributes no hooks: just the skill and its permission pre-seeds.
|
|
45
|
+
getRuntimeContributions(): RuntimeContributionsByRuntime {
|
|
33
46
|
const skillTrees = [{ name: SKILL_NAME, sourceDir: SKILL_SRC }];
|
|
34
47
|
return {
|
|
35
48
|
claude: {
|
|
36
|
-
|
|
37
|
-
// Record edits cheaply, then scan once when the edited turn finishes.
|
|
38
|
-
{
|
|
39
|
-
event: "PostToolUse",
|
|
40
|
-
marker: "react-doctor.sh",
|
|
41
|
-
group: {
|
|
42
|
-
matcher: "Write|Edit|MultiEdit|NotebookEdit|ApplyPatch",
|
|
43
|
-
hooks: [{ type: "command", command: hookCommand, timeout: 5 }],
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
event: "Stop",
|
|
48
|
-
marker: "react-doctor.sh",
|
|
49
|
-
group: { hooks: [{ type: "command", command: hookCommand, timeout: 45 }] },
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
permissionEntries: [
|
|
53
|
-
"Skill(react-doctor)",
|
|
54
|
-
"Bash(npx react-doctor:*)",
|
|
55
|
-
"Bash(npx react-doctor@latest *)",
|
|
56
|
-
],
|
|
49
|
+
permissionEntries: [`Skill(${SKILL_NAME})`, `Bash(${getReactDoctorBinPath()}:*)`],
|
|
57
50
|
skillTrees,
|
|
58
51
|
},
|
|
59
52
|
opencode: { skillTrees },
|
|
60
53
|
};
|
|
61
54
|
},
|
|
62
55
|
async apply(ctx: SetupContext) {
|
|
63
|
-
await
|
|
56
|
+
await removeLegacySkills(ctx.companionPath);
|
|
64
57
|
},
|
|
65
58
|
async teardown(ctx: SetupContext) {
|
|
66
|
-
await
|
|
59
|
+
await removeLegacySkills(ctx.companionPath);
|
|
67
60
|
},
|
|
61
|
+
// Cleanup only: stale hook groups are stripped by settings-sync through the
|
|
62
|
+
// `react-doctor.sh` managed marker; the script file itself is removed here.
|
|
68
63
|
forProvider: {
|
|
69
64
|
claude: {
|
|
70
65
|
async apply(ctx: SetupContext) {
|
|
71
|
-
|
|
72
|
-
await fs.mkdir(path.dirname(hookDest), { recursive: true });
|
|
73
|
-
await fs.copyFile(CLAUDE_HOOK_SRC, hookDest);
|
|
74
|
-
await fs.chmod(hookDest, 0o755);
|
|
66
|
+
await removeLegacyHookScript(ctx.companionPath);
|
|
75
67
|
},
|
|
76
68
|
async teardown(ctx: SetupContext) {
|
|
77
|
-
|
|
78
|
-
await fs.unlink(path.join(ctx.companionPath, ".claude", "hooks", "react-doctor.sh"));
|
|
79
|
-
} catch {
|
|
80
|
-
/* not present */
|
|
81
|
-
}
|
|
82
|
-
await pruneEmptyAncestors(
|
|
83
|
-
path.join(ctx.companionPath, ".claude", "hooks"),
|
|
84
|
-
ctx.companionPath,
|
|
85
|
-
);
|
|
69
|
+
await removeLegacyHookScript(ctx.companionPath);
|
|
86
70
|
},
|
|
87
71
|
},
|
|
88
72
|
},
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { getActiveDistribution } from "../../../distribution";
|
|
5
5
|
import { FRAMEWORK_NAME } from "../../../framework";
|
|
6
|
-
import { CompanionResolver } from "../../../lib/orchestrator/
|
|
6
|
+
import { CompanionResolver } from "../../../lib/orchestrator/repository-link";
|
|
7
7
|
import { GlobalConfigStore } from "../../../lib/orchestrator/global-config-store";
|
|
8
8
|
import type { Plugin } from "../plugin";
|
|
9
9
|
import type { PluginRegistry } from "../registry";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// oxlint-disable no-await-in-loop
|
|
2
2
|
import { getActiveDistribution } from "../../distribution";
|
|
3
3
|
import { FRAMEWORK_NAME } from "../../framework";
|
|
4
|
-
import type { RenderedRuntimeDocument } from "../../lib/orchestrator/projection-types";
|
|
5
4
|
import type { FrameworkConfig } from "../../lib/orchestrator/types";
|
|
5
|
+
import type { RenderedRuntimeDocument } from "../../runtime/runtime-document-manifest";
|
|
6
6
|
import { collectHostingProviders, ContextServiceMediator } from "./context-services";
|
|
7
7
|
import type {
|
|
8
8
|
CapabilityContributionInput,
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
} from "./plugin";
|
|
14
14
|
import { reconcileClaudeContributions } from "./providers/claude";
|
|
15
15
|
import { reconcileOpenCodeContributions } from "./providers/opencode";
|
|
16
|
-
import { renderDocumentsForTarget } from "./runtime-documents";
|
|
16
|
+
import { mergeCompanionOpenCodeConfig, renderDocumentsForTarget } from "./runtime-documents";
|
|
17
17
|
import { normalizeRegistration, type NormalizedRegistration } from "./registry";
|
|
18
18
|
|
|
19
19
|
export interface SetupInstallationPlanAction {
|
|
@@ -252,6 +252,12 @@ async function reconcileCapabilityContributions(
|
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/** What a render returns: its warnings are data for the caller, never printed here. */
|
|
256
|
+
export interface RenderedRuntimeDocuments {
|
|
257
|
+
documents: RenderedRuntimeDocument[];
|
|
258
|
+
warnings: string[];
|
|
259
|
+
}
|
|
260
|
+
|
|
255
261
|
/**
|
|
256
262
|
* The same declarations, rendered rather than written: a working-target pass
|
|
257
263
|
* produces the documents and the Projection Root places them, so no Runtime
|
|
@@ -261,7 +267,12 @@ export async function renderRuntimeDocuments(
|
|
|
261
267
|
ctx: SetupContext,
|
|
262
268
|
registrations: PluginRegistration[],
|
|
263
269
|
plan: SetupInstallationPlan,
|
|
264
|
-
): Promise<
|
|
270
|
+
): Promise<RenderedRuntimeDocuments> {
|
|
265
271
|
const plugins = registrations.map((entry) => normalizeRegistration(entry).plugin);
|
|
266
|
-
|
|
272
|
+
const documents = renderDocumentsForTarget(
|
|
273
|
+
ctx,
|
|
274
|
+
await collectContributionInputs(ctx, plugins, plan),
|
|
275
|
+
);
|
|
276
|
+
if (ctx.target !== "working") return { documents, warnings: [] };
|
|
277
|
+
return mergeCompanionOpenCodeConfig(ctx.companionPath, documents);
|
|
267
278
|
}
|
package/src/tools/setup/mate.ts
CHANGED
|
@@ -81,7 +81,7 @@ A \`diff\` section carries a unified diff patch as produced by \`git diff\`. The
|
|
|
81
81
|
{ "id": "changes", "title": "Changes", "type": "diff", "patch": "diff --git a/src/acme.ts b/src/acme.ts\\n..." }
|
|
82
82
|
\`\`\`
|
|
83
83
|
|
|
84
|
-
A report carrying a mermaid payload
|
|
84
|
+
A report carrying a mermaid payload loads the pinned, integrity-checked diagram runtime from a CDN; a report without one does not. Offline, each diagram stays readable as its source. Otherwise the page stays a single self-contained file.
|
|
85
85
|
|
|
86
86
|
## Invocation
|
|
87
87
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
import { hookGroupKey, type ClaudeHookGroup } from "../../../runtime/claude-hook-group";
|
|
5
|
+
|
|
4
6
|
// Structurally matches `McpServerDescriptor` from ../plugin; declared here so
|
|
5
7
|
// the format module stays dependency-free of the plugin contract (which
|
|
6
8
|
// imports hook types from this module).
|
|
@@ -17,17 +19,11 @@ export interface McpEntryDescriptor {
|
|
|
17
19
|
// groups are Mate-managed lives here too, because the companion-side reconcile
|
|
18
20
|
// and the working-repo projection entry must strip the same set.
|
|
19
21
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ClaudeHookGroup {
|
|
28
|
-
matcher?: string;
|
|
29
|
-
hooks?: ClaudeHookCommand[];
|
|
30
|
-
}
|
|
22
|
+
export {
|
|
23
|
+
hookGroupKey,
|
|
24
|
+
type ClaudeHookCommand,
|
|
25
|
+
type ClaudeHookGroup,
|
|
26
|
+
} from "../../../runtime/claude-hook-group";
|
|
31
27
|
|
|
32
28
|
export type ClaudeHookMap = Record<string, ClaudeHookGroup[]>;
|
|
33
29
|
|
|
@@ -43,10 +39,6 @@ export interface ClaudeMcpConfig {
|
|
|
43
39
|
[key: string]: unknown;
|
|
44
40
|
}
|
|
45
41
|
|
|
46
|
-
function hookGroupKey(group: ClaudeHookGroup): string {
|
|
47
|
-
return JSON.stringify(group);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
42
|
/** Append incoming hook groups per event, skipping structurally identical ones. */
|
|
51
43
|
export function mergeClaudeHookGroups(
|
|
52
44
|
existingHooks: ClaudeHookMap,
|
|
@@ -84,9 +76,10 @@ export function filterClaudeHookGroups(
|
|
|
84
76
|
|
|
85
77
|
// Command substrings that mark a hook group as Mate-managed. The mate plugin
|
|
86
78
|
// hooks (validate-artifact-path, mate-session-banner) now ship in the bundled
|
|
87
|
-
// Claude plugin, and the retired mate-artifact-finish.sh archive hook
|
|
88
|
-
// entirely; their markers are retained
|
|
89
|
-
// written by earlier releases keep being
|
|
79
|
+
// Claude plugin, and the retired mate-artifact-finish.sh archive hook and
|
|
80
|
+
// react-doctor.sh post-edit scan are gone entirely; their markers are retained
|
|
81
|
+
// migration-only so stale managed groups written by earlier releases keep being
|
|
82
|
+
// stripped, and are never re-added.
|
|
90
83
|
const MANAGED_HOOK_MARKERS = [
|
|
91
84
|
"validate-artifact-path",
|
|
92
85
|
"mate-session-banner",
|
|
@@ -3,7 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { PROJECTED_BANNER_FLAG } from "../../../hooks/session-banner";
|
|
5
5
|
import { getClaudePluginRoot } from "../../../lib/package-paths";
|
|
6
|
-
import
|
|
6
|
+
import { runtimeClaudePluginRoot } from "../../../runtime/runtime-directory";
|
|
7
|
+
import { hookGroupKey, type ClaudeHookGroup } from "./claude-format";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* The bundled Claude plugin's hook wiring, read as a value. A managed launch
|
|
@@ -17,11 +18,16 @@ import type { ClaudeHookGroup } from "./claude-format";
|
|
|
17
18
|
* environment is present and so cannot double the prompt the launch already
|
|
18
19
|
* appended; and the banner, whose carried copy is marked so it defers to the
|
|
19
20
|
* launch's.
|
|
21
|
+
*
|
|
22
|
+
* The wiring is read from the installed plugin, but every command names
|
|
23
|
+
* `<mate home>/runtime/claude-plugin`, so a reinstall or Node switch does not
|
|
24
|
+
* strand a wrapped repository.
|
|
20
25
|
*/
|
|
21
26
|
|
|
22
27
|
const GUARD_HOOK_SHIM = "validate-artifact-path.mjs";
|
|
23
28
|
const GUIDANCE_HOOK_SHIM = "session-guidance.mjs";
|
|
24
29
|
const BANNER_HOOK_SHIM = "session-banner.mjs";
|
|
30
|
+
const PREPARE_HOOK_SHIM = "session-prepare.mjs";
|
|
25
31
|
const PLUGIN_ROOT_TOKEN = "${CLAUDE_PLUGIN_ROOT}";
|
|
26
32
|
|
|
27
33
|
interface PluginHookWiring {
|
|
@@ -50,26 +56,24 @@ function resolveCommands(group: ClaudeHookGroup, pluginRoot: string): ClaudeHook
|
|
|
50
56
|
};
|
|
51
57
|
}
|
|
52
58
|
|
|
53
|
-
function runsShim(shim: string):
|
|
54
|
-
return (group
|
|
59
|
+
function runsShim(group: ClaudeHookGroup, shim: string): boolean {
|
|
60
|
+
return (group.hooks ?? []).some((hook) => (hook.command ?? "").includes(shim));
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
function groupsFor(
|
|
58
|
-
event
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
): ClaudeHookGroup[] {
|
|
62
|
-
return (readPluginHookWiring(pluginRoot).hooks?.[event] ?? [])
|
|
63
|
-
.filter(runsShim(shim))
|
|
64
|
-
.map((group) => resolveCommands(group, pluginRoot));
|
|
63
|
+
function groupsFor(event: string, shim: string, commandRoot: string): ClaudeHookGroup[] {
|
|
64
|
+
return (readPluginHookWiring(getClaudePluginRoot()).hooks?.[event] ?? []).flatMap((group) =>
|
|
65
|
+
runsShim(group, shim) ? [resolveCommands(group, commandRoot)] : [],
|
|
66
|
+
);
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/**
|
|
68
70
|
* The guard's `PreToolUse` groups with `${CLAUDE_PLUGIN_ROOT}` resolved against
|
|
69
|
-
* the
|
|
71
|
+
* the runtime link, so the command works with no launch environment.
|
|
70
72
|
*/
|
|
71
|
-
export function mateGuardHookGroups(
|
|
72
|
-
|
|
73
|
+
export function mateGuardHookGroups(
|
|
74
|
+
commandRoot: string = runtimeClaudePluginRoot(),
|
|
75
|
+
): ClaudeHookGroup[] {
|
|
76
|
+
return groupsFor("PreToolUse", GUARD_HOOK_SHIM, commandRoot);
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
/**
|
|
@@ -79,9 +83,9 @@ export function mateGuardHookGroups(pluginRoot: string = getClaudePluginRoot()):
|
|
|
79
83
|
* flag.
|
|
80
84
|
*/
|
|
81
85
|
export function mateGuidanceHookGroups(
|
|
82
|
-
|
|
86
|
+
commandRoot: string = runtimeClaudePluginRoot(),
|
|
83
87
|
): ClaudeHookGroup[] {
|
|
84
|
-
return groupsFor("SessionStart", GUIDANCE_HOOK_SHIM,
|
|
88
|
+
return groupsFor("SessionStart", GUIDANCE_HOOK_SHIM, commandRoot);
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
/**
|
|
@@ -91,9 +95,9 @@ export function mateGuidanceHookGroups(
|
|
|
91
95
|
* would print the banner twice.
|
|
92
96
|
*/
|
|
93
97
|
export function mateBannerHookGroups(
|
|
94
|
-
|
|
98
|
+
commandRoot: string = runtimeClaudePluginRoot(),
|
|
95
99
|
): ClaudeHookGroup[] {
|
|
96
|
-
return groupsFor("SessionStart", BANNER_HOOK_SHIM,
|
|
100
|
+
return groupsFor("SessionStart", BANNER_HOOK_SHIM, commandRoot).map((group) => ({
|
|
97
101
|
...group,
|
|
98
102
|
hooks: (group.hooks ?? []).map((hook) => ({
|
|
99
103
|
...hook,
|
|
@@ -102,16 +106,49 @@ export function mateBannerHookGroups(
|
|
|
102
106
|
}));
|
|
103
107
|
}
|
|
104
108
|
|
|
109
|
+
/**
|
|
110
|
+
* The session-prepare `SessionStart` group, resolved the same way. Its declared
|
|
111
|
+
* `timeout` sits above the shim's own bound, so Claude Code never kills a
|
|
112
|
+
* prepare the shim has not given up on. Inert under a launch environment.
|
|
113
|
+
*/
|
|
114
|
+
export function mateSessionPrepareHookGroups(
|
|
115
|
+
commandRoot: string = runtimeClaudePluginRoot(),
|
|
116
|
+
): ClaudeHookGroup[] {
|
|
117
|
+
return groupsFor("SessionStart", PREPARE_HOOK_SHIM, commandRoot);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function mateOwnHookGroups(commandRoot: string): ClaudeHookGroup[] {
|
|
121
|
+
return [
|
|
122
|
+
...mateGuardHookGroups(commandRoot),
|
|
123
|
+
...mateGuidanceHookGroups(commandRoot),
|
|
124
|
+
...mateBannerHookGroups(commandRoot),
|
|
125
|
+
...mateSessionPrepareHookGroups(commandRoot),
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const ownGroupKeys = new Map<string, ReadonlySet<string>>();
|
|
130
|
+
|
|
131
|
+
/** Memoized per plugin-root pair; hooks are short-lived, so a refresh mid-process is not tracked. */
|
|
132
|
+
function mateOwnHookGroupKeys(runtimeRoot: string, installRoot: string): ReadonlySet<string> {
|
|
133
|
+
const memoKey = `${runtimeRoot}\0${installRoot}`;
|
|
134
|
+
let keys = ownGroupKeys.get(memoKey);
|
|
135
|
+
if (!keys) {
|
|
136
|
+
keys = new Set(
|
|
137
|
+
[...mateOwnHookGroups(runtimeRoot), ...mateOwnHookGroups(installRoot)].map(hookGroupKey),
|
|
138
|
+
);
|
|
139
|
+
ownGroupKeys.set(memoKey, keys);
|
|
140
|
+
}
|
|
141
|
+
return keys;
|
|
142
|
+
}
|
|
143
|
+
|
|
105
144
|
/**
|
|
106
145
|
* Whether a hook group is one the working target placed. It matches a managed
|
|
107
146
|
* marker, so the entry that strips managed groups from a Working Repository has
|
|
108
|
-
* to be told to leave these alone.
|
|
147
|
+
* to be told to leave these alone. Both the runtime-link form and the
|
|
148
|
+
* install-path form an older release wrote are recognized.
|
|
109
149
|
*/
|
|
110
150
|
export function isMateGuardHookGroup(group: ClaudeHookGroup): boolean {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
(candidate) => JSON.stringify(candidate),
|
|
114
|
-
),
|
|
151
|
+
return mateOwnHookGroupKeys(runtimeClaudePluginRoot(), getClaudePluginRoot()).has(
|
|
152
|
+
hookGroupKey(group),
|
|
115
153
|
);
|
|
116
|
-
return own.has(JSON.stringify(group));
|
|
117
154
|
}
|
|
@@ -89,6 +89,10 @@ function getBaseManagedPermissionEntries(companionPath: string): string[] {
|
|
|
89
89
|
const LEGACY_MANAGED_PERMISSION_ENTRIES = [
|
|
90
90
|
"Bash($MATE_COMPANION_BIN_PATH/openspec:*)",
|
|
91
91
|
"Bash($MATE_COMPANION_BIN_PATH/graphify:*)",
|
|
92
|
+
// React Doctor's pre-seeds before it moved to the pinned mate-react-doctor skill.
|
|
93
|
+
"Skill(react-doctor)",
|
|
94
|
+
"Bash(npx react-doctor:*)",
|
|
95
|
+
"Bash(npx react-doctor@latest *)",
|
|
92
96
|
];
|
|
93
97
|
|
|
94
98
|
function getAllManagedPermissionEntries(companionPath: string): Set<string> {
|
|
@@ -102,16 +106,12 @@ function getAllManagedPermissionEntries(companionPath: string): Set<string> {
|
|
|
102
106
|
]);
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
// single source of truth for Mate-managed Claude hooks, permissions, and MCP
|
|
107
|
-
// servers; it is loaded at launch via `claude --settings`.
|
|
109
|
+
/** Companion-owned Claude settings: sole source of Mate-managed hooks, permissions and MCP servers. */
|
|
108
110
|
export function getCompanionClaudeSettingsPath(companionPath: string): string {
|
|
109
111
|
return path.join(companionPath, ".claude", "settings.local.json");
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
// MCP servers here and loads it at launch via `claude --mcp-config`, keeping
|
|
114
|
-
// the MCP surface out of the working repo and out of settings.local.json.
|
|
114
|
+
/** Companion-owned `.mcp.json`; loaded via `--mcp-config` only by a companion-scoped launch. */
|
|
115
115
|
export function getCompanionClaudeMcpConfigPath(companionPath: string): string {
|
|
116
116
|
return path.join(companionPath, ".mcp.json");
|
|
117
117
|
}
|
|
@@ -147,6 +147,23 @@ export function renderManagedClaudeMcpServers(
|
|
|
147
147
|
return servers;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Enabled Capabilities' Claude hook groups per event, in declared order, as a
|
|
152
|
+
* value: what a Claude wrap carries into the Working Repository.
|
|
153
|
+
*/
|
|
154
|
+
export function renderManagedClaudeHookGroups(
|
|
155
|
+
contributions: CapabilityContributionInput[] = [],
|
|
156
|
+
): Record<string, ClaudeHookGroup[]> {
|
|
157
|
+
const hooks: Record<string, ClaudeHookGroup[]> = {};
|
|
158
|
+
for (const input of contributions) {
|
|
159
|
+
if (!input.enabled) continue;
|
|
160
|
+
for (const hook of input.contributions.hookGroups ?? []) {
|
|
161
|
+
(hooks[hook.event] ??= []).push(hook.group);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return hooks;
|
|
165
|
+
}
|
|
166
|
+
|
|
150
167
|
// Reconcile the Mate-managed Claude settings shape (hooks, permissions, MCP
|
|
151
168
|
// servers) on top of whatever already exists in the settings document. Managed
|
|
152
169
|
// groups/entries always lead their arrays so the emitted shape stays stable
|