@maestria/opencode 0.4.2 → 0.4.5
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/agents/adventurer.md +5 -5
- package/agents/architect.md +6 -6
- package/agents/builder.md +8 -8
- package/agents/diagnose.md +9 -9
- package/agents/orchestrator.md +10 -10
- package/agents/planner.md +5 -5
- package/agents/reviewer.md +5 -5
- package/agents/writer.md +5 -5
- package/dist/index.d.ts +7 -3
- package/dist/index.js +6 -101
- package/dist/index.js.map +1 -0
- package/package.json +6 -6
- package/dist/modes/index.d.ts +0 -39
- package/dist/modes/index.js +0 -144
- package/dist/modes/prompts.d.ts +0 -17
- package/dist/modes/prompts.js +0 -46
- package/dist/modes/types.d.ts +0 -51
- package/dist/modes/types.js +0 -24
package/agents/adventurer.md
CHANGED
|
@@ -18,11 +18,11 @@ permission:
|
|
|
18
18
|
todowrite: allow
|
|
19
19
|
edit: deny
|
|
20
20
|
bash:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
'*': ask
|
|
22
|
+
'git log*': allow
|
|
23
|
+
'git diff*': allow
|
|
24
|
+
'git status*': allow
|
|
25
|
+
'which *': allow
|
|
26
26
|
---
|
|
27
27
|
|
|
28
28
|
You are a codebase reconnaissance agent.
|
package/agents/architect.md
CHANGED
|
@@ -14,12 +14,12 @@ permission:
|
|
|
14
14
|
skill: allow
|
|
15
15
|
edit: deny
|
|
16
16
|
bash:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
'*': ask
|
|
18
|
+
'git diff*': allow
|
|
19
|
+
'git log*': allow
|
|
20
|
+
'git status*': allow
|
|
21
|
+
'which *': allow
|
|
22
|
+
'npm view *': allow
|
|
23
23
|
---
|
|
24
24
|
|
|
25
25
|
You make architecture decisions systematically.
|
package/agents/builder.md
CHANGED
|
@@ -14,14 +14,14 @@ permission:
|
|
|
14
14
|
todowrite: allow
|
|
15
15
|
skill: allow
|
|
16
16
|
bash:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
'*': ask
|
|
18
|
+
'git status*': allow
|
|
19
|
+
'git diff*': allow
|
|
20
|
+
'git log*': allow
|
|
21
|
+
'npm test*': allow
|
|
22
|
+
'pnpm test*': allow
|
|
23
|
+
'npx tsc*': allow
|
|
24
|
+
'npm view *': allow
|
|
25
25
|
---
|
|
26
26
|
|
|
27
27
|
You are a focused implementation agent.
|
package/agents/diagnose.md
CHANGED
|
@@ -15,15 +15,15 @@ permission:
|
|
|
15
15
|
todowrite: allow
|
|
16
16
|
edit: ask
|
|
17
17
|
bash:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
'*': ask
|
|
19
|
+
'git status*': allow
|
|
20
|
+
'git diff*': allow
|
|
21
|
+
'git log*': allow
|
|
22
|
+
'git blame*': allow
|
|
23
|
+
'git show*': allow
|
|
24
|
+
'which *': allow
|
|
25
|
+
'env': allow
|
|
26
|
+
'pwd': allow
|
|
27
27
|
---
|
|
28
28
|
|
|
29
29
|
You trace bugs systematically.
|
package/agents/orchestrator.md
CHANGED
|
@@ -12,19 +12,19 @@ permission:
|
|
|
12
12
|
webfetch: deny
|
|
13
13
|
edit: deny
|
|
14
14
|
bash:
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
'*': deny
|
|
16
|
+
'npx --yes skills@latest *': allow
|
|
17
17
|
question: allow
|
|
18
18
|
todowrite: allow
|
|
19
19
|
task:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
'*': deny
|
|
21
|
+
'adventurer': allow
|
|
22
|
+
'architect': allow
|
|
23
|
+
'builder': allow
|
|
24
|
+
'diagnose': allow
|
|
25
|
+
'planner': allow
|
|
26
|
+
'reviewer': allow
|
|
27
|
+
'writer': allow
|
|
28
28
|
skill: allow
|
|
29
29
|
---
|
|
30
30
|
|
package/agents/planner.md
CHANGED
|
@@ -11,11 +11,11 @@ permission:
|
|
|
11
11
|
lsp: allow
|
|
12
12
|
edit: ask
|
|
13
13
|
bash:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
'*': ask
|
|
15
|
+
'git status*': allow
|
|
16
|
+
'git diff*': allow
|
|
17
|
+
'git log*': allow
|
|
18
|
+
'which *': allow
|
|
19
19
|
webfetch: allow
|
|
20
20
|
todowrite: allow
|
|
21
21
|
skill: allow
|
package/agents/reviewer.md
CHANGED
|
@@ -13,11 +13,11 @@ permission:
|
|
|
13
13
|
skill: allow
|
|
14
14
|
edit: deny
|
|
15
15
|
bash:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
'*': ask
|
|
17
|
+
'git status*': allow
|
|
18
|
+
'git diff*': allow
|
|
19
|
+
'git log*': allow
|
|
20
|
+
'git show*': allow
|
|
21
21
|
webfetch: allow
|
|
22
22
|
---
|
|
23
23
|
|
package/agents/writer.md
CHANGED
|
@@ -14,11 +14,11 @@ permission:
|
|
|
14
14
|
skill: allow
|
|
15
15
|
todowrite: allow
|
|
16
16
|
bash:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
'*': ask
|
|
18
|
+
'git status*': allow
|
|
19
|
+
'git diff*': allow
|
|
20
|
+
'git log*': allow
|
|
21
|
+
'npm view *': allow
|
|
22
22
|
---
|
|
23
23
|
|
|
24
24
|
You write documentation.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
declare const MaestriaPlugin: Plugin;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { MaestriaPlugin, MaestriaPlugin as default };
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,101 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const agentsDir = join(__dirname, "..", "agents");
|
|
9
|
-
const rulesPath = join(__dirname, "..", "rules", "AGENTS.md");
|
|
10
|
-
function parseFrontmatter(yamlStr) {
|
|
11
|
-
const result = parseYaml(yamlStr);
|
|
12
|
-
return {
|
|
13
|
-
description: result.description || "",
|
|
14
|
-
mode: result.mode || "subagent",
|
|
15
|
-
permission: result.permission || {},
|
|
16
|
-
color: result.color,
|
|
17
|
-
maxSteps: result.maxSteps ? Number(result.maxSteps) : undefined,
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Read an agent markdown file and split into frontmatter + prompt.
|
|
22
|
-
*/
|
|
23
|
-
function parseAgentFile(filePath) {
|
|
24
|
-
const content = readFileSync(filePath, "utf-8");
|
|
25
|
-
const name = basename(filePath, ".md");
|
|
26
|
-
// Split on ---
|
|
27
|
-
const parts = content.split("---");
|
|
28
|
-
if (parts.length < 3) {
|
|
29
|
-
throw new Error(`Invalid agent file: ${filePath} — missing frontmatter`);
|
|
30
|
-
}
|
|
31
|
-
const frontmatter = parseFrontmatter(parts[1].trim());
|
|
32
|
-
const prompt = parts.slice(2).join("---").trim();
|
|
33
|
-
const config = {
|
|
34
|
-
description: frontmatter.description,
|
|
35
|
-
mode: frontmatter.mode,
|
|
36
|
-
prompt,
|
|
37
|
-
permission: frontmatter.permission,
|
|
38
|
-
};
|
|
39
|
-
if (frontmatter.color)
|
|
40
|
-
config.color = frontmatter.color;
|
|
41
|
-
if (frontmatter.maxSteps)
|
|
42
|
-
config.maxSteps = frontmatter.maxSteps;
|
|
43
|
-
return { name, config };
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Load all agent configs from the bundled agents/ directory.
|
|
47
|
-
*/
|
|
48
|
-
function loadAgents() {
|
|
49
|
-
const files = readdirSync(agentsDir).filter((f) => f.endsWith(".md"));
|
|
50
|
-
const agents = {};
|
|
51
|
-
for (const file of files) {
|
|
52
|
-
const { name, config } = parseAgentFile(join(agentsDir, file));
|
|
53
|
-
agents[name] = config;
|
|
54
|
-
}
|
|
55
|
-
return agents;
|
|
56
|
-
}
|
|
57
|
-
export const MaestriaPlugin = async (_input, options) => {
|
|
58
|
-
// Validate and parse options with zod
|
|
59
|
-
const parsed = maestriaOptionsSchema.parse(options ?? {});
|
|
60
|
-
const disabledKeywords = new Set((parsed.modes?.disabledKeywords ?? []).map((k) => k.toLowerCase()));
|
|
61
|
-
const agents = loadAgents();
|
|
62
|
-
return {
|
|
63
|
-
config: async (input) => {
|
|
64
|
-
input.agent = {
|
|
65
|
-
...input.agent,
|
|
66
|
-
...agents,
|
|
67
|
-
};
|
|
68
|
-
input.instructions = [...(input.instructions ?? []), rulesPath];
|
|
69
|
-
},
|
|
70
|
-
"experimental.session.compacting": async (_input, output) => {
|
|
71
|
-
output.context.push("Session was compacted. Task tracking is maintained via todowrite. " +
|
|
72
|
-
"Active context (files, decisions, blockers) was captured before compaction. " +
|
|
73
|
-
"Continue where you left off.");
|
|
74
|
-
},
|
|
75
|
-
"chat.message": async (hookInput, hookOutput) => {
|
|
76
|
-
// Only fire for the orchestrator agent
|
|
77
|
-
if (hookInput.agent !== "orchestrator")
|
|
78
|
-
return;
|
|
79
|
-
// Find the first text part with user content
|
|
80
|
-
const textPart = hookOutput.parts.find((p) => p.type === "text");
|
|
81
|
-
if (!textPart)
|
|
82
|
-
return;
|
|
83
|
-
// Detect keyword in the text
|
|
84
|
-
const result = detectMode(textPart.text, disabledKeywords);
|
|
85
|
-
if (!result)
|
|
86
|
-
return;
|
|
87
|
-
// Strip keyword from text and prepend mode marker + prompt inline.
|
|
88
|
-
// We embed everything in the existing text part rather than injecting
|
|
89
|
-
// a second text part into `parts`, because the OpenCode runtime does
|
|
90
|
-
// not handle multiple text parts per message (causes a hang).
|
|
91
|
-
textPart.text = [
|
|
92
|
-
getModeMarker(result.mode),
|
|
93
|
-
"",
|
|
94
|
-
getModePrompt(result.mode),
|
|
95
|
-
"",
|
|
96
|
-
stripKeyword(textPart.text, result),
|
|
97
|
-
].join("\n");
|
|
98
|
-
},
|
|
99
|
-
};
|
|
100
|
-
};
|
|
101
|
-
export default MaestriaPlugin;
|
|
1
|
+
import{readFileSync as e,readdirSync as t}from"fs";import{basename as n,dirname as r,join as i}from"path";import{parse as a}from"yaml";import{fileURLToPath as o}from"url";import{z as s}from"zod";import{escapeRegExp as c}from"es-toolkit";const l=s.enum([`fein`,`sonar`,`blitz`]),u=s.object({modes:s.object({disabledKeywords:s.array(l).optional()}).optional()}),d={fein:[`## MODE: fein (Full Pipeline)`,``,`Execute the complete fein pipeline: mandatory reconnaissance`,`(@adventurer) → design/plan (@architect or @planner) →`,`implementation (@builder) → review (@reviewer).`,`Do NOT skip any phase unless the user explicitly overrides`,`in the same turn.`].join(`
|
|
2
|
+
`),sonar:[`## MODE: sonar (Research Only)`,``,`Research mode: reconnaissance and design only. Delegate to`,`@adventurer (recon) followed by @architect or @planner`,`(analysis/design). STOP after delivering findings and design.`,`Do NOT implement, write code, or create any production files.`].join(`
|
|
3
|
+
`),blitz:[`## MODE: blitz (Fast Implementation)`,``,`Speed mode: skip reconnaissance and design gates. Go directly`,`to @builder for implementation. Only use @adventurer if the`,`codebase context is genuinely unknown (not as a default step).`,`Skip @reviewer unless the user explicitly requests review.`].join(`
|
|
4
|
+
`)},f={fein:`[MODE: fein]`,sonar:`[MODE: sonar]`,blitz:`[MODE: blitz]`},p=[`fein`,`sonar`,`blitz`],m={fein:3,sonar:2,blitz:1},h=/```[\s\S]*?```|`[^`]*`/g;function g(e){let t=[],n;for(;(n=h.exec(e))!==null;)t.push([n.index,n.index+n[0].length]);return t}function _(e,t){return t.some(([t,n])=>e>=t&&e<n)}function v(e){return RegExp(`\\b${c(e)}\\b`,`gi`)}function y(e,t){let n=g(e),r=t?new Set(Array.from(t).map(e=>e.toLowerCase())):void 0,i=null;for(let t of p){if(r?.has(t))continue;let a=v(t),o;for(;(o=a.exec(e))!==null;)_(o.index,n)||(i===null||m[t]>m[i.mode])&&(i={keyword:o[0],index:o.index,mode:t})}return i===null?null:{mode:i.mode,keyword:i.keyword,index:i.index,prompt:d[i.mode],marker:f[i.mode]}}function b(e,t){return(e.slice(0,t.index)+e.slice(t.index+t.keyword.length).replace(/^:\s*/,``)).replace(/\s{2,}/g,` `).trim()}function x(e){return C(e)?d[e]:``}function S(e){return C(e)?f[e]:``}function C(e){return p.includes(e)}const w=r(o(import.meta.url)),T=i(w,`..`,`agents`),E=i(w,`..`,`rules`,`AGENTS.md`);function D(e){let t=a(e);return{description:t.description||``,mode:t.mode||`subagent`,permission:t.permission||{},color:t.color,maxSteps:t.maxSteps?Number(t.maxSteps):void 0}}function O(t){let r=e(t,`utf-8`),i=n(t,`.md`),a=r.split(`---`);if(a.length<3)throw Error(`Invalid agent file: ${t} — missing frontmatter`);let o=D(a[1].trim()),s=a.slice(2).join(`---`).trim(),c={description:o.description,mode:o.mode,prompt:s,permission:o.permission};return o.color&&(c.color=o.color),o.maxSteps&&(c.maxSteps=o.maxSteps),{name:i,config:c}}function k(){let e=t(T).filter(e=>e.endsWith(`.md`)),n={};for(let t of e){let{name:e,config:r}=O(i(T,t));n[e]=r}return n}const A=async(e,t)=>{let n=u.parse(t??{}),r=new Set((n.modes?.disabledKeywords??[]).map(e=>e.toLowerCase())),i=k();return{config:async e=>{e.agent={...e.agent,...i},e.instructions=[...e.instructions??[],E]},"experimental.session.compacting":async(e,t)=>{t.context.push(`Session was compacted. Task tracking is maintained via todowrite. Active context (files, decisions, blockers) was captured before compaction. Continue where you left off.`)},"chat.message":async(e,t)=>{if(e.agent!==`orchestrator`)return;let n=t.parts.find(e=>e.type===`text`);if(!n)return;let i=y(n.text,r);i&&(n.text=[S(i.mode),``,x(i.mode),``,b(n.text,i)].join(`
|
|
5
|
+
`))}}};export{A as MaestriaPlugin,A as default};
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["parseYaml"],"sources":["../src/modes/types.ts","../src/modes/prompts.ts","../src/modes/index.ts","../src/index.ts"],"sourcesContent":["/**\n * Types for keyword-triggered workflow modes.\n *\n * @see ADR-008 for full design context.\n */\n\nimport { z } from 'zod';\n\n/**\n * Valid mode keywords.\n *\n * - `\"fein\"` -- Full pipeline (recon -> design -> build -> review)\n * - `\"sonar\"` -- Research only (recon + design, stop before build)\n * - `\"blitz\"` -- Fast implementation (builder direct, skip recon/design/review)\n */\nexport const modeKeywordSchema = z.enum(['fein', 'sonar', 'blitz']);\nexport type ModeKeyword = z.infer<typeof modeKeywordSchema>;\n\n/**\n * Plugin-level options for @maestria/opencode.\n */\nexport const maestriaOptionsSchema = z.object({\n modes: z\n .object({\n disabledKeywords: z.array(modeKeywordSchema).optional(),\n })\n .optional(),\n});\nexport type MaestriaPluginOptions = z.infer<typeof maestriaOptionsSchema>;\n\n/**\n * Result returned when a mode keyword is detected in a message.\n */\nexport interface ModeResult {\n /** The resolved mode keyword (lowercase). */\n mode: ModeKeyword;\n /** The keyword string as matched in the original text. */\n keyword: string;\n /** The character index where the keyword starts in the original text. */\n index: number;\n /** The mode prompt text to inject. */\n prompt: string;\n /** The mode marker string like `[MODE: fein]`. */\n marker: string;\n}\n","import type { ModeKeyword } from '@/modes/types.js';\n\n/**\n * Mode prompt text for each keyword.\n * These are injected into the turn when a mode is detected.\n *\n * @see ADR-008 (section \"Mode Prompts\")\n */\nexport const MODE_PROMPTS: Record<ModeKeyword, string> = {\n fein: [\n '## MODE: fein (Full Pipeline)',\n '',\n 'Execute the complete fein pipeline: mandatory reconnaissance',\n '(@adventurer) → design/plan (@architect or @planner) →',\n 'implementation (@builder) → review (@reviewer).',\n 'Do NOT skip any phase unless the user explicitly overrides',\n 'in the same turn.',\n ].join('\\n'),\n\n sonar: [\n '## MODE: sonar (Research Only)',\n '',\n 'Research mode: reconnaissance and design only. Delegate to',\n '@adventurer (recon) followed by @architect or @planner',\n '(analysis/design). STOP after delivering findings and design.',\n 'Do NOT implement, write code, or create any production files.',\n ].join('\\n'),\n\n blitz: [\n '## MODE: blitz (Fast Implementation)',\n '',\n 'Speed mode: skip reconnaissance and design gates. Go directly',\n 'to @builder for implementation. Only use @adventurer if the',\n 'codebase context is genuinely unknown (not as a default step).',\n 'Skip @reviewer unless the user explicitly requests review.',\n ].join('\\n'),\n};\n\n/**\n * Marker strings for each mode keyword, used to signal the active mode.\n * Format: `[MODE: <keyword>]`\n */\nexport const MODE_MARKERS: Record<ModeKeyword, string> = {\n fein: '[MODE: fein]',\n sonar: '[MODE: sonar]',\n blitz: '[MODE: blitz]',\n};\n\n/**\n * Array of all valid mode keywords for runtime iteration.\n */\nexport const VALID_KEYWORDS: readonly ModeKeyword[] = ['fein', 'sonar', 'blitz'];\n","import { escapeRegExp } from 'es-toolkit';\nimport { MODE_PROMPTS, MODE_MARKERS, VALID_KEYWORDS } from '@/modes/prompts.js';\nimport type { ModeKeyword, ModeResult } from '@/modes/types.js';\n\n/**\n * Priority mapping for mode keyword restrictiveness.\n * Higher number = more restrictive = wins when multiple keywords are present.\n * fein (3): full pipeline with mandatory gates\n * sonar (2): research only, no code\n * blitz (1): fast implementation, skip all gates\n */\nconst MODE_PRIORITY: Record<ModeKeyword, number> = {\n fein: 3,\n sonar: 2,\n blitz: 1,\n};\n\n/**\n * Regex matching fenced code blocks (```) and inline backtick spans (`).\n * Used to exclude keyword matches inside code spans.\n */\n// Note: Unclosed fenced code blocks (``` without closing ```) are not\n// excluded — the regex requires matching fences. This is an accepted\n// false-positive risk (see ADR-008 consequences).\nconst CODE_BLOCK_RE = /```[\\s\\S]*?```|`[^`]*`/g;\n\n/**\n * Find ranges of code blocks and inline code spans in text.\n * Returns [start, end) positions. Keywords inside these ranges\n * are ignored during detection.\n */\nfunction findAllCodeBlockRanges(text: string): Array<[number, number]> {\n const ranges: Array<[number, number]> = [];\n let match: RegExpExecArray | null;\n while ((match = CODE_BLOCK_RE.exec(text)) !== null) {\n ranges.push([match.index, match.index + match[0].length]);\n }\n return ranges;\n}\n\nfunction isInRanges(index: number, ranges: Array<[number, number]>): boolean {\n return ranges.some(([start, end]) => index >= start && index < end);\n}\n\n/**\n * Build a regex pattern for word-boundary matching of the given keyword.\n *\n * The pattern uses `\\b` word boundaries to ensure we match whole words only,\n * and is case-insensitive so `Fein`, `FEIN`, `fein` all match.\n */\nfunction buildKeywordRegex(keyword: string): RegExp {\n return new RegExp(`\\\\b${escapeRegExp(keyword)}\\\\b`, 'gi');\n}\n\n/**\n * Detect a workflow mode keyword in the given text.\n *\n * Detection rules (per ADR-008):\n * - Word-boundary regex matching (`\\bfein\\b`, `\\bsonar\\b`, `\\bblitz\\b`)\n * - Most restrictive match wins (fein > sonar > blitz)\n * - Case-insensitive\n * - Disabled keywords are ignored\n * - Matches inside fenced code blocks (```) and inline backticks (`) are ignored\n *\n * @param text The user message to scan.\n * @param disabled Optional set of disabled mode keywords (lowercase).\n * @returns A `ModeResult` if a keyword was detected, or `null`.\n */\nexport function detectMode(text: string, disabled?: Set<string>): ModeResult | null {\n const codeRanges = findAllCodeBlockRanges(text);\n // Normalize disabled keywords to lowercase for case-insensitive comparison\n const normalizedDisabled = disabled\n ? new Set(Array.from(disabled).map((k) => k.toLowerCase()))\n : undefined;\n let bestMatch: { keyword: string; index: number; mode: ModeKeyword } | null = null;\n\n for (const keyword of VALID_KEYWORDS) {\n if (normalizedDisabled?.has(keyword)) continue;\n\n const regex = buildKeywordRegex(keyword);\n let match: RegExpExecArray | null;\n\n while ((match = regex.exec(text)) !== null) {\n if (isInRanges(match.index, codeRanges)) continue;\n // Most-restrictive wins: prefer higher-priority mode over position\n if (bestMatch === null || MODE_PRIORITY[keyword] > MODE_PRIORITY[bestMatch.mode]) {\n bestMatch = {\n keyword: match[0],\n index: match.index,\n mode: keyword,\n };\n }\n }\n }\n\n if (bestMatch === null) return null;\n\n return {\n mode: bestMatch.mode,\n keyword: bestMatch.keyword,\n index: bestMatch.index,\n prompt: MODE_PROMPTS[bestMatch.mode],\n marker: MODE_MARKERS[bestMatch.mode],\n };\n}\n\n/**\n * Remove the matched keyword from the text, cleaning up any trailing colon\n * or whitespace that may follow it.\n *\n * @param text The original message text.\n * @param result The `ModeResult` from `detectMode()`.\n * @returns The text with the keyword stripped.\n */\nexport function stripKeyword(text: string, result: ModeResult): string {\n const before = text.slice(0, result.index);\n const after = text.slice(result.index + result.keyword.length);\n\n // Remove any colon + optional whitespace after the keyword\n // (e.g. \"fein: do this\" -> \"do this\")\n const cleaned = after.replace(/^:\\s*/, '');\n\n // Collapse double spaces and trim both ends (handles keyword at start,\n // end, or middle of text, plus extra whitespace around colon)\n return (before + cleaned).replace(/\\s{2,}/g, ' ').trim();\n}\n\n/**\n * Get the mode prompt text for a given mode name.\n *\n * @param mode The mode keyword (e.g. \"fein\", \"sonar\", \"blitz\").\n * @returns The prompt string, or empty string if mode is unknown.\n */\nexport function getModePrompt(mode: string): string {\n if (isModeKeyword(mode)) {\n return MODE_PROMPTS[mode];\n }\n return '';\n}\n\n/**\n * Get the mode marker string for a given mode name.\n *\n * @param mode The mode keyword (e.g. \"fein\", \"sonar\", \"blitz\").\n * @returns The marker string (e.g. `[MODE: fein]`), or empty string if unknown.\n */\nexport function getModeMarker(mode: string): string {\n if (isModeKeyword(mode)) {\n return MODE_MARKERS[mode];\n }\n return '';\n}\n\n/**\n * Type guard to check if a string is a valid ModeKeyword.\n */\nfunction isModeKeyword(value: string): value is ModeKeyword {\n return (VALID_KEYWORDS as readonly string[]).includes(value);\n}\n","import type { Plugin } from '@opencode-ai/plugin';\nimport { readFileSync, readdirSync } from 'fs';\nimport { join, dirname, basename } from 'path';\nimport { parse as parseYaml } from 'yaml';\nimport { fileURLToPath } from 'url';\nimport { type MaestriaPluginOptions, maestriaOptionsSchema } from '@/modes/types.js';\nimport { detectMode, stripKeyword, getModeMarker, getModePrompt } from '@/modes/index.js';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst agentsDir = join(__dirname, '..', 'agents');\nconst rulesPath = join(__dirname, '..', 'rules', 'AGENTS.md');\n\ninterface AgentFrontmatter {\n description: string;\n mode: string;\n permission: Record<string, unknown>;\n color?: string;\n maxSteps?: number;\n}\n\nfunction parseFrontmatter(yamlStr: string): AgentFrontmatter {\n const result = parseYaml(yamlStr) as Record<string, unknown>;\n return {\n description: (result.description as string) || '',\n mode: (result.mode as string) || 'subagent',\n permission: (result.permission as Record<string, unknown>) || {},\n color: result.color as string | undefined,\n maxSteps: result.maxSteps ? Number(result.maxSteps) : undefined,\n };\n}\n\n/**\n * Read an agent markdown file and split into frontmatter + prompt.\n */\nfunction parseAgentFile(filePath: string): { name: string; config: Record<string, unknown> } {\n const content = readFileSync(filePath, 'utf-8');\n const name = basename(filePath, '.md');\n\n // Split on ---\n const parts = content.split('---');\n if (parts.length < 3) {\n throw new Error(`Invalid agent file: ${filePath} — missing frontmatter`);\n }\n\n const frontmatter = parseFrontmatter(parts[1].trim());\n const prompt = parts.slice(2).join('---').trim();\n\n const config: Record<string, unknown> = {\n description: frontmatter.description,\n mode: frontmatter.mode,\n prompt,\n permission: frontmatter.permission,\n };\n\n if (frontmatter.color) config.color = frontmatter.color;\n if (frontmatter.maxSteps) config.maxSteps = frontmatter.maxSteps;\n\n return { name, config };\n}\n\n/**\n * Load all agent configs from the bundled agents/ directory.\n */\nfunction loadAgents(): Record<string, Record<string, unknown>> {\n const files = readdirSync(agentsDir).filter((f) => f.endsWith('.md'));\n const agents: Record<string, Record<string, unknown>> = {};\n\n for (const file of files) {\n const { name, config } = parseAgentFile(join(agentsDir, file));\n agents[name] = config;\n }\n\n return agents;\n}\n\nexport const MaestriaPlugin: Plugin = async (_input, options?: MaestriaPluginOptions) => {\n // Validate and parse options with zod\n const parsed = maestriaOptionsSchema.parse(options ?? {});\n const disabledKeywords = new Set<string>(\n (parsed.modes?.disabledKeywords ?? []).map((k) => k.toLowerCase()),\n );\n const agents = loadAgents();\n\n return {\n config: async (input) => {\n input.agent = {\n ...input.agent,\n ...agents,\n };\n input.instructions = [...(input.instructions ?? []), rulesPath];\n },\n 'experimental.session.compacting': async (_input, output) => {\n output.context.push(\n 'Session was compacted. Task tracking is maintained via todowrite. ' +\n 'Active context (files, decisions, blockers) was captured before compaction. ' +\n 'Continue where you left off.',\n );\n },\n 'chat.message': async (hookInput, hookOutput) => {\n // Only fire for the orchestrator agent\n if (hookInput.agent !== 'orchestrator') return;\n\n // Find the first text part with user content\n const textPart = hookOutput.parts.find((p) => p.type === 'text') as\n | { text: string; type: 'text' }\n | undefined;\n if (!textPart) return;\n\n // Detect keyword in the text\n const result = detectMode(textPart.text, disabledKeywords);\n if (!result) return;\n\n // Strip keyword from text and prepend mode marker + prompt inline.\n // We embed everything in the existing text part rather than injecting\n // a second text part into `parts`, because the OpenCode runtime does\n // not handle multiple text parts per message (causes a hang).\n textPart.text = [\n getModeMarker(result.mode),\n '',\n getModePrompt(result.mode),\n '',\n stripKeyword(textPart.text, result),\n ].join('\\n');\n },\n };\n};\n\nexport default MaestriaPlugin;\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maestria/opencode",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "OpenCode plugin encoding AI engineering praxis: rules, agents, and workflow discipline.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -46,15 +46,15 @@
|
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^24",
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"vitest": "npm:@voidzero-dev/vite-plus-test@^0.1.24"
|
|
49
|
+
"typescript": "^6.0.3",
|
|
50
|
+
"vitest": "4.1.9"
|
|
52
51
|
},
|
|
53
52
|
"engines": {
|
|
54
53
|
"node": ">=22.12.0"
|
|
55
54
|
},
|
|
56
55
|
"scripts": {
|
|
57
|
-
"build": "
|
|
58
|
-
"
|
|
56
|
+
"build": "vp pack",
|
|
57
|
+
"dev": "vp pack --watch",
|
|
58
|
+
"test": "vp test"
|
|
59
59
|
}
|
|
60
60
|
}
|
package/dist/modes/index.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import type { ModeResult } from "./types.js";
|
|
2
|
-
/**
|
|
3
|
-
* Detect a workflow mode keyword in the given text.
|
|
4
|
-
*
|
|
5
|
-
* Detection rules (per ADR-008):
|
|
6
|
-
* - Word-boundary regex matching (`\bfein\b`, `\bsonar\b`, `\bblitz\b`)
|
|
7
|
-
* - Most restrictive match wins (fein > sonar > blitz)
|
|
8
|
-
* - Case-insensitive
|
|
9
|
-
* - Disabled keywords are ignored
|
|
10
|
-
* - Matches inside fenced code blocks (```) and inline backticks (`) are ignored
|
|
11
|
-
*
|
|
12
|
-
* @param text The user message to scan.
|
|
13
|
-
* @param disabled Optional set of disabled mode keywords (lowercase).
|
|
14
|
-
* @returns A `ModeResult` if a keyword was detected, or `null`.
|
|
15
|
-
*/
|
|
16
|
-
export declare function detectMode(text: string, disabled?: Set<string>): ModeResult | null;
|
|
17
|
-
/**
|
|
18
|
-
* Remove the matched keyword from the text, cleaning up any trailing colon
|
|
19
|
-
* or whitespace that may follow it.
|
|
20
|
-
*
|
|
21
|
-
* @param text The original message text.
|
|
22
|
-
* @param result The `ModeResult` from `detectMode()`.
|
|
23
|
-
* @returns The text with the keyword stripped.
|
|
24
|
-
*/
|
|
25
|
-
export declare function stripKeyword(text: string, result: ModeResult): string;
|
|
26
|
-
/**
|
|
27
|
-
* Get the mode prompt text for a given mode name.
|
|
28
|
-
*
|
|
29
|
-
* @param mode The mode keyword (e.g. "fein", "sonar", "blitz").
|
|
30
|
-
* @returns The prompt string, or empty string if mode is unknown.
|
|
31
|
-
*/
|
|
32
|
-
export declare function getModePrompt(mode: string): string;
|
|
33
|
-
/**
|
|
34
|
-
* Get the mode marker string for a given mode name.
|
|
35
|
-
*
|
|
36
|
-
* @param mode The mode keyword (e.g. "fein", "sonar", "blitz").
|
|
37
|
-
* @returns The marker string (e.g. `[MODE: fein]`), or empty string if unknown.
|
|
38
|
-
*/
|
|
39
|
-
export declare function getModeMarker(mode: string): string;
|
package/dist/modes/index.js
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { escapeRegExp } from "es-toolkit";
|
|
2
|
-
import { MODE_PROMPTS, MODE_MARKERS, VALID_KEYWORDS } from "./prompts.js";
|
|
3
|
-
/**
|
|
4
|
-
* Priority mapping for mode keyword restrictiveness.
|
|
5
|
-
* Higher number = more restrictive = wins when multiple keywords are present.
|
|
6
|
-
* fein (3): full pipeline with mandatory gates
|
|
7
|
-
* sonar (2): research only, no code
|
|
8
|
-
* blitz (1): fast implementation, skip all gates
|
|
9
|
-
*/
|
|
10
|
-
const MODE_PRIORITY = {
|
|
11
|
-
fein: 3,
|
|
12
|
-
sonar: 2,
|
|
13
|
-
blitz: 1,
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* Regex matching fenced code blocks (```) and inline backtick spans (`).
|
|
17
|
-
* Used to exclude keyword matches inside code spans.
|
|
18
|
-
*/
|
|
19
|
-
// Note: Unclosed fenced code blocks (``` without closing ```) are not
|
|
20
|
-
// excluded — the regex requires matching fences. This is an accepted
|
|
21
|
-
// false-positive risk (see ADR-008 consequences).
|
|
22
|
-
const CODE_BLOCK_RE = /```[\s\S]*?```|`[^`]*`/g;
|
|
23
|
-
/**
|
|
24
|
-
* Find ranges of code blocks and inline code spans in text.
|
|
25
|
-
* Returns [start, end) positions. Keywords inside these ranges
|
|
26
|
-
* are ignored during detection.
|
|
27
|
-
*/
|
|
28
|
-
function findAllCodeBlockRanges(text) {
|
|
29
|
-
const ranges = [];
|
|
30
|
-
let match;
|
|
31
|
-
while ((match = CODE_BLOCK_RE.exec(text)) !== null) {
|
|
32
|
-
ranges.push([match.index, match.index + match[0].length]);
|
|
33
|
-
}
|
|
34
|
-
return ranges;
|
|
35
|
-
}
|
|
36
|
-
function isInRanges(index, ranges) {
|
|
37
|
-
return ranges.some(([start, end]) => index >= start && index < end);
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Build a regex pattern for word-boundary matching of the given keyword.
|
|
41
|
-
*
|
|
42
|
-
* The pattern uses `\b` word boundaries to ensure we match whole words only,
|
|
43
|
-
* and is case-insensitive so `Fein`, `FEIN`, `fein` all match.
|
|
44
|
-
*/
|
|
45
|
-
function buildKeywordRegex(keyword) {
|
|
46
|
-
return new RegExp(`\\b${escapeRegExp(keyword)}\\b`, "gi");
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Detect a workflow mode keyword in the given text.
|
|
50
|
-
*
|
|
51
|
-
* Detection rules (per ADR-008):
|
|
52
|
-
* - Word-boundary regex matching (`\bfein\b`, `\bsonar\b`, `\bblitz\b`)
|
|
53
|
-
* - Most restrictive match wins (fein > sonar > blitz)
|
|
54
|
-
* - Case-insensitive
|
|
55
|
-
* - Disabled keywords are ignored
|
|
56
|
-
* - Matches inside fenced code blocks (```) and inline backticks (`) are ignored
|
|
57
|
-
*
|
|
58
|
-
* @param text The user message to scan.
|
|
59
|
-
* @param disabled Optional set of disabled mode keywords (lowercase).
|
|
60
|
-
* @returns A `ModeResult` if a keyword was detected, or `null`.
|
|
61
|
-
*/
|
|
62
|
-
export function detectMode(text, disabled) {
|
|
63
|
-
const codeRanges = findAllCodeBlockRanges(text);
|
|
64
|
-
// Normalize disabled keywords to lowercase for case-insensitive comparison
|
|
65
|
-
const normalizedDisabled = disabled
|
|
66
|
-
? new Set(Array.from(disabled).map((k) => k.toLowerCase()))
|
|
67
|
-
: undefined;
|
|
68
|
-
let bestMatch = null;
|
|
69
|
-
for (const keyword of VALID_KEYWORDS) {
|
|
70
|
-
if (normalizedDisabled?.has(keyword))
|
|
71
|
-
continue;
|
|
72
|
-
const regex = buildKeywordRegex(keyword);
|
|
73
|
-
let match;
|
|
74
|
-
while ((match = regex.exec(text)) !== null) {
|
|
75
|
-
if (isInRanges(match.index, codeRanges))
|
|
76
|
-
continue;
|
|
77
|
-
// Most-restrictive wins: prefer higher-priority mode over position
|
|
78
|
-
if (bestMatch === null || MODE_PRIORITY[keyword] > MODE_PRIORITY[bestMatch.mode]) {
|
|
79
|
-
bestMatch = {
|
|
80
|
-
keyword: match[0],
|
|
81
|
-
index: match.index,
|
|
82
|
-
mode: keyword,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (bestMatch === null)
|
|
88
|
-
return null;
|
|
89
|
-
return {
|
|
90
|
-
mode: bestMatch.mode,
|
|
91
|
-
keyword: bestMatch.keyword,
|
|
92
|
-
index: bestMatch.index,
|
|
93
|
-
prompt: MODE_PROMPTS[bestMatch.mode],
|
|
94
|
-
marker: MODE_MARKERS[bestMatch.mode],
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Remove the matched keyword from the text, cleaning up any trailing colon
|
|
99
|
-
* or whitespace that may follow it.
|
|
100
|
-
*
|
|
101
|
-
* @param text The original message text.
|
|
102
|
-
* @param result The `ModeResult` from `detectMode()`.
|
|
103
|
-
* @returns The text with the keyword stripped.
|
|
104
|
-
*/
|
|
105
|
-
export function stripKeyword(text, result) {
|
|
106
|
-
const before = text.slice(0, result.index);
|
|
107
|
-
const after = text.slice(result.index + result.keyword.length);
|
|
108
|
-
// Remove any colon + optional whitespace after the keyword
|
|
109
|
-
// (e.g. "fein: do this" -> "do this")
|
|
110
|
-
const cleaned = after.replace(/^:\s*/, "");
|
|
111
|
-
// Collapse double spaces and trim both ends (handles keyword at start,
|
|
112
|
-
// end, or middle of text, plus extra whitespace around colon)
|
|
113
|
-
return (before + cleaned).replace(/\s{2,}/g, " ").trim();
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Get the mode prompt text for a given mode name.
|
|
117
|
-
*
|
|
118
|
-
* @param mode The mode keyword (e.g. "fein", "sonar", "blitz").
|
|
119
|
-
* @returns The prompt string, or empty string if mode is unknown.
|
|
120
|
-
*/
|
|
121
|
-
export function getModePrompt(mode) {
|
|
122
|
-
if (isModeKeyword(mode)) {
|
|
123
|
-
return MODE_PROMPTS[mode];
|
|
124
|
-
}
|
|
125
|
-
return "";
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Get the mode marker string for a given mode name.
|
|
129
|
-
*
|
|
130
|
-
* @param mode The mode keyword (e.g. "fein", "sonar", "blitz").
|
|
131
|
-
* @returns The marker string (e.g. `[MODE: fein]`), or empty string if unknown.
|
|
132
|
-
*/
|
|
133
|
-
export function getModeMarker(mode) {
|
|
134
|
-
if (isModeKeyword(mode)) {
|
|
135
|
-
return MODE_MARKERS[mode];
|
|
136
|
-
}
|
|
137
|
-
return "";
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Type guard to check if a string is a valid ModeKeyword.
|
|
141
|
-
*/
|
|
142
|
-
function isModeKeyword(value) {
|
|
143
|
-
return VALID_KEYWORDS.includes(value);
|
|
144
|
-
}
|
package/dist/modes/prompts.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { ModeKeyword } from "./types.js";
|
|
2
|
-
/**
|
|
3
|
-
* Mode prompt text for each keyword.
|
|
4
|
-
* These are injected into the turn when a mode is detected.
|
|
5
|
-
*
|
|
6
|
-
* @see ADR-008 (section "Mode Prompts")
|
|
7
|
-
*/
|
|
8
|
-
export declare const MODE_PROMPTS: Record<ModeKeyword, string>;
|
|
9
|
-
/**
|
|
10
|
-
* Marker strings for each mode keyword, used to signal the active mode.
|
|
11
|
-
* Format: `[MODE: <keyword>]`
|
|
12
|
-
*/
|
|
13
|
-
export declare const MODE_MARKERS: Record<ModeKeyword, string>;
|
|
14
|
-
/**
|
|
15
|
-
* Array of all valid mode keywords for runtime iteration.
|
|
16
|
-
*/
|
|
17
|
-
export declare const VALID_KEYWORDS: readonly ModeKeyword[];
|
package/dist/modes/prompts.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mode prompt text for each keyword.
|
|
3
|
-
* These are injected into the turn when a mode is detected.
|
|
4
|
-
*
|
|
5
|
-
* @see ADR-008 (section "Mode Prompts")
|
|
6
|
-
*/
|
|
7
|
-
export const MODE_PROMPTS = {
|
|
8
|
-
fein: [
|
|
9
|
-
"## MODE: fein (Full Pipeline)",
|
|
10
|
-
"",
|
|
11
|
-
"Execute the complete fein pipeline: mandatory reconnaissance",
|
|
12
|
-
"(@adventurer) → design/plan (@architect or @planner) →",
|
|
13
|
-
"implementation (@builder) → review (@reviewer).",
|
|
14
|
-
"Do NOT skip any phase unless the user explicitly overrides",
|
|
15
|
-
"in the same turn.",
|
|
16
|
-
].join("\n"),
|
|
17
|
-
sonar: [
|
|
18
|
-
"## MODE: sonar (Research Only)",
|
|
19
|
-
"",
|
|
20
|
-
"Research mode: reconnaissance and design only. Delegate to",
|
|
21
|
-
"@adventurer (recon) followed by @architect or @planner",
|
|
22
|
-
"(analysis/design). STOP after delivering findings and design.",
|
|
23
|
-
"Do NOT implement, write code, or create any production files.",
|
|
24
|
-
].join("\n"),
|
|
25
|
-
blitz: [
|
|
26
|
-
"## MODE: blitz (Fast Implementation)",
|
|
27
|
-
"",
|
|
28
|
-
"Speed mode: skip reconnaissance and design gates. Go directly",
|
|
29
|
-
"to @builder for implementation. Only use @adventurer if the",
|
|
30
|
-
"codebase context is genuinely unknown (not as a default step).",
|
|
31
|
-
"Skip @reviewer unless the user explicitly requests review.",
|
|
32
|
-
].join("\n"),
|
|
33
|
-
};
|
|
34
|
-
/**
|
|
35
|
-
* Marker strings for each mode keyword, used to signal the active mode.
|
|
36
|
-
* Format: `[MODE: <keyword>]`
|
|
37
|
-
*/
|
|
38
|
-
export const MODE_MARKERS = {
|
|
39
|
-
fein: "[MODE: fein]",
|
|
40
|
-
sonar: "[MODE: sonar]",
|
|
41
|
-
blitz: "[MODE: blitz]",
|
|
42
|
-
};
|
|
43
|
-
/**
|
|
44
|
-
* Array of all valid mode keywords for runtime iteration.
|
|
45
|
-
*/
|
|
46
|
-
export const VALID_KEYWORDS = ["fein", "sonar", "blitz"];
|
package/dist/modes/types.d.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Types for keyword-triggered workflow modes.
|
|
3
|
-
*
|
|
4
|
-
* @see ADR-008 for full design context.
|
|
5
|
-
*/
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
/**
|
|
8
|
-
* Valid mode keywords.
|
|
9
|
-
*
|
|
10
|
-
* - `"fein"` -- Full pipeline (recon -> design -> build -> review)
|
|
11
|
-
* - `"sonar"` -- Research only (recon + design, stop before build)
|
|
12
|
-
* - `"blitz"` -- Fast implementation (builder direct, skip recon/design/review)
|
|
13
|
-
*/
|
|
14
|
-
export declare const modeKeywordSchema: z.ZodEnum<["fein", "sonar", "blitz"]>;
|
|
15
|
-
export type ModeKeyword = z.infer<typeof modeKeywordSchema>;
|
|
16
|
-
/**
|
|
17
|
-
* Plugin-level options for @maestria/opencode.
|
|
18
|
-
*/
|
|
19
|
-
export declare const maestriaOptionsSchema: z.ZodObject<{
|
|
20
|
-
modes: z.ZodOptional<z.ZodObject<{
|
|
21
|
-
disabledKeywords: z.ZodOptional<z.ZodArray<z.ZodEnum<["fein", "sonar", "blitz"]>, "many">>;
|
|
22
|
-
}, "strip", z.ZodTypeAny, {
|
|
23
|
-
disabledKeywords?: ("fein" | "sonar" | "blitz")[] | undefined;
|
|
24
|
-
}, {
|
|
25
|
-
disabledKeywords?: ("fein" | "sonar" | "blitz")[] | undefined;
|
|
26
|
-
}>>;
|
|
27
|
-
}, "strip", z.ZodTypeAny, {
|
|
28
|
-
modes?: {
|
|
29
|
-
disabledKeywords?: ("fein" | "sonar" | "blitz")[] | undefined;
|
|
30
|
-
} | undefined;
|
|
31
|
-
}, {
|
|
32
|
-
modes?: {
|
|
33
|
-
disabledKeywords?: ("fein" | "sonar" | "blitz")[] | undefined;
|
|
34
|
-
} | undefined;
|
|
35
|
-
}>;
|
|
36
|
-
export type MaestriaPluginOptions = z.infer<typeof maestriaOptionsSchema>;
|
|
37
|
-
/**
|
|
38
|
-
* Result returned when a mode keyword is detected in a message.
|
|
39
|
-
*/
|
|
40
|
-
export interface ModeResult {
|
|
41
|
-
/** The resolved mode keyword (lowercase). */
|
|
42
|
-
mode: ModeKeyword;
|
|
43
|
-
/** The keyword string as matched in the original text. */
|
|
44
|
-
keyword: string;
|
|
45
|
-
/** The character index where the keyword starts in the original text. */
|
|
46
|
-
index: number;
|
|
47
|
-
/** The mode prompt text to inject. */
|
|
48
|
-
prompt: string;
|
|
49
|
-
/** The mode marker string like `[MODE: fein]`. */
|
|
50
|
-
marker: string;
|
|
51
|
-
}
|
package/dist/modes/types.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Types for keyword-triggered workflow modes.
|
|
3
|
-
*
|
|
4
|
-
* @see ADR-008 for full design context.
|
|
5
|
-
*/
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
/**
|
|
8
|
-
* Valid mode keywords.
|
|
9
|
-
*
|
|
10
|
-
* - `"fein"` -- Full pipeline (recon -> design -> build -> review)
|
|
11
|
-
* - `"sonar"` -- Research only (recon + design, stop before build)
|
|
12
|
-
* - `"blitz"` -- Fast implementation (builder direct, skip recon/design/review)
|
|
13
|
-
*/
|
|
14
|
-
export const modeKeywordSchema = z.enum(["fein", "sonar", "blitz"]);
|
|
15
|
-
/**
|
|
16
|
-
* Plugin-level options for @maestria/opencode.
|
|
17
|
-
*/
|
|
18
|
-
export const maestriaOptionsSchema = z.object({
|
|
19
|
-
modes: z
|
|
20
|
-
.object({
|
|
21
|
-
disabledKeywords: z.array(modeKeywordSchema).optional(),
|
|
22
|
-
})
|
|
23
|
-
.optional(),
|
|
24
|
-
});
|