@maestria/opencode 0.7.2 → 0.7.4
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 +9 -9
- package/agents/architect.md +8 -8
- package/agents/builder.md +8 -8
- package/agents/diagnose.md +10 -10
- package/agents/orchestrator.md +9 -9
- package/agents/planner.md +7 -7
- package/agents/reviewer.md +9 -9
- package/agents/writer.md +8 -8
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/rules/AGENTS.md +2 -1
package/agents/adventurer.md
CHANGED
|
@@ -8,15 +8,6 @@ description: |-
|
|
|
8
8
|
One role per session: exploration only - never implement or design.
|
|
9
9
|
mode: subagent
|
|
10
10
|
permission:
|
|
11
|
-
read: allow
|
|
12
|
-
glob: allow
|
|
13
|
-
grep: allow
|
|
14
|
-
lsp: allow
|
|
15
|
-
webfetch: allow
|
|
16
|
-
websearch: ask
|
|
17
|
-
skill: allow
|
|
18
|
-
todowrite: allow
|
|
19
|
-
edit: deny
|
|
20
11
|
bash:
|
|
21
12
|
"*": ask
|
|
22
13
|
ls*: allow
|
|
@@ -48,6 +39,15 @@ permission:
|
|
|
48
39
|
opensrc*: allow
|
|
49
40
|
agent-browser*: allow
|
|
50
41
|
rtk*: allow
|
|
42
|
+
edit: deny
|
|
43
|
+
glob: allow
|
|
44
|
+
grep: allow
|
|
45
|
+
lsp: allow
|
|
46
|
+
read: allow
|
|
47
|
+
skill: allow
|
|
48
|
+
todowrite: allow
|
|
49
|
+
webfetch: allow
|
|
50
|
+
websearch: ask
|
|
51
51
|
---
|
|
52
52
|
|
|
53
53
|
<!-- Auto-generated from @maestria/core. Do not edit directly.
|
package/agents/architect.md
CHANGED
|
@@ -5,14 +5,6 @@ description: |-
|
|
|
5
5
|
Use for: technology choices, implementation approaches, trade-off analysis.
|
|
6
6
|
mode: subagent
|
|
7
7
|
permission:
|
|
8
|
-
read: allow
|
|
9
|
-
glob: allow
|
|
10
|
-
grep: allow
|
|
11
|
-
lsp: allow
|
|
12
|
-
webfetch: allow
|
|
13
|
-
websearch: ask
|
|
14
|
-
skill: allow
|
|
15
|
-
edit: deny
|
|
16
8
|
bash:
|
|
17
9
|
"*": ask
|
|
18
10
|
ls*: allow
|
|
@@ -39,6 +31,14 @@ permission:
|
|
|
39
31
|
pnpm*: allow
|
|
40
32
|
npm*: allow
|
|
41
33
|
npm view *: allow
|
|
34
|
+
edit: deny
|
|
35
|
+
glob: allow
|
|
36
|
+
grep: allow
|
|
37
|
+
lsp: allow
|
|
38
|
+
read: allow
|
|
39
|
+
skill: allow
|
|
40
|
+
webfetch: allow
|
|
41
|
+
websearch: ask
|
|
42
42
|
---
|
|
43
43
|
|
|
44
44
|
<!-- Auto-generated from @maestria/core. Do not edit directly.
|
package/agents/builder.md
CHANGED
|
@@ -5,14 +5,6 @@ description: |-
|
|
|
5
5
|
Use for: targeted fixes, feature implementation, refactors, adding tests.
|
|
6
6
|
mode: subagent
|
|
7
7
|
permission:
|
|
8
|
-
read: allow
|
|
9
|
-
glob: allow
|
|
10
|
-
grep: allow
|
|
11
|
-
lsp: allow
|
|
12
|
-
edit: allow
|
|
13
|
-
webfetch: allow
|
|
14
|
-
todowrite: allow
|
|
15
|
-
skill: allow
|
|
16
8
|
bash:
|
|
17
9
|
ls*: allow
|
|
18
10
|
cat*: allow
|
|
@@ -43,6 +35,14 @@ permission:
|
|
|
43
35
|
eslint*: allow
|
|
44
36
|
prettier*: allow
|
|
45
37
|
"*": ask
|
|
38
|
+
edit: allow
|
|
39
|
+
glob: allow
|
|
40
|
+
grep: allow
|
|
41
|
+
lsp: allow
|
|
42
|
+
read: allow
|
|
43
|
+
skill: allow
|
|
44
|
+
todowrite: allow
|
|
45
|
+
webfetch: allow
|
|
46
46
|
---
|
|
47
47
|
|
|
48
48
|
<!-- Auto-generated from @maestria/core. Do not edit directly.
|
package/agents/diagnose.md
CHANGED
|
@@ -5,15 +5,6 @@ description: |-
|
|
|
5
5
|
Use for: cryptic errors, regressions, production bugs.
|
|
6
6
|
mode: subagent
|
|
7
7
|
permission:
|
|
8
|
-
read: allow
|
|
9
|
-
glob: allow
|
|
10
|
-
grep: allow
|
|
11
|
-
lsp: allow
|
|
12
|
-
webfetch: allow
|
|
13
|
-
websearch: ask
|
|
14
|
-
skill: allow
|
|
15
|
-
todowrite: allow
|
|
16
|
-
edit: allow
|
|
17
8
|
bash:
|
|
18
9
|
ls*: allow
|
|
19
10
|
cat*: allow
|
|
@@ -38,6 +29,15 @@ permission:
|
|
|
38
29
|
env: allow
|
|
39
30
|
pwd: allow
|
|
40
31
|
"*": ask
|
|
32
|
+
edit: allow
|
|
33
|
+
glob: allow
|
|
34
|
+
grep: allow
|
|
35
|
+
lsp: allow
|
|
36
|
+
read: allow
|
|
37
|
+
skill: allow
|
|
38
|
+
todowrite: allow
|
|
39
|
+
webfetch: allow
|
|
40
|
+
websearch: ask
|
|
41
41
|
---
|
|
42
42
|
|
|
43
43
|
<!-- Auto-generated from @maestria/core. Do not edit directly.
|
|
@@ -100,7 +100,7 @@ Fix the root cause with minimal changes:
|
|
|
100
100
|
|
|
101
101
|
Prevent similar bugs:
|
|
102
102
|
|
|
103
|
-
-
|
|
103
|
+
- Consider regression tests where a durable contract or plausible recurrence justifies them (per Global Rules testing judgment)
|
|
104
104
|
- Consider linting rules to catch the pattern
|
|
105
105
|
- Document the lesson in a knowledge artifact for future reference
|
|
106
106
|
|
package/agents/orchestrator.md
CHANGED
|
@@ -5,17 +5,16 @@ description: |-
|
|
|
5
5
|
Use for: multi-file features, cross-domain tasks, 3+ step workflows.
|
|
6
6
|
mode: all
|
|
7
7
|
permission:
|
|
8
|
-
read: deny
|
|
9
|
-
glob: deny
|
|
10
|
-
grep: deny
|
|
11
|
-
lsp: deny
|
|
12
|
-
webfetch: deny
|
|
13
|
-
edit: deny
|
|
14
8
|
bash:
|
|
15
9
|
"*": deny
|
|
16
10
|
"* npx --yes skills@latest *": allow
|
|
11
|
+
edit: deny
|
|
12
|
+
glob: deny
|
|
13
|
+
grep: deny
|
|
14
|
+
lsp: deny
|
|
17
15
|
question: allow
|
|
18
|
-
|
|
16
|
+
read: deny
|
|
17
|
+
skill: allow
|
|
19
18
|
task:
|
|
20
19
|
"*": deny
|
|
21
20
|
adventurer: allow
|
|
@@ -25,7 +24,8 @@ permission:
|
|
|
25
24
|
planner: allow
|
|
26
25
|
reviewer: allow
|
|
27
26
|
writer: allow
|
|
28
|
-
|
|
27
|
+
todowrite: allow
|
|
28
|
+
webfetch: deny
|
|
29
29
|
---
|
|
30
30
|
|
|
31
31
|
<!-- Auto-generated from @maestria/core. Do not edit directly.
|
|
@@ -97,7 +97,7 @@ Modes are case-insensitive and per-turn.
|
|
|
97
97
|
|
|
98
98
|
For implementation work, own the delivery path: inspect -> plan -> implement -> validate -> one independent review -> repair material blockers only when required -> targeted validation of repaired scope -> final verification -> commit -> push -> PR.
|
|
99
99
|
|
|
100
|
-
**Routine delivery is autonomous.** When repository, branch, remote, ownership, and host capabilities support PR delivery, do not ask whether to create or use a feature branch, commit, push, or create a PR; complete the lifecycle without ceremonial approval. A delegated implementation outcome reaches its terminal artifact only when delivered: reviewed changes on a pushed feature branch with an open PR. Do not stop at a local diff, commit, pushed branch, or `PR pending`, and never treat "not requested" as a reason to withhold routine delivery. Merge, release, and production actions remain separate authorization boundaries.
|
|
100
|
+
**Routine delivery is autonomous.** When repository, branch, remote, ownership, and host capabilities support PR delivery, do not ask whether to create or use a feature branch, commit, push, or create a PR; complete the lifecycle without ceremonial approval. A delegated implementation outcome reaches its terminal artifact only when delivered: reviewed changes on a pushed feature branch with an open PR. Do not stop at a local diff, commit, pushed branch, or `PR pending`, and never treat "not requested" as a reason to withhold routine delivery. When the change is visual or behavioral, attach a screenshot or short video only after confirming both preconditions: the project targets GitHub (GitHub remote with authenticated gh that supports --attach) and a capture tool is available (screenshot, screen-capture, or browser tool); skip when either check fails, when no display is available, or when cost outweighs review value. Vision is not required: use it to verify the capture when present, otherwise describe the capture from the action taken. Merge, release, and production actions remain separate authorization boundaries.
|
|
101
101
|
|
|
102
102
|
The parent session owns continuation until the selected implementation outcome reaches its terminal artifact. Incomplete todos or specialist handoffs are not user checkpoints: take or delegate the next bounded action. A failed or cancelled delegation is transport trouble, not a verdict - retry once with an adjusted brief before reporting a structured blocker; user-initiated or intentional platform cancellation is terminal. Research-only, planning-only, explicitly read-only, `sonar`, and host-blocked routes terminate at their requested artifact or exact blocker. Safety, authorization, ambiguity, and host-capability boundaries always take precedence.
|
|
103
103
|
|
package/agents/planner.md
CHANGED
|
@@ -5,11 +5,6 @@ description: |-
|
|
|
5
5
|
Use for: complex features requiring multi-phase execution, when the plan needs review before building.
|
|
6
6
|
mode: subagent
|
|
7
7
|
permission:
|
|
8
|
-
read: allow
|
|
9
|
-
glob: allow
|
|
10
|
-
grep: allow
|
|
11
|
-
lsp: allow
|
|
12
|
-
edit: ask
|
|
13
8
|
bash:
|
|
14
9
|
"*": ask
|
|
15
10
|
ls*: allow
|
|
@@ -36,9 +31,14 @@ permission:
|
|
|
36
31
|
mkdir*: allow
|
|
37
32
|
pnpm*: allow
|
|
38
33
|
npm*: allow
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
edit: ask
|
|
35
|
+
glob: allow
|
|
36
|
+
grep: allow
|
|
37
|
+
lsp: allow
|
|
38
|
+
read: allow
|
|
41
39
|
skill: allow
|
|
40
|
+
todowrite: allow
|
|
41
|
+
webfetch: allow
|
|
42
42
|
---
|
|
43
43
|
|
|
44
44
|
<!-- Auto-generated from @maestria/core. Do not edit directly.
|
package/agents/reviewer.md
CHANGED
|
@@ -6,12 +6,6 @@ description: |-
|
|
|
6
6
|
Use for: PR review, pre-commit review, architecture document review.
|
|
7
7
|
mode: subagent
|
|
8
8
|
permission:
|
|
9
|
-
read: allow
|
|
10
|
-
glob: allow
|
|
11
|
-
grep: allow
|
|
12
|
-
lsp: allow
|
|
13
|
-
skill: allow
|
|
14
|
-
edit: deny
|
|
15
9
|
bash:
|
|
16
10
|
"*": ask
|
|
17
11
|
ls*: allow
|
|
@@ -40,6 +34,12 @@ permission:
|
|
|
40
34
|
vp*: allow
|
|
41
35
|
rtk*: allow
|
|
42
36
|
node*: allow
|
|
37
|
+
edit: deny
|
|
38
|
+
glob: allow
|
|
39
|
+
grep: allow
|
|
40
|
+
lsp: allow
|
|
41
|
+
read: allow
|
|
42
|
+
skill: allow
|
|
43
43
|
webfetch: allow
|
|
44
44
|
---
|
|
45
45
|
|
|
@@ -101,9 +101,9 @@ The initial general reviewer must give a verdict for every category. A specializ
|
|
|
101
101
|
|
|
102
102
|
### 7. Test Coverage
|
|
103
103
|
|
|
104
|
-
-
|
|
105
|
-
- Do tests cover edge cases and error paths?
|
|
106
|
-
- Are tests meaningful (
|
|
104
|
+
- Is meaningful regression risk covered proportionate to stakes (per Global Rules testing judgment)?
|
|
105
|
+
- Do tests cover edge cases and error paths where the contract demands it?
|
|
106
|
+
- Are tests meaningful (observable behavior, not implementation details)?
|
|
107
107
|
|
|
108
108
|
### 8. Assumption Validation
|
|
109
109
|
|
package/agents/writer.md
CHANGED
|
@@ -5,14 +5,6 @@ description: |-
|
|
|
5
5
|
Use for: README files, API docs, architecture docs, changelogs, decision records.
|
|
6
6
|
mode: subagent
|
|
7
7
|
permission:
|
|
8
|
-
read: allow
|
|
9
|
-
glob: allow
|
|
10
|
-
grep: allow
|
|
11
|
-
lsp: allow
|
|
12
|
-
edit: allow
|
|
13
|
-
webfetch: allow
|
|
14
|
-
skill: allow
|
|
15
|
-
todowrite: allow
|
|
16
8
|
bash:
|
|
17
9
|
"*": ask
|
|
18
10
|
ls*: allow
|
|
@@ -41,6 +33,14 @@ permission:
|
|
|
41
33
|
npm view *: allow
|
|
42
34
|
vp*: allow
|
|
43
35
|
mkdir*: allow
|
|
36
|
+
edit: allow
|
|
37
|
+
glob: allow
|
|
38
|
+
grep: allow
|
|
39
|
+
lsp: allow
|
|
40
|
+
read: allow
|
|
41
|
+
skill: allow
|
|
42
|
+
todowrite: allow
|
|
43
|
+
webfetch: allow
|
|
44
44
|
---
|
|
45
45
|
|
|
46
46
|
<!-- Auto-generated from @maestria/core. Do not edit directly.
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import{
|
|
2
|
-
|
|
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"node:fs";import r from"node:path";import{parse as i}from"yaml";import{z as a}from"zod";const o=[`fein`,`sonar`,`blitz`],s={blitz:`[MODE: blitz]`,fein:`[MODE: fein]`,sonar:`[MODE: sonar]`},c={blitz:1,fein:3,sonar:2},l=/```[\s\S]*?```|`[^`]*`/gu,u=e=>{let t=[],n;for(l.lastIndex=0;(n=l.exec(e))!==null;)t.push([n.index,n.index+n[0].length]);return t},d=(e,t)=>t.some(([t,n])=>e>=t&&e<n),f=e=>{let t=e.indexOf(`## MODE:`);return t===-1?`${e.replace(/\s+$/u,``)}\n`:`${e.slice(t).replace(/\s+$/u,``)}\n`},p=e=>e.replaceAll(/[.*+?^${}()|[\]\\]/gu,`\\$&`),m=e=>RegExp(`\\b${p(e)}\\b`,`giu`),h=e=>o.includes(e),g=e=>h(e)?s[e]:``,_=(e,t)=>{if(!e)return null;let n=u(e),r=t?new Set([...t].map(e=>e.toLowerCase())):void 0,i=null;for(let t of o){if(r!=null&&r.has(t))continue;let a=m(t),o;for(;(o=a.exec(e))!==null;)d(o.index,n)||(i===null||c[t]>c[i.mode])&&(i={index:o.index,keyword:o[0],mode:t})}return i},v=(e,t)=>(e.slice(0,t.index)+e.slice(t.index+t.keyword.length).replace(/^:\s*/u,``)).replaceAll(/ {2,}/gu,` `).trim(),y=import.meta.dirname,b=r.resolve(y,`..`),x=r.join(b,`agents`),S=r.join(b,`agents`,`commands`),C=r.join(b,`rules`,`AGENTS.md`),w=[`fein`,`sonar`,`blitz`],T=e=>{let n=t(r.resolve(S,`${e}.md`),`utf-8`);return f(n)},E=e=>w.some(t=>t===e),D=new Proxy({},{get(e,t,n){if(typeof t==`string`&&E(t)){if(!(t in e))try{e[t]=T(t)}catch(n){console.warn(`[maestria] Failed to load mode prompt "${t}":`,n),e[t]=``}return e[t]}return Reflect.get(e,t,n)}}),O={blitz:`[MODE: blitz]`,fein:`[MODE: fein]`,sonar:`[MODE: sonar]`},k=e=>w.includes(e),A=(e,t)=>{let n=_(e,t);return n===null?null:{index:n.index,keyword:n.keyword,marker:O[n.mode],mode:n.mode,prompt:D[n.mode]}},j=(e,t)=>v(e,t),M=e=>k(e)?D[e]:``,N=e=>k(e)?g(e):``,P=a.enum([`fein`,`sonar`,`blitz`]),F=a.object({modes:a.object({disabledKeywords:a.array(P).optional()}).optional()}),I=e=>typeof e==`object`&&!!e&&!Array.isArray(e),L=e=>e===`all`||e===`primary`||e===`subagent`,R=e=>{if(!I(e))return{};let t={};for(let[n,r]of Object.entries(e))t[n]=r;return t},z=e=>{let t=i(e),n=I(t)?t:{};return{color:typeof n.color==`string`?n.color:void 0,description:typeof n.description==`string`?n.description:``,maxSteps:n.maxSteps!==void 0&&n.maxSteps!==null&&n.maxSteps!==``?Number(n.maxSteps):void 0,mode:L(n.mode)?n.mode:`subagent`,permission:R(n.permission)}},B=e=>{let n=t(e,`utf-8`),i=r.basename(e,`.md`),a=n.split(`---`);if(a.length<3)throw Error(`Invalid agent file: ${e} - missing frontmatter`);let o=z(a[1].trim()),s=a.slice(2).join(`---`).trim(),c={description:o.description,mode:o.mode,permission:o.permission,prompt:s};return o.color!==void 0&&o.color!==null&&o.color!==``&&(c.color=o.color),o.maxSteps!==void 0&&o.maxSteps!==null&&o.maxSteps!==0&&(c.maxSteps=o.maxSteps),{config:c,name:i}},V=()=>{try{let e=n(x).filter(e=>e.endsWith(`.md`)),t={};for(let n of e)try{let{name:e,config:i}=B(r.join(x,n));t[e]=i}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 "${x}": ${e instanceof Error?e.message:String(e)}`,{cause:e})}},H=(e,t,n)=>{if(e.agent!==`orchestrator`)return;let r=t.parts.find(e=>e.type===`text`);if(r===void 0)return;let i=A(r.text,n);i!==null&&(r.text=[N(i.mode),``,M(i.mode),``,j(r.text,i)].join(`
|
|
2
|
+
`))},U=(t,n)=>{t.agent=e(t.agent??{},n),t.instructions=[...t.instructions??[],C]},W=e=>{e.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.`)},G=async(e,t)=>{let n=F.parse(t??{}),r=new Set((n.modes?.disabledKeywords??[]).map(e=>e.toLowerCase())),i=V();return await Promise.resolve(),{"chat.message":async(e,t)=>{H(e,t,r),await Promise.resolve()},config:async e=>{U(e,i),await Promise.resolve()},"experimental.session.compacting":async(e,t)=>{W(t),await Promise.resolve()}}};export{G as MaestriaPlugin,G as default};
|
|
5
3
|
//# 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","isModeKeyword","sharedDetectMode","sharedStripKeyword","sharedGetMarker","parseYaml"],"sources":["../../shared/mode/src/index.ts","../src/root.ts","../src/modes/prompts.ts","../src/modes/index.ts","../src/modes/types.ts","../src/index.ts"],"sourcesContent":["/**\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 blitz: '[MODE: blitz]',\n fein: '[MODE: fein]',\n sonar: '[MODE: sonar]',\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 blitz: 1,\n fein: 3,\n sonar: 2,\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 = /```[\\s\\S]*?```|`[^`]*`/gu;\n\nexport const findCodeBlockRanges = (text: string): [number, number][] => {\n const ranges: [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 const isInRanges = (index: number, ranges: [number, number][]): boolean =>\n ranges.some(([start, end]) => index >= start && index < end);\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 const extractModeSection = (content: string): string => {\n const modeIdx = content.indexOf('## MODE:');\n if (modeIdx !== -1) {\n return `${content.slice(modeIdx).replace(/\\s+$/u, '')}\\n`;\n }\n return `${content.replace(/\\s+$/u, '')}\\n`;\n};\n\nconst escapeRegExp = (value: string): string => value.replaceAll(/[.*+?^${}()|[\\]\\\\]/gu, '\\\\$&');\n\nconst buildKeywordRegex = (keyword: string): RegExp =>\n new RegExp(`\\\\b${escapeRegExp(keyword)}\\\\b`, 'giu');\n\nconst isModeKeyword = (value: string): value is ModeKeyword =>\n (MODE_KEYWORDS as readonly string[]).includes(value);\n\nexport const getModeMarker = (mode: string): string => {\n if (isModeKeyword(mode)) {\n return MODE_MARKERS[mode];\n }\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 const detectMode = (text: string, disabled?: Set<string>): ModeDetectPure | null => {\n if (!text) {\n return null;\n }\n const codeRanges = findCodeBlockRanges(text);\n const normalizedDisabled = disabled\n ? new Set([...disabled].map((k) => k.toLowerCase()))\n : undefined;\n let best: ModeDetectPure | null = null;\n\n for (const keyword of MODE_KEYWORDS) {\n if (\n normalizedDisabled !== undefined &&\n normalizedDisabled !== null &&\n normalizedDisabled.has(keyword)\n ) {\n continue;\n }\n const regex = buildKeywordRegex(keyword);\n let match: RegExpExecArray | null;\n while ((match = regex.exec(text)) !== null) {\n if (isInRanges(match.index, codeRanges)) {\n continue;\n }\n if (best === null || MODE_PRIORITY[keyword] > MODE_PRIORITY[best.mode]) {\n best = { index: match.index, keyword: match[0], mode: keyword };\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 const 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*/u, '');\n return (before + cleaned).replaceAll(/ {2,}/gu, ' ').trim();\n};\n","import path from 'node:path';\n\nconst __dirname = import.meta.dirname;\nexport const PACKAGE_ROOT = path.resolve(__dirname, '..');\nexport const AGENTS_DIR = path.join(PACKAGE_ROOT, 'agents');\nexport const COMMANDS_DIR = path.join(PACKAGE_ROOT, 'agents', 'commands');\nexport const RULES_PATH = path.join(PACKAGE_ROOT, 'rules', 'AGENTS.md');\n","import { extractModeSection } from '@maestria/shared-mode';\nimport { readFileSync } from 'node:fs';\nimport path from 'node:path';\n\nimport type { ModeKeyword } from '@/modes/types.js';\nimport { COMMANDS_DIR } from '@/root.js';\n\nconst VALID_KEYWORDS: readonly ModeKeyword[] = ['fein', 'sonar', 'blitz'];\n\nconst loadModePrompt = (name: string): string => {\n const content = readFileSync(path.resolve(COMMANDS_DIR, `${name}.md`), 'utf-8');\n return extractModeSection(content);\n};\n\nconst isModeKeyword = (value: string): value is ModeKeyword =>\n VALID_KEYWORDS.some((keyword) => keyword === value);\n\nconst modePromptTarget: Record<string, string> = {};\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(modePromptTarget, {\n get(target, key, receiver) {\n if (typeof key === 'string' && isModeKeyword(key)) {\n if (!(key in target)) {\n try {\n target[key] = loadModePrompt(key);\n } catch (error) {\n console.warn(`[maestria] Failed to load mode prompt \"${key}\":`, error);\n target[key] = '';\n }\n }\n return target[key];\n }\n return Reflect.get(target, key, receiver) as unknown;\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 blitz: '[MODE: blitz]',\n fein: '[MODE: fein]',\n sonar: '[MODE: sonar]',\n};\n\n/**\n * Array of all valid mode keywords for runtime iteration.\n */\nexport { VALID_KEYWORDS };\n","import {\n detectMode as sharedDetectMode,\n getModeMarker as sharedGetMarker,\n stripKeyword as sharedStripKeyword,\n} from '@maestria/shared-mode';\n\nimport { MODE_MARKERS, MODE_PROMPTS, 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 */\nconst isModeKeyword = (value: string): value is ModeKeyword =>\n (VALID_KEYWORDS as readonly string[]).includes(value);\n\nexport const detectMode = (text: string, disabled?: Set<string>): ModeResult | null => {\n const pure = sharedDetectMode(text, disabled);\n if (pure === null) {\n return null;\n }\n return {\n index: pure.index,\n keyword: pure.keyword,\n marker: MODE_MARKERS[pure.mode],\n mode: pure.mode,\n prompt: MODE_PROMPTS[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 const stripKeyword = (text: string, result: ModeResult): string =>\n sharedStripKeyword(text, result);\n\n/**\n * Get the mode prompt text for a given mode name.\n */\nexport const 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 const getModeMarker = (mode: string): string => {\n if (isModeKeyword(mode)) {\n return sharedGetMarker(mode);\n }\n return '';\n};\n","/**\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 type { Config, Hooks, Plugin, PluginInput } from '@opencode-ai/plugin';\nimport { merge } from 'es-toolkit';\nimport { readdirSync, readFileSync } from 'node:fs';\nimport path from 'node:path';\nimport { parse as parseYaml } from 'yaml';\n\nimport { detectMode, getModeMarker, getModePrompt, stripKeyword } from '@/modes/index.js';\nimport { maestriaOptionsSchema } from '@/modes/types.js';\nimport type { MaestriaPluginOptions } from '@/modes/types.js';\nimport { AGENTS_DIR, RULES_PATH } from '@/root.js';\n\ntype OpenCodeAgentConfig = NonNullable<NonNullable<Config['agent']>[string]>;\ntype AgentMode = NonNullable<OpenCodeAgentConfig['mode']>;\ntype AgentPermission = NonNullable<OpenCodeAgentConfig['permission']> & Record<string, unknown>;\ntype AgentConfig = Omit<OpenCodeAgentConfig, 'mode' | 'permission'> & {\n mode: AgentMode;\n permission: AgentPermission;\n};\ntype ChatMessageHook = NonNullable<Hooks['chat.message']>;\ntype ChatMessageInput = Parameters<ChatMessageHook>[0];\ntype ChatMessageOutput = Parameters<ChatMessageHook>[1];\ntype TextPart = Extract<ChatMessageOutput['parts'][number], { type: 'text' }>;\ntype ConfigInput = Parameters<NonNullable<Hooks['config']>>[0];\ntype CompactingOutput = Parameters<NonNullable<Hooks['experimental.session.compacting']>>[1];\n\ninterface AgentFrontmatter {\n description: string;\n mode: AgentMode;\n permission: AgentPermission;\n color?: string;\n maxSteps?: number;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst isAgentMode = (value: unknown): value is AgentMode =>\n value === 'all' || value === 'primary' || value === 'subagent';\n\nconst parsePermission = (value: unknown): AgentPermission => {\n if (!isRecord(value)) {\n return {};\n }\n\n const permission: AgentPermission = {};\n for (const [key, permissionValue] of Object.entries(value)) {\n permission[key] = permissionValue;\n }\n return permission;\n};\n\nconst parseFrontmatter = (yamlStr: string): AgentFrontmatter => {\n const parsed = parseYaml(yamlStr) as unknown;\n const result = isRecord(parsed) ? parsed : {};\n\n return {\n color: typeof result.color === 'string' ? result.color : undefined,\n description: typeof result.description === 'string' ? result.description : '',\n maxSteps:\n result.maxSteps !== undefined && result.maxSteps !== null && result.maxSteps !== ''\n ? Number(result.maxSteps)\n : undefined,\n mode: isAgentMode(result.mode) ? result.mode : 'subagent',\n permission: parsePermission(result.permission),\n };\n};\n\n/**\n * Read an agent markdown file and split into frontmatter + prompt.\n */\nconst parseAgentFile = (filePath: string): { name: string; config: AgentConfig } => {\n const content = readFileSync(filePath, 'utf-8');\n const name = path.basename(filePath, '.md');\n\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: AgentConfig = {\n description: frontmatter.description,\n mode: frontmatter.mode,\n permission: frontmatter.permission,\n prompt,\n };\n\n if (frontmatter.color !== undefined && frontmatter.color !== null && frontmatter.color !== '') {\n config.color = frontmatter.color;\n }\n if (\n frontmatter.maxSteps !== undefined &&\n frontmatter.maxSteps !== null &&\n frontmatter.maxSteps !== 0\n ) {\n config.maxSteps = frontmatter.maxSteps;\n }\n\n return { config, name };\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 */\nconst loadAgents = (): NonNullable<Config['agent']> => {\n try {\n const files = readdirSync(AGENTS_DIR).filter((file) => file.endsWith('.md'));\n const agents: NonNullable<Config['agent']> = {};\n\n for (const file of files) {\n try {\n const { name, config } = parseAgentFile(path.join(AGENTS_DIR, file));\n agents[name] = config;\n } catch (error) {\n console.warn(`[maestria] Failed to parse agent file \"${file}\":`, error);\n }\n }\n\n return agents;\n } catch (error) {\n console.error('[maestria] Failed to read agents directory:', error);\n throw new Error(\n `[maestria] Failed to load agents from \"${AGENTS_DIR}\": ${\n error instanceof Error ? error.message : String(error)\n }`,\n { cause: error },\n );\n }\n};\n\nconst applyModeToMessage = (\n hookInput: ChatMessageInput,\n hookOutput: ChatMessageOutput,\n disabledKeywords: Set<string>,\n): void => {\n if (hookInput.agent !== 'orchestrator') {\n return;\n }\n\n const textPart = hookOutput.parts.find((part): part is TextPart => part.type === 'text');\n if (textPart === undefined) {\n return;\n }\n\n const result = detectMode(textPart.text, disabledKeywords);\n if (result === null) {\n return;\n }\n\n textPart.text = [\n getModeMarker(result.mode),\n '',\n getModePrompt(result.mode),\n '',\n stripKeyword(textPart.text, result),\n ].join('\\n');\n};\n\nconst configureAgents = (input: ConfigInput, agents: NonNullable<Config['agent']>): void => {\n input.agent = merge(input.agent ?? {}, agents);\n input.instructions = [...(input.instructions ?? []), RULES_PATH];\n};\n\nconst appendCompactionContext = (output: CompactingOutput): void => {\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\nexport const MaestriaPlugin: Plugin = async (\n _input: PluginInput,\n options?: MaestriaPluginOptions,\n) => {\n const parsed = maestriaOptionsSchema.parse(options ?? {});\n const disabledKeywords = new Set<string>(\n (parsed.modes?.disabledKeywords ?? []).map((keyword) => keyword.toLowerCase()),\n );\n const agents = loadAgents();\n await Promise.resolve();\n\n return {\n 'chat.message': async (hookInput, hookOutput) => {\n applyModeToMessage(hookInput, hookOutput, disabledKeywords);\n await Promise.resolve();\n },\n config: async (input) => {\n configureAgents(input, agents);\n await Promise.resolve();\n },\n 'experimental.session.compacting': async (_compactingInput, output) => {\n appendCompactionContext(output);\n await Promise.resolve();\n },\n };\n};\n\nexport default MaestriaPlugin;\n"],"mappings":"yKAkBA,MAAa,EAAgB,CAAC,OAAQ,QAAS,OAAO,EAQzCA,EAA4C,CACvD,MAAO,gBACP,KAAM,eACN,MAAO,eACT,EASa,EAA6C,CACxD,MAAO,EACP,KAAM,EACN,MAAO,CACT,EAsBa,EAAgB,2BAEhB,EAAuB,GAAqC,CACvE,IAAM,EAA6B,CAAC,EAChC,EAGJ,IADA,EAAc,UAAY,GAClB,EAAQ,EAAc,KAAK,CAAI,KAAO,MAC5C,EAAO,KAAK,CAAC,EAAM,MAAO,EAAM,MAAQ,EAAM,EAAE,CAAC,MAAM,CAAC,EAE1D,OAAO,CACT,EAEa,GAAc,EAAe,IACxC,EAAO,MAAM,CAAC,EAAO,KAAS,GAAS,GAAS,EAAQ,CAAG,EAQhD,EAAsB,GAA4B,CAC7D,IAAM,EAAU,EAAQ,QAAQ,UAAU,EAI1C,OAHI,IAAY,GAGT,GAAG,EAAQ,QAAQ,QAAS,EAAE,EAAE,IAF9B,GAAG,EAAQ,MAAM,CAAO,CAAC,CAAC,QAAQ,QAAS,EAAE,EAAE,GAG1D,EAEM,EAAgB,GAA0B,EAAM,WAAW,uBAAwB,MAAM,EAEzF,EAAqB,GACrB,OAAO,MAAM,EAAa,CAAO,EAAE,KAAM,KAAK,EAE9CC,EAAiB,GACpB,EAAoC,SAAS,CAAK,EAExCC,EAAiB,GACxBD,EAAc,CAAI,EACbD,EAAa,GAEf,GAiBIG,GAAc,EAAc,IAAkD,CACzF,GAAI,CAAC,EACH,OAAO,KAET,IAAM,EAAa,EAAoB,CAAI,EACrC,EAAqB,EACvB,IAAI,IAAI,CAAC,GAAG,CAAQ,CAAC,CAAC,IAAK,GAAM,EAAE,YAAY,CAAC,CAAC,EACjD,IAAA,GACA,EAA8B,KAElC,IAAK,IAAM,KAAW,EAAe,CACnC,GACE,GACuB,MACvB,EAAmB,IAAI,CAAO,EAE9B,SAEF,IAAM,EAAQ,EAAkB,CAAO,EACnC,EACJ,MAAQ,EAAQ,EAAM,KAAK,CAAI,KAAO,MAChC,EAAW,EAAM,MAAO,CAAU,IAGlC,IAAS,MAAQ,EAAc,GAAW,EAAc,EAAK,SAC/D,EAAO,CAAE,MAAO,EAAM,MAAO,QAAS,EAAM,GAAI,KAAM,CAAQ,EAGpE,CAEA,OAAO,CACT,EASaC,GAAgB,EAAc,KAC1B,EAAK,MAAM,EAAG,EAAO,KAGvB,EAFC,EAAK,MAAM,EAAO,MAAQ,EAAO,QAAQ,MACnC,CAAC,CAAC,QAAQ,SAAU,EACjB,EAAA,CAAG,WAAW,UAAW,GAAG,CAAC,CAAC,KAAK,ECtKtD,EAAY,YAAY,QACjB,EAAe,EAAK,QAAQ,EAAW,IAAI,EAC3C,EAAa,EAAK,KAAK,EAAc,QAAQ,EAC7C,EAAe,EAAK,KAAK,EAAc,SAAU,UAAU,EAC3D,EAAa,EAAK,KAAK,EAAc,QAAS,WAAW,ECChE,EAAyC,CAAC,OAAQ,QAAS,OAAO,EAElE,EAAkB,GAAyB,CAC/C,IAAM,EAAU,EAAa,EAAK,QAAQ,EAAc,GAAG,EAAK,IAAI,EAAG,OAAO,EAC9E,OAAO,EAAmB,CAAO,CACnC,EAEMC,EAAiB,GACrB,EAAe,KAAM,GAAY,IAAY,CAAK,EAWvC,EAA4C,IAAI,MAAM,CAAA,EAAkB,CACnF,IAAI,EAAQ,EAAK,EAAU,CACzB,GAAI,OAAO,GAAQ,UAAYA,EAAc,CAAG,EAAG,CACjD,GAAI,EAAE,KAAO,GACX,GAAI,CACF,EAAO,GAAO,EAAe,CAAG,CAClC,OAAS,EAAO,CACd,QAAQ,KAAK,0CAA0C,EAAI,IAAK,CAAK,EACrE,EAAO,GAAO,EAChB,CAEF,OAAO,EAAO,EAChB,CACA,OAAO,QAAQ,IAAI,EAAQ,EAAK,CAAQ,CAC1C,CACF,CAAC,EAMY,EAA4C,CACvD,MAAO,gBACP,KAAM,eACN,MAAO,eACT,EC9BM,EAAiB,GACpB,EAAqC,SAAS,CAAK,EAEzC,GAAc,EAAc,IAA8C,CACrF,IAAM,EAAOC,EAAiB,EAAM,CAAQ,EAI5C,OAHI,IAAS,KACJ,KAEF,CACL,MAAO,EAAK,MACZ,QAAS,EAAK,QACd,OAAQ,EAAa,EAAK,MAC1B,KAAM,EAAK,KACX,OAAQ,EAAa,EAAK,KAC5B,CACF,EAMa,GAAgB,EAAc,IACzCC,EAAmB,EAAM,CAAM,EAKpB,EAAiB,GACxB,EAAc,CAAI,EACb,EAAa,GAEf,GAMI,EAAiB,GACxB,EAAc,CAAI,EACbC,EAAgB,CAAI,EAEtB,GC/CI,EAAoB,EAAE,KAAK,CAAC,OAAQ,QAAS,OAAO,CAAC,EAMrD,EAAwB,EAAE,OAAO,CAC5C,MAAO,EACJ,OAAO,CACN,iBAAkB,EAAE,MAAM,CAAiB,CAAC,CAAC,SAAS,CACxD,CAAC,CAAC,CACD,SAAS,CACd,CAAC,ECMK,EAAY,GAChB,OAAO,GAAU,YAAY,GAAkB,CAAC,MAAM,QAAQ,CAAK,EAE/D,EAAe,GACnB,IAAU,OAAS,IAAU,WAAa,IAAU,WAEhD,EAAmB,GAAoC,CAC3D,GAAI,CAAC,EAAS,CAAK,EACjB,MAAO,CAAC,EAGV,IAAM,EAA8B,CAAC,EACrC,IAAK,GAAM,CAAC,EAAK,KAAoB,OAAO,QAAQ,CAAK,EACvD,EAAW,GAAO,EAEpB,OAAO,CACT,EAEM,EAAoB,GAAsC,CAC9D,IAAM,EAASC,EAAU,CAAO,EAC1B,EAAS,EAAS,CAAM,EAAI,EAAS,CAAC,EAE5C,MAAO,CACL,MAAO,OAAO,EAAO,OAAU,SAAW,EAAO,MAAQ,IAAA,GACzD,YAAa,OAAO,EAAO,aAAgB,SAAW,EAAO,YAAc,GAC3E,SACE,EAAO,WAAa,IAAA,IAAa,EAAO,WAAa,MAAQ,EAAO,WAAa,GAC7E,OAAO,EAAO,QAAQ,EACtB,IAAA,GACN,KAAM,EAAY,EAAO,IAAI,EAAI,EAAO,KAAO,WAC/C,WAAY,EAAgB,EAAO,UAAU,CAC/C,CACF,EAKM,EAAkB,GAA4D,CAClF,IAAM,EAAU,EAAa,EAAU,OAAO,EACxC,EAAO,EAAK,SAAS,EAAU,KAAK,EAEpC,EAAQ,EAAQ,MAAM,KAAK,EACjC,GAAI,EAAM,OAAS,EACjB,MAAU,MAAM,uBAAuB,EAAS,uBAAuB,EAGzE,IAAM,EAAc,EAAiB,EAAM,EAAE,CAAC,KAAK,CAAC,EAC9C,EAAS,EAAM,MAAM,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK,EAEzC,EAAsB,CAC1B,YAAa,EAAY,YACzB,KAAM,EAAY,KAClB,WAAY,EAAY,WACxB,QACF,EAaA,OAXI,EAAY,QAAU,IAAA,IAAa,EAAY,QAAU,MAAQ,EAAY,QAAU,KACzF,EAAO,MAAQ,EAAY,OAG3B,EAAY,WAAa,IAAA,IACzB,EAAY,WAAa,MACzB,EAAY,WAAa,IAEzB,EAAO,SAAW,EAAY,UAGzB,CAAE,SAAQ,MAAK,CACxB,EAMM,MAAiD,CACrD,GAAI,CACF,IAAM,EAAQ,EAAY,CAAU,CAAC,CAAC,OAAQ,GAAS,EAAK,SAAS,KAAK,CAAC,EACrE,EAAuC,CAAC,EAE9C,IAAK,IAAM,KAAQ,EACjB,GAAI,CACF,GAAM,CAAE,OAAM,UAAW,EAAe,EAAK,KAAK,EAAY,CAAI,CAAC,EACnE,EAAO,GAAQ,CACjB,OAAS,EAAO,CACd,QAAQ,KAAK,0CAA0C,EAAK,IAAK,CAAK,CACxE,CAGF,OAAO,CACT,OAAS,EAAO,CAEd,MADA,QAAQ,MAAM,8CAA+C,CAAK,EACxD,MACR,0CAA0C,EAAW,KACnD,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,IAEvD,CAAE,MAAO,CAAM,CACjB,CACF,CACF,EAEM,GACJ,EACA,EACA,IACS,CACT,GAAI,EAAU,QAAU,eACtB,OAGF,IAAM,EAAW,EAAW,MAAM,KAAM,GAA2B,EAAK,OAAS,MAAM,EACvF,GAAI,IAAa,IAAA,GACf,OAGF,IAAM,EAAS,EAAW,EAAS,KAAM,CAAgB,EACrD,IAAW,OAIf,EAAS,KAAO,CACd,EAAc,EAAO,IAAI,EACzB,GACA,EAAc,EAAO,IAAI,EACzB,GACA,EAAa,EAAS,KAAM,CAAM,CACpC,CAAC,CAAC,KAAK;CAAI,EACb,EAEM,GAAmB,EAAoB,IAA+C,CAC1F,EAAM,MAAQ,EAAM,EAAM,OAAS,CAAC,EAAG,CAAM,EAC7C,EAAM,aAAe,CAAC,GAAI,EAAM,cAAgB,CAAC,EAAI,CAAU,CACjE,EAEM,EAA2B,GAAmC,CAClE,EAAO,QAAQ,KACb,4KAGF,CACF,EAEa,EAAyB,MACpC,EACA,IACG,CACH,IAAM,EAAS,EAAsB,MAAM,GAAW,CAAC,CAAC,EAClD,EAAmB,IAAI,KAC1B,EAAO,OAAO,kBAAoB,CAAC,EAAA,CAAG,IAAK,GAAY,EAAQ,YAAY,CAAC,CAC/E,EACM,EAAS,EAAW,EAG1B,OAFA,MAAM,QAAQ,QAAQ,EAEf,CACL,eAAgB,MAAO,EAAW,IAAe,CAC/C,EAAmB,EAAW,EAAY,CAAgB,EAC1D,MAAM,QAAQ,QAAQ,CACxB,EACA,OAAQ,KAAO,IAAU,CACvB,EAAgB,EAAO,CAAM,EAC7B,MAAM,QAAQ,QAAQ,CACxB,EACA,kCAAmC,MAAO,EAAkB,IAAW,CACrE,EAAwB,CAAM,EAC9B,MAAM,QAAQ,QAAQ,CACxB,CACF,CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maestria/opencode",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
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"
|
package/rules/AGENTS.md
CHANGED
|
@@ -13,6 +13,7 @@ Cross-platform behavior contract for outcomes, evidence, safety, delegation, rev
|
|
|
13
13
|
- **!!! Match effort to stakes.** Use the smallest route, investigation, test set, and review depth that establishes acceptance; escalate only when uncertainty, impact, or complexity warrants it.
|
|
14
14
|
- **!!! Prefer reuse over reinvention.** Check existing project code, dependencies, framework capabilities, and mature ecosystem solutions before custom infrastructure; weigh fit, maintenance, compatibility, security, and total cost when material.
|
|
15
15
|
- **!!! Exhaust available evidence before asking.** Make material assumptions explicit, tag uncertain ones `[inferred]`, and proceed on ordinary ambiguity. Ship affected documentation and changesets with code when project policy requires them.
|
|
16
|
+
- **!!! Exercise testing judgment, not coverage.** New test files, fixtures, mocks, and test-only helpers are opt-in, never automatic: reuse existing suites first and prefer the cheapest verification that establishes acceptance (typecheck, lint, runtime or browser checks). Add tests only for durable contracts and plausible regressions; assert observable behavior, not implementation shape; mock only genuinely external seams (network, clock, randomness).
|
|
16
17
|
- **!!! Keep output self-contained and professional.** Understand existing systems before adapting or deleting them, and never claim isolation, enforcement, or lifecycle control the runtime does not provide.
|
|
17
18
|
- **!!! Human-facing output.** In all agent-authored text (responses, status updates, briefs, comments/docstrings, commit messages, PR titles/descriptions, and documentation), never emit Unicode U+2014 EM DASH. Prefer commas, colons, parentheses, or ASCII hyphen-minus (`-`). Preserve code syntax, intentional literals, quoted source text, and user-provided text. Scan authored output before handoff or delivery.
|
|
18
19
|
|
|
@@ -44,7 +45,7 @@ Default to one independent review and, only when blockers exist, one repair/re-r
|
|
|
44
45
|
|
|
45
46
|
Safety and authorization override user intent, methodology, and brevity. Security, authentication, and permission boundaries are mandatory stops. Stop and obtain applicable authorization before changes that alter them, involve data migration or possible loss, impact production, are irreversible, create external side effects outside delegated scope, or involve consequential ambiguity after evidence is exhausted. Ordinary in-scope security defects may be repaired autonomously.
|
|
46
47
|
|
|
47
|
-
The orchestrator owns continuation for implementation and delivery work until the outcome reaches its terminal artifact; incomplete todos, pending handoffs, or specialist messages saying "continue if needed" are not a user checkpoint. Routine delivery is autonomous. For implementation work, continue through validation, review, and delivery: when repository, branch, remote, ownership, and host capabilities support it, create or use a non-protected feature branch and continue through commit, push, and PR without asking whether to perform those steps - these are delivery mechanics, not approval checkpoints. Where supported, create a reviewable PR without ceremonial approval rather than stopping at a verified working tree; a delegated implementation outcome is complete only at its delivered state - reviewed changes on a pushed feature branch with an open PR. Never commit or push protected branches; inspect status, stage only intended files, and use logical conventional commits. Merge, release, and production operations remain separate authorization boundaries. Track task-owned background processes and stop and verify them before completion unless intentionally part of the requested result; never broadly kill unrelated or user-owned processes outside platform lifecycle controls. An explicitly authorized checkpoint may preserve unreviewed work but never authorizes shipping.
|
|
48
|
+
The orchestrator owns continuation for implementation and delivery work until the outcome reaches its terminal artifact; incomplete todos, pending handoffs, or specialist messages saying "continue if needed" are not a user checkpoint. Routine delivery is autonomous. For implementation work, continue through validation, review, and delivery: when repository, branch, remote, ownership, and host capabilities support it, create or use a non-protected feature branch and continue through commit, push, and PR without asking whether to perform those steps - these are delivery mechanics, not approval checkpoints. Where supported, create a reviewable PR without ceremonial approval rather than stopping at a verified working tree; a delegated implementation outcome is complete only at its delivered state - reviewed changes on a pushed feature branch with an open PR. Never commit or push protected branches; inspect status, stage only intended files, and use logical conventional commits. Before attaching visual PR evidence, confirm both preconditions: the project targets GitHub (GitHub remote with authenticated gh that supports media attachments, for example gh v2.99.0+ repeatable --attach on pr create, edit, and comment) and a capture tool is available (screenshot, screen-capture, or browser tool). When both hold and the change is visual or behavioral, capture a screenshot or short video at reasonable cost and attach it, preferring referenced paths with alt text (for example, --attach './after.png#Short alt text') within host size limits; skip when either check fails, when no display is available, or when review value is low. Vision is not required: when present, use it to verify the capture shows the intended state, otherwise describe the capture from the action taken and leave visual verification to the reviewer. Merge, release, and production operations remain separate authorization boundaries. Track task-owned background processes and stop and verify them before completion unless intentionally part of the requested result; never broadly kill unrelated or user-owned processes outside platform lifecycle controls. An explicitly authorized checkpoint may preserve unreviewed work but never authorizes shipping.
|
|
48
49
|
|
|
49
50
|
## Canonical Source Invariant
|
|
50
51
|
|