@uniqbit/mate-core 0.18.5 → 1.0.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-plugin/hooks/companion-instructions.mjs +9 -0
- package/claude-plugin/hooks/hooks.json +12 -2
- package/package.json +10 -2
- package/src/brand/art.generated.ts +69 -0
- package/src/brand/index.ts +12 -0
- package/src/brand/palette.ts +19 -0
- package/src/brand/svg.generated.ts +8 -0
- package/src/brand/terminal.ts +83 -0
- package/src/brand/types.ts +33 -0
- package/src/cli/commands/artifact/artifact.ts +8 -0
- package/src/cli/commands/artifact/delivery/archive.ts +117 -0
- package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
- package/src/cli/commands/artifact/delivery/environment.ts +104 -0
- package/src/cli/commands/artifact/delivery/link.ts +154 -0
- package/src/cli/commands/artifact/delivery/output.ts +58 -0
- package/src/cli/commands/artifact/finish/command.ts +33 -3
- package/src/cli/commands/artifact/finish/engine.ts +47 -3
- package/src/cli/commands/artifact/finish/finisher.ts +10 -0
- package/src/cli/commands/artifact/finish/openspec.ts +177 -138
- package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
- package/src/cli/commands/artifact/pending/command.ts +31 -3
- package/src/cli/commands/artifact/pending/discovery.ts +21 -57
- package/src/cli/commands/cap/graphify.ts +52 -3
- package/src/cli/commands/cap/index-cmd.ts +8 -1
- package/src/cli/commands/cap/openspec.ts +13 -25
- package/src/cli/commands/companion/open.ts +56 -39
- package/src/cli/commands/companion/update.ts +75 -42
- package/src/cli/commands/doctor.ts +176 -3
- package/src/cli/commands/plugin/install.ts +9 -4
- package/src/cli/commands/plugin/plugin.ts +3 -2
- package/src/cli/commands/report/adapter.ts +11 -2
- package/src/cli/commands/report/contract.ts +21 -0
- package/src/cli/commands/report/highlight.ts +34 -4
- package/src/cli/commands/report/markdown.ts +110 -0
- package/src/cli/commands/report/palette.ts +118 -0
- package/src/cli/commands/report/renderer.ts +456 -153
- package/src/cli/commands/report/types.ts +16 -0
- package/src/cli/commands/setup.ts +7 -15
- package/src/cli/commands/studio/client-asset.ts +16 -0
- package/src/cli/commands/studio/openspec-cli.ts +10 -58
- package/src/cli/commands/studio/routes.ts +2 -0
- package/src/cli/commands/studio/selection.ts +19 -1
- package/src/cli/commands/studio/server.ts +42 -1
- package/src/cli/commands/studio/views/client.ts +6 -616
- package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
- package/src/cli/commands/studio/views/document.tsx +137 -69
- package/src/cli/commands/studio/views/markdown.tsx +250 -0
- package/src/cli/commands/studio/views/palette.ts +134 -0
- package/src/cli/commands/studio/views/reports/index.tsx +2 -7
- package/src/cli/commands/studio/views/skills/index.tsx +43 -62
- package/src/cli/commands/studio/views/specs/index.tsx +7 -15
- package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
- package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
- package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
- package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
- package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
- package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
- package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
- package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
- package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
- package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
- package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
- package/src/cli/commands/studio/views/styles.ts +217 -206
- package/src/cli/commands/studio/views/vault/state.ts +3 -5
- package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
- package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
- package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
- package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
- package/src/cli/commands/wrap.ts +5 -13
- package/src/cli/main.ts +4 -1
- package/src/cli/parse-flags.ts +25 -7
- package/src/cli/setup-selector.tsx +38 -162
- package/src/cli/usage.ts +4 -0
- package/src/hooks/companion-instructions.ts +190 -0
- package/src/hooks/hook-io.ts +63 -0
- package/src/hooks/session-banner.ts +21 -29
- package/src/hooks/session-guidance.ts +9 -17
- package/src/hooks/session-prepare.ts +115 -28
- package/src/hooks/validate-artifact-path.ts +82 -24
- package/src/index.ts +4 -0
- package/src/lib/change-layout.ts +154 -0
- package/src/lib/components/wizard-header.tsx +3 -1
- package/src/lib/context-mode-package.ts +24 -11
- package/src/lib/delivery/archive-change.ts +154 -0
- package/src/lib/delivery/branches.ts +108 -0
- package/src/lib/delivery/environment.ts +29 -0
- package/src/lib/delivery/forge.ts +279 -0
- package/src/lib/delivery/overlap.ts +202 -0
- package/src/lib/delivery/record-delivery.ts +196 -0
- package/src/lib/delivery/record.ts +343 -0
- package/src/lib/delivery/refresh.ts +174 -0
- package/src/lib/delivery/repository-access.ts +97 -0
- package/src/lib/delivery/stages.ts +111 -0
- package/src/lib/delivery/token.ts +58 -0
- package/src/lib/frontmatter.ts +18 -0
- package/src/lib/opencode-plugin-package.ts +5 -5
- package/src/lib/openspec-cli.ts +116 -0
- package/src/lib/openspec-scopes.ts +86 -0
- package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
- package/src/lib/orchestrator/adapters/claude.ts +48 -40
- package/src/lib/orchestrator/adapters/opencode.ts +2 -0
- package/src/lib/orchestrator/editor.ts +9 -9
- package/src/lib/orchestrator/openspec-store.ts +14 -54
- package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
- package/src/lib/orchestrator/projection-claude-links.ts +159 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +19 -137
- package/src/lib/orchestrator/projection-entries.ts +45 -14
- package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
- package/src/lib/orchestrator/projection-types.ts +3 -0
- package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
- package/src/lib/orchestrator/session-prepare.ts +45 -5
- package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
- package/src/lib/orchestrator/types.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +22 -1
- package/src/lib/package-paths.ts +8 -0
- package/src/opencode/companion-hooks.ts +64 -5
- package/src/opencode/companion-policy.ts +37 -18
- package/src/opencode/projected-guidance.ts +4 -1
- package/src/opencode/session-prepare.ts +2 -1
- package/src/opencode/tui.tsx +4 -2
- package/src/runtime/artifact-refusal.ts +63 -0
- package/src/runtime/claude-delivery-plan.ts +89 -7
- package/src/runtime/commit-attribution-guard.ts +181 -0
- package/src/runtime/companion-guidance.ts +15 -3
- package/src/runtime/companion-instructions.ts +340 -0
- package/src/runtime/error-message.ts +4 -0
- package/src/runtime/index.ts +5 -0
- package/src/runtime/managed-block.ts +117 -0
- package/src/runtime/markdown-scan.ts +266 -0
- package/src/runtime/policy.ts +15 -1
- package/src/runtime/projected-flag.ts +7 -0
- package/src/runtime/projected-guidance.ts +1 -0
- package/src/runtime/runtime-document-manifest.ts +62 -11
- package/src/runtime/secret-guard.ts +148 -0
- package/src/runtime/secret-patterns.ts +164 -0
- package/src/runtime/secret-scan.ts +90 -0
- package/src/runtime/session-kind.ts +13 -0
- package/src/runtime/shell-heredoc.ts +81 -0
- package/src/runtime/wrap-state.ts +36 -2
- package/src/runtime/write-atomic.ts +64 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
- package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
- package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/{claude/mate-artifact-publish → agents/mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/{agents/mate-artifact-publish → claude/mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/openspec-references.ts +140 -0
- package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
- package/src/tools/setup/capabilities/context-mode.ts +25 -5
- package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
- package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
- package/src/tools/setup/capabilities/graphify.ts +12 -0
- package/src/tools/setup/capabilities/openspec.ts +151 -270
- package/src/tools/setup/capabilities/tokensave.ts +36 -3
- package/src/tools/setup/context-services.ts +9 -17
- package/src/tools/setup/dynamic-plugins/install.ts +24 -28
- package/src/tools/setup/engine.ts +97 -21
- package/src/tools/setup/managed-output.ts +51 -0
- package/src/tools/setup/mate.ts +86 -11
- package/src/tools/setup/plugin.ts +35 -0
- package/src/tools/setup/plugins/guidance.ts +7 -4
- package/src/tools/setup/providers/agent-file-sections.ts +4 -2
- package/src/tools/setup/providers/claude-attribution.ts +65 -0
- package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
- package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
- package/src/tools/setup/providers/claude.ts +284 -8
- package/src/tools/setup/providers/opencode.ts +180 -1
- package/src/tools/setup/utils.ts +14 -4
- package/src/tools/setup.ts +126 -10
- package/wrappers/bin/graphify +52 -4
- package/wrappers/bin/openspec +33 -9
- package/wrappers/lib/graphify-gate.mjs +8 -0
- package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
- package/src/tools/setup/runtime-documents.ts +0 -291
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Stdin and stdout of a Claude hook shim. */
|
|
2
|
+
|
|
3
|
+
export type HookEnv = Record<string, string | undefined>;
|
|
4
|
+
|
|
5
|
+
/** One run of a hook shim: its environment, directory, shim arguments and stdin payload. */
|
|
6
|
+
export interface HookCall {
|
|
7
|
+
env: HookEnv;
|
|
8
|
+
cwd: string;
|
|
9
|
+
argv: readonly string[];
|
|
10
|
+
input: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface HookOutcome {
|
|
14
|
+
exitCode: number;
|
|
15
|
+
stdout: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A hook's JSON output line, or nothing. Always exit zero: a hook must never block the session. */
|
|
19
|
+
export function hookOutcome(output: object | null): HookOutcome {
|
|
20
|
+
return { exitCode: 0, stdout: output ? `${JSON.stringify(output)}\n` : "" };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function readStdin(): Promise<string> {
|
|
24
|
+
if (process.stdin.isTTY) return "";
|
|
25
|
+
const chunks: Buffer[] = [];
|
|
26
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
27
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The hook payload on stdin; input that is not JSON reads as `null`, no event. */
|
|
31
|
+
async function readHookInput(): Promise<unknown> {
|
|
32
|
+
const raw = await readStdin();
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(raw) as unknown;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A hook shim's whole run: read the payload, build the outcome, write it.
|
|
42
|
+
* `onError` turns a failure in either step into an outcome, so the shim still exits zero.
|
|
43
|
+
*/
|
|
44
|
+
export async function runHook(
|
|
45
|
+
build: (input: unknown) => HookOutcome | Promise<HookOutcome>,
|
|
46
|
+
onError: (error: unknown) => HookOutcome,
|
|
47
|
+
): Promise<number> {
|
|
48
|
+
let outcome: HookOutcome;
|
|
49
|
+
try {
|
|
50
|
+
outcome = await build(await readHookInput());
|
|
51
|
+
} catch (error) {
|
|
52
|
+
outcome = onError(error);
|
|
53
|
+
}
|
|
54
|
+
writeHookStdout(outcome.stdout);
|
|
55
|
+
return outcome.exitCode;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Swallows stdout errors: a parent that stopped reading would otherwise end the write in an uncaught `EPIPE`. */
|
|
59
|
+
function writeHookStdout(stdout: string): void {
|
|
60
|
+
if (!stdout) return;
|
|
61
|
+
process.stdout.on("error", () => {});
|
|
62
|
+
process.stdout.write(stdout);
|
|
63
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Show the active Mate companion paths in Claude Code, and repair the
|
|
3
|
+
* companion's Git state before the session reads it.
|
|
4
|
+
*/
|
|
5
|
+
import { errorMessage } from "../runtime/error-message";
|
|
3
6
|
import { unmanagedClaudeDelivery } from "../runtime/claude-delivery-plan";
|
|
4
7
|
import {
|
|
5
8
|
syncCompanionUnattended,
|
|
@@ -7,24 +10,11 @@ import {
|
|
|
7
10
|
type UnattendedSyncOptions,
|
|
8
11
|
} from "../runtime/companion-sync";
|
|
9
12
|
import { hasLaunchEnvironment, resolveCompanionRuntime } from "../runtime/env";
|
|
13
|
+
import { FRAMEWORK_NAME } from "../runtime/framework";
|
|
10
14
|
import { projectionFreshness, projectionStalenessLines } from "../runtime/freshness";
|
|
11
15
|
import { mateVersion } from "../runtime/install";
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
export interface BannerOutcome {
|
|
15
|
-
exitCode: number;
|
|
16
|
-
stdout: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Marks the copy a wrap inlines into the Working Repository's settings. A
|
|
21
|
-
* managed launch loads the plugin's own copy *and* that document, and both
|
|
22
|
-
* registrations resolve to the same command against the same environment, so
|
|
23
|
-
* nothing inside the process tells them apart. The flag is what keeps the
|
|
24
|
-
* banner from printing twice in exactly the session that is configured
|
|
25
|
-
* correctly — the projected copy defers, the launch's own copy prints.
|
|
26
|
-
*/
|
|
27
|
-
export const PROJECTED_BANNER_FLAG = "--projected";
|
|
16
|
+
import { PROJECTED_FLAG } from "../runtime/projected-flag";
|
|
17
|
+
import { hookOutcome, runHook, type HookEnv, type HookOutcome } from "./hook-io";
|
|
28
18
|
|
|
29
19
|
/**
|
|
30
20
|
* Resolves in-process from either source — no subprocess in either branch.
|
|
@@ -36,13 +26,11 @@ export async function buildBanner(
|
|
|
36
26
|
cwd: string = process.cwd(),
|
|
37
27
|
argv: readonly string[] = [],
|
|
38
28
|
syncOptions: UnattendedSyncOptions = {},
|
|
39
|
-
): Promise<
|
|
40
|
-
if (argv.includes(
|
|
41
|
-
return { exitCode: 0, stdout: "" };
|
|
42
|
-
}
|
|
29
|
+
): Promise<HookOutcome> {
|
|
30
|
+
if (argv.includes(PROJECTED_FLAG) && hasLaunchEnvironment(env)) return hookOutcome(null);
|
|
43
31
|
|
|
44
32
|
const { context, projection } = resolveCompanionRuntime(env, cwd);
|
|
45
|
-
if (!context.repositoryPath || !context.companionPath) return
|
|
33
|
+
if (!context.repositoryPath || !context.companionPath) return hookOutcome(null);
|
|
46
34
|
|
|
47
35
|
/**
|
|
48
36
|
* Repaired before the session reads the companion, so its first reads are
|
|
@@ -78,12 +66,16 @@ export async function buildBanner(
|
|
|
78
66
|
/** `systemMessage` only: the model must never be told to run the command. */
|
|
79
67
|
for (const note of gitNotes) lines.push(` ${note}`);
|
|
80
68
|
|
|
81
|
-
return
|
|
69
|
+
return hookOutcome({ systemMessage: lines.join("\n") });
|
|
82
70
|
}
|
|
83
71
|
|
|
84
|
-
|
|
85
|
-
export
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
72
|
+
/** Plugin-shim entry; exits zero even when the banner fails. */
|
|
73
|
+
export function run(argv: readonly string[] = process.argv.slice(2)): Promise<number> {
|
|
74
|
+
return runHook(
|
|
75
|
+
() => buildBanner(process.env, process.cwd(), argv),
|
|
76
|
+
(error) =>
|
|
77
|
+
hookOutcome({
|
|
78
|
+
systemMessage: `${FRAMEWORK_NAME}: session banner failed: ${errorMessage(error)}`,
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
89
81
|
}
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
// Deliver the companion guidance to a Claude session Mate did not launch.
|
|
2
2
|
import { buildProjectedGuidance } from "../runtime/projected-guidance";
|
|
3
|
-
import type
|
|
4
|
-
|
|
5
|
-
export interface GuidanceOutcome {
|
|
6
|
-
exitCode: number;
|
|
7
|
-
stdout: string;
|
|
8
|
-
}
|
|
3
|
+
import { hookOutcome, type HookEnv, type HookOutcome } from "./hook-io";
|
|
9
4
|
|
|
10
5
|
/**
|
|
11
6
|
* `SessionStart.additionalContext` is the file-based form of the managed
|
|
@@ -17,19 +12,16 @@ export interface GuidanceOutcome {
|
|
|
17
12
|
* Emits nothing rather than a note when no companion resolves: an unwrapped
|
|
18
13
|
* repository must read exactly as it did before the hook existed.
|
|
19
14
|
*/
|
|
20
|
-
export function buildSessionGuidance(env: HookEnv, cwd: string = process.cwd()):
|
|
15
|
+
export function buildSessionGuidance(env: HookEnv, cwd: string = process.cwd()): HookOutcome {
|
|
21
16
|
const guidance = buildProjectedGuidance(env, cwd);
|
|
22
|
-
if (!guidance) return
|
|
17
|
+
if (!guidance) return hookOutcome(null);
|
|
23
18
|
|
|
24
|
-
return {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
},
|
|
31
|
-
})}\n`,
|
|
32
|
-
};
|
|
19
|
+
return hookOutcome({
|
|
20
|
+
hookSpecificOutput: {
|
|
21
|
+
hookEventName: "SessionStart",
|
|
22
|
+
additionalContext: guidance,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
33
25
|
}
|
|
34
26
|
|
|
35
27
|
// Plugin-shim entry.
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/** Prepares a plain Claude session in a Claude-wrapped repository as `mate claude` would. */
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
|
|
4
|
+
import { errorMessage } from "../runtime/error-message";
|
|
4
5
|
import { unmanagedClaudeDelivery } from "../runtime/claude-delivery-plan";
|
|
5
6
|
import { hasLaunchEnvironment } from "../runtime/env";
|
|
7
|
+
import { FRAMEWORK_NAME } from "../runtime/framework";
|
|
8
|
+
import { PROJECTED_FLAG } from "../runtime/projected-flag";
|
|
6
9
|
import { resolveProjection } from "../runtime/projection";
|
|
7
10
|
import { runtimeDirectoryPath, runtimeMateBin } from "../runtime/runtime-directory";
|
|
8
11
|
import {
|
|
@@ -12,12 +15,8 @@ import {
|
|
|
12
15
|
type PrepareRun,
|
|
13
16
|
type PrepareRunner,
|
|
14
17
|
} from "../runtime/session-prepare-runner";
|
|
15
|
-
import type
|
|
16
|
-
|
|
17
|
-
export interface SessionPrepareHookOutcome {
|
|
18
|
-
exitCode: number;
|
|
19
|
-
stdout: string;
|
|
20
|
-
}
|
|
18
|
+
import { instructionHookPayload, type InstructionHookDeps } from "./companion-instructions";
|
|
19
|
+
import { hookOutcome, runHook, type HookCall, type HookEnv, type HookOutcome } from "./hook-io";
|
|
21
20
|
|
|
22
21
|
export interface SessionPrepareHookDeps {
|
|
23
22
|
/** The runtime-directory launcher; `null` until a `mate` run wrote it. */
|
|
@@ -25,6 +24,8 @@ export interface SessionPrepareHookDeps {
|
|
|
25
24
|
runner: (mateBin: string, repoPath: string) => PrepareRunner;
|
|
26
25
|
startSync: (mateBin: string, repoPath: string) => void;
|
|
27
26
|
timeoutMs: number;
|
|
27
|
+
/** The main thread's instruction read, after the prepare. */
|
|
28
|
+
instructions?: InstructionHookDeps;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export const sessionPrepareHookDeps: SessionPrepareHookDeps = {
|
|
@@ -37,12 +38,9 @@ export const sessionPrepareHookDeps: SessionPrepareHookDeps = {
|
|
|
37
38
|
timeoutMs: PREPARE_TIMEOUT_MS,
|
|
38
39
|
};
|
|
39
40
|
|
|
40
|
-
const INERT: SessionPrepareHookOutcome = { exitCode: 0, stdout: "" };
|
|
41
|
-
|
|
42
41
|
/** `systemMessage` only: the model must never be told to repair the companion. */
|
|
43
|
-
function operatorMessage(lines: string[]):
|
|
44
|
-
|
|
45
|
-
return { exitCode: 0, stdout: `${JSON.stringify({ systemMessage: lines.join("\n") })}\n` };
|
|
42
|
+
function operatorMessage(lines: string[]): HookOutcome {
|
|
43
|
+
return hookOutcome(lines.length > 0 ? { systemMessage: lines.join("\n") } : null);
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
async function runLocalPhase(
|
|
@@ -54,10 +52,18 @@ async function runLocalPhase(
|
|
|
54
52
|
try {
|
|
55
53
|
return await pending;
|
|
56
54
|
} catch (error) {
|
|
57
|
-
return { ok: false, notes: [
|
|
55
|
+
return { ok: false, notes: [errorMessage(error)] };
|
|
58
56
|
}
|
|
59
57
|
}
|
|
60
58
|
|
|
59
|
+
/** The local phase's notes, and the Git sync it leaves for the caller to start. */
|
|
60
|
+
interface Prepared {
|
|
61
|
+
notes: string[];
|
|
62
|
+
startSync: () => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const NO_SYNC = (): void => {};
|
|
66
|
+
|
|
61
67
|
/**
|
|
62
68
|
* Inert under a launch environment or where the plan gives it nothing to own.
|
|
63
69
|
* Awaits the local phase within a bound, then starts Git sync detached. Always
|
|
@@ -67,35 +73,116 @@ export async function runSessionPrepareHook(
|
|
|
67
73
|
env: HookEnv,
|
|
68
74
|
cwd: string = process.cwd(),
|
|
69
75
|
deps: SessionPrepareHookDeps = sessionPrepareHookDeps,
|
|
70
|
-
): Promise<
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
): Promise<HookOutcome> {
|
|
77
|
+
const prepared = await guardedPrepare(env, cwd, deps);
|
|
78
|
+
return operatorMessage([...prepared.notes, ...startSync(prepared)]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The folder the prepare runs in, and the main thread's instructions are read from. */
|
|
82
|
+
function prepareDirectory(env: HookEnv, cwd: string): string {
|
|
83
|
+
return env.CLAUDE_PROJECT_DIR || cwd;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function prepareFailure(error: unknown): string {
|
|
87
|
+
return `${FRAMEWORK_NAME}: session prepare failed: ${errorMessage(error)}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A prepare that throws becomes a note, so the instructions after it still go out. */
|
|
91
|
+
async function guardedPrepare(
|
|
92
|
+
env: HookEnv,
|
|
93
|
+
cwd: string,
|
|
94
|
+
deps: SessionPrepareHookDeps,
|
|
95
|
+
): Promise<Prepared> {
|
|
96
|
+
try {
|
|
97
|
+
return await prepare(env, cwd, deps);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return { notes: [prepareFailure(error)], startSync: NO_SYNC };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A sync that fails to start becomes a note. */
|
|
104
|
+
function startSync(prepared: Prepared): string[] {
|
|
105
|
+
try {
|
|
106
|
+
prepared.startSync();
|
|
107
|
+
return [];
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return [prepareFailure(error)];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function prepare(env: HookEnv, cwd: string, deps: SessionPrepareHookDeps): Promise<Prepared> {
|
|
114
|
+
const nothing = { notes: [], startSync: NO_SYNC };
|
|
115
|
+
if (hasLaunchEnvironment(env)) return nothing;
|
|
116
|
+
const projectDir = prepareDirectory(env, cwd);
|
|
73
117
|
const projection = resolveProjection(projectDir);
|
|
74
|
-
if (!projection) return
|
|
118
|
+
if (!projection) return nothing;
|
|
75
119
|
const plan = unmanagedClaudeDelivery(projection.repoRoot);
|
|
76
120
|
const reconciles = plan.reconcile === "prepare-hook";
|
|
77
121
|
const syncs = plan.gitSync === "prepare-hook";
|
|
78
|
-
if (!reconciles && !syncs) return
|
|
122
|
+
if (!reconciles && !syncs) return nothing;
|
|
79
123
|
|
|
80
124
|
const mateBin = deps.mateBin();
|
|
81
125
|
if (!mateBin) {
|
|
82
|
-
return
|
|
83
|
-
|
|
84
|
-
|
|
126
|
+
return {
|
|
127
|
+
notes: [
|
|
128
|
+
`${FRAMEWORK_NAME}: session prepare skipped: ${runtimeDirectoryPath()} is missing; run \`${FRAMEWORK_NAME}\` once to repair it.`,
|
|
129
|
+
],
|
|
130
|
+
startSync: NO_SYNC,
|
|
131
|
+
};
|
|
85
132
|
}
|
|
86
133
|
|
|
87
134
|
const local = reconciles
|
|
88
135
|
? await runLocalPhase(deps, mateBin, projectDir)
|
|
89
136
|
: { ok: true, notes: [] };
|
|
90
|
-
if (syncs) deps.startSync(mateBin, projectDir);
|
|
91
|
-
|
|
92
137
|
const verdict = local.ok ? "warning" : "failed";
|
|
93
|
-
return
|
|
138
|
+
return {
|
|
139
|
+
notes: local.notes.map((note) => `${FRAMEWORK_NAME}: session prepare ${verdict}: ${note}`),
|
|
140
|
+
startSync: syncs ? () => deps.startSync(mateBin, projectDir) : NO_SYNC,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Legacy rule: wraps before `--projected` registered this copy bare, and only
|
|
146
|
+
* this hook re-wraps them. So a bare copy counts as the wrap's unless
|
|
147
|
+
* `CLAUDE_PLUGIN_ROOT`, set for plugin hooks only, marks it as the plugin's.
|
|
148
|
+
*/
|
|
149
|
+
function isWrapCopy({ argv, env }: HookCall): boolean {
|
|
150
|
+
return argv.includes(PROJECTED_FLAG) || !env.CLAUDE_PLUGIN_ROOT;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The prepare, the main thread's Companion Instructions, then the Git sync, in
|
|
155
|
+
* one hook: Claude Code runs an event's hooks in parallel, and the prepare and
|
|
156
|
+
* a fast-forward both rewrite the files the instructions are read from. Only
|
|
157
|
+
* the wrap's copy runs any. Input that is not a JSON object is no event.
|
|
158
|
+
*/
|
|
159
|
+
export async function runSessionStartHook(
|
|
160
|
+
call: HookCall,
|
|
161
|
+
deps: SessionPrepareHookDeps = sessionPrepareHookDeps,
|
|
162
|
+
): Promise<HookOutcome> {
|
|
163
|
+
const { env, cwd, input } = call;
|
|
164
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return hookOutcome(null);
|
|
165
|
+
if (!isWrapCopy(call)) return hookOutcome(null);
|
|
166
|
+
const prepared = await guardedPrepare(env, cwd, deps);
|
|
167
|
+
const { hookSpecificOutput, systemMessage } =
|
|
168
|
+
instructionHookPayload(
|
|
169
|
+
{ env, cwd, input, projected: true, directory: prepareDirectory(env, cwd) },
|
|
170
|
+
deps.instructions,
|
|
171
|
+
) ?? {};
|
|
172
|
+
const notes = [...prepared.notes, ...startSync(prepared)];
|
|
173
|
+
const messages = systemMessage ? [...notes, systemMessage] : notes;
|
|
174
|
+
if (!hookSpecificOutput) return operatorMessage(messages);
|
|
175
|
+
return hookOutcome({
|
|
176
|
+
hookSpecificOutput,
|
|
177
|
+
...(messages.length > 0 ? { systemMessage: messages.join("\n") } : {}),
|
|
178
|
+
});
|
|
94
179
|
}
|
|
95
180
|
|
|
96
|
-
/** Plugin-shim entry. */
|
|
97
|
-
export
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
181
|
+
/** Plugin-shim entry; exits zero even when reading its input fails. */
|
|
182
|
+
export function run(argv: readonly string[] = process.argv.slice(2)): Promise<number> {
|
|
183
|
+
return runHook(
|
|
184
|
+
(input) => runSessionStartHook({ env: process.env, cwd: process.cwd(), argv, input }),
|
|
185
|
+
(error) =>
|
|
186
|
+
operatorMessage([`${FRAMEWORK_NAME}: session start hook failed: ${errorMessage(error)}`]),
|
|
187
|
+
);
|
|
101
188
|
}
|
|
@@ -12,12 +12,15 @@ import fs from "node:fs";
|
|
|
12
12
|
import os from "node:os";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
|
|
15
|
+
import { artifactRefusal, resolveArtifactTarget } from "../runtime/artifact-refusal";
|
|
16
|
+
import { checkCommitAttribution } from "../runtime/commit-attribution-guard";
|
|
15
17
|
import { gitEnvironment } from "../runtime/companion-git";
|
|
16
18
|
import { companionForkRefusal } from "../runtime/companion-sync";
|
|
17
19
|
import { readCompanionRuntimeContext } from "../runtime/env";
|
|
18
20
|
import { companionLinkPath } from "../runtime/repo-local";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
import { checkSecretAccess, type SecretToolCall } from "../runtime/secret-guard";
|
|
22
|
+
import { stripHeredocBodies } from "../runtime/shell-heredoc";
|
|
23
|
+
import type { HookEnv } from "./hook-io";
|
|
21
24
|
|
|
22
25
|
export interface HookOutcome {
|
|
23
26
|
exitCode: number;
|
|
@@ -44,8 +47,9 @@ const ARTIFACT_PATH_MARKERS = [
|
|
|
44
47
|
"/docs/prd/",
|
|
45
48
|
];
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
const
|
|
50
|
+
/** `[ \t]`, not `\s`: a redirect or `tee` names its target on its own line. */
|
|
51
|
+
const MD_REDIRECT_PATTERN = />{1,2}[ \t]*([^\s;|&<>]+\.md)\b/g;
|
|
52
|
+
const MD_TEE_PATTERN = /\btee[ \t]+(?:-a[ \t]+)?([^\s;|&<>]+\.md)\b/g;
|
|
49
53
|
|
|
50
54
|
export function artifactLikePath(filePath: string): boolean {
|
|
51
55
|
if (!filePath) return false;
|
|
@@ -118,11 +122,6 @@ function claudeConfigRoot(env: HookEnv): string {
|
|
|
118
122
|
return path.normalize(path.join(env.HOME || os.homedir(), ".claude"));
|
|
119
123
|
}
|
|
120
124
|
|
|
121
|
-
function normalizePath(filePath: string, ctx: GuardContext): string {
|
|
122
|
-
if (path.isAbsolute(filePath)) return path.normalize(filePath);
|
|
123
|
-
return path.normalize(path.join(ctx.repoRoot, filePath));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
125
|
function isUnder(target: string, root: string): boolean {
|
|
127
126
|
const relative = path.relative(path.normalize(root), path.normalize(target));
|
|
128
127
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
@@ -181,17 +180,14 @@ function isProductDocumentationPath(target: string, ctx: GuardContext): boolean
|
|
|
181
180
|
return false;
|
|
182
181
|
}
|
|
183
182
|
|
|
184
|
-
function blockWrite(target: string,
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
lines.push(`Note: CLAUDE.md already lives in the companion repo at ${companion}/CLAUDE.md.`);
|
|
193
|
-
}
|
|
194
|
-
return { exitCode: 2, stderr: lines.join("\n") + "\n" };
|
|
183
|
+
function blockWrite(target: string, resolved: string, ctx: GuardContext): HookOutcome {
|
|
184
|
+
const refusal = artifactRefusal({
|
|
185
|
+
target,
|
|
186
|
+
resolved,
|
|
187
|
+
companion: ctx.companion,
|
|
188
|
+
repoRoot: ctx.repoRoot,
|
|
189
|
+
});
|
|
190
|
+
return { exitCode: 2, stderr: `${refusal}\n` };
|
|
195
191
|
}
|
|
196
192
|
|
|
197
193
|
function checkFilePath(
|
|
@@ -201,7 +197,7 @@ function checkFilePath(
|
|
|
201
197
|
): HookOutcome | null {
|
|
202
198
|
if (!filePath) return null;
|
|
203
199
|
|
|
204
|
-
const normalized =
|
|
200
|
+
const normalized = resolveArtifactTarget(filePath, ctx.repoRoot);
|
|
205
201
|
if (isCompanionPath(normalized, ctx)) return null;
|
|
206
202
|
|
|
207
203
|
if (isUnder(normalized, claudeConfigRoot(ctx.env))) return null;
|
|
@@ -219,7 +215,7 @@ function checkFilePath(
|
|
|
219
215
|
return null;
|
|
220
216
|
}
|
|
221
217
|
|
|
222
|
-
return blockWrite(filePath, ctx
|
|
218
|
+
return blockWrite(filePath, normalized, ctx);
|
|
223
219
|
}
|
|
224
220
|
|
|
225
221
|
/**
|
|
@@ -234,17 +230,19 @@ async function checkForkedCompanion(
|
|
|
234
230
|
ctx: GuardContext,
|
|
235
231
|
): Promise<HookOutcome | null> {
|
|
236
232
|
if (!filePath || !artifactLikePath(filePath)) return null;
|
|
237
|
-
if (!isCompanionPath(
|
|
233
|
+
if (!isCompanionPath(resolveArtifactTarget(filePath, ctx.repoRoot), ctx)) return null;
|
|
238
234
|
|
|
239
235
|
const refusal = await companionForkRefusal(ctx.env, ctx.companion);
|
|
240
236
|
return refusal ? { exitCode: 2, stderr: `${refusal}\n` } : null;
|
|
241
237
|
}
|
|
242
238
|
|
|
239
|
+
/** Heredoc bodies are what a command writes, not what it runs: they are not scanned. */
|
|
243
240
|
export function commandMatches(command: string): string[] {
|
|
244
241
|
const targets: string[] = [];
|
|
242
|
+
const commandText = stripHeredocBodies(command);
|
|
245
243
|
for (const pattern of [MD_REDIRECT_PATTERN, MD_TEE_PATTERN]) {
|
|
246
244
|
const matcher = new RegExp(pattern.source, pattern.flags);
|
|
247
|
-
for (const match of
|
|
245
|
+
for (const match of commandText.matchAll(matcher)) {
|
|
248
246
|
const target = match[1].trim().replace(/^["']+|["']+$/g, "");
|
|
249
247
|
if (target) targets.push(target);
|
|
250
248
|
}
|
|
@@ -252,6 +250,53 @@ export function commandMatches(command: string): string[] {
|
|
|
252
250
|
return targets;
|
|
253
251
|
}
|
|
254
252
|
|
|
253
|
+
/** Claude tool payloads in the shape the shared secret guard reads. */
|
|
254
|
+
function secretToolCall(
|
|
255
|
+
toolName: string,
|
|
256
|
+
toolInput: Record<string, unknown>,
|
|
257
|
+
ctx: GuardContext,
|
|
258
|
+
): SecretToolCall | null {
|
|
259
|
+
const text = (value: unknown) => (typeof value === "string" ? value : "");
|
|
260
|
+
const target = (value: unknown) =>
|
|
261
|
+
text(value) ? resolveArtifactTarget(text(value), ctx.repoRoot) : "";
|
|
262
|
+
switch (toolName) {
|
|
263
|
+
case "Read":
|
|
264
|
+
return { kind: "read", paths: [target(toolInput.file_path)] };
|
|
265
|
+
case "Write":
|
|
266
|
+
return {
|
|
267
|
+
kind: "write",
|
|
268
|
+
writes: [{ path: target(toolInput.file_path), content: text(toolInput.content) }],
|
|
269
|
+
};
|
|
270
|
+
case "Edit":
|
|
271
|
+
return {
|
|
272
|
+
kind: "write",
|
|
273
|
+
writes: [{ path: target(toolInput.file_path), content: text(toolInput.new_string) }],
|
|
274
|
+
};
|
|
275
|
+
case "MultiEdit": {
|
|
276
|
+
const edits = Array.isArray(toolInput.edits) ? toolInput.edits : [];
|
|
277
|
+
const content = edits
|
|
278
|
+
.map((edit) => (edit && typeof edit === "object" ? text(edit.new_string) : ""))
|
|
279
|
+
.join("\n");
|
|
280
|
+
return { kind: "write", writes: [{ path: target(toolInput.file_path), content }] };
|
|
281
|
+
}
|
|
282
|
+
case "NotebookEdit":
|
|
283
|
+
return {
|
|
284
|
+
kind: "write",
|
|
285
|
+
writes: [{ path: target(toolInput.notebook_path), content: text(toolInput.new_source) }],
|
|
286
|
+
};
|
|
287
|
+
case "Bash":
|
|
288
|
+
return { kind: "shell", command: text(toolInput.command) };
|
|
289
|
+
case "Grep":
|
|
290
|
+
return {
|
|
291
|
+
kind: "search",
|
|
292
|
+
paths: [target(toolInput.path)],
|
|
293
|
+
globs: [text(toolInput.glob)],
|
|
294
|
+
};
|
|
295
|
+
default:
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
255
300
|
/**
|
|
256
301
|
* Fails open only when no Mate context resolves from either source; in a
|
|
257
302
|
* wrapped Working Repository the guard runs without a launch.
|
|
@@ -273,6 +318,16 @@ export async function evaluate(
|
|
|
273
318
|
? (input.tool_input as Record<string, unknown>)
|
|
274
319
|
: {};
|
|
275
320
|
|
|
321
|
+
/** Secrets first: a secret refusal wins over every artifact-placement verdict. */
|
|
322
|
+
const secretCall = secretToolCall(toolName, toolInput, ctx);
|
|
323
|
+
if (secretCall) {
|
|
324
|
+
const refusal = await checkSecretAccess(secretCall, {
|
|
325
|
+
companionPath: ctx.companion,
|
|
326
|
+
isCompanionPath: (target) => isCompanionPath(target, ctx),
|
|
327
|
+
});
|
|
328
|
+
if (refusal) return { exitCode: 2, stderr: `${refusal}\n` };
|
|
329
|
+
}
|
|
330
|
+
|
|
276
331
|
if (toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit") {
|
|
277
332
|
const filePath = String(toolInput.file_path ?? "");
|
|
278
333
|
/** The path refusal wins when both apply: its message is not replaced. */
|
|
@@ -282,6 +337,9 @@ export async function evaluate(
|
|
|
282
337
|
|
|
283
338
|
if (toolName === "Bash") {
|
|
284
339
|
const command = String(toolInput.command ?? "");
|
|
340
|
+
const attribution = checkCommitAttribution(command);
|
|
341
|
+
if (attribution) return { exitCode: 2, stderr: `${attribution}\n` };
|
|
342
|
+
|
|
285
343
|
const targets = commandMatches(command);
|
|
286
344
|
for (const target of targets) {
|
|
287
345
|
const outcome = checkFilePath(target, ctx);
|
package/src/index.ts
CHANGED
|
@@ -39,13 +39,17 @@ export type {
|
|
|
39
39
|
McpService,
|
|
40
40
|
Plugin,
|
|
41
41
|
PluginCliCommand,
|
|
42
|
+
PluginDirectoryContribution,
|
|
42
43
|
PluginPolicy,
|
|
43
44
|
PluginRegistration,
|
|
44
45
|
ProviderHosting,
|
|
45
46
|
ProviderPlugin,
|
|
47
|
+
RuntimeContributions,
|
|
48
|
+
RuntimeContributionsByRuntime,
|
|
46
49
|
SetupContext,
|
|
47
50
|
TemplatesService,
|
|
48
51
|
} from "./tools/setup/plugin";
|
|
52
|
+
export type { SessionKind } from "./runtime/session-kind";
|
|
49
53
|
export type {
|
|
50
54
|
FrameworkType,
|
|
51
55
|
FrameworkConfig,
|