opencode-orchestrator 1.0.37 → 1.0.40
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 +2 -1
- package/dist/core/orchestrator/session-manager.d.ts +37 -0
- package/dist/hooks/custom/resource-control.d.ts +0 -1
- package/dist/hooks/custom/strict-role-guard.d.ts +4 -1
- package/dist/hooks/features/mission-loop.d.ts +2 -0
- package/dist/index.js +235 -178
- package/dist/shared/constants/security-patterns.d.ts +13 -0
- package/dist/shared/constants/system-messages.d.ts +23 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ npm install -g opencode-orchestrator
|
|
|
19
19
|
In an OpenCode environment:
|
|
20
20
|
```bash
|
|
21
21
|
/task "Implement"
|
|
22
|
-
```
|
|
22
|
+
```
|
|
23
23
|
|
|
24
24
|
## Overview
|
|
25
25
|
|
|
@@ -67,6 +67,7 @@ OpenCode Orchestrator manages complex software tasks through **parallel multi-ag
|
|
|
67
67
|
[MISSION SEALED]
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
|
|
70
71
|
---
|
|
71
72
|
|
|
72
73
|
## 🚀 Agents
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager
|
|
3
|
+
*
|
|
4
|
+
* Centralizes all direct access to the global `state` and session initialization logic.
|
|
5
|
+
* Eliminates redundant state checks across hooks and handlers.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Ensures a local session object exists in the plugin context map.
|
|
9
|
+
* This is "Session State 1" (Plugin-local tracking).
|
|
10
|
+
*/
|
|
11
|
+
export declare function ensureSessionInitialized(sessions: Map<string, any>, sessionID: string): any;
|
|
12
|
+
/**
|
|
13
|
+
* Activates the global mission state for a specific session.
|
|
14
|
+
* This is "Session State 2" (Global Orchestrator State).
|
|
15
|
+
*/
|
|
16
|
+
export declare function activateMissionState(sessionID: string): void;
|
|
17
|
+
/**
|
|
18
|
+
* Checks if the mission is globally active and the session is enabled.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isMissionActive(sessionID: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Deactivates mission state (e.g., on cancellation or error).
|
|
23
|
+
*/
|
|
24
|
+
export declare function deactivateMissionState(sessionID: string): void;
|
|
25
|
+
/**
|
|
26
|
+
* Updates token usage and estimated cost for a session.
|
|
27
|
+
*/
|
|
28
|
+
export declare function updateSessionTokens(sessions: Map<string, any>, sessionID: string, inputLen: number, outputLen: number): void;
|
|
29
|
+
/**
|
|
30
|
+
* Anomaly Management
|
|
31
|
+
*/
|
|
32
|
+
export declare function recordAnomaly(sessionID: string): number;
|
|
33
|
+
export declare function resetAnomaly(sessionID: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* Task Tracking
|
|
36
|
+
*/
|
|
37
|
+
export declare function updateCurrentTask(sessionID: string, taskID: string): void;
|
|
@@ -10,5 +10,4 @@ export declare class ResourceControlHook implements PostToolUseHook, AssistantDo
|
|
|
10
10
|
private lastCompactionTime;
|
|
11
11
|
execute(ctx: HookContext, toolOrText: string, input?: any, output?: any): Promise<any>;
|
|
12
12
|
private checkMemoryHealth;
|
|
13
|
-
private generateCompactionPrompt;
|
|
14
13
|
}
|
|
@@ -10,7 +10,10 @@ export declare class StrictRoleGuardHook implements PreToolUseHook {
|
|
|
10
10
|
name: "StrictRoleGuard";
|
|
11
11
|
execute(ctx: HookContext, tool: string, args: any): Promise<{
|
|
12
12
|
action: "block";
|
|
13
|
-
reason:
|
|
13
|
+
reason: "Fork bomb detected.";
|
|
14
|
+
} | {
|
|
15
|
+
action: "block";
|
|
16
|
+
reason: "Root deletion blocked.";
|
|
14
17
|
} | {
|
|
15
18
|
action: "allow";
|
|
16
19
|
reason?: undefined;
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* - Mission seal detection (Stop)
|
|
6
6
|
* - Auto-continuation injection (Loop)
|
|
7
7
|
* - User cancellation detection
|
|
8
|
+
*
|
|
9
|
+
* Refactored to use SessionManager and SystemMessages for better maintainability.
|
|
8
10
|
*/
|
|
9
11
|
import type { AssistantDoneHook, ChatMessageHook, HookContext } from "../types.js";
|
|
10
12
|
export declare class MissionControlHook implements AssistantDoneHook, ChatMessageHook {
|
package/dist/index.js
CHANGED
|
@@ -19714,55 +19714,75 @@ function checkOutputSanity(text) {
|
|
|
19714
19714
|
return { isHealthy: true, severity: SEVERITY.OK };
|
|
19715
19715
|
}
|
|
19716
19716
|
|
|
19717
|
-
// src/
|
|
19718
|
-
|
|
19719
|
-
|
|
19720
|
-
|
|
19721
|
-
|
|
19722
|
-
|
|
19723
|
-
|
|
19724
|
-
|
|
19725
|
-
|
|
19726
|
-
|
|
19727
|
-
|
|
19717
|
+
// src/core/orchestrator/session-manager.ts
|
|
19718
|
+
function ensureSessionInitialized(sessions, sessionID) {
|
|
19719
|
+
if (!sessions.has(sessionID)) {
|
|
19720
|
+
const now = Date.now();
|
|
19721
|
+
const newSession = {
|
|
19722
|
+
active: true,
|
|
19723
|
+
step: 0,
|
|
19724
|
+
timestamp: now,
|
|
19725
|
+
startTime: now,
|
|
19726
|
+
lastStepTime: now,
|
|
19727
|
+
tokens: { totalInput: 0, totalOutput: 0, estimatedCost: 0 }
|
|
19728
|
+
};
|
|
19729
|
+
sessions.set(sessionID, newSession);
|
|
19730
|
+
}
|
|
19731
|
+
return sessions.get(sessionID);
|
|
19732
|
+
}
|
|
19733
|
+
function activateMissionState(sessionID) {
|
|
19734
|
+
let stateSession = state.sessions.get(sessionID);
|
|
19735
|
+
if (!stateSession) {
|
|
19736
|
+
state.sessions.set(sessionID, {
|
|
19737
|
+
enabled: true,
|
|
19738
|
+
iterations: 0,
|
|
19739
|
+
taskRetries: /* @__PURE__ */ new Map(),
|
|
19740
|
+
currentTask: "",
|
|
19741
|
+
anomalyCount: 0
|
|
19742
|
+
});
|
|
19743
|
+
stateSession = state.sessions.get(sessionID);
|
|
19744
|
+
log(`[SessionManager] Created new global mission state for ${sessionID}`);
|
|
19728
19745
|
}
|
|
19729
|
-
|
|
19730
|
-
|
|
19731
|
-
|
|
19732
|
-
const sanityResult = checkOutputSanity(toolOutput.output);
|
|
19733
|
-
if (!sanityResult.isHealthy) {
|
|
19734
|
-
stateSession.anomalyCount = (stateSession.anomalyCount || 0) + 1;
|
|
19735
|
-
const agentName = toolInput?.agent || "unknown";
|
|
19736
|
-
const errorMsg = `[${agentName.toUpperCase()}] OUTPUT ANOMALY DETECTED
|
|
19737
|
-
|
|
19738
|
-
Gibberish/loop detected: ${sanityResult.reason}
|
|
19739
|
-
Anomaly count: ${stateSession.anomalyCount}
|
|
19740
|
-
|
|
19741
|
-
` + (stateSession.anomalyCount >= 2 ? ESCALATION_PROMPT : RECOVERY_PROMPT);
|
|
19742
|
-
return { output: errorMsg };
|
|
19743
|
-
} else {
|
|
19744
|
-
if (stateSession.anomalyCount > 0) stateSession.anomalyCount = 0;
|
|
19745
|
-
}
|
|
19746
|
-
return {};
|
|
19746
|
+
if (stateSession) {
|
|
19747
|
+
stateSession.enabled = true;
|
|
19748
|
+
stateSession.anomalyCount = 0;
|
|
19747
19749
|
}
|
|
19748
|
-
|
|
19749
|
-
|
|
19750
|
-
|
|
19751
|
-
|
|
19752
|
-
|
|
19753
|
-
|
|
19754
|
-
|
|
19755
|
-
|
|
19756
|
-
|
|
19757
|
-
|
|
19758
|
-
|
|
19759
|
-
|
|
19760
|
-
|
|
19761
|
-
}
|
|
19762
|
-
if (stateSession.anomalyCount > 0) stateSession.anomalyCount = 0;
|
|
19763
|
-
return { action: HOOK_ACTIONS.CONTINUE };
|
|
19750
|
+
state.missionActive = true;
|
|
19751
|
+
log(`[SessionManager] Mission Activated: ${sessionID}`);
|
|
19752
|
+
}
|
|
19753
|
+
function isMissionActive(sessionID) {
|
|
19754
|
+
const stateSession = state.sessions.get(sessionID);
|
|
19755
|
+
return !!(state.missionActive && stateSession?.enabled);
|
|
19756
|
+
}
|
|
19757
|
+
var COST_PER_1K_INPUT = 3e-3;
|
|
19758
|
+
var COST_PER_1K_OUTPUT = 0.015;
|
|
19759
|
+
function updateSessionTokens(sessions, sessionID, inputLen, outputLen) {
|
|
19760
|
+
const session = ensureSessionInitialized(sessions, sessionID);
|
|
19761
|
+
if (!session.tokens) {
|
|
19762
|
+
session.tokens = { totalInput: 0, totalOutput: 0, estimatedCost: 0 };
|
|
19764
19763
|
}
|
|
19765
|
-
|
|
19764
|
+
const inputTokens = Math.ceil(inputLen / 4);
|
|
19765
|
+
const outputTokens = Math.ceil(outputLen / 4);
|
|
19766
|
+
session.tokens.totalInput += inputTokens;
|
|
19767
|
+
session.tokens.totalOutput += outputTokens;
|
|
19768
|
+
const cost = session.tokens.totalInput / 1e3 * COST_PER_1K_INPUT + session.tokens.totalOutput / 1e3 * COST_PER_1K_OUTPUT;
|
|
19769
|
+
session.tokens.estimatedCost = Number(cost.toFixed(4));
|
|
19770
|
+
}
|
|
19771
|
+
function recordAnomaly(sessionID) {
|
|
19772
|
+
const session = ensureSessionInitialized(state.sessions, sessionID);
|
|
19773
|
+
session.anomalyCount = (session.anomalyCount || 0) + 1;
|
|
19774
|
+
return session.anomalyCount;
|
|
19775
|
+
}
|
|
19776
|
+
function resetAnomaly(sessionID) {
|
|
19777
|
+
const session = ensureSessionInitialized(state.sessions, sessionID);
|
|
19778
|
+
if (session.anomalyCount > 0) {
|
|
19779
|
+
session.anomalyCount = 0;
|
|
19780
|
+
}
|
|
19781
|
+
}
|
|
19782
|
+
function updateCurrentTask(sessionID, taskID) {
|
|
19783
|
+
const session = ensureSessionInitialized(state.sessions, sessionID);
|
|
19784
|
+
session.currentTask = taskID;
|
|
19785
|
+
}
|
|
19766
19786
|
|
|
19767
19787
|
// src/core/loop/mission-seal.ts
|
|
19768
19788
|
import { existsSync as existsSync4, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
|
|
@@ -19925,6 +19945,119 @@ ${state2.prompt}
|
|
|
19925
19945
|
</mission_loop>`;
|
|
19926
19946
|
}
|
|
19927
19947
|
|
|
19948
|
+
// src/shared/constants/system-messages.ts
|
|
19949
|
+
var MISSION_MESSAGES = {
|
|
19950
|
+
START_LOG: "[MissionControl] Detected /task command. Starting mission...",
|
|
19951
|
+
CANCEL_LOG: "[MissionControl] Detected user cancellation signal.",
|
|
19952
|
+
SEAL_LOG: "[MissionControl] Mission Seal detected! Finishing loop.",
|
|
19953
|
+
TOAST_COMPLETE_TITLE: "Mission Complete",
|
|
19954
|
+
TOAST_COMPLETE_MESSAGE: "Agent sealed the mission.",
|
|
19955
|
+
STOP_TRIGGER: "STOP MISSION",
|
|
19956
|
+
CANCEL_TRIGGER: "CANCEL MISSION",
|
|
19957
|
+
// UI Messages
|
|
19958
|
+
AGENT_HEADER_FORMAT: (indicator, name) => `[${indicator}] [${name}] Working...
|
|
19959
|
+
|
|
19960
|
+
`,
|
|
19961
|
+
// Security Messages
|
|
19962
|
+
BLOCK_REASON_FORK_BOMB: "Fork bomb detected.",
|
|
19963
|
+
BLOCK_REASON_ROOT_DELETE: "Root deletion blocked.",
|
|
19964
|
+
SECRET_REDACTED_MSG: "********** [SECRET REDACTED] **********",
|
|
19965
|
+
// Sanity Messages
|
|
19966
|
+
ANOMALY_DETECTED_TITLE: (name) => `[${name}] OUTPUT ANOMALY DETECTED`,
|
|
19967
|
+
ANOMALY_DETECTED_BODY: (reason, count, recoveryText) => `Gibberish/loop detected: ${reason}
|
|
19968
|
+
Anomaly count: ${count}
|
|
19969
|
+
|
|
19970
|
+
${recoveryText}`,
|
|
19971
|
+
ANOMALY_INJECT_MSG: (count, reason, recoveryText) => `\u26A0\uFE0F ANOMALY #${count}: ${reason}
|
|
19972
|
+
|
|
19973
|
+
${recoveryText}`
|
|
19974
|
+
};
|
|
19975
|
+
var COMPACTION_PROMPT = `
|
|
19976
|
+
<system_interrupt type="memory_compaction">
|
|
19977
|
+
\u26A0\uFE0F **CRITICAL: Context Memory High ($USAGE%)**
|
|
19978
|
+
|
|
19979
|
+
Your context window is filling up. To prevent memory loss:
|
|
19980
|
+
1. **STOP** your current task immediately.
|
|
19981
|
+
2. **SUMMARIZE** all completed work and pending todos.
|
|
19982
|
+
3. **UPDATE** the file \`./.opencode/context.md\` with this summary.
|
|
19983
|
+
- Keep it concise but lossless (don't lose task IDs).
|
|
19984
|
+
- Section: ## Current Status, ## Pending Tasks.
|
|
19985
|
+
4. After updating, output exactly: \`[COMPACTION_COMPLETE]\`
|
|
19986
|
+
|
|
19987
|
+
Do this NOW before proceeding.
|
|
19988
|
+
</system_interrupt>
|
|
19989
|
+
`;
|
|
19990
|
+
var CONTINUE_INSTRUCTION = `<auto_continue>
|
|
19991
|
+
<status>Mission not complete. Keep executing.</status>
|
|
19992
|
+
|
|
19993
|
+
<rules>
|
|
19994
|
+
1. DO NOT stop - mission is incomplete
|
|
19995
|
+
2. DO NOT wait for user input
|
|
19996
|
+
3. If previous action failed, try different approach
|
|
19997
|
+
4. If agent returned nothing, proceed to next step
|
|
19998
|
+
5. Check your todo list - complete ALL pending items
|
|
19999
|
+
</rules>
|
|
20000
|
+
|
|
20001
|
+
<next_step>
|
|
20002
|
+
1. Check todo list for incomplete items
|
|
20003
|
+
2. Identify the highest priority pending task
|
|
20004
|
+
3. Execute it NOW
|
|
20005
|
+
4. Mark complete when done
|
|
20006
|
+
5. Continue until ALL todos are complete
|
|
20007
|
+
</next_step>
|
|
20008
|
+
|
|
20009
|
+
<completion_criteria>
|
|
20010
|
+
You are ONLY done when:
|
|
20011
|
+
- All todos are marked complete or cancelled
|
|
20012
|
+
- All features are implemented and tesWait:
|
|
20013
|
+
1. Don't ask for permission
|
|
20014
|
+
2. Check works
|
|
20015
|
+
3. Only when done:
|
|
20016
|
+
Then output: ${SEAL_PATTERN}
|
|
20017
|
+
</completion_criteria>
|
|
20018
|
+
</auto_continue>`;
|
|
20019
|
+
|
|
20020
|
+
// src/hooks/features/sanity-check.ts
|
|
20021
|
+
var SanityCheckHook = class {
|
|
20022
|
+
name = HOOK_NAMES.SANITY_CHECK;
|
|
20023
|
+
async execute(ctx, toolOrText, input, output) {
|
|
20024
|
+
if (output) {
|
|
20025
|
+
if (toolOrText === TOOL_NAMES.CALL_AGENT) {
|
|
20026
|
+
return this.checkToolOutput(ctx, input, output);
|
|
20027
|
+
}
|
|
20028
|
+
} else {
|
|
20029
|
+
return this.checkFinalText(ctx, toolOrText);
|
|
20030
|
+
}
|
|
20031
|
+
}
|
|
20032
|
+
async checkToolOutput(ctx, toolInput, toolOutput) {
|
|
20033
|
+
const sanityResult = checkOutputSanity(toolOutput.output);
|
|
20034
|
+
if (!sanityResult.isHealthy) {
|
|
20035
|
+
const count = recordAnomaly(ctx.sessionID);
|
|
20036
|
+
const agentName = toolInput?.agent || "unknown";
|
|
20037
|
+
const recoveryText = count >= 2 ? ESCALATION_PROMPT : RECOVERY_PROMPT;
|
|
20038
|
+
const errorMsg = MISSION_MESSAGES.ANOMALY_DETECTED_TITLE(agentName.toUpperCase()) + "\n\n" + MISSION_MESSAGES.ANOMALY_DETECTED_BODY(sanityResult.reason || "Unknown anomaly", count, recoveryText);
|
|
20039
|
+
return { output: errorMsg };
|
|
20040
|
+
} else {
|
|
20041
|
+
resetAnomaly(ctx.sessionID);
|
|
20042
|
+
}
|
|
20043
|
+
return {};
|
|
20044
|
+
}
|
|
20045
|
+
async checkFinalText(ctx, finalText) {
|
|
20046
|
+
const sanityResult = checkOutputSanity(finalText);
|
|
20047
|
+
if (!sanityResult.isHealthy) {
|
|
20048
|
+
const count = recordAnomaly(ctx.sessionID);
|
|
20049
|
+
const recoveryText = count >= 2 ? ESCALATION_PROMPT : RECOVERY_PROMPT;
|
|
20050
|
+
const prompt = MISSION_MESSAGES.ANOMALY_INJECT_MSG(count, sanityResult.reason || "Unknown anomaly", recoveryText);
|
|
20051
|
+
return {
|
|
20052
|
+
action: HOOK_ACTIONS.INJECT,
|
|
20053
|
+
prompts: [prompt]
|
|
20054
|
+
};
|
|
20055
|
+
}
|
|
20056
|
+
resetAnomaly(ctx.sessionID);
|
|
20057
|
+
return { action: HOOK_ACTIONS.CONTINUE };
|
|
20058
|
+
}
|
|
20059
|
+
};
|
|
20060
|
+
|
|
19928
20061
|
// src/core/progress/store.ts
|
|
19929
20062
|
var progressHistory = /* @__PURE__ */ new Map();
|
|
19930
20063
|
var sessionStartTimes = /* @__PURE__ */ new Map();
|
|
@@ -20001,79 +20134,24 @@ function formatElapsedTime(startMs, endMs = Date.now()) {
|
|
|
20001
20134
|
}
|
|
20002
20135
|
|
|
20003
20136
|
// src/hooks/features/mission-loop.ts
|
|
20004
|
-
var CONTINUE_INSTRUCTION = `<auto_continue>
|
|
20005
|
-
<status>Mission not complete. Keep executing.</status>
|
|
20006
|
-
|
|
20007
|
-
<rules>
|
|
20008
|
-
1. DO NOT stop - mission is incomplete
|
|
20009
|
-
2. DO NOT wait for user input
|
|
20010
|
-
3. If previous action failed, try different approach
|
|
20011
|
-
4. If agent returned nothing, proceed to next step
|
|
20012
|
-
5. Check your todo list - complete ALL pending items
|
|
20013
|
-
</rules>
|
|
20014
|
-
|
|
20015
|
-
<next_step>
|
|
20016
|
-
1. Check todo list for incomplete items
|
|
20017
|
-
2. Identify the highest priority pending task
|
|
20018
|
-
3. Execute it NOW
|
|
20019
|
-
4. Mark complete when done
|
|
20020
|
-
5. Continue until ALL todos are complete
|
|
20021
|
-
</next_step>
|
|
20022
|
-
|
|
20023
|
-
<completion_criteria>
|
|
20024
|
-
You are ONLY done when:
|
|
20025
|
-
- All todos are marked complete or cancelled
|
|
20026
|
-
- All features are implemented and tesWait:
|
|
20027
|
-
1. Don't ask for permission
|
|
20028
|
-
2. Check works
|
|
20029
|
-
3. Only when done:
|
|
20030
|
-
Then output: ${SEAL_PATTERN}
|
|
20031
|
-
</completion_criteria>
|
|
20032
|
-
</auto_continue>`;
|
|
20033
20137
|
var MissionControlHook = class {
|
|
20034
20138
|
name = HOOK_NAMES.MISSION_LOOP;
|
|
20035
|
-
// Update usage to MISSION_CONTROL later if desired
|
|
20036
20139
|
async execute(ctx, text) {
|
|
20037
20140
|
const chatResult = await this.handleChatCommand(ctx, text);
|
|
20038
20141
|
if (chatResult) return chatResult;
|
|
20039
20142
|
return this.handleMissionSeal(ctx, text);
|
|
20040
20143
|
}
|
|
20041
20144
|
// -------------------------------------------------------------------------------
|
|
20042
|
-
// 1. Chat Logic: Detect /task
|
|
20145
|
+
// 1. Chat Logic: Detect /task & Initialize
|
|
20043
20146
|
// -------------------------------------------------------------------------------
|
|
20044
20147
|
async handleChatCommand(ctx, message) {
|
|
20045
20148
|
const parsed = detectSlashCommand(message);
|
|
20046
20149
|
if (!parsed || parsed.command !== COMMAND_NAMES.TASK) return null;
|
|
20047
20150
|
const command = COMMANDS[parsed.command];
|
|
20048
20151
|
const { sessionID, sessions, directory } = ctx;
|
|
20049
|
-
log(
|
|
20050
|
-
|
|
20051
|
-
|
|
20052
|
-
sessions.set(sessionID, {
|
|
20053
|
-
active: true,
|
|
20054
|
-
step: 0,
|
|
20055
|
-
timestamp: now,
|
|
20056
|
-
startTime: now,
|
|
20057
|
-
lastStepTime: now,
|
|
20058
|
-
tokens: { totalInput: 0, totalOutput: 0, estimatedCost: 0 }
|
|
20059
|
-
});
|
|
20060
|
-
}
|
|
20061
|
-
let stateSession = state.sessions.get(sessionID);
|
|
20062
|
-
if (!stateSession) {
|
|
20063
|
-
state.sessions.set(sessionID, {
|
|
20064
|
-
enabled: true,
|
|
20065
|
-
iterations: 0,
|
|
20066
|
-
taskRetries: /* @__PURE__ */ new Map(),
|
|
20067
|
-
currentTask: "",
|
|
20068
|
-
anomalyCount: 0
|
|
20069
|
-
});
|
|
20070
|
-
stateSession = state.sessions.get(sessionID);
|
|
20071
|
-
}
|
|
20072
|
-
if (stateSession) {
|
|
20073
|
-
stateSession.enabled = true;
|
|
20074
|
-
stateSession.anomalyCount = 0;
|
|
20075
|
-
}
|
|
20076
|
-
state.missionActive = true;
|
|
20152
|
+
log(MISSION_MESSAGES.START_LOG);
|
|
20153
|
+
ensureSessionInitialized(sessions, sessionID);
|
|
20154
|
+
activateMissionState(sessionID);
|
|
20077
20155
|
const prompt = parsed.args || "continue from where we left off";
|
|
20078
20156
|
startMissionLoop(directory, sessionID, prompt);
|
|
20079
20157
|
startSession(sessionID);
|
|
@@ -20087,30 +20165,29 @@ var MissionControlHook = class {
|
|
|
20087
20165
|
return { action: HOOK_ACTIONS.PROCESS };
|
|
20088
20166
|
}
|
|
20089
20167
|
// -------------------------------------------------------------------------------
|
|
20090
|
-
// 2. Done Logic: Check Seal
|
|
20168
|
+
// 2. Done Logic: Check Seal & Auto-Continue
|
|
20091
20169
|
// -------------------------------------------------------------------------------
|
|
20092
20170
|
async handleMissionSeal(ctx, agentText) {
|
|
20093
20171
|
const { sessionID, directory, sessions } = ctx;
|
|
20094
20172
|
const session = sessions.get(sessionID);
|
|
20095
|
-
|
|
20096
|
-
if (!stateSession || !state.missionActive) {
|
|
20173
|
+
if (!isMissionActive(sessionID)) {
|
|
20097
20174
|
return { action: HOOK_ACTIONS.CONTINUE };
|
|
20098
20175
|
}
|
|
20099
20176
|
if (!isLoopActive(directory, sessionID)) {
|
|
20100
20177
|
return { action: HOOK_ACTIONS.CONTINUE };
|
|
20101
20178
|
}
|
|
20102
20179
|
const finalText = agentText || "";
|
|
20103
|
-
if (finalText.includes(
|
|
20104
|
-
log(
|
|
20180
|
+
if (finalText.includes(MISSION_MESSAGES.STOP_TRIGGER) || finalText.includes(MISSION_MESSAGES.CANCEL_TRIGGER)) {
|
|
20181
|
+
log(MISSION_MESSAGES.CANCEL_LOG);
|
|
20105
20182
|
await cancelMissionLoop(directory, sessionID);
|
|
20106
20183
|
return { action: HOOK_ACTIONS.STOP, reason: "User cancelled via text" };
|
|
20107
20184
|
}
|
|
20108
20185
|
if (detectSealInText(finalText)) {
|
|
20109
|
-
log(
|
|
20186
|
+
log(MISSION_MESSAGES.SEAL_LOG);
|
|
20110
20187
|
clearLoopState(directory);
|
|
20111
20188
|
await show({
|
|
20112
|
-
title:
|
|
20113
|
-
message:
|
|
20189
|
+
title: MISSION_MESSAGES.TOAST_COMPLETE_TITLE,
|
|
20190
|
+
message: MISSION_MESSAGES.TOAST_COMPLETE_MESSAGE,
|
|
20114
20191
|
variant: "success"
|
|
20115
20192
|
});
|
|
20116
20193
|
return { action: HOOK_ACTIONS.STOP, reason: "Mission Sealed" };
|
|
@@ -20130,20 +20207,40 @@ var MissionControlHook = class {
|
|
|
20130
20207
|
}
|
|
20131
20208
|
};
|
|
20132
20209
|
|
|
20210
|
+
// src/shared/constants/security-patterns.ts
|
|
20211
|
+
var SECURITY_PATTERNS = {
|
|
20212
|
+
// Dangerous Commands
|
|
20213
|
+
FORK_BOMB: ":(){ :|:& };:",
|
|
20214
|
+
ROOT_DELETION: /rm\s+(-r?f?\s+)*\/\s*$/,
|
|
20215
|
+
// rm -rf /
|
|
20216
|
+
// Secret Detection
|
|
20217
|
+
SECRETS: [
|
|
20218
|
+
/sk-[a-zA-Z0-9]{20,}T3BlbkFJ/g,
|
|
20219
|
+
// OpenAI-like
|
|
20220
|
+
/(AWS|aws|Aws)?[_ ]?(SECRET|secret|Secret)?[_ ]?(KEY|key|Key)[:= ]+[A-Za-z0-9\/+]{40}/g,
|
|
20221
|
+
// AWS Secret Key
|
|
20222
|
+
/ghp_[a-zA-Z0-9]{36}/g,
|
|
20223
|
+
// GitHub PAT
|
|
20224
|
+
/xox[baprs]-([0-9a-zA-Z]{10,48})/g
|
|
20225
|
+
// Slack Token
|
|
20226
|
+
]
|
|
20227
|
+
};
|
|
20228
|
+
var UI_PATTERNS = {
|
|
20229
|
+
TASK_ID: /\[(TASK-\d+)\]/i
|
|
20230
|
+
};
|
|
20231
|
+
|
|
20133
20232
|
// src/hooks/custom/strict-role-guard.ts
|
|
20134
20233
|
var StrictRoleGuardHook = class {
|
|
20135
20234
|
name = HOOK_NAMES.STRICT_ROLE_GUARD;
|
|
20136
20235
|
async execute(ctx, tool2, args) {
|
|
20137
|
-
const session = state.sessions.get(ctx.sessionID);
|
|
20138
20236
|
if (tool2 === "run_command" || tool2 === TOOL_NAMES.RUN_BACKGROUND) {
|
|
20139
20237
|
const cmd = args?.command;
|
|
20140
20238
|
if (cmd) {
|
|
20141
|
-
if (cmd.includes(
|
|
20142
|
-
return { action: HOOK_ACTIONS.BLOCK, reason:
|
|
20239
|
+
if (cmd.includes(SECURITY_PATTERNS.FORK_BOMB)) {
|
|
20240
|
+
return { action: HOOK_ACTIONS.BLOCK, reason: MISSION_MESSAGES.BLOCK_REASON_FORK_BOMB };
|
|
20143
20241
|
}
|
|
20144
|
-
|
|
20145
|
-
|
|
20146
|
-
return { action: HOOK_ACTIONS.BLOCK, reason: "Root deletion blocked." };
|
|
20242
|
+
if (SECURITY_PATTERNS.ROOT_DELETION.test(cmd.trim())) {
|
|
20243
|
+
return { action: HOOK_ACTIONS.BLOCK, reason: MISSION_MESSAGES.BLOCK_REASON_ROOT_DELETE };
|
|
20147
20244
|
}
|
|
20148
20245
|
}
|
|
20149
20246
|
}
|
|
@@ -20152,24 +20249,14 @@ var StrictRoleGuardHook = class {
|
|
|
20152
20249
|
};
|
|
20153
20250
|
|
|
20154
20251
|
// src/hooks/custom/secret-scanner.ts
|
|
20155
|
-
var SECRET_PATTERNS = [
|
|
20156
|
-
/sk-[a-zA-Z0-9]{20,}T3BlbkFJ/g,
|
|
20157
|
-
// OpenAI-like (example)
|
|
20158
|
-
/(AWS|aws|Aws)?[_ ]?(SECRET|secret|Secret)?[_ ]?(KEY|key|Key)[:= ]+[A-Za-z0-9\/+]{40}/g,
|
|
20159
|
-
// AWS Secret Key
|
|
20160
|
-
/ghp_[a-zA-Z0-9]{36}/g,
|
|
20161
|
-
// GitHub Personal Access Token
|
|
20162
|
-
/xox[baprs]-([0-9a-zA-Z]{10,48})/g
|
|
20163
|
-
// Slack Token
|
|
20164
|
-
];
|
|
20165
20252
|
var SecretScannerHook = class {
|
|
20166
20253
|
name = HOOK_NAMES.SECRET_SCANNER;
|
|
20167
20254
|
async execute(ctx, tool2, input, output) {
|
|
20168
20255
|
let content = output.output;
|
|
20169
20256
|
let modified = false;
|
|
20170
|
-
for (const pattern of
|
|
20257
|
+
for (const pattern of SECURITY_PATTERNS.SECRETS) {
|
|
20171
20258
|
if (pattern.test(content)) {
|
|
20172
|
-
content = content.replace(pattern,
|
|
20259
|
+
content = content.replace(pattern, MISSION_MESSAGES.SECRET_REDACTED_MSG);
|
|
20173
20260
|
modified = true;
|
|
20174
20261
|
}
|
|
20175
20262
|
}
|
|
@@ -20185,19 +20272,16 @@ var AgentUIHook = class {
|
|
|
20185
20272
|
name = HOOK_NAMES.AGENT_UI;
|
|
20186
20273
|
async execute(ctx, tool2, input, output) {
|
|
20187
20274
|
if (tool2 !== TOOL_NAMES.CALL_AGENT) return {};
|
|
20188
|
-
|
|
20189
|
-
|
|
20190
|
-
const taskIdMatch = input.task.match(/\[(TASK-\d+)\]/i);
|
|
20275
|
+
if (input?.task) {
|
|
20276
|
+
const taskIdMatch = input.task.match(UI_PATTERNS.TASK_ID);
|
|
20191
20277
|
if (taskIdMatch) {
|
|
20192
|
-
|
|
20278
|
+
updateCurrentTask(ctx.sessionID, taskIdMatch[1].toUpperCase());
|
|
20193
20279
|
}
|
|
20194
20280
|
}
|
|
20195
20281
|
if (input?.agent) {
|
|
20196
20282
|
const agentName = input.agent;
|
|
20197
20283
|
const indicator = agentName[0].toUpperCase();
|
|
20198
|
-
const header =
|
|
20199
|
-
|
|
20200
|
-
`;
|
|
20284
|
+
const header = MISSION_MESSAGES.AGENT_HEADER_FORMAT(indicator, agentName.toUpperCase());
|
|
20201
20285
|
if (!output.output.startsWith("[" + indicator + "]")) {
|
|
20202
20286
|
return { output: header + output.output };
|
|
20203
20287
|
}
|
|
@@ -20302,45 +20386,34 @@ function cleanupSession(sessionID) {
|
|
|
20302
20386
|
}
|
|
20303
20387
|
|
|
20304
20388
|
// src/hooks/custom/resource-control.ts
|
|
20305
|
-
var COST_PER_1K_INPUT = 3e-3;
|
|
20306
|
-
var COST_PER_1K_OUTPUT = 0.015;
|
|
20307
20389
|
var COMPACTION_THRESHOLD = CONTEXT_THRESHOLDS.WARNING;
|
|
20308
20390
|
var COOLDOWN_MS = 10 * 60 * 1e3;
|
|
20309
20391
|
var ResourceControlHook = class {
|
|
20310
20392
|
name = HOOK_NAMES.RESOURCE_CONTROL;
|
|
20311
20393
|
lastCompactionTime = /* @__PURE__ */ new Map();
|
|
20312
20394
|
async execute(ctx, toolOrText, input, output) {
|
|
20313
|
-
|
|
20314
|
-
|
|
20315
|
-
if (!session.tokens) {
|
|
20316
|
-
session.tokens = { totalInput: 0, totalOutput: 0, estimatedCost: 0 };
|
|
20317
|
-
}
|
|
20318
|
-
let inputTokens = 0;
|
|
20319
|
-
let outputTokens = 0;
|
|
20395
|
+
let inputLen = 0;
|
|
20396
|
+
let outputLen = 0;
|
|
20320
20397
|
if (input) {
|
|
20321
20398
|
const inputStr = typeof input === "string" ? input : JSON.stringify(input);
|
|
20322
|
-
|
|
20399
|
+
inputLen = inputStr.length;
|
|
20323
20400
|
}
|
|
20324
20401
|
if (output) {
|
|
20325
20402
|
const outputStr = typeof output === "string" ? output : JSON.stringify(output);
|
|
20326
|
-
|
|
20403
|
+
outputLen = outputStr.length;
|
|
20327
20404
|
}
|
|
20328
20405
|
if (arguments.length === 2 && typeof toolOrText === "string") {
|
|
20329
|
-
|
|
20406
|
+
outputLen = toolOrText.length;
|
|
20330
20407
|
}
|
|
20331
|
-
|
|
20332
|
-
session.
|
|
20333
|
-
|
|
20334
|
-
session.tokens.estimatedCost = Number(cost.toFixed(4));
|
|
20335
|
-
const result = await this.checkMemoryHealth(ctx, session);
|
|
20336
|
-
return result;
|
|
20408
|
+
updateSessionTokens(ctx.sessions, ctx.sessionID, inputLen, outputLen);
|
|
20409
|
+
const session = ctx.sessions.get(ctx.sessionID);
|
|
20410
|
+
return this.checkMemoryHealth(ctx, session);
|
|
20337
20411
|
}
|
|
20338
20412
|
async checkMemoryHealth(ctx, session) {
|
|
20413
|
+
if (!session?.tokens) return { action: HOOK_ACTIONS.CONTINUE };
|
|
20339
20414
|
const totalUsed = session.tokens.totalInput + session.tokens.totalOutput;
|
|
20340
20415
|
const maxTokens = CONTEXT_MONITOR_CONFIG.DEFAULT_MAX_TOKENS;
|
|
20341
20416
|
const usage = calculateUsage(totalUsed, maxTokens);
|
|
20342
|
-
if (usage > CONTEXT_THRESHOLDS.INFO) {
|
|
20343
|
-
}
|
|
20344
20417
|
if (usage < COMPACTION_THRESHOLD) {
|
|
20345
20418
|
return { action: HOOK_ACTIONS.CONTINUE };
|
|
20346
20419
|
}
|
|
@@ -20350,30 +20423,14 @@ var ResourceControlHook = class {
|
|
|
20350
20423
|
return { action: HOOK_ACTIONS.CONTINUE };
|
|
20351
20424
|
}
|
|
20352
20425
|
this.lastCompactionTime.set(ctx.sessionID, now);
|
|
20353
|
-
const
|
|
20354
|
-
|
|
20426
|
+
const usagePercent = Math.round(usage * 100);
|
|
20427
|
+
const prompt = COMPACTION_PROMPT.replace("$USAGE", usagePercent.toString());
|
|
20428
|
+
log(`[ResourceControl] Triggering compaction for session ${ctx.sessionID} (Usage: ${usagePercent}%)`);
|
|
20355
20429
|
return {
|
|
20356
20430
|
action: HOOK_ACTIONS.INJECT,
|
|
20357
|
-
prompts: [
|
|
20431
|
+
prompts: [prompt]
|
|
20358
20432
|
};
|
|
20359
20433
|
}
|
|
20360
|
-
generateCompactionPrompt(usage) {
|
|
20361
|
-
return `
|
|
20362
|
-
<system_interrupt type="memory_compaction">
|
|
20363
|
-
\u26A0\uFE0F **CRITICAL: Context Memory High (${Math.round(usage * 100)}%)**
|
|
20364
|
-
|
|
20365
|
-
Your context window is filling up. To prevent memory loss:
|
|
20366
|
-
1. **STOP** your current task immediately.
|
|
20367
|
-
2. **SUMMARIZE** all completed work and pending todos.
|
|
20368
|
-
3. **UPDATE** the file \`./.opencode/context.md\` with this summary.
|
|
20369
|
-
- Keep it concise but lossless (don't lose task IDs).
|
|
20370
|
-
- Section: ## Current Status, ## Pending Tasks.
|
|
20371
|
-
4. After updating, output exactly: \`[COMPACTION_COMPLETE]\`
|
|
20372
|
-
|
|
20373
|
-
Do this NOW before proceeding.
|
|
20374
|
-
</system_interrupt>
|
|
20375
|
-
`;
|
|
20376
|
-
}
|
|
20377
20434
|
};
|
|
20378
20435
|
|
|
20379
20436
|
// src/core/loop/stats.ts
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Patterns & Constants
|
|
3
|
+
*
|
|
4
|
+
* Centralized definition of dangerous patterns, secrets, and security rules.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SECURITY_PATTERNS: {
|
|
7
|
+
readonly FORK_BOMB: ":(){ :|:& };:";
|
|
8
|
+
readonly ROOT_DELETION: RegExp;
|
|
9
|
+
readonly SECRETS: readonly [RegExp, RegExp, RegExp, RegExp];
|
|
10
|
+
};
|
|
11
|
+
export declare const UI_PATTERNS: {
|
|
12
|
+
readonly TASK_ID: RegExp;
|
|
13
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System Messages & Templates
|
|
3
|
+
*
|
|
4
|
+
* Centralized storage for long prompt templates, user messages, and notifications.
|
|
5
|
+
*/
|
|
6
|
+
export declare const MISSION_MESSAGES: {
|
|
7
|
+
readonly START_LOG: "[MissionControl] Detected /task command. Starting mission...";
|
|
8
|
+
readonly CANCEL_LOG: "[MissionControl] Detected user cancellation signal.";
|
|
9
|
+
readonly SEAL_LOG: "[MissionControl] Mission Seal detected! Finishing loop.";
|
|
10
|
+
readonly TOAST_COMPLETE_TITLE: "Mission Complete";
|
|
11
|
+
readonly TOAST_COMPLETE_MESSAGE: "Agent sealed the mission.";
|
|
12
|
+
readonly STOP_TRIGGER: "STOP MISSION";
|
|
13
|
+
readonly CANCEL_TRIGGER: "CANCEL MISSION";
|
|
14
|
+
readonly AGENT_HEADER_FORMAT: (indicator: string, name: string) => string;
|
|
15
|
+
readonly BLOCK_REASON_FORK_BOMB: "Fork bomb detected.";
|
|
16
|
+
readonly BLOCK_REASON_ROOT_DELETE: "Root deletion blocked.";
|
|
17
|
+
readonly SECRET_REDACTED_MSG: "********** [SECRET REDACTED] **********";
|
|
18
|
+
readonly ANOMALY_DETECTED_TITLE: (name: string) => string;
|
|
19
|
+
readonly ANOMALY_DETECTED_BODY: (reason: string, count: number, recoveryText: string) => string;
|
|
20
|
+
readonly ANOMALY_INJECT_MSG: (count: number, reason: string, recoveryText: string) => string;
|
|
21
|
+
};
|
|
22
|
+
export declare const COMPACTION_PROMPT = "\n<system_interrupt type=\"memory_compaction\">\n\u26A0\uFE0F **CRITICAL: Context Memory High ($USAGE%)**\n\nYour context window is filling up. To prevent memory loss:\n1. **STOP** your current task immediately.\n2. **SUMMARIZE** all completed work and pending todos.\n3. **UPDATE** the file `./.opencode/context.md` with this summary.\n - Keep it concise but lossless (don't lose task IDs).\n - Section: ## Current Status, ## Pending Tasks.\n4. After updating, output exactly: `[COMPACTION_COMPLETE]`\n\nDo this NOW before proceeding.\n</system_interrupt>\n";
|
|
23
|
+
export declare const CONTINUE_INSTRUCTION: string;
|
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.0.
|
|
5
|
+
"version": "1.0.40",
|
|
6
6
|
"author": "agnusdei1207",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"release:minor": "npm run build && npm run rust:dist && npm version minor && git push --follow-tags && npm publish --access public",
|
|
63
63
|
"release:major": "npm run build && npm run rust:dist && npm version major && git push --follow-tags && npm publish --access public",
|
|
64
64
|
"reset:local": "brew uninstall opencode 2>/dev/null; rm -rf ~/.config/opencode ~/.opencode ~/.local/share/opencode ~/.cache/opencode/node_modules/opencode-orchestrator && echo '=== Clean done ===' && brew install opencode && echo '{\"plugin\": [\"opencode-orchestrator\"], \"$schema\": \"https://opencode.ai/config.json\"}' > ~/.config/opencode/opencode.json && echo '=== Reset (Dev) complete. Run: opencode ==='",
|
|
65
|
-
"reset:prod": "brew uninstall opencode 2>/dev/null; rm -rf ~/.config/opencode ~/.opencode ~/.local/share/opencode ~/.cache/opencode/node_modules/opencode-orchestrator && echo '=== Clean done ===' && brew install opencode && echo '{\"plugin\": [\"opencode-orchestrator\"], \"$schema\": \"https://opencode.ai/config.json\"}' > ~/.config/opencode/opencode.json && npm
|
|
65
|
+
"reset:prod": "brew uninstall opencode 2>/dev/null; rm -rf ~/.config/opencode ~/.opencode ~/.local/share/opencode ~/.cache/opencode/node_modules/opencode-orchestrator && echo '=== Clean done ===' && brew install opencode && echo '{\"plugin\": [\"opencode-orchestrator\"], \"$schema\": \"https://opencode.ai/config.json\"}' > ~/.config/opencode/opencode.json && npm uninstall -g opencode-orchestrator && echo '=== Reset (Prod) complete. Run: opencode ==='",
|
|
66
66
|
"ginstall": "npm install -g opencode-orchestrator",
|
|
67
67
|
"log": "tail -f \"$(node -e 'console.log(require(\"os\").tmpdir())')/opencode-orchestrator.log\""
|
|
68
68
|
},
|