opencode-onboard 0.4.11 → 0.4.15
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/README.md +14 -14
- package/content/{.agents → .opencode}/agents/basic-engineer.md +2 -3
- package/content/{.agents → .opencode}/agents/devops-manager.md +18 -19
- package/content/.opencode/commands/ob-create-engineer.md +16 -17
- package/content/.opencode/commands/opsx-apply.md +6 -6
- package/content/.opencode/skills/openspec-apply-change/SKILL.md +6 -6
- package/content/AGENTS.md +44 -44
- package/package.json +1 -1
- package/src/commands/wizard.js +1 -1
- package/src/steps/copy/agents.js +5 -5
- package/src/steps/copy/agents.test.js +2 -2
- package/src/steps/models/index.js +4 -4
- package/src/steps/openspec/ensemble.js +4 -4
- package/src/steps/openspec/ensemble.test.js +1 -1
- package/src/steps/openspec/index.js +3 -3
- package/src/steps/optimization/codegraph.js +3 -3
- package/src/steps/optimization/codegraph.test.js +5 -5
- package/content/.agents/session-log.json +0 -41
- package/content/.opencode/plugins/session-log.js +0 -523
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**One command to prepare any codebase for AI agent workflows in OpenCode.**
|
|
8
8
|
|
|
9
|
-
Works with [OpenCode](https://opencode.ai), [OpenCode Ensemble](https://github.com/hueyexe/opencode-ensemble), [OpenSpec](https://github.com/fission-ai/openspec), GitHub, Azure DevOps, or no tracker/PR platform at all.
|
|
9
|
+
Works with [OpenCode](https://opencode.ai), [OpenCode Ensemble](https://github.com/hueyexe/opencode-ensemble), [OpenSpec](https://github.com/fission-ai/openspec), GitHub, Azure DevOps, or no tracker/PR platform at all.
|
|
10
10
|
|
|
11
11
|
[](https://www.npmjs.com/package/opencode-onboard)
|
|
12
12
|
[](https://www.npmjs.com/package/opencode-onboard)
|
|
@@ -74,8 +74,8 @@ The CLI runs a 10-step onboarding wizard. It keeps the current step visible, plu
|
|
|
74
74
|
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
75
75
|
| **1. Source scope** | Choose current repo or sibling source roots for code analysis |
|
|
76
76
|
| **2. Clean AI files** | Detects existing `AGENTS.md`, `.cursorrules`, `CLAUDE.md`, `.agents/` etc. and removes them, preserves your `.agents/skills/` |
|
|
77
|
-
| **3. Choose platform** | GitHub, Azure DevOps, or None |
|
|
78
|
-
| **4. Check platform CLI** | Verifies `gh` (GitHub) or `az` + `azure-devops` (Azure DevOps), or skips checks when platform is None |
|
|
77
|
+
| **3. Choose platform** | GitHub, Azure DevOps, or None |
|
|
78
|
+
| **4. Check platform CLI** | Verifies `gh` (GitHub) or `az` + `azure-devops` (Azure DevOps), or skips checks when platform is None |
|
|
79
79
|
| **5. Copy scaffolding** | Copies agents + built-in skills + bootstrap docs, writes source-roots metadata, applies AGENTS bootstrap patching, copies `skills-lock.json`, then runs `npx skills` |
|
|
80
80
|
| **6. Init OpenSpec** | Runs `npx @fission-ai/openspec init` silently for structured change management |
|
|
81
81
|
| **7. Choose models** | Fetches live model list from [models.dev](https://models.dev), lets you pick plan / build / fast models with cost indicators and canonical pricing |
|
|
@@ -100,7 +100,7 @@ Custom slash commands are installed into `.opencode/commands/` and are available
|
|
|
100
100
|
| Command | Description |
|
|
101
101
|
| -------------- | ----------------------------------------------------------------------------------------------------- |
|
|
102
102
|
| `/ob-init` | Initialize the project. Asks greenfield vs brownfield, then activates the agent team. Supports skipping doc generation for new projects. |
|
|
103
|
-
| `/ob-plan <url>` | Parse a user story URL and produce a plan, proposal, specs, and tasks. Stops before implementation. Use platform mode, not `None`. |
|
|
103
|
+
| `/ob-plan <url>` | Parse a user story URL and produce a plan, proposal, specs, and tasks. Stops before implementation. Use platform mode, not `None`. |
|
|
104
104
|
| `/ob-main <task>` | Quick direct implementation, no OpenSpec, no ensemble, no PRs. Just do it. |
|
|
105
105
|
| `/ob-create-engineer <name> "<description>"` | Create a custom engineer agent from a description, with skills auto-installed from [skills.sh](https://www.skills.sh/) |
|
|
106
106
|
| `/ob-create-architecture` | Generate or regenerate `ARCHITECTURE.md` from the codebase. Safe to rerun any time the architecture changes. |
|
|
@@ -123,14 +123,14 @@ devops-manager lead/orchestrator, planning, PR lifecycle
|
|
|
123
123
|
basic-engineer implementation worker, ability-driven
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
`basic-engineer` behavior is composed by abilities, not hardcoded role silos.
|
|
127
|
-
Project-specific specialization comes from user-created custom engineers via `/ob-create-engineer`. During `/opsx-apply`, the lead should inspect the engineers that actually exist in `.
|
|
126
|
+
`basic-engineer` behavior is composed by abilities, not hardcoded role silos.
|
|
127
|
+
Project-specific specialization comes from user-created custom engineers via `/ob-create-engineer`. During `/opsx-apply`, the lead should inspect the engineers that actually exist in `.opencode/agents/`, prefer matching custom engineers, and fall back to `basic-engineer` only when no specialist is a clear fit.
|
|
128
128
|
|
|
129
|
-
### Skills, platform knowledge
|
|
130
|
-
|
|
131
|
-
Skills define _what to know_. They provide project rules, platform behavior, and task-specific execution guidance. Agents auto-detect/load relevant skills; **you do not manually choose skills per prompt**.
|
|
132
|
-
|
|
133
|
-
If you choose platform `None` during onboarding, no userstory or pull-request platform skills are injected into the workflow. The project works from direct conversation, local repo context, and optional OpenSpec artifacts only.
|
|
129
|
+
### Skills, platform knowledge
|
|
130
|
+
|
|
131
|
+
Skills define _what to know_. They provide project rules, platform behavior, and task-specific execution guidance. Agents auto-detect/load relevant skills; **you do not manually choose skills per prompt**.
|
|
132
|
+
|
|
133
|
+
If you choose platform `None` during onboarding, no userstory or pull-request platform skills are injected into the workflow. The project works from direct conversation, local repo context, and optional OpenSpec artifacts only.
|
|
134
134
|
|
|
135
135
|
Current loading model:
|
|
136
136
|
|
|
@@ -179,7 +179,7 @@ Models are fetched live from [models.dev](https://models.dev) (3000+ models, cac
|
|
|
179
179
|
|
|
180
180
|
## The pipeline
|
|
181
181
|
|
|
182
|
-
When you give the lead agent a work item URL, execution follows this pipeline. If onboarding platform is `None`, skip the work item / PR stages and work directly from conversation plus optional OpenSpec artifacts:
|
|
182
|
+
When you give the lead agent a work item URL, execution follows this pipeline. If onboarding platform is `None`, skip the work item / PR stages and work directly from conversation plus optional OpenSpec artifacts:
|
|
183
183
|
|
|
184
184
|
```
|
|
185
185
|
devops-manager (load ob-global first)
|
|
@@ -191,7 +191,7 @@ devops-manager (load ob-global first)
|
|
|
191
191
|
↓
|
|
192
192
|
[confirm with user]
|
|
193
193
|
↓
|
|
194
|
-
basic-engineer +
|
|
194
|
+
basic-engineer + *-engineer (parallel)
|
|
195
195
|
claim tasks → load abilities → implement
|
|
196
196
|
↓
|
|
197
197
|
verify (tests/build/lint as needed)
|
|
@@ -218,7 +218,7 @@ Each agent runs in its own isolated git worktree via [OpenCode Ensemble](https:/
|
|
|
218
218
|
|
|
219
219
|
```
|
|
220
220
|
your-project/
|
|
221
|
-
├── AGENTS.md ← bootstrap mode, replaced after first "init"
|
|
221
|
+
├── AGENTS.md ← bootstrap mode, replaced after first "/ob-init"
|
|
222
222
|
├── ARCHITECTURE.md ← prompt for agents to fill in from your codebase
|
|
223
223
|
├── DESIGN.md ← prompt for agents to fill in from your codebase
|
|
224
224
|
├── .opencode/
|
|
@@ -25,10 +25,10 @@ Skills are located in `.agents/skills/`. Load required skills explicitly from co
|
|
|
25
25
|
|
|
26
26
|
Always load `@ob-global` FIRST before any other skill.
|
|
27
27
|
|
|
28
|
-
Platform skill selection must follow onboarding platform choice from CLI step:
|
|
29
|
-
<!-- OB-PLATFORM-SKILLS-START -->
|
|
30
|
-
- Platform-specific skill instructions are injected during onboarding copy step.
|
|
31
|
-
<!-- OB-PLATFORM-SKILLS-END -->
|
|
28
|
+
Platform skill selection must follow onboarding platform choice from CLI step:
|
|
29
|
+
<!-- OB-PLATFORM-SKILLS-START -->
|
|
30
|
+
- Platform-specific skill instructions are injected during onboarding copy step.
|
|
31
|
+
<!-- OB-PLATFORM-SKILLS-END -->
|
|
32
32
|
|
|
33
33
|
1. If the spawn prompt lists specific skills to load, read those `SKILL.md` files FIRST before any implementation
|
|
34
34
|
2. Additionally, identify the platform from URLs or context
|
|
@@ -39,16 +39,16 @@ Examples of intent → skill mapping:
|
|
|
39
39
|
- "PR has comments" or "review feedback" → load platform pullrequest observer skill
|
|
40
40
|
- URL-based platform inference is fallback-only when onboarding metadata is unavailable
|
|
41
41
|
|
|
42
|
-
Rules:
|
|
43
|
-
- Platform selected in onboarding metadata takes precedence over URL inference when both exist
|
|
44
|
-
- Never interact with a platform without loading the matching skill first
|
|
45
|
-
- Follow skill instructions exactly, do not partially apply them
|
|
46
|
-
- If no skill exists for the platform, report it as a blocker rather than improvising
|
|
47
|
-
- Skills listed in the spawn prompt are MANDATORY, not optional
|
|
48
|
-
|
|
49
|
-
<!-- OB-PLATFORM-MODE-START -->
|
|
50
|
-
This project uses platform-integrated workflow modes described below.
|
|
51
|
-
<!-- OB-PLATFORM-MODE-END -->
|
|
42
|
+
Rules:
|
|
43
|
+
- Platform selected in onboarding metadata takes precedence over URL inference when both exist
|
|
44
|
+
- Never interact with a platform without loading the matching skill first
|
|
45
|
+
- Follow skill instructions exactly, do not partially apply them
|
|
46
|
+
- If no skill exists for the platform, report it as a blocker rather than improvising
|
|
47
|
+
- Skills listed in the spawn prompt are MANDATORY, not optional
|
|
48
|
+
|
|
49
|
+
<!-- OB-PLATFORM-MODE-START -->
|
|
50
|
+
This project uses platform-integrated workflow modes described below.
|
|
51
|
+
<!-- OB-PLATFORM-MODE-END -->
|
|
52
52
|
|
|
53
53
|
## Working Modes
|
|
54
54
|
|
|
@@ -65,11 +65,10 @@ This project uses platform-integrated workflow modes described below.
|
|
|
65
65
|
3. Resolve platform from `.opencode/opencode-onboard.json` (`wizard.platform`) when available; fallback to URL inference only if missing/ambiguous
|
|
66
66
|
4. Load the matching pullrequest skill for that resolved platform
|
|
67
67
|
5. Capture screenshots of local running app if UI changes exist
|
|
68
|
-
6.
|
|
69
|
-
7.
|
|
70
|
-
8.
|
|
71
|
-
9.
|
|
72
|
-
10. Report PR URL to the lead
|
|
68
|
+
6. Commit and push the feature branch. If `## Source Roots` lists multiple roots, each root is a separate git repository, create and push the feature branch in EACH repo that has changes; never assume a single repo
|
|
69
|
+
7. Create the PR following the skill instructions (one PR per repo that has changes)
|
|
70
|
+
8. Post PR comment with screenshots and change summary
|
|
71
|
+
9. Report PR URL to the lead
|
|
73
72
|
|
|
74
73
|
### Feedback Mode (PR review loop)
|
|
75
74
|
1. Load `@ob-global` first
|
|
@@ -40,14 +40,13 @@ Example: `/ob-create-engineer frontend-engineer "A frontend engineer specialized
|
|
|
40
40
|
|
|
41
41
|
4. **Create the agent file**
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
Create `.opencode/agents/<name>.md` with this structure:
|
|
44
44
|
|
|
45
45
|
```markdown
|
|
46
46
|
---
|
|
47
47
|
description: <description>
|
|
48
|
-
mode:
|
|
49
|
-
color: <pick a
|
|
50
|
-
temperature: 0.2
|
|
48
|
+
mode: primary
|
|
49
|
+
color: <pick a theme color: primary|secondary|accent|success|warning|error|info>
|
|
51
50
|
permission:
|
|
52
51
|
edit: allow
|
|
53
52
|
bash: allow
|
|
@@ -65,17 +64,17 @@ Example: `/ob-create-engineer frontend-engineer "A frontend engineer specialized
|
|
|
65
64
|
## Workflow
|
|
66
65
|
|
|
67
66
|
When spawned by the lead:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
67
|
+
1. Call `team_tasks_list` immediately and identify your assigned task IDs.
|
|
68
|
+
2. Claim the first assigned task that is unblocked with `team_claim task_id:<id>`. If the first assigned task is blocked, claim the next assigned task whose dependencies are already `done`. Do not wait once you have an unblocked assigned task.
|
|
69
|
+
3. After claiming, load `@ob-global` first, then load mandatory ability `Guardrails`.
|
|
70
|
+
4. Load additional abilities from the `## Abilities` section as needed for the claimed task domain (for example: development, testing, infrastructure). Each ability can include one or more skills; load all relevant skills listed under each selected ability.
|
|
71
|
+
5. Send a short `team_message` to lead confirming the claimed task ID and loaded skills.
|
|
72
|
+
6. Implement the task following all loaded skill rules.
|
|
73
|
+
7. Call `team_tasks_complete task_id:<id>` after finishing that task.
|
|
74
|
+
8. Repeat until all currently assigned tasks are completed or blocked.
|
|
75
|
+
9. Message lead with results via `team_message`. Lead may assign more tasks, do NOT stop working or shut down until lead confirms no more tasks for you.
|
|
76
|
+
10. If lead sends new task IDs via `team_message`, treat them as new assignments and go back to step 1.
|
|
77
|
+
```
|
|
79
78
|
|
|
80
79
|
Place the installed skills under the most relevant ability category:
|
|
81
80
|
- **Development** — language frameworks, UI libraries, application code skills
|
|
@@ -88,13 +87,13 @@ Example: `/ob-create-engineer frontend-engineer "A frontend engineer specialized
|
|
|
88
87
|
|
|
89
88
|
Add the new agent to the agents table in AGENTS.md:
|
|
90
89
|
```
|
|
91
|
-
|
|
90
|
+
| `<name>` | .opencode/agents/<name>.md | <short role description> |
|
|
92
91
|
```
|
|
93
92
|
|
|
94
93
|
6. **Show summary**
|
|
95
94
|
|
|
96
95
|
Report:
|
|
97
|
-
|
|
96
|
+
- Agent file created at `.opencode/agents/<name>-engineer.md`
|
|
98
97
|
- Skills installed (list each with source)
|
|
99
98
|
- How to use: "This agent will be spawned by the lead during `/opsx-apply` for tasks matching its specialty."
|
|
100
99
|
|
|
@@ -117,15 +117,15 @@ Implement tasks from an OpenSpec change using the ensemble agent team.
|
|
|
117
117
|
- Only shut down an agent when the board has no more tasks for its domain.
|
|
118
118
|
|
|
119
119
|
Before spawning:
|
|
120
|
-
- scan `.
|
|
120
|
+
- scan `.opencode/agents/` and list the engineers that actually exist in this project
|
|
121
121
|
- exclude `devops-manager` from implementation selection
|
|
122
122
|
- read each engineer's description and abilities
|
|
123
123
|
- prefer the most specialized custom engineer whose description and abilities match the task
|
|
124
124
|
- use `basic-engineer` only when no custom engineer is a clear fit or as a recovery fallback
|
|
125
|
-
- never spawn an engineer name that is not present in `.
|
|
125
|
+
- never spawn an engineer name that is not present in `.opencode/agents/`
|
|
126
126
|
|
|
127
127
|
REQUIRED assignment algorithm (do not skip):
|
|
128
|
-
1. Build candidate list from `.
|
|
128
|
+
1. Build candidate list from `.opencode/agents/*.md` excluding `devops-manager`.
|
|
129
129
|
2. Classify each task by domain using task text (api/backend, ui/frontend, infra/devops, testing/qa).
|
|
130
130
|
3. For each task, score every candidate agent:
|
|
131
131
|
- +3 if agent description explicitly matches domain
|
|
@@ -137,7 +137,7 @@ Implement tasks from an OpenSpec change using the ensemble agent team.
|
|
|
137
137
|
|
|
138
138
|
HARD RULES:
|
|
139
139
|
- NEVER assign a task to `basic-engineer` if a specialized agent has higher score.
|
|
140
|
-
- NEVER skip agent discovery from `.
|
|
140
|
+
- NEVER skip agent discovery from `.opencode/agents/*.md`.
|
|
141
141
|
- ALWAYS include assignment rationale only when it changes task routing or is needed to justify using `basic-engineer`.
|
|
142
142
|
|
|
143
143
|
Skill loading is worker-driven:
|
|
@@ -217,7 +217,7 @@ Implement tasks from an OpenSpec change using the ensemble agent team.
|
|
|
217
217
|
- `team_merge member:"<name>"`
|
|
218
218
|
- If team_merge blocks on local changes: `git stash`, retry merge, `git stash pop`.
|
|
219
219
|
6. **If ALL agents are shut down and tasks remain unassigned** (new domain, dependencies unblocked):
|
|
220
|
-
- Discover the remaining matching engineers from `.
|
|
220
|
+
- Discover the remaining matching engineers from `.opencode/agents/` and spawn a new wave (back to step 6d).
|
|
221
221
|
7. **If ALL tasks are done:** proceed to step 7.
|
|
222
222
|
|
|
223
223
|
**ZERO PENDING TASKS GUARANTEE:** Before proceeding to step 7, call `team_tasks_list` and verify EVERY task is either `done` or `blocked`. If any task is `pending` and unassigned, assign it to an agent or spawn a new one. Never leave pending tasks orphaned.
|
|
@@ -264,7 +264,7 @@ Implement tasks from an OpenSpec change using the ensemble agent team.
|
|
|
264
264
|
- ALWAYS call team_tasks_list after each agent reports done to check for remaining unassigned tasks
|
|
265
265
|
- ALWAYS repeat the same literal task IDs in any task assignment message, never send a generic "claim your first task" without the actual IDs
|
|
266
266
|
- NEVER send a start message that omits task IDs; if a task ID is missing from the start message, the agent cannot claim
|
|
267
|
-
- ALWAYS discover engineers from `.
|
|
267
|
+
- ALWAYS discover engineers from `.opencode/agents/` and prefer matching custom engineers over `basic-engineer`
|
|
268
268
|
- ALWAYS spawn agents sequentially (wait for each team_spawn result before the next), then send start messages to all of them together
|
|
269
269
|
- ALWAYS set `claim_task` for the first unblocked task in each initial batch and instruct agents to claim before any other work
|
|
270
270
|
- ALWAYS shut down + merge agents only when no more tasks remain for their domain
|
|
@@ -123,15 +123,15 @@ Implement tasks from an OpenSpec change using the ensemble agent team.
|
|
|
123
123
|
- Only shut down an agent when the board has no more tasks for its domain.
|
|
124
124
|
|
|
125
125
|
Before spawning:
|
|
126
|
-
- scan `.
|
|
126
|
+
- scan `.opencode/agents/` and list the engineers that actually exist in this project
|
|
127
127
|
- exclude `devops-manager` from implementation selection
|
|
128
128
|
- read each engineer's description and abilities
|
|
129
129
|
- prefer the most specialized custom engineer whose description and abilities match the task
|
|
130
130
|
- use `basic-engineer` only when no custom engineer is a clear fit or as a recovery fallback
|
|
131
|
-
- never spawn an engineer name that is not present in `.
|
|
131
|
+
- never spawn an engineer name that is not present in `.opencode/agents/`
|
|
132
132
|
|
|
133
133
|
REQUIRED assignment algorithm (do not skip):
|
|
134
|
-
1. Build candidate list from `.
|
|
134
|
+
1. Build candidate list from `.opencode/agents/*.md` excluding `devops-manager`.
|
|
135
135
|
2. Classify each task by domain using task text (api/backend, ui/frontend, infra/devops, testing/qa).
|
|
136
136
|
3. For each task, score every candidate agent:
|
|
137
137
|
- +3 if agent description explicitly matches domain
|
|
@@ -143,7 +143,7 @@ Implement tasks from an OpenSpec change using the ensemble agent team.
|
|
|
143
143
|
|
|
144
144
|
HARD RULES:
|
|
145
145
|
- NEVER assign a task to `basic-engineer` if a specialized agent has higher score.
|
|
146
|
-
- NEVER skip agent discovery from `.
|
|
146
|
+
- NEVER skip agent discovery from `.opencode/agents/*.md`.
|
|
147
147
|
- ALWAYS include assignment rationale only when it changes task routing or is needed to justify using `basic-engineer`.
|
|
148
148
|
|
|
149
149
|
Skill loading is worker-driven:
|
|
@@ -223,7 +223,7 @@ Implement tasks from an OpenSpec change using the ensemble agent team.
|
|
|
223
223
|
- `team_merge member:"<name>"`
|
|
224
224
|
- If team_merge blocks on local changes: `git stash`, retry merge, `git stash pop`.
|
|
225
225
|
6. **If ALL agents are shut down and tasks remain unassigned** (new domain, dependencies unblocked):
|
|
226
|
-
- Discover the remaining matching engineers from `.
|
|
226
|
+
- Discover the remaining matching engineers from `.opencode/agents/` and spawn a new wave (back to step 6d).
|
|
227
227
|
7. **If ALL tasks are done:** proceed to step 7.
|
|
228
228
|
|
|
229
229
|
**ZERO PENDING TASKS GUARANTEE:** Before proceeding to step 7, call `team_tasks_list` and verify EVERY task is either `done` or `blocked`. If any task is `pending` and unassigned, assign it to an agent or spawn a new one. Never leave pending tasks orphaned.
|
|
@@ -270,7 +270,7 @@ Implement tasks from an OpenSpec change using the ensemble agent team.
|
|
|
270
270
|
- NEVER send a start message that omits task IDs; if a task ID is missing from the start message, the agent cannot claim
|
|
271
271
|
- NEVER edit files between team_spawn and team_merge, team_merge blocks on overlapping local changes
|
|
272
272
|
- ALWAYS add every task to the board before spawning, using multiple `team_tasks_add` calls when dependency wiring requires it
|
|
273
|
-
- ALWAYS discover engineers from `.
|
|
273
|
+
- ALWAYS discover engineers from `.opencode/agents/` and prefer matching custom engineers over `basic-engineer`
|
|
274
274
|
- ALWAYS spawn agents sequentially (wait for each team_spawn result before the next), then send start messages to all of them together
|
|
275
275
|
- ALWAYS set `claim_task` for the first unblocked task in each initial batch and instruct agents to claim before any other work
|
|
276
276
|
- ALWAYS shut down + merge agents only when no more tasks remain for their domain
|
package/content/AGENTS.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## Trigger
|
|
8
8
|
|
|
9
|
-
When the user says anything resembling initialization, "init", "initialize", "setup", "start", "bootstrap", "get started", "prepare", execute the steps below. Follow the greenfield/brownfield branching exactly.
|
|
9
|
+
When the user says anything resembling initialization, "/ob-init", "initialize", "setup", "start", "bootstrap", "get started", "prepare", execute the steps below. Follow the greenfield/brownfield branching exactly.
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -174,10 +174,10 @@ Source scope is defined by mandatory `ob-global` skill.
|
|
|
174
174
|
- Follow the generated `## Source Roots` section from that skill.
|
|
175
175
|
- Do not duplicate source-scope rules here.
|
|
176
176
|
|
|
177
|
-
## I Am the Lead, Full Workflow Ownership
|
|
178
|
-
|
|
179
|
-
<!-- OB-PLATFORM-WORKFLOW-START -->
|
|
180
|
-
When the user provides a work item URL or says "implement the plan" or "I've added comments to the PR", **I own the full lifecycle**. I load `ob-global` skill first, then the appropriate userstory skill, and use ensemble tools to coordinate the agent team.
|
|
177
|
+
## I Am the Lead, Full Workflow Ownership
|
|
178
|
+
|
|
179
|
+
<!-- OB-PLATFORM-WORKFLOW-START -->
|
|
180
|
+
When the user provides a work item URL or says "implement the plan" or "I've added comments to the PR", **I own the full lifecycle**. I load `ob-global` skill first, then the appropriate userstory skill, and use ensemble tools to coordinate the agent team.
|
|
181
181
|
|
|
182
182
|
Trigger patterns, I recognize ALL of these, exact wording does not matter:
|
|
183
183
|
- User pastes or mentions a GitHub Issue URL → load `ob-userstory-gh` skill → parse issue → run `/opsx-propose` → confirm with user → run `/opsx-apply` → ship
|
|
@@ -186,20 +186,20 @@ Trigger patterns, I recognize ALL of these, exact wording does not matter:
|
|
|
186
186
|
- `I've added comments to the PR` → read PR comments → fix → update PR
|
|
187
187
|
- Any GitHub/Azure DevOps PR URL in a feedback/fix request (e.g. "check comments", "fix PR feedback") → run PR Feedback Loop
|
|
188
188
|
|
|
189
|
-
**A GitHub or Azure DevOps URL anywhere in the user's message is always a trigger, regardless of surrounding words.**
|
|
190
|
-
<!-- OB-PLATFORM-WORKFLOW-END -->
|
|
191
|
-
|
|
192
|
-
**Never delegate without a plan. Default to specialists for implementation. If ensemble is clearly non-functional in the current session (idle teammate, no claim, or repeated spawn failure after one retry), stop forcing it: report the failure, then continue in the main session or ask the user whether to retry later.**
|
|
193
|
-
|
|
194
|
-
## Engineer Selection
|
|
195
|
-
|
|
196
|
-
Before spawning implementation workers:
|
|
197
|
-
- Inspect `.
|
|
198
|
-
- Exclude `devops-manager` from implementation selection.
|
|
199
|
-
- Prefer the most specialized custom engineer whose description and abilities clearly match the task domain.
|
|
200
|
-
- Use `basic-engineer` only when no custom engineer is a clear fit or as a recovery fallback.
|
|
201
|
-
- Never spawn engineer names that are not present in `.
|
|
202
|
-
- When multiple engineers could fit, choose the narrower specialist before the generalist.
|
|
189
|
+
**A GitHub or Azure DevOps URL anywhere in the user's message is always a trigger, regardless of surrounding words.**
|
|
190
|
+
<!-- OB-PLATFORM-WORKFLOW-END -->
|
|
191
|
+
|
|
192
|
+
**Never delegate without a plan. Default to specialists for implementation. If ensemble is clearly non-functional in the current session (idle teammate, no claim, or repeated spawn failure after one retry), stop forcing it: report the failure, then continue in the main session or ask the user whether to retry later.**
|
|
193
|
+
|
|
194
|
+
## Engineer Selection
|
|
195
|
+
|
|
196
|
+
Before spawning implementation workers:
|
|
197
|
+
- Inspect `.opencode/agents/*.md` and build the list of engineers that actually exist in this project.
|
|
198
|
+
- Exclude `devops-manager` from implementation selection.
|
|
199
|
+
- Prefer the most specialized custom engineer whose description and abilities clearly match the task domain.
|
|
200
|
+
- Use `basic-engineer` only when no custom engineer is a clear fit or as a recovery fallback.
|
|
201
|
+
- Never spawn engineer names that are not present in `.opencode/agents/`.
|
|
202
|
+
- When multiple engineers could fit, choose the narrower specialist before the generalist.
|
|
203
203
|
|
|
204
204
|
## Multi-Agent Execution, opencode-ensemble
|
|
205
205
|
|
|
@@ -218,10 +218,10 @@ Core tools used in this workflow:
|
|
|
218
218
|
- **Max {{MAX_CONCURRENT_AGENTS}} truly concurrent agents.** All {{MAX_CONCURRENT_AGENTS}} must be spawned and running simultaneously, not sequentially. Spawn in waves if more than {{MAX_CONCURRENT_AGENTS}} are needed. Wait for wave N to finish before spawning wave N+1.
|
|
219
219
|
- **Non-overlapping file domains.** Each agent owns exclusive directories. Two agents must NEVER touch the same file.
|
|
220
220
|
- **Immediate shutdown on completion.** The moment an agent's domain has no more pending tasks → `team_shutdown` → `team_merge`. Keep agents alive if more tasks in their domain are pending (rolling batch).
|
|
221
|
-
- **Rolling batch assignment.** Agents receive up to 3 tasks initially. When they complete a batch, lead assigns the next batch of up to 3 from the board. Never leave pending tasks orphaned.
|
|
222
|
-
- **Stall detection at 5 minutes.** No commits after 5 min → nudge message → 2 min grace → force shutdown + respawn.
|
|
223
|
-
- **Idle-without-claim is an earlier stall.** If a spawned teammate sits idle with no claimed task after a short wait, resend one short claim-only message with the exact task IDs. If still idle, force shutdown + respawn once with a shorter prompt. If the retry repeats the same failure, treat ensemble as unavailable for that session and stop recycling equivalent workers.
|
|
224
|
-
- **Retry limit.** Max 3 retries per failing task → stop-and-report to user. Never retry indefinitely.
|
|
221
|
+
- **Rolling batch assignment.** Agents receive up to 3 tasks initially. When they complete a batch, lead assigns the next batch of up to 3 from the board. Never leave pending tasks orphaned.
|
|
222
|
+
- **Stall detection at 5 minutes.** No commits after 5 min → nudge message → 2 min grace → force shutdown + respawn.
|
|
223
|
+
- **Idle-without-claim is an earlier stall.** If a spawned teammate sits idle with no claimed task after a short wait, resend one short claim-only message with the exact task IDs. If still idle, force shutdown + respawn once with a shorter prompt. If the retry repeats the same failure, treat ensemble as unavailable for that session and stop recycling equivalent workers.
|
|
224
|
+
- **Retry limit.** Max 3 retries per failing task → stop-and-report to user. Never retry indefinitely.
|
|
225
225
|
|
|
226
226
|
**Progress inspection commands (tell user explicitly after spawning):**
|
|
227
227
|
- `team_status` for live team snapshot
|
|
@@ -236,19 +236,19 @@ If a teammate stalls due to model quota/rate-limit exhaustion:
|
|
|
236
236
|
|
|
237
237
|
---
|
|
238
238
|
|
|
239
|
-
## Pipeline
|
|
240
|
-
|
|
241
|
-
<!-- OB-PLATFORM-PIPELINE-START -->
|
|
242
|
-
```
|
|
243
|
-
devops-manager (lead mode)
|
|
244
|
-
→ load ob-global + parse work item via skill
|
|
239
|
+
## Pipeline
|
|
240
|
+
|
|
241
|
+
<!-- OB-PLATFORM-PIPELINE-START -->
|
|
242
|
+
```
|
|
243
|
+
devops-manager (lead mode)
|
|
244
|
+
→ load ob-global + parse work item via skill
|
|
245
245
|
↓
|
|
246
246
|
openspec-propose
|
|
247
247
|
→ proposal.md + specs + tasks
|
|
248
248
|
↓
|
|
249
249
|
[confirm with user]
|
|
250
250
|
↓
|
|
251
|
-
basic-engineer +
|
|
251
|
+
basic-engineer + *-engineer (parallel as needed)
|
|
252
252
|
→ claim tasks + load abilities + implement
|
|
253
253
|
↓
|
|
254
254
|
devops-manager (ship mode)
|
|
@@ -269,11 +269,11 @@ devops-manager (ship mode)
|
|
|
269
269
|
|
|
270
270
|
```
|
|
271
271
|
0. Run /quota to check remaining budget before spawning.
|
|
272
|
-
1. Run /opsx-apply.
|
|
273
|
-
- Step 5b: classify cost tier, announce scope, ask user to confirm if ≥4 tasks.
|
|
274
|
-
- Lead adds all tasks to board.
|
|
275
|
-
- When dependencies exist, lead uses multiple `team_tasks_add` waves so later tasks can reference real task IDs returned by earlier waves.
|
|
276
|
-
- Lead discovers available engineers from `.
|
|
272
|
+
1. Run /opsx-apply.
|
|
273
|
+
- Step 5b: classify cost tier, announce scope, ask user to confirm if ≥4 tasks.
|
|
274
|
+
- Lead adds all tasks to board.
|
|
275
|
+
- When dependencies exist, lead uses multiple `team_tasks_add` waves so later tasks can reference real task IDs returned by earlier waves.
|
|
276
|
+
- Lead discovers available engineers from `.opencode/agents/*.md`, prefers matching custom engineers, then spawns engineers with initial batch of up to 3 tasks each (rolling batch model).
|
|
277
277
|
- Each engineer claims tasks, implements, completes, messages lead.
|
|
278
278
|
- Lead assigns next batch (up to 3) to agents that report done. Repeat until board empty.
|
|
279
279
|
- Lead merges each engineer branch after shutdown, then marks tasks done in tasks.md.
|
|
@@ -306,10 +306,10 @@ When user says "I've added comments to the PR" or asks to fix PR comments from P
|
|
|
306
306
|
6. Wait for engineer results → team_shutdown + team_merge per engineer
|
|
307
307
|
7. Run verification tasks (tests/build/lint) and fix blockers if any
|
|
308
308
|
8. team_spawn devops-manager (ship mode) with "push + update PR threads" task ID + team_message "Start now"
|
|
309
|
-
9. Wait → team_results → report what was updated
|
|
310
|
-
10. team_cleanup
|
|
311
|
-
```
|
|
312
|
-
<!-- OB-PLATFORM-PIPELINE-END -->
|
|
309
|
+
9. Wait → team_results → report what was updated
|
|
310
|
+
10. team_cleanup
|
|
311
|
+
```
|
|
312
|
+
<!-- OB-PLATFORM-PIPELINE-END -->
|
|
313
313
|
|
|
314
314
|
---
|
|
315
315
|
|
|
@@ -319,10 +319,10 @@ All agents are universal, no project-specific knowledge. Platform and tech knowl
|
|
|
319
319
|
|
|
320
320
|
| Agent | File | Role |
|
|
321
321
|
|-------|------|------|
|
|
322
|
-
| `devops-manager` | .
|
|
323
|
-
| `basic-engineer` | .
|
|
322
|
+
| `devops-manager` | .opencode/agents/devops-manager.md | Reads work items, creates PRs, handles review feedback |
|
|
323
|
+
| `basic-engineer` | .opencode/agents/basic-engineer.md | Generic implementation worker using ability-loaded skills |
|
|
324
324
|
|
|
325
|
-
User can add more custom engineer agents and run them in parallel. Keep behavior ability-driven via skill mappings. Custom engineers are the primary specialization mechanism; `basic-engineer` is the general fallback when no custom engineer is a clear fit.
|
|
325
|
+
User can add more custom engineer agents and run them in parallel. Keep behavior ability-driven via skill mappings. Custom engineers are the primary specialization mechanism; `basic-engineer` is the general fallback when no custom engineer is a clear fit.
|
|
326
326
|
|
|
327
327
|
Default `basic-engineer` abilities:
|
|
328
328
|
|
|
@@ -336,7 +336,7 @@ Default `basic-engineer` abilities:
|
|
|
336
336
|
|
|
337
337
|
## Skills
|
|
338
338
|
|
|
339
|
-
Skills provide platform and tech-specific knowledge. Agents usually detect and load them automatically. Prefer auto-detection, but explicitly naming a skill in a spawn prompt is allowed when a workflow requires it or repeated misses show the agent is not loading the right context.
|
|
339
|
+
Skills provide platform and tech-specific knowledge. Agents usually detect and load them automatically. Prefer auto-detection, but explicitly naming a skill in a spawn prompt is allowed when a workflow requires it or repeated misses show the agent is not loading the right context.
|
|
340
340
|
|
|
341
341
|
`ob-global` is always loaded first, it provides baseline rules for all agents.
|
|
342
342
|
|
|
@@ -377,7 +377,7 @@ When `## Source Roots` lists multiple roots, each root is an independent git rep
|
|
|
377
377
|
│ ├── agents/ # Agent definitions (universal, no project knowledge)
|
|
378
378
|
│ │ ├── devops-manager.md
|
|
379
379
|
│ │ ├── basic-engineer.md
|
|
380
|
-
│ │ └──
|
|
380
|
+
│ │ └── *-engineer.md # optional, user-defined workers
|
|
381
381
|
│ └── skills/ # Skills (platform/tech specific knowledge)
|
|
382
382
|
│ ├── ob-global/ ← baseline skill, load first
|
|
383
383
|
│ ├── ob-default/ ← fallback skill
|
package/package.json
CHANGED
package/src/commands/wizard.js
CHANGED
|
@@ -111,7 +111,7 @@ export async function runWizard(version) {
|
|
|
111
111
|
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
112
112
|
console.log()
|
|
113
113
|
console.log(' Open this project in OpenCode and type:')
|
|
114
|
-
console.log(chalk.bold(' "init"'))
|
|
114
|
+
console.log(chalk.bold(' "/ob-init"'))
|
|
115
115
|
console.log()
|
|
116
116
|
if (toGenerate.length > 0) {
|
|
117
117
|
console.log(` OpenCode will generate ${toGenerate.join(' and ')}`)
|
package/src/steps/copy/agents.js
CHANGED
|
@@ -123,7 +123,7 @@ function buildPipelineSection(platform) {
|
|
|
123
123
|
' ↓',
|
|
124
124
|
' [confirm with user when scope needs it]',
|
|
125
125
|
' ↓',
|
|
126
|
-
'basic-engineer +
|
|
126
|
+
'basic-engineer + *-engineer (parallel as needed)',
|
|
127
127
|
' → claim tasks + load abilities + implement',
|
|
128
128
|
' ↓',
|
|
129
129
|
'main session',
|
|
@@ -148,7 +148,7 @@ function buildPipelineSection(platform) {
|
|
|
148
148
|
' - Step 5b: classify cost tier, announce scope, ask user to confirm if ≥4 tasks.',
|
|
149
149
|
' - Lead adds all tasks to board.',
|
|
150
150
|
' - When dependencies exist, lead uses multiple `team_tasks_add` waves so later tasks can reference real task IDs returned by earlier waves.',
|
|
151
|
-
|
|
151
|
+
' - Lead discovers available engineers from `.opencode/agents/*.md`, prefers matching custom engineers, then spawns engineers with initial batch of up to 3 tasks each (rolling batch model).',
|
|
152
152
|
' - Each engineer claims tasks, implements, completes, messages lead.',
|
|
153
153
|
' - Lead assigns next batch (up to 3) to agents that report done. Repeat until board empty.',
|
|
154
154
|
' - Lead merges each engineer branch after shutdown, then marks tasks done in tasks.md.',
|
|
@@ -177,7 +177,7 @@ function buildPipelineSection(platform) {
|
|
|
177
177
|
' ↓',
|
|
178
178
|
' [confirm with user]',
|
|
179
179
|
' ↓',
|
|
180
|
-
'basic-engineer +
|
|
180
|
+
'basic-engineer + *-engineer (parallel as needed)',
|
|
181
181
|
' → claim tasks + load abilities + implement',
|
|
182
182
|
' ↓',
|
|
183
183
|
'devops-manager (ship mode)',
|
|
@@ -202,7 +202,7 @@ function buildPipelineSection(platform) {
|
|
|
202
202
|
' - Step 5b: classify cost tier, announce scope, ask user to confirm if ≥4 tasks.',
|
|
203
203
|
' - Lead adds all tasks to board.',
|
|
204
204
|
' - When dependencies exist, lead uses multiple `team_tasks_add` waves so later tasks can reference real task IDs returned by earlier waves.',
|
|
205
|
-
|
|
205
|
+
' - Lead discovers available engineers from `.opencode/agents/*.md`, prefers matching custom engineers, then spawns engineers with initial batch of up to 3 tasks each (rolling batch model).',
|
|
206
206
|
' - Each engineer claims tasks, implements, completes, messages lead.',
|
|
207
207
|
' - Lead assigns next batch (up to 3) to agents that report done. Repeat until board empty.',
|
|
208
208
|
' - Lead merges each engineer branch after shutdown, then marks tasks done in tasks.md.',
|
|
@@ -319,7 +319,7 @@ export async function patchAgentGuidance(platform, cwd = process.cwd()) {
|
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
export async function patchDevopsManagerMd(platform, cwd = process.cwd()) {
|
|
322
|
-
const devopsPath = path.join(cwd, '.
|
|
322
|
+
const devopsPath = path.join(cwd, '.opencode', 'agents', 'devops-manager.md')
|
|
323
323
|
if (!await fse.pathExists(devopsPath)) return
|
|
324
324
|
|
|
325
325
|
const resolved = platform === 'azure' ? 'azure' : platform === 'none' ? 'none' : 'github'
|
|
@@ -30,8 +30,8 @@ describe('platform patching', () => {
|
|
|
30
30
|
})
|
|
31
31
|
|
|
32
32
|
it('patches devops-manager for none mode without platform skills', async () => {
|
|
33
|
-
const source = path.join(process.cwd(), 'content', '.
|
|
34
|
-
const dest = path.join(tmpDir, '.
|
|
33
|
+
const source = path.join(process.cwd(), 'content', '.opencode', 'agents', 'devops-manager.md')
|
|
34
|
+
const dest = path.join(tmpDir, '.opencode', 'agents', 'devops-manager.md')
|
|
35
35
|
await fse.ensureDir(path.dirname(dest))
|
|
36
36
|
await fse.copyFile(source, dest)
|
|
37
37
|
|
|
@@ -12,7 +12,7 @@ export async function chooseModels() {
|
|
|
12
12
|
|
|
13
13
|
if (!rawModels) {
|
|
14
14
|
warn('Could not fetch models (offline and no cache). Skipping model selection.');
|
|
15
|
-
warn('Set models later in .
|
|
15
|
+
warn('Set models later in .opencode/agents/<name>.md and .opencode/opencode.json');
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -26,7 +26,7 @@ export async function chooseModels() {
|
|
|
26
26
|
success(`${models.length} models available`);
|
|
27
27
|
console.log();
|
|
28
28
|
info('Cost indicators: [$] cheap [$$] mid [$$$] expensive');
|
|
29
|
-
info('Type to search. Change selections later in .
|
|
29
|
+
info('Type to search. Change selections later in .opencode/agents/ and .opencode/opencode.json');
|
|
30
30
|
console.log();
|
|
31
31
|
|
|
32
32
|
for (const line of modelsPreset.roles.plan.info) info(line);
|
|
@@ -41,12 +41,12 @@ export async function chooseModels() {
|
|
|
41
41
|
const fastModel = await pickModel(modelsPreset.roles.fast.prompt, models);
|
|
42
42
|
console.log();
|
|
43
43
|
|
|
44
|
-
const agentsDir = path.join(process.cwd(), '.
|
|
44
|
+
const agentsDir = path.join(process.cwd(), '.opencode', 'agents');
|
|
45
45
|
await writeModelsToConfigs({ planModel, buildModel, fastModel, agentsDir, preset: modelsPreset });
|
|
46
46
|
|
|
47
47
|
console.log();
|
|
48
48
|
warn('Make sure you have API access to the selected models.');
|
|
49
|
-
warn('Change them anytime in .
|
|
49
|
+
warn('Change them anytime in .opencode/agents/<name>.md and .opencode/opencode.json');
|
|
50
50
|
|
|
51
51
|
return { planModel, buildModel, fastModel };
|
|
52
52
|
}
|
|
@@ -51,12 +51,12 @@ export const ENSEMBLE_SECTION = `6. **Implement via ensemble team**
|
|
|
51
51
|
- Only shut down an agent when the board has no more tasks for its domain.
|
|
52
52
|
|
|
53
53
|
Before spawning:
|
|
54
|
-
- scan \`.
|
|
54
|
+
- scan \`.opencode/agents/\` and list the engineers that actually exist in this project
|
|
55
55
|
- exclude \`devops-manager\` from implementation selection
|
|
56
56
|
- read each engineer's description and abilities
|
|
57
57
|
- prefer the most specialized custom engineer whose description and abilities match the task
|
|
58
58
|
- use \`basic-engineer\` only when no custom engineer is a clear fit or as a recovery fallback
|
|
59
|
-
- never spawn an engineer name that is not present in \`.
|
|
59
|
+
- never spawn an engineer name that is not present in \`.opencode/agents/\`
|
|
60
60
|
|
|
61
61
|
Each \`team_spawn\` MUST include the agent field (required, causes NOT NULL error if omitted).
|
|
62
62
|
|
|
@@ -131,7 +131,7 @@ export const ENSEMBLE_SECTION = `6. **Implement via ensemble team**
|
|
|
131
131
|
- \`team_merge member:"<name>"\`
|
|
132
132
|
- If team_merge blocks on local changes: \`git stash\`, retry merge, \`git stash pop\`.
|
|
133
133
|
6. **If ALL agents are shut down and tasks remain unassigned** (new domain, dependencies unblocked):
|
|
134
|
-
- Discover the remaining matching engineers from \`.
|
|
134
|
+
- Discover the remaining matching engineers from \`.opencode/agents/\` and spawn a new wave (back to step 6d).
|
|
135
135
|
7. **If ALL tasks are done:** proceed to step 7.
|
|
136
136
|
If a teammate reports rate-limit/quota/token exhaustion, immediately shutdown that teammate and respawn with an available model.
|
|
137
137
|
|
|
@@ -176,7 +176,7 @@ export const ENSEMBLE_SECTION = `6. **Implement via ensemble team**
|
|
|
176
176
|
- NEVER send a start message that omits task IDs; if a task ID is missing from the start message, the agent cannot claim
|
|
177
177
|
- NEVER edit files between team_spawn and team_merge, team_merge blocks on overlapping local changes
|
|
178
178
|
- ALWAYS add every task to the board before spawning, using multiple \`team_tasks_add\` calls when dependency wiring requires it
|
|
179
|
-
- ALWAYS discover engineers from \`.
|
|
179
|
+
- ALWAYS discover engineers from \`.opencode/agents/\` and prefer matching custom engineers over \`basic-engineer\`
|
|
180
180
|
- ALWAYS spawn agents sequentially (wait for each team_spawn result before the next), then send start messages to all of them together
|
|
181
181
|
- ALWAYS set \`claim_task\` for the first unblocked task in each initial batch and instruct agents to claim before any other work
|
|
182
182
|
- ALWAYS shut down + merge agents only when no more tasks remain for their domain
|
|
@@ -92,7 +92,7 @@ describe('ENSEMBLE_SECTION dependency guidance', () => {
|
|
|
92
92
|
})
|
|
93
93
|
|
|
94
94
|
it('prefers discovered custom engineers over hardcoded role inventory', () => {
|
|
95
|
-
expect(ENSEMBLE_SECTION).toContain('scan
|
|
95
|
+
expect(ENSEMBLE_SECTION).toContain('scan \`.opencode/agents/\` and list the engineers that actually exist in this project')
|
|
96
96
|
expect(ENSEMBLE_SECTION).toContain('prefer the most specialized custom engineer')
|
|
97
97
|
expect(ENSEMBLE_SECTION).not.toContain('front-engineer: UI, components, framework skills')
|
|
98
98
|
expect(ENSEMBLE_SECTION).not.toContain('team_spawn name:"front" agent:"front-engineer"')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execa } from "execa"
|
|
2
2
|
import path from "node:path"
|
|
3
|
-
import { commandExists, error, header, info, loading, success, warn} from "../../utils/exec.js"
|
|
3
|
+
import { commandExists, error, header, info, loading, success, warn } from "../../utils/exec.js"
|
|
4
4
|
import { APPLY_TARGETS, patchApplyFile } from "./ensemble.js"
|
|
5
5
|
|
|
6
6
|
export const openspecSteps = {
|
|
@@ -58,7 +58,7 @@ async function install() {
|
|
|
58
58
|
success("OpenSpec installed")
|
|
59
59
|
} catch (err) {
|
|
60
60
|
error(`Failed to run openspec install: ${err.message}`)
|
|
61
|
-
}
|
|
61
|
+
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
async function check() {
|
|
@@ -77,7 +77,7 @@ async function init() {
|
|
|
77
77
|
try {
|
|
78
78
|
const result = await execa(
|
|
79
79
|
"npx",
|
|
80
|
-
["@fission-ai/openspec", "init", "--tools", "opencode", "--force"],
|
|
80
|
+
["@fission-ai/openspec", "/ob-init", "--tools", "opencode", "--force"],
|
|
81
81
|
{
|
|
82
82
|
cwd: process.cwd(),
|
|
83
83
|
stdio: "pipe",
|
|
@@ -45,7 +45,7 @@ export async function fixCodegraphConfig() {
|
|
|
45
45
|
if (rogueMcp) {
|
|
46
46
|
for (const entry of Object.values(rogueMcp)) {
|
|
47
47
|
if (Array.isArray(entry.command) && entry.command[0] === 'codegraph') {
|
|
48
|
-
entry.command = ['npx', ...entry.command]
|
|
48
|
+
entry.command = ['npx', '@colbymchenry/codegraph', ...entry.command.slice(1)]
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
correctContent.mcp = { ...(correctContent.mcp || {}), ...rogueMcp }
|
|
@@ -69,7 +69,7 @@ export async function installCodegraph(options = {}) {
|
|
|
69
69
|
try {
|
|
70
70
|
const installResult = await execa(
|
|
71
71
|
'npx',
|
|
72
|
-
['@colbymchenry/codegraph', 'install', '--target=opencode', `--location=${location}`, '--yes'],
|
|
72
|
+
['--yes', '@colbymchenry/codegraph', 'install', '--target=opencode', `--location=${location}`, '--yes'],
|
|
73
73
|
{
|
|
74
74
|
cwd: process.cwd(),
|
|
75
75
|
reject: false,
|
|
@@ -98,7 +98,7 @@ export async function installCodegraph(options = {}) {
|
|
|
98
98
|
loading('initializing codegraph project index...')
|
|
99
99
|
|
|
100
100
|
try {
|
|
101
|
-
const initResult = await execa('npx', ['codegraph', 'init'], {
|
|
101
|
+
const initResult = await execa('npx', ['@colbymchenry/codegraph', 'init', '-i'], {
|
|
102
102
|
cwd: process.cwd(),
|
|
103
103
|
reject: false,
|
|
104
104
|
stdio: 'pipe',
|
|
@@ -55,7 +55,7 @@ describe('fixCodegraphConfig()', () => {
|
|
|
55
55
|
expect(result).toBe(true)
|
|
56
56
|
expect(fs.existsSync(path.join(tmpDir, 'opencode.jsonc'))).toBe(false)
|
|
57
57
|
const readResult = await fse.readJson(path.join(opencodeDir, 'opencode.json'))
|
|
58
|
-
expect(readResult.mcp.codegraph).toEqual({ command: ['npx', 'codegraph', 'serve', '--mcp'] })
|
|
58
|
+
expect(readResult.mcp.codegraph).toEqual({ command: ['npx', '@colbymchenry/codegraph', 'serve', '--mcp'] })
|
|
59
59
|
expect(readResult.plugin).toEqual(["opencode-plugin-openspec@latest"])
|
|
60
60
|
})
|
|
61
61
|
|
|
@@ -74,7 +74,7 @@ describe('fixCodegraphConfig()', () => {
|
|
|
74
74
|
expect(result).toBe(true)
|
|
75
75
|
expect(fs.existsSync(path.join(tmpDir, 'opencode.jsonc'))).toBe(false)
|
|
76
76
|
const readResult = await fse.readJson(path.join(tmpDir, '.opencode', 'opencode.json'))
|
|
77
|
-
expect(readResult.mcp.codegraph.command).toEqual(['npx', 'codegraph', 'serve', '--mcp'])
|
|
77
|
+
expect(readResult.mcp.codegraph.command).toEqual(['npx', '@colbymchenry/codegraph', 'serve', '--mcp'])
|
|
78
78
|
})
|
|
79
79
|
|
|
80
80
|
it('handles JSONC with comments', async () => {
|
|
@@ -95,7 +95,7 @@ describe('fixCodegraphConfig()', () => {
|
|
|
95
95
|
expect(result).toBe(true)
|
|
96
96
|
expect(fs.existsSync(path.join(tmpDir, 'opencode.jsonc'))).toBe(false)
|
|
97
97
|
const readResult = await fse.readJson(path.join(opencodeDir, 'opencode.json'))
|
|
98
|
-
expect(readResult.mcp.codegraph.command).toEqual(['npx', 'codegraph', 'serve', '--mcp'])
|
|
98
|
+
expect(readResult.mcp.codegraph.command).toEqual(['npx', '@colbymchenry/codegraph', 'serve', '--mcp'])
|
|
99
99
|
})
|
|
100
100
|
|
|
101
101
|
it('handles JSONC with URLs containing //', async () => {
|
|
@@ -118,7 +118,7 @@ describe('fixCodegraphConfig()', () => {
|
|
|
118
118
|
expect(result).toBe(true)
|
|
119
119
|
expect(fs.existsSync(path.join(tmpDir, 'opencode.jsonc'))).toBe(false)
|
|
120
120
|
const readResult = await fse.readJson(path.join(opencodeDir, 'opencode.json'))
|
|
121
|
-
expect(readResult.mcp.codegraph.command).toEqual(['npx', 'codegraph', 'serve', '--mcp'])
|
|
121
|
+
expect(readResult.mcp.codegraph.command).toEqual(['npx', '@colbymchenry/codegraph', 'serve', '--mcp'])
|
|
122
122
|
})
|
|
123
123
|
|
|
124
124
|
it('removes unparseable opencode.jsonc, warns, and returns false', async () => {
|
|
@@ -143,7 +143,7 @@ describe('fixCodegraphConfig()', () => {
|
|
|
143
143
|
|
|
144
144
|
expect(result).toBe(true)
|
|
145
145
|
const readResult = await fse.readJson(path.join(tmpDir, '.opencode', 'opencode.json'))
|
|
146
|
-
expect(readResult.mcp.codegraph.command).toEqual(['npx', 'codegraph', 'serve', '--mcp'])
|
|
146
|
+
expect(readResult.mcp.codegraph.command).toEqual(['npx', '@colbymchenry/codegraph', 'serve', '--mcp'])
|
|
147
147
|
expect(fs.existsSync(path.join(tmpDir, 'opencode.jsonc'))).toBe(false)
|
|
148
148
|
})
|
|
149
149
|
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
[
|
|
2
|
-
{
|
|
3
|
-
"ts": "2026-05-04T21:04:09.899Z",
|
|
4
|
-
"agent": "lead",
|
|
5
|
-
"member": null,
|
|
6
|
-
"agentType": null,
|
|
7
|
-
"team": null,
|
|
8
|
-
"action": "started",
|
|
9
|
-
"sessionId": "ses_20b31c36cffeBfsYI2hJ1BNcM5"
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
"ts": "2026-05-04T21:04:26.180Z",
|
|
13
|
-
"agent": "lead",
|
|
14
|
-
"member": null,
|
|
15
|
-
"agentType": null,
|
|
16
|
-
"team": null,
|
|
17
|
-
"action": "skill-loaded",
|
|
18
|
-
"skill": "ob-userstory-gh",
|
|
19
|
-
"source": "read-skill-file"
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
"ts": "2026-05-04T21:04:47.306Z",
|
|
23
|
-
"agent": "lead",
|
|
24
|
-
"member": null,
|
|
25
|
-
"agentType": null,
|
|
26
|
-
"team": null,
|
|
27
|
-
"action": "completed",
|
|
28
|
-
"filesEdited": 0,
|
|
29
|
-
"skills": [
|
|
30
|
-
"ob-userstory-gh"
|
|
31
|
-
],
|
|
32
|
-
"usage": {
|
|
33
|
-
"inputTokensReported": null,
|
|
34
|
-
"outputTokensReported": null,
|
|
35
|
-
"totalTokensReported": null,
|
|
36
|
-
"tokenEstimateLow": 3469,
|
|
37
|
-
"tokenEstimateHigh": 6940,
|
|
38
|
-
"method": "heuristic"
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
]
|
|
@@ -1,523 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs"
|
|
2
|
-
import path from "node:path"
|
|
3
|
-
|
|
4
|
-
const LOG_FILE = ".agents/session-log.json"
|
|
5
|
-
const SPAWN_MATCH_WINDOW_MS = 30000
|
|
6
|
-
const UNMATCHED_SESSION_WARN_MS = 30000
|
|
7
|
-
const DEBUG = process.env.SESSION_LOG_DEBUG === "true"
|
|
8
|
-
|
|
9
|
-
// Per-session state
|
|
10
|
-
const sessionState = new Map()
|
|
11
|
-
|
|
12
|
-
// Lead session -> current team name
|
|
13
|
-
const leadTeamBySession = new Map()
|
|
14
|
-
|
|
15
|
-
// Pending spawn records waiting for a session.created match
|
|
16
|
-
// Each entry: { leadSessionId, at, member, agentType, teamName, spawnedSessionId, intendedSkills }
|
|
17
|
-
const pendingSpawns = []
|
|
18
|
-
|
|
19
|
-
// spawnedSessionId -> pending spawn (direct correlation fast path)
|
|
20
|
-
const pendingSpawnBySessionId = new Map()
|
|
21
|
-
|
|
22
|
-
// Team -> completed session snapshots
|
|
23
|
-
const completedByTeam = new Map()
|
|
24
|
-
|
|
25
|
-
function ts() {
|
|
26
|
-
return new Date().toISOString()
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function nowMs() {
|
|
30
|
-
return Date.now()
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function appendEntry(directory, entry) {
|
|
34
|
-
const logPath = path.join(directory, LOG_FILE)
|
|
35
|
-
const dir = path.dirname(logPath)
|
|
36
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
37
|
-
|
|
38
|
-
let entries = []
|
|
39
|
-
if (fs.existsSync(logPath)) {
|
|
40
|
-
try { entries = JSON.parse(fs.readFileSync(logPath, "utf8")) } catch { /* ignore parse errors */ }
|
|
41
|
-
}
|
|
42
|
-
entries.push(entry)
|
|
43
|
-
fs.writeFileSync(logPath, JSON.stringify(entries, null, 2), "utf8")
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function resolveAgentName(session) {
|
|
47
|
-
const agentPath = session?.agent
|
|
48
|
-
if (agentPath) {
|
|
49
|
-
const base = path.basename(agentPath, ".md")
|
|
50
|
-
if (base) return base
|
|
51
|
-
}
|
|
52
|
-
return "lead"
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function resolveModel(session) {
|
|
56
|
-
// Try common field names used by OpenCode session objects
|
|
57
|
-
return session?.model || session?.modelId || session?.model_id || null
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function addSkillToState(state, skillName) {
|
|
61
|
-
if (!skillName || !state) return false
|
|
62
|
-
if (!state.skills) state.skills = new Set()
|
|
63
|
-
if (state.skills.has(skillName)) return false
|
|
64
|
-
state.skills.add(skillName)
|
|
65
|
-
return true
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Extract skill names hinted in a spawn prompt by scanning for known skill-like tokens.
|
|
69
|
-
// Matches strings that look like skill directory names (kebab-case words after "skill" keyword
|
|
70
|
-
// or adjacent to known skill name patterns).
|
|
71
|
-
function extractIntendedSkills(prompt) {
|
|
72
|
-
if (!prompt || typeof prompt !== "string") return []
|
|
73
|
-
const skills = new Set()
|
|
74
|
-
|
|
75
|
-
// Match explicit skill name mentions: e.g. "ob-pullrequest-gh", "browser-automation"
|
|
76
|
-
const kebabPattern = /\b([a-z][a-z0-9]*(?:-[a-z0-9]+){1,})\b/g
|
|
77
|
-
let m
|
|
78
|
-
while ((m = kebabPattern.exec(prompt)) !== null) {
|
|
79
|
-
const candidate = m[1]
|
|
80
|
-
// Filter to plausible skill names: 2+ segments, known prefixes or suffixes
|
|
81
|
-
if (
|
|
82
|
-
candidate.startsWith("ob-") ||
|
|
83
|
-
candidate.startsWith("openspec-") ||
|
|
84
|
-
candidate.startsWith("browser-") ||
|
|
85
|
-
candidate.endsWith("-gh") ||
|
|
86
|
-
candidate.endsWith("-az") ||
|
|
87
|
-
candidate.endsWith("-automation") ||
|
|
88
|
-
candidate.endsWith("-change") ||
|
|
89
|
-
candidate.endsWith("-engineer") ||
|
|
90
|
-
candidate.endsWith("-manager") ||
|
|
91
|
-
candidate.endsWith("-auditor")
|
|
92
|
-
) {
|
|
93
|
-
skills.add(candidate)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return Array.from(skills).sort()
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function toNum(v) {
|
|
100
|
-
if (typeof v === "number" && Number.isFinite(v)) return v
|
|
101
|
-
if (typeof v === "string" && v.trim() !== "" && !Number.isNaN(Number(v))) return Number(v)
|
|
102
|
-
return null
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function extractReportedTokens(obj) {
|
|
106
|
-
const out = { input: 0, output: 0, total: 0, found: false }
|
|
107
|
-
const visited = new Set()
|
|
108
|
-
|
|
109
|
-
function walk(node) {
|
|
110
|
-
if (!node || typeof node !== "object") return
|
|
111
|
-
if (visited.has(node)) return
|
|
112
|
-
visited.add(node)
|
|
113
|
-
|
|
114
|
-
for (const [k, v] of Object.entries(node)) {
|
|
115
|
-
const key = String(k).toLowerCase()
|
|
116
|
-
const n = toNum(v)
|
|
117
|
-
|
|
118
|
-
if (n !== null) {
|
|
119
|
-
if ((key.includes("input") || key.includes("prompt")) && key.includes("token")) {
|
|
120
|
-
out.input += n
|
|
121
|
-
out.found = true
|
|
122
|
-
} else if ((key.includes("output") || key.includes("completion")) && key.includes("token")) {
|
|
123
|
-
out.output += n
|
|
124
|
-
out.found = true
|
|
125
|
-
} else if (key.includes("total") && key.includes("token")) {
|
|
126
|
-
out.total += n
|
|
127
|
-
out.found = true
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (v && typeof v === "object") walk(v)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
walk(obj)
|
|
136
|
-
return out
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function estimateTokens(state) {
|
|
140
|
-
const inTokens = Math.ceil((state.charIn || 0) / 4)
|
|
141
|
-
const outTokens = Math.ceil((state.charOut || 0) / 4)
|
|
142
|
-
const base = inTokens + outTokens + Math.max(0, (state.toolCalls || 0) * 20)
|
|
143
|
-
const low = Math.max(0, Math.floor(base * 0.7))
|
|
144
|
-
const high = Math.max(low, Math.ceil(base * 1.4))
|
|
145
|
-
return { low, high }
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function usagePayload(state) {
|
|
149
|
-
const est = estimateTokens(state)
|
|
150
|
-
const reportedIn = state.reportedInputTokens || 0
|
|
151
|
-
const reportedOut = state.reportedOutputTokens || 0
|
|
152
|
-
const reportedTotalDirect = state.reportedTotalTokens || 0
|
|
153
|
-
const reportedTotalDerived = reportedIn + reportedOut
|
|
154
|
-
const reportedTotal = reportedTotalDirect || reportedTotalDerived || 0
|
|
155
|
-
|
|
156
|
-
let method = "heuristic"
|
|
157
|
-
if (reportedTotal > 0 && (est.low > 0 || est.high > 0)) method = "mixed"
|
|
158
|
-
else if (reportedTotal > 0) method = "reported"
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
inputTokensReported: reportedIn || null,
|
|
162
|
-
outputTokensReported: reportedOut || null,
|
|
163
|
-
totalTokensReported: reportedTotal || null,
|
|
164
|
-
tokenEstimateLow: est.low,
|
|
165
|
-
tokenEstimateHigh: est.high,
|
|
166
|
-
method,
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function buildCompletedSnapshot(state, sessionId) {
|
|
171
|
-
return {
|
|
172
|
-
sessionId,
|
|
173
|
-
agent: state.agentName,
|
|
174
|
-
member: state.member || null,
|
|
175
|
-
agentType: state.agentType || null,
|
|
176
|
-
team: state.teamName || null,
|
|
177
|
-
model: state.model || null,
|
|
178
|
-
skills: Array.from(state.skills || []).sort(),
|
|
179
|
-
intendedSkills: state.intendedSkills || [],
|
|
180
|
-
usage: usagePayload(state),
|
|
181
|
-
filesEdited: state.editCount || 0,
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function buildTeamSkillsSummary(teamName) {
|
|
186
|
-
const rows = completedByTeam.get(teamName) || []
|
|
187
|
-
const byAgent = {}
|
|
188
|
-
for (const row of rows) {
|
|
189
|
-
const key = row.member || row.agent || "unknown"
|
|
190
|
-
if (!byAgent[key]) byAgent[key] = { agentType: row.agentType || null, model: row.model || null, skills: new Set(), intendedSkills: new Set() }
|
|
191
|
-
for (const s of row.skills || []) byAgent[key].skills.add(s)
|
|
192
|
-
for (const s of row.intendedSkills || []) byAgent[key].intendedSkills.add(s)
|
|
193
|
-
}
|
|
194
|
-
const out = {}
|
|
195
|
-
for (const [k, v] of Object.entries(byAgent)) {
|
|
196
|
-
const skills = Array.from(v.skills).sort()
|
|
197
|
-
const intendedSkills = Array.from(v.intendedSkills).sort()
|
|
198
|
-
const missingSkills = intendedSkills.filter(s => !v.skills.has(s))
|
|
199
|
-
out[k] = {
|
|
200
|
-
agentType: v.agentType,
|
|
201
|
-
model: v.model,
|
|
202
|
-
skills,
|
|
203
|
-
intendedSkills,
|
|
204
|
-
missingSkills: missingSkills.length > 0 ? missingSkills : undefined,
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return out
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function trackCompletedByTeam(snapshot) {
|
|
211
|
-
if (!snapshot.team) return
|
|
212
|
-
if (!completedByTeam.has(snapshot.team)) completedByTeam.set(snapshot.team, [])
|
|
213
|
-
completedByTeam.get(snapshot.team).push(snapshot)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function enqueuePendingSpawn(leadSessionId, args, spawnOutput) {
|
|
217
|
-
// Try to extract spawnedSessionId from team_spawn output for direct correlation
|
|
218
|
-
const spawnedSessionId =
|
|
219
|
-
spawnOutput?.sessionId ||
|
|
220
|
-
spawnOutput?.session_id ||
|
|
221
|
-
spawnOutput?.id ||
|
|
222
|
-
spawnOutput?.data?.sessionId ||
|
|
223
|
-
spawnOutput?.data?.session_id ||
|
|
224
|
-
spawnOutput?.data?.id ||
|
|
225
|
-
null
|
|
226
|
-
|
|
227
|
-
const record = {
|
|
228
|
-
leadSessionId,
|
|
229
|
-
at: nowMs(),
|
|
230
|
-
member: args?.name || null,
|
|
231
|
-
agentType: args?.agent || null,
|
|
232
|
-
teamName: leadTeamBySession.get(leadSessionId) || null,
|
|
233
|
-
spawnedSessionId,
|
|
234
|
-
intendedSkills: extractIntendedSkills(args?.prompt),
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
pendingSpawns.push(record)
|
|
238
|
-
|
|
239
|
-
if (spawnedSessionId) {
|
|
240
|
-
pendingSpawnBySessionId.set(spawnedSessionId, record)
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function matchPendingSpawn(sessionId) {
|
|
245
|
-
const now = nowMs()
|
|
246
|
-
|
|
247
|
-
// Fast path: direct session ID correlation from team_spawn output
|
|
248
|
-
if (sessionId && pendingSpawnBySessionId.has(sessionId)) {
|
|
249
|
-
const record = pendingSpawnBySessionId.get(sessionId)
|
|
250
|
-
pendingSpawnBySessionId.delete(sessionId)
|
|
251
|
-
const idx = pendingSpawns.indexOf(record)
|
|
252
|
-
if (idx !== -1) pendingSpawns.splice(idx, 1)
|
|
253
|
-
return record
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Fallback: time-window heuristic (drop expired first)
|
|
257
|
-
for (let i = pendingSpawns.length - 1; i >= 0; i--) {
|
|
258
|
-
if (now - pendingSpawns[i].at > SPAWN_MATCH_WINDOW_MS) {
|
|
259
|
-
pendingSpawnBySessionId.delete(pendingSpawns[i].spawnedSessionId)
|
|
260
|
-
pendingSpawns.splice(i, 1)
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
if (pendingSpawns.length === 0) return null
|
|
264
|
-
return pendingSpawns.shift()
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Maps ensemble tool name -> function that extracts log entry fields from args
|
|
268
|
-
const ENSEMBLE_TOOL_HANDLERS = {
|
|
269
|
-
team_create: (args) => ({ action: "team-created", team: args.name }),
|
|
270
|
-
team_spawn: (args) => ({ action: "teammate-spawned", name: args.name, agentType: args.agent }),
|
|
271
|
-
team_shutdown: (args) => ({ action: "teammate-shutdown", name: args.name }),
|
|
272
|
-
team_merge: (args) => ({ action: "teammate-merged", name: args.name }),
|
|
273
|
-
team_cleanup: () => ({ action: "team-cleanup" }),
|
|
274
|
-
team_status: () => ({ action: "team-status-checked" }),
|
|
275
|
-
team_results: (args) => ({ action: "team-results-read", from: args.from }),
|
|
276
|
-
team_message: (args) => ({ action: "team-message", to: args.to ?? "lead", preview: String(args.text ?? "").slice(0, 120) }),
|
|
277
|
-
team_broadcast: (args) => ({ action: "team-broadcast", preview: String(args.text ?? "").slice(0, 120) }),
|
|
278
|
-
team_tasks_add: (args) => ({ action: "tasks-added", count: Array.isArray(args.tasks) ? args.tasks.length : "?" }),
|
|
279
|
-
team_tasks_complete: (args) => ({ action: "task-completed", taskId: args.task_id }),
|
|
280
|
-
team_claim: (args) => ({ action: "task-claimed", taskId: args.task_id }),
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export const SessionLogPlugin = async ({ client, directory }) => {
|
|
284
|
-
return {
|
|
285
|
-
event: async ({ event }) => {
|
|
286
|
-
try {
|
|
287
|
-
if (event?.type === "session.created") {
|
|
288
|
-
const sessionId = event.properties?.id ?? event.properties?.sessionID
|
|
289
|
-
if (!sessionId) return
|
|
290
|
-
|
|
291
|
-
const res = await client.session.get({ path: { id: sessionId } })
|
|
292
|
-
const session = res?.data
|
|
293
|
-
const fallbackAgent = resolveAgentName(session)
|
|
294
|
-
const model = resolveModel(session)
|
|
295
|
-
const spawnMatch = matchPendingSpawn(sessionId)
|
|
296
|
-
|
|
297
|
-
const state = {
|
|
298
|
-
agentName: spawnMatch?.member || fallbackAgent,
|
|
299
|
-
member: spawnMatch?.member || null,
|
|
300
|
-
agentType: spawnMatch?.agentType || null,
|
|
301
|
-
teamName: spawnMatch?.teamName || null,
|
|
302
|
-
model,
|
|
303
|
-
intendedSkills: spawnMatch?.intendedSkills || [],
|
|
304
|
-
editCount: 0,
|
|
305
|
-
skills: new Set(),
|
|
306
|
-
startedAtMs: nowMs(),
|
|
307
|
-
toolCalls: 0,
|
|
308
|
-
charIn: 0,
|
|
309
|
-
charOut: 0,
|
|
310
|
-
reportedInputTokens: 0,
|
|
311
|
-
reportedOutputTokens: 0,
|
|
312
|
-
reportedTotalTokens: 0,
|
|
313
|
-
// Track whether this session was matched to a spawn record
|
|
314
|
-
spawnMatched: !!spawnMatch,
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
sessionState.set(sessionId, state)
|
|
318
|
-
|
|
319
|
-
const startedEntry = {
|
|
320
|
-
ts: ts(),
|
|
321
|
-
agent: state.agentName,
|
|
322
|
-
member: state.member,
|
|
323
|
-
agentType: state.agentType,
|
|
324
|
-
team: state.teamName,
|
|
325
|
-
model: state.model,
|
|
326
|
-
action: "started",
|
|
327
|
-
sessionId,
|
|
328
|
-
}
|
|
329
|
-
appendEntry(directory, startedEntry)
|
|
330
|
-
|
|
331
|
-
// Emit explicit teammate-registered entry when a spawn is matched
|
|
332
|
-
if (spawnMatch) {
|
|
333
|
-
appendEntry(directory, {
|
|
334
|
-
ts: ts(),
|
|
335
|
-
agent: state.agentName,
|
|
336
|
-
member: state.member,
|
|
337
|
-
agentType: state.agentType,
|
|
338
|
-
team: state.teamName,
|
|
339
|
-
model: state.model,
|
|
340
|
-
intendedSkills: state.intendedSkills,
|
|
341
|
-
action: "teammate-registered",
|
|
342
|
-
sessionId,
|
|
343
|
-
correlationMethod: spawnMatch.spawnedSessionId === sessionId ? "direct" : "time-window",
|
|
344
|
-
})
|
|
345
|
-
} else {
|
|
346
|
-
// No spawn match, schedule an unmatched-session warning
|
|
347
|
-
const capturedSessionId = sessionId
|
|
348
|
-
setTimeout(() => {
|
|
349
|
-
const s = sessionState.get(capturedSessionId)
|
|
350
|
-
if (!s || s.spawnMatched) return
|
|
351
|
-
appendEntry(directory, {
|
|
352
|
-
ts: ts(),
|
|
353
|
-
agent: s.agentName,
|
|
354
|
-
member: s.member,
|
|
355
|
-
team: s.teamName,
|
|
356
|
-
model: s.model,
|
|
357
|
-
action: "unmatched-session",
|
|
358
|
-
sessionId: capturedSessionId,
|
|
359
|
-
warning: "Session started with no matching team_spawn record. Agent identity may be inaccurate.",
|
|
360
|
-
})
|
|
361
|
-
}, UNMATCHED_SESSION_WARN_MS)
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (event?.type === "file.edited") {
|
|
366
|
-
const sessionId = event.properties?.sessionID ?? event.properties?.id
|
|
367
|
-
if (!sessionId) return
|
|
368
|
-
const state = sessionState.get(sessionId)
|
|
369
|
-
if (state) state.editCount++
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (event?.type === "session.idle") {
|
|
373
|
-
const sessionId = event.properties?.id ?? event.properties?.sessionID
|
|
374
|
-
if (!sessionId) return
|
|
375
|
-
const state = sessionState.get(sessionId)
|
|
376
|
-
if (!state) return
|
|
377
|
-
|
|
378
|
-
const skills = Array.from(state.skills || []).sort()
|
|
379
|
-
const usage = usagePayload(state)
|
|
380
|
-
appendEntry(directory, {
|
|
381
|
-
ts: ts(),
|
|
382
|
-
agent: state.agentName,
|
|
383
|
-
member: state.member,
|
|
384
|
-
agentType: state.agentType,
|
|
385
|
-
team: state.teamName,
|
|
386
|
-
model: state.model,
|
|
387
|
-
action: "completed",
|
|
388
|
-
filesEdited: state.editCount,
|
|
389
|
-
skills,
|
|
390
|
-
intendedSkills: state.intendedSkills,
|
|
391
|
-
usage,
|
|
392
|
-
})
|
|
393
|
-
|
|
394
|
-
trackCompletedByTeam(buildCompletedSnapshot(state, sessionId))
|
|
395
|
-
sessionState.delete(sessionId)
|
|
396
|
-
}
|
|
397
|
-
} catch {
|
|
398
|
-
// ignore
|
|
399
|
-
}
|
|
400
|
-
},
|
|
401
|
-
|
|
402
|
-
"tool.execute.after": async (input, output) => {
|
|
403
|
-
try {
|
|
404
|
-
const sessionId = input?.sessionID ?? input?.session_id
|
|
405
|
-
if (!sessionId) return
|
|
406
|
-
|
|
407
|
-
const state = sessionState.get(sessionId)
|
|
408
|
-
if (!state) return
|
|
409
|
-
|
|
410
|
-
const tool = input?.tool
|
|
411
|
-
const args = input?.args ?? {}
|
|
412
|
-
|
|
413
|
-
state.toolCalls++
|
|
414
|
-
state.charIn += JSON.stringify(args).length
|
|
415
|
-
state.charOut += JSON.stringify(output ?? {}).length
|
|
416
|
-
|
|
417
|
-
const reportedIn = extractReportedTokens(input)
|
|
418
|
-
const reportedOut = extractReportedTokens(output)
|
|
419
|
-
if (reportedIn.found) {
|
|
420
|
-
state.reportedInputTokens += reportedIn.input
|
|
421
|
-
state.reportedOutputTokens += reportedIn.output
|
|
422
|
-
state.reportedTotalTokens += reportedIn.total
|
|
423
|
-
}
|
|
424
|
-
if (reportedOut.found) {
|
|
425
|
-
state.reportedInputTokens += reportedOut.input
|
|
426
|
-
state.reportedOutputTokens += reportedOut.output
|
|
427
|
-
state.reportedTotalTokens += reportedOut.total
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if (DEBUG && !reportedIn.found && !reportedOut.found && tool !== "read") {
|
|
431
|
-
appendEntry(directory, {
|
|
432
|
-
ts: ts(),
|
|
433
|
-
agent: state.agentName,
|
|
434
|
-
action: "debug-no-token-metrics",
|
|
435
|
-
tool,
|
|
436
|
-
})
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Track skill loads via skill tool (primary)
|
|
440
|
-
if (tool === "skill") {
|
|
441
|
-
const skillName = input?.args?.name
|
|
442
|
-
const added = addSkillToState(state, skillName)
|
|
443
|
-
if (added) {
|
|
444
|
-
appendEntry(directory, {
|
|
445
|
-
ts: ts(),
|
|
446
|
-
agent: state.agentName,
|
|
447
|
-
member: state.member,
|
|
448
|
-
agentType: state.agentType,
|
|
449
|
-
team: state.teamName,
|
|
450
|
-
model: state.model,
|
|
451
|
-
action: "skill-loaded",
|
|
452
|
-
skill: skillName,
|
|
453
|
-
source: "skill-tool",
|
|
454
|
-
})
|
|
455
|
-
}
|
|
456
|
-
return
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Track skill loads via reading SKILL.md (fallback)
|
|
460
|
-
if (tool === "read") {
|
|
461
|
-
const filePath = input?.args?.filePath ?? ""
|
|
462
|
-
const match = filePath.match(/[/\\]skills[/\\]([^/\\]+)[/\\]SKILL\.md$/i)
|
|
463
|
-
if (match) {
|
|
464
|
-
const skillName = match[1]
|
|
465
|
-
const added = addSkillToState(state, skillName)
|
|
466
|
-
if (added) {
|
|
467
|
-
appendEntry(directory, {
|
|
468
|
-
ts: ts(),
|
|
469
|
-
agent: state.agentName,
|
|
470
|
-
member: state.member,
|
|
471
|
-
agentType: state.agentType,
|
|
472
|
-
team: state.teamName,
|
|
473
|
-
model: state.model,
|
|
474
|
-
action: "skill-loaded",
|
|
475
|
-
skill: skillName,
|
|
476
|
-
source: "read-skill-file",
|
|
477
|
-
})
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
return
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Track ensemble tool calls
|
|
484
|
-
const ensembleHandler = ENSEMBLE_TOOL_HANDLERS[tool]
|
|
485
|
-
if (!ensembleHandler) return
|
|
486
|
-
|
|
487
|
-
const entry = {
|
|
488
|
-
ts: ts(),
|
|
489
|
-
agent: state.agentName,
|
|
490
|
-
member: state.member,
|
|
491
|
-
agentType: state.agentType,
|
|
492
|
-
team: state.teamName,
|
|
493
|
-
model: state.model,
|
|
494
|
-
...ensembleHandler(args),
|
|
495
|
-
}
|
|
496
|
-
appendEntry(directory, entry)
|
|
497
|
-
|
|
498
|
-
if (tool === "team_create") {
|
|
499
|
-
leadTeamBySession.set(sessionId, args?.name || null)
|
|
500
|
-
state.teamName = args?.name || state.teamName
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
if (tool === "team_spawn") {
|
|
504
|
-
enqueuePendingSpawn(sessionId, args, output)
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
if (tool === "team_cleanup") {
|
|
508
|
-
const teamName = state.teamName || leadTeamBySession.get(sessionId)
|
|
509
|
-
appendEntry(directory, {
|
|
510
|
-
ts: ts(),
|
|
511
|
-
agent: state.agentName,
|
|
512
|
-
model: state.model,
|
|
513
|
-
action: "team-skills-summary",
|
|
514
|
-
team: teamName || null,
|
|
515
|
-
byAgent: teamName ? buildTeamSkillsSummary(teamName) : {},
|
|
516
|
-
})
|
|
517
|
-
}
|
|
518
|
-
} catch {
|
|
519
|
-
// ignore
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|