opencode-orchestrator 1.3.4 β†’ 1.3.5

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 CHANGED
@@ -1,59 +1,44 @@
1
-
2
- ---
3
-
4
1
  <div align="center">
5
- <img src="assets/logo.png" alt="logo" width="200" />
2
+ <img src="assets/logo.png" alt="OpenCode Orchestrator logo" width="160" />
6
3
  <h1>OpenCode Orchestrator</h1>
7
- <p>Production-Grade Multi-Agent Orchestration Engine for High-Integrity Software Engineering</p>
4
+ <p>Multi-agent mission control for OpenCode.</p>
8
5
 
9
6
  [![MIT License](https://img.shields.io/badge/license-MIT-red.svg)](LICENSE)
10
7
  [![npm](https://img.shields.io/npm/v/opencode-orchestrator.svg)](https://www.npmjs.com/package/opencode-orchestrator)
11
8
  <!-- VERSION:START -->
12
- **Version:** `1.3.4`
9
+ **Version:** `1.3.5`
13
10
  <!-- VERSION:END -->
14
11
  </div>
15
12
 
16
-
17
13
  ---
18
14
 
19
- ## ⚑ Quick Start
15
+ ## 1. Install
20
16
 
21
17
  ```bash
22
18
  npm install -g opencode-orchestrator
23
19
  ```
24
20
 
25
- Install hooks are source-checkout safe, prefer `opencode.jsonc` when present, preserve sibling plugin entries, and skip automatic config mutation in CI environments.
21
+ The install hook merges OpenCode config instead of replacing it, prefers `opencode.jsonc` when present, preserves existing plugin tuple options, and skips automatic config mutation in CI.
26
22
 
27
- To remove the plugin safely later, run:
23
+ To remove the plugin from OpenCode config:
28
24
 
29
25
  ```bash
30
26
  npm explore -g opencode-orchestrator -- npm run cleanup:plugin
31
27
  npm uninstall -g opencode-orchestrator
32
28
  ```
33
29
 
34
- `npm uninstall -g` does not run dependency uninstall hooks in the npm 11 flow verified for this repo, so config cleanup is explicit.
35
-
36
- ### Model, Permissions, and Concurrency
30
+ Manual fallback: remove `"opencode-orchestrator"` or `["opencode-orchestrator", {...}]` from the `plugin` array in `opencode.json` or `opencode.jsonc`.
37
31
 
38
- The plugin does not force a model by default. OpenCode's model rules apply: primary agents use the global `model` unless the agent has its own `model`, and subagents use the invoking primary agent's model unless the subagent has its own `model`.
32
+ ## 2. Configure
39
33
 
40
- Recommended `opencode.jsonc` configuration:
34
+ OpenCode supports plugin options as `["plugin-name", {...}]` tuples. Use that form for orchestrator-specific settings:
41
35
 
42
36
  ```jsonc
43
37
  {
44
38
  "$schema": "https://opencode.ai/config.json",
45
- "model": "opencode/gpt-5.1-codex",
46
39
  "permission": {
47
40
  "question": "allow"
48
41
  },
49
- "agent": {
50
- "commander": {
51
- "model": "opencode/gpt-5.1-codex"
52
- },
53
- "worker": {
54
- "model": "anthropic/claude-opus-4-5-20251101"
55
- }
56
- },
57
42
  "plugin": [
58
43
  [
59
44
  "opencode-orchestrator",
@@ -75,371 +60,92 @@ Recommended `opencode.jsonc` configuration:
75
60
  }
76
61
  ```
77
62
 
78
- Global permissions are copied into the generated Commander, Planner, Worker, and Reviewer agents. Same-name agent config keeps user model/options and can override permission keys per agent. When `permission.question` is `allow`, orchestrator agents can still ask concise clarification questions if they are truly blocked.
79
-
80
- Inside an OpenCode environment:
81
- ```bash
82
- /task "Implement a new authentication module with JWT and audit logs"
83
- ```
84
-
85
-
86
- ---
87
-
88
- ## πŸš€ Engine Workflow
89
-
90
- **Hub-and-Spoke Architecture** with Work-Stealing Queues for parallel, context-isolated task execution.
91
- ```
92
- ╔══════════════════════════════════════════════════════════════════════════════╗
93
- β•‘ USER INPUT /task "..." β•‘
94
- β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
95
- β”‚
96
- β–Ό
97
- ╔══════════════════════════════════════════════════════════════════════════════╗
98
- β•‘ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β•‘
99
- β•‘ β”‚ C O M M A N D E R β”‚ β•‘
100
- β•‘ β”‚ [ Mission Analysis & Delegation ] β”‚ β•‘
101
- β•‘ β”‚ β”‚ β•‘
102
- β•‘ β”‚ β€’ Interprets user intent β€’ Coordinates multi-agent workflow β”‚ β•‘
103
- β•‘ β”‚ β€’ Monitors progress β€’ Manages work-stealing queues β”‚ β•‘
104
- β•‘ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β•‘
105
- β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
106
- β”‚
107
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
108
- β–Ό β–Ό β–Ό
109
- ╔═════════════════╗ ╔═════════════════╗ ╔═════════════════╗
110
- β•‘ P L A N N E R β•‘ β•‘ W O R K E R β•‘ β•‘ W O R K E R β•‘
111
- β•‘ [Architect] β•‘ β•‘ [Implementer] β•‘ β•‘ [Implementer] β•‘
112
- β•‘ β•‘ β•‘ β•‘ β•‘ β•‘
113
- β•‘ β€’ Dependency β•‘ β•‘ β€’ File coding β•‘ β•‘ β€’ File coding β•‘
114
- β•‘ analysis β•‘ β•‘ β€’ TDD workflow β•‘ β•‘ β€’ TDD workflow β•‘
115
- β•‘ β€’ Roadmap gen β•‘ β•‘ β€’ Documentationβ•‘ β•‘ β€’ Documentationβ•‘
116
- β•‘ β€’ TODO.md β•‘ β•‘ β•‘ β•‘ β•‘
117
- β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
118
- β”‚ β”‚ β”‚
119
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
120
- β”‚ β”‚ β”‚
121
- β–Ό β–Ό β–Ό
122
- ╔══════════════════════════════════════╗
123
- β•‘ SESSION POOL (MVCC Sync) β•‘
124
- β•‘ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β•‘
125
- β•‘ β”‚ Object Pool β”‚ Buffer Pool β”‚ β•‘
126
- β•‘ β”‚ String Pool β”‚ Connection Pool β”‚ β•‘
127
- β•‘ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β•‘
128
- β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
129
- β”‚
130
- β–Ό
131
- ╔══════════════════════════════════════╗
132
- β•‘ MSVP MONITOR / REVIEWER β•‘
133
- β•‘ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β•‘
134
- β•‘ β”‚ β€’ Adaptive polling (500ms-5s) β”‚ β•‘
135
- β•‘ β”‚ β€’ Stability detection β”‚ β•‘
136
- β•‘ β”‚ β€’ Unit test verification β”‚ β•‘
137
- β•‘ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β•‘
138
- β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
139
- β”‚
140
- β–Ό
141
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
142
- β”‚ ✨ COMPLETED β”‚
143
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
144
- ```
145
-
146
- ---
147
-
148
- ## πŸ—οΈ Architecture Layers
63
+ Optional model routing stays in normal OpenCode config. The plugin does not force a model:
149
64
 
150
- ```
151
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
152
- β”‚ PRESENTATION LAYER β”‚
153
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
154
- β”‚ β”‚ Task Toast β”‚ β”‚ Progress β”‚ β”‚ Notificationβ”‚ β”‚ Mission Summary β”‚ β”‚
155
- β”‚ β”‚ Manager β”‚ β”‚ Notifier β”‚ β”‚ Manager β”‚ β”‚ Display β”‚ β”‚
156
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
157
- β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
158
- β”‚ BUSINESS LOGIC LAYER β”‚
159
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
160
- β”‚ β”‚ Parallel Agent Orchestration β”‚ β”‚
161
- β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
162
- β”‚ β”‚ β”‚ Commander β”‚ β”‚ Planner β”‚ β”‚ Worker β”‚ β”‚ Reviewer β”‚ β”‚ β”‚
163
- β”‚ β”‚ β”‚ Agent β”‚ β”‚ Agent β”‚ β”‚ Agent β”‚ β”‚ Agent β”‚ β”‚ β”‚
164
- β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
165
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
166
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
167
- β”‚ β”‚ Concurrency β”‚ β”‚ Task Store β”‚ β”‚ Hook System β”‚β”‚
168
- β”‚ β”‚ Controller β”‚ β”‚ (In-Memory) β”‚ β”‚ [Early/Normal/Late Phases] β”‚β”‚
169
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
170
- β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
171
- β”‚ INFRASTRUCTURE LAYER β”‚
172
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
173
- β”‚ β”‚ Session Pool β”‚ β”‚ Work-Stealing β”‚ β”‚ Memory Pools β”‚ β”‚
174
- β”‚ β”‚ [5 per agent] β”‚ β”‚ [Chase-Lev] β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”β”‚ β”‚
175
- β”‚ β”‚ [10 reuse max] β”‚ β”‚ [LIFO/FIFO] β”‚ β”‚ β”‚Objectβ”‚ β”‚Stringβ”‚ β”‚Bufferβ”‚β”‚ β”‚
176
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ 200 β”‚ β”‚ internβ”‚ β”‚ 4KB β”‚β”‚ β”‚
177
- β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜β”‚ β”‚
178
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
179
- β”‚ β”‚ MVCC State β”‚ β”‚ Circuit Breaker β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
180
- │ │ [Atomic Sync] │ │ [5 failures→open│ │ Rust Connection Pool │ │
181
- │ │ │ │ [2 success→close│ │ [4 processes, 30s idle] │ │
182
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
183
- β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
184
- β”‚ SAFETY LAYER β”‚
185
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
186
- β”‚ β”‚ RAII Pattern β”‚ β”‚ Shutdown β”‚ β”‚ Auto-Recovery β”‚ β”‚
187
- β”‚ β”‚ [Zero Leaks] β”‚ β”‚ Manager β”‚ β”‚ [Exponential Backoff] β”‚ β”‚
188
- β”‚ β”‚ β”‚ β”‚ [5s timeout] β”‚ β”‚ [Rate limit handling] β”‚ β”‚
189
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
190
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
65
+ ```jsonc
66
+ {
67
+ "model": "provider/model-id",
68
+ "agent": {
69
+ "commander": {
70
+ "model": "provider/model-id"
71
+ },
72
+ "worker": {
73
+ "model": "provider/stronger-model-id"
74
+ }
75
+ }
76
+ }
191
77
  ```
192
78
 
193
- ---
79
+ Generated Commander, Planner, Worker, and Reviewer agents inherit global permissions. Same-name user agent config can still override specific model or permission keys.
194
80
 
195
- ## πŸ§ͺ Test Utilities
81
+ ## 3. Run
196
82
 
197
- Reusable test helpers keep filesystem, task, and process-heavy flows deterministic:
83
+ Inside OpenCode:
198
84
 
199
- ```
200
- tests/harness/
201
- β”œβ”€β”€ fixture.ts Disposable tmpdir utilities
202
- β”‚ β”œβ”€β”€ tmpdir() async disposable with cleanup
203
- β”‚ β”œβ”€β”€ tmpdirSync() sync disposable with cleanup
204
- β”‚ β”œβ”€β”€ createMockFs() In-memory fs mock
205
- β”‚ └── waitFor() Async condition waiter
206
- β”‚
207
- β”œβ”€β”€ builders.ts Factory functions for test objects
208
- β”‚ β”œβ”€β”€ createParallelTask() Build ParallelTask instances
209
- β”‚ β”œβ”€β”€ createBackgroundTask() Build BackgroundTask instances
210
- β”‚ └── createTodo() Build Todo instances
211
- β”‚
212
- β”œβ”€β”€ mocks.ts Mock utilities
213
- β”‚ β”œβ”€β”€ mockConsole() Spy on console.log/error
214
- β”‚ β”œβ”€β”€ mockProcessExit() Mock process.exit
215
- β”‚ β”œβ”€β”€ useFakeTimers() Time manipulation
216
- β”‚ └── createMockEmitter() EventEmitter spy
217
- β”‚
218
- └── index.ts Unified exports
85
+ ```bash
86
+ /task "Implement the requested change and verify it"
219
87
  ```
220
88
 
221
- ### Usage Example
222
-
223
- ```typescript
224
- import { tmpdir, createParallelTask, mockConsole } from "@/tests/harness";
225
-
226
- describe("My test", () => {
227
- let consoleMock;
228
-
229
- beforeEach(() => {
230
- consoleMock = mockConsole();
231
- consoleMock.setup();
232
- });
233
-
234
- afterEach(() => {
235
- consoleMock.restore();
236
- });
237
-
238
- it("should work", async () => {
239
- await using tmp = await tmpdir({ git: true });
240
- const task = createParallelTask({ description: "Test" });
241
- expect(task.status).toBe("pending");
242
- });
243
- });
89
+ Mission controls:
90
+
91
+ 1. `/task ...` starts a persisted mission loop under `.opencode/`.
92
+ 2. `Esc`/OpenCode interrupt is respected by idle guards so the plugin does not immediately re-continue an interrupted turn.
93
+ 3. `/cancel` and `/stop` deactivate the current mission loop.
94
+ 4. The default mission iteration ceiling is `1,000,000,000`.
95
+
96
+ ## 4. How It Works
97
+
98
+ ```mermaid
99
+ flowchart LR
100
+ U["/task input"] --> C["Commander"]
101
+ C --> P["Planner"]
102
+ C --> W["Worker pool"]
103
+ W --> R["Reviewer"]
104
+ P --> S["Mission state"]
105
+ W --> S
106
+ R --> V{"Verified?"}
107
+ V -- "no" --> C
108
+ V -- "yes" --> D["Done"]
244
109
  ```
245
110
 
246
- ---
247
-
248
- ## ⚑ Elite Multi-Agent Swarm
249
-
250
- | Agent | Role | Core Responsibilities |
251
- |:------|:-----|:------------------------|
252
- | **Commander** | Mission Hub | Task decomposition, agent coordination, work-stealing orchestration, final mission seal |
253
- | **Planner** | Architect | Dependency analysis, roadmap generation, TODO.md creation via MVCC, file-level planning |
254
- | **Worker** | Implementer | High-throughput coding, TDD workflow, documentation, isolated file execution |
255
- | **Reviewer** | Auditor | Unit test verification, LSP/Lint validation, integration testing, quality gate |
256
- ---
257
-
258
- ## πŸ› οΈ Core Capabilities
259
-
260
- ### πŸ”’ Atomic MVCC State Synchronization
261
- Solves the "Concurrent TODO Update" problem using **MVCC + Mutex**. Agents safely mark tasks complete in parallel without data loss or race conditions. Every state change is cryptographically hashed and logged.
111
+ | Agent | Purpose |
112
+ | --- | --- |
113
+ | Commander | Interprets the mission, coordinates agents, and keeps the loop aligned. |
114
+ | Planner | Breaks work into ordered steps and tracks dependencies. |
115
+ | Worker | Implements scoped file changes with isolated context. |
116
+ | Reviewer | Checks completion evidence, tests, and integration risk. |
262
117
 
263
- ### 🧩 Advanced Hook Orchestration
264
- Execution flows governed by a **Priority-Phase Hook Registry**. Hooks are grouped into phases (`early`, `normal`, `late`) and executed via **Topological Sort** for predictable, dependency-aware ordering.
118
+ Runtime evidence is written only when enabled:
265
119
 
266
- ### πŸŽ›οΈ Mission Loop Control
267
- `/task` starts a persisted mission loop under `.opencode/`. The loop watches TODO/checklist verification, background task state, compaction safety, recovery state, and stagnation signals before injecting compact continuation prompts that keep Commander aligned with the active objective.
120
+ | Artifact | Purpose |
121
+ | --- | --- |
122
+ | `.opencode/mission-ledger.jsonl` | Bounded event trail for mission decisions. |
123
+ | `.opencode/docs/brain/scratchpad.md` | Generated Markdown memory surface for active missions. |
124
+ | `.opencode/docs/brain/knowledge-map.canvas` | Obsidian-compatible visual map of objective, evidence, and verification nodes. |
268
125
 
269
- The mission loop also writes a bounded runtime evidence trail to `.opencode/mission-ledger.jsonl` and a generated Markdown memory surface under `.opencode/docs/brain/`. Disable those artifacts with the `missionLoop.ledger` and `missionLoop.markdownMemory` plugin options when a project needs a quieter workspace.
270
-
271
- ### πŸ›‘οΈ Autonomous Recovery
272
- - **Self-healing loops** with adaptive stagnation detection
273
- - **Proactive Agency**: Smart monitoring that audits logs and plans ahead during background tasks
274
- - **Auto-retry with backoff**: Exponential backoff for transient failures
275
-
276
- ### 🎯 State-Level Session Isolation
277
- Reused sessions in the **SessionPool** are explicitly reset via server-side compaction, ensuring previous task context never leaks into new tasks.
278
-
279
- ### πŸš€ Zero-Payload Turbo Mode
280
- Leverages `system.transform` to unshift agent instruction sets server-side, reducing initial message payloads by **90%+** and preventing context fragmentation.
281
-
282
- ### 🧠 Hierarchical Memory System
283
- Maintains focus across thousands of conversation turns using a 4-tier memory structure with **EMA-based Context Gating** to preserve architectural truth while pruning noise.
284
-
285
- ### πŸ”„ Adaptive Intelligence Loop
286
- - **Stagnation Detection**: Senses when no progress is made across iterations
287
- - **Diagnostic Intervention**: Forces "Diagnostic Mode" mandating log audits and strategy pivots
288
- - **Proactive Agency**: Mandates Speculative Planning during background task execution
289
-
290
- ### 🧬 Knowledge Graph RAG (Second Brain)
291
- An autonomous **Obsidian-style evolutionary memory system** that gives agents persistent, searchable knowledge across sessions.
292
-
293
- Runtime context injection is active for orchestrated sessions through `experimental.chat.system.transform`, indexing `docs/**/*.md` and `.opencode/docs/**/*.md` with BM25/tag/graph fusion before prompt injection.
294
-
295
- During active missions, generated runtime memory is available at `.opencode/docs/brain/scratchpad.md`; `.opencode/docs/brain/knowledge-map.canvas` provides an Obsidian-compatible visual map of objective, runtime, verification, and recent evidence nodes.
296
-
297
- ```
298
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
299
- β”‚ πŸ“ Ingest │────▢│ πŸ” Search │────▢│ 🧠 Recall β”‚
300
- β”‚ ────────── β”‚ β”‚ ────────── β”‚ β”‚ ────────── β”‚
301
- β”‚ YAML Parser β”‚ β”‚ BM25 Lexicalβ”‚ β”‚ RRF-Ranked β”‚
302
- β”‚ Tag HashMap β”‚ β”‚ Tag Index β”‚ β”‚ Top-K Notes β”‚
303
- β”‚ Wiki-Links β”‚ β”‚ 2-Hop Graph β”‚ β”‚ β†’ Context β”‚
304
- β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
305
- β”‚
306
- β–Ό
307
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
308
- β”‚ πŸ”„ Evolve │────▢│ πŸ›‘οΈ Guard │────▢│ πŸ“¦ Archive β”‚
309
- β”‚ ────────── β”‚ β”‚ ────────── β”‚ β”‚ ────────── β”‚
310
- β”‚ Fission β”‚ β”‚ Cycle DFS β”‚ β”‚ Orphan GC β”‚
311
- β”‚ Fusion β”‚ β”‚ Write FIFO β”‚ β”‚ MOC Hubs β”‚
312
- β”‚ (Auto-Split β”‚ β”‚ Keep-Pin β”‚ β”‚ MD Export β”‚
313
- β”‚ & Merge) β”‚ β”‚ Shield β”‚ β”‚ β”‚
314
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
315
- ```
316
-
317
- **Six integrated modules** power the knowledge pipeline:
318
-
319
- | Module | Purpose |
320
- |:-------|:--------|
321
- | **TagIndexer** | O(1) YAML frontmatter parsing β†’ tag-to-file HashMap |
322
- | **GraphParser** | `[[wiki-link]]` extraction, bi-directional adjacency graph |
323
- | **HybridSearch** | BM25 + Tag + 2-Hop Graph β†’ **Reciprocal Rank Fusion** (k=60) |
324
- | **Scratchpad** | LRU register cache (64 slots, 4KB max) with markdown persistence |
325
- | **SafetyGuards** | Circular link DFS, async FIFO write queue, keep-pin shield |
326
- | **MemoryConsolidation** | Fission/Fusion/GC/MOC β€” pure functional analysis |
327
-
328
- ---
329
-
330
- ## βš™οΈ Performance Benchmarks
331
-
332
- | Metric | Improvement |
333
- |:-------|:------------|
334
- | CPU Utilization | 90%+ (up from 50-70%) |
335
- | Tool Call Speed | 10x faster (5-10ms vs 50-100ms) via Rust pool |
336
- | Session Creation | 90% faster (50ms vs 500ms) via session pooling |
337
- | Memory Usage | 60% reduction via object/string/buffer pooling |
338
- | GC Pressure | 80% reduction |
339
- | Token Efficiency | 40% reduction via Incremental State & System Transform |
340
- | Sync Accuracy | 99.95% via MVCC+Mutex |
341
- | Parallel Efficiency | 80% improvement (50% β†’ 90%+) |
342
-
343
- ---
344
-
345
- ## πŸ—οΈ Infrastructure & Reliability
346
-
347
- ### Resource Safety
348
- - **RAII Pattern**: Guaranteed resource cleanup with zero leaks
349
- - **ShutdownManager**: Priority-based graceful shutdown (5s timeout per handler)
350
- - **Atomic File Operations**: Temp file + rename for corruption-proof writes
351
- - **Automatic Backups**: Timestamped config backups with rollback support
352
-
353
- ### Safety Features
354
- - **Circuit Breaker**: Auto-recovery from API failures (5 failures β†’ open)
355
- - **Resource Pressure Detection**: Rejects low-priority tasks when memory > 80%
356
- - **Terminal Node Guard**: Prevents infinite recursion via depth limit
357
- - **Auto-Scaling**: Concurrency slots adjust based on success/failure rate
358
-
359
- ### Technical Stack
360
- - **Runtime**: Node.js 24+ (TypeScript)
361
- - **Tools**: Rust-based CLI tools (grep, glob, ast) via connection pool
362
- - **Concurrency**: Chase-Lev work-stealing deque + priority queues
363
- - **Memory**: Object pooling + string interning + buffer pooling
364
- - **State Management**: MVCC + Mutex
365
- - **Safety**: RAII + circuit breaker + resource pressure detection
366
- - **Knowledge**: In-memory graph RAG with BM25/Tag/Graph search fusion
367
-
368
- ---
369
-
370
- ## πŸ”§ Installation & Configuration
371
-
372
- ### Safe Installation
373
- The installation process is **production-safe** with multiple protection layers:
374
-
375
- 1. βœ… **Never overwrites** β€” always merges with existing config
376
- 2. βœ… **Automatic backups** β€” timestamped, last 5 kept
377
- 3. βœ… **Atomic writes** β€” temp file + rename (OS-level atomic)
378
- 4. βœ… **Automatic rollback** β€” restores from backup on any failure
379
- 5. βœ… **Cross-platform** β€” Windows (native, Git Bash, WSL2), macOS, Linux
380
- 6. βœ… **CI-aware** β€” skips non-essential operations in CI environments
381
- 7. βœ… **Timeout protection** β€” 30s timeout prevents hanging
382
- 8. βœ… **Graceful degradation** β€” exits 0 on non-critical failures
383
-
384
- ### Safe Removal
385
- OpenCode config cleanup is provided as an explicit command because global package uninstall does not invoke dependency uninstall hooks in the npm flow validated for this package.
126
+ ## 5. Developer Notes
386
127
 
387
128
  ```bash
388
- npm explore -g opencode-orchestrator -- npm run cleanup:plugin
389
- npm uninstall -g opencode-orchestrator
129
+ npm run build
130
+ npx tsc --noEmit
131
+ npm test
132
+ cargo test --workspace --all-targets
390
133
  ```
391
134
 
392
- Manual fallback:
393
- - Open `~/.config/opencode/opencode.json` or `opencode.jsonc`
394
- - Remove `"opencode-orchestrator"` from the `plugin` array
395
-
396
- ### Configuration Logs
397
- - Unix: `/tmp/opencode-orchestrator.log`
398
- - Windows: `%TEMP%\opencode-orchestrator.log`
135
+ Useful references:
399
136
 
400
- ---
401
-
402
- ## πŸ§ͺ Testing & Stability
403
-
404
- ### Test Utilities
405
- Reusable helpers under `tests/harness/` provide:
406
- - **Disposable tmpdir**: Automatic cleanup with `Symbol.asyncDispose` / `Symbol.dispose`
407
- - **Test builders**: Factory functions for `ParallelTask`, `BackgroundTask`, `Todo`
408
- - **Mock utilities**: Console, process, timers, file system, event emitter mocks
137
+ 1. OpenCode plugins: https://opencode.ai/docs/plugins/
138
+ 2. OpenCode config: https://opencode.ai/docs/config/
139
+ 3. OpenCode keybinds: https://opencode.ai/docs/keybinds/
140
+ 4. Project issues: https://github.com/agnusdei1207/opencode-orchestrator/issues
409
141
 
410
- ```typescript
411
- import { tmpdir, createParallelTask, mockConsole } from "@/tests/harness";
142
+ Config logs:
412
143
 
413
- await using tmp = await tmpdir({ git: true });
414
- const task = createParallelTask({ description: "Test" });
415
- ```
416
-
417
- ### TUI Stability
418
- - **Cleanup guarantees**: `initToastClient()` returns a cleanup function
419
- - **Timeout protection**: AbortController-based 2-10s timeout for async toast operations
420
- - **Error isolation**: Try/catch around all toast operations prevents cascade failures
144
+ | Platform | Path |
145
+ | --- | --- |
146
+ | Unix | `/tmp/opencode-orchestrator.log` |
147
+ | Windows | `%TEMP%\opencode-orchestrator.log` |
421
148
 
422
- ### Cross-Platform Reliability
423
- - **Windows guard**: Proper handling of WSL2, Git Bash, native Windows paths
424
- - **Signal handling**: Graceful shutdown on SIGINT/SIGTERM
425
- - **Process isolation**: Child process cleanup with timeout
149
+ ## 6. License
426
150
 
427
- ---
428
-
429
- ## πŸ“š Documentation
430
-
431
- - [System Architecture Deep-Dive β†’](docs/SYSTEM_ARCHITECTURE.md)
432
- - [Developer Notes β†’](docs/DEVELOPERS_NOTE.md)
433
- - [Knowledge RAG Roadmap β†’](docs/histories/2026/05/31/PLAN_SecondBrainOrchestration_2026-05-31.md)
434
-
435
- ---
436
-
437
- ## πŸ“„ License
438
-
439
- MIT License β€” see [LICENSE](LICENSE) for details.
440
-
441
- ---
442
-
443
- <div align="center">
444
- <sub>Built with ⚑ for production-grade autonomous software engineering</sub>
445
- </div>
151
+ MIT License. See [LICENSE](LICENSE).
@@ -4,7 +4,7 @@
4
4
  import { SessionState } from "./interfaces/session-state.js";
5
5
  export declare const state: {
6
6
  missionActive: boolean;
7
- maxIterations: 10000;
7
+ maxIterations: 1000000000;
8
8
  maxRetries: 3;
9
9
  sessions: Map<string, SessionState>;
10
10
  };
@@ -11,6 +11,7 @@ export declare class MissionControlHook implements AssistantDoneHook, ChatMessag
11
11
  name: "MissionLoop";
12
12
  execute(ctx: HookContext, text: string): Promise<any>;
13
13
  private handleChatCommand;
14
+ private cancelMission;
14
15
  private handleMissionProgress;
15
16
  private buildContinuationResponse;
16
17
  private handleMissionComplete;
package/dist/index.js CHANGED
@@ -199,7 +199,7 @@ var init_limits = __esm({
199
199
  "use strict";
200
200
  LIMITS = {
201
201
  /** Maximum mission loop iterations */
202
- MAX_ITERATIONS: 1e4,
202
+ MAX_ITERATIONS: 1e9,
203
203
  /** Default scan limit for file listing */
204
204
  DEFAULT_SCAN_LIMIT: 20,
205
205
  /** Max message history to check for conclusion */
@@ -1477,6 +1477,9 @@ var init_session_events = __esm({
1477
1477
  "use strict";
1478
1478
  SESSION_EVENTS = {
1479
1479
  IDLE: "session.idle",
1480
+ STATUS: "session.status",
1481
+ UPDATED: "session.updated",
1482
+ COMPACTED: "session.compacted",
1480
1483
  BUSY: "session.busy",
1481
1484
  ERROR: "session.error",
1482
1485
  DELETED: "session.deleted",
@@ -36896,6 +36899,9 @@ function ensureSessionInitialized(sessions, sessionID, directory) {
36896
36899
  startTime: now,
36897
36900
  lastStepTime: now,
36898
36901
  lastCompletedMessageID: void 0,
36902
+ lastUserMessageAt: void 0,
36903
+ lastAssistantCompletedAt: void 0,
36904
+ lastAbortAt: void 0,
36899
36905
  tokens: { totalInput: 0, totalOutput: 0, estimatedCost: 0 }
36900
36906
  };
36901
36907
  if (directory) {
@@ -36950,6 +36956,14 @@ function isMissionActive(sessionID, directory) {
36950
36956
  }
36951
36957
  return false;
36952
36958
  }
36959
+ function deactivateMissionState(sessionID) {
36960
+ const stateSession = state.sessions.get(sessionID);
36961
+ if (stateSession) {
36962
+ stateSession.enabled = false;
36963
+ }
36964
+ state.missionActive = false;
36965
+ log(`[SessionManager] Mission Deactivated: ${sessionID}`);
36966
+ }
36953
36967
  var COST_PER_1K_INPUT = 3e-3;
36954
36968
  var COST_PER_1K_OUTPUT = 0.015;
36955
36969
  function updateSessionTokens(sessions, sessionID, inputLen, outputLen) {
@@ -37712,7 +37726,12 @@ var MissionControlHook = class {
37712
37726
  // -------------------------------------------------------------------------------
37713
37727
  async handleChatCommand(ctx, message) {
37714
37728
  const parsed = detectSlashCommand(message);
37715
- if (!parsed || parsed.command !== COMMAND_NAMES.TASK) return null;
37729
+ if (!parsed) return null;
37730
+ if (parsed.command === COMMAND_NAMES.CANCEL || parsed.command === COMMAND_NAMES.STOP) {
37731
+ await this.cancelMission(ctx);
37732
+ return { action: HOOK_ACTIONS.INTERCEPT };
37733
+ }
37734
+ if (parsed.command !== COMMAND_NAMES.TASK) return null;
37716
37735
  const command = COMMANDS[parsed.command];
37717
37736
  const { sessionID, sessions, directory } = ctx;
37718
37737
  log(MISSION_MESSAGES.START_LOG);
@@ -37730,6 +37749,17 @@ var MissionControlHook = class {
37730
37749
  }
37731
37750
  return { action: HOOK_ACTIONS.PROCESS };
37732
37751
  }
37752
+ async cancelMission(ctx) {
37753
+ const { sessionID, sessions, directory } = ctx;
37754
+ log(MISSION_MESSAGES.CANCEL_LOG);
37755
+ await cancelMissionLoop(directory, sessionID);
37756
+ deactivateMissionState(sessionID);
37757
+ clearSession2(sessionID);
37758
+ const session = sessions.get(sessionID);
37759
+ if (isRecord(session)) {
37760
+ session.active = false;
37761
+ }
37762
+ }
37733
37763
  // -------------------------------------------------------------------------------
37734
37764
  // 2. Done Logic: Check Completion & Auto-Continue
37735
37765
  // -------------------------------------------------------------------------------
@@ -37782,6 +37812,9 @@ var MissionControlHook = class {
37782
37812
  // 4. Helper: Build Continuation Response
37783
37813
  // -------------------------------------------------------------------------------
37784
37814
  buildContinuationResponse(session, sessionID) {
37815
+ if (!isMissionSessionState(session)) {
37816
+ return { action: HOOK_ACTIONS.CONTINUE };
37817
+ }
37785
37818
  const now = Date.now();
37786
37819
  const stepDuration = formatElapsedTime(session.lastStepTime, now);
37787
37820
  const totalElapsed = formatElapsedTime(session.startTime, now);
@@ -37839,6 +37872,12 @@ var MissionControlHook = class {
37839
37872
  }
37840
37873
  }
37841
37874
  };
37875
+ function isRecord(value) {
37876
+ return typeof value === "object" && value !== null && !Array.isArray(value);
37877
+ }
37878
+ function isMissionSessionState(value) {
37879
+ return isRecord(value) && typeof value.step === "number" && typeof value.startTime === "number" && typeof value.lastStepTime === "number";
37880
+ }
37842
37881
 
37843
37882
  // src/hooks/custom/strict-role-guard.ts
37844
37883
  init_shared();
@@ -38557,6 +38596,13 @@ function handleSessionError2(sessionID, error95) {
38557
38596
  }
38558
38597
  cancelCountdown(sessionID);
38559
38598
  }
38599
+ function handleAbort(sessionID) {
38600
+ const state2 = getState3(sessionID);
38601
+ state2.isAborting = true;
38602
+ state2.abortDetectedAt = Date.now();
38603
+ cancelCountdown(sessionID);
38604
+ log("[todo-continuation] Marked as aborting", { sessionID });
38605
+ }
38560
38606
  function cleanupSession2(sessionID) {
38561
38607
  cancelCountdown(sessionID);
38562
38608
  sessionStates2.delete(sessionID);
@@ -41137,11 +41183,11 @@ var CONCURRENCY_KEYS = [
41137
41183
  "providerConcurrency",
41138
41184
  "modelConcurrency"
41139
41185
  ];
41140
- function isRecord(value) {
41186
+ function isRecord2(value) {
41141
41187
  return typeof value === "object" && value !== null && !Array.isArray(value);
41142
41188
  }
41143
41189
  function readLimitMap(value) {
41144
- if (!isRecord(value)) return void 0;
41190
+ if (!isRecord2(value)) return void 0;
41145
41191
  const result = {};
41146
41192
  for (const [key, limit] of Object.entries(value)) {
41147
41193
  if (typeof limit === "number" && Number.isFinite(limit) && limit >= 0) {
@@ -41151,7 +41197,7 @@ function readLimitMap(value) {
41151
41197
  return Object.keys(result).length > 0 ? result : void 0;
41152
41198
  }
41153
41199
  function extractConcurrencyConfig(source) {
41154
- if (!isRecord(source)) return {};
41200
+ if (!isRecord2(source)) return {};
41155
41201
  const config3 = {};
41156
41202
  if (typeof source.defaultConcurrency === "number" && source.defaultConcurrency >= 0) {
41157
41203
  config3.defaultConcurrency = source.defaultConcurrency;
@@ -41165,7 +41211,7 @@ function extractConcurrencyConfig(source) {
41165
41211
  return config3;
41166
41212
  }
41167
41213
  function hasConcurrencyConfig(source) {
41168
- if (!isRecord(source)) return false;
41214
+ if (!isRecord2(source)) return false;
41169
41215
  return CONCURRENCY_KEYS.some((key) => source[key] !== void 0);
41170
41216
  }
41171
41217
  function mergeConcurrencyConfig(base, override) {
@@ -41189,14 +41235,14 @@ function mergeConcurrencyConfig(base, override) {
41189
41235
 
41190
41236
  // src/core/config/plugin-options.ts
41191
41237
  function parseOrchestratorPluginOptions(options) {
41192
- const source = isRecord2(options) ? options : {};
41238
+ const source = isRecord3(options) ? options : {};
41193
41239
  return {
41194
41240
  concurrency: extractConcurrencyConfig(source),
41195
41241
  missionLoop: readMissionLoopOptions(source.missionLoop)
41196
41242
  };
41197
41243
  }
41198
41244
  function readMissionLoopOptions(value) {
41199
- if (!isRecord2(value)) return DEFAULT_MISSION_RUNTIME_OPTIONS;
41245
+ if (!isRecord3(value)) return DEFAULT_MISSION_RUNTIME_OPTIONS;
41200
41246
  return {
41201
41247
  ledger: readBoolean(value.ledger, DEFAULT_MISSION_RUNTIME_OPTIONS.ledger),
41202
41248
  markdownMemory: readBoolean(value.markdownMemory, DEFAULT_MISSION_RUNTIME_OPTIONS.markdownMemory),
@@ -41212,7 +41258,7 @@ function readBoolean(value, fallback) {
41212
41258
  function readPositiveInteger(value, fallback) {
41213
41259
  return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : fallback;
41214
41260
  }
41215
- function isRecord2(value) {
41261
+ function isRecord3(value) {
41216
41262
  return typeof value === "object" && value !== null && !Array.isArray(value);
41217
41263
  }
41218
41264
 
@@ -41259,6 +41305,7 @@ function createChatMessageHandler(ctx) {
41259
41305
  const sessionID = msgInput.sessionID;
41260
41306
  const agentName = (msgInput.agent || "").toLowerCase();
41261
41307
  log("[chat-message-handler] hook triggered", { sessionID, agent: agentName, textLength: originalText.length });
41308
+ markUserMessage(sessions, sessionID);
41262
41309
  if (sessionID && !sessions.has(sessionID)) {
41263
41310
  }
41264
41311
  const hooks = HookRegistry.getInstance();
@@ -41269,6 +41316,7 @@ function createChatMessageHandler(ctx) {
41269
41316
  };
41270
41317
  const hookResult = await hooks.executeChat(hookContext, originalText);
41271
41318
  if (hookResult.action === HOOK_ACTIONS.INTERCEPT) {
41319
+ parts.splice(0, parts.length);
41272
41320
  return;
41273
41321
  }
41274
41322
  if (hookResult.modifiedMessage) {
@@ -41276,6 +41324,11 @@ function createChatMessageHandler(ctx) {
41276
41324
  }
41277
41325
  };
41278
41326
  }
41327
+ function markUserMessage(sessions, sessionID) {
41328
+ const session = sessions.get(sessionID);
41329
+ if (!session) return;
41330
+ session.lastUserMessageAt = Date.now();
41331
+ }
41279
41332
 
41280
41333
  // src/plugin-handlers/config-handler.ts
41281
41334
  init_shared();
@@ -41327,7 +41380,7 @@ This plugin runs in "Claude Code Compatibility Mode".
41327
41380
  }
41328
41381
 
41329
41382
  // src/plugin-handlers/config-handler.ts
41330
- function isRecord3(value) {
41383
+ function isRecord4(value) {
41331
41384
  return typeof value === "object" && value !== null && !Array.isArray(value);
41332
41385
  }
41333
41386
  function isPermissionAction(value) {
@@ -41337,10 +41390,10 @@ function mergePermission(globalPermission, agentPermission) {
41337
41390
  if (agentPermission === void 0) {
41338
41391
  return globalPermission;
41339
41392
  }
41340
- if (isRecord3(globalPermission) && isRecord3(agentPermission)) {
41393
+ if (isRecord4(globalPermission) && isRecord4(agentPermission)) {
41341
41394
  return { ...globalPermission, ...agentPermission };
41342
41395
  }
41343
- if (isPermissionAction(globalPermission) && isRecord3(agentPermission)) {
41396
+ if (isPermissionAction(globalPermission) && isRecord4(agentPermission)) {
41344
41397
  return { "*": globalPermission, ...agentPermission };
41345
41398
  }
41346
41399
  return agentPermission;
@@ -42045,9 +42098,11 @@ async function handleMissionIdle(client, directory, sessionID, mainSessionID) {
42045
42098
  }, MISSION_CONTROL.DEFAULT_COUNTDOWN_SECONDS * 1e3);
42046
42099
  }
42047
42100
  function handleUserMessage2(sessionID) {
42101
+ const state2 = sessionStateStore.getState(sessionID);
42102
+ state2.isAborting = false;
42048
42103
  sessionStateStore.cancelCountdown(sessionID);
42049
42104
  }
42050
- function handleAbort(sessionID) {
42105
+ function handleAbort2(sessionID) {
42051
42106
  const state2 = sessionStateStore.getState(sessionID);
42052
42107
  state2.isAborting = true;
42053
42108
  sessionStateStore.cancelCountdown(sessionID);
@@ -42148,7 +42203,7 @@ function createEventHandler(ctx) {
42148
42203
  const error95 = event.properties?.error;
42149
42204
  if (sessionID) {
42150
42205
  handleSessionError2(sessionID, error95);
42151
- handleAbort(sessionID);
42206
+ handleAbort2(sessionID);
42152
42207
  }
42153
42208
  if (sessionID && error95) {
42154
42209
  const recovered = await handleSessionError(
@@ -42175,6 +42230,7 @@ function createEventHandler(ctx) {
42175
42230
  if (sessionID && role === MESSAGE_ROLES.ASSISTANT) {
42176
42231
  markRecoveryComplete(sessionID);
42177
42232
  if (messageInfo?.id && messageInfo.time?.completed) {
42233
+ markAssistantCompleted(sessions, sessionID);
42178
42234
  await handleCompletedAssistantMessage(ctx, sessionID, messageInfo.id);
42179
42235
  }
42180
42236
  }
@@ -42186,35 +42242,75 @@ function createEventHandler(ctx) {
42186
42242
  if (event.type === SESSION_EVENTS.IDLE) {
42187
42243
  const sessionID = event.properties?.sessionID || "";
42188
42244
  if (sessionID) {
42189
- const isMainSession = sessions.has(sessionID);
42190
- if (isMainSession) {
42191
- setTimeout(async () => {
42192
- const session = sessions.get(sessionID);
42193
- if (session?.active) {
42194
- if (isLoopActive(directory, sessionID)) {
42195
- await handleMissionIdle(
42196
- client,
42197
- directory,
42198
- sessionID,
42199
- sessionID
42200
- ).catch(() => {
42201
- });
42202
- } else {
42203
- await handleSessionIdle(
42204
- client,
42205
- directory,
42206
- sessionID,
42207
- sessionID
42208
- ).catch(() => {
42209
- });
42210
- }
42211
- }
42212
- }, 500);
42213
- }
42245
+ scheduleIdleContinuation(ctx, sessionID);
42246
+ }
42247
+ }
42248
+ if (event.type === SESSION_EVENTS.STATUS) {
42249
+ const properties = event.properties;
42250
+ const sessionID = properties?.sessionID ?? "";
42251
+ if (sessionID && properties?.status?.type === "idle") {
42252
+ scheduleIdleContinuation(ctx, sessionID);
42214
42253
  }
42215
42254
  }
42216
42255
  };
42217
42256
  }
42257
+ function markAssistantCompleted(sessions, sessionID) {
42258
+ const session = sessions.get(sessionID);
42259
+ if (!session) return;
42260
+ session.lastAssistantCompletedAt = Date.now();
42261
+ }
42262
+ function shouldContinueAfterIdle(session) {
42263
+ if (!session?.active || !session.lastAssistantCompletedAt) {
42264
+ return false;
42265
+ }
42266
+ if (session.lastUserMessageAt && session.lastAssistantCompletedAt < session.lastUserMessageAt) {
42267
+ return false;
42268
+ }
42269
+ if (session.lastAbortAt && session.lastAssistantCompletedAt < session.lastAbortAt) {
42270
+ return false;
42271
+ }
42272
+ return true;
42273
+ }
42274
+ function markAbort(sessions, sessionID) {
42275
+ const session = sessions.get(sessionID);
42276
+ if (session) {
42277
+ session.lastAbortAt = Date.now();
42278
+ }
42279
+ handleAbort(sessionID);
42280
+ handleAbort2(sessionID);
42281
+ }
42282
+ function scheduleIdleContinuation(ctx, sessionID) {
42283
+ const { client, directory, sessions } = ctx;
42284
+ if (!sessions.has(sessionID)) return;
42285
+ setTimeout(async () => {
42286
+ const session = sessions.get(sessionID);
42287
+ if (!shouldContinueAfterIdle(session)) {
42288
+ markAbort(sessions, sessionID);
42289
+ return;
42290
+ }
42291
+ if (isLoopActive(directory, sessionID)) {
42292
+ try {
42293
+ await handleMissionIdle(
42294
+ client,
42295
+ directory,
42296
+ sessionID,
42297
+ sessionID
42298
+ );
42299
+ } catch {
42300
+ }
42301
+ return;
42302
+ }
42303
+ try {
42304
+ await handleSessionIdle(
42305
+ client,
42306
+ directory,
42307
+ sessionID,
42308
+ sessionID
42309
+ );
42310
+ } catch {
42311
+ }
42312
+ }, 500);
42313
+ }
42218
42314
 
42219
42315
  // src/plugin-handlers/tool-execute-handler.ts
42220
42316
  init_logger();
@@ -8,6 +8,9 @@ export interface SessionState {
8
8
  startTime: number;
9
9
  lastStepTime: number;
10
10
  lastCompletedMessageID?: string;
11
+ lastUserMessageAt?: number;
12
+ lastAssistantCompletedAt?: number;
13
+ lastAbortAt?: number;
11
14
  tokens: {
12
15
  totalInput: number;
13
16
  totalOutput: number;
@@ -1459,8 +1459,22 @@ function formatError(err, context) {
1459
1459
  return `Failed to ${context}: ${String(err)}`;
1460
1460
  }
1461
1461
  var PLUGIN_NAME = "opencode-orchestrator";
1462
- function isOurPluginEntry(p) {
1463
- return p === PLUGIN_NAME || p.startsWith(`${PLUGIN_NAME}@`);
1462
+ function isRecord(value) {
1463
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1464
+ }
1465
+ function isPluginTuple(value) {
1466
+ return Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && isRecord(value[1]);
1467
+ }
1468
+ function isPluginEntry(value) {
1469
+ return typeof value === "string" || isPluginTuple(value);
1470
+ }
1471
+ function getPluginName(entry) {
1472
+ return typeof entry === "string" ? entry : entry[0];
1473
+ }
1474
+ function isOurPluginEntry(entry) {
1475
+ if (!isPluginEntry(entry)) return false;
1476
+ const pluginName = getPluginName(entry);
1477
+ return pluginName === PLUGIN_NAME || pluginName.startsWith(`${PLUGIN_NAME}@`);
1464
1478
  }
1465
1479
  function getConfigFileCandidates(configDir) {
1466
1480
  return [join(configDir, "opencode.jsonc"), join(configDir, "opencode.json")];
@@ -1578,8 +1592,8 @@ function validateConfig(config) {
1578
1592
  return false;
1579
1593
  }
1580
1594
  if (config.plugin) {
1581
- for (const p of config.plugin) {
1582
- if (typeof p !== "string") {
1595
+ for (const entry of config.plugin) {
1596
+ if (!isPluginEntry(entry)) {
1583
1597
  return false;
1584
1598
  }
1585
1599
  }
@@ -1672,10 +1686,7 @@ function registerInConfig(configDir) {
1672
1686
  config["$schema"] = "https://opencode.ai/config.json";
1673
1687
  }
1674
1688
  }
1675
- const hasPlugin = config.plugin.some((p) => {
1676
- if (typeof p !== "string") return false;
1677
- return isOurPluginEntry(p);
1678
- });
1689
+ const hasPlugin = config.plugin.some((entry) => isOurPluginEntry(entry));
1679
1690
  if (hasPlugin) {
1680
1691
  log("Plugin already registered", { configFile });
1681
1692
  return { success: false, backupFile };
@@ -1693,7 +1704,7 @@ function registerInConfig(configDir) {
1693
1704
  throw new Error(`Verification parse failed: ${verifyParsed.parseError ?? "unknown parse error"}`);
1694
1705
  }
1695
1706
  const verifyConfig = verifyParsed.config;
1696
- if (!verifyConfig.plugin?.some((p) => isOurPluginEntry(p))) {
1707
+ if (!verifyConfig.plugin?.some((entry) => isOurPluginEntry(entry))) {
1697
1708
  throw new Error("Verification failed: plugin not found after write");
1698
1709
  }
1699
1710
  } catch (verifyError) {
@@ -1760,7 +1771,7 @@ try {
1760
1771
  continue;
1761
1772
  }
1762
1773
  targetConfigDir = configDir;
1763
- if (existing.config.plugin?.some((p) => typeof p === "string" && isOurPluginEntry(p))) {
1774
+ if (existing.config.plugin?.some((entry) => isOurPluginEntry(entry))) {
1764
1775
  alreadyRegistered = true;
1765
1776
  log("Plugin already registered in this location", { configFile: existing.file });
1766
1777
  break;
@@ -1459,8 +1459,22 @@ function formatError(err, context) {
1459
1459
  return `Failed to ${context}: ${String(err)}`;
1460
1460
  }
1461
1461
  var PLUGIN_NAME = "opencode-orchestrator";
1462
- function isOurPluginEntry(p) {
1463
- return p === PLUGIN_NAME || p.startsWith(`${PLUGIN_NAME}@`);
1462
+ function isRecord(value) {
1463
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1464
+ }
1465
+ function isPluginTuple(value) {
1466
+ return Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && isRecord(value[1]);
1467
+ }
1468
+ function isPluginEntry(value) {
1469
+ return typeof value === "string" || isPluginTuple(value);
1470
+ }
1471
+ function getPluginName(entry) {
1472
+ return typeof entry === "string" ? entry : entry[0];
1473
+ }
1474
+ function isOurPluginEntry(entry) {
1475
+ if (!isPluginEntry(entry)) return false;
1476
+ const pluginName = getPluginName(entry);
1477
+ return pluginName === PLUGIN_NAME || pluginName.startsWith(`${PLUGIN_NAME}@`);
1464
1478
  }
1465
1479
  function getConfigFileCandidates(configDir) {
1466
1480
  return [join(configDir, "opencode.jsonc"), join(configDir, "opencode.json")];
@@ -1562,8 +1576,8 @@ function validateConfig(config) {
1562
1576
  return false;
1563
1577
  }
1564
1578
  if (config.plugin) {
1565
- for (const p of config.plugin) {
1566
- if (typeof p !== "string") {
1579
+ for (const entry of config.plugin) {
1580
+ if (!isPluginEntry(entry)) {
1567
1581
  return false;
1568
1582
  }
1569
1583
  }
@@ -1646,10 +1660,7 @@ function removeFromConfig(configDir) {
1646
1660
  }
1647
1661
  const originalLength = config.plugin.length;
1648
1662
  const originalPlugins = [...config.plugin];
1649
- config.plugin = config.plugin.filter((p) => {
1650
- if (typeof p !== "string") return true;
1651
- return !isOurPluginEntry(p);
1652
- });
1663
+ config.plugin = config.plugin.filter((entry) => !isOurPluginEntry(entry));
1653
1664
  if (config.plugin.length === originalLength) {
1654
1665
  log("Plugin not found in config", { configFile });
1655
1666
  return { success: false, backupFile };
@@ -1671,9 +1682,7 @@ function removeFromConfig(configDir) {
1671
1682
  throw new Error(`Verification parse failed: ${verifyParsed.parseError ?? "unknown parse error"}`);
1672
1683
  }
1673
1684
  const verifyConfig = verifyParsed.config;
1674
- const stillHasPlugin = verifyConfig.plugin?.some(
1675
- (p) => typeof p === "string" && isOurPluginEntry(p)
1676
- );
1685
+ const stillHasPlugin = verifyConfig.plugin?.some((entry) => isOurPluginEntry(entry));
1677
1686
  if (stillHasPlugin) {
1678
1687
  throw new Error("Verification failed: plugin still present after removal");
1679
1688
  }
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export declare const LIMITS: {
5
5
  /** Maximum mission loop iterations */
6
- readonly MAX_ITERATIONS: 10000;
6
+ readonly MAX_ITERATIONS: 1000000000;
7
7
  /** Default scan limit for file listing */
8
8
  readonly DEFAULT_SCAN_LIMIT: 20;
9
9
  /** Max message history to check for conclusion */
@@ -11,7 +11,7 @@ export declare const LOOP: {
11
11
  /** Window to consider abort as recent */
12
12
  readonly ABORT_WINDOW_MS: number;
13
13
  /** Maximum iterations for mission loop */
14
- readonly DEFAULT_MAX_ITERATIONS: 10000;
14
+ readonly DEFAULT_MAX_ITERATIONS: 1000000000;
15
15
  /** Rust tool timeout */
16
16
  readonly RUST_TOOL_TIMEOUT_MS: number;
17
17
  };
@@ -2,7 +2,7 @@
2
2
  * Mission Control Configuration
3
3
  */
4
4
  export declare const MISSION_CONTROL: {
5
- readonly DEFAULT_MAX_ITERATIONS: 10000;
5
+ readonly DEFAULT_MAX_ITERATIONS: 1000000000;
6
6
  readonly DEFAULT_COUNTDOWN_SECONDS: 3;
7
7
  readonly STATE_FILE: "loop-state.json";
8
8
  readonly STOP_COMMAND: "/stop";
@@ -11,7 +11,7 @@ export declare const MISSION_CONTROL: {
11
11
  };
12
12
  /** @deprecated Use MISSION_CONTROL instead */
13
13
  export declare const MISSION: {
14
- readonly DEFAULT_MAX_ITERATIONS: 10000;
14
+ readonly DEFAULT_MAX_ITERATIONS: 1000000000;
15
15
  readonly DEFAULT_COUNTDOWN_SECONDS: 3;
16
16
  readonly STATE_FILE: "loop-state.json";
17
17
  readonly STOP_COMMAND: "/stop";
@@ -30,7 +30,7 @@ export interface MissionLoopState {
30
30
  lastContinuationAt?: string;
31
31
  }
32
32
  export interface MissionLoopOptions {
33
- /** Maximum iterations before stopping (default: 1000) */
33
+ /** Maximum iterations before stopping (default: 1,000,000,000) */
34
34
  maxIterations?: number;
35
35
  /** Countdown seconds before auto-continue (default: 3) */
36
36
  countdownSeconds?: number;
@@ -10,6 +10,8 @@ export declare const EVENT_TYPES: {
10
10
  readonly CACHED: "document.cached";
11
11
  readonly EXPIRED: "document.expired";
12
12
  readonly IDLE: "session.idle";
13
+ readonly STATUS: "session.status";
14
+ readonly COMPACTED: "session.compacted";
13
15
  readonly BUSY: "session.busy";
14
16
  readonly ERROR: "session.error";
15
17
  readonly DELETED: "session.deleted";
@@ -3,6 +3,9 @@
3
3
  */
4
4
  export declare const SESSION_EVENTS: {
5
5
  readonly IDLE: "session.idle";
6
+ readonly STATUS: "session.status";
7
+ readonly UPDATED: "session.updated";
8
+ readonly COMPACTED: "session.compacted";
6
9
  readonly BUSY: "session.busy";
7
10
  readonly ERROR: "session.error";
8
11
  readonly DELETED: "session.deleted";
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "opencode-orchestrator",
3
3
  "displayName": "OpenCode Orchestrator",
4
4
  "description": "Distributed Cognitive Architecture for OpenCode. Turns simple prompts into specialized multi-agent workflows (Planner, Coder, Reviewer).",
5
- "version": "1.3.4",
5
+ "version": "1.3.5",
6
6
  "author": "agnusdei1207",
7
7
  "license": "MIT",
8
8
  "repository": {
@@ -59,9 +59,9 @@
59
59
  "sync:readme-version": "node scripts/sync-readme-version.mjs",
60
60
  "version": "node scripts/sync-readme-version.mjs --stage",
61
61
  "release:preflight": "node scripts/release-preflight.mjs",
62
- "release:patch": "node scripts/release-auth-check.mjs && node scripts/release-version.mjs patch && npm run release:preflight && npm run docker:rust-dist && npm run publish:token",
63
- "release:minor": "node scripts/release-auth-check.mjs && node scripts/release-version.mjs minor && npm run release:preflight && npm run docker:rust-dist && npm run publish:token",
64
- "release:major": "node scripts/release-auth-check.mjs && node scripts/release-version.mjs major && npm run release:preflight && npm run docker:rust-dist && npm run publish:token",
62
+ "release:patch": "node scripts/release-auth-check.mjs && node scripts/release-version.mjs patch && npm run release:preflight && npm run docker:rust-dist && node scripts/release-sync-artifacts.mjs && npm run publish:token",
63
+ "release:minor": "node scripts/release-auth-check.mjs && node scripts/release-version.mjs minor && npm run release:preflight && npm run docker:rust-dist && node scripts/release-sync-artifacts.mjs && npm run publish:token",
64
+ "release:major": "node scripts/release-auth-check.mjs && node scripts/release-version.mjs major && npm run release:preflight && npm run docker:rust-dist && node scripts/release-sync-artifacts.mjs && npm run publish:token",
65
65
  "release:dry-run": "node scripts/release-preflight.mjs --allow-dirty --skip-branch --skip-version-check",
66
66
  "release:push-tags": "git push origin main && git push origin --tags",
67
67
  "release:clean": "shx rm -rf dist bin && docker compose down -v",