@maestria/opencode 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import{escapeRegExp as e,merge as t}from"es-toolkit";import{readFileSync as n,readdirSync as r}from"fs";import{basename as i,join as a}from"path";import{parse as o}from"yaml";import{z as s}from"zod";import{readFileSync as c}from"node:fs";import{dirname as l,join as u,resolve as d}from"node:path";import{fileURLToPath as f}from"node:url";const p=s.enum([`fein`,`sonar`,`blitz`]),m=s.object({modes:s.object({disabledKeywords:s.array(p).optional()}).optional()}),h=l(f(import.meta.url)),g=d(h,`..`),_=u(g,`agents`),v=u(g,`agents`,`commands`),y=u(g,`rules`,`AGENTS.md`),b=[`fein`,`sonar`,`blitz`];function x(e){let t=c(d(v,`${e}.md`),`utf-8`),n=t.indexOf(`## MODE:`);return n===-1?t.replace(/\s+$/,``)+`
2
- `:t.slice(n).replace(/\s+$/,``)+`
3
- `}const S=new Proxy({},{get(e,t,n){if(typeof t==`string`&&b.includes(t)){if(!(t in e))try{e[t]=x(t)}catch(n){console.warn(`[maestria] Failed to load mode prompt "${t}":`,n),e[t]=``}return e[t]}return Reflect.get(e,t,n)}}),C={fein:`[MODE: fein]`,sonar:`[MODE: sonar]`,blitz:`[MODE: blitz]`},w={fein:3,sonar:2,blitz:1},T=/```[\s\S]*?```|`[^`]*`/g;function E(e){let t=[],n;for(;(n=T.exec(e))!==null;)t.push([n.index,n.index+n[0].length]);return t}function D(e,t){return t.some(([t,n])=>e>=t&&e<n)}function O(t){return RegExp(`\\b${e(t)}\\b`,`gi`)}function k(e,t){let n=E(e),r=t?new Set(Array.from(t).map(e=>e.toLowerCase())):void 0,i=null;for(let t of b){if(r?.has(t))continue;let a=O(t),o;for(;(o=a.exec(e))!==null;)D(o.index,n)||(i===null||w[t]>w[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:S[i.mode],marker:C[i.mode]}}function A(e,t){return(e.slice(0,t.index)+e.slice(t.index+t.keyword.length).replace(/^:\s*/,``)).replace(/ {2,}/g,` `).trim()}function j(e){return N(e)?S[e]:``}function M(e){return N(e)?C[e]:``}function N(e){return b.includes(e)}function P(e){let t=o(e);return{description:t.description||``,mode:t.mode||`subagent`,permission:t.permission||{},color:t.color,maxSteps:t.maxSteps?Number(t.maxSteps):void 0}}function F(e){let t=n(e,`utf-8`),r=i(e,`.md`),a=t.split(`---`);if(a.length<3)throw Error(`Invalid agent file: ${e} - missing frontmatter`);let o=P(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:r,config:c}}function I(){try{let e=r(_).filter(e=>e.endsWith(`.md`)),t={};for(let n of e)try{let{name:e,config:r}=F(a(_,n));t[e]=r}catch(e){console.warn(`[maestria] Failed to parse agent file "${n}":`,e)}return t}catch(e){throw console.error(`[maestria] Failed to read agents directory:`,e),Error(`[maestria] Failed to load agents from "${_}": `+(e instanceof Error?e.message:String(e)))}}const L=async(e,n)=>{let r=m.parse(n??{}),i=new Set((r.modes?.disabledKeywords??[]).map(e=>e.toLowerCase())),a=I();return{config:async e=>{e.agent=t(e.agent??{},a),e.instructions=[...e.instructions??[],y]},"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 r=k(n.text,i);r&&(n.text=[M(r.mode),``,j(r.mode),``,A(n.text,r)].join(`
4
- `))}}};export{L as MaestriaPlugin,L as default};
1
+ import{merge as e}from"es-toolkit";import{readFileSync as t,readdirSync as n}from"fs";import{basename as r,join as i}from"path";import{parse as a}from"yaml";import{z as o}from"zod";import{readFileSync as s}from"node:fs";import{dirname as c,join as l,resolve as u}from"node:path";import{fileURLToPath as d}from"node:url";const f=o.enum([`fein`,`sonar`,`blitz`]),p=o.object({modes:o.object({disabledKeywords:o.array(f).optional()}).optional()}),m=[`fein`,`sonar`,`blitz`],h={fein:`[MODE: fein]`,sonar:`[MODE: sonar]`,blitz:`[MODE: blitz]`},g={fein:3,sonar:2,blitz:1},_=/```[\s\S]*?```|`[^`]*`/g;function v(e){let t=[],n;for(_.lastIndex=0;(n=_.exec(e))!==null;)t.push([n.index,n.index+n[0].length]);return t}function y(e,t){return t.some(([t,n])=>e>=t&&e<n)}function b(e){let t=e.indexOf(`## MODE:`);return t===-1?e.replace(/\s+$/,``)+`
2
+ `:e.slice(t).replace(/\s+$/,``)+`
3
+ `}function x(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}function S(e){return RegExp(`\\b${x(e)}\\b`,`gi`)}function C(e){return m.includes(e)}function w(e){return C(e)?h[e]:``}function T(e,t){if(!e)return null;let n=v(e),r=t?new Set(Array.from(t).map(e=>e.toLowerCase())):void 0,i=null;for(let t of m){if(r?.has(t))continue;let a=S(t),o;for(;(o=a.exec(e))!==null;)y(o.index,n)||(i===null||g[t]>g[i.mode])&&(i={mode:t,keyword:o[0],index:o.index})}return i}function E(e,t){return(e.slice(0,t.index)+e.slice(t.index+t.keyword.length).replace(/^:\s*/,``)).replace(/ {2,}/g,` `).trim()}const D=c(d(import.meta.url)),O=u(D,`..`),k=l(O,`agents`),A=l(O,`agents`,`commands`),j=l(O,`rules`,`AGENTS.md`),M=[`fein`,`sonar`,`blitz`];function N(e){return b(s(u(A,`${e}.md`),`utf-8`))}const P=new Proxy({},{get(e,t,n){if(typeof t==`string`&&M.includes(t)){if(!(t in e))try{e[t]=N(t)}catch(n){console.warn(`[maestria] Failed to load mode prompt "${t}":`,n),e[t]=``}return e[t]}return Reflect.get(e,t,n)}}),F={fein:`[MODE: fein]`,sonar:`[MODE: sonar]`,blitz:`[MODE: blitz]`};function I(e,t){let n=T(e,t);return n===null?null:{mode:n.mode,keyword:n.keyword,index:n.index,prompt:P[n.mode],marker:F[n.mode]}}function L(e,t){return E(e,t)}function R(e){return B(e)?P[e]:``}function z(e){return B(e)?w(e):``}function B(e){return M.includes(e)}function V(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 H(e){let n=t(e,`utf-8`),i=r(e,`.md`),a=n.split(`---`);if(a.length<3)throw Error(`Invalid agent file: ${e} - missing frontmatter`);let o=V(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 U(){try{let e=n(k).filter(e=>e.endsWith(`.md`)),t={};for(let n of e)try{let{name:e,config:r}=H(i(k,n));t[e]=r}catch(e){console.warn(`[maestria] Failed to parse agent file "${n}":`,e)}return t}catch(e){throw console.error(`[maestria] Failed to read agents directory:`,e),Error(`[maestria] Failed to load agents from "${k}": `+(e instanceof Error?e.message:String(e)))}}const W=async(t,n)=>{let r=p.parse(n??{}),i=new Set((r.modes?.disabledKeywords??[]).map(e=>e.toLowerCase())),a=U();return{config:async t=>{t.agent=e(t.agent??{},a),t.instructions=[...t.instructions??[],j]},"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 r=I(n.text,i);r&&(n.text=[z(r.mode),``,R(r.mode),``,L(n.text,r)].join(`
4
+ `))}}};export{W as MaestriaPlugin,W as default};
5
5
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["join","readFileSync","parseYaml"],"sources":["../src/modes/types.ts","../src/root.ts","../src/modes/prompts.ts","../src/modes/index.ts","../src/index.ts"],"sourcesContent":["/**\n * Types for keyword-triggered workflow modes.\n *\n * @see ADR-OC-003 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 optional recon/design; required review remains)\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 { fileURLToPath } from 'node:url';\nimport { dirname, resolve, join } from 'node:path';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nexport const PACKAGE_ROOT = resolve(__dirname, '..');\nexport const AGENTS_DIR = join(PACKAGE_ROOT, 'agents');\nexport const COMMANDS_DIR = join(PACKAGE_ROOT, 'agents', 'commands');\nexport const RULES_PATH = join(PACKAGE_ROOT, 'rules', 'AGENTS.md');\n","import { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { COMMANDS_DIR } from '@/root.js';\nimport type { ModeKeyword } from '@/modes/types.js';\n\nconst VALID_KEYWORDS: readonly ModeKeyword[] = ['fein', 'sonar', 'blitz'];\n\nfunction loadModePrompt(name: string): string {\n const content = readFileSync(resolve(COMMANDS_DIR, `${name}.md`), 'utf-8');\n // Find the `## MODE:` heading which marks the start of the actual prompt text.\n // The synced command files start with an HTML comment (`<!-- Auto-generated... -->`),\n // not YAML frontmatter (`---`), so a frontmatter regex would never match.\n const modeIdx = content.indexOf('## MODE:');\n if (modeIdx !== -1) {\n return content.slice(modeIdx).replace(/\\s+$/, '') + '\\n';\n }\n return content.replace(/\\s+$/, '') + '\\n';\n}\n\n/**\n * Mode prompt text for each keyword, lazily loaded on first access.\n * If a prompt file is missing or unreadable, logs a warning and caches\n * an empty string - never throws at module evaluation time.\n *\n * @see ADR-OC-003 (section \"Mode Prompts\")\n */\nexport const MODE_PROMPTS: Record<ModeKeyword, string> = new Proxy(\n {} as Record<ModeKeyword, string>,\n {\n get(target, key, receiver) {\n if (typeof key === 'string' && (VALID_KEYWORDS as readonly string[]).includes(key)) {\n if (!(key in target)) {\n try {\n (target as Record<string, string>)[key] = loadModePrompt(key);\n } catch (e) {\n console.warn(`[maestria] Failed to load mode prompt \"${key}\":`, e);\n (target as Record<string, string>)[key] = '';\n }\n }\n return (target as Record<string, string>)[key as string];\n }\n return Reflect.get(target, key, receiver);\n },\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 { VALID_KEYWORDS };\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 optional ceremony; required review remains\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-OC-003 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-OC-003):\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(/ {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 { merge } from 'es-toolkit';\nimport { readFileSync, readdirSync } from 'fs';\nimport { join, basename } from 'path';\nimport { parse as parseYaml } from 'yaml';\nimport { type MaestriaPluginOptions, maestriaOptionsSchema } from '@/modes/types.js';\nimport { detectMode, stripKeyword, getModeMarker, getModePrompt } from '@/modes/index.js';\nimport { AGENTS_DIR, RULES_PATH } from '@/root.js';\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 * Returns partial results if some agent files fail to load.\n */\nfunction loadAgents(): Record<string, Record<string, unknown>> {\n try {\n const files = readdirSync(AGENTS_DIR).filter((f) => f.endsWith('.md'));\n const agents: Record<string, Record<string, unknown>> = {};\n\n for (const file of files) {\n try {\n const { name, config } = parseAgentFile(join(AGENTS_DIR, file));\n agents[name] = config;\n } catch (err) {\n console.warn(`[maestria] Failed to parse agent file \"${file}\":`, err);\n }\n }\n\n return agents;\n } catch (err) {\n console.error(`[maestria] Failed to read agents directory:`, err);\n throw new Error(\n `[maestria] Failed to load agents from \"${AGENTS_DIR}\": ` +\n (err instanceof Error ? err.message : String(err)),\n );\n }\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 // Deep-merge plugin agent defaults over the user's agent entries. A\n // shallow `{ ...input.agent, ...agents }` would replace each entry\n // wholesale, dropping user-set keys (model, variant, temperature) for\n // the 8 maestria agent names. Plugin defaults win on conflict; user\n // keys the plugin does not set survive.\n input.agent = merge(input.agent ?? {}, agents);\n input.instructions = [...(input.instructions ?? []), RULES_PATH];\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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"}
1
+ {"version":3,"file":"index.js","names":["MODE_MARKERS","isModeKeyword","getModeMarker","detectMode","stripKeyword","join","readFileSync","sharedDetectMode","sharedStripKeyword","sharedGetMarker","parseYaml"],"sources":["../src/modes/types.ts","../../shared/mode/src/index.ts","../src/root.ts","../src/modes/prompts.ts","../src/modes/index.ts","../src/index.ts"],"sourcesContent":["/**\n * Types for keyword-triggered workflow modes.\n *\n * @see ADR-OC-003 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 optional recon/design; required review remains)\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","/**\n * Private runtime-neutral shared mode mechanics.\n *\n * Pure TypeScript - no host SDKs, no filesystem APIs.\n * Consolidates the duplicated pure logic from:\n * - `packages/opencode/src/modes/index.ts`\n * - `packages/shared/pi/src/modes-core.ts`\n *\n * Consumers (OpenCode, shared-pi) delegate pure detection,\n * stripping, and mode-section extraction to this module while\n * keeping host-specific concerns (lazy file loading, session\n * state, command registration) locally.\n *\n * @module @maestria/shared-mode\n */\n\n// ── Constants ──\n\nexport const MODE_KEYWORDS = ['fein', 'sonar', 'blitz'] as const;\nexport type ModeKeyword = (typeof MODE_KEYWORDS)[number];\n\n/**\n * Backward-compatible alias used by OpenCode (`VALID_KEYWORDS`).\n */\nexport const VALID_KEYWORDS: readonly ModeKeyword[] = MODE_KEYWORDS;\n\nexport const MODE_MARKERS: Record<ModeKeyword, string> = {\n fein: '[MODE: fein]',\n sonar: '[MODE: sonar]',\n blitz: '[MODE: blitz]',\n};\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 optional ceremony; required review remains\n */\nexport const MODE_PRIORITY: Record<ModeKeyword, number> = {\n fein: 3,\n sonar: 2,\n blitz: 1,\n};\n\n// ── Types ──\n\n/**\n * Result of pure mode detection (no prompt/marker).\n * Host wrappers add prompt/marker and stripped text as needed.\n */\nexport interface ModeDetectPure {\n mode: ModeKeyword;\n keyword: string;\n index: number;\n}\n\n// ── Pure helpers ──\n\n/**\n * Regex matching fenced code blocks (```) and inline backtick spans (`).\n * Unclosed fenced blocks (``` without closing ```) are intentionally not\n * excluded - the regex requires matching fences. This is the accepted\n * false-positive behavior documented in ADR-OC-003.\n */\nexport const CODE_BLOCK_RE: RegExp = /```[\\s\\S]*?```|`[^`]*`/g;\n\nexport function findCodeBlockRanges(text: string): Array<[number, number]> {\n const ranges: Array<[number, number]> = [];\n let match: RegExpExecArray | null;\n // Reset lastIndex in case callers reuse the exported regex\n CODE_BLOCK_RE.lastIndex = 0;\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\nexport function isInRanges(index: number, ranges: Array<[number, number]>): boolean {\n return ranges.some(([start, end]) => index >= start && index < end);\n}\n\n/**\n * Extract the `## MODE:` section from a command file's content.\n * Mirrors the logic in both original modules: find `## MODE:`, slice\n * from there, trim trailing whitespace, append a single newline; if\n * no marker is found, normalize trailing whitespace the same way.\n */\nexport function extractModeSection(content: string): string {\n const modeIdx = content.indexOf('## MODE:');\n if (modeIdx !== -1) {\n return content.slice(modeIdx).replace(/\\s+$/, '') + '\\n';\n }\n return content.replace(/\\s+$/, '') + '\\n';\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction buildKeywordRegex(keyword: string): RegExp {\n return new RegExp(`\\\\b${escapeRegExp(keyword)}\\\\b`, 'gi');\n}\n\nfunction isModeKeyword(value: string): value is ModeKeyword {\n return (MODE_KEYWORDS as readonly string[]).includes(value);\n}\n\nexport function getModeMarker(mode: string): string {\n if (isModeKeyword(mode)) return MODE_MARKERS[mode];\n return '';\n}\n\n// ── Detection ──\n\n/**\n * Detect a workflow mode keyword in the given text.\n *\n * Rules (per ADR-OC-003):\n * - Word-boundary regex matching (`\\bfein\\b`, etc.), case-insensitive\n * - Most restrictive match wins (fein > sonar > blitz) regardless of position\n * - Disabled keywords (case-insensitive) are ignored\n * - Matches inside fenced code blocks (```) and inline backticks (`) are ignored\n * - Unclosed fences are not excluded (accepted false-positive)\n *\n * Returns the pure detection result (mode, matched keyword text, index) or null.\n */\nexport function detectMode(text: string, disabled?: Set<string>): ModeDetectPure | null {\n if (!text) return null;\n const codeRanges = findCodeBlockRanges(text);\n const normalizedDisabled = disabled\n ? new Set(Array.from(disabled).map((k) => k.toLowerCase()))\n : undefined;\n let best: ModeDetectPure | null = null;\n\n for (const keyword of MODE_KEYWORDS) {\n if (normalizedDisabled?.has(keyword)) continue;\n const regex = buildKeywordRegex(keyword);\n let match: RegExpExecArray | null;\n while ((match = regex.exec(text)) !== null) {\n if (isInRanges(match.index, codeRanges)) continue;\n if (best === null || MODE_PRIORITY[keyword] > MODE_PRIORITY[best.mode]) {\n best = { mode: keyword, keyword: match[0], index: match.index };\n }\n }\n }\n\n return best;\n}\n\n/**\n * Remove the matched keyword from the text, cleaning up a trailing colon\n * and collapsing double spaces.\n *\n * Mirrors both originals: strip a leading colon and surrounding whitespace\n * after the keyword, then collapse double spaces and trim.\n */\nexport function stripKeyword(text: string, result: { index: number; keyword: string }): string {\n const before = text.slice(0, result.index);\n const after = text.slice(result.index + result.keyword.length);\n const cleaned = after.replace(/^:\\s*/, '');\n return (before + cleaned).replace(/ {2,}/g, ' ').trim();\n}\n","import { fileURLToPath } from 'node:url';\nimport { dirname, resolve, join } from 'node:path';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nexport const PACKAGE_ROOT = resolve(__dirname, '..');\nexport const AGENTS_DIR = join(PACKAGE_ROOT, 'agents');\nexport const COMMANDS_DIR = join(PACKAGE_ROOT, 'agents', 'commands');\nexport const RULES_PATH = join(PACKAGE_ROOT, 'rules', 'AGENTS.md');\n","import { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { COMMANDS_DIR } from '@/root.js';\nimport { extractModeSection } from '@maestria/shared-mode';\nimport type { ModeKeyword } from '@/modes/types.js';\n\nconst VALID_KEYWORDS: readonly ModeKeyword[] = ['fein', 'sonar', 'blitz'];\n\nfunction loadModePrompt(name: string): string {\n const content = readFileSync(resolve(COMMANDS_DIR, `${name}.md`), 'utf-8');\n return extractModeSection(content);\n}\n\n/**\n * Mode prompt text for each keyword, lazily loaded on first access.\n * If a prompt file is missing or unreadable, logs a warning and caches\n * an empty string - never throws at module evaluation time.\n *\n * @see ADR-OC-003 (section \"Mode Prompts\")\n */\nexport const MODE_PROMPTS: Record<ModeKeyword, string> = new Proxy(\n {} as Record<ModeKeyword, string>,\n {\n get(target, key, receiver) {\n if (typeof key === 'string' && (VALID_KEYWORDS as readonly string[]).includes(key)) {\n if (!(key in target)) {\n try {\n (target as Record<string, string>)[key] = loadModePrompt(key);\n } catch (e) {\n console.warn(`[maestria] Failed to load mode prompt \"${key}\":`, e);\n (target as Record<string, string>)[key] = '';\n }\n }\n return (target as Record<string, string>)[key as string];\n }\n return Reflect.get(target, key, receiver);\n },\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 { VALID_KEYWORDS };\n","import {\n detectMode as sharedDetectMode,\n stripKeyword as sharedStripKeyword,\n getModeMarker as sharedGetMarker,\n} from '@maestria/shared-mode';\nimport { MODE_PROMPTS, MODE_MARKERS, VALID_KEYWORDS } from '@/modes/prompts.js';\nimport type { ModeKeyword, ModeResult } from '@/modes/types.js';\n\n/**\n * Detect a workflow mode keyword in the given text.\n *\n * Delegates pure detection (word-boundary, priority, code-block\n * exclusion, disabled-keyword handling, case-insensitivity) to\n * `@maestria/shared-mode` and augments with prompt/marker so the\n * existing public API and result shape are preserved.\n *\n * Behavior (per ADR-OC-003) is unchanged: most restrictive wins\n * (fein > sonar > blitz), code spans are excluded, unclosed fences\n * are not excluded (accepted false-positive).\n */\nexport function detectMode(text: string, disabled?: Set<string>): ModeResult | null {\n const pure = sharedDetectMode(text, disabled);\n if (pure === null) return null;\n return {\n mode: pure.mode,\n keyword: pure.keyword,\n index: pure.index,\n prompt: MODE_PROMPTS[pure.mode],\n marker: MODE_MARKERS[pure.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 */\nexport function stripKeyword(text: string, result: ModeResult): string {\n return sharedStripKeyword(text, result);\n}\n\n/**\n * Get the mode prompt text for a given mode name.\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 */\nexport function getModeMarker(mode: string): string {\n if (isModeKeyword(mode)) {\n return sharedGetMarker(mode);\n }\n return '';\n}\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 { merge } from 'es-toolkit';\nimport { readFileSync, readdirSync } from 'fs';\nimport { join, basename } from 'path';\nimport { parse as parseYaml } from 'yaml';\nimport { type MaestriaPluginOptions, maestriaOptionsSchema } from '@/modes/types.js';\nimport { detectMode, stripKeyword, getModeMarker, getModePrompt } from '@/modes/index.js';\nimport { AGENTS_DIR, RULES_PATH } from '@/root.js';\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 * Returns partial results if some agent files fail to load.\n */\nfunction loadAgents(): Record<string, Record<string, unknown>> {\n try {\n const files = readdirSync(AGENTS_DIR).filter((f) => f.endsWith('.md'));\n const agents: Record<string, Record<string, unknown>> = {};\n\n for (const file of files) {\n try {\n const { name, config } = parseAgentFile(join(AGENTS_DIR, file));\n agents[name] = config;\n } catch (err) {\n console.warn(`[maestria] Failed to parse agent file \"${file}\":`, err);\n }\n }\n\n return agents;\n } catch (err) {\n console.error(`[maestria] Failed to read agents directory:`, err);\n throw new Error(\n `[maestria] Failed to load agents from \"${AGENTS_DIR}\": ` +\n (err instanceof Error ? err.message : String(err)),\n );\n }\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 // Deep-merge plugin agent defaults over the user's agent entries. A\n // shallow `{ ...input.agent, ...agents }` would replace each entry\n // wholesale, dropping user-set keys (model, variant, temperature) for\n // the 8 maestria agent names. Plugin defaults win on conflict; user\n // keys the plugin does not set survive.\n input.agent = merge(input.agent ?? {}, agents);\n input.instructions = [...(input.instructions ?? []), RULES_PATH];\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.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "OpenCode plugin encoding AI engineering praxis: rules, agents, and workflow discipline.",
5
5
  "keywords": [
6
6
  "agents",
@@ -47,7 +47,8 @@
47
47
  "@opencode-ai/plugin": "^1.18.19",
48
48
  "@types/node": "^26.2.0",
49
49
  "typescript": "^7.0.2",
50
- "vitest": "4.1.11"
50
+ "vitest": "4.1.11",
51
+ "@maestria/shared-mode": "0.1.0"
51
52
  },
52
53
  "peerDependencies": {
53
54
  "@opencode-ai/plugin": "^1.18.4"