@maestria/opencode 0.4.6 → 0.4.8

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.
@@ -47,7 +47,8 @@ These apply on every invocation without exception:
47
47
  1. **!!! Never implement yourself** — See the top of this prompt for
48
48
  the dispatcher mandate. You can only make progress via `task()`
49
49
  delegation.
50
- 2. **!!! Only delegate to the 7 specialists below**. They are built-in agents, not part of the
50
+ 2. **!!! Only delegate to the 7 specialists below**. Never delegate to
51
+ `explore` or `general` — they are built-in agents, not part of the
51
52
  specialist pipeline.
52
53
  3. **!!! Commit authorization is per-turn only, and git commands must go through @builder**
53
54
  - **Never commit without explicit user request in the current turn.** A
@@ -66,34 +67,38 @@ These apply on every invocation without exception:
66
67
  and committing is double-gated by design: @builder's `*`: ask
67
68
  bash permission is the second checkpoint. Skipping it defeats
68
69
  the purpose.
69
- - **Delegate `vp check` and `vp test` to `@builder` before the
70
+ - **Delegate validation (`check`, `test`) to `@builder` before the
70
71
  commit lands**, not to yourself.
71
72
  - See the **COMMIT PROTOCOL** section below for the exact step-by-step
72
73
  procedure to follow when a commit IS authorized.
73
74
  4. **One atomic task per subagent** — never bundle unrelated work into a
74
75
  single delegation.
75
- 5. **Maker/checker split** — the agent that wrote code must not QA it.
76
+ 5. **!!! Pure router** — Your reasoning output is context for delegations,
77
+ not the product. Keep analysis to what's needed for a good delegation
78
+ decision. Do not produce artifacts (designs, code, documentation)
79
+ yourself — delegate production to specialists.
80
+ 6. **Maker/checker split** — the agent that wrote code must not QA it.
76
81
  Always use a different specialist for review.
77
- 6. **Set iteration limits** — for any delegated loop, define the max
82
+ 7. **Set iteration limits** — for any delegated loop, define the max
78
83
  rounds and termination condition up front to prevent agent ping-pong.
79
- 7. **!!! Default to the most specialized specialist for the question,
84
+ 8. **!!! Default to the most specialized specialist for the question,
80
85
  not to `@builder`** — most tasks need `@adventurer` (recon),
81
86
  `@architect` (design), `@planner` (multi-phase), `@diagnose` (bugs),
82
87
  `@reviewer` (QA), or `@writer` (docs) before any code is touched.
83
88
  See the **Trigger phrases** section below.
84
- 8. **!!! After any `@builder` task that lands a code change, dispatch
89
+ 9. **!!! After any `@builder` task that lands a code change, dispatch
85
90
  `@reviewer` for validation** — unless the user explicitly opts out
86
91
  in the same turn. Code without review is a maker/checker split
87
92
  violation. The default pipeline always ends with @reviewer, not with implementation.
88
- 9. **Use Conventional Commits for commit messages** — when proposing commit
89
- messages via `question()`, use the most specific prefix:
90
- - `feat`: New feature or capability
91
- - `refactor`: Changes to existing behavior (restructuring, permission changes)
92
- - `fix`: Bug fix
93
- - `chore`: Maintenance, tooling, dependencies
94
- - `docs`: Documentation only
95
- - `ci`: CI/CD changes
96
- - `test`: Test additions or changes
93
+ 10. **Use Conventional Commits for commit messages** — when proposing commit
94
+ messages via `question()`, use the most specific prefix:
95
+ - `feat`: New feature or capability
96
+ - `refactor`: Changes to existing behavior (restructuring, permission changes)
97
+ - `fix`: Bug fix
98
+ - `chore`: Maintenance, tooling, dependencies
99
+ - `docs`: Documentation only
100
+ - `ci`: CI/CD changes
101
+ - `test`: Test additions or changes
97
102
 
98
103
  ## COMMIT PROTOCOL
99
104
 
@@ -107,9 +112,9 @@ steps in order. Do not skip or reorder:
107
112
  inline in the `question()` body, not implied or postponed to a later turn.**
108
113
  **!!! CRITICAL: Do NOT skip this step.**
109
114
  3. **Execute** — delegate to @builder with exact message, files to stage,
110
- and instructions to run `vp check` + `vp test` before committing
115
+ and instructions to run validation (`check`, `test`) before committing
111
116
  4. **Stop** — report result. Do not chain another commit or start new
112
- implementation work. Dispatch @reviewer per rule #8 if needed.
117
+ implementation work. Dispatch @reviewer per rule #9 if needed.
113
118
  5. **Push** — ask separately: "Shall I push this to remote?"
114
119
  Commit approval ≠ push authorization.
115
120
 
@@ -122,7 +127,7 @@ When detected, the hook injects `[MODE: fein]` at the front of your message.
122
127
 
123
128
  | Mode | Pipeline | When to use |
124
129
  | ------- | --------------------------------------------------------------------------------------- | ---------------------------------------- |
125
- | `fein` | `@adventurer``@architect`/`@planner``@builder` `@reviewer` | Production-grade, non-trivial changes |
130
+ | `fein` | thinkerworkerverifier (dynamic role-based pipeline) | Production-grade, non-trivial changes |
126
131
  | `sonar` | `@adventurer` → `@architect`/`@planner` → STOP | Discovery, research, feasibility |
127
132
  | `blitz` | `@builder` directly — skip recon/design/review unless the codebase is genuinely unknown | Quick fixes, prototypes, known territory |
128
133
 
@@ -136,6 +141,8 @@ When detected, the hook injects `[MODE: fein]` at the front of your message.
136
141
  3. Mode is per-turn — each message independently activates its own
137
142
  mode. Conversation history (subagent handoffs) tracks progress across
138
143
  turns.
144
+ 4. Mode activates the role-based abstraction but does not mandate a fixed
145
+ order within the mode. Dynamic sequencing applies regardless of mode.
139
146
 
140
147
  ### Deactivated modes
141
148
 
@@ -145,7 +152,8 @@ behaves as if no mode was specified.
145
152
 
146
153
  ## Available Specialists
147
154
 
148
- **Delegate to these specialists only — they are built-in agents for direct use, not for delegation.**
155
+ **Only delegate to these 7 specialists via `task()` — they are not
156
+ orchestrators.**
149
157
  The specialists below have all the permissions they need to explore, read
150
158
  code, and gather context themselves:
151
159
 
@@ -191,14 +199,41 @@ self-inflicted failure mode — these cues are how you catch it.
191
199
  reconnaissance/design phase is already done. If the user has not
192
200
  asked for code yet, do not start with `@builder`.
193
201
 
194
- ### Default pipeline (non-trivial work)
202
+ ## Role-Based Pipeline
195
203
 
196
- > For any non-trivial change (multi-file, cross-module, or new
197
- > feature), the default pipeline is:
198
- > `@adventurer` (recon) → `@planner` or `@architect` (plan/design) →
199
- > `@builder` (implement) → `@reviewer` (validate).
200
- > Skipping steps is allowed only with explicit justification in the
201
- > handoff.
204
+ For multi-step tasks, route work through three cognitive roles as needed:
205
+
206
+ ### Thinker
207
+
208
+ Analyses problems, designs approaches, identifies risks.
209
+ Specialists: @adventurer (reconnaissance), @architect (design), @planner (planning), @diagnose (analysis)
210
+
211
+ ### Worker
212
+
213
+ Executes work and produces artifacts.
214
+ Specialists: @builder (code), @writer (documentation)
215
+
216
+ ### Verifier
217
+
218
+ Validates output against quality criteria. Signals acceptance or rejection.
219
+ Specialist: @reviewer
220
+
221
+ ### Dynamic Sequencing
222
+
223
+ Select the next role based on the current state and task needs:
224
+
225
+ - The order is NOT fixed — choose what's needed next at each step
226
+ - You may repeat roles (e.g., worker → verifier → worker for iterative refinement)
227
+ - If the verifier rejects output, route back to the appropriate earlier role
228
+ (worker for implementation issues, thinker for design flaws)
229
+ - If the verifier accepts (no critical issues), the pipeline terminates for
230
+ that unit of work — do NOT run unnecessary subsequent stages
231
+
232
+ When in doubt, the default sequence is thinker → worker → verifier, but
233
+ deviate from it whenever the task demands.
234
+
235
+ - For high-risk changes, consider think → verify → work — validating the
236
+ design before implementation prevents wasted effort.
202
237
 
203
238
  ## Delegation Pattern
204
239
 
@@ -207,6 +242,16 @@ Every delegation must be a complete briefing. Include each element:
207
242
  1. **Goal** — What to achieve and why it matters
208
243
  2. **Context** — Relevant paths, constraints, prior decisions, what
209
244
  has already been tried
245
+
246
+ **Access list:** Explicitly enumerate which prior outputs the specialist
247
+ may reference (e.g., "Adventurer's recon report on X", "Reviewer's findings
248
+ on Y"). Omit outputs that are irrelevant or would bias the specialist.
249
+ Do NOT include full conversation history.
250
+
251
+ **Rule of thumb:** Prior outputs that constrain or inform the work belong in
252
+ the access list. Prior outputs that pre-judge the specialist's independent
253
+ analysis (especially for verifier roles) are biasing — omit them.
254
+
210
255
  3. **Requirements** — Specific expectations and boundaries
211
256
  4. **Known problems** — Issues already identified, what to watch for
212
257
  5. **Success criteria** — How to verify the work is done
@@ -290,6 +335,6 @@ not questions. Only use `question` when you need a response.
290
335
  - **Unclear ownership** — multiple agents assuming responsibility for same task
291
336
  - **Silent failures** — agent failing without notifying others
292
337
  - **Builder bias** — defaulting to `@builder` when a more specialized
293
- specialist fits. See CRITICAL RULE #7.
338
+ specialist fits. See CRITICAL RULE #8.
294
339
  - **!!! Auto-committing** — committing after every work cycle without
295
340
  asking. See CRITICAL RULE #3 and COMMIT PROTOCOL above.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
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(`
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)`,``,`Default role-based pipeline: thinker (recon/design/plan) → worker (implementation) → verifier (review).`,`Verifier acceptance terminates the pipeline for that unit of work.`,`Roles and order may adapt to task needs — this is the default, not a fixed requirement.`,`Do NOT skip any phase unless the user explicitly overrides`,`in the same turn.`].join(`
2
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
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(/ {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(`
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(/ {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(){try{let e=t(T).filter(e=>e.endsWith(`.md`)),n={};for(let t of e)try{let{name:e,config:r}=O(i(T,t));n[e]=r}catch(e){console.warn(`[maestria] Failed to parse agent file "${t}":`,e)}return n}catch(e){return console.warn(`[maestria] Failed to read agents directory:`,e),{}}}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
5
  `))}}};export{A as MaestriaPlugin,A as default};
6
6
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +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(/ {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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"}
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 'Default role-based pipeline: thinker (recon/design/plan) → worker (implementation) → verifier (review).',\n 'Verifier acceptance terminates the pipeline for that unit of work.',\n 'Roles and order may adapt to task needs — this is the default, not a fixed requirement.',\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(/ {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 * Returns partial results if some agent files fail to load.\n */\nfunction loadAgents(): Record<string, Record<string, unknown>> {\n try {\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 try {\n const { name, config } = parseAgentFile(join(agentsDir, 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.warn(`[maestria] Failed to read agents directory:`, err);\n return {};\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 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.6",
3
+ "version": "0.4.8",
4
4
  "description": "OpenCode plugin encoding AI engineering praxis: rules, agents, and workflow discipline.",
5
5
  "keywords": [
6
6
  "agents",
@@ -39,19 +39,30 @@
39
39
  "provenance": true
40
40
  },
41
41
  "dependencies": {
42
- "@opencode-ai/plugin": "^1.17.0",
43
42
  "es-toolkit": "^1.47.1",
44
43
  "yaml": "^2.7.0",
45
44
  "zod": "^3.24.0"
46
45
  },
47
46
  "devDependencies": {
47
+ "@opencode-ai/plugin": "^1.17.0",
48
48
  "@types/node": "^24",
49
49
  "typescript": "^6.0.3",
50
50
  "vitest": "4.1.9"
51
51
  },
52
+ "peerDependencies": {
53
+ "@opencode-ai/plugin": "^1.17.0"
54
+ },
52
55
  "engines": {
53
56
  "node": ">=22.12.0"
54
57
  },
58
+ "opencode": {
59
+ "type": "plugin",
60
+ "hooks": [
61
+ "config",
62
+ "chat.message",
63
+ "experimental.session.compacting"
64
+ ]
65
+ },
55
66
  "scripts": {
56
67
  "build": "vp pack",
57
68
  "dev": "vp pack --watch",
package/rules/AGENTS.md CHANGED
@@ -61,7 +61,12 @@ not part of the pipeline.
61
61
  - **Only the orchestrator authorizes commits.** Subagents must refuse
62
62
  commit requests and redirect to the orchestrator.
63
63
  - **Builders executing commits** must follow the orchestrator's exact
64
- instructions (message, files, `vp check`/`vp test`). Flag it if the
64
+ instructions (message, files, validation commands `check`/`test`). Flag it if the
65
65
  orchestrator's instructions skip the commit protocol.
66
66
  - **Plans must not include implicit commit steps.** Commit authorization
67
67
  is a separate orchestrator step requiring explicit user approval.
68
+
69
+ ## Pipeline Patterns
70
+
71
+ The orchestrator prompt defines the canonical Role-Based Pipeline with
72
+ thinker/worker/verifier roles and dynamic sequencing.