@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Cleanup, Context } from "@opencode/plugin/promise/plugin";
|
|
2
2
|
|
|
3
|
+
import { errorMessage } from "../runtime/error-message";
|
|
3
4
|
import {
|
|
4
5
|
PREPARE_TIMEOUT_MS,
|
|
5
6
|
type PrepareRun,
|
|
@@ -52,7 +53,7 @@ async function report(
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
function failedRun(error: unknown): PrepareRun {
|
|
55
|
-
return { ok: false, notes: [
|
|
56
|
+
return { ok: false, notes: [errorMessage(error)] };
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/** No remote: OpenCode holds MCP servers and skills until `setup` returns. */
|
package/src/opencode/tui.tsx
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* does not accept it, so the jsx-key rules cannot be satisfied — only suppressed.
|
|
7
7
|
* Arrays render through `.map`: Solid re-runs it whole on a store change, fine for a few lines.
|
|
8
8
|
*/
|
|
9
|
+
import { BRAND_ACCENT } from "../brand/palette";
|
|
9
10
|
import { mateVersion } from "../runtime/install";
|
|
10
11
|
import { readContext, readStalenessLines } from "./companion-policy";
|
|
11
12
|
|
|
@@ -47,7 +48,8 @@ export const STALENESS_POLL_MS = 5_000;
|
|
|
47
48
|
|
|
48
49
|
type SessionNotes = { lines: string[] };
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
/** The `mate v<version>` line uses the brand accent, which reads on light and dark themes alike. */
|
|
52
|
+
export const MATE_VERSION_COLOR = BRAND_ACCENT;
|
|
51
53
|
const MATE_VERSION = process.env.MATE_VERSION ?? mateVersion();
|
|
52
54
|
|
|
53
55
|
function SessionContext({
|
|
@@ -72,7 +74,7 @@ function SessionContext({
|
|
|
72
74
|
paddingBottom={sidebar ? 1 : 0}
|
|
73
75
|
flexShrink={0}
|
|
74
76
|
>
|
|
75
|
-
<text fg={
|
|
77
|
+
<text fg={MATE_VERSION_COLOR}>mate v{MATE_VERSION}</text>
|
|
76
78
|
<text fg={text.muted}>repo: {context.repositoryPath}</text>
|
|
77
79
|
<text fg={text.muted}>mate: {context.companionPath}</text>
|
|
78
80
|
{notes.lines.map((note) => (
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** The artifact-write refusal both runtimes print; pure, so either guard can import it. */
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { FRAMEWORK_NAME } from "./framework";
|
|
5
|
+
|
|
6
|
+
/** Matched by consumers and tests; keep byte-identical. */
|
|
7
|
+
export const ARTIFACT_REFUSAL_HEADLINE = "artifact writes must go to the companion framework path.";
|
|
8
|
+
|
|
9
|
+
export interface ArtifactRefusalInput {
|
|
10
|
+
/** As the tool supplied it, relative or absolute. */
|
|
11
|
+
target: string;
|
|
12
|
+
/** The absolute path the guard judged. */
|
|
13
|
+
resolved: string;
|
|
14
|
+
companion: string;
|
|
15
|
+
/** The Working Repository root relative targets resolve against. */
|
|
16
|
+
repoRoot: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The absolute path both guards judge: a relative target resolves against the repo root, never a `cd`. */
|
|
20
|
+
export function resolveArtifactTarget(target: string, repoRoot: string): string {
|
|
21
|
+
if (path.isAbsolute(target)) return path.normalize(target);
|
|
22
|
+
return path.normalize(path.join(repoRoot, target));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function relativeInside(target: string, root: string): string | null {
|
|
26
|
+
const relative = path.relative(path.normalize(root), path.normalize(target));
|
|
27
|
+
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) return null;
|
|
28
|
+
return relative;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The refusal text, without a trailing newline. */
|
|
32
|
+
export function artifactRefusal({
|
|
33
|
+
target,
|
|
34
|
+
resolved,
|
|
35
|
+
companion,
|
|
36
|
+
repoRoot,
|
|
37
|
+
}: ArtifactRefusalInput): string {
|
|
38
|
+
const isRelative = !path.isAbsolute(target);
|
|
39
|
+
const inside = relativeInside(resolved, repoRoot);
|
|
40
|
+
const suggested = inside ? path.join(companion, inside) : null;
|
|
41
|
+
|
|
42
|
+
const lines = [
|
|
43
|
+
`${FRAMEWORK_NAME} guardrail: ${ARTIFACT_REFUSAL_HEADLINE}`,
|
|
44
|
+
` target: ${target}`,
|
|
45
|
+
];
|
|
46
|
+
if (isRelative) lines.push(` resolved: ${resolved}`);
|
|
47
|
+
lines.push(` companion: ${companion}`);
|
|
48
|
+
if (suggested) lines.push(` suggested: ${suggested}`);
|
|
49
|
+
if (isRelative) {
|
|
50
|
+
lines.push(
|
|
51
|
+
"Relative paths resolve against the working repository; a preceding `cd` is not followed.",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
lines.push(
|
|
55
|
+
suggested
|
|
56
|
+
? "Rewrite to the suggested absolute path and retry."
|
|
57
|
+
: `Use an absolute path under ${companion}.`,
|
|
58
|
+
);
|
|
59
|
+
if (path.basename(target) === "CLAUDE.md") {
|
|
60
|
+
lines.push(`Note: CLAUDE.md already lives in the companion repo at ${companion}/CLAUDE.md.`);
|
|
61
|
+
}
|
|
62
|
+
return lines.join("\n");
|
|
63
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
1
3
|
import type { ClaudeHookGroup } from "./claude-hook-group";
|
|
4
|
+
import type { SessionKind } from "./session-kind";
|
|
2
5
|
import { NOT_WRAPPED, readWrapStateSync, type WrapState } from "./wrap-state";
|
|
3
6
|
|
|
4
7
|
/**
|
|
@@ -7,13 +10,6 @@ import { NOT_WRAPPED, readWrapStateSync, type WrapState } from "./wrap-state";
|
|
|
7
10
|
* may import it.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
|
-
/**
|
|
11
|
-
* `managed-repo`: `mate claude` with a Working Repository; its prepare just
|
|
12
|
-
* reconciled the Claude wrap, which the launcher asserts. `managed-companion`:
|
|
13
|
-
* a companion-scoped launch. `unmanaged`: a session with no launch environment.
|
|
14
|
-
*/
|
|
15
|
-
export type SessionKind = "managed-repo" | "managed-companion" | "unmanaged";
|
|
16
|
-
|
|
17
13
|
export interface ClaudeDeliveryPlan {
|
|
18
14
|
/** Capability hook groups; `launchSettingsExclude` is what `--settings` must not repeat. */
|
|
19
15
|
readonly hooks: {
|
|
@@ -28,9 +24,95 @@ export interface ClaudeDeliveryPlan {
|
|
|
28
24
|
readonly reconcile: "attended-prepare" | "prepare-hook" | "none";
|
|
29
25
|
/** Companion Git synchronization. */
|
|
30
26
|
readonly gitSync: "launch-preflight" | "prepare-hook" | "banner";
|
|
27
|
+
/**
|
|
28
|
+
* Plugin folders loaded as Claude plugins. A managed launch passes Mate's
|
|
29
|
+
* bundled plugin and every gathered Capability folder but `launchExclude`,
|
|
30
|
+
* the folders the wrap carries; a Claude-wrapped plain session gets those
|
|
31
|
+
* from the wrap alone.
|
|
32
|
+
*/
|
|
33
|
+
readonly plugins: {
|
|
34
|
+
readonly owner: "launch-flag" | "wrap" | "none";
|
|
35
|
+
readonly launchExclude: readonly string[];
|
|
36
|
+
};
|
|
37
|
+
/** Companion Instructions, per thread kind; `dirAccess` through settings loads no memory file. */
|
|
38
|
+
readonly instructions: ClaudeInstructionOwners;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ClaudeInstructionOwners {
|
|
42
|
+
/** `wrap-session-hook`: the wrap's session-prepare hook, once it has prepared, for a launch and a plain session alike. */
|
|
43
|
+
readonly main: "native-memory" | "wrap-session-hook" | "none";
|
|
44
|
+
/** `plugin-subagent-hook`: the bundled plugin's SubagentStart copy; `wrap-subagent-hook`: the wrap's. */
|
|
45
|
+
readonly subagents: "plugin-subagent-hook" | "native-memory" | "wrap-subagent-hook" | "none";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether the session's launch passes `--plugin-dir`: every managed launch does, a plain
|
|
50
|
+
* session cannot. Not whether the session loads plugin folders: a Claude-wrapped plain
|
|
51
|
+
* session gets them from the wrap.
|
|
52
|
+
*/
|
|
53
|
+
export function launchPassesClaudePluginFolders(kind: SessionKind): boolean {
|
|
54
|
+
return kind !== "unmanaged";
|
|
31
55
|
}
|
|
32
56
|
|
|
33
57
|
export function planClaudeDelivery(kind: SessionKind, wrap: WrapState): ClaudeDeliveryPlan {
|
|
58
|
+
return {
|
|
59
|
+
...planChannels(kind, wrap),
|
|
60
|
+
plugins: planPlugins(kind, wrap),
|
|
61
|
+
instructions: planClaudeInstructions(kind, wrap.runtimes.has("claude")),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The instruction owners alone, for a hook that knows its wrap without reading
|
|
67
|
+
* one: a wrap-carried hook runs only because a Claude wrap installed it, maybe
|
|
68
|
+
* the main checkout's, so it must not gate on this Checkout's manifest.
|
|
69
|
+
*/
|
|
70
|
+
export function planClaudeInstructions(
|
|
71
|
+
kind: SessionKind,
|
|
72
|
+
wrappedForClaude: boolean,
|
|
73
|
+
): ClaudeInstructionOwners {
|
|
74
|
+
switch (kind) {
|
|
75
|
+
case "managed-repo":
|
|
76
|
+
return { main: "wrap-session-hook", subagents: "plugin-subagent-hook" };
|
|
77
|
+
case "managed-companion":
|
|
78
|
+
return { main: "native-memory", subagents: "native-memory" };
|
|
79
|
+
case "unmanaged":
|
|
80
|
+
return wrappedForClaude
|
|
81
|
+
? { main: "wrap-session-hook", subagents: "wrap-subagent-hook" }
|
|
82
|
+
: { main: "none", subagents: "none" };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The folders a managed launch passes with `--plugin-dir`, in order: the
|
|
88
|
+
* bundled plugin, then each gathered folder the wrap does not carry, matched
|
|
89
|
+
* by resolved path. None when the launch flag does not own plugin folders.
|
|
90
|
+
*/
|
|
91
|
+
export function launchPluginFolders(
|
|
92
|
+
plan: ClaudeDeliveryPlan,
|
|
93
|
+
gathered: readonly string[],
|
|
94
|
+
bundledPluginRoot: string,
|
|
95
|
+
): string[] {
|
|
96
|
+
if (plan.plugins.owner !== "launch-flag") return [];
|
|
97
|
+
const carried = new Set(plan.plugins.launchExclude.map((dir) => path.resolve(dir)));
|
|
98
|
+
return [bundledPluginRoot, ...gathered.filter((dir) => !carried.has(path.resolve(dir)))];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function planPlugins(kind: SessionKind, wrap: WrapState): ClaudeDeliveryPlan["plugins"] {
|
|
102
|
+
switch (kind) {
|
|
103
|
+
case "managed-repo":
|
|
104
|
+
return { owner: "launch-flag", launchExclude: wrap.carriedClaudePluginDirectories };
|
|
105
|
+
case "managed-companion":
|
|
106
|
+
return { owner: "launch-flag", launchExclude: [] };
|
|
107
|
+
case "unmanaged":
|
|
108
|
+
return { owner: wrap.runtimes.has("claude") ? "wrap" : "none", launchExclude: [] };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function planChannels(
|
|
113
|
+
kind: SessionKind,
|
|
114
|
+
wrap: WrapState,
|
|
115
|
+
): Omit<ClaudeDeliveryPlan, "plugins" | "instructions"> {
|
|
34
116
|
switch (kind) {
|
|
35
117
|
case "managed-repo":
|
|
36
118
|
return {
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { heredocBodies, stripHeredocBodies } from "./shell-heredoc";
|
|
4
|
+
|
|
5
|
+
const CO_AUTHOR_TRAILER = /^\s*co-authored-by\s*[:=]\s*(.+)$/gim;
|
|
6
|
+
/** Substrings, not words: `ChatGPT` and `claude_bot` mention a model too. */
|
|
7
|
+
const MODEL_NAME = /claude|anthropic|gpt|openai|codex|gemini|copilot|opencode/i;
|
|
8
|
+
|
|
9
|
+
/** `git` options before the subcommand that take the next word as their value. */
|
|
10
|
+
const GIT_VALUE_OPTIONS = new Set(["-C", "-c"]);
|
|
11
|
+
/** `git commit` short options whose value is the next word, or the rest of the cluster. */
|
|
12
|
+
const COMMIT_VALUE_SHORTS = new Set(["m", "F", "t", "c", "C"]);
|
|
13
|
+
/** Words that run the command after them, with their own dash options. */
|
|
14
|
+
const COMMAND_WRAPPERS = new Set(["env", "sudo", "command", "exec", "nice", "nohup", "time"]);
|
|
15
|
+
/** Characters a backslash escapes inside double quotes; before any other it stays. */
|
|
16
|
+
const DOUBLE_QUOTE_ESCAPES = new Set(["$", "`", '"', "\\", "\n"]);
|
|
17
|
+
|
|
18
|
+
const SEPARATOR = Symbol("separator");
|
|
19
|
+
/** A word and the first and last lines it spans. */
|
|
20
|
+
interface Word {
|
|
21
|
+
text: string;
|
|
22
|
+
start: number;
|
|
23
|
+
end: number;
|
|
24
|
+
}
|
|
25
|
+
type Token = Word | typeof SEPARATOR;
|
|
26
|
+
|
|
27
|
+
/** One simple command's words and the lines it spans. */
|
|
28
|
+
interface Segment {
|
|
29
|
+
words: string[];
|
|
30
|
+
start: number;
|
|
31
|
+
end: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Shell words and command separators (`&&`, `||`, `;`, `|`, `&`, parentheses,
|
|
36
|
+
* newline), quotes removed. A quoted string stays one word across lines, so a
|
|
37
|
+
* multi-line `-m` message is one value. A heuristic, not a parser.
|
|
38
|
+
*/
|
|
39
|
+
function shellTokens(command: string): Token[] {
|
|
40
|
+
const tokens: Token[] = [];
|
|
41
|
+
let word = "";
|
|
42
|
+
let quote: string | undefined;
|
|
43
|
+
let quoted = false;
|
|
44
|
+
let line = 0;
|
|
45
|
+
let start = 0;
|
|
46
|
+
const markStart = () => {
|
|
47
|
+
if (!word && !quoted) start = line;
|
|
48
|
+
};
|
|
49
|
+
const append = (text: string) => {
|
|
50
|
+
markStart();
|
|
51
|
+
word += text;
|
|
52
|
+
};
|
|
53
|
+
const flush = () => {
|
|
54
|
+
if (word || quoted) tokens.push({ text: word, start, end: line });
|
|
55
|
+
word = "";
|
|
56
|
+
quoted = false;
|
|
57
|
+
};
|
|
58
|
+
for (let i = 0; i < command.length; i++) {
|
|
59
|
+
const char = command[i];
|
|
60
|
+
if (quote) {
|
|
61
|
+
if (char === quote) quote = undefined;
|
|
62
|
+
else if (char === "\\" && quote === '"' && DOUBLE_QUOTE_ESCAPES.has(command[i + 1] ?? "")) {
|
|
63
|
+
append(command[++i]);
|
|
64
|
+
} else append(char);
|
|
65
|
+
} else if (char === "'" || char === '"') {
|
|
66
|
+
markStart();
|
|
67
|
+
quote = char;
|
|
68
|
+
quoted = true;
|
|
69
|
+
} else if (char === "\\" && i + 1 < command.length) {
|
|
70
|
+
if (command[i + 1] !== "\n") append(command[i + 1]);
|
|
71
|
+
i++;
|
|
72
|
+
} else if (";|&\n()".includes(char)) {
|
|
73
|
+
flush();
|
|
74
|
+
if (tokens.at(-1) !== SEPARATOR) tokens.push(SEPARATOR);
|
|
75
|
+
} else if (/\s/.test(char)) {
|
|
76
|
+
flush();
|
|
77
|
+
} else {
|
|
78
|
+
append(char);
|
|
79
|
+
}
|
|
80
|
+
if (command[i] === "\n") line++;
|
|
81
|
+
}
|
|
82
|
+
flush();
|
|
83
|
+
return tokens;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function segments(tokens: Token[]): Segment[] {
|
|
87
|
+
const result: Segment[] = [];
|
|
88
|
+
let current: Segment | undefined;
|
|
89
|
+
for (const token of tokens) {
|
|
90
|
+
if (token === SEPARATOR) {
|
|
91
|
+
current = undefined;
|
|
92
|
+
} else if (current) {
|
|
93
|
+
current.words.push(token.text);
|
|
94
|
+
current.end = token.end;
|
|
95
|
+
} else {
|
|
96
|
+
current = { words: [token.text], start: token.start, end: token.end };
|
|
97
|
+
result.push(current);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The words after `git commit`, or null when the segment runs something else. */
|
|
104
|
+
function commitArguments(segment: Segment): string[] | null {
|
|
105
|
+
const words = segment.words;
|
|
106
|
+
let i = 0;
|
|
107
|
+
while (i < words.length) {
|
|
108
|
+
const word = words[i];
|
|
109
|
+
if (COMMAND_WRAPPERS.has(word) || word === "{" || word === "!" || /^\w+=/.test(word)) i++;
|
|
110
|
+
else if (i > 0 && COMMAND_WRAPPERS.has(words[i - 1]) && word.startsWith("-")) i++;
|
|
111
|
+
else break;
|
|
112
|
+
}
|
|
113
|
+
if (i >= words.length || path.basename(words[i]) !== "git") return null;
|
|
114
|
+
i++;
|
|
115
|
+
while (i < words.length && words[i].startsWith("-")) {
|
|
116
|
+
i += GIT_VALUE_OPTIONS.has(words[i]) ? 2 : 1;
|
|
117
|
+
}
|
|
118
|
+
return words[i] === "commit" ? words.slice(i + 1) : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Message text the arguments carry: `-m x`, `-mx`, `-am x`, `--message=x` and
|
|
123
|
+
* `--message x` values, plus each `--trailer` value as a trailer line.
|
|
124
|
+
*/
|
|
125
|
+
function messageValues(args: string[]): string[] {
|
|
126
|
+
const values: string[] = [];
|
|
127
|
+
for (let i = 0; i < args.length; i++) {
|
|
128
|
+
const arg = args[i];
|
|
129
|
+
if (arg === "--") break;
|
|
130
|
+
if (arg === "--message" || arg === "--trailer") {
|
|
131
|
+
if (i + 1 < args.length) values.push(args[++i]);
|
|
132
|
+
} else if (arg.startsWith("--message=") || arg.startsWith("--trailer=")) {
|
|
133
|
+
values.push(arg.slice(arg.indexOf("=") + 1));
|
|
134
|
+
} else if (/^-[^-]/.test(arg)) {
|
|
135
|
+
for (let j = 1; j < arg.length; j++) {
|
|
136
|
+
if (!COMMIT_VALUE_SHORTS.has(arg[j])) continue;
|
|
137
|
+
const rest = arg.slice(j + 1);
|
|
138
|
+
const value = rest || args[++i];
|
|
139
|
+
if (arg[j] === "m" && value !== undefined) values.push(value);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return values;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Whether a trailer credits a model; a literal `\n`, as `printf` or `$'…'` expand it, counts as a line break. */
|
|
148
|
+
function creditsModel(text: string): boolean {
|
|
149
|
+
for (const trailer of text.replace(/\\n/g, "\n").matchAll(CO_AUTHOR_TRAILER)) {
|
|
150
|
+
if (MODEL_NAME.test(trailer[1])) return true;
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const REFUSAL = [
|
|
156
|
+
"Mate guardrail: Co-Authored-By trailers that credit an AI model are not allowed in this repository.",
|
|
157
|
+
"No commit was created. Rerun the same commit without the Co-Authored-By trailer; do not amend or rewrite history to remove it.",
|
|
158
|
+
].join("\n");
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Returns the refusal for a shell command running `git commit` whose message
|
|
162
|
+
* credits an AI model as co-author, or null. Reads only message text in the
|
|
163
|
+
* command: `-m`/`--message`/`--trailer` values and the heredoc bodies a commit
|
|
164
|
+
* reads, those whose marker sits on a line the commit spans. `-F`, `-t` and
|
|
165
|
+
* editor messages pass unchecked.
|
|
166
|
+
*/
|
|
167
|
+
export function checkCommitAttribution(command: string): string | null {
|
|
168
|
+
const commits = segments(shellTokens(stripHeredocBodies(command))).flatMap((segment) => {
|
|
169
|
+
const args = commitArguments(segment);
|
|
170
|
+
return args ? [{ segment, args }] : [];
|
|
171
|
+
});
|
|
172
|
+
if (commits.length === 0) return null;
|
|
173
|
+
|
|
174
|
+
const bodies = heredocBodies(command)
|
|
175
|
+
.filter((body) =>
|
|
176
|
+
commits.some(({ segment }) => body.line >= segment.start && body.line <= segment.end),
|
|
177
|
+
)
|
|
178
|
+
.map((body) => body.text);
|
|
179
|
+
const messages = [...commits.flatMap(({ args }) => messageValues(args)), ...bodies];
|
|
180
|
+
return messages.some(creditsModel) ? REFUSAL : null;
|
|
181
|
+
}
|
|
@@ -27,8 +27,15 @@ export interface GuidanceContext {
|
|
|
27
27
|
companionPath: string;
|
|
28
28
|
repository?: { id: string; path: string };
|
|
29
29
|
capabilities?: GuidanceCapability[];
|
|
30
|
+
/**
|
|
31
|
+
* Whether the companion's secret guardrails are active. Absent means active:
|
|
32
|
+
* a caller that cannot tell never drops the rule.
|
|
33
|
+
*/
|
|
34
|
+
secretGuardrails?: boolean;
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
const SECRET_HANDLING_RULE = ` <rule id="secret-handling" severity="critical">Never read, print, copy, or write secret files (.env, .env.*, keys, certificates, credential files such as .npmrc or .netrc) or secret values. To learn which variables exist, read only the example files (.env.example, .env.sample, .env.template, .env.dist). Refer to a secret by its variable name only. Do not print environment variables that may hold secrets. When a secret must be set or rotated, ask the user to do it instead of writing it yourself. Guarded calls are blocked; the companion's framework.yaml secrets block is the user's switch, not yours.</rule>`;
|
|
38
|
+
|
|
32
39
|
export function hasGraphifyCapability(capabilities: GuidanceCapability[] = []): boolean {
|
|
33
40
|
return capabilities.some((capability) => capability.name === "graphify");
|
|
34
41
|
}
|
|
@@ -149,7 +156,7 @@ export function buildCompanionPolicyXml(
|
|
|
149
156
|
` <rule id="pre-write-classification" severity="critical">Before ANY write, classify the target as product-code or agent-artifact. If unsure, treat it as agent-artifact.</rule>`,
|
|
150
157
|
` <rule id="product-code-location" severity="critical">Product code (README, docs, source, tests) belongs in ${context.repository.path}. Agent-artifacts belong in ${context.companionPath}.</rule>`,
|
|
151
158
|
` <rule id="local-artifact-exception" severity="critical">Only write artifacts in ${context.repository.path} when the exact path is gitignored AND intentionally local-only; otherwise use ${context.companionPath}.</rule>`,
|
|
152
|
-
` <rule id="guardrail" severity="critical">Bad artifact writes to ${context.repository.path} are rejected. Classify correctly first.</rule>`,
|
|
159
|
+
` <rule id="guardrail" severity="critical">Bad artifact writes to ${context.repository.path} are rejected. The guardrail resolves relative paths against ${context.repository.path} and does not follow cd, so write artifacts, including shell redirects and tee, with absolute paths under ${context.companionPath}. Classify correctly first.</rule>`,
|
|
153
160
|
` <rule id="companion-multi-repository" severity="critical">This Companion Repository may serve multiple Working Repositories. The working-repository path above identifies this session's single primary Working Repository, not the companion's full repository set. The Companion Repository is the shared artifact and context plane; the primary Working Repository is the product-code plane.</rule>`,
|
|
154
161
|
` <rule id="repository-area-scope" severity="critical">For domain modeling, identify the canonical Working Repository and its repository-relative Area first. A checkout basename or Area alone is not a repository identity. Shared context applies only when the Companion Repository's CONTEXT-MAP explicitly maps it to the current repository and Area.</rule>`,
|
|
155
162
|
);
|
|
@@ -167,10 +174,14 @@ export function buildCompanionPolicyXml(
|
|
|
167
174
|
);
|
|
168
175
|
}
|
|
169
176
|
|
|
177
|
+
if (context.secretGuardrails !== false) {
|
|
178
|
+
lines.push(SECRET_HANDLING_RULE);
|
|
179
|
+
}
|
|
180
|
+
|
|
170
181
|
if (hasOpenspecCapability(context.capabilities)) {
|
|
171
182
|
lines.push(
|
|
172
183
|
` <rule id="openspec-scope" severity="critical">In OpenSpec, the frontmatter repository identifies the Working Repository. areas are repository-relative paths within that repository. Canonical specs use repository plus flat areas; change artifacts use paired scopes entries. In a monorepo, an Area normally stops at the package root. A cross-repository change may declare multiple scopes, but each resulting spec remains owned by one repository, and each requirement must bind its Area explicitly.</rule>`,
|
|
173
|
-
` <rule id="openspec-publish" severity="critical">Archiving an OpenSpec change is a local operation and publishes nothing. Publish through the mate-artifact
|
|
184
|
+
` <rule id="openspec-publish" severity="critical">Archiving an OpenSpec change is a local operation and publishes nothing. Publish through the mate-publish-artifact skill: it lists candidates with ${FRAMEWORK_NAME} artifact pending --json, holds back archives whose delivery branch is not merged yet, takes an explicit user selection, confirms the commit, tag, and push, then runs ${FRAMEWORK_NAME} artifact publish "<name>" --json per selected change. That command is the only sanctioned publication; never hand-commit or hand-tag one. Publishing only publishes an already-archived change: archiving is a precondition, and running publish on a still-active change is an error that names openspec archive as the missing step. Publish never archives and never applies delta specs itself, so archive first with the mate-archive-change skill, which may run before the merge, and then publish. Publish also refuses to run anywhere but the companion's default branch.</rule>`,
|
|
174
185
|
);
|
|
175
186
|
}
|
|
176
187
|
|
|
@@ -225,7 +236,7 @@ export function buildCompanionGuidance(
|
|
|
225
236
|
*/
|
|
226
237
|
export function buildOpenCodeGuidance(
|
|
227
238
|
capabilities: GuidanceCapability[],
|
|
228
|
-
options: { companionScoped?: boolean } = {},
|
|
239
|
+
options: { companionScoped?: boolean; secretGuardrails?: boolean } = {},
|
|
229
240
|
): MateGuidanceFile {
|
|
230
241
|
const repository = options.companionScoped
|
|
231
242
|
? undefined
|
|
@@ -235,6 +246,7 @@ export function buildOpenCodeGuidance(
|
|
|
235
246
|
companionPath: "$MATE_ARTIFACT_PATH",
|
|
236
247
|
repository,
|
|
237
248
|
capabilities,
|
|
249
|
+
secretGuardrails: options.secretGuardrails,
|
|
238
250
|
},
|
|
239
251
|
{ wrapperBinPath: "$MATE_WRAPPER_BIN_PATH" },
|
|
240
252
|
);
|