@uniqbit/mate-core 0.18.4 → 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/opencode-plugin/src/add-dir.ts +76 -10
- package/opencode-plugin/src/companion.ts +1 -8
- package/opencode-plugin/src/server.ts +2 -15
- 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 +200 -129
- 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 +257 -78
- package/src/cli/commands/install.ts +0 -8
- 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/terminal-launch.ts +7 -16
- 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 +9 -42
- 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 +5 -2
- 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 +10 -36
- 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 +60 -162
- package/src/lib/orchestrator/adapters/opencode.ts +9 -24
- package/src/lib/orchestrator/config-store.ts +2 -70
- 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 -190
- package/src/lib/orchestrator/projection-entries.ts +45 -15
- package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
- package/src/lib/orchestrator/projection-types.ts +3 -2
- package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
- package/src/lib/orchestrator/session-prepare.ts +49 -87
- 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 -88
- 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/guidance.ts +0 -17
- 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 +16 -15
- package/src/runtime/projected-flag.ts +7 -0
- package/src/runtime/projected-guidance.ts +1 -0
- package/src/runtime/runtime-document-manifest.ts +62 -28
- 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 -6
- 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/agents/{mate-artifact-publish → 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/claude/{mate-artifact-publish → 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 +273 -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/audiences.ts +7 -39
- package/src/tools/setup/dynamic-plugins/declarations.ts +1 -1
- package/src/tools/setup/dynamic-plugins/hydrate.ts +4 -7
- package/src/tools/setup/dynamic-plugins/install.ts +24 -28
- package/src/tools/setup/engine.ts +93 -148
- package/src/tools/setup/managed-output.ts +51 -0
- package/src/tools/setup/mate.ts +86 -11
- package/src/tools/setup/plugin.ts +37 -17
- package/src/tools/setup/plugins/gitignore.ts +1 -7
- 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 +285 -34
- package/src/tools/setup/providers/opencode.ts +180 -11
- package/src/tools/setup/registry.ts +2 -24
- package/src/tools/setup/surface-target.ts +4 -13
- package/src/tools/setup/utils.ts +14 -4
- package/src/tools/setup.ts +131 -18
- package/wrappers/bin/graphify +52 -4
- package/wrappers/bin/openspec +33 -9
- package/wrappers/lib/graphify-gate.mjs +8 -0
- package/opencode-plugin/src/audience-layer.ts +0 -91
- package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
- package/src/runtime/audience-layer.ts +0 -84
- package/src/runtime/opencode-audience-layer.ts +0 -176
- package/src/tools/setup/audience-isolation.fixture.ts +0 -325
- package/src/tools/setup/audience-layer.ts +0 -207
- package/src/tools/setup/audience-registrations.ts +0 -52
- package/src/tools/setup/contribution-ledger.ts +0 -207
- package/src/tools/setup/isolation-diagnostics.ts +0 -139
- package/src/tools/setup/runtime-documents.ts +0 -291
- package/src/tools/setup/upgrade-sweep.ts +0 -153
|
@@ -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
|
);
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { MATE_ENV } from "./env-names";
|
|
5
|
+
import { FRAMEWORK_NAME } from "./framework";
|
|
6
|
+
import {
|
|
7
|
+
findCompanionBlock,
|
|
8
|
+
findManagedBlocks,
|
|
9
|
+
managedBlockPluginId,
|
|
10
|
+
stripManagedBlocks,
|
|
11
|
+
type ManagedBlock,
|
|
12
|
+
} from "./managed-block";
|
|
13
|
+
import { joinLines, scanLinesWithoutComments, type ScannedLine } from "./markdown-scan";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Companion Instructions: the companion's own `AGENTS.md`, rendered for one
|
|
17
|
+
* Claude session. Read-only. The text mirrors what native loading of the
|
|
18
|
+
* companion `CLAUDE.md` (`@AGENTS.md`) yields: block-level HTML comments
|
|
19
|
+
* stripped, `AGENTS.override.md` not read. `@` lines stay literal, because
|
|
20
|
+
* expanding an import outside the project needs an approval a hook or `-p`
|
|
21
|
+
* session never gets.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export const COMPANION_INSTRUCTIONS_TAG = "companion-instructions";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Below Claude Code's 10,000-character cap on one hook's `additionalContext`;
|
|
28
|
+
* a longer value arrives as a file preview instead of in context.
|
|
29
|
+
*/
|
|
30
|
+
export const COMPANION_INSTRUCTIONS_BUDGET = 9_000;
|
|
31
|
+
|
|
32
|
+
/** Where the Claude Runtime Surface writes its `mate:managed` blocks; those in `AGENTS.md` are OpenCode's. */
|
|
33
|
+
const CLAUDE_BLOCK_FILE = "CLAUDE.md";
|
|
34
|
+
|
|
35
|
+
export interface CompanionInstructionsInput {
|
|
36
|
+
companionPath: string;
|
|
37
|
+
repository: { id: string; path: string };
|
|
38
|
+
wrapperBinPath: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type CompanionInstructions =
|
|
42
|
+
| {
|
|
43
|
+
kind: "available";
|
|
44
|
+
/** Absolute path of the companion `AGENTS.md`. */
|
|
45
|
+
source: string;
|
|
46
|
+
text: string;
|
|
47
|
+
omitted: string[];
|
|
48
|
+
/** The `MATE:COMPANION` block alone overruns the budget; it is never cut, so every other segment is omitted. */
|
|
49
|
+
oversized: boolean;
|
|
50
|
+
}
|
|
51
|
+
| { kind: "unavailable"; source: string; reason: string };
|
|
52
|
+
|
|
53
|
+
export type InstructionReader = (input: CompanionInstructionsInput) => CompanionInstructions;
|
|
54
|
+
|
|
55
|
+
export function companionInstructionsUnavailableLine(
|
|
56
|
+
result: Extract<CompanionInstructions, { kind: "unavailable" }>,
|
|
57
|
+
): string {
|
|
58
|
+
return `Companion instructions unavailable: ${result.source} ${result.reason}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface CompanionInstructionsDeps {
|
|
62
|
+
isFile?: (filePath: string) => boolean;
|
|
63
|
+
readFile?: (filePath: string) => string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function defaultIsFile(filePath: string): boolean {
|
|
67
|
+
return fs.statSync(filePath).isFile();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function defaultReadFile(filePath: string): string {
|
|
71
|
+
return fs.readFileSync(filePath, "utf8");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Only a regular file is opened: a FIFO would block the read, and a hook that never returns stalls the session. */
|
|
75
|
+
function readText(
|
|
76
|
+
filePath: string,
|
|
77
|
+
deps: CompanionInstructionsDeps,
|
|
78
|
+
): { text: string } | { reason: string } {
|
|
79
|
+
const isFile = deps.isFile ?? defaultIsFile;
|
|
80
|
+
const readFile = deps.readFile ?? defaultReadFile;
|
|
81
|
+
try {
|
|
82
|
+
if (!isFile(filePath)) return { reason: "is not a regular file" };
|
|
83
|
+
return { text: readFile(filePath).replace(/\r\n?/g, "\n") };
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
86
|
+
if (code === "ENOENT") return { reason: "not found" };
|
|
87
|
+
return { reason: `could not be read (${code ?? (error as Error).message})` };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function fillPlaceholders(text: string, input: CompanionInstructionsInput): string {
|
|
92
|
+
const values: Record<string, string> = {
|
|
93
|
+
[MATE_ENV.repositoryPath]: input.repository.path,
|
|
94
|
+
[MATE_ENV.repositoryId]: input.repository.id,
|
|
95
|
+
[MATE_ENV.companionPath]: input.companionPath,
|
|
96
|
+
[MATE_ENV.wrapperBinPath]: input.wrapperBinPath,
|
|
97
|
+
};
|
|
98
|
+
return text.replace(
|
|
99
|
+
/\$\{(MATE_\w+)\}|\$(MATE_\w+)/g,
|
|
100
|
+
(match, braced?: string, bare?: string) => values[(braced ?? bare)!] ?? match,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface Segment {
|
|
105
|
+
/** The `##` heading, or a description for text no heading introduces. */
|
|
106
|
+
name: string;
|
|
107
|
+
text: string;
|
|
108
|
+
/** The `MATE:COMPANION` block: always kept whole. */
|
|
109
|
+
required: boolean;
|
|
110
|
+
source: string;
|
|
111
|
+
/** Shared by the two halves of a section the block sits in, so they are kept or omitted together. */
|
|
112
|
+
section: object;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const UNTITLED = "text before the first section";
|
|
116
|
+
/** An empty `##` heading still opens a section, which the omission line must name. */
|
|
117
|
+
const EMPTY_HEADING = "untitled section";
|
|
118
|
+
|
|
119
|
+
function splitSections(text: string, source: string): Segment[] {
|
|
120
|
+
const sections: { name: string; lines: ScannedLine[] }[] = [{ name: UNTITLED, lines: [] }];
|
|
121
|
+
for (const line of scanLinesWithoutComments(text)) {
|
|
122
|
+
if (line.heading !== null) sections.push({ name: line.heading || EMPTY_HEADING, lines: [] });
|
|
123
|
+
sections.at(-1)!.lines.push(line);
|
|
124
|
+
}
|
|
125
|
+
return sections.map(({ name, lines }) => ({
|
|
126
|
+
name,
|
|
127
|
+
text: joinLines(lines),
|
|
128
|
+
required: false,
|
|
129
|
+
source,
|
|
130
|
+
section: {},
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The block's markers are found before the scan, which would strip them as comments. */
|
|
135
|
+
function segmentAgents(text: string, source: string): Segment[] {
|
|
136
|
+
const found = findCompanionBlock(text, FRAMEWORK_NAME);
|
|
137
|
+
if (!found) return splitSections(text, source).filter((part) => part.text.length > 0);
|
|
138
|
+
|
|
139
|
+
const before = splitSections(text.slice(0, found.start), source);
|
|
140
|
+
const after = splitSections(text.slice(found.end), source);
|
|
141
|
+
/** The text after the block, up to the next heading, continues the section the block opened in. */
|
|
142
|
+
const enclosing = before.at(-1)!;
|
|
143
|
+
after[0] = { ...after[0]!, name: enclosing.name, section: enclosing.section };
|
|
144
|
+
const block: Segment = {
|
|
145
|
+
name: `${FRAMEWORK_NAME.toUpperCase()}:COMPANION block`,
|
|
146
|
+
text: joinLines(scanLinesWithoutComments(text.slice(found.start, found.end))),
|
|
147
|
+
required: true,
|
|
148
|
+
source,
|
|
149
|
+
section: {},
|
|
150
|
+
};
|
|
151
|
+
return [...before, block, ...after].filter((part) => part.text.length > 0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function segmentBlocks(
|
|
155
|
+
blocks: readonly ManagedBlock[],
|
|
156
|
+
source: string,
|
|
157
|
+
input: CompanionInstructionsInput,
|
|
158
|
+
): Segment[] {
|
|
159
|
+
return blocks
|
|
160
|
+
.map((block) => ({
|
|
161
|
+
name: `${path.basename(source)} block ${managedBlockPluginId(block.key)}`,
|
|
162
|
+
text: joinLines(scanLinesWithoutComments(fillPlaceholders(block.body, input))),
|
|
163
|
+
required: false,
|
|
164
|
+
source,
|
|
165
|
+
section: {},
|
|
166
|
+
}))
|
|
167
|
+
.filter((part) => part.text.length > 0);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function segment(
|
|
171
|
+
agents: string,
|
|
172
|
+
source: string,
|
|
173
|
+
input: CompanionInstructionsInput,
|
|
174
|
+
deps: CompanionInstructionsDeps,
|
|
175
|
+
): Segment[] {
|
|
176
|
+
const own = stripManagedBlocks(agents, FRAMEWORK_NAME);
|
|
177
|
+
const blockSource = path.join(input.companionPath, CLAUDE_BLOCK_FILE);
|
|
178
|
+
const read = readText(blockSource, deps);
|
|
179
|
+
const blocks = "text" in read ? findManagedBlocks(read.text, FRAMEWORK_NAME) : [];
|
|
180
|
+
return [
|
|
181
|
+
...segmentAgents(fillPlaceholders(own, input), source),
|
|
182
|
+
...segmentBlocks(blocks, blockSource, input),
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function escapeAttribute(value: string): string {
|
|
187
|
+
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function render(source: string, segments: readonly Segment[], closing?: string): string {
|
|
191
|
+
const body = [...segments.map((part) => part.text), ...(closing ? [closing] : [])].join("\n\n");
|
|
192
|
+
return `<${COMPANION_INSTRUCTIONS_TAG} source="${escapeAttribute(source)}">\n${body}\n</${COMPANION_INSTRUCTIONS_TAG}>`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Hundreds of headings would otherwise make the omission line alone exceed the budget. */
|
|
196
|
+
const OMISSION_NAMES_CAP = 400;
|
|
197
|
+
const OMISSION_NAME_MAX = 80;
|
|
198
|
+
|
|
199
|
+
/** Cut by code point, so a heading of astral characters never ends in half a surrogate pair. */
|
|
200
|
+
function omissionName(name: string): string {
|
|
201
|
+
const points = [...name];
|
|
202
|
+
if (points.length <= OMISSION_NAME_MAX) return name;
|
|
203
|
+
return `${points.slice(0, OMISSION_NAME_MAX - 1).join("")}…`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function listOmitted(names: readonly string[], cap: number): string {
|
|
207
|
+
const shown: string[] = [];
|
|
208
|
+
for (const name of names.map(omissionName)) {
|
|
209
|
+
const listing = [...shown, name].join(", ");
|
|
210
|
+
if (shown.length > 0 && listing.length > cap) break;
|
|
211
|
+
shown.push(name);
|
|
212
|
+
}
|
|
213
|
+
const more = names.length - shown.length;
|
|
214
|
+
return more > 0 ? `${shown.join(", ")} and ${more} more` : shown.join(", ");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function omissionLine(listing: string, sources: readonly string[]): string {
|
|
218
|
+
return `Sections not included here: ${listing}. Full text: ${sources.join(" and ")}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Capped at `room` too: a block that leaves little room gets a count, not a listing that carries the result past Claude Code's hook cap. */
|
|
222
|
+
function closingLine(names: readonly string[], sources: readonly string[], room: number): string {
|
|
223
|
+
const fixed = omissionLine("", sources).length + ` and ${names.length} more`.length;
|
|
224
|
+
return omissionLine(listOmitted(names, Math.min(OMISSION_NAMES_CAP, room - fixed)), sources);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function fullTextSources(source: string, parts: readonly Segment[]): string[] {
|
|
228
|
+
const others = new Set(parts.map((part) => part.source).filter((file) => file !== source));
|
|
229
|
+
return [source, ...others];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The longest omission line any subset of `optional` can produce. A subset
|
|
234
|
+
* lists at most the capped names, and its count of unlisted names, which only
|
|
235
|
+
* appears once the full listing passes the cap, has at most as many digits as
|
|
236
|
+
* the whole.
|
|
237
|
+
*/
|
|
238
|
+
function omissionReserve(sections: readonly Segment[][], source: string): number {
|
|
239
|
+
const listed = sections.map((parts) => omissionName(parts[0]!.name)).join(", ").length;
|
|
240
|
+
const more = listed > OMISSION_NAMES_CAP ? ` and ${sections.length} more`.length : 0;
|
|
241
|
+
const fixed = omissionLine("", fullTextSources(source, sections.flat())).length;
|
|
242
|
+
return fixed + Math.min(listed, OMISSION_NAMES_CAP) + more;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** The segments other than the block, grouped by section in document order. */
|
|
246
|
+
function optionalSections(segments: readonly Segment[]): Segment[][] {
|
|
247
|
+
const sections = new Map<object, Segment[]>();
|
|
248
|
+
for (const part of segments) {
|
|
249
|
+
if (part.required) continue;
|
|
250
|
+
const parts = sections.get(part.section);
|
|
251
|
+
if (parts) parts.push(part);
|
|
252
|
+
else sections.set(part.section, [part]);
|
|
253
|
+
}
|
|
254
|
+
return [...sections.values()];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** The omission line's worst case is reserved before any section is added, so adding the real line never pushes a fitting result over. */
|
|
258
|
+
function fit(
|
|
259
|
+
source: string,
|
|
260
|
+
segments: readonly Segment[],
|
|
261
|
+
): { text: string; omitted: string[]; oversized: boolean } {
|
|
262
|
+
const whole = render(source, segments);
|
|
263
|
+
if (whole.length <= COMPANION_INSTRUCTIONS_BUDGET) {
|
|
264
|
+
return { text: whole, omitted: [], oversized: false };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const sections = optionalSections(segments);
|
|
268
|
+
const reserve = "#".repeat(omissionReserve(sections, source));
|
|
269
|
+
const kept = new Set<Segment>(segments.filter((part) => part.required));
|
|
270
|
+
let length = render(source, [...kept], reserve).length;
|
|
271
|
+
for (const parts of sections) {
|
|
272
|
+
const next = parts.reduce((sum, part) => sum + part.text.length + "\n\n".length, length);
|
|
273
|
+
if (next > COMPANION_INSTRUCTIONS_BUDGET) continue;
|
|
274
|
+
for (const part of parts) kept.add(part);
|
|
275
|
+
length = next;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const build = (): { text: string; omitted: string[] } => {
|
|
279
|
+
const chosen = segments.filter((part) => kept.has(part));
|
|
280
|
+
const omitted = sections.filter((parts) => !kept.has(parts[0]!));
|
|
281
|
+
const names = omitted.map((parts) => parts[0]!.name);
|
|
282
|
+
const body = render(source, chosen);
|
|
283
|
+
const closing =
|
|
284
|
+
omitted.length > 0
|
|
285
|
+
? closingLine(
|
|
286
|
+
names,
|
|
287
|
+
fullTextSources(source, omitted.flat()),
|
|
288
|
+
COMPANION_INSTRUCTIONS_BUDGET - body.length - "\n\n".length,
|
|
289
|
+
)
|
|
290
|
+
: undefined;
|
|
291
|
+
return { text: render(source, chosen, closing), omitted: names };
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
let result = build();
|
|
295
|
+
/**
|
|
296
|
+
* The reserve names every section; a dropped one may still fit beside the
|
|
297
|
+
* real, shorter line. Each section added shortens that line, so the pass
|
|
298
|
+
* repeats until none is added.
|
|
299
|
+
*/
|
|
300
|
+
for (let added = true; added;) {
|
|
301
|
+
added = false;
|
|
302
|
+
for (const parts of sections) {
|
|
303
|
+
if (result.text.length > COMPANION_INSTRUCTIONS_BUDGET || kept.has(parts[0]!)) continue;
|
|
304
|
+
for (const part of parts) kept.add(part);
|
|
305
|
+
const next = build();
|
|
306
|
+
if (next.text.length <= COMPANION_INSTRUCTIONS_BUDGET) {
|
|
307
|
+
result = next;
|
|
308
|
+
added = true;
|
|
309
|
+
} else for (const part of parts) kept.delete(part);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return { ...result, oversized: result.text.length > COMPANION_INSTRUCTIONS_BUDGET };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function buildCompanionInstructions(
|
|
316
|
+
input: CompanionInstructionsInput,
|
|
317
|
+
deps: CompanionInstructionsDeps = {},
|
|
318
|
+
): CompanionInstructions {
|
|
319
|
+
const source = path.join(input.companionPath, "AGENTS.md");
|
|
320
|
+
const read = readText(source, deps);
|
|
321
|
+
if (!("text" in read)) return { kind: "unavailable", source, reason: read.reason };
|
|
322
|
+
|
|
323
|
+
return { kind: "available", source, ...fit(source, segment(read.text, source, input, deps)) };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Text beyond the `@AGENTS.md` import, Mate-managed blocks and HTML comments:
|
|
328
|
+
* only a companion-scoped session reads it; the Companion Instructions leave it out.
|
|
329
|
+
*/
|
|
330
|
+
export function companionClaudeMdHasOwnText(
|
|
331
|
+
companionPath: string,
|
|
332
|
+
deps: CompanionInstructionsDeps = {},
|
|
333
|
+
): boolean {
|
|
334
|
+
const read = readText(path.join(companionPath, "CLAUDE.md"), deps);
|
|
335
|
+
if (!("text" in read)) return false;
|
|
336
|
+
return scanLinesWithoutComments(stripManagedBlocks(read.text, FRAMEWORK_NAME)).some((line) => {
|
|
337
|
+
const trimmed = line.text.trim();
|
|
338
|
+
return trimmed !== "" && trimmed !== "@AGENTS.md" && trimmed !== "@./AGENTS.md";
|
|
339
|
+
});
|
|
340
|
+
}
|
package/src/runtime/guidance.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Versioned companion guidance contract. The CLI builds the guidance data
|
|
5
3
|
* from its playbooks at launch time and delivers it through the
|
|
@@ -18,12 +16,6 @@ export type MateGuidanceFile = {
|
|
|
18
16
|
companionGuidance: string;
|
|
19
17
|
codebaseExplorationGuidance: string;
|
|
20
18
|
errors: string[];
|
|
21
|
-
/**
|
|
22
|
-
* The active audience's layer, for a launch that hosts its own server only.
|
|
23
|
-
* The plugin applies it only from a payload it trusts; a masked payload
|
|
24
|
-
* carries none, so the shared service and bare sessions never see one.
|
|
25
|
-
*/
|
|
26
|
-
audienceLayer?: string;
|
|
27
19
|
};
|
|
28
20
|
|
|
29
21
|
export type GuidanceValidation = {
|
|
@@ -84,21 +76,12 @@ export function validateGuidanceData(value: unknown): GuidanceValidation {
|
|
|
84
76
|
errors.push("injected codebase exploration guidance is malformed");
|
|
85
77
|
}
|
|
86
78
|
|
|
87
|
-
const audienceLayer =
|
|
88
|
-
typeof value.audienceLayer === "string" && path.isAbsolute(value.audienceLayer)
|
|
89
|
-
? value.audienceLayer
|
|
90
|
-
: undefined;
|
|
91
|
-
if (value.audienceLayer !== undefined && audienceLayer === undefined) {
|
|
92
|
-
errors.push("injected audience layer is not an absolute path");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
79
|
return {
|
|
96
80
|
guidance: {
|
|
97
81
|
version: GUIDANCE_FILE_VERSION,
|
|
98
82
|
companionGuidance,
|
|
99
83
|
codebaseExplorationGuidance,
|
|
100
84
|
errors,
|
|
101
|
-
...(audienceLayer ? { audienceLayer } : {}),
|
|
102
85
|
},
|
|
103
86
|
errors,
|
|
104
87
|
};
|