opencode-onboard 0.1.8 → 0.1.12

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.
@@ -72,11 +72,3 @@ Rules:
72
72
  **Files changed:** <list>
73
73
  **Blockers:** none | <description>
74
74
  ```
75
-
76
- ## Session Log
77
-
78
- Append to `.agents/session-log.md`. Create the file with header if it does not exist (see AGENTS.md Session Log section). This is mandatory, do it before any other work.
79
-
80
- - On start: `| {ISO timestamp} | back-engineer | started | {task summary} |`
81
- - On skill load: `| {ISO timestamp} | back-engineer | skill-loaded | {skill-name} |`
82
- - On done: `| {ISO timestamp} | back-engineer | completed | {files changed count} files, skills: {comma-separated skill names or none} |`
@@ -56,7 +56,7 @@ Rules:
56
56
  1. Verify all changes are on a feature branch, never `main`
57
57
  2. Load the matching pullrequest skill
58
58
  3. Capture screenshots of local running app if UI changes exist
59
- 4. Read `.agents/session-log.md` if it exists, include a "Session Activity" section in the PR description with agent names, task counts, and total duration
59
+ 4. Read `.agents/session-log.json` if it exists, parse the JSON array and include a "Session Activity" section in the PR description with agent names, task counts, and skills used
60
60
  5. Commit and push the feature branch
61
61
  6. Create the PR following the skill instructions
62
62
  7. Post PR comment with screenshots and change summary
@@ -74,7 +74,7 @@ Rules:
74
74
  - Does not merge PRs, human-only
75
75
  - Does not approve PRs, human-only
76
76
  - Does not force push
77
- - ALL GitHub and Azure DevOps data MUST come from `gh` or `az` CLI NEVER use webfetch or HTTP requests to fetch platform URLs, even as a fallback. If CLI is unavailable, report as a blocker.
77
+ - ALL GitHub and Azure DevOps data MUST come from `gh` or `az` CLI, NEVER use webfetch or HTTP requests to fetch platform URLs, even as a fallback. If CLI is unavailable, report as a blocker.
78
78
  - Browser MCP tools permitted only for screenshots of local app on `localhost` URLs, never for navigating GitHub or Azure DevOps
79
79
 
80
80
  ## Output Format
@@ -108,11 +108,3 @@ Rules:
108
108
  **Questions for human:** <count>, <list>
109
109
  **Acknowledged only:** <count>
110
110
  ```
111
-
112
- ## Session Log
113
-
114
- Append to `.agents/session-log.md`. Create the file with header if it does not exist (see AGENTS.md Session Log section). This is mandatory — do it before any other work.
115
-
116
- - On start: `| {ISO timestamp} | devops-manager | started | {mode} mode |`
117
- - On skill load: `| {ISO timestamp} | devops-manager | skill-loaded | {skill-name} |`
118
- - On done: `| {ISO timestamp} | devops-manager | completed | {summary}, skills: {comma-separated skill names or none} |`
@@ -71,11 +71,3 @@ Rules:
71
71
  **Files changed:** <list>
72
72
  **Blockers:** none | <description>
73
73
  ```
74
-
75
- ## Session Log
76
-
77
- Append to `.agents/session-log.md`. Create the file with header if it does not exist (see AGENTS.md Session Log section). This is mandatory, do it before any other work.
78
-
79
- - On start: `| {ISO timestamp} | front-engineer | started | {task summary} |`
80
- - On skill load: `| {ISO timestamp} | front-engineer | skill-loaded | {skill-name} |`
81
- - On done: `| {ISO timestamp} | front-engineer | completed | {files changed count} files, skills: {comma-separated skill names or none} |`
@@ -72,11 +72,3 @@ Rules:
72
72
  **Resources affected:** <list>
73
73
  **Blockers:** none | <description>
74
74
  ```
75
-
76
- ## Session Log
77
-
78
- Append to `.agents/session-log.md`. Create the file with header if it does not exist (see AGENTS.md Session Log section). This is mandatory — do it before any other work.
79
-
80
- - On start: `| {ISO timestamp} | infra-engineer | started | {task summary} |`
81
- - On skill load: `| {ISO timestamp} | infra-engineer | skill-loaded | {skill-name} |`
82
- - On done: `| {ISO timestamp} | infra-engineer | completed | {files changed count} files, skills: {comma-separated skill names or none} |`
@@ -72,11 +72,3 @@ Rules:
72
72
  **Acceptance criteria:** met | <unmet items>
73
73
  **Blockers:** none | <description>
74
74
  ```
75
-
76
- ## Session Log
77
-
78
- Append to `.agents/session-log.md`. Create the file with header if it does not exist (see AGENTS.md Session Log section). This is mandatory — do it before any other work.
79
-
80
- - On start: `| {ISO timestamp} | quality-engineer | started | {task summary} |`
81
- - On skill load: `| {ISO timestamp} | quality-engineer | skill-loaded | {skill-name} |`
82
- - On done: `| {ISO timestamp} | quality-engineer | completed | {tests added count} tests, skills: {comma-separated skill names or none} |`
@@ -82,11 +82,3 @@ Rules:
82
82
 
83
83
  **Blockers:** none | <critical findings that must be resolved before PR>
84
84
  ```
85
-
86
- ## Session Log
87
-
88
- Append to `.agents/session-log.md`. Create the file with header if it does not exist (see AGENTS.md Session Log section). This is mandatory — do it before any other work.
89
-
90
- - On start: `| {ISO timestamp} | security-auditor | started | {task summary} |`
91
- - On skill load: `| {ISO timestamp} | security-auditor | skill-loaded | {skill-name} |`
92
- - On done: `| {ISO timestamp} | security-auditor | completed | {findings count} findings, skills: {comma-separated skill names or none} |`
@@ -18,8 +18,8 @@ Use `rtk` wrapper for ALL CLI commands:
18
18
  - `rtk gh pr comment` NOT `gh pr comment`
19
19
  - `rtk gh api` NOT `gh api`
20
20
 
21
- **Browser MCP tools are FORBIDDEN for all GitHub operations.**
22
- Browser tools are ONLY permitted for screenshots of the LOCAL running app on `localhost` URLs.
21
+ **ALL GitHub data MUST come from `gh` CLI. NEVER use webfetch, HTTP requests, or browser MCP tools for GitHub operations, even if gh CLI fails. If `gh` is unavailable, report as a blocker.**
22
+ Always pass `--repo {owner}/{repo}` explicitly, never rely on git context to resolve the repo.
23
23
 
24
24
  ---
25
25
 
@@ -85,13 +85,13 @@ Triggered when user says "I've added comments to the PR" or "check PR feedback".
85
85
 
86
86
  If PR link provided, extract number from URL. Otherwise:
87
87
  ```bash
88
- rtk gh pr list --state open --limit 1
88
+ rtk gh pr list --repo {owner}/{repo} --state open --limit 1
89
89
  ```
90
90
 
91
91
  ### Step 2: Read comment threads
92
92
 
93
93
  ```bash
94
- rtk gh pr view {pr-number} --comments
94
+ rtk gh pr view {pr-number} --repo {owner}/{repo} --comments
95
95
  # Or structured output:
96
96
  rtk gh api repos/{owner}/{repo}/pulls/{pr-number}/comments
97
97
  rtk gh api repos/{owner}/{repo}/pulls/{pr-number}/reviews
@@ -132,9 +132,10 @@ rtk gh pr comment {pr-number} --body "Updated design.md to reflect feedback."
132
132
  ## Guardrails
133
133
 
134
134
  - ✅ Commit and push to feature branches only
135
- - ✅ Create and comment on PRs via gh CLI
135
+ - ✅ Create and comment on PRs via gh CLI with explicit `--repo {owner}/{repo}`
136
136
  - ✅ Screenshots of localhost only via browser_screenshot
137
137
  - ❌ Commit or push to `main`, FORBIDDEN
138
138
  - ❌ Force push, FORBIDDEN
139
139
  - ❌ Merge or approve PRs, human-only
140
140
  - ❌ Navigate browser to github.com, FORBIDDEN
141
+ - ❌ webfetch or HTTP requests to GitHub URLs, FORBIDDEN
@@ -156,17 +156,17 @@ https://dev.azure.com/{org}/{project}/_git/{repo}/pullrequest/{pr-id}
156
156
  **State:** {state}
157
157
 
158
158
  **Change Created:** us-{id}-{slug}
159
-
160
- ### Next Steps
161
- 1. Review the proposal
162
- 2. Say "implement the plan" to start implementation
163
159
  ```
164
160
 
161
+ After outputting the above, the lead MUST run `/opsx-propose` to generate the proposal, specs, and tasks. After `/opsx-propose` completes, STOP and ask the user: **"Ready to implement? (yes/no)"**, do NOT proceed to `/opsx-apply` until confirmed.
162
+
165
163
  ---
166
164
 
167
165
  ## Guardrails
168
166
 
169
167
  - ✅ Parse Azure DevOps URL and create OpenSpec change
170
168
  - ✅ Use `rtk` for all Azure CLI operations
169
+ - ✅ Always run `/opsx-propose` after parsing, never skip to implementation
170
+ - ✅ Always stop and confirm with user after propose, before running `/opsx-apply`
171
171
  - ❌ Browser MCP tools for Azure DevOps operations, FORBIDDEN
172
- - ❌ Implementation, this skill only parses and proposes
172
+ - ❌ Jump to implementation without user confirmation, FORBIDDEN
@@ -16,7 +16,7 @@ Use `rtk` wrapper for ALL CLI commands:
16
16
  - `rtk gh issue edit` NOT `gh issue edit`
17
17
  - `rtk openspec new change` NOT `openspec new change`
18
18
 
19
- **ALL GitHub data MUST come from `gh` CLI. NEVER use webfetch, HTTP requests, or browser MCP tools to fetch GitHub URLs even if gh CLI fails. If `gh` is unavailable, report it as a blocker.**
19
+ **ALL GitHub data MUST come from `gh` CLI. NEVER use webfetch, HTTP requests, or browser MCP tools to fetch GitHub URLs, even if gh CLI fails. If `gh` is unavailable, report it as a blocker.**
20
20
 
21
21
  ---
22
22
 
@@ -36,13 +36,14 @@ gh auth status
36
36
 
37
37
  ## Steps
38
38
 
39
- 1. **Extract Issue Number** from URL
40
- - `https://github.com/{owner}/{repo}/issues/42` → number: 42
39
+ 1. **Extract owner, repo, and issue number** from URL
40
+ - `https://github.com/{owner}/{repo}/issues/42` → owner: `{owner}`, repo: `{repo}`, number: `42`
41
41
 
42
- 2. **Fetch Issue**
42
+ 2. **Fetch Issue**, always pass `--repo` explicitly, never rely on git context:
43
43
  ```bash
44
- rtk gh issue view 42 --json number,title,body,labels,milestone,state
44
+ rtk gh issue view 42 --repo {owner}/{repo} --json number,title,body,labels,milestone,state
45
45
  ```
46
+ If this returns an auth error or 404, report as a blocker, do NOT fall back to webfetch or web search.
46
47
 
47
48
  3. **Extract Key Fields** from JSON response:
48
49
  - `number` → Issue number
@@ -61,18 +62,18 @@ gh auth status
61
62
 
62
63
  ## Full GitHub CLI Reference
63
64
 
64
- Use these for ALL GitHub operations, browser MCP is FORBIDDEN.
65
+ Use these for ALL GitHub operations, browser MCP and webfetch are FORBIDDEN. Always pass `--repo {owner}/{repo}`, never rely on git context.
65
66
 
66
67
  ### Issues
67
68
  ```bash
68
69
  # Read issue
69
- rtk gh issue view <number>
70
+ rtk gh issue view <number> --repo {owner}/{repo}
70
71
 
71
72
  # List open issues
72
- rtk gh issue list --state open --limit 10
73
+ rtk gh issue list --repo {owner}/{repo} --state open --limit 10
73
74
 
74
75
  # Update issue
75
- rtk gh issue edit <number> --add-label "in-progress"
76
+ rtk gh issue edit <number> --repo {owner}/{repo} --add-label "in-progress"
76
77
  ```
77
78
 
78
79
  ---
@@ -119,18 +120,18 @@ https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}
119
120
  **Milestone:** {milestone}
120
121
 
121
122
  **Change Created:** gh-{number}-{slug}
122
-
123
- ### Next Steps
124
- 1. Review the proposal
125
- 2. Say "implement the plan" to start implementation
126
123
  ```
127
124
 
125
+ After outputting the above, the lead MUST run `/opsx-propose` to generate the proposal, specs, and tasks. After `/opsx-propose` completes, STOP and ask the user: **"Ready to implement? (yes/no)"**, do NOT proceed to `/opsx-apply` until confirmed.
126
+
128
127
  ---
129
128
 
130
129
  ## Guardrails
131
130
 
132
131
  - ✅ Parse GitHub Issue URL and create OpenSpec change
133
132
  - ✅ Use `rtk gh` for all GitHub CLI operations
134
- - `webfetch` or HTTP requests to GitHub URLs, FORBIDDEN use `gh` CLI only
133
+ - Always run `/opsx-propose` after parsing, never skip to implementation
134
+ - ✅ Always stop and confirm with user after propose, before running `/opsx-apply`
135
+ - ❌ `webfetch` or HTTP requests to GitHub URLs, FORBIDDEN, use `gh` CLI only
135
136
  - ❌ Browser MCP tools for GitHub operations, FORBIDDEN
136
- - ❌ Implementation, this skill only parses and proposes
137
+ - ❌ Jump to implementation without user confirmation, FORBIDDEN
@@ -3,10 +3,6 @@
3
3
  "mergeOnCleanup": true,
4
4
  "timeoutMs": 1800000,
5
5
  "stallThresholdMs": 300000,
6
- "defaultModel": "anthropic/claude-sonnet-4-6",
7
- "modelsByAgent": {
8
- "build": "anthropic/claude-sonnet-4-6",
9
- "explore": "anthropic/claude-haiku-4.5"
10
- },
6
+ "modelsByAgent": {},
11
7
  "modelAssignment": "default"
12
8
  }
@@ -3,6 +3,6 @@
3
3
  "plugin": [
4
4
  "opencode-plugin-openspec",
5
5
  "@different-ai/opencode-browser",
6
- "@hueyexe/opencode-ensemble@0.13.1"
6
+ "@hueyexe/opencode-ensemble@0.13.2"
7
7
  ]
8
- }
8
+ }
@@ -1,8 +1,7 @@
1
1
  import fs from "node:fs"
2
2
  import path from "node:path"
3
3
 
4
- const LOG_FILE = ".agents/session-log.md"
5
- const LOG_HEADER = "# Session Log\n\n| Timestamp | Agent | Action | Detail |\n|-----------|-------|--------|--------|\n"
4
+ const LOG_FILE = ".agents/session-log.json"
6
5
 
7
6
  // Per-session state: editCount and skills loaded
8
7
  const sessionState = new Map()
@@ -11,99 +10,81 @@ function ts() {
11
10
  return new Date().toISOString()
12
11
  }
13
12
 
14
- function appendLog(directory, row) {
13
+ function appendEntry(directory, entry) {
15
14
  const logPath = path.join(directory, LOG_FILE)
16
15
  const dir = path.dirname(logPath)
17
16
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
18
- if (!fs.existsSync(logPath)) fs.writeFileSync(logPath, LOG_HEADER, "utf8")
19
- fs.appendFileSync(logPath, row + "\n", "utf8")
17
+
18
+ let entries = []
19
+ if (fs.existsSync(logPath)) {
20
+ try { entries = JSON.parse(fs.readFileSync(logPath, "utf8")) } catch (_) {}
21
+ }
22
+ entries.push(entry)
23
+ fs.writeFileSync(logPath, JSON.stringify(entries, null, 2), "utf8")
20
24
  }
21
25
 
22
26
  function resolveAgentName(session) {
23
- // session.agent is the path to the agent .md file, e.g. ".agents/agents/back-engineer.md"
24
27
  const agentPath = session?.agent
25
28
  if (agentPath) {
26
29
  const base = path.basename(agentPath, ".md")
27
30
  if (base) return base
28
31
  }
29
- // Fall back to session title (ensemble sets it to agent name)
30
- const title = session?.title
31
- if (title) return title
32
32
  return "lead"
33
33
  }
34
34
 
35
35
  export const SessionLogPlugin = async ({ client, directory }) => {
36
36
  return {
37
- "session.created": async ({ event }) => {
37
+ event: async ({ event }) => {
38
38
  try {
39
- const sessionId = event.properties?.sessionID
40
- if (!sessionId) return
39
+ if (event?.type === "session.created") {
40
+ const sessionId = event.properties?.id ?? event.properties?.sessionID
41
+ if (!sessionId) return
41
42
 
42
- const res = await client.session.get({ path: { id: sessionId } })
43
- const session = res?.data
44
- const agentName = resolveAgentName(session)
43
+ const res = await client.session.get({ path: { id: sessionId } })
44
+ const session = res?.data
45
+ const agentName = resolveAgentName(session)
45
46
 
46
- sessionState.set(sessionId, { agentName, editCount: 0, skills: [] })
47
-
48
- appendLog(
49
- directory,
50
- `| ${ts()} | ${agentName} | started | session ${sessionId.slice(0, 8)} |`
51
- )
52
- } catch (_) {}
53
- },
47
+ sessionState.set(sessionId, { agentName, editCount: 0, skills: [] })
48
+ appendEntry(directory, { ts: ts(), agent: agentName, action: "started", sessionId })
49
+ }
54
50
 
55
- "tool.execute.after": async ({ event }) => {
56
- try {
57
- const { sessionID, tool, output } = event.properties ?? {}
58
- if (!sessionID) return
51
+ if (event?.type === "file.edited") {
52
+ const sessionId = event.properties?.sessionID ?? event.properties?.id
53
+ if (!sessionId) return
54
+ const state = sessionState.get(sessionId)
55
+ if (state) state.editCount++
56
+ }
59
57
 
60
- const state = sessionState.get(sessionID)
61
- const agentName = state?.agentName ?? "unknown"
58
+ if (event?.type === "session.idle") {
59
+ const sessionId = event.properties?.id ?? event.properties?.sessionID
60
+ if (!sessionId) return
61
+ const state = sessionState.get(sessionId)
62
+ if (!state) return
62
63
 
63
- // Skill detection: agent read a SKILL.md
64
- if (tool === "read") {
65
- const filePath = output?.args?.filePath ?? output?.input?.filePath ?? ""
66
- const match = filePath.match(/[/\\]skills[/\\]([^/\\]+)[/\\]SKILL\.md$/i)
67
- if (match) {
68
- const skillName = match[1]
69
- if (state && !state.skills.includes(skillName)) {
70
- state.skills.push(skillName)
71
- }
72
- appendLog(
73
- directory,
74
- `| ${ts()} | ${agentName} | skill-loaded | ${skillName} |`
75
- )
76
- }
64
+ const { agentName, editCount, skills } = state
65
+ appendEntry(directory, { ts: ts(), agent: agentName, action: "completed", filesEdited: editCount, skills })
66
+ sessionState.delete(sessionId)
77
67
  }
78
68
  } catch (_) {}
79
69
  },
80
70
 
81
- "file.edited": async ({ event }) => {
82
- try {
83
- const { sessionID } = event.properties ?? {}
84
- if (!sessionID) return
85
- const state = sessionState.get(sessionID)
86
- if (state) state.editCount++
87
- } catch (_) {}
88
- },
89
-
90
- "session.idle": async ({ event }) => {
71
+ "tool.execute.after": async (input, output) => {
91
72
  try {
92
- const sessionId = event.properties?.sessionID
73
+ const sessionId = input?.sessionID ?? input?.session_id
93
74
  if (!sessionId) return
94
75
 
95
76
  const state = sessionState.get(sessionId)
96
77
  if (!state) return
97
78
 
98
- const { agentName, editCount, skills } = state
99
- const skillsSummary = skills.length > 0 ? skills.join(", ") : "none"
100
-
101
- appendLog(
102
- directory,
103
- `| ${ts()} | ${agentName} | completed | ${editCount} files edited, skills: ${skillsSummary} |`
104
- )
105
-
106
- sessionState.delete(sessionId)
79
+ if (input?.tool === "read") {
80
+ const filePath = input?.args?.filePath ?? ""
81
+ const match = filePath.match(/[/\\]skills[/\\]([^/\\]+)[/\\]SKILL\.md$/i)
82
+ if (match) {
83
+ const skillName = match[1]
84
+ if (!state.skills.includes(skillName)) state.skills.push(skillName)
85
+ appendEntry(directory, { ts: ts(), agent: state.agentName, action: "skill-loaded", skill: skillName })
86
+ }
87
+ }
107
88
  } catch (_) {}
108
89
  },
109
90
  }
package/content/AGENTS.md CHANGED
@@ -67,75 +67,6 @@ Replace the entire contents of this file (`AGENTS.md`) with everything below the
67
67
 
68
68
  ---
69
69
 
70
- ### Step 4b, Patch opsx-apply for ensemble
71
-
72
- Read **both** `.opencode/commands/opsx-apply.md` AND `.opencode/skills/openspec-apply-change/SKILL.md`. They contain the same solo implementation loop. Patch both identically.
73
-
74
- In each file, find the step that instructs the agent to **implement tasks directly**, it will contain phrases like "make the code changes", "implement tasks", "loop until done or blocked". This step tells the agent to write code itself.
75
-
76
- **Replace that step and everything after it** (completion output, pause output, guardrails, fluid workflow) with the following:
77
-
78
- ```markdown
79
- 6. **Implement via ensemble team**
80
-
81
- NEVER implement tasks directly. Always delegate to specialists via ensemble.
82
-
83
- a. Create feature branch if not already on one: `feature/{id}-{slug}`
84
- b. Create team:
85
- ```
86
- team_create "<change-name>"
87
- ```
88
- Announce: "Team running. Monitor at http://localhost:4747/"
89
-
90
- c. Spawn only what the tasks require (in parallel):
91
- ```
92
- team_spawn name:front agent:front-engineer → UI/frontend tasks
93
- team_spawn name:back agent:back-engineer → backend/API tasks
94
- team_spawn name:infra agent:infra-engineer → infra/pipeline tasks
95
- ```
96
- Pass each specialist: their relevant tasks + all context file paths from step above.
97
- Log each spawn to `.agents/session-log.md` (see Session Log section).
98
-
99
- d. Wait for all → `team_results` → `team_shutdown` + `team_merge`
100
-
101
- 7. **Quality check**
102
-
103
- ```
104
- team_spawn name:quality agent:quality-engineer
105
- ```
106
- Wait → `team_results` → fix blockers → `team_shutdown`
107
-
108
- 8. **Mark tasks complete in openspec**
109
-
110
- After specialists finish, update the tasks file: `- [ ]` → `- [x]` for each completed task.
111
- Run `openspec status --change "<name>" --json` to confirm progress.
112
-
113
- 9. **On completion or pause, show status**
114
-
115
- Display:
116
- - Tasks completed this session
117
- - Overall progress: "N/M tasks complete"
118
- - If all done: suggest archive with `/opsx-archive`
119
- - If paused: explain why and wait for guidance
120
-
121
- Then run `team_cleanup`.
122
-
123
- **Guardrails**
124
- - NEVER implement tasks directly, always use `team_create` + `team_spawn`, no exceptions
125
- - "Small feature", "faster to do it directly", "environment issues" are NOT valid reasons to skip ensemble, if you find yourself thinking this, stop and spawn anyway
126
- - Always read context files before spawning (from the apply instructions output)
127
- - Always pass context file paths and task list to spawned specialists
128
- - Mark tasks complete in openspec AFTER specialists finish, not before
129
- - If task is ambiguous, pause and ask before spawning
130
- - If implementation reveals issues, pause and suggest artifact updates
131
- - Pause on errors, blockers, or unclear requirements, don't guess
132
- - Use contextFiles from CLI output, don't assume specific file names
133
- ```
134
-
135
- Keep all steps before the implementation step unchanged in both files, they are openspec's domain (select change, check status, get instructions, read context, show progress).
136
-
137
- ---
138
-
139
70
  ### Step 5, Confirm
140
71
 
141
72
  Tell the user:
@@ -245,9 +176,7 @@ devops-manager (ship mode)
245
176
  ### Phase 2, Implement
246
177
 
247
178
  ```
248
- 1. Run /opsx-apply (or load skill openspec-apply-change)
249
- The command handles context reading, ensemble orchestration, and task marking automatically.
250
- DO NOT implement tasks directly, the command spawns specialists via ensemble.
179
+ 1. Run /opsx-apply, handles context reading, ensemble orchestration, and task marking.
251
180
  2. After /opsx-apply completes, proceed to quality check.
252
181
  ```
253
182
 
@@ -345,41 +274,6 @@ Example: `feature/42-add-user-auth`
345
274
 
346
275
  ---
347
276
 
348
- ## Session Log
349
-
350
- <!-- session-logging: enabled -->
351
-
352
- All agents MUST log their activity to `.agents/session-log.md`.
353
-
354
- **Check before logging:** Read the `session-logging` comment above. If it says `disabled`, skip all logging.
355
-
356
- **Every agent MUST create or append to this file when it starts, no exceptions.** If the file does not exist, create it with this exact header first:
357
-
358
- ```markdown
359
- # Session Log
360
-
361
- | Timestamp | Agent | Action | Detail |
362
- |-----------|-------|--------|--------|
363
- ```
364
-
365
- Then immediately append a `started` row.
366
-
367
- **Log these events** by appending a row:
368
- - Lead spawns an agent → `| {ISO timestamp} | lead | spawned | {agent-name}, {purpose} |`
369
- - Agent starts → `| {ISO timestamp} | {agent-name} | started | {task summary} |`
370
- - Agent loads a skill → `| {ISO timestamp} | {agent-name} | skill-loaded | {skill-name} |`
371
- - Agent completes → `| {ISO timestamp} | {agent-name} | completed | {files changed count} files, skills: {comma-separated skill names or none} |`
372
- - Agent blocked → `| {ISO timestamp} | {agent-name} | blocked | {reason} |`
373
-
374
- **Rules:**
375
- - Creating the file and logging `started` is mandatory, do it before any other work
376
- - Append only, never overwrite previous entries
377
- - One row per event, keep detail column short
378
- - Use ISO 8601 timestamps
379
- - The file is gitignored, never commit it
380
-
381
- ---
382
-
383
277
  ## RTK
384
278
 
385
279
  Use `rtk` wrapper for ALL CLI commands. Never run git, az, gh, or openspec commands directly.
@@ -411,7 +305,7 @@ Agents CANNOT:
411
305
 
412
306
  ### Platform CLI
413
307
 
414
- ALL platform interactions via CLI only. Browser MCP and webfetch FORBIDDEN for any DevOps or GitHub operation use `gh` or `az` CLI exclusively, never fall back to HTTP requests.
308
+ ALL platform interactions via CLI only. Browser MCP and webfetch FORBIDDEN for any DevOps or GitHub operation, use `gh` or `az` CLI exclusively, never fall back to HTTP requests.
415
309
 
416
310
  | Operation | Azure DevOps | GitHub |
417
311
  |-----------|-------------|--------|
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-onboard",
3
- "version": "0.1.8",
3
+ "version": "0.1.12",
4
4
  "description": "Prepare any brownfield codebase for AI agent workflows using OpenCode, OpenSpec, and ensemble orchestration.",
5
5
  "keywords": [
6
6
  "opencode",
package/src/index.js CHANGED
@@ -10,7 +10,6 @@ import { chooseSkillsProvider } from './steps/choose-skills-provider.js'
10
10
  import { cleanAiFiles } from './steps/clean-ai-files.js'
11
11
  import { copyContentStep } from './steps/copy-content.js'
12
12
  import { initOpenspec } from './steps/init-openspec.js'
13
- import { installBrowser } from './steps/install-browser.js'
14
13
 
15
14
  if (process.stdout.isTTY) console.clear()
16
15
  console.log()
@@ -132,7 +132,22 @@ export async function chooseModels() {
132
132
  const config = await fse.readJson(opencodeJsonPath)
133
133
  config.model = planModel
134
134
  await fse.writeJson(opencodeJsonPath, config, { spaces: 2 })
135
- success(`plan model ${planModel} (written to .opencode/opencode.json)`)
135
+ success(`plan model -> ${planModel} (written to .opencode/opencode.json)`)
136
+ }
137
+
138
+ // Write build and fast models to ensemble.json
139
+ const ensembleJsonPath = path.join(process.cwd(), '.opencode', 'ensemble.json')
140
+ if (await fse.pathExists(ensembleJsonPath)) {
141
+ const ensemble = await fse.readJson(ensembleJsonPath)
142
+ delete ensemble.defaultModel
143
+ ensemble.modelsByAgent = {
144
+ ...ensemble.modelsByAgent,
145
+ build: buildModel,
146
+ explore: fastModel,
147
+ }
148
+ await fse.writeJson(ensembleJsonPath, ensemble, { spaces: 2 })
149
+ success(`build model -> ${buildModel} (written to .opencode/ensemble.json)`)
150
+ success(`fast model -> ${fastModel} (written to .opencode/ensemble.json)`)
136
151
  }
137
152
 
138
153
  console.log()
@@ -3,25 +3,45 @@ import path from 'path'
3
3
  import { findAiFiles } from '../utils/copy.js'
4
4
  import { header, info, prompt, success, warn } from '../utils/exec.js'
5
5
 
6
+ /**
7
+ * Enumerate immediate children of a directory, returning their absolute paths.
8
+ * Skips any entry named 'skills' at any level to preserve user-installed skills.
9
+ */
10
+ async function childrenExcludingSkills(dir) {
11
+ const results = []
12
+ if (!await fse.pathExists(dir)) return results
13
+ const entries = await fse.readdir(dir)
14
+ for (const entry of entries) {
15
+ if (entry === 'skills') continue
16
+ results.push(path.join(dir, entry))
17
+ }
18
+ return results
19
+ }
20
+
6
21
  export async function cleanAiFiles() {
7
22
  header('Step 2, Existing AI config files')
8
23
 
9
24
  const cwd = process.cwd()
10
- const found = await findAiFiles(cwd)
11
-
12
- // Also find .agents contents except skills/ (preserve user skills)
13
- const agentsDir = path.join(cwd, '.agents')
14
- const agentsEntries = []
15
- if (await fse.pathExists(agentsDir)) {
16
- const entries = await fse.readdir(agentsDir)
17
- for (const entry of entries) {
18
- if (entry !== 'skills') {
19
- agentsEntries.push(path.join(agentsDir, entry))
20
- }
21
- }
25
+
26
+ // Flat AI config files (not directories)
27
+ const flatFiles = await findAiFiles(cwd)
28
+
29
+ // For directory targets (.opencode, .agents), enumerate children and skip skills/
30
+ const dirTargets = ['.opencode', '.agents']
31
+ const dirEntries = []
32
+ for (const dirName of dirTargets) {
33
+ const dirPath = path.join(cwd, dirName)
34
+ const children = await childrenExcludingSkills(dirPath)
35
+ dirEntries.push(...children)
22
36
  }
23
37
 
24
- const allFiles = [...found, ...agentsEntries]
38
+ // Remove the directory targets themselves from flat list (we handle them via children)
39
+ const filteredFlat = flatFiles.filter(f => {
40
+ const rel = path.relative(cwd, f)
41
+ return !dirTargets.includes(rel)
42
+ })
43
+
44
+ const allFiles = [...filteredFlat, ...dirEntries]
25
45
 
26
46
  if (allFiles.length === 0) {
27
47
  success('No existing AI config files found')
@@ -33,7 +53,7 @@ export async function cleanAiFiles() {
33
53
  info(f.replace(cwd, '.'))
34
54
  }
35
55
  console.log()
36
- prompt('Press Enter to remove them all (your .agents/skills/ will be kept)')
56
+ prompt('Press Enter to remove them all (skills/ folders will be kept)')
37
57
  console.log()
38
58
 
39
59
  await new Promise(resolve => {
@@ -1,6 +1,130 @@
1
1
  import { execa } from 'execa'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
2
4
  import { error, header, success, warn } from '../utils/exec.js'
3
5
 
6
+ const ENSEMBLE_PATCH = `6. **Implement via ensemble team**
7
+
8
+ NEVER implement tasks directly. Always delegate to specialists via ensemble.
9
+ Do NOT touch any source files before the team is running, not even a single edit.
10
+
11
+ Steps MUST be followed in order. Do not skip any step.
12
+
13
+ **Step 6a.** Create feature branch if not already on one: \`feature/{id}-{slug}\`
14
+
15
+ **Step 6b.** Clean up stale state, then create the team:
16
+ \`\`\`
17
+ team_cleanup force:true acknowledge_uncommitted:true
18
+ team_create "<change-name>"
19
+ \`\`\`
20
+ "not in a team" error from team_cleanup is expected, ignore it.
21
+ Announce: "Team running. Monitor at http://localhost:4747/"
22
+
23
+ **Step 6c.** Add ALL tasks to the shared board BEFORE spawning anyone.
24
+ Schema: { content: string, priority: "high"|"medium"|"low" }. No other fields.
25
+ \`\`\`
26
+ team_tasks_add tasks:[
27
+ { content: "1.1 <exact task text from tasks.md>", priority: "high" },
28
+ { content: "1.2 <exact task text>", priority: "high" },
29
+ ...every task from tasks.md, one entry each...
30
+ ]
31
+ \`\`\`
32
+ Save the task IDs returned. You MUST pass them to agents in step 6d.
33
+ DO NOT proceed to 6d until team_tasks_add succeeds.
34
+
35
+ **Step 6d.** Spawn specialists ONE AT A TIME. Wait for each team_spawn result before calling the next.
36
+ Each team_spawn MUST include agent field (required, causes NOT NULL error if omitted).
37
+ Include: full task list, all context file paths, the task IDs for their tasks, and these instructions:
38
+ - call team_claim with the task ID before starting each task
39
+ - call team_tasks_complete with the task ID after finishing each task
40
+ - send results to lead via team_message when all done or blocked
41
+ \`\`\`
42
+ team_spawn name:"back" agent:"back-engineer" prompt:"..."
43
+ (wait for result)
44
+ team_spawn name:"front" agent:"front-engineer" prompt:"..."
45
+ (wait for result)
46
+ team_spawn name:"infra" agent:"infra-engineer" prompt:"..."
47
+ (wait for result)
48
+ \`\`\`
49
+
50
+ **Step 6e.** After all spawns, tell the user what is running, then STOP and wait.
51
+ Do NOT call team_results, team_status, or team_broadcast in a loop.
52
+ Teammates will message you when done or blocked. Wait for those messages.
53
+
54
+ **Step 6f.** When teammates message back: call team_results to get full output,
55
+ then team_shutdown + team_merge for each. Fix any blockers reported.
56
+
57
+ 7. **Quality check**
58
+
59
+ Spawn quality engineer, wait for message back:
60
+ \`\`\`
61
+ team_spawn name:"quality" agent:"quality-engineer" prompt:"<task list, context files, run tests + build + lint + verify acceptance criteria, call team_claim/team_tasks_complete per task, send results to lead when done>"
62
+ \`\`\`
63
+ Wait for message → team_results → fix blockers → team_shutdown + team_merge
64
+
65
+ 8. **Mark tasks complete in openspec**
66
+
67
+ Update tasks.md: \`- [ ]\` → \`- [x]\` for each completed task.
68
+ Run \`rtk openspec status --change "<name>" --json\` to confirm.
69
+
70
+ 9. **Show status, then cleanup**
71
+
72
+ Display:
73
+ - Tasks completed this session
74
+ - Overall progress: "N/M tasks complete"
75
+ - If all done: suggest archive with \`/opsx-archive\`
76
+ - If paused: explain why and wait for guidance
77
+
78
+ Then run \`team_cleanup\`.
79
+
80
+ **Guardrails**
81
+ - NEVER skip or reorder steps 6a-6f
82
+ - NEVER implement tasks directly. Always use team_create + team_spawn, no exceptions
83
+ - NEVER touch source files before team_create is called, not even one edit
84
+ - NEVER call team_spawn without the agent field — it is required and will fail without it
85
+ - NEVER call team_spawn before team_tasks_add — tasks must exist before agents are spawned
86
+ - NEVER poll team_results or team_status in a loop — wait for teammates to message you
87
+ - NEVER claim or complete tasks as lead — only subagents call team_claim and team_tasks_complete
88
+ - ALWAYS run team_cleanup force:true before team_create to clear stale state
89
+ - ALWAYS add every task from tasks.md to the board with team_tasks_add before spawning
90
+ - ALWAYS pass the task IDs returned by team_tasks_add to each agent's spawn prompt
91
+ - ALWAYS spawn one at a time, waiting for each result before the next (avoids worktree contention)
92
+ - If teammates are stuck, use team_message to resend tasks, then wait — never implement directly
93
+ - Mark tasks complete in openspec AFTER specialists finish, not before
94
+ - Pause on errors, blockers, or unclear requirements. Do not guess
95
+ - Use contextFiles from CLI output, do not assume specific file paths
96
+ `
97
+
98
+ // Patterns that identify the solo implementation step in openspec-generated files
99
+ const SOLO_IMPL_PATTERNS = [
100
+ /^#{1,3}\s+\d+\..*(implement|loop until|make the code changes|for each (pending )?task)/im,
101
+ /\*\*Implement tasks.*loop until/im,
102
+ /^6\.\s+\*\*Implement tasks/im,
103
+ ]
104
+
105
+ function patchOpensxApply(filePath) {
106
+ if (!fs.existsSync(filePath)) return false
107
+
108
+ const content = fs.readFileSync(filePath, 'utf8')
109
+ const lines = content.split('\n')
110
+
111
+ // Find the line index where the solo implementation step starts
112
+ let cutLine = -1
113
+ for (let i = 0; i < lines.length; i++) {
114
+ if (SOLO_IMPL_PATTERNS.some(p => p.test(lines[i]))) {
115
+ cutLine = i
116
+ break
117
+ }
118
+ }
119
+
120
+ if (cutLine === -1) return false // Pattern not found, skip
121
+
122
+ const preamble = lines.slice(0, cutLine).join('\n').trimEnd()
123
+ const patched = preamble + '\n\n' + ENSEMBLE_PATCH
124
+ fs.writeFileSync(filePath, patched, 'utf8')
125
+ return true
126
+ }
127
+
4
128
  export async function initOpenspec() {
5
129
  header('Step 6, Initializing OpenSpec')
6
130
 
@@ -19,4 +143,20 @@ export async function initOpenspec() {
19
143
  } catch (err) {
20
144
  error(`Failed to run openspec init: ${err.message}`)
21
145
  }
146
+
147
+ // Patch opsx-apply.md to use ensemble orchestration instead of solo implementation
148
+ const targets = [
149
+ path.join(process.cwd(), '.opencode', 'commands', 'opsx-apply.md'),
150
+ path.join(process.cwd(), '.opencode', 'skills', 'openspec-apply-change', 'SKILL.md'),
151
+ ]
152
+
153
+ for (const target of targets) {
154
+ try {
155
+ const patched = patchOpensxApply(target)
156
+ if (patched) success(`Patched ${path.relative(process.cwd(), target)} for ensemble`)
157
+ } catch (err) {
158
+ warn(`Could not patch ${path.relative(process.cwd(), target)}: ${err.message}`)
159
+ }
160
+ }
22
161
  }
162
+
package/src/utils/copy.js CHANGED
@@ -40,6 +40,8 @@ const AI_FILES = [
40
40
  'copilot-instructions.md',
41
41
  '.aider.conf.yml',
42
42
  '.aider',
43
+ '.opencode',
44
+ '.agents'
43
45
  ]
44
46
 
45
47
  export async function findAiFiles(dir) {