opencode-orchestrator 0.7.1 β 0.8.1
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 +21 -4
- package/dist/core/loop/mission-seal-handler.d.ts +31 -0
- package/dist/core/loop/mission-seal.d.ts +92 -0
- package/dist/index.js +360 -17
- package/dist/shared/constants.d.ts +31 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -109,7 +109,7 @@ Restart OpenCode after installation.
|
|
|
109
109
|
|
|
110
110
|
| Mode | Trigger | Behavior |
|
|
111
111
|
|------|---------|----------|
|
|
112
|
-
| **Commander Mode** π― | `/task "mission"` | Full autonomous execution until
|
|
112
|
+
| **Commander Mode** π― | `/task "mission"` | Full autonomous execution until sealed |
|
|
113
113
|
| **Chat Mode** π¬ | Regular conversation | Simple Q&A, no autonomous behavior |
|
|
114
114
|
|
|
115
115
|
---
|
|
@@ -125,15 +125,32 @@ Use `/task` when you need the AI to **complete a mission autonomously**:
|
|
|
125
125
|
```
|
|
126
126
|
|
|
127
127
|
**What Commander Mode Does:**
|
|
128
|
-
- βΎοΈ **Runs until
|
|
128
|
+
- βΎοΈ **Runs until sealed** β Loops until agent outputs `<mission_seal>SEALED</mission_seal>`
|
|
129
129
|
- π§ **Anti-Hallucination** β Researches docs before coding
|
|
130
130
|
- β‘ **Parallel Execution** β Up to 50 concurrent agents
|
|
131
131
|
- π **Auto-Recovery** β Handles errors automatically
|
|
132
|
-
- π **
|
|
132
|
+
- π **Triage System** β Adapts strategy to complexity (L1/L2/L3)
|
|
133
|
+
|
|
134
|
+
**ποΈ Mission Seal Loop:**
|
|
135
|
+
```
|
|
136
|
+
/task "mission" β Agent works β Idle? β Seal found?
|
|
137
|
+
β β
|
|
138
|
+
β No β Yes
|
|
139
|
+
ββββββββββββββββ΄βββ β
Complete
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
When the agent finishes ALL work, it outputs:
|
|
143
|
+
```xml
|
|
144
|
+
<mission_seal>SEALED</mission_seal>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Control Commands:**
|
|
148
|
+
- `/stop` or `/cancel` β Stop the loop manually
|
|
149
|
+
- Max 20 iterations (configurable)
|
|
133
150
|
|
|
134
151
|
<div align="center">
|
|
135
152
|
<img src="assets/tui_image.png" alt="Commander TUI" width="600" />
|
|
136
|
-
<p><sub><b>/task "mission"</b> triggers full Commander mode</sub></p>
|
|
153
|
+
<p><sub><b>/task "mission"</b> triggers full Commander mode with Mission Seal loop</sub></p>
|
|
137
154
|
</div>
|
|
138
155
|
|
|
139
156
|
---
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission Seal Handler
|
|
3
|
+
*
|
|
4
|
+
* Integrates Mission Seal detection with session events.
|
|
5
|
+
* When session goes idle, checks for seal and either:
|
|
6
|
+
* - Completes loop if seal detected
|
|
7
|
+
* - Continues with next iteration if seal not detected
|
|
8
|
+
*/
|
|
9
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
10
|
+
type OpencodeClient = PluginInput["client"];
|
|
11
|
+
/**
|
|
12
|
+
* Handle session.idle event for mission seal loop
|
|
13
|
+
*/
|
|
14
|
+
export declare function handleMissionSealIdle(client: OpencodeClient, directory: string, sessionID: string, mainSessionID?: string): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Handle user message - cancel countdown
|
|
17
|
+
*/
|
|
18
|
+
export declare function handleUserMessage(sessionID: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Handle abort - prevent continuation
|
|
21
|
+
*/
|
|
22
|
+
export declare function handleAbort(sessionID: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Clean up session state
|
|
25
|
+
*/
|
|
26
|
+
export declare function cleanupSession(sessionID: string): void;
|
|
27
|
+
/**
|
|
28
|
+
* Check if there's a pending countdown
|
|
29
|
+
*/
|
|
30
|
+
export declare function hasPendingContinuation(sessionID: string): boolean;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission Seal - Explicit completion detection system
|
|
3
|
+
*
|
|
4
|
+
* When an agent outputs `<mission_seal>SEALED</mission_seal>`,
|
|
5
|
+
* the task loop knows the mission is truly complete.
|
|
6
|
+
*
|
|
7
|
+
* This prevents false-positive idle detection and ensures
|
|
8
|
+
* agents explicitly confirm task completion.
|
|
9
|
+
*/
|
|
10
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
11
|
+
/** Tag for mission seal detection */
|
|
12
|
+
export declare const MISSION_SEAL_TAG: "mission_seal";
|
|
13
|
+
/** Seal confirmation value */
|
|
14
|
+
export declare const SEAL_CONFIRMATION: "SEALED";
|
|
15
|
+
/** Full seal pattern: <mission_seal>SEALED</mission_seal> */
|
|
16
|
+
export declare const SEAL_PATTERN: "<mission_seal>SEALED</mission_seal>";
|
|
17
|
+
/** Regex for detecting seal in text */
|
|
18
|
+
export declare const SEAL_REGEX: RegExp;
|
|
19
|
+
export interface MissionLoopState {
|
|
20
|
+
/** Whether loop is active */
|
|
21
|
+
active: boolean;
|
|
22
|
+
/** Current iteration number */
|
|
23
|
+
iteration: number;
|
|
24
|
+
/** Maximum allowed iterations */
|
|
25
|
+
maxIterations: number;
|
|
26
|
+
/** Original task prompt */
|
|
27
|
+
prompt: string;
|
|
28
|
+
/** Session ID */
|
|
29
|
+
sessionID: string;
|
|
30
|
+
/** When loop started */
|
|
31
|
+
startedAt: string;
|
|
32
|
+
/** Last activity timestamp */
|
|
33
|
+
lastActivity?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface MissionLoopOptions {
|
|
36
|
+
/** Maximum iterations before stopping (default: 20) */
|
|
37
|
+
maxIterations?: number;
|
|
38
|
+
/** Countdown seconds before auto-continue (default: 3) */
|
|
39
|
+
countdownSeconds?: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Read loop state from disk
|
|
43
|
+
*/
|
|
44
|
+
export declare function readLoopState(directory: string): MissionLoopState | null;
|
|
45
|
+
/**
|
|
46
|
+
* Write loop state to disk
|
|
47
|
+
*/
|
|
48
|
+
export declare function writeLoopState(directory: string, state: MissionLoopState): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Clear loop state (delete file)
|
|
51
|
+
*/
|
|
52
|
+
export declare function clearLoopState(directory: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Increment iteration counter
|
|
55
|
+
*/
|
|
56
|
+
export declare function incrementIteration(directory: string): MissionLoopState | null;
|
|
57
|
+
/**
|
|
58
|
+
* Check if text contains mission seal
|
|
59
|
+
*/
|
|
60
|
+
export declare function detectSealInText(text: string): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Check session messages for mission seal
|
|
63
|
+
*/
|
|
64
|
+
export declare function detectSealInSession(client: PluginInput["client"], sessionID: string): Promise<boolean>;
|
|
65
|
+
/**
|
|
66
|
+
* Start a mission loop
|
|
67
|
+
*/
|
|
68
|
+
export declare function startMissionLoop(directory: string, sessionID: string, prompt: string, options?: MissionLoopOptions): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Cancel an active mission loop
|
|
71
|
+
*/
|
|
72
|
+
export declare function cancelMissionLoop(directory: string, sessionID: string): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Check if loop is active for session
|
|
75
|
+
*/
|
|
76
|
+
export declare function isLoopActive(directory: string, sessionID: string): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Get remaining iterations
|
|
79
|
+
*/
|
|
80
|
+
export declare function getRemainingIterations(directory: string): number;
|
|
81
|
+
/**
|
|
82
|
+
* Generate continuation prompt for mission loop
|
|
83
|
+
*/
|
|
84
|
+
export declare function generateMissionContinuationPrompt(state: MissionLoopState): string;
|
|
85
|
+
/**
|
|
86
|
+
* Generate completion notification
|
|
87
|
+
*/
|
|
88
|
+
export declare function generateSealedNotification(state: MissionLoopState): string;
|
|
89
|
+
/**
|
|
90
|
+
* Generate max iterations reached notification
|
|
91
|
+
*/
|
|
92
|
+
export declare function generateMaxIterationsNotification(state: MissionLoopState): string;
|
package/dist/index.js
CHANGED
|
@@ -116,16 +116,25 @@ var TOOL_NAMES = {
|
|
|
116
116
|
CALL_AGENT: "call_agent",
|
|
117
117
|
SLASHCOMMAND: "slashcommand"
|
|
118
118
|
};
|
|
119
|
-
var
|
|
120
|
-
/**
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
|
|
119
|
+
var MISSION_SEAL = {
|
|
120
|
+
/** XML tag name for mission seal */
|
|
121
|
+
TAG: "mission_seal",
|
|
122
|
+
/** Confirmation value inside tag */
|
|
123
|
+
CONFIRMATION: "SEALED",
|
|
124
|
+
/** Full seal pattern: <mission_seal>SEALED</mission_seal> */
|
|
125
|
+
PATTERN: "<mission_seal>SEALED</mission_seal>",
|
|
126
|
+
/** Default max loop iterations */
|
|
127
|
+
DEFAULT_MAX_ITERATIONS: 20,
|
|
128
|
+
/** Default countdown seconds before continue */
|
|
129
|
+
DEFAULT_COUNTDOWN_SECONDS: 3,
|
|
130
|
+
/** Loop state file name */
|
|
131
|
+
STATE_FILE: "loop-state.json",
|
|
124
132
|
/** Stop command */
|
|
125
133
|
STOP_COMMAND: "/stop",
|
|
126
134
|
/** Cancel command */
|
|
127
135
|
CANCEL_COMMAND: "/cancel"
|
|
128
136
|
};
|
|
137
|
+
var MISSION = MISSION_SEAL;
|
|
129
138
|
var AGENT_EMOJI = {
|
|
130
139
|
Commander: "\u{1F3AF}",
|
|
131
140
|
Planner: "\u{1F4CB}",
|
|
@@ -171,7 +180,7 @@ Complete missions efficiently using multiple agents simultaneously. Never stop u
|
|
|
171
180
|
<core_principles>
|
|
172
181
|
1. PARALLELISM FIRST: Always run independent tasks simultaneously
|
|
173
182
|
2. NEVER BLOCK: Use background execution for slow operations
|
|
174
|
-
3. NEVER STOP: Loop until "${
|
|
183
|
+
3. NEVER STOP: Loop until "${MISSION_SEAL.PATTERN}"
|
|
175
184
|
4. THINK FIRST: Reason before every action
|
|
176
185
|
5. SESSION REUSE: Resume sessions to preserve context
|
|
177
186
|
</core_principles>
|
|
@@ -354,9 +363,22 @@ OUTPUT ONLY WHEN:
|
|
|
354
363
|
2. Build/tests pass
|
|
355
364
|
3. ${AGENT_NAMES.REVIEWER} approves
|
|
356
365
|
|
|
357
|
-
|
|
366
|
+
**MISSION SEAL** (Explicit Completion Confirmation):
|
|
367
|
+
When ALL work is truly complete, output the seal tag:
|
|
368
|
+
\`\`\`
|
|
369
|
+
${MISSION_SEAL.PATTERN}
|
|
370
|
+
\`\`\`
|
|
371
|
+
|
|
372
|
+
Then output:
|
|
373
|
+
${MISSION_SEAL.PATTERN}
|
|
358
374
|
Summary: [accomplishments]
|
|
359
375
|
Evidence: [test/build results]
|
|
376
|
+
|
|
377
|
+
\u26A0\uFE0F IMPORTANT: Only output ${MISSION_SEAL.PATTERN} when:
|
|
378
|
+
- All todos are marked [x] complete
|
|
379
|
+
- All tests pass
|
|
380
|
+
- All builds succeed
|
|
381
|
+
- You have verified the final result
|
|
360
382
|
</completion>`,
|
|
361
383
|
canWrite: true,
|
|
362
384
|
canBash: true
|
|
@@ -13216,7 +13238,7 @@ $ARGUMENTS
|
|
|
13216
13238
|
<execution_rules>
|
|
13217
13239
|
1. Complete this mission without user intervention
|
|
13218
13240
|
2. Use your full capabilities: research, implement, verify
|
|
13219
|
-
3. Output "${
|
|
13241
|
+
3. Output "${MISSION_SEAL.PATTERN}" when done
|
|
13220
13242
|
</execution_rules>
|
|
13221
13243
|
</mission>`;
|
|
13222
13244
|
var COMMANDS = {
|
|
@@ -13254,7 +13276,7 @@ var COMMANDS = {
|
|
|
13254
13276
|
|
|
13255
13277
|
| Agent | Role | Capabilities |
|
|
13256
13278
|
|-------|------|--------------|
|
|
13257
|
-
| **${AGENT_NAMES.COMMANDER}** \u{1F3AF} | Master Orchestrator | Autonomous mission control, parallel task coordination, never stops until
|
|
13279
|
+
| **${AGENT_NAMES.COMMANDER}** \u{1F3AF} | Master Orchestrator | Autonomous mission control, parallel task coordination, never stops until ${MISSION_SEAL.PATTERN} |
|
|
13258
13280
|
| **${AGENT_NAMES.PLANNER}** \u{1F4CB} | Strategic Planner | Task decomposition, research, caching docs, dependency analysis |
|
|
13259
13281
|
| **${AGENT_NAMES.WORKER}** \u{1F528} | Implementation | Code, files, terminal, documentation lookup when needed |
|
|
13260
13282
|
| **${AGENT_NAMES.REVIEWER}** \u2705 | Quality & Context | Verification, TODO updates, context management, auto-fix |
|
|
@@ -16756,6 +16778,310 @@ function cleanupSession(sessionID) {
|
|
|
16756
16778
|
sessionStates.delete(sessionID);
|
|
16757
16779
|
}
|
|
16758
16780
|
|
|
16781
|
+
// src/core/loop/mission-seal.ts
|
|
16782
|
+
import { existsSync as existsSync4, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
16783
|
+
import { join as join5 } from "node:path";
|
|
16784
|
+
var MISSION_SEAL_TAG = MISSION_SEAL.TAG;
|
|
16785
|
+
var SEAL_CONFIRMATION = MISSION_SEAL.CONFIRMATION;
|
|
16786
|
+
var SEAL_PATTERN = MISSION_SEAL.PATTERN;
|
|
16787
|
+
var SEAL_REGEX = new RegExp(
|
|
16788
|
+
`<${MISSION_SEAL.TAG}>\\s*${MISSION_SEAL.CONFIRMATION}\\s*</${MISSION_SEAL.TAG}>`,
|
|
16789
|
+
"i"
|
|
16790
|
+
);
|
|
16791
|
+
var STATE_FILE = MISSION_SEAL.STATE_FILE;
|
|
16792
|
+
var DEFAULT_MAX_ITERATIONS = MISSION_SEAL.DEFAULT_MAX_ITERATIONS;
|
|
16793
|
+
var DEFAULT_COUNTDOWN_SECONDS = MISSION_SEAL.DEFAULT_COUNTDOWN_SECONDS;
|
|
16794
|
+
function getStateFilePath(directory) {
|
|
16795
|
+
return join5(directory, PATHS.OPENCODE, STATE_FILE);
|
|
16796
|
+
}
|
|
16797
|
+
function readLoopState(directory) {
|
|
16798
|
+
const filePath = getStateFilePath(directory);
|
|
16799
|
+
if (!existsSync4(filePath)) {
|
|
16800
|
+
return null;
|
|
16801
|
+
}
|
|
16802
|
+
try {
|
|
16803
|
+
const content = readFileSync(filePath, "utf-8");
|
|
16804
|
+
return JSON.parse(content);
|
|
16805
|
+
} catch (error45) {
|
|
16806
|
+
log2(`[mission-seal] Failed to read state: ${error45}`);
|
|
16807
|
+
return null;
|
|
16808
|
+
}
|
|
16809
|
+
}
|
|
16810
|
+
function writeLoopState(directory, state2) {
|
|
16811
|
+
const filePath = getStateFilePath(directory);
|
|
16812
|
+
try {
|
|
16813
|
+
writeFileSync(filePath, JSON.stringify(state2, null, 2), "utf-8");
|
|
16814
|
+
return true;
|
|
16815
|
+
} catch (error45) {
|
|
16816
|
+
log2(`[mission-seal] Failed to write state: ${error45}`);
|
|
16817
|
+
return false;
|
|
16818
|
+
}
|
|
16819
|
+
}
|
|
16820
|
+
function clearLoopState(directory) {
|
|
16821
|
+
const filePath = getStateFilePath(directory);
|
|
16822
|
+
if (!existsSync4(filePath)) {
|
|
16823
|
+
return true;
|
|
16824
|
+
}
|
|
16825
|
+
try {
|
|
16826
|
+
unlinkSync(filePath);
|
|
16827
|
+
return true;
|
|
16828
|
+
} catch (error45) {
|
|
16829
|
+
log2(`[mission-seal] Failed to clear state: ${error45}`);
|
|
16830
|
+
return false;
|
|
16831
|
+
}
|
|
16832
|
+
}
|
|
16833
|
+
function incrementIteration(directory) {
|
|
16834
|
+
const state2 = readLoopState(directory);
|
|
16835
|
+
if (!state2) return null;
|
|
16836
|
+
state2.iteration += 1;
|
|
16837
|
+
state2.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
16838
|
+
if (writeLoopState(directory, state2)) {
|
|
16839
|
+
return state2;
|
|
16840
|
+
}
|
|
16841
|
+
return null;
|
|
16842
|
+
}
|
|
16843
|
+
function detectSealInText(text) {
|
|
16844
|
+
return SEAL_REGEX.test(text);
|
|
16845
|
+
}
|
|
16846
|
+
async function detectSealInSession(client, sessionID) {
|
|
16847
|
+
try {
|
|
16848
|
+
const response = await client.session.messages({ path: { id: sessionID } });
|
|
16849
|
+
const messages = response.data ?? [];
|
|
16850
|
+
const assistantMessages = messages.filter((m) => m.info?.role === "assistant");
|
|
16851
|
+
const recentMessages = assistantMessages.slice(-3);
|
|
16852
|
+
for (const msg of recentMessages) {
|
|
16853
|
+
if (!msg.parts) continue;
|
|
16854
|
+
const textParts = msg.parts.filter(
|
|
16855
|
+
(p) => p.type === PART_TYPES.TEXT || p.type === PART_TYPES.REASONING
|
|
16856
|
+
);
|
|
16857
|
+
for (const part of textParts) {
|
|
16858
|
+
if (part.text && detectSealInText(part.text)) {
|
|
16859
|
+
return true;
|
|
16860
|
+
}
|
|
16861
|
+
}
|
|
16862
|
+
}
|
|
16863
|
+
return false;
|
|
16864
|
+
} catch (error45) {
|
|
16865
|
+
log2(`[mission-seal] Failed to check session messages: ${error45}`);
|
|
16866
|
+
return false;
|
|
16867
|
+
}
|
|
16868
|
+
}
|
|
16869
|
+
function isLoopActive(directory, sessionID) {
|
|
16870
|
+
const state2 = readLoopState(directory);
|
|
16871
|
+
return state2?.active === true && state2?.sessionID === sessionID;
|
|
16872
|
+
}
|
|
16873
|
+
function generateMissionContinuationPrompt(state2) {
|
|
16874
|
+
return `<mission_loop iteration="${state2.iteration}" max="${state2.maxIterations}">
|
|
16875
|
+
\u{1F4CB} **Mission Loop Active** - Iteration ${state2.iteration}/${state2.maxIterations}
|
|
16876
|
+
|
|
16877
|
+
Your previous iteration did not seal the mission. Continue working.
|
|
16878
|
+
|
|
16879
|
+
**RULES**:
|
|
16880
|
+
1. Review your progress from the previous iteration
|
|
16881
|
+
2. Continue from where you left off
|
|
16882
|
+
3. Check TODO list for incomplete items
|
|
16883
|
+
4. When ALL work is TRULY complete, output:
|
|
16884
|
+
|
|
16885
|
+
\`\`\`
|
|
16886
|
+
${SEAL_PATTERN}
|
|
16887
|
+
\`\`\`
|
|
16888
|
+
|
|
16889
|
+
**IMPORTANT**:
|
|
16890
|
+
- Do NOT seal until the mission is genuinely complete
|
|
16891
|
+
- Verify all todos are marked [x] before sealing
|
|
16892
|
+
- Run tests/builds if applicable before sealing
|
|
16893
|
+
|
|
16894
|
+
**Original Task**:
|
|
16895
|
+
${state2.prompt}
|
|
16896
|
+
</mission_loop>`;
|
|
16897
|
+
}
|
|
16898
|
+
|
|
16899
|
+
// src/core/loop/mission-seal-handler.ts
|
|
16900
|
+
var COUNTDOWN_SECONDS2 = 3;
|
|
16901
|
+
var TOAST_DURATION_MS2 = 1500;
|
|
16902
|
+
var MIN_TIME_BETWEEN_CHECKS_MS = 3e3;
|
|
16903
|
+
var sessionStates2 = /* @__PURE__ */ new Map();
|
|
16904
|
+
function getState3(sessionID) {
|
|
16905
|
+
let state2 = sessionStates2.get(sessionID);
|
|
16906
|
+
if (!state2) {
|
|
16907
|
+
state2 = {};
|
|
16908
|
+
sessionStates2.set(sessionID, state2);
|
|
16909
|
+
}
|
|
16910
|
+
return state2;
|
|
16911
|
+
}
|
|
16912
|
+
function cancelCountdown2(sessionID) {
|
|
16913
|
+
const state2 = sessionStates2.get(sessionID);
|
|
16914
|
+
if (state2?.countdownTimer) {
|
|
16915
|
+
clearTimeout(state2.countdownTimer);
|
|
16916
|
+
state2.countdownTimer = void 0;
|
|
16917
|
+
}
|
|
16918
|
+
}
|
|
16919
|
+
function hasRunningBackgroundTasks2(parentSessionID) {
|
|
16920
|
+
try {
|
|
16921
|
+
const manager = ParallelAgentManager.getInstance();
|
|
16922
|
+
const tasks = manager.getTasksByParent(parentSessionID);
|
|
16923
|
+
return tasks.some((t) => t.status === "running");
|
|
16924
|
+
} catch {
|
|
16925
|
+
return false;
|
|
16926
|
+
}
|
|
16927
|
+
}
|
|
16928
|
+
async function showCountdownToast2(client, seconds, iteration, maxIterations) {
|
|
16929
|
+
try {
|
|
16930
|
+
const tuiClient2 = client;
|
|
16931
|
+
if (tuiClient2.tui?.showToast) {
|
|
16932
|
+
await tuiClient2.tui.showToast({
|
|
16933
|
+
body: {
|
|
16934
|
+
title: "\u{1F504} Mission Loop",
|
|
16935
|
+
message: `Continuing in ${seconds}s... (iteration ${iteration}/${maxIterations})`,
|
|
16936
|
+
variant: "warning",
|
|
16937
|
+
duration: TOAST_DURATION_MS2
|
|
16938
|
+
}
|
|
16939
|
+
});
|
|
16940
|
+
}
|
|
16941
|
+
} catch {
|
|
16942
|
+
}
|
|
16943
|
+
}
|
|
16944
|
+
async function showSealedToast(client, state2) {
|
|
16945
|
+
try {
|
|
16946
|
+
const tuiClient2 = client;
|
|
16947
|
+
if (tuiClient2.tui?.showToast) {
|
|
16948
|
+
await tuiClient2.tui.showToast({
|
|
16949
|
+
body: {
|
|
16950
|
+
title: "\u{1F396}\uFE0F Mission Sealed!",
|
|
16951
|
+
message: `Completed after ${state2.iteration} iteration(s)`,
|
|
16952
|
+
variant: "success",
|
|
16953
|
+
duration: 5e3
|
|
16954
|
+
}
|
|
16955
|
+
});
|
|
16956
|
+
}
|
|
16957
|
+
} catch {
|
|
16958
|
+
}
|
|
16959
|
+
}
|
|
16960
|
+
async function showMaxIterationsToast(client, state2) {
|
|
16961
|
+
try {
|
|
16962
|
+
const tuiClient2 = client;
|
|
16963
|
+
if (tuiClient2.tui?.showToast) {
|
|
16964
|
+
await tuiClient2.tui.showToast({
|
|
16965
|
+
body: {
|
|
16966
|
+
title: "\u26A0\uFE0F Mission Loop Stopped",
|
|
16967
|
+
message: `Max iterations (${state2.maxIterations}) reached`,
|
|
16968
|
+
variant: "warning",
|
|
16969
|
+
duration: 5e3
|
|
16970
|
+
}
|
|
16971
|
+
});
|
|
16972
|
+
}
|
|
16973
|
+
} catch {
|
|
16974
|
+
}
|
|
16975
|
+
}
|
|
16976
|
+
async function injectContinuation2(client, directory, sessionID, loopState) {
|
|
16977
|
+
const handlerState = getState3(sessionID);
|
|
16978
|
+
if (handlerState.isAborting) {
|
|
16979
|
+
log2("[mission-seal-handler] Skipped: user is aborting");
|
|
16980
|
+
return;
|
|
16981
|
+
}
|
|
16982
|
+
if (hasRunningBackgroundTasks2(sessionID)) {
|
|
16983
|
+
log2("[mission-seal-handler] Skipped: background tasks running");
|
|
16984
|
+
return;
|
|
16985
|
+
}
|
|
16986
|
+
if (isSessionRecovering(sessionID)) {
|
|
16987
|
+
log2("[mission-seal-handler] Skipped: session recovering");
|
|
16988
|
+
return;
|
|
16989
|
+
}
|
|
16990
|
+
const sealDetected = await detectSealInSession(client, sessionID);
|
|
16991
|
+
if (sealDetected) {
|
|
16992
|
+
log2("[mission-seal-handler] Seal detected before injection, completing");
|
|
16993
|
+
await handleSealDetected(client, directory, loopState);
|
|
16994
|
+
return;
|
|
16995
|
+
}
|
|
16996
|
+
const prompt = generateMissionContinuationPrompt(loopState);
|
|
16997
|
+
try {
|
|
16998
|
+
await client.session.prompt({
|
|
16999
|
+
path: { id: sessionID },
|
|
17000
|
+
body: {
|
|
17001
|
+
parts: [{ type: PART_TYPES.TEXT, text: prompt }]
|
|
17002
|
+
}
|
|
17003
|
+
});
|
|
17004
|
+
log2("[mission-seal-handler] Continuation injected", {
|
|
17005
|
+
sessionID,
|
|
17006
|
+
iteration: loopState.iteration
|
|
17007
|
+
});
|
|
17008
|
+
} catch (error45) {
|
|
17009
|
+
log2(`[mission-seal-handler] Failed to inject: ${error45}`);
|
|
17010
|
+
}
|
|
17011
|
+
}
|
|
17012
|
+
async function handleSealDetected(client, directory, loopState) {
|
|
17013
|
+
clearLoopState(directory);
|
|
17014
|
+
await showSealedToast(client, loopState);
|
|
17015
|
+
log2("[mission-seal-handler] Mission sealed!", {
|
|
17016
|
+
sessionID: loopState.sessionID,
|
|
17017
|
+
iterations: loopState.iteration
|
|
17018
|
+
});
|
|
17019
|
+
}
|
|
17020
|
+
async function handleMaxIterations(client, directory, loopState) {
|
|
17021
|
+
clearLoopState(directory);
|
|
17022
|
+
await showMaxIterationsToast(client, loopState);
|
|
17023
|
+
log2("[mission-seal-handler] Max iterations reached", {
|
|
17024
|
+
sessionID: loopState.sessionID,
|
|
17025
|
+
iterations: loopState.iteration,
|
|
17026
|
+
max: loopState.maxIterations
|
|
17027
|
+
});
|
|
17028
|
+
}
|
|
17029
|
+
async function handleMissionSealIdle(client, directory, sessionID, mainSessionID) {
|
|
17030
|
+
const handlerState = getState3(sessionID);
|
|
17031
|
+
const now = Date.now();
|
|
17032
|
+
if (handlerState.lastCheckTime && now - handlerState.lastCheckTime < MIN_TIME_BETWEEN_CHECKS_MS) {
|
|
17033
|
+
return;
|
|
17034
|
+
}
|
|
17035
|
+
handlerState.lastCheckTime = now;
|
|
17036
|
+
cancelCountdown2(sessionID);
|
|
17037
|
+
if (mainSessionID && sessionID !== mainSessionID) {
|
|
17038
|
+
return;
|
|
17039
|
+
}
|
|
17040
|
+
if (isSessionRecovering(sessionID)) {
|
|
17041
|
+
log2("[mission-seal-handler] Skipped: recovering");
|
|
17042
|
+
return;
|
|
17043
|
+
}
|
|
17044
|
+
if (hasRunningBackgroundTasks2(sessionID)) {
|
|
17045
|
+
log2("[mission-seal-handler] Skipped: background tasks");
|
|
17046
|
+
return;
|
|
17047
|
+
}
|
|
17048
|
+
const loopState = readLoopState(directory);
|
|
17049
|
+
if (!loopState || !loopState.active) {
|
|
17050
|
+
return;
|
|
17051
|
+
}
|
|
17052
|
+
if (loopState.sessionID !== sessionID) {
|
|
17053
|
+
return;
|
|
17054
|
+
}
|
|
17055
|
+
log2("[mission-seal-handler] Checking for seal", {
|
|
17056
|
+
sessionID,
|
|
17057
|
+
iteration: loopState.iteration
|
|
17058
|
+
});
|
|
17059
|
+
const sealDetected = await detectSealInSession(client, sessionID);
|
|
17060
|
+
if (sealDetected) {
|
|
17061
|
+
await handleSealDetected(client, directory, loopState);
|
|
17062
|
+
return;
|
|
17063
|
+
}
|
|
17064
|
+
if (loopState.iteration >= loopState.maxIterations) {
|
|
17065
|
+
await handleMaxIterations(client, directory, loopState);
|
|
17066
|
+
return;
|
|
17067
|
+
}
|
|
17068
|
+
const newState = incrementIteration(directory);
|
|
17069
|
+
if (!newState) {
|
|
17070
|
+
log2("[mission-seal-handler] Failed to increment iteration");
|
|
17071
|
+
return;
|
|
17072
|
+
}
|
|
17073
|
+
await showCountdownToast2(client, COUNTDOWN_SECONDS2, newState.iteration, newState.maxIterations);
|
|
17074
|
+
handlerState.countdownTimer = setTimeout(async () => {
|
|
17075
|
+
cancelCountdown2(sessionID);
|
|
17076
|
+
await injectContinuation2(client, directory, sessionID, newState);
|
|
17077
|
+
}, COUNTDOWN_SECONDS2 * 1e3);
|
|
17078
|
+
log2("[mission-seal-handler] Countdown started", {
|
|
17079
|
+
sessionID,
|
|
17080
|
+
iteration: newState.iteration,
|
|
17081
|
+
seconds: COUNTDOWN_SECONDS2
|
|
17082
|
+
});
|
|
17083
|
+
}
|
|
17084
|
+
|
|
16759
17085
|
// src/core/progress/store.ts
|
|
16760
17086
|
var progressHistory = /* @__PURE__ */ new Map();
|
|
16761
17087
|
var sessionStartTimes = /* @__PURE__ */ new Map();
|
|
@@ -16868,7 +17194,7 @@ You are ONLY done when:
|
|
|
16868
17194
|
- All todos are marked complete or cancelled
|
|
16869
17195
|
- All features are implemented and tested
|
|
16870
17196
|
- Final verification passes
|
|
16871
|
-
Then output:
|
|
17197
|
+
Then output: ${MISSION_SEAL.PATTERN}
|
|
16872
17198
|
</completion_criteria>
|
|
16873
17199
|
</auto_continue>`;
|
|
16874
17200
|
var OrchestratorPlugin = async (input) => {
|
|
@@ -17046,12 +17372,27 @@ var OrchestratorPlugin = async (input) => {
|
|
|
17046
17372
|
if (sessionID) {
|
|
17047
17373
|
const isMainSession = sessions.has(sessionID);
|
|
17048
17374
|
if (isMainSession) {
|
|
17049
|
-
setTimeout(() => {
|
|
17375
|
+
setTimeout(async () => {
|
|
17050
17376
|
const session = sessions.get(sessionID);
|
|
17051
17377
|
if (session?.active) {
|
|
17052
|
-
|
|
17053
|
-
|
|
17054
|
-
|
|
17378
|
+
if (isLoopActive(directory, sessionID)) {
|
|
17379
|
+
await handleMissionSealIdle(
|
|
17380
|
+
client,
|
|
17381
|
+
directory,
|
|
17382
|
+
sessionID,
|
|
17383
|
+
sessionID
|
|
17384
|
+
).catch((err) => {
|
|
17385
|
+
log2("[index.ts] mission-seal-handler error", err);
|
|
17386
|
+
});
|
|
17387
|
+
} else {
|
|
17388
|
+
await handleSessionIdle(
|
|
17389
|
+
client,
|
|
17390
|
+
sessionID,
|
|
17391
|
+
sessionID
|
|
17392
|
+
).catch((err) => {
|
|
17393
|
+
log2("[index.ts] todo-continuation error", err);
|
|
17394
|
+
});
|
|
17395
|
+
}
|
|
17055
17396
|
}
|
|
17056
17397
|
}, 500);
|
|
17057
17398
|
}
|
|
@@ -17210,7 +17551,7 @@ Anomaly count: ${stateSession.anomalyCount}
|
|
|
17210
17551
|
// -----------------------------------------------------------------
|
|
17211
17552
|
// assistant.done hook - runs when the LLM finishes responding
|
|
17212
17553
|
// This is the heart of the "relentless loop" - we keep pushing it
|
|
17213
|
-
// to continue until we see
|
|
17554
|
+
// to continue until we see <mission_seal>SEALED</mission_seal> or hit the limit
|
|
17214
17555
|
// -----------------------------------------------------------------
|
|
17215
17556
|
"assistant.done": async (assistantInput, assistantOutput) => {
|
|
17216
17557
|
const sessionID = assistantInput.sessionID;
|
|
@@ -17250,10 +17591,12 @@ Anomaly count: ${stateSession.anomalyCount}
|
|
|
17250
17591
|
if (stateSession && stateSession.anomalyCount > 0) {
|
|
17251
17592
|
stateSession.anomalyCount = 0;
|
|
17252
17593
|
}
|
|
17253
|
-
if (
|
|
17594
|
+
if (detectSealInText(textContent)) {
|
|
17254
17595
|
session.active = false;
|
|
17255
17596
|
state.missionActive = false;
|
|
17256
|
-
|
|
17597
|
+
clearLoopState(directory);
|
|
17598
|
+
presets.missionComplete("\u{1F396}\uFE0F Mission Sealed - Explicit completion confirmed");
|
|
17599
|
+
log2("[index.ts] Mission sealed detected", { sessionID });
|
|
17257
17600
|
clearSession(sessionID);
|
|
17258
17601
|
sessions.delete(sessionID);
|
|
17259
17602
|
state.sessions.delete(sessionID);
|
|
@@ -69,11 +69,38 @@ export declare const TOOL_NAMES: {
|
|
|
69
69
|
readonly CALL_AGENT: "call_agent";
|
|
70
70
|
readonly SLASHCOMMAND: "slashcommand";
|
|
71
71
|
};
|
|
72
|
+
export declare const MISSION_SEAL: {
|
|
73
|
+
/** XML tag name for mission seal */
|
|
74
|
+
readonly TAG: "mission_seal";
|
|
75
|
+
/** Confirmation value inside tag */
|
|
76
|
+
readonly CONFIRMATION: "SEALED";
|
|
77
|
+
/** Full seal pattern: <mission_seal>SEALED</mission_seal> */
|
|
78
|
+
readonly PATTERN: "<mission_seal>SEALED</mission_seal>";
|
|
79
|
+
/** Default max loop iterations */
|
|
80
|
+
readonly DEFAULT_MAX_ITERATIONS: 20;
|
|
81
|
+
/** Default countdown seconds before continue */
|
|
82
|
+
readonly DEFAULT_COUNTDOWN_SECONDS: 3;
|
|
83
|
+
/** Loop state file name */
|
|
84
|
+
readonly STATE_FILE: "loop-state.json";
|
|
85
|
+
/** Stop command */
|
|
86
|
+
readonly STOP_COMMAND: "/stop";
|
|
87
|
+
/** Cancel command */
|
|
88
|
+
readonly CANCEL_COMMAND: "/cancel";
|
|
89
|
+
};
|
|
90
|
+
/** @deprecated Use MISSION_SEAL instead */
|
|
72
91
|
export declare const MISSION: {
|
|
73
|
-
/**
|
|
74
|
-
readonly
|
|
75
|
-
/**
|
|
76
|
-
readonly
|
|
92
|
+
/** XML tag name for mission seal */
|
|
93
|
+
readonly TAG: "mission_seal";
|
|
94
|
+
/** Confirmation value inside tag */
|
|
95
|
+
readonly CONFIRMATION: "SEALED";
|
|
96
|
+
/** Full seal pattern: <mission_seal>SEALED</mission_seal> */
|
|
97
|
+
readonly PATTERN: "<mission_seal>SEALED</mission_seal>";
|
|
98
|
+
/** Default max loop iterations */
|
|
99
|
+
readonly DEFAULT_MAX_ITERATIONS: 20;
|
|
100
|
+
/** Default countdown seconds before continue */
|
|
101
|
+
readonly DEFAULT_COUNTDOWN_SECONDS: 3;
|
|
102
|
+
/** Loop state file name */
|
|
103
|
+
readonly STATE_FILE: "loop-state.json";
|
|
77
104
|
/** Stop command */
|
|
78
105
|
readonly STOP_COMMAND: "/stop";
|
|
79
106
|
/** Cancel command */
|
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": "0.
|
|
5
|
+
"version": "0.8.1",
|
|
6
6
|
"author": "agnusdei1207",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|