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 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: string;
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/hooks/features/sanity-check.ts
19718
- var SanityCheckHook = class {
19719
- name = HOOK_NAMES.SANITY_CHECK;
19720
- async execute(ctx, toolOrText, input, output) {
19721
- if (output) {
19722
- if (toolOrText === TOOL_NAMES.CALL_AGENT) {
19723
- return this.checkToolOutput(ctx, input, output);
19724
- }
19725
- } else {
19726
- return this.checkFinalText(ctx, toolOrText);
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
- async checkToolOutput(ctx, toolInput, toolOutput) {
19730
- const stateSession = state.sessions.get(ctx.sessionID);
19731
- if (!stateSession) return {};
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
- async checkFinalText(ctx, finalText) {
19749
- const stateSession = state.sessions.get(ctx.sessionID);
19750
- if (!stateSession) return { action: HOOK_ACTIONS.CONTINUE };
19751
- const sanityResult = checkOutputSanity(finalText);
19752
- if (!sanityResult.isHealthy) {
19753
- stateSession.anomalyCount = (stateSession.anomalyCount || 0) + 1;
19754
- const recoveryText = stateSession.anomalyCount >= 2 ? ESCALATION_PROMPT : RECOVERY_PROMPT;
19755
- return {
19756
- action: HOOK_ACTIONS.INJECT,
19757
- prompts: [`\u26A0\uFE0F ANOMALY #${stateSession.anomalyCount}: ${sanityResult.reason}
19758
-
19759
- ${recoveryText}`]
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(`[MissionControl] Detected /task command. Starting mission...`);
20050
- if (!sessions.has(sessionID)) {
20051
- const now = Date.now();
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
- const stateSession = state.sessions.get(sessionID);
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("STOP MISSION") || finalText.includes("CANCEL MISSION")) {
20104
- log("[mission-control] detected user cancellation signal.");
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("[mission-control] Mission Seal detected! Finishing loop.");
20186
+ log(MISSION_MESSAGES.SEAL_LOG);
20110
20187
  clearLoopState(directory);
20111
20188
  await show({
20112
- title: "Mission Complete",
20113
- message: "Agent sealed the mission.",
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: "Fork bomb detected." };
20239
+ if (cmd.includes(SECURITY_PATTERNS.FORK_BOMB)) {
20240
+ return { action: HOOK_ACTIONS.BLOCK, reason: MISSION_MESSAGES.BLOCK_REASON_FORK_BOMB };
20143
20241
  }
20144
- const isRootDeletion = /rm\s+(-r?f?\s+)*\/\s*$/.test(cmd.trim());
20145
- if (isRootDeletion) {
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 SECRET_PATTERNS) {
20257
+ for (const pattern of SECURITY_PATTERNS.SECRETS) {
20171
20258
  if (pattern.test(content)) {
20172
- content = content.replace(pattern, "********** [SECRET REDACTED] **********");
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
- const stateSession = state.sessions.get(ctx.sessionID);
20189
- if (input?.task && stateSession) {
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
- stateSession.currentTask = taskIdMatch[1].toUpperCase();
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 = `[${indicator}] [${agentName.toUpperCase()}] Working...
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
- const session = ctx.sessions.get(ctx.sessionID);
20314
- if (!session) return { action: HOOK_ACTIONS.CONTINUE };
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
- inputTokens = Math.ceil(inputStr.length / 4);
20399
+ inputLen = inputStr.length;
20323
20400
  }
20324
20401
  if (output) {
20325
20402
  const outputStr = typeof output === "string" ? output : JSON.stringify(output);
20326
- outputTokens = Math.ceil(outputStr.length / 4);
20403
+ outputLen = outputStr.length;
20327
20404
  }
20328
20405
  if (arguments.length === 2 && typeof toolOrText === "string") {
20329
- outputTokens = Math.ceil(toolOrText.length / 4);
20406
+ outputLen = toolOrText.length;
20330
20407
  }
20331
- session.tokens.totalInput += inputTokens;
20332
- session.tokens.totalOutput += outputTokens;
20333
- const cost = session.tokens.totalInput / 1e3 * COST_PER_1K_INPUT + session.tokens.totalOutput / 1e3 * COST_PER_1K_OUTPUT;
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 compactionPrompt = this.generateCompactionPrompt(usage);
20354
- log(`[ResourceControl] Triggering compaction for session ${ctx.sessionID} (Usage: ${Math.round(usage * 100)}%)`);
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: [compactionPrompt]
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.37",
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 install -g opencode-orchestrator && echo '=== Reset (Prod) 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 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
  },