opencode-orchestrator 1.0.65 → 1.0.72

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.
Files changed (31) hide show
  1. package/README.md +60 -104
  2. package/dist/core/agents/agent-registry.d.ts +29 -0
  3. package/dist/core/agents/interfaces/session-pool.interface.d.ts +79 -0
  4. package/dist/core/agents/manager/task-cleaner.d.ts +3 -1
  5. package/dist/core/agents/manager/task-launcher.d.ts +4 -2
  6. package/dist/core/agents/manager.d.ts +2 -0
  7. package/dist/core/agents/session-pool.d.ts +58 -0
  8. package/dist/core/loop/todo-manager.d.ts +18 -0
  9. package/dist/core/memory/interfaces.d.ts +33 -0
  10. package/dist/core/memory/memory-manager.d.ts +40 -0
  11. package/dist/core/metrics/collector.d.ts +27 -0
  12. package/dist/core/orchestrator/session-manager.d.ts +0 -1
  13. package/dist/core/plugins/interfaces.d.ts +30 -0
  14. package/dist/core/plugins/plugin-manager.d.ts +21 -0
  15. package/dist/core/progress/progress-notifier.d.ts +14 -0
  16. package/dist/core/progress/state-broadcaster.d.ts +29 -0
  17. package/dist/core/progress/terminal-monitor.d.ts +13 -0
  18. package/dist/core/recovery/interfaces/recovery-action.d.ts +1 -0
  19. package/dist/hooks/constants.d.ts +4 -1
  20. package/dist/hooks/custom/memory-gate.d.ts +21 -0
  21. package/dist/hooks/custom/metrics.d.ts +14 -0
  22. package/dist/index.js +1621 -469
  23. package/dist/shared/core/constants/index.d.ts +1 -0
  24. package/dist/shared/core/constants/memory-hooks.d.ts +66 -0
  25. package/dist/shared/core/constants/paths.d.ts +2 -0
  26. package/dist/shared/tool/constants/tool-names.d.ts +3 -0
  27. package/dist/tools/lsp/diagnostics-cache.d.ts +14 -0
  28. package/dist/tools/parallel/list-agents.d.ts +10 -0
  29. package/dist/tools/parallel/show-metrics.d.ts +10 -0
  30. package/dist/tools/parallel/update-todo.d.ts +28 -0
  31. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14,13 +14,13 @@ __export(store_exports, {
14
14
  addDecision: () => addDecision,
15
15
  addDocument: () => addDocument,
16
16
  addFinding: () => addFinding,
17
- clear: () => clear,
17
+ clear: () => clear2,
18
18
  clearAll: () => clearAll,
19
19
  create: () => create,
20
20
  get: () => get,
21
21
  getChildren: () => getChildren,
22
22
  getMerged: () => getMerged,
23
- getStats: () => getStats
23
+ getStats: () => getStats2
24
24
  });
25
25
  function create(sessionId, parentId) {
26
26
  const context = {
@@ -88,7 +88,7 @@ function addDecision(sessionId, decision) {
88
88
  function getChildren(parentId) {
89
89
  return Array.from(parentChildMap.get(parentId) || []);
90
90
  }
91
- function clear(sessionId) {
91
+ function clear2(sessionId) {
92
92
  const context = contexts.get(sessionId);
93
93
  if (context?.parentId) {
94
94
  parentChildMap.get(context.parentId)?.delete(sessionId);
@@ -99,7 +99,7 @@ function clearAll() {
99
99
  contexts.clear();
100
100
  parentChildMap.clear();
101
101
  }
102
- function getStats() {
102
+ function getStats2() {
103
103
  let totalDocuments = 0;
104
104
  let totalFindings = 0;
105
105
  let totalDecisions = 0;
@@ -133,6 +133,7 @@ var AGENT_NAMES = {
133
133
  PLANNER: "Planner",
134
134
  WORKER: "Worker",
135
135
  REVIEWER: "Reviewer"
136
+ // Unit Review & Final Quality Gate (Conceptual Master)
136
137
  };
137
138
 
138
139
  // src/shared/agent/constants/agent-tokens.ts
@@ -194,7 +195,10 @@ var PATHS = {
194
195
  SYNC_ISSUES: ".opencode/sync-issues.md",
195
196
  INTEGRATION_STATUS: ".opencode/integration-status.md",
196
197
  // Progress tracking
197
- STATUS: ".opencode/status.md"
198
+ STATUS: ".opencode/status.md",
199
+ // Configuration
200
+ AGENTS_CONFIG: ".opencode/agents.json",
201
+ PLUGINS: ".opencode/plugins"
198
202
  };
199
203
 
200
204
  // src/shared/core/constants/memory-limits.ts
@@ -314,6 +318,132 @@ var PHASES = {
314
318
  }
315
319
  };
316
320
 
321
+ // src/shared/tool/constants/tool-names.ts
322
+ var TOOL_NAMES = {
323
+ // Parallel task tools
324
+ DELEGATE_TASK: "delegate_task",
325
+ GET_TASK_RESULT: "get_task_result",
326
+ LIST_TASKS: "list_tasks",
327
+ CANCEL_TASK: "cancel_task",
328
+ LIST_AGENTS: "list_agents",
329
+ SHOW_METRICS: "show_metrics",
330
+ UPDATE_TODO: "update_todo",
331
+ // Background command tools
332
+ RUN_BACKGROUND: "run_background",
333
+ CHECK_BACKGROUND: "check_background",
334
+ LIST_BACKGROUND: "list_background",
335
+ KILL_BACKGROUND: "kill_background",
336
+ // Command tools
337
+ RUN_COMMAND: "run_command",
338
+ // Search tools
339
+ GREP_SEARCH: "grep_search",
340
+ GLOB_SEARCH: "glob_search",
341
+ MGREP: "mgrep",
342
+ SED_REPLACE: "sed_replace",
343
+ // Diff & Compare tools
344
+ DIFF: "diff",
345
+ // JSON tools
346
+ JQ: "jq",
347
+ // HTTP tools
348
+ HTTP: "http",
349
+ // File tools
350
+ FILE_STATS: "file_stats",
351
+ // Git tools
352
+ GIT_DIFF: "git_diff",
353
+ GIT_STATUS: "git_status",
354
+ // Web tools
355
+ WEBFETCH: "webfetch",
356
+ WEBSEARCH: "websearch",
357
+ CODESEARCH: "codesearch",
358
+ CACHE_DOCS: "cache_docs",
359
+ // LSP tools
360
+ LSP_DIAGNOSTICS: "lsp_diagnostics",
361
+ // AST tools
362
+ AST_SEARCH: "ast_search",
363
+ AST_REPLACE: "ast_replace",
364
+ // Other tools
365
+ CALL_AGENT: "call_agent",
366
+ SLASHCOMMAND: "slashcommand",
367
+ SKILL: "skill"
368
+ };
369
+
370
+ // src/shared/core/constants/memory-hooks.ts
371
+ var MEMORY_CONSTANTS = {
372
+ ID_PREFIX: "mem_",
373
+ LEVELS: {
374
+ SYSTEM: "system",
375
+ PROJECT: "project",
376
+ MISSION: "mission",
377
+ TASK: "task"
378
+ },
379
+ IMPORTANCE: {
380
+ LOW: 0.3,
381
+ NORMAL: 0.5,
382
+ HIGH: 0.7,
383
+ CRITICAL: 0.9
384
+ },
385
+ // Tools that produce high volume or irrelevant output for memory
386
+ NOISY_TOOLS: [
387
+ TOOL_NAMES.LIST_TASKS,
388
+ TOOL_NAMES.GET_TASK_RESULT,
389
+ TOOL_NAMES.LIST_BACKGROUND,
390
+ TOOL_NAMES.CHECK_BACKGROUND,
391
+ TOOL_NAMES.LIST_AGENTS,
392
+ TOOL_NAMES.SHOW_METRICS
393
+ ],
394
+ // Significant keywords for memory promotion
395
+ KEYWORDS: {
396
+ DONE: "DONE",
397
+ SUCCESS: "SUCCESS",
398
+ ERROR: "ERROR",
399
+ FAIL: "FAIL"
400
+ },
401
+ MAX_CONTENT_LENGTH: 1e3
402
+ };
403
+ var HOOK_NAMES = {
404
+ MEMORY_GATE: "MemoryGate",
405
+ METRICS_TELEMETRY: "MetricsTelemetry",
406
+ SANITY_CHECK: "SanityCheck",
407
+ MISSION_LOOP: "MissionLoop",
408
+ MISSION_CONTROL: "MissionControl",
409
+ STRICT_ROLE_GUARD: "StrictRoleGuard",
410
+ SECRET_SCANNER: "SecretScanner",
411
+ AGENT_UI: "AgentUI",
412
+ RESOURCE_CONTROL: "ResourceControl",
413
+ USER_ACTIVITY: "UserActivity",
414
+ SLASH_COMMAND: "SlashCommandDispatcher"
415
+ };
416
+ var TODO_CONSTANTS = {
417
+ MARKERS: {
418
+ PENDING: "[ ]",
419
+ COMPLETED: "[x]",
420
+ PROGRESS: "[/]",
421
+ FAILED: "[-]"
422
+ },
423
+ STATUS: {
424
+ PENDING: "pending",
425
+ COMPLETED: "completed",
426
+ PROGRESS: "progress",
427
+ FAILED: "failed"
428
+ }
429
+ };
430
+ var TUI_CONSTANTS = {
431
+ BAR_WIDTH: 30,
432
+ COLORS: {
433
+ PROGRESS: "\x1B[36m",
434
+ AGENT: "\x1B[32m",
435
+ RESET: "\x1B[0m",
436
+ BOLD: "\x1B[1m",
437
+ DIM: "\x1B[2m"
438
+ },
439
+ LABELS: {
440
+ IDLE: "Idle",
441
+ WAITING: "Waiting for tasks...",
442
+ PROGRESS_TITLE: "MISSION PROGRESS",
443
+ AGENT_TITLE: "ACTIVE AGENTS"
444
+ }
445
+ };
446
+
317
447
  // src/shared/core/constants/cli.ts
318
448
  var CLI_NAME = {
319
449
  NPX: "npx",
@@ -835,52 +965,6 @@ var EVENT_TYPES = {
835
965
  ...SPECIAL_EVENTS
836
966
  };
837
967
 
838
- // src/shared/tool/constants/tool-names.ts
839
- var TOOL_NAMES = {
840
- // Parallel task tools
841
- DELEGATE_TASK: "delegate_task",
842
- GET_TASK_RESULT: "get_task_result",
843
- LIST_TASKS: "list_tasks",
844
- CANCEL_TASK: "cancel_task",
845
- // Background command tools
846
- RUN_BACKGROUND: "run_background",
847
- CHECK_BACKGROUND: "check_background",
848
- LIST_BACKGROUND: "list_background",
849
- KILL_BACKGROUND: "kill_background",
850
- // Command tools
851
- RUN_COMMAND: "run_command",
852
- // Search tools
853
- GREP_SEARCH: "grep_search",
854
- GLOB_SEARCH: "glob_search",
855
- MGREP: "mgrep",
856
- SED_REPLACE: "sed_replace",
857
- // Diff & Compare tools
858
- DIFF: "diff",
859
- // JSON tools
860
- JQ: "jq",
861
- // HTTP tools
862
- HTTP: "http",
863
- // File tools
864
- FILE_STATS: "file_stats",
865
- // Git tools
866
- GIT_DIFF: "git_diff",
867
- GIT_STATUS: "git_status",
868
- // Web tools
869
- WEBFETCH: "webfetch",
870
- WEBSEARCH: "websearch",
871
- CODESEARCH: "codesearch",
872
- CACHE_DOCS: "cache_docs",
873
- // LSP tools
874
- LSP_DIAGNOSTICS: "lsp_diagnostics",
875
- // AST tools
876
- AST_SEARCH: "ast_search",
877
- AST_REPLACE: "ast_replace",
878
- // Other tools
879
- CALL_AGENT: "call_agent",
880
- SLASHCOMMAND: "slashcommand",
881
- SKILL: "skill"
882
- };
883
-
884
968
  // src/shared/tool/constants/lsp/lsp-severity.ts
885
969
  var LSP_SEVERITY = {
886
970
  ERROR: 1,
@@ -2142,10 +2226,10 @@ function mergeDefs(...defs) {
2142
2226
  function cloneDef(schema) {
2143
2227
  return mergeDefs(schema._zod.def);
2144
2228
  }
2145
- function getElementAtPath(obj, path6) {
2146
- if (!path6)
2229
+ function getElementAtPath(obj, path9) {
2230
+ if (!path9)
2147
2231
  return obj;
2148
- return path6.reduce((acc, key) => acc?.[key], obj);
2232
+ return path9.reduce((acc, key) => acc?.[key], obj);
2149
2233
  }
2150
2234
  function promiseAllObject(promisesObj) {
2151
2235
  const keys = Object.keys(promisesObj);
@@ -2506,11 +2590,11 @@ function aborted(x, startIndex = 0) {
2506
2590
  }
2507
2591
  return false;
2508
2592
  }
2509
- function prefixIssues(path6, issues) {
2593
+ function prefixIssues(path9, issues) {
2510
2594
  return issues.map((iss) => {
2511
2595
  var _a;
2512
2596
  (_a = iss).path ?? (_a.path = []);
2513
- iss.path.unshift(path6);
2597
+ iss.path.unshift(path9);
2514
2598
  return iss;
2515
2599
  });
2516
2600
  }
@@ -2678,7 +2762,7 @@ function treeifyError(error45, _mapper) {
2678
2762
  return issue2.message;
2679
2763
  };
2680
2764
  const result = { errors: [] };
2681
- const processError = (error46, path6 = []) => {
2765
+ const processError = (error46, path9 = []) => {
2682
2766
  var _a, _b;
2683
2767
  for (const issue2 of error46.issues) {
2684
2768
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -2688,7 +2772,7 @@ function treeifyError(error45, _mapper) {
2688
2772
  } else if (issue2.code === "invalid_element") {
2689
2773
  processError({ issues: issue2.issues }, issue2.path);
2690
2774
  } else {
2691
- const fullpath = [...path6, ...issue2.path];
2775
+ const fullpath = [...path9, ...issue2.path];
2692
2776
  if (fullpath.length === 0) {
2693
2777
  result.errors.push(mapper(issue2));
2694
2778
  continue;
@@ -2720,8 +2804,8 @@ function treeifyError(error45, _mapper) {
2720
2804
  }
2721
2805
  function toDotPath(_path) {
2722
2806
  const segs = [];
2723
- const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2724
- for (const seg of path6) {
2807
+ const path9 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2808
+ for (const seg of path9) {
2725
2809
  if (typeof seg === "number")
2726
2810
  segs.push(`[${seg}]`);
2727
2811
  else if (typeof seg === "symbol")
@@ -17332,141 +17416,490 @@ var TaskWAL = class {
17332
17416
  };
17333
17417
  var taskWAL = new TaskWAL();
17334
17418
 
17335
- // src/core/agents/manager/task-launcher.ts
17336
- var TaskLauncher = class {
17337
- constructor(client, directory, store, concurrency, onTaskError, startPolling) {
17338
- this.client = client;
17339
- this.directory = directory;
17340
- this.store = store;
17341
- this.concurrency = concurrency;
17342
- this.onTaskError = onTaskError;
17343
- this.startPolling = startPolling;
17344
- }
17345
- /**
17346
- * Unified launch method - handles both single and multiple tasks efficiently.
17347
- * All session creations happen in parallel immediately.
17348
- * Concurrency acquisition and prompt firing happen in the background.
17349
- */
17350
- async launch(inputs) {
17351
- const isArray = Array.isArray(inputs);
17352
- const taskInputs = isArray ? inputs : [inputs];
17353
- if (taskInputs.length === 0) return isArray ? [] : null;
17354
- const tasks = await Promise.all(taskInputs.map(
17355
- (input) => this.prepareTask(input).catch(() => null)
17356
- ));
17357
- const successfulTasks = tasks.filter((t) => t !== null);
17358
- successfulTasks.forEach((task) => {
17359
- this.executeBackground(task).catch((error45) => {
17360
- this.onTaskError(task.id, error45);
17361
- });
17362
- });
17363
- if (successfulTasks.length > 0) {
17364
- this.startPolling();
17419
+ // src/core/recovery/constants.ts
17420
+ var MAX_RETRIES = RECOVERY.MAX_ATTEMPTS;
17421
+ var BASE_DELAY = RECOVERY.BASE_DELAY_MS;
17422
+ var MAX_HISTORY = HISTORY.MAX_RECOVERY;
17423
+
17424
+ // src/core/recovery/patterns.ts
17425
+ var errorPatterns = [
17426
+ // Rate limiting
17427
+ {
17428
+ pattern: /rate.?limit|too.?many.?requests|429/i,
17429
+ category: "rate_limit",
17430
+ handler: (ctx) => {
17431
+ const delay = BASE_DELAY * Math.pow(2, ctx.attempt);
17432
+ presets_exports.warningRateLimited();
17433
+ return { type: "retry", delay, attempt: ctx.attempt + 1 };
17365
17434
  }
17366
- return isArray ? successfulTasks : successfulTasks[0] || null;
17367
- }
17368
- /**
17369
- * Prepare task: Create session and registration without blocking on concurrency
17370
- */
17371
- async prepareTask(input) {
17372
- const currentDepth = input.depth ?? 0;
17373
- if (currentDepth >= PARALLEL_TASK.MAX_DEPTH) {
17374
- throw new Error(`Maximum task depth (${PARALLEL_TASK.MAX_DEPTH}) reached. To prevent infinite recursion, no further sub-tasks can be spawned.`);
17435
+ },
17436
+ // Context overflow
17437
+ {
17438
+ pattern: /context.?length|token.?limit|maximum.?context/i,
17439
+ category: "context_overflow",
17440
+ handler: () => {
17441
+ presets_exports.errorRecovery("Compacting context");
17442
+ return { type: "compact", reason: "Context limit reached" };
17375
17443
  }
17376
- const sessionCreatePromise = this.client.session.create({
17377
- body: {
17378
- parentID: input.parentSessionID,
17379
- title: `${PARALLEL_TASK.SESSION_TITLE_PREFIX}: ${input.description}`
17380
- },
17381
- query: { directory: this.directory }
17382
- });
17383
- const createResult = await Promise.race([
17384
- sessionCreatePromise,
17385
- new Promise(
17386
- (_, reject) => setTimeout(() => reject(new Error("Session creation timed out after 60s")), 6e4)
17387
- )
17388
- ]);
17389
- if (createResult.error || !createResult.data?.id) {
17390
- throw new Error(`Session creation failed: ${createResult.error || "No ID"}`);
17444
+ },
17445
+ // Network errors
17446
+ {
17447
+ pattern: /ECONNREFUSED|ETIMEDOUT|network|fetch.?failed/i,
17448
+ category: "network",
17449
+ handler: (ctx) => {
17450
+ if (ctx.attempt >= MAX_RETRIES) {
17451
+ return { type: "abort", reason: "Network unavailable after retries" };
17452
+ }
17453
+ return { type: "retry", delay: BASE_DELAY * (ctx.attempt + 1), attempt: ctx.attempt + 1 };
17391
17454
  }
17392
- const sessionID = createResult.data.id;
17393
- const taskId = `${ID_PREFIX.TASK}${crypto.randomUUID().slice(0, 8)}`;
17394
- const task = {
17395
- id: taskId,
17396
- sessionID,
17397
- parentSessionID: input.parentSessionID,
17398
- description: input.description,
17399
- prompt: input.prompt,
17400
- agent: input.agent,
17401
- status: TASK_STATUS.PENDING,
17402
- // Start as PENDING
17403
- startedAt: /* @__PURE__ */ new Date(),
17404
- concurrencyKey: input.agent,
17405
- depth: (input.depth ?? 0) + 1,
17406
- mode: input.mode || "normal",
17407
- groupID: input.groupID
17408
- };
17409
- this.store.set(taskId, task);
17410
- this.store.trackPending(input.parentSessionID, taskId);
17411
- taskWAL.log(WAL_ACTIONS.LAUNCH, task).catch(() => {
17412
- });
17413
- const toastManager = getTaskToastManager();
17414
- if (toastManager) {
17415
- toastManager.addTask({
17416
- id: taskId,
17417
- description: input.description,
17418
- agent: input.agent,
17419
- isBackground: true,
17420
- parentSessionID: input.parentSessionID,
17421
- sessionID
17422
- });
17455
+ },
17456
+ // Session errors
17457
+ {
17458
+ pattern: /session.?not.?found|session.?expired/i,
17459
+ category: "session",
17460
+ handler: () => {
17461
+ return { type: "abort", reason: "Session no longer available" };
17423
17462
  }
17424
- presets_exports.sessionCreated(sessionID, input.agent);
17425
- return task;
17426
- }
17427
- /**
17428
- * Background execution: Acquire slot and fire prompt
17429
- */
17430
- async executeBackground(task) {
17431
- try {
17432
- await this.concurrency.acquire(task.agent);
17433
- task.status = TASK_STATUS.RUNNING;
17434
- task.startedAt = /* @__PURE__ */ new Date();
17435
- this.store.set(task.id, task);
17436
- taskWAL.log(WAL_ACTIONS.LAUNCH, task).catch(() => {
17437
- });
17438
- const promptPromise = this.client.session.prompt({
17439
- path: { id: task.sessionID },
17440
- body: {
17441
- agent: task.agent,
17442
- tools: {
17443
- // HPFA: Allow agents to delegate sub-tasks (Fractal Spawning)
17444
- delegate_task: true,
17445
- get_task_result: true,
17446
- list_tasks: true,
17447
- cancel_task: true,
17448
- [TOOL_NAMES.SKILL]: true,
17449
- [TOOL_NAMES.RUN_COMMAND]: true
17450
- },
17451
- parts: [{ type: PART_TYPES.TEXT, text: task.prompt }]
17452
- }
17453
- });
17454
- await Promise.race([
17455
- promptPromise,
17456
- new Promise(
17457
- (_, reject) => setTimeout(() => reject(new Error("Session prompt execution timed out after 600s")), 6e5)
17458
- )
17459
- ]);
17460
- } catch (error45) {
17461
- this.concurrency.release(task.agent);
17462
- throw error45;
17463
+ },
17464
+ // Tool errors
17465
+ {
17466
+ pattern: /tool.?not.?found|unknown.?tool/i,
17467
+ category: "tool",
17468
+ handler: (ctx) => {
17469
+ return { type: "escalate", to: "Reviewer", reason: `Unknown tool used by ${ctx.agent}` };
17463
17470
  }
17464
- }
17465
- };
17466
-
17467
- // src/core/agents/manager/task-resumer.ts
17468
- var TaskResumer = class {
17469
- constructor(client, store, findBySession, startPolling, notifyParentIfAllComplete) {
17471
+ },
17472
+ // Parse errors
17473
+ {
17474
+ pattern: /parse.?error|invalid.?json|syntax.?error/i,
17475
+ category: "parse",
17476
+ handler: (ctx) => {
17477
+ if (ctx.attempt >= 2) {
17478
+ return { type: "skip", reason: "Persistent parse error" };
17479
+ }
17480
+ return { type: "retry", delay: 500, attempt: ctx.attempt + 1 };
17481
+ }
17482
+ },
17483
+ // Gibberish / hallucination
17484
+ {
17485
+ pattern: /gibberish|hallucination|mixed.?language/i,
17486
+ category: "gibberish",
17487
+ handler: () => {
17488
+ presets_exports.errorRecovery("Retrying with clean context");
17489
+ return { type: "retry", delay: 1e3, attempt: 1 };
17490
+ }
17491
+ },
17492
+ // LSP specific errors
17493
+ {
17494
+ pattern: /lsp.?diagnostics|tsc.?error|eslint.?error/i,
17495
+ category: "lsp",
17496
+ handler: (ctx) => {
17497
+ return { type: "retry", delay: 2e3, attempt: ctx.attempt + 1, modifyPrompt: "Note: Previous attempt had LSP issues. Ensure code quality and consider simpler implementation." };
17498
+ }
17499
+ },
17500
+ // execution timeout
17501
+ {
17502
+ pattern: /execution.?timed.?out|timeout/i,
17503
+ category: "timeout",
17504
+ handler: (ctx) => {
17505
+ return { type: "retry", delay: 5e3, attempt: ctx.attempt + 1, modifyPrompt: "Note: Previous attempt timed out. Break down the task into smaller sub-tasks if necessary." };
17506
+ }
17507
+ }
17508
+ ];
17509
+
17510
+ // src/core/recovery/handler.ts
17511
+ var recoveryHistory = [];
17512
+ function handleError(context) {
17513
+ const errorMessage = context.error.message || String(context.error);
17514
+ for (const pattern of errorPatterns) {
17515
+ const matches = typeof pattern.pattern === "string" ? errorMessage.includes(pattern.pattern) : pattern.pattern.test(errorMessage);
17516
+ if (matches) {
17517
+ const action = pattern.handler(context);
17518
+ recoveryHistory.push({
17519
+ context,
17520
+ action,
17521
+ timestamp: /* @__PURE__ */ new Date()
17522
+ });
17523
+ if (recoveryHistory.length > MAX_HISTORY) {
17524
+ recoveryHistory.shift();
17525
+ }
17526
+ return action;
17527
+ }
17528
+ }
17529
+ if (context.attempt < MAX_RETRIES) {
17530
+ return {
17531
+ type: "retry",
17532
+ delay: BASE_DELAY * Math.pow(2, context.attempt),
17533
+ attempt: context.attempt + 1
17534
+ };
17535
+ }
17536
+ return { type: "abort", reason: `Unknown error after ${MAX_RETRIES} retries` };
17537
+ }
17538
+
17539
+ // src/core/memory/interfaces.ts
17540
+ var MemoryLevel = /* @__PURE__ */ ((MemoryLevel2) => {
17541
+ MemoryLevel2["SYSTEM"] = "system";
17542
+ MemoryLevel2["PROJECT"] = "project";
17543
+ MemoryLevel2["MISSION"] = "mission";
17544
+ MemoryLevel2["TASK"] = "task";
17545
+ return MemoryLevel2;
17546
+ })(MemoryLevel || {});
17547
+
17548
+ // src/core/memory/memory-manager.ts
17549
+ var MemoryManager = class _MemoryManager {
17550
+ static instance;
17551
+ memories = /* @__PURE__ */ new Map();
17552
+ // id -> level mapping
17553
+ storage = {
17554
+ ["system" /* SYSTEM */]: [],
17555
+ ["project" /* PROJECT */]: [],
17556
+ ["mission" /* MISSION */]: [],
17557
+ ["task" /* TASK */]: []
17558
+ };
17559
+ config = {
17560
+ tokenBudgets: {
17561
+ ["system" /* SYSTEM */]: 2e3,
17562
+ ["project" /* PROJECT */]: 5e3,
17563
+ ["mission" /* MISSION */]: 1e4,
17564
+ ["task" /* TASK */]: 2e4
17565
+ },
17566
+ enableRelevanceFiltering: true
17567
+ };
17568
+ constructor() {
17569
+ }
17570
+ static getInstance() {
17571
+ if (!_MemoryManager.instance) {
17572
+ _MemoryManager.instance = new _MemoryManager();
17573
+ }
17574
+ return _MemoryManager.instance;
17575
+ }
17576
+ /**
17577
+ * Add a memory entry
17578
+ */
17579
+ add(level, content, importance = MEMORY_CONSTANTS.IMPORTANCE.NORMAL, metadata) {
17580
+ const id = `${MEMORY_CONSTANTS.ID_PREFIX}${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
17581
+ const entry = {
17582
+ id,
17583
+ level,
17584
+ content: content.trim(),
17585
+ timestamp: Date.now(),
17586
+ importance,
17587
+ metadata
17588
+ };
17589
+ this.storage[level].push(entry);
17590
+ this.memories.set(id, level);
17591
+ this.storage[level].sort((a, b) => b.importance - a.importance || b.timestamp - a.timestamp);
17592
+ log(`[MemoryManager] Added ${level} memory: ${id.slice(0, 10)}...`);
17593
+ this.prune(level);
17594
+ return id;
17595
+ }
17596
+ /**
17597
+ * Retrieve memory for prompt construction
17598
+ */
17599
+ getContext(query) {
17600
+ let context = "";
17601
+ for (const level of ["system" /* SYSTEM */, "project" /* PROJECT */, "mission" /* MISSION */, "task" /* TASK */]) {
17602
+ const entries = this.storage[level];
17603
+ if (entries.length === 0) continue;
17604
+ context += `
17605
+ ### ${level.toUpperCase()} MEMORY
17606
+ `;
17607
+ const filteredEntries = query && this.config.enableRelevanceFiltering ? entries.filter((e) => this.isRelevant(e, query)) : entries;
17608
+ for (const entry of filteredEntries) {
17609
+ context += `- [${new Date(entry.timestamp).toISOString()}] ${entry.content}
17610
+ `;
17611
+ }
17612
+ }
17613
+ return context.trim();
17614
+ }
17615
+ /**
17616
+ * Simple keyword-based relevance check
17617
+ */
17618
+ isRelevant(entry, query) {
17619
+ if (entry.importance > 0.8) return true;
17620
+ const q = query.toLowerCase();
17621
+ const content = entry.content.toLowerCase();
17622
+ const keywords = q.split(/\s+/).filter((k) => k.length > 3);
17623
+ if (keywords.length === 0) return true;
17624
+ return keywords.some((k) => content.includes(k));
17625
+ }
17626
+ /**
17627
+ * Prune memory based on token budget (simplified)
17628
+ */
17629
+ prune(level) {
17630
+ const budget = this.config.tokenBudgets[level];
17631
+ let currentSize = this.storage[level].reduce((acc, e) => acc + e.content.length / 4, 0);
17632
+ while (currentSize > budget && this.storage[level].length > 0) {
17633
+ const removed = this.storage[level].pop();
17634
+ if (removed) {
17635
+ this.memories.delete(removed.id);
17636
+ currentSize -= removed.content.length / 4;
17637
+ }
17638
+ }
17639
+ }
17640
+ /**
17641
+ * Clear task memory (Short-term)
17642
+ */
17643
+ clearTaskMemory() {
17644
+ for (const entry of this.storage["task" /* TASK */]) {
17645
+ this.memories.delete(entry.id);
17646
+ }
17647
+ this.storage["task" /* TASK */] = [];
17648
+ log("[MemoryManager] Task memory cleared.");
17649
+ }
17650
+ /**
17651
+ * Export full memory state (for persistence)
17652
+ */
17653
+ export() {
17654
+ return { ...this.storage };
17655
+ }
17656
+ /**
17657
+ * Import memory state
17658
+ */
17659
+ import(snapshot) {
17660
+ this.storage = snapshot;
17661
+ this.memories.clear();
17662
+ for (const level of Object.values(MemoryLevel)) {
17663
+ for (const entry of this.storage[level]) {
17664
+ this.memories.set(entry.id, level);
17665
+ }
17666
+ }
17667
+ }
17668
+ };
17669
+
17670
+ // src/core/agents/agent-registry.ts
17671
+ import * as fs4 from "fs/promises";
17672
+ import * as path4 from "path";
17673
+ var AgentRegistry = class _AgentRegistry {
17674
+ static instance;
17675
+ agents = /* @__PURE__ */ new Map();
17676
+ directory = "";
17677
+ constructor() {
17678
+ for (const [name, def] of Object.entries(AGENTS)) {
17679
+ this.agents.set(name, def);
17680
+ }
17681
+ }
17682
+ static getInstance() {
17683
+ if (!_AgentRegistry.instance) {
17684
+ _AgentRegistry.instance = new _AgentRegistry();
17685
+ }
17686
+ return _AgentRegistry.instance;
17687
+ }
17688
+ setDirectory(dir) {
17689
+ this.directory = dir;
17690
+ this.loadCustomAgents();
17691
+ }
17692
+ /**
17693
+ * Get agent definition by name
17694
+ */
17695
+ getAgent(name) {
17696
+ return this.agents.get(name);
17697
+ }
17698
+ /**
17699
+ * List all available agent names
17700
+ */
17701
+ listAgents() {
17702
+ return Array.from(this.agents.keys());
17703
+ }
17704
+ /**
17705
+ * Add or update an agent definition
17706
+ */
17707
+ registerAgent(name, def) {
17708
+ this.agents.set(name, def);
17709
+ log(`[AgentRegistry] Registered agent: ${name}`);
17710
+ }
17711
+ /**
17712
+ * Load custom agents from .opencode/agents.json
17713
+ */
17714
+ async loadCustomAgents() {
17715
+ if (!this.directory) return;
17716
+ const agentsConfigPath = path4.join(this.directory, PATHS.AGENTS_CONFIG);
17717
+ try {
17718
+ const content = await fs4.readFile(agentsConfigPath, "utf-8");
17719
+ const customAgents = JSON.parse(content);
17720
+ if (typeof customAgents === "object" && customAgents !== null) {
17721
+ for (const [name, def] of Object.entries(customAgents)) {
17722
+ if (this.isValidAgentDefinition(def)) {
17723
+ this.registerAgent(name, def);
17724
+ } else {
17725
+ log(`[AgentRegistry] Invalid custom agent definition for: ${name}`);
17726
+ }
17727
+ }
17728
+ }
17729
+ } catch (error45) {
17730
+ if (error45.code !== "ENOENT") {
17731
+ log(`[AgentRegistry] Error loading custom agents: ${error45}`);
17732
+ }
17733
+ }
17734
+ }
17735
+ isValidAgentDefinition(def) {
17736
+ return typeof def.id === "string" && typeof def.description === "string" && typeof def.systemPrompt === "string" && typeof def.canWrite === "boolean" && typeof def.canBash === "boolean";
17737
+ }
17738
+ };
17739
+
17740
+ // src/core/agents/manager/task-launcher.ts
17741
+ var TaskLauncher = class {
17742
+ constructor(client, directory, store, concurrency, sessionPool2, onTaskError, startPolling) {
17743
+ this.client = client;
17744
+ this.directory = directory;
17745
+ this.store = store;
17746
+ this.concurrency = concurrency;
17747
+ this.sessionPool = sessionPool2;
17748
+ this.onTaskError = onTaskError;
17749
+ this.startPolling = startPolling;
17750
+ }
17751
+ /**
17752
+ * Unified launch method - handles both single and multiple tasks efficiently.
17753
+ * All session creations happen in parallel immediately.
17754
+ * Concurrency acquisition and prompt firing happen in the background.
17755
+ */
17756
+ async launch(inputs) {
17757
+ const isArray = Array.isArray(inputs);
17758
+ const taskInputs = isArray ? inputs : [inputs];
17759
+ if (taskInputs.length === 0) return isArray ? [] : null;
17760
+ const tasks = await Promise.all(taskInputs.map(
17761
+ (input) => this.prepareTask(input).catch(() => null)
17762
+ ));
17763
+ const successfulTasks = tasks.filter((t) => t !== null);
17764
+ successfulTasks.forEach((task) => {
17765
+ this.executeBackground(task).catch((error45) => {
17766
+ this.onTaskError(task.id, error45);
17767
+ });
17768
+ });
17769
+ if (successfulTasks.length > 0) {
17770
+ this.startPolling();
17771
+ }
17772
+ return isArray ? successfulTasks : successfulTasks[0] || null;
17773
+ }
17774
+ /**
17775
+ * Prepare task: Create session and registration without blocking on concurrency
17776
+ */
17777
+ async prepareTask(input) {
17778
+ const currentDepth = input.depth ?? 0;
17779
+ if (currentDepth >= PARALLEL_TASK.MAX_DEPTH) {
17780
+ throw new Error(`Maximum task depth (${PARALLEL_TASK.MAX_DEPTH}) reached. To prevent infinite recursion, no further sub-tasks can be spawned.`);
17781
+ }
17782
+ const session = await this.sessionPool.acquire(
17783
+ input.agent,
17784
+ input.parentSessionID,
17785
+ input.description
17786
+ );
17787
+ const sessionID = session.id;
17788
+ const taskId = `${ID_PREFIX.TASK}${crypto.randomUUID().slice(0, 8)}`;
17789
+ const task = {
17790
+ id: taskId,
17791
+ sessionID,
17792
+ parentSessionID: input.parentSessionID,
17793
+ description: input.description,
17794
+ prompt: input.prompt,
17795
+ agent: input.agent,
17796
+ status: TASK_STATUS.PENDING,
17797
+ // Start as PENDING
17798
+ startedAt: /* @__PURE__ */ new Date(),
17799
+ concurrencyKey: input.agent,
17800
+ depth: (input.depth ?? 0) + 1,
17801
+ mode: input.mode || "normal",
17802
+ groupID: input.groupID
17803
+ };
17804
+ this.store.set(taskId, task);
17805
+ this.store.trackPending(input.parentSessionID, taskId);
17806
+ taskWAL.log(WAL_ACTIONS.LAUNCH, task).catch(() => {
17807
+ });
17808
+ const toastManager = getTaskToastManager();
17809
+ if (toastManager) {
17810
+ toastManager.addTask({
17811
+ id: taskId,
17812
+ description: input.description,
17813
+ agent: input.agent,
17814
+ isBackground: true,
17815
+ parentSessionID: input.parentSessionID,
17816
+ sessionID
17817
+ });
17818
+ }
17819
+ presets_exports.sessionCreated(sessionID, input.agent);
17820
+ return task;
17821
+ }
17822
+ /**
17823
+ * Background execution: Acquire slot and fire prompt with auto-retry
17824
+ */
17825
+ async executeBackground(task) {
17826
+ let attempt = 1;
17827
+ while (true) {
17828
+ try {
17829
+ await this.concurrency.acquire(task.agent);
17830
+ task.status = TASK_STATUS.RUNNING;
17831
+ task.startedAt = /* @__PURE__ */ new Date();
17832
+ this.store.set(task.id, task);
17833
+ taskWAL.log(WAL_ACTIONS.LAUNCH, task).catch(() => {
17834
+ });
17835
+ const agentDef = AgentRegistry.getInstance().getAgent(task.agent);
17836
+ let finalPrompt = task.prompt;
17837
+ if (agentDef) {
17838
+ finalPrompt = `### AGENT ROLE: ${agentDef.id}
17839
+ ${agentDef.description}
17840
+
17841
+ ${agentDef.systemPrompt}
17842
+
17843
+ ${finalPrompt}`;
17844
+ }
17845
+ const memory = MemoryManager.getInstance().getContext(finalPrompt);
17846
+ const injectedPrompt = memory ? `${memory}
17847
+
17848
+ ${finalPrompt}` : finalPrompt;
17849
+ const wireAgent = Object.values(AGENT_NAMES).includes(task.agent) ? task.agent : AGENT_NAMES.COMMANDER;
17850
+ const promptPromise = this.client.session.prompt({
17851
+ path: { id: task.sessionID },
17852
+ body: {
17853
+ agent: wireAgent,
17854
+ tools: {
17855
+ [TOOL_NAMES.DELEGATE_TASK]: true,
17856
+ [TOOL_NAMES.GET_TASK_RESULT]: true,
17857
+ [TOOL_NAMES.LIST_TASKS]: true,
17858
+ [TOOL_NAMES.CANCEL_TASK]: true,
17859
+ [TOOL_NAMES.SKILL]: true,
17860
+ [TOOL_NAMES.RUN_COMMAND]: true
17861
+ },
17862
+ parts: [{ type: PART_TYPES.TEXT, text: injectedPrompt }]
17863
+ }
17864
+ });
17865
+ await Promise.race([
17866
+ promptPromise,
17867
+ new Promise(
17868
+ (_, reject) => setTimeout(() => reject(new Error("Session prompt execution timed out after 600s")), 6e5)
17869
+ )
17870
+ ]);
17871
+ return;
17872
+ } catch (error45) {
17873
+ this.concurrency.release(task.agent);
17874
+ const context = {
17875
+ sessionId: task.sessionID,
17876
+ taskId: task.id,
17877
+ agent: task.agent,
17878
+ error: error45 instanceof Error ? error45 : new Error(String(error45)),
17879
+ attempt,
17880
+ timestamp: /* @__PURE__ */ new Date()
17881
+ };
17882
+ const action = handleError(context);
17883
+ if (action.type === "retry") {
17884
+ log(`[AutoRetry] Task ${task.id} failed (attempt ${attempt}). Retrying in ${action.delay}ms...`);
17885
+ if (action.modifyPrompt) {
17886
+ task.prompt += `
17887
+
17888
+ ${action.modifyPrompt}`;
17889
+ }
17890
+ await new Promise((r) => setTimeout(r, action.delay));
17891
+ attempt++;
17892
+ continue;
17893
+ }
17894
+ throw error45;
17895
+ }
17896
+ }
17897
+ }
17898
+ };
17899
+
17900
+ // src/core/agents/manager/task-resumer.ts
17901
+ var TaskResumer = class {
17902
+ constructor(client, store, findBySession, startPolling, notifyParentIfAllComplete) {
17470
17903
  this.client = client;
17471
17904
  this.store = store;
17472
17905
  this.findBySession = findBySession;
@@ -17520,6 +17953,98 @@ var CONFIG = {
17520
17953
  POLL_INTERVAL_MS: PARALLEL_TASK.POLL_INTERVAL_MS
17521
17954
  };
17522
17955
 
17956
+ // src/core/progress/state-broadcaster.ts
17957
+ var StateBroadcaster = class _StateBroadcaster {
17958
+ static _instance;
17959
+ listeners = /* @__PURE__ */ new Set();
17960
+ currentState = null;
17961
+ constructor() {
17962
+ }
17963
+ static getInstance() {
17964
+ if (!_StateBroadcaster._instance) {
17965
+ _StateBroadcaster._instance = new _StateBroadcaster();
17966
+ }
17967
+ return _StateBroadcaster._instance;
17968
+ }
17969
+ subscribe(listener) {
17970
+ this.listeners.add(listener);
17971
+ if (this.currentState) {
17972
+ listener(this.currentState);
17973
+ }
17974
+ return () => this.listeners.delete(listener);
17975
+ }
17976
+ broadcast(state2) {
17977
+ this.currentState = state2;
17978
+ this.listeners.forEach((listener) => {
17979
+ try {
17980
+ listener(state2);
17981
+ } catch (error45) {
17982
+ }
17983
+ });
17984
+ }
17985
+ getCurrentState() {
17986
+ return this.currentState;
17987
+ }
17988
+ };
17989
+ var stateBroadcaster = StateBroadcaster.getInstance();
17990
+
17991
+ // src/core/progress/progress-notifier.ts
17992
+ var ProgressNotifier = class _ProgressNotifier {
17993
+ static _instance;
17994
+ manager = null;
17995
+ constructor() {
17996
+ stateBroadcaster.subscribe(this.handleStateChange.bind(this));
17997
+ }
17998
+ static getInstance() {
17999
+ if (!_ProgressNotifier._instance) {
18000
+ _ProgressNotifier._instance = new _ProgressNotifier();
18001
+ }
18002
+ return _ProgressNotifier._instance;
18003
+ }
18004
+ setManager(manager) {
18005
+ this.manager = manager;
18006
+ }
18007
+ /**
18008
+ * Poll current status from ParallelAgentManager and broadcast it
18009
+ */
18010
+ update() {
18011
+ if (!this.manager) return;
18012
+ const tasks = this.manager.getAllTasks();
18013
+ const running = tasks.filter((t) => t.status === TASK_STATUS.RUNNING);
18014
+ const completed = tasks.filter((t) => t.status === TASK_STATUS.COMPLETED);
18015
+ const total = tasks.length;
18016
+ const percentage = total > 0 ? Math.round(completed.length / total * 100) : 0;
18017
+ const state2 = {
18018
+ missionId: "current-mission",
18019
+ // Could be dynamic
18020
+ status: percentage === 100 ? "completed" : "executing",
18021
+ progress: {
18022
+ totalTasks: total,
18023
+ completedTasks: completed.length,
18024
+ percentage
18025
+ },
18026
+ activeAgents: running.map((t) => ({
18027
+ id: t.id,
18028
+ type: t.agent,
18029
+ status: t.status,
18030
+ currentTask: t.description
18031
+ })),
18032
+ todo: [],
18033
+ // Need to fetch from TodoEnforcer if possible
18034
+ lastUpdated: /* @__PURE__ */ new Date()
18035
+ };
18036
+ stateBroadcaster.broadcast(state2);
18037
+ }
18038
+ handleStateChange(state2) {
18039
+ if (state2.progress.percentage > 0 && state2.progress.percentage % 25 === 0) {
18040
+ const toastManager = getTaskToastManager();
18041
+ if (toastManager) {
18042
+ }
18043
+ }
18044
+ }
18045
+ };
18046
+ var progressNotifier = ProgressNotifier.getInstance();
18047
+
17523
18048
  // src/core/agents/manager/task-poller.ts
17524
18049
  var TaskPoller = class {
17525
18050
  constructor(client, store, concurrency, notifyParentIfAllComplete, scheduleCleanup, pruneExpiredTasks, onTaskComplete) {
@@ -17581,6 +18106,7 @@ var TaskPoller = class {
17581
18106
  log(`Poll error for task ${task.id}:`, error45);
17582
18107
  }
17583
18108
  }
18109
+ progressNotifier.update();
17584
18110
  } catch (error45) {
17585
18111
  log("Polling error:", error45);
17586
18112
  }
@@ -17619,6 +18145,7 @@ var TaskPoller = class {
17619
18145
  const duration3 = formatDuration(task.startedAt, task.completedAt);
17620
18146
  presets_exports.sessionCompleted(task.sessionID, duration3);
17621
18147
  log(`Completed ${task.id} (${duration3})`);
18148
+ progressNotifier.update();
17622
18149
  }
17623
18150
  async updateTaskProgress(task) {
17624
18151
  try {
@@ -17661,10 +18188,11 @@ var TaskPoller = class {
17661
18188
  // src/core/agents/manager/task-cleaner.ts
17662
18189
  init_store();
17663
18190
  var TaskCleaner = class {
17664
- constructor(client, store, concurrency) {
18191
+ constructor(client, store, concurrency, sessionPool2) {
17665
18192
  this.client = client;
17666
18193
  this.store = store;
17667
18194
  this.concurrency = concurrency;
18195
+ this.sessionPool = sessionPool2;
17668
18196
  }
17669
18197
  pruneExpiredTasks() {
17670
18198
  const now = Date.now();
@@ -17689,9 +18217,9 @@ var TaskCleaner = class {
17689
18217
  });
17690
18218
  }
17691
18219
  }
17692
- this.client.session.delete({ path: { id: task.sessionID } }).catch(() => {
18220
+ this.sessionPool.release(task.sessionID).catch(() => {
17693
18221
  });
17694
- clear(task.sessionID);
18222
+ clear2(task.sessionID);
17695
18223
  this.store.delete(taskId);
17696
18224
  taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
17697
18225
  });
@@ -17704,8 +18232,8 @@ var TaskCleaner = class {
17704
18232
  setTimeout(async () => {
17705
18233
  if (sessionID) {
17706
18234
  try {
17707
- await this.client.session.delete({ path: { id: sessionID } });
17708
- clear(sessionID);
18235
+ await this.sessionPool.release(sessionID);
18236
+ clear2(sessionID);
17709
18237
  } catch {
17710
18238
  }
17711
18239
  }
@@ -17832,6 +18360,7 @@ var EventHandler = class {
17832
18360
  if (this.onTaskComplete) {
17833
18361
  Promise.resolve(this.onTaskComplete(task)).catch((err) => log("Error in onTaskComplete callback:", err));
17834
18362
  }
18363
+ progressNotifier.update();
17835
18364
  log(`Task ${task.id} completed via session.idle event (${formatDuration(task.startedAt, task.completedAt)})`);
17836
18365
  }
17837
18366
  handleSessionDeleted(task) {
@@ -17851,10 +18380,378 @@ var EventHandler = class {
17851
18380
  this.store.delete(task.id);
17852
18381
  taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
17853
18382
  });
18383
+ progressNotifier.update();
17854
18384
  log(`Cleaned up deleted session task: ${task.id}`);
17855
18385
  }
17856
18386
  };
17857
18387
 
18388
+ // src/core/agents/session-pool.ts
18389
+ var DEFAULT_CONFIG = {
18390
+ maxPoolSizePerAgent: 5,
18391
+ idleTimeoutMs: 3e5,
18392
+ // 5 minutes
18393
+ maxReuseCount: 10,
18394
+ healthCheckIntervalMs: 6e4
18395
+ // 1 minute
18396
+ };
18397
+ var SessionPool = class _SessionPool {
18398
+ static _instance;
18399
+ pool = /* @__PURE__ */ new Map();
18400
+ // key: agentName
18401
+ sessionsById = /* @__PURE__ */ new Map();
18402
+ config;
18403
+ client;
18404
+ directory;
18405
+ healthCheckInterval = null;
18406
+ // Statistics
18407
+ stats = {
18408
+ reuseHits: 0,
18409
+ creationMisses: 0
18410
+ };
18411
+ constructor(client, directory, config2 = {}) {
18412
+ this.client = client;
18413
+ this.directory = directory;
18414
+ this.config = { ...DEFAULT_CONFIG, ...config2 };
18415
+ this.startHealthCheck();
18416
+ }
18417
+ static getInstance(client, directory, config2) {
18418
+ if (!_SessionPool._instance) {
18419
+ if (!client || !directory) {
18420
+ throw new Error("SessionPool requires client and directory on first call");
18421
+ }
18422
+ _SessionPool._instance = new _SessionPool(client, directory, config2);
18423
+ }
18424
+ return _SessionPool._instance;
18425
+ }
18426
+ /**
18427
+ * Acquire a session from the pool or create a new one.
18428
+ */
18429
+ async acquire(agentName, parentSessionID, description) {
18430
+ const poolKey = this.getPoolKey(agentName);
18431
+ const agentPool = this.pool.get(poolKey) || [];
18432
+ const available = agentPool.find((s) => !s.inUse && s.reuseCount < this.config.maxReuseCount);
18433
+ if (available) {
18434
+ available.inUse = true;
18435
+ available.lastUsedAt = /* @__PURE__ */ new Date();
18436
+ available.reuseCount++;
18437
+ this.stats.reuseHits++;
18438
+ log(`[SessionPool] Reusing session ${available.id.slice(0, 8)}... for ${agentName} (reuse #${available.reuseCount})`);
18439
+ return available;
18440
+ }
18441
+ this.stats.creationMisses++;
18442
+ return this.createSession(agentName, parentSessionID, description);
18443
+ }
18444
+ /**
18445
+ * Release a session back to the pool for reuse.
18446
+ */
18447
+ async release(sessionId) {
18448
+ const session = this.sessionsById.get(sessionId);
18449
+ if (!session) {
18450
+ log(`[SessionPool] Session ${sessionId.slice(0, 8)}... not found in pool`);
18451
+ return;
18452
+ }
18453
+ const now = Date.now();
18454
+ const age = now - session.createdAt.getTime();
18455
+ const idle = now - session.lastUsedAt.getTime();
18456
+ if (session.reuseCount >= this.config.maxReuseCount || age > this.config.idleTimeoutMs * 2) {
18457
+ await this.invalidate(sessionId);
18458
+ return;
18459
+ }
18460
+ const poolKey = this.getPoolKey(session.agentName);
18461
+ const agentPool = this.pool.get(poolKey) || [];
18462
+ const availableCount = agentPool.filter((s) => !s.inUse).length;
18463
+ if (availableCount >= this.config.maxPoolSizePerAgent) {
18464
+ const oldest = agentPool.filter((s) => !s.inUse).sort((a, b) => a.lastUsedAt.getTime() - b.lastUsedAt.getTime())[0];
18465
+ if (oldest) {
18466
+ await this.deleteSession(oldest.id);
18467
+ }
18468
+ }
18469
+ session.inUse = false;
18470
+ log(`[SessionPool] Released session ${sessionId.slice(0, 8)}... to pool`);
18471
+ }
18472
+ /**
18473
+ * Invalidate a session (remove from pool and delete).
18474
+ */
18475
+ async invalidate(sessionId) {
18476
+ const session = this.sessionsById.get(sessionId);
18477
+ if (!session) return;
18478
+ await this.deleteSession(sessionId);
18479
+ log(`[SessionPool] Invalidated session ${sessionId.slice(0, 8)}...`);
18480
+ }
18481
+ /**
18482
+ * Get current pool statistics.
18483
+ */
18484
+ getStats() {
18485
+ const byAgent = {};
18486
+ for (const [agentName, sessions] of this.pool.entries()) {
18487
+ const inUse = sessions.filter((s) => s.inUse).length;
18488
+ byAgent[agentName] = {
18489
+ total: sessions.length,
18490
+ inUse,
18491
+ available: sessions.length - inUse
18492
+ };
18493
+ }
18494
+ const allSessions = Array.from(this.sessionsById.values());
18495
+ const inUseCount = allSessions.filter((s) => s.inUse).length;
18496
+ return {
18497
+ totalSessions: allSessions.length,
18498
+ sessionsInUse: inUseCount,
18499
+ availableSessions: allSessions.length - inUseCount,
18500
+ reuseHits: this.stats.reuseHits,
18501
+ creationMisses: this.stats.creationMisses,
18502
+ byAgent
18503
+ };
18504
+ }
18505
+ /**
18506
+ * Cleanup stale sessions.
18507
+ */
18508
+ async cleanup() {
18509
+ const now = Date.now();
18510
+ let cleanedCount = 0;
18511
+ for (const [sessionId, session] of this.sessionsById.entries()) {
18512
+ if (session.inUse) continue;
18513
+ const idle = now - session.lastUsedAt.getTime();
18514
+ if (idle > this.config.idleTimeoutMs) {
18515
+ await this.deleteSession(sessionId);
18516
+ cleanedCount++;
18517
+ }
18518
+ }
18519
+ if (cleanedCount > 0) {
18520
+ log(`[SessionPool] Cleaned up ${cleanedCount} stale sessions`);
18521
+ }
18522
+ return cleanedCount;
18523
+ }
18524
+ /**
18525
+ * Shutdown the pool.
18526
+ */
18527
+ async shutdown() {
18528
+ log("[SessionPool] Shutting down...");
18529
+ if (this.healthCheckInterval) {
18530
+ clearInterval(this.healthCheckInterval);
18531
+ this.healthCheckInterval = null;
18532
+ }
18533
+ const deletePromises = Array.from(this.sessionsById.keys()).map(
18534
+ (id) => this.deleteSession(id).catch(() => {
18535
+ })
18536
+ );
18537
+ await Promise.all(deletePromises);
18538
+ this.pool.clear();
18539
+ this.sessionsById.clear();
18540
+ log("[SessionPool] Shutdown complete");
18541
+ }
18542
+ // =========================================================================
18543
+ // Private Methods
18544
+ // =========================================================================
18545
+ getPoolKey(agentName) {
18546
+ return agentName;
18547
+ }
18548
+ async createSession(agentName, parentSessionID, description) {
18549
+ log(`[SessionPool] Creating new session for ${agentName}`);
18550
+ const result = await Promise.race([
18551
+ this.client.session.create({
18552
+ body: {
18553
+ parentID: parentSessionID,
18554
+ title: `${PARALLEL_TASK.SESSION_TITLE_PREFIX}: ${description}`
18555
+ },
18556
+ query: { directory: this.directory }
18557
+ }),
18558
+ new Promise(
18559
+ (_, reject) => setTimeout(() => reject(new Error("Session creation timed out after 60s")), 6e4)
18560
+ )
18561
+ ]);
18562
+ if (result.error || !result.data?.id) {
18563
+ throw new Error(`Session creation failed: ${result.error || "No ID"}`);
18564
+ }
18565
+ const session = {
18566
+ id: result.data.id,
18567
+ agentName,
18568
+ projectDirectory: this.directory,
18569
+ createdAt: /* @__PURE__ */ new Date(),
18570
+ lastUsedAt: /* @__PURE__ */ new Date(),
18571
+ reuseCount: 0,
18572
+ inUse: true
18573
+ };
18574
+ const poolKey = this.getPoolKey(agentName);
18575
+ const agentPool = this.pool.get(poolKey) || [];
18576
+ agentPool.push(session);
18577
+ this.pool.set(poolKey, agentPool);
18578
+ this.sessionsById.set(session.id, session);
18579
+ return session;
18580
+ }
18581
+ async deleteSession(sessionId) {
18582
+ const session = this.sessionsById.get(sessionId);
18583
+ if (!session) return;
18584
+ this.sessionsById.delete(sessionId);
18585
+ const poolKey = this.getPoolKey(session.agentName);
18586
+ const agentPool = this.pool.get(poolKey);
18587
+ if (agentPool) {
18588
+ const idx = agentPool.findIndex((s) => s.id === sessionId);
18589
+ if (idx !== -1) {
18590
+ agentPool.splice(idx, 1);
18591
+ }
18592
+ if (agentPool.length === 0) {
18593
+ this.pool.delete(poolKey);
18594
+ }
18595
+ }
18596
+ try {
18597
+ await this.client.session.delete({ path: { id: sessionId } });
18598
+ } catch {
18599
+ }
18600
+ }
18601
+ startHealthCheck() {
18602
+ this.healthCheckInterval = setInterval(() => {
18603
+ this.cleanup().catch(() => {
18604
+ });
18605
+ }, this.config.healthCheckIntervalMs);
18606
+ this.healthCheckInterval.unref?.();
18607
+ }
18608
+ };
18609
+ var sessionPool = {
18610
+ getInstance: SessionPool.getInstance.bind(SessionPool)
18611
+ };
18612
+
18613
+ // src/core/progress/terminal-monitor.ts
18614
+ var TerminalMonitor = class _TerminalMonitor {
18615
+ static instance;
18616
+ lastLineCount = 0;
18617
+ active = false;
18618
+ constructor() {
18619
+ stateBroadcaster.subscribe(this.render.bind(this));
18620
+ }
18621
+ static getInstance() {
18622
+ if (!_TerminalMonitor.instance) {
18623
+ _TerminalMonitor.instance = new _TerminalMonitor();
18624
+ }
18625
+ return _TerminalMonitor.instance;
18626
+ }
18627
+ start() {
18628
+ this.active = true;
18629
+ }
18630
+ stop() {
18631
+ this.active = false;
18632
+ process.stdout.write("\n");
18633
+ }
18634
+ render(state2) {
18635
+ if (!this.active) return;
18636
+ const { COLORS, BAR_WIDTH, LABELS } = TUI_CONSTANTS;
18637
+ if (this.lastLineCount > 0) {
18638
+ process.stdout.write(`\x1B[${this.lastLineCount}A`);
18639
+ }
18640
+ let output = "";
18641
+ const filledWidth = Math.round(state2.progress.percentage / 100 * BAR_WIDTH);
18642
+ const bar = "\u2588".repeat(filledWidth) + "\u2591".repeat(BAR_WIDTH - filledWidth);
18643
+ output += `${COLORS.BOLD}${COLORS.PROGRESS}${LABELS.PROGRESS_TITLE}: [${bar}] ${state2.progress.percentage}% ${COLORS.RESET}
18644
+ `;
18645
+ output += `Tasks: ${state2.progress.completedTasks}/${state2.progress.totalTasks} completed
18646
+
18647
+ `;
18648
+ if (state2.activeAgents.length > 0) {
18649
+ output += `${COLORS.BOLD}${LABELS.AGENT_TITLE}:${COLORS.RESET}
18650
+ `;
18651
+ state2.activeAgents.forEach((agent) => {
18652
+ const desc = agent.currentTask && agent.currentTask.length > 50 ? agent.currentTask.substring(0, 47) + "..." : agent.currentTask || LABELS.IDLE;
18653
+ output += ` ${COLORS.AGENT}\u25CF${COLORS.RESET} ${COLORS.BOLD}${agent.type}${COLORS.RESET}: ${desc}
18654
+ `;
18655
+ });
18656
+ output += "\n";
18657
+ } else {
18658
+ output += `${COLORS.DIM}${LABELS.WAITING}${COLORS.RESET}
18659
+
18660
+ `;
18661
+ }
18662
+ const lines = output.split("\n");
18663
+ this.lastLineCount = lines.length - 1;
18664
+ process.stdout.write(output);
18665
+ }
18666
+ };
18667
+
18668
+ // src/core/loop/todo-manager.ts
18669
+ import { readFileSync, writeFileSync, existsSync as existsSync3 } from "node:fs";
18670
+ import { join as join5 } from "node:path";
18671
+ var TodoManager = class _TodoManager {
18672
+ static instance;
18673
+ directory = "";
18674
+ constructor() {
18675
+ }
18676
+ static getInstance() {
18677
+ if (!_TodoManager.instance) {
18678
+ _TodoManager.instance = new _TodoManager();
18679
+ }
18680
+ return _TodoManager.instance;
18681
+ }
18682
+ setDirectory(dir) {
18683
+ this.directory = dir;
18684
+ }
18685
+ /**
18686
+ * Update a specific TODO item by its text content
18687
+ */
18688
+ updateItem(searchText, newStatus) {
18689
+ const todoPath = join5(this.directory, PATHS.TODO);
18690
+ if (!existsSync3(todoPath)) return false;
18691
+ try {
18692
+ const content = readFileSync(todoPath, "utf-8");
18693
+ const lines = content.split("\n");
18694
+ const statusMap = {
18695
+ [TODO_CONSTANTS.STATUS.PENDING]: TODO_CONSTANTS.MARKERS.PENDING,
18696
+ [TODO_CONSTANTS.STATUS.COMPLETED]: TODO_CONSTANTS.MARKERS.COMPLETED,
18697
+ [TODO_CONSTANTS.STATUS.PROGRESS]: TODO_CONSTANTS.MARKERS.PROGRESS,
18698
+ [TODO_CONSTANTS.STATUS.FAILED]: TODO_CONSTANTS.MARKERS.FAILED
18699
+ };
18700
+ const marker = statusMap[newStatus] || TODO_CONSTANTS.MARKERS.PENDING;
18701
+ let updated = false;
18702
+ const newLines = lines.map((line) => {
18703
+ if (line.includes(searchText) && (line.includes(TODO_CONSTANTS.MARKERS.PENDING) || line.includes(TODO_CONSTANTS.MARKERS.PROGRESS) || line.includes(TODO_CONSTANTS.MARKERS.COMPLETED) || line.includes(TODO_CONSTANTS.MARKERS.FAILED))) {
18704
+ updated = true;
18705
+ return line.replace(/\[[ x\/\-]\]/, marker);
18706
+ }
18707
+ return line;
18708
+ });
18709
+ if (updated) {
18710
+ writeFileSync(todoPath, newLines.join("\n"), "utf-8");
18711
+ log(`[TodoManager] Updated item: "${searchText}" -> ${newStatus}`);
18712
+ return true;
18713
+ }
18714
+ return false;
18715
+ } catch (error45) {
18716
+ log(`[TodoManager] Error updating TODO: ${error45}`);
18717
+ return false;
18718
+ }
18719
+ }
18720
+ /**
18721
+ * Add a new sub-task under a parent task
18722
+ */
18723
+ addSubTask(parentText, subTaskText) {
18724
+ const todoPath = join5(this.directory, PATHS.TODO);
18725
+ if (!existsSync3(todoPath)) return false;
18726
+ try {
18727
+ const content = readFileSync(todoPath, "utf-8");
18728
+ const lines = content.split("\n");
18729
+ let parentIndex = -1;
18730
+ let parentIndent = "";
18731
+ for (let i = 0; i < lines.length; i++) {
18732
+ if (lines[i].includes(parentText)) {
18733
+ parentIndex = i;
18734
+ const match = lines[i].match(/^(\s*)/);
18735
+ parentIndent = match ? match[1] : "";
18736
+ break;
18737
+ }
18738
+ }
18739
+ if (parentIndex !== -1) {
18740
+ const subTaskIndent = parentIndent + " ";
18741
+ const newLine = `${subTaskIndent}- ${TODO_CONSTANTS.MARKERS.PENDING} ${subTaskText}`;
18742
+ lines.splice(parentIndex + 1, 0, newLine);
18743
+ writeFileSync(todoPath, lines.join("\n"), "utf-8");
18744
+ log(`[TodoManager] Added sub-task: "${subTaskText}" under "${parentText}"`);
18745
+ return true;
18746
+ }
18747
+ return false;
18748
+ } catch (error45) {
18749
+ log(`[TodoManager] Error adding sub-task: ${error45}`);
18750
+ return false;
18751
+ }
18752
+ }
18753
+ };
18754
+
17858
18755
  // src/core/agents/manager.ts
17859
18756
  var ParallelAgentManager = class _ParallelAgentManager {
17860
18757
  static _instance;
@@ -17862,6 +18759,7 @@ var ParallelAgentManager = class _ParallelAgentManager {
17862
18759
  client;
17863
18760
  directory;
17864
18761
  concurrency = new ConcurrencyController();
18762
+ sessionPool;
17865
18763
  // Composed components
17866
18764
  launcher;
17867
18765
  resumer;
@@ -17871,7 +18769,13 @@ var ParallelAgentManager = class _ParallelAgentManager {
17871
18769
  constructor(client, directory) {
17872
18770
  this.client = client;
17873
18771
  this.directory = directory;
17874
- this.cleaner = new TaskCleaner(client, this.store, this.concurrency);
18772
+ const memory = MemoryManager.getInstance();
18773
+ memory.add("system" /* SYSTEM */, CORE_PHILOSOPHY, 1);
18774
+ memory.add("project" /* PROJECT */, `Working directory: ${directory}`, 0.9);
18775
+ AgentRegistry.getInstance().setDirectory(directory);
18776
+ TodoManager.getInstance().setDirectory(directory);
18777
+ this.sessionPool = SessionPool.getInstance(client, directory);
18778
+ this.cleaner = new TaskCleaner(client, this.store, this.concurrency, this.sessionPool);
17875
18779
  this.poller = new TaskPoller(
17876
18780
  client,
17877
18781
  this.store,
@@ -17886,6 +18790,7 @@ var ParallelAgentManager = class _ParallelAgentManager {
17886
18790
  directory,
17887
18791
  this.store,
17888
18792
  this.concurrency,
18793
+ this.sessionPool,
17889
18794
  (taskId, error45) => this.handleTaskError(taskId, error45),
17890
18795
  () => this.poller.start()
17891
18796
  );
@@ -17906,6 +18811,8 @@ var ParallelAgentManager = class _ParallelAgentManager {
17906
18811
  (sessionID) => this.poller.validateSessionHasOutput(sessionID),
17907
18812
  (task) => this.handleTaskComplete(task)
17908
18813
  );
18814
+ progressNotifier.setManager(this);
18815
+ TerminalMonitor.getInstance().start();
17909
18816
  this.recoverActiveTasks().catch((err) => {
17910
18817
  log("Recovery error:", err);
17911
18818
  });
@@ -17924,7 +18831,9 @@ var ParallelAgentManager = class _ParallelAgentManager {
17924
18831
  // ========================================================================
17925
18832
  async launch(inputs) {
17926
18833
  this.cleaner.pruneExpiredTasks();
17927
- return this.launcher.launch(inputs);
18834
+ const result = await this.launcher.launch(inputs);
18835
+ progressNotifier.update();
18836
+ return result;
17928
18837
  }
17929
18838
  async resume(input) {
17930
18839
  return this.resumer.resume(input);
@@ -17958,6 +18867,7 @@ var ParallelAgentManager = class _ParallelAgentManager {
17958
18867
  this.cleaner.scheduleCleanup(taskId);
17959
18868
  taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
17960
18869
  });
18870
+ progressNotifier.update();
17961
18871
  log(`Cancelled ${taskId}`);
17962
18872
  return true;
17963
18873
  }
@@ -17992,6 +18902,8 @@ var ParallelAgentManager = class _ParallelAgentManager {
17992
18902
  cleanup() {
17993
18903
  this.poller.stop();
17994
18904
  this.store.clear();
18905
+ MemoryManager.getInstance().clearTaskMemory();
18906
+ TerminalMonitor.getInstance().stop();
17995
18907
  Promise.resolve().then(() => (init_store(), store_exports)).then((store) => store.clearAll()).catch(() => {
17996
18908
  });
17997
18909
  }
@@ -18021,6 +18933,7 @@ var ParallelAgentManager = class _ParallelAgentManager {
18021
18933
  this.store.untrackPending(task.parentSessionID, taskId);
18022
18934
  this.cleaner.notifyParentIfAllComplete(task.parentSessionID);
18023
18935
  this.cleaner.scheduleCleanup(taskId);
18936
+ progressNotifier.update();
18024
18937
  taskWAL.log(WAL_ACTIONS.UPDATE, task).catch(() => {
18025
18938
  });
18026
18939
  }
@@ -18047,6 +18960,7 @@ This task ensures the completeness of the unit before global integration.`,
18047
18960
  log(`[MSVP] Failed to trigger review for ${task.id}:`, error45);
18048
18961
  }
18049
18962
  }
18963
+ progressNotifier.update();
18050
18964
  }
18051
18965
  async recoverActiveTasks() {
18052
18966
  const tasks = await taskWAL.readAll();
@@ -18085,7 +18999,13 @@ This task ensures the completeness of the unit before global integration.`,
18085
18999
  }
18086
19000
  };
18087
19001
  var parallelAgentManager = {
18088
- getInstance: ParallelAgentManager.getInstance.bind(ParallelAgentManager)
19002
+ getInstance: ParallelAgentManager.getInstance.bind(ParallelAgentManager),
19003
+ cleanup: () => {
19004
+ try {
19005
+ ParallelAgentManager.getInstance().cleanup();
19006
+ } catch {
19007
+ }
19008
+ }
18089
19009
  };
18090
19010
 
18091
19011
  // src/tools/parallel/delegate-task.ts
@@ -18221,6 +19141,7 @@ ${PROMPT_TAGS.RESUME.close}
18221
19141
  ${PROMPT_TAGS.SAFETY.open}
18222
19142
  - Max 10 tasks per agent type (configurable)
18223
19143
  - Auto-timeout: 60 minutes
19144
+ - Use \`${TOOL_NAMES.LIST_AGENTS}\` to see all available agents (including custom ones).
18224
19145
  ${PROMPT_TAGS.SAFETY.close}`,
18225
19146
  args: {
18226
19147
  [PARALLEL_PARAMS.AGENT]: tool.schema.string().describe("Agent name"),
@@ -18352,7 +19273,7 @@ Session: \`${task.sessionID}\` (save for resume)`;
18352
19273
  if (pollResult.timedOut) {
18353
19274
  log(`${PARALLEL_LOG.DELEGATE_TASK} Sync: timed out`, pollResult);
18354
19275
  return `${OUTPUT_LABEL.TIMEOUT} after ${Math.floor(pollResult.elapsedMs / 1e3)}s (${pollResult.pollCount} polls)
18355
- Session: \`${sessionID}\` - Use get_task_result or resume later.`;
19276
+ Session: \`${sessionID}\` - Use ${TOOL_NAMES.GET_TASK_RESULT} or resume later.`;
18356
19277
  }
18357
19278
  const text = await extractSessionResult(session, sessionID);
18358
19279
  log(`${PARALLEL_LOG.DELEGATE_TASK} Sync: completed`, { sessionID, elapsedMs: pollResult.elapsedMs });
@@ -18383,59 +19304,201 @@ var createGetTaskResultTool = (manager) => tool({
18383
19304
  if (task.status === STATUS_LABEL.ERROR || task.status === STATUS_LABEL.TIMEOUT) {
18384
19305
  return `[${task.status.toUpperCase()}] ${task.error}`;
18385
19306
  }
18386
- return `${OUTPUT_LABEL.DONE} Completed in ${duration3}
19307
+ return `${OUTPUT_LABEL.DONE} Completed in ${duration3}
19308
+
19309
+ ${result || "(No output)"}`;
19310
+ }
19311
+ });
19312
+
19313
+ // src/tools/parallel/list-tasks.ts
19314
+ var createListTasksTool = (manager) => tool({
19315
+ description: `List all background tasks.`,
19316
+ args: {
19317
+ [PARALLEL_PARAMS.STATUS]: tool.schema.string().optional().describe("Filter: all, running, completed, error")
19318
+ },
19319
+ async execute(args) {
19320
+ const status = args[PARALLEL_PARAMS.STATUS] || STATUS_LABEL.ALL;
19321
+ let tasks;
19322
+ switch (status) {
19323
+ case STATUS_LABEL.RUNNING:
19324
+ tasks = manager.getRunningTasks();
19325
+ break;
19326
+ case STATUS_LABEL.COMPLETED:
19327
+ tasks = manager.getAllTasks().filter((t) => t.status === TASK_STATUS.COMPLETED);
19328
+ break;
19329
+ case STATUS_LABEL.ERROR:
19330
+ tasks = manager.getAllTasks().filter((t) => t.status === TASK_STATUS.ERROR || t.status === TASK_STATUS.TIMEOUT);
19331
+ break;
19332
+ default:
19333
+ tasks = manager.getAllTasks();
19334
+ }
19335
+ if (tasks.length === 0) return `No tasks found.`;
19336
+ const rows = tasks.map((t) => {
19337
+ const elapsed = Math.floor((Date.now() - t.startedAt.getTime()) / 1e3);
19338
+ return `| \`${t.id}\` | [${t.status.toUpperCase()}] | ${t.agent} | ${elapsed}s |`;
19339
+ }).join("\n");
19340
+ return `**Tasks List**
19341
+
19342
+ | ID | ${LOOP_LABELS.STATUS_TITLE || "Status"} | ${PARALLEL_PARAMS.AGENT.toUpperCase()} | Time |
19343
+ |----|--------|-------|------|
19344
+ ${rows}`;
19345
+ }
19346
+ });
19347
+
19348
+ // src/tools/parallel/cancel-task.ts
19349
+ var createCancelTaskTool = (manager) => tool({
19350
+ description: `Cancel a running task.`,
19351
+ args: {
19352
+ taskId: tool.schema.string().describe("Task ID to cancel")
19353
+ },
19354
+ async execute(args) {
19355
+ const cancelled = await manager.cancelTask(args.taskId);
19356
+ if (cancelled) return `${OUTPUT_LABEL.CANCELLED} task: \`${args.taskId}\``;
19357
+ const task = manager.getTask(args.taskId);
19358
+ if (task) return `${OUTPUT_LABEL.WARNING} Cannot cancel: Task is ${task.status}`;
19359
+ return `${OUTPUT_LABEL.ERROR} Not found: \`${args.taskId}\``;
19360
+ }
19361
+ });
19362
+
19363
+ // src/tools/parallel/list-agents.ts
19364
+ var createListAgentsTool = () => tool({
19365
+ description: "List all available agents for delegation, including built-in and custom templates.",
19366
+ args: {},
19367
+ async execute() {
19368
+ const registry2 = AgentRegistry.getInstance();
19369
+ const agentNames = registry2.listAgents();
19370
+ if (agentNames.length === 0) {
19371
+ return `${OUTPUT_LABEL.INFO} No agents registered.`;
19372
+ }
19373
+ let output = `${OUTPUT_LABEL.INFO} Available Agents:
19374
+
19375
+ `;
19376
+ for (const name of agentNames) {
19377
+ const def = registry2.getAgent(name);
19378
+ if (def) {
19379
+ output += `- **${name}**: ${def.description}
19380
+ `;
19381
+ }
19382
+ }
19383
+ return output;
19384
+ }
19385
+ });
19386
+
19387
+ // src/core/metrics/collector.ts
19388
+ var MetricsCollector = class _MetricsCollector {
19389
+ static instance;
19390
+ agentLatencies = /* @__PURE__ */ new Map();
19391
+ toolLatencies = /* @__PURE__ */ new Map();
19392
+ tokenUsage = 0;
19393
+ lineCount = 0;
19394
+ tasks = [];
19395
+ constructor() {
19396
+ }
19397
+ static getInstance() {
19398
+ if (!_MetricsCollector.instance) {
19399
+ _MetricsCollector.instance = new _MetricsCollector();
19400
+ }
19401
+ return _MetricsCollector.instance;
19402
+ }
19403
+ recordAgentExecution(agent, duration3) {
19404
+ const latencies = this.agentLatencies.get(agent) || [];
19405
+ latencies.push(duration3);
19406
+ this.agentLatencies.set(agent, latencies);
19407
+ }
19408
+ recordToolExecution(tool2, duration3) {
19409
+ const latencies = this.toolLatencies.get(tool2) || [];
19410
+ latencies.push(duration3);
19411
+ this.toolLatencies.set(tool2, latencies);
19412
+ }
19413
+ recordTokenUsage(tokens) {
19414
+ this.tokenUsage += tokens;
19415
+ }
19416
+ recordTaskResult(id, success2) {
19417
+ this.tasks.push({ id, success: success2 });
19418
+ }
19419
+ recordLinesProduced(lines) {
19420
+ this.lineCount += lines;
19421
+ }
19422
+ getStats() {
19423
+ const avgAgentLatency = {};
19424
+ for (const [agent, lats] of this.agentLatencies.entries()) {
19425
+ avgAgentLatency[agent] = Math.round(lats.reduce((a, b) => a + b, 0) / lats.length);
19426
+ }
19427
+ const avgToolLatency = {};
19428
+ for (const [tool2, lats] of this.toolLatencies.entries()) {
19429
+ avgToolLatency[tool2] = Math.round(lats.reduce((a, b) => a + b, 0) / lats.length);
19430
+ }
19431
+ const successfulTasks = this.tasks.filter((t) => t.success).length;
19432
+ return {
19433
+ avgAgentLatency,
19434
+ avgToolLatency,
19435
+ tokenUsage: this.tokenUsage,
19436
+ efficiency: this.lineCount > 0 ? this.tokenUsage / this.lineCount : 0,
19437
+ totalTasks: this.tasks.length,
19438
+ successRate: this.tasks.length > 0 ? successfulTasks / this.tasks.length : 0
19439
+ };
19440
+ }
19441
+ };
19442
+
19443
+ // src/tools/parallel/show-metrics.ts
19444
+ var createShowMetricsTool = () => tool({
19445
+ description: "Display a performance dashboard for the current mission, showing tool/agent latency and token usage.",
19446
+ args: {},
19447
+ async execute() {
19448
+ const stats2 = MetricsCollector.getInstance().getStats();
19449
+ let output = `${OUTPUT_LABEL.INFO} **Performance Dashboard**
19450
+
19451
+ `;
19452
+ output += `### \u23F1\uFE0F Latency (Average)
19453
+
19454
+ `;
19455
+ output += `**Tools:**
19456
+ `;
19457
+ for (const [tool2, lat] of Object.entries(stats2.avgToolLatency)) {
19458
+ output += `- \`${tool2}\`: ${lat}ms
19459
+ `;
19460
+ }
19461
+ output += `
19462
+ **Agents:**
19463
+ `;
19464
+ for (const [agent, lat] of Object.entries(stats2.avgAgentLatency)) {
19465
+ output += `- \`${agent}\`: ${lat}ms
19466
+ `;
19467
+ }
19468
+ output += `
19469
+ ### \u{1FA99} Resource Usage
18387
19470
 
18388
- ${result || "(No output)"}`;
19471
+ `;
19472
+ output += `- **Total Tokens (Est.)**: ${Math.round(stats2.tokenUsage).toLocaleString()}
19473
+ `;
19474
+ output += `- **Mission Success Rate**: ${Math.round(stats2.successRate * 100)}%
19475
+ `;
19476
+ output += `- **Total Sub-tasks**: ${stats2.totalTasks}
19477
+ `;
19478
+ return output;
18389
19479
  }
18390
19480
  });
18391
19481
 
18392
- // src/tools/parallel/list-tasks.ts
18393
- var createListTasksTool = (manager) => tool({
18394
- description: `List all background tasks.`,
19482
+ // src/tools/parallel/update-todo.ts
19483
+ var createUpdateTodoTool = () => tool({
19484
+ description: "Update the status of a task in .opencode/todo.md or add a sub-task. Use this for incremental updates instead of rewriting the whole file.",
18395
19485
  args: {
18396
- [PARALLEL_PARAMS.STATUS]: tool.schema.string().optional().describe("Filter: all, running, completed, error")
19486
+ action: tool.schema.enum(["update", "add"]).describe("Action to perform"),
19487
+ task: tool.schema.string().describe("Text content of the task to update or the parent task to add under"),
19488
+ status: tool.schema.enum(Object.values(TODO_CONSTANTS.STATUS)).optional().describe("New status (for 'update' action)"),
19489
+ subtask: tool.schema.string().optional().describe("Description of the new sub-task (for 'add' action)")
18397
19490
  },
18398
19491
  async execute(args) {
18399
- const status = args[PARALLEL_PARAMS.STATUS] || STATUS_LABEL.ALL;
18400
- let tasks;
18401
- switch (status) {
18402
- case STATUS_LABEL.RUNNING:
18403
- tasks = manager.getRunningTasks();
18404
- break;
18405
- case STATUS_LABEL.COMPLETED:
18406
- tasks = manager.getAllTasks().filter((t) => t.status === TASK_STATUS.COMPLETED);
18407
- break;
18408
- case STATUS_LABEL.ERROR:
18409
- tasks = manager.getAllTasks().filter((t) => t.status === TASK_STATUS.ERROR || t.status === TASK_STATUS.TIMEOUT);
18410
- break;
18411
- default:
18412
- tasks = manager.getAllTasks();
19492
+ const manager = TodoManager.getInstance();
19493
+ if (args.action === "update") {
19494
+ if (!args.status) return `${OUTPUT_LABEL.ERROR} 'status' is required for update action.`;
19495
+ const success2 = manager.updateItem(args.task, args.status);
19496
+ return success2 ? `${OUTPUT_LABEL.DONE} Updated task status: "${args.task}" -> ${args.status}` : `${OUTPUT_LABEL.ERROR} Task not found: "${args.task}"`;
19497
+ } else {
19498
+ if (!args.subtask) return `${OUTPUT_LABEL.ERROR} 'subtask' is required for add action.`;
19499
+ const success2 = manager.addSubTask(args.task, args.subtask);
19500
+ return success2 ? `${OUTPUT_LABEL.DONE} Added sub-task: "${args.subtask}" under "${args.task}"` : `${OUTPUT_LABEL.ERROR} Parent task not found: "${args.task}"`;
18413
19501
  }
18414
- if (tasks.length === 0) return `No tasks found.`;
18415
- const rows = tasks.map((t) => {
18416
- const elapsed = Math.floor((Date.now() - t.startedAt.getTime()) / 1e3);
18417
- return `| \`${t.id}\` | [${t.status.toUpperCase()}] | ${t.agent} | ${elapsed}s |`;
18418
- }).join("\n");
18419
- return `**Tasks List**
18420
-
18421
- | ID | ${LOOP_LABELS.STATUS_TITLE || "Status"} | ${PARALLEL_PARAMS.AGENT.toUpperCase()} | Time |
18422
- |----|--------|-------|------|
18423
- ${rows}`;
18424
- }
18425
- });
18426
-
18427
- // src/tools/parallel/cancel-task.ts
18428
- var createCancelTaskTool = (manager) => tool({
18429
- description: `Cancel a running task.`,
18430
- args: {
18431
- taskId: tool.schema.string().describe("Task ID to cancel")
18432
- },
18433
- async execute(args) {
18434
- const cancelled = await manager.cancelTask(args.taskId);
18435
- if (cancelled) return `${OUTPUT_LABEL.CANCELLED} task: \`${args.taskId}\``;
18436
- const task = manager.getTask(args.taskId);
18437
- if (task) return `${OUTPUT_LABEL.WARNING} Cannot cancel: Task is ${task.status}`;
18438
- return `${OUTPUT_LABEL.ERROR} Not found: \`${args.taskId}\``;
18439
19502
  }
18440
19503
  });
18441
19504
 
@@ -18445,7 +19508,10 @@ function createAsyncAgentTools(manager, client) {
18445
19508
  [TOOL_NAMES.DELEGATE_TASK]: createDelegateTaskTool(manager, client),
18446
19509
  [TOOL_NAMES.GET_TASK_RESULT]: createGetTaskResultTool(manager),
18447
19510
  [TOOL_NAMES.LIST_TASKS]: createListTasksTool(manager),
18448
- [TOOL_NAMES.CANCEL_TASK]: createCancelTaskTool(manager)
19511
+ [TOOL_NAMES.CANCEL_TASK]: createCancelTaskTool(manager),
19512
+ [TOOL_NAMES.LIST_AGENTS]: createListAgentsTool(),
19513
+ [TOOL_NAMES.SHOW_METRICS]: createShowMetricsTool(),
19514
+ [TOOL_NAMES.UPDATE_TODO]: createUpdateTodoTool()
18449
19515
  };
18450
19516
  }
18451
19517
 
@@ -18499,15 +19565,15 @@ var METADATA_FILE = PATHS.DOC_METADATA;
18499
19565
  var DEFAULT_TTL_MS = CACHE.DEFAULT_TTL_MS;
18500
19566
 
18501
19567
  // src/core/cache/operations.ts
18502
- import * as fs5 from "node:fs/promises";
18503
- import * as path4 from "node:path";
19568
+ import * as fs6 from "node:fs/promises";
19569
+ import * as path5 from "node:path";
18504
19570
 
18505
19571
  // src/core/cache/utils.ts
18506
- import * as fs4 from "node:fs/promises";
18507
- import { existsSync as existsSync3 } from "node:fs";
19572
+ import * as fs5 from "node:fs/promises";
19573
+ import { existsSync as existsSync4 } from "node:fs";
18508
19574
  async function ensureCacheDir() {
18509
- if (!existsSync3(CACHE_DIR)) {
18510
- await fs4.mkdir(CACHE_DIR, { recursive: true });
19575
+ if (!existsSync4(CACHE_DIR)) {
19576
+ await fs5.mkdir(CACHE_DIR, { recursive: true });
18511
19577
  }
18512
19578
  }
18513
19579
  function urlToFilename(url2) {
@@ -18522,7 +19588,7 @@ function urlToFilename(url2) {
18522
19588
  }
18523
19589
  async function readMetadata() {
18524
19590
  try {
18525
- const content = await fs4.readFile(METADATA_FILE, "utf-8");
19591
+ const content = await fs5.readFile(METADATA_FILE, "utf-8");
18526
19592
  return JSON.parse(content);
18527
19593
  } catch {
18528
19594
  return { documents: {}, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() };
@@ -18531,7 +19597,7 @@ async function readMetadata() {
18531
19597
  async function writeMetadata(metadata) {
18532
19598
  await ensureCacheDir();
18533
19599
  metadata.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
18534
- await fs4.writeFile(METADATA_FILE, JSON.stringify(metadata, null, 2));
19600
+ await fs5.writeFile(METADATA_FILE, JSON.stringify(metadata, null, 2));
18535
19601
  }
18536
19602
 
18537
19603
  // src/core/cache/operations.ts
@@ -18545,8 +19611,8 @@ async function get2(url2) {
18545
19611
  return null;
18546
19612
  }
18547
19613
  try {
18548
- const filepath = path4.join(CACHE_DIR, filename);
18549
- const content = await fs5.readFile(filepath, "utf-8");
19614
+ const filepath = path5.join(CACHE_DIR, filename);
19615
+ const content = await fs6.readFile(filepath, "utf-8");
18550
19616
  return { ...entry, content };
18551
19617
  } catch {
18552
19618
  return null;
@@ -18557,8 +19623,8 @@ async function getByFilename(filename) {
18557
19623
  const entry = metadata.documents[filename];
18558
19624
  if (!entry) return null;
18559
19625
  try {
18560
- const filepath = path4.join(CACHE_DIR, filename);
18561
- const content = await fs5.readFile(filepath, "utf-8");
19626
+ const filepath = path5.join(CACHE_DIR, filename);
19627
+ const content = await fs6.readFile(filepath, "utf-8");
18562
19628
  return { ...entry, content };
18563
19629
  } catch {
18564
19630
  return null;
@@ -18567,7 +19633,7 @@ async function getByFilename(filename) {
18567
19633
  async function set2(url2, content, title, ttlMs = DEFAULT_TTL_MS) {
18568
19634
  await ensureCacheDir();
18569
19635
  const filename = urlToFilename(url2);
18570
- const filepath = path4.join(CACHE_DIR, filename);
19636
+ const filepath = path5.join(CACHE_DIR, filename);
18571
19637
  const now = /* @__PURE__ */ new Date();
18572
19638
  const header = `# ${title}
18573
19639
 
@@ -18578,7 +19644,7 @@ async function set2(url2, content, title, ttlMs = DEFAULT_TTL_MS) {
18578
19644
 
18579
19645
  `;
18580
19646
  const fullContent = header + content;
18581
- await fs5.writeFile(filepath, fullContent);
19647
+ await fs6.writeFile(filepath, fullContent);
18582
19648
  const metadata = await readMetadata();
18583
19649
  metadata.documents[filename] = {
18584
19650
  url: url2,
@@ -18592,9 +19658,9 @@ async function set2(url2, content, title, ttlMs = DEFAULT_TTL_MS) {
18592
19658
  }
18593
19659
  async function remove(url2) {
18594
19660
  const filename = urlToFilename(url2);
18595
- const filepath = path4.join(CACHE_DIR, filename);
19661
+ const filepath = path5.join(CACHE_DIR, filename);
18596
19662
  try {
18597
- await fs5.unlink(filepath);
19663
+ await fs6.unlink(filepath);
18598
19664
  const metadata = await readMetadata();
18599
19665
  delete metadata.documents[filename];
18600
19666
  await writeMetadata(metadata);
@@ -18612,13 +19678,13 @@ async function list() {
18612
19678
  expired: new Date(entry.expiresAt) < now
18613
19679
  }));
18614
19680
  }
18615
- async function clear2() {
19681
+ async function clear3() {
18616
19682
  const metadata = await readMetadata();
18617
19683
  const count = Object.keys(metadata.documents).length;
18618
19684
  for (const filename of Object.keys(metadata.documents)) {
18619
- const filepath = path4.join(CACHE_DIR, filename);
19685
+ const filepath = path5.join(CACHE_DIR, filename);
18620
19686
  try {
18621
- await fs5.unlink(filepath);
19687
+ await fs6.unlink(filepath);
18622
19688
  } catch {
18623
19689
  }
18624
19690
  }
@@ -19096,7 +20162,7 @@ Cached: ${doc.fetchedAt}
19096
20162
  ${doc.content}`;
19097
20163
  }
19098
20164
  case CACHE_ACTIONS.CLEAR: {
19099
- const count = await clear2();
20165
+ const count = await clear3();
19100
20166
  return `Cleared ${count} cached documents`;
19101
20167
  }
19102
20168
  case CACHE_ACTIONS.STATS: {
@@ -19276,6 +20342,60 @@ ${r.content}
19276
20342
  }
19277
20343
  });
19278
20344
 
20345
+ // src/tools/lsp/diagnostics-cache.ts
20346
+ import fs7 from "fs/promises";
20347
+ import path6 from "path";
20348
+ var DiagnosticsCache = class {
20349
+ cache = /* @__PURE__ */ new Map();
20350
+ defaultTTL = 3e4;
20351
+ // 30 seconds
20352
+ async get(directory, file2) {
20353
+ const key = this.getCacheKey(directory, file2);
20354
+ const cached2 = this.cache.get(key);
20355
+ if (!cached2) return null;
20356
+ if (Date.now() - cached2.timestamp > this.defaultTTL) {
20357
+ this.cache.delete(key);
20358
+ return null;
20359
+ }
20360
+ try {
20361
+ const currentMtime = await this.getFilesMtime(directory, file2);
20362
+ if (currentMtime > cached2.filesMtime) {
20363
+ this.cache.delete(key);
20364
+ return null;
20365
+ }
20366
+ } catch (error45) {
20367
+ this.cache.delete(key);
20368
+ return null;
20369
+ }
20370
+ log(`[DiagnosticsCache] Cache hit for ${file2 || "all files"}`);
20371
+ return cached2.diagnostics;
20372
+ }
20373
+ async set(directory, file2, diagnostics) {
20374
+ try {
20375
+ const key = this.getCacheKey(directory, file2);
20376
+ const filesMtime = await this.getFilesMtime(directory, file2);
20377
+ this.cache.set(key, {
20378
+ diagnostics,
20379
+ timestamp: Date.now(),
20380
+ filesMtime
20381
+ });
20382
+ } catch (error45) {
20383
+ }
20384
+ }
20385
+ getCacheKey(directory, file2) {
20386
+ return file2 ? `${directory}:${file2}` : directory;
20387
+ }
20388
+ async getFilesMtime(directory, file2) {
20389
+ if (file2 && file2 !== "*") {
20390
+ const stats3 = await fs7.stat(path6.join(directory, file2));
20391
+ return stats3.mtimeMs;
20392
+ }
20393
+ const stats2 = await fs7.stat(directory);
20394
+ return stats2.mtimeMs;
20395
+ }
20396
+ };
20397
+ var diagnosticsCache = new DiagnosticsCache();
20398
+
19279
20399
  // src/tools/lsp/index.ts
19280
20400
  var lspDiagnosticsTool = (directory) => tool({
19281
20401
  description: `Get LSP diagnostics (errors/warnings) for files.
@@ -19293,11 +20413,17 @@ Runs TypeScript compiler and/or ESLint to find issues.
19293
20413
  include_warnings: tool.schema.boolean().optional().describe("Include warnings (default: true)")
19294
20414
  },
19295
20415
  async execute(args) {
19296
- return callRustTool("lsp_diagnostics", {
20416
+ const cached2 = await diagnosticsCache.get(directory, args.file);
20417
+ if (cached2) return cached2;
20418
+ const result = await callRustTool("lsp_diagnostics", {
19297
20419
  directory,
19298
20420
  file: args.file,
19299
20421
  include_warnings: args.include_warnings
19300
20422
  });
20423
+ if (result) {
20424
+ await diagnosticsCache.set(directory, args.file, result);
20425
+ }
20426
+ return result;
19301
20427
  }
19302
20428
  });
19303
20429
 
@@ -19316,16 +20442,7 @@ var HOOK_ACTIONS = {
19316
20442
  PROCESS: "process",
19317
20443
  INTERCEPT: "intercept"
19318
20444
  };
19319
- var HOOK_NAMES = {
19320
- SANITY_CHECK: "SanityCheck",
19321
- MISSION_LOOP: "MissionLoop",
19322
- STRICT_ROLE_GUARD: "StrictRoleGuard",
19323
- SECRET_SCANNER: "SecretScanner",
19324
- AGENT_UI: "AgentUI",
19325
- RESOURCE_CONTROL: "ResourceControl",
19326
- SLASH_COMMAND: "SlashCommandDispatcher",
19327
- USER_ACTIVITY: "UserActivity"
19328
- };
20445
+ var HOOK_NAMES2 = HOOK_NAMES;
19329
20446
 
19330
20447
  // src/hooks/registry.ts
19331
20448
  var HookRegistry = class _HookRegistry {
@@ -19548,23 +20665,25 @@ function ensureSessionInitialized(sessions, sessionID) {
19548
20665
  }
19549
20666
  return sessions.get(sessionID);
19550
20667
  }
19551
- function activateMissionState(sessionID) {
20668
+ function ensureGlobalState(sessionID) {
19552
20669
  let stateSession = state.sessions.get(sessionID);
19553
20670
  if (!stateSession) {
19554
- state.sessions.set(sessionID, {
20671
+ const newState = {
19555
20672
  enabled: true,
19556
20673
  iterations: 0,
19557
20674
  taskRetries: /* @__PURE__ */ new Map(),
19558
20675
  currentTask: "",
19559
20676
  anomalyCount: 0
19560
- });
19561
- stateSession = state.sessions.get(sessionID);
19562
- log(`[SessionManager] Created new global mission state for ${sessionID}`);
19563
- }
19564
- if (stateSession) {
19565
- stateSession.enabled = true;
19566
- stateSession.anomalyCount = 0;
20677
+ };
20678
+ state.sessions.set(sessionID, newState);
20679
+ return newState;
19567
20680
  }
20681
+ return stateSession;
20682
+ }
20683
+ function activateMissionState(sessionID) {
20684
+ const stateSession = ensureGlobalState(sessionID);
20685
+ stateSession.enabled = true;
20686
+ stateSession.anomalyCount = 0;
19568
20687
  state.missionActive = true;
19569
20688
  log(`[SessionManager] Mission Activated: ${sessionID}`);
19570
20689
  }
@@ -19587,18 +20706,16 @@ function updateSessionTokens(sessions, sessionID, inputLen, outputLen) {
19587
20706
  session.tokens.estimatedCost = Number(cost.toFixed(4));
19588
20707
  }
19589
20708
  function recordAnomaly(sessionID) {
19590
- const session = ensureSessionInitialized(state.sessions, sessionID);
20709
+ const session = ensureGlobalState(sessionID);
19591
20710
  session.anomalyCount = (session.anomalyCount || 0) + 1;
19592
20711
  return session.anomalyCount;
19593
20712
  }
19594
20713
  function resetAnomaly(sessionID) {
19595
- const session = ensureSessionInitialized(state.sessions, sessionID);
19596
- if (session.anomalyCount > 0) {
19597
- session.anomalyCount = 0;
19598
- }
20714
+ const session = ensureGlobalState(sessionID);
20715
+ session.anomalyCount = 0;
19599
20716
  }
19600
20717
  function updateCurrentTask(sessionID, taskID) {
19601
- const session = ensureSessionInitialized(state.sessions, sessionID);
20718
+ const session = ensureGlobalState(sessionID);
19602
20719
  session.currentTask = taskID;
19603
20720
  }
19604
20721
 
@@ -19673,7 +20790,7 @@ var CONTINUE_INSTRUCTION = `<auto_continue>
19673
20790
 
19674
20791
  // src/hooks/features/sanity-check.ts
19675
20792
  var SanityCheckHook = class {
19676
- name = HOOK_NAMES.SANITY_CHECK;
20793
+ name = HOOK_NAMES2.SANITY_CHECK;
19677
20794
  async execute(ctx, toolOrText, input, output) {
19678
20795
  if (output) {
19679
20796
  if (toolOrText === TOOL_NAMES.CALL_AGENT) {
@@ -19713,20 +20830,20 @@ var SanityCheckHook = class {
19713
20830
  };
19714
20831
 
19715
20832
  // src/core/loop/mission-loop.ts
19716
- import { existsSync as existsSync4, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
19717
- import { join as join5 } from "node:path";
20833
+ import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, mkdirSync } from "node:fs";
20834
+ import { join as join7 } from "node:path";
19718
20835
  var STATE_FILE = MISSION_CONTROL.STATE_FILE;
19719
20836
  var DEFAULT_MAX_ITERATIONS = MISSION_CONTROL.DEFAULT_MAX_ITERATIONS;
19720
20837
  function getStateFilePath(directory) {
19721
- return join5(directory, PATHS.OPENCODE, STATE_FILE);
20838
+ return join7(directory, PATHS.OPENCODE, STATE_FILE);
19722
20839
  }
19723
20840
  function readLoopState(directory) {
19724
20841
  const filePath = getStateFilePath(directory);
19725
- if (!existsSync4(filePath)) {
20842
+ if (!existsSync5(filePath)) {
19726
20843
  return null;
19727
20844
  }
19728
20845
  try {
19729
- const content = readFileSync(filePath, "utf-8");
20846
+ const content = readFileSync2(filePath, "utf-8");
19730
20847
  return JSON.parse(content);
19731
20848
  } catch (error45) {
19732
20849
  log(`[${MISSION_CONTROL.LOG_SOURCE}] Failed to read state: ${error45}`);
@@ -19735,12 +20852,12 @@ function readLoopState(directory) {
19735
20852
  }
19736
20853
  function writeLoopState(directory, state2) {
19737
20854
  const filePath = getStateFilePath(directory);
19738
- const dirPath = join5(directory, PATHS.OPENCODE);
20855
+ const dirPath = join7(directory, PATHS.OPENCODE);
19739
20856
  try {
19740
- if (!existsSync4(dirPath)) {
20857
+ if (!existsSync5(dirPath)) {
19741
20858
  mkdirSync(dirPath, { recursive: true });
19742
20859
  }
19743
- writeFileSync(filePath, JSON.stringify(state2, null, 2), "utf-8");
20860
+ writeFileSync2(filePath, JSON.stringify(state2, null, 2), "utf-8");
19744
20861
  return true;
19745
20862
  } catch (error45) {
19746
20863
  log(`[${MISSION_CONTROL.LOG_SOURCE}] Failed to write state: ${error45}`);
@@ -19749,7 +20866,7 @@ function writeLoopState(directory, state2) {
19749
20866
  }
19750
20867
  function clearLoopState(directory) {
19751
20868
  const filePath = getStateFilePath(directory);
19752
- if (!existsSync4(filePath)) {
20869
+ if (!existsSync5(filePath)) {
19753
20870
  return false;
19754
20871
  }
19755
20872
  try {
@@ -19781,6 +20898,7 @@ function startMissionLoop(directory, sessionID, prompt, options = {}) {
19781
20898
  };
19782
20899
  const success2 = writeLoopState(directory, state2);
19783
20900
  if (success2) {
20901
+ TerminalMonitor.getInstance().start();
19784
20902
  log(`[${MISSION_CONTROL.LOG_SOURCE}] Loop started`, {
19785
20903
  sessionID,
19786
20904
  maxIterations: state2.maxIterations
@@ -19807,29 +20925,12 @@ function generateMissionContinuationPrompt(state2, verificationSummary) {
19807
20925
  const summaryHeader = verificationSummary ? `
19808
20926
  [Verification Status]: ${verificationSummary}
19809
20927
  ` : "";
19810
- return `<mission_loop iteration="${state2.iteration}" max="${state2.maxIterations}">
20928
+ return `${CONTINUE_INSTRUCTION}
20929
+
20930
+ <mission_loop iteration="${state2.iteration}" max="${state2.maxIterations}">
19811
20931
  \u26A0\uFE0F **MISSION NOT COMPLETE** - Iteration ${state2.iteration}/${state2.maxIterations}
19812
20932
  ${summaryHeader}
19813
20933
 
19814
- The mission is INCOMPLETE. You MUST continue working NOW.
19815
-
19816
- **HIERARCHICAL TODO MANDATE**:
19817
- Your work is strictly governed by the hierarchy in \`.opencode/todo.md\`.
19818
- 1\uFE0F\u20E3 **Milestones (Grade 1)**: Large phases of the mission.
19819
- 2\uFE0F\u20E3 **Tasks (Grade 2)**: Sub-tasks within milestones.
19820
- 3\uFE0F\u20E3 **Sub-tasks (Grade 3)**: Atomic actions.
19821
-
19822
- **CONCLUDING RULES**:
19823
- \u274C Do NOT stop if there are ANY \`[ ]\` items remaining.
19824
- \u274C Do NOT ask for permission to continue.
19825
- \u274C Do NOT declare completion without verifying EVERY leaf node.
19826
-
19827
- **REQUIRED SEQUENCE**:
19828
- 1. Read \`.opencode/todo.md\` to identify the next \`[ ]\` item.
19829
- 2. If the plan is too abstract, breakdown the next task into smaller sub-tasks.
19830
- 3. Execute and mark as \`[x]\` ONLY after verification.
19831
- 4. Move to the next item immediately.
19832
-
19833
20934
  **Your Original Task**:
19834
20935
  ${state2.prompt}
19835
20936
 
@@ -19848,7 +20949,7 @@ function getLatest(sessionId) {
19848
20949
  const history = progressHistory.get(sessionId);
19849
20950
  return history?.[history.length - 1];
19850
20951
  }
19851
- function clearSession(sessionId) {
20952
+ function clearSession2(sessionId) {
19852
20953
  progressHistory.delete(sessionId);
19853
20954
  sessionStartTimes.delete(sessionId);
19854
20955
  }
@@ -19913,8 +21014,8 @@ function formatElapsedTime(startMs, endMs = Date.now()) {
19913
21014
  }
19914
21015
 
19915
21016
  // src/core/loop/verification.ts
19916
- import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
19917
- import { join as join6 } from "node:path";
21017
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
21018
+ import { join as join8 } from "node:path";
19918
21019
  var CHECKLIST_FILE = CHECKLIST.FILE;
19919
21020
  function parseChecklistLine(line, currentCategory) {
19920
21021
  const trimmedLine = line.trim();
@@ -19979,12 +21080,12 @@ function parseChecklist(content) {
19979
21080
  return items;
19980
21081
  }
19981
21082
  function readChecklist(directory) {
19982
- const filePath = join6(directory, CHECKLIST_FILE);
19983
- if (!existsSync5(filePath)) {
21083
+ const filePath = join8(directory, CHECKLIST_FILE);
21084
+ if (!existsSync6(filePath)) {
19984
21085
  return [];
19985
21086
  }
19986
21087
  try {
19987
- const content = readFileSync2(filePath, "utf-8");
21088
+ const content = readFileSync3(filePath, "utf-8");
19988
21089
  return parseChecklist(content);
19989
21090
  } catch (error45) {
19990
21091
  log(`[checklist] Failed to read checklist: ${error45}`);
@@ -20001,8 +21102,8 @@ function verifyChecklist(directory) {
20001
21102
  incompleteList: [],
20002
21103
  errors: []
20003
21104
  };
20004
- const filePath = join6(directory, CHECKLIST_FILE);
20005
- if (!existsSync5(filePath)) {
21105
+ const filePath = join8(directory, CHECKLIST_FILE);
21106
+ if (!existsSync6(filePath)) {
20006
21107
  result.errors.push(`Verification checklist not found at ${CHECKLIST_FILE}`);
20007
21108
  result.errors.push("Create checklist with at least: build, tests, and any environment-specific checks");
20008
21109
  return result;
@@ -20078,10 +21179,10 @@ function verifyMissionCompletion(directory) {
20078
21179
  result.errors.push(` ... and ${checklistResult.incompleteList.length - 5} more`);
20079
21180
  }
20080
21181
  }
20081
- const todoPath = join6(directory, PATHS.TODO);
20082
- if (existsSync5(todoPath)) {
21182
+ const todoPath = join8(directory, PATHS.TODO);
21183
+ if (existsSync6(todoPath)) {
20083
21184
  try {
20084
- const content = readFileSync2(todoPath, "utf-8");
21185
+ const content = readFileSync3(todoPath, "utf-8");
20085
21186
  const incompleteCount = countMatches(content, TODO_INCOMPLETE_PATTERN);
20086
21187
  const completeCount = countMatches(content, TODO_COMPLETE_PATTERN);
20087
21188
  const total = incompleteCount + completeCount;
@@ -20103,10 +21204,10 @@ function verifyMissionCompletion(directory) {
20103
21204
  } else if (!hasChecklist) {
20104
21205
  result.errors.push(`TODO file not found at ${PATHS.TODO}`);
20105
21206
  }
20106
- const syncPath = join6(directory, PATHS.SYNC_ISSUES);
20107
- if (existsSync5(syncPath)) {
21207
+ const syncPath = join8(directory, PATHS.SYNC_ISSUES);
21208
+ if (existsSync6(syncPath)) {
20108
21209
  try {
20109
- const content = readFileSync2(syncPath, "utf-8");
21210
+ const content = readFileSync3(syncPath, "utf-8");
20110
21211
  result.syncIssuesEmpty = !hasRealSyncIssues(content);
20111
21212
  if (!result.syncIssuesEmpty) {
20112
21213
  const issueLines = content.split("\n").filter(
@@ -20241,10 +21342,10 @@ async function resolveCommandPath(key, commandName) {
20241
21342
  const currentPending = pending.get(key);
20242
21343
  if (currentPending) return currentPending;
20243
21344
  const promise2 = (async () => {
20244
- const path6 = await findCommand(commandName);
20245
- cache[key] = path6;
21345
+ const path9 = await findCommand(commandName);
21346
+ cache[key] = path9;
20246
21347
  pending.delete(key);
20247
- return path6;
21348
+ return path9;
20248
21349
  })();
20249
21350
  pending.set(key, promise2);
20250
21351
  return promise2;
@@ -20253,21 +21354,21 @@ async function resolveCommandPath(key, commandName) {
20253
21354
  // src/core/notification/os-notify/notifier.ts
20254
21355
  var execAsync2 = promisify2(exec2);
20255
21356
  async function notifyDarwin(title, message) {
20256
- const path6 = await resolveCommandPath(
21357
+ const path9 = await resolveCommandPath(
20257
21358
  NOTIFICATION_COMMAND_KEYS.OSASCRIPT,
20258
21359
  NOTIFICATION_COMMANDS.OSASCRIPT
20259
21360
  );
20260
- if (!path6) return;
21361
+ if (!path9) return;
20261
21362
  const escT = title.replace(/"/g, '\\"');
20262
21363
  const escM = message.replace(/"/g, '\\"');
20263
- await execAsync2(`${path6} -e 'display notification "${escM}" with title "${escT}" sound name "Glass"'`);
21364
+ await execAsync2(`${path9} -e 'display notification "${escM}" with title "${escT}" sound name "Glass"'`);
20264
21365
  }
20265
21366
  async function notifyLinux(title, message) {
20266
- const path6 = await resolveCommandPath(
21367
+ const path9 = await resolveCommandPath(
20267
21368
  NOTIFICATION_COMMAND_KEYS.NOTIFY_SEND,
20268
21369
  NOTIFICATION_COMMANDS.NOTIFY_SEND
20269
21370
  );
20270
- if (path6) await execAsync2(`${path6} "${title}" "${message}" 2>/dev/null`);
21371
+ if (path9) await execAsync2(`${path9} "${title}" "${message}" 2>/dev/null`);
20271
21372
  }
20272
21373
  async function notifyWindows(title, message) {
20273
21374
  const ps = await resolveCommandPath(
@@ -20313,11 +21414,11 @@ import { exec as exec3 } from "node:child_process";
20313
21414
  async function playDarwin(soundPath) {
20314
21415
  if (!soundPath) return;
20315
21416
  try {
20316
- const path6 = await resolveCommandPath(
21417
+ const path9 = await resolveCommandPath(
20317
21418
  NOTIFICATION_COMMAND_KEYS.AFPLAY,
20318
21419
  NOTIFICATION_COMMANDS.AFPLAY
20319
21420
  );
20320
- if (path6) exec3(`"${path6}" "${soundPath}"`);
21421
+ if (path9) exec3(`"${path9}" "${soundPath}"`);
20321
21422
  } catch (err) {
20322
21423
  log(`[session-notify] Error playing sound (Darwin): ${err}`);
20323
21424
  }
@@ -20387,7 +21488,7 @@ function getDefaultSoundPath(p) {
20387
21488
 
20388
21489
  // src/hooks/features/mission-loop.ts
20389
21490
  var MissionControlHook = class {
20390
- name = HOOK_NAMES.MISSION_LOOP;
21491
+ name = HOOK_NAMES2.MISSION_LOOP;
20391
21492
  async execute(ctx, text) {
20392
21493
  const chatResult = await this.handleChatCommand(ctx, text);
20393
21494
  if (chatResult) return chatResult;
@@ -20473,6 +21574,7 @@ var MissionControlHook = class {
20473
21574
  async handleMissionComplete(directory, verification) {
20474
21575
  log(MISSION_MESSAGES.COMPLETE_LOG + " " + buildVerificationSummary(verification));
20475
21576
  const cleared = clearLoopState(directory);
21577
+ parallelAgentManager.cleanup();
20476
21578
  if (cleared) {
20477
21579
  const toastManager = getTaskToastManager();
20478
21580
  if (toastManager) {
@@ -20535,7 +21637,7 @@ var UI_PATTERNS = {
20535
21637
 
20536
21638
  // src/hooks/custom/strict-role-guard.ts
20537
21639
  var StrictRoleGuardHook = class {
20538
- name = HOOK_NAMES.STRICT_ROLE_GUARD;
21640
+ name = HOOK_NAMES2.STRICT_ROLE_GUARD;
20539
21641
  async execute(ctx, tool2, args) {
20540
21642
  if (tool2 === TOOL_NAMES.RUN_COMMAND || tool2 === TOOL_NAMES.RUN_BACKGROUND) {
20541
21643
  const cmd = args?.command;
@@ -20554,7 +21656,7 @@ var StrictRoleGuardHook = class {
20554
21656
 
20555
21657
  // src/hooks/custom/secret-scanner.ts
20556
21658
  var SecretScannerHook = class {
20557
- name = HOOK_NAMES.SECRET_SCANNER;
21659
+ name = HOOK_NAMES2.SECRET_SCANNER;
20558
21660
  async execute(ctx, tool2, input, output) {
20559
21661
  let content = output.output;
20560
21662
  let modified = false;
@@ -20573,7 +21675,7 @@ var SecretScannerHook = class {
20573
21675
 
20574
21676
  // src/hooks/custom/agent-ui.ts
20575
21677
  var AgentUIHook = class {
20576
- name = HOOK_NAMES.AGENT_UI;
21678
+ name = HOOK_NAMES2.AGENT_UI;
20577
21679
  async execute(ctx, tool2, input, output) {
20578
21680
  if (tool2 !== TOOL_NAMES.CALL_AGENT) return {};
20579
21681
  if (input?.task) {
@@ -20693,7 +21795,7 @@ function cleanupSession(sessionID) {
20693
21795
  var COMPACTION_THRESHOLD = CONTEXT_THRESHOLDS.WARNING;
20694
21796
  var COOLDOWN_MS = 10 * 60 * 1e3;
20695
21797
  var ResourceControlHook = class {
20696
- name = HOOK_NAMES.RESOURCE_CONTROL;
21798
+ name = HOOK_NAMES2.RESOURCE_CONTROL;
20697
21799
  lastCompactionTime = /* @__PURE__ */ new Map();
20698
21800
  async execute(ctx, toolOrText, input, output) {
20699
21801
  let inputLen = 0;
@@ -20758,7 +21860,7 @@ function getNextPending(todos) {
20758
21860
  pending2.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
20759
21861
  return pending2[0];
20760
21862
  }
20761
- function getStats2(todos) {
21863
+ function getStats3(todos) {
20762
21864
  const stats2 = {
20763
21865
  total: todos.length,
20764
21866
  pending: todos.filter((t) => t.status === TODO_STATUS2.PENDING).length,
@@ -20777,7 +21879,7 @@ function getStats2(todos) {
20777
21879
 
20778
21880
  // src/core/loop/formatters.ts
20779
21881
  function formatProgress(todos) {
20780
- const stats2 = getStats2(todos);
21882
+ const stats2 = getStats3(todos);
20781
21883
  const done = stats2.completed + stats2.cancelled;
20782
21884
  return `${done}/${stats2.total} (${stats2.percentComplete}%)`;
20783
21885
  }
@@ -20841,110 +21943,6 @@ ${LOOP_LABELS.ACTION_DONT_STOP}
20841
21943
  return prompt;
20842
21944
  }
20843
21945
 
20844
- // src/core/recovery/constants.ts
20845
- var MAX_RETRIES = RECOVERY.MAX_ATTEMPTS;
20846
- var BASE_DELAY = RECOVERY.BASE_DELAY_MS;
20847
- var MAX_HISTORY = HISTORY.MAX_RECOVERY;
20848
-
20849
- // src/core/recovery/patterns.ts
20850
- var errorPatterns = [
20851
- // Rate limiting
20852
- {
20853
- pattern: /rate.?limit|too.?many.?requests|429/i,
20854
- category: "rate_limit",
20855
- handler: (ctx) => {
20856
- const delay = BASE_DELAY * Math.pow(2, ctx.attempt);
20857
- presets_exports.warningRateLimited();
20858
- return { type: "retry", delay, attempt: ctx.attempt + 1 };
20859
- }
20860
- },
20861
- // Context overflow
20862
- {
20863
- pattern: /context.?length|token.?limit|maximum.?context/i,
20864
- category: "context_overflow",
20865
- handler: () => {
20866
- presets_exports.errorRecovery("Compacting context");
20867
- return { type: "compact", reason: "Context limit reached" };
20868
- }
20869
- },
20870
- // Network errors
20871
- {
20872
- pattern: /ECONNREFUSED|ETIMEDOUT|network|fetch.?failed/i,
20873
- category: "network",
20874
- handler: (ctx) => {
20875
- if (ctx.attempt >= MAX_RETRIES) {
20876
- return { type: "abort", reason: "Network unavailable after retries" };
20877
- }
20878
- return { type: "retry", delay: BASE_DELAY * (ctx.attempt + 1), attempt: ctx.attempt + 1 };
20879
- }
20880
- },
20881
- // Session errors
20882
- {
20883
- pattern: /session.?not.?found|session.?expired/i,
20884
- category: "session",
20885
- handler: () => {
20886
- return { type: "abort", reason: "Session no longer available" };
20887
- }
20888
- },
20889
- // Tool errors
20890
- {
20891
- pattern: /tool.?not.?found|unknown.?tool/i,
20892
- category: "tool",
20893
- handler: (ctx) => {
20894
- return { type: "escalate", to: "Reviewer", reason: `Unknown tool used by ${ctx.agent}` };
20895
- }
20896
- },
20897
- // Parse errors
20898
- {
20899
- pattern: /parse.?error|invalid.?json|syntax.?error/i,
20900
- category: "parse",
20901
- handler: (ctx) => {
20902
- if (ctx.attempt >= 2) {
20903
- return { type: "skip", reason: "Persistent parse error" };
20904
- }
20905
- return { type: "retry", delay: 500, attempt: ctx.attempt + 1 };
20906
- }
20907
- },
20908
- // Gibberish / hallucination
20909
- {
20910
- pattern: /gibberish|hallucination|mixed.?language/i,
20911
- category: "gibberish",
20912
- handler: () => {
20913
- presets_exports.errorRecovery("Retrying with clean context");
20914
- return { type: "retry", delay: 1e3, attempt: 1 };
20915
- }
20916
- }
20917
- ];
20918
-
20919
- // src/core/recovery/handler.ts
20920
- var recoveryHistory = [];
20921
- function handleError(context) {
20922
- const errorMessage = context.error.message || String(context.error);
20923
- for (const pattern of errorPatterns) {
20924
- const matches = typeof pattern.pattern === "string" ? errorMessage.includes(pattern.pattern) : pattern.pattern.test(errorMessage);
20925
- if (matches) {
20926
- const action = pattern.handler(context);
20927
- recoveryHistory.push({
20928
- context,
20929
- action,
20930
- timestamp: /* @__PURE__ */ new Date()
20931
- });
20932
- if (recoveryHistory.length > MAX_HISTORY) {
20933
- recoveryHistory.shift();
20934
- }
20935
- return action;
20936
- }
20937
- }
20938
- if (context.attempt < MAX_RETRIES) {
20939
- return {
20940
- type: "retry",
20941
- delay: BASE_DELAY * Math.pow(2, context.attempt),
20942
- attempt: context.attempt + 1
20943
- };
20944
- }
20945
- return { type: "abort", reason: `Unknown error after ${MAX_RETRIES} retries` };
20946
- }
20947
-
20948
21946
  // src/core/recovery/session-recovery.ts
20949
21947
  var recoveryState = /* @__PURE__ */ new Map();
20950
21948
  function getState2(sessionID) {
@@ -21292,7 +22290,7 @@ function cleanupSession2(sessionID) {
21292
22290
 
21293
22291
  // src/hooks/custom/user-activity.ts
21294
22292
  var UserActivityHook = class {
21295
- name = HOOK_NAMES.USER_ACTIVITY;
22293
+ name = HOOK_NAMES2.USER_ACTIVITY;
21296
22294
  async execute(ctx, message) {
21297
22295
  if (ctx.sessionID) {
21298
22296
  handleUserMessage(ctx.sessionID);
@@ -21301,6 +22299,70 @@ var UserActivityHook = class {
21301
22299
  }
21302
22300
  };
21303
22301
 
22302
+ // src/hooks/custom/memory-gate.ts
22303
+ var MemoryGateHook = class {
22304
+ name = HOOK_NAMES.MEMORY_GATE;
22305
+ memoryManager = MemoryManager.getInstance();
22306
+ async execute(context, toolOrText, input, output) {
22307
+ if (output) {
22308
+ return this.handlePostTool(context, toolOrText, input, output);
22309
+ } else {
22310
+ return this.handleAssistantDone(context, toolOrText);
22311
+ }
22312
+ }
22313
+ /**
22314
+ * Post-Tool: Capture tool outputs to TASK memory
22315
+ */
22316
+ async handlePostTool(context, tool2, input, output) {
22317
+ if (MEMORY_CONSTANTS.NOISY_TOOLS.includes(tool2)) return {};
22318
+ const maxContentLength = MEMORY_CONSTANTS.MAX_CONTENT_LENGTH;
22319
+ let content = output.output;
22320
+ if (content.length > maxContentLength) {
22321
+ content = content.substring(0, maxContentLength) + "... [truncated]";
22322
+ }
22323
+ const memoryText = `Tool [${tool2}] result for input ${JSON.stringify(input)}: ${content}`;
22324
+ this.memoryManager.add("task" /* TASK */, memoryText, MEMORY_CONSTANTS.IMPORTANCE.LOW);
22325
+ return {};
22326
+ }
22327
+ /**
22328
+ * Assistant Done: Capture turn summary to MISSION memory if important
22329
+ */
22330
+ async handleAssistantDone(context, finalText) {
22331
+ const { KEYWORDS, IMPORTANCE } = MEMORY_CONSTANTS;
22332
+ if (finalText.includes(KEYWORDS.DONE) || finalText.includes(KEYWORDS.SUCCESS) || finalText.includes(KEYWORDS.ERROR)) {
22333
+ const importance = finalText.includes(KEYWORDS.ERROR) ? IMPORTANCE.CRITICAL : IMPORTANCE.HIGH;
22334
+ const summary = finalText.length > 500 ? finalText.substring(0, 500) + "..." : finalText;
22335
+ this.memoryManager.add("mission" /* MISSION */, `Agent [${context.agent}] turn summary: ${summary}`, importance);
22336
+ }
22337
+ return { action: HOOK_ACTIONS.CONTINUE };
22338
+ }
22339
+ };
22340
+
22341
+ // src/hooks/custom/metrics.ts
22342
+ var MetricsHook = class {
22343
+ name = HOOK_NAMES.METRICS_TELEMETRY;
22344
+ startTimes = /* @__PURE__ */ new Map();
22345
+ metrics = MetricsCollector.getInstance();
22346
+ async execute(context, toolOrText, input, output) {
22347
+ if (!output && input) {
22348
+ this.startTimes.set(`tool_${toolOrText}_${context.sessionID}`, Date.now());
22349
+ return { action: HOOK_ACTIONS.ALLOW };
22350
+ } else if (output) {
22351
+ const startTime = this.startTimes.get(`tool_${toolOrText}_${context.sessionID}`);
22352
+ if (startTime) {
22353
+ const duration3 = Date.now() - startTime;
22354
+ this.metrics.recordToolExecution(toolOrText, duration3);
22355
+ this.startTimes.delete(`tool_${toolOrText}_${context.sessionID}`);
22356
+ }
22357
+ this.metrics.recordTokenUsage(output.output.length / 4);
22358
+ return {};
22359
+ } else {
22360
+ this.metrics.recordTokenUsage(toolOrText.length / 4);
22361
+ return { action: HOOK_ACTIONS.CONTINUE };
22362
+ }
22363
+ }
22364
+ };
22365
+
21304
22366
  // src/hooks/index.ts
21305
22367
  function initializeHooks() {
21306
22368
  const registry2 = HookRegistry.getInstance();
@@ -21311,18 +22373,103 @@ function initializeHooks() {
21311
22373
  const agentUI = new AgentUIHook();
21312
22374
  const resourceControl = new ResourceControlHook();
21313
22375
  const userActivity = new UserActivityHook();
22376
+ const memoryGate = new MemoryGateHook();
22377
+ const metricsHook = new MetricsHook();
21314
22378
  registry2.registerChat(userActivity);
21315
22379
  registry2.registerChat(missionControl);
21316
22380
  registry2.registerPostTool(sanityCheck);
21317
22381
  registry2.registerPostTool(secretScanner);
21318
22382
  registry2.registerPostTool(agentUI);
21319
22383
  registry2.registerPostTool(resourceControl);
22384
+ registry2.registerPostTool(memoryGate);
22385
+ registry2.registerPostTool(metricsHook);
21320
22386
  registry2.registerPreTool(roleGuard);
22387
+ registry2.registerPreTool(metricsHook);
21321
22388
  registry2.registerDone(sanityCheck);
21322
22389
  registry2.registerDone(missionControl);
21323
22390
  registry2.registerDone(resourceControl);
22391
+ registry2.registerDone(memoryGate);
22392
+ registry2.registerDone(metricsHook);
21324
22393
  }
21325
22394
 
22395
+ // src/core/plugins/plugin-manager.ts
22396
+ import * as fs8 from "fs/promises";
22397
+ import * as path7 from "path";
22398
+ var PluginManager = class _PluginManager {
22399
+ static instance;
22400
+ plugins = /* @__PURE__ */ new Map();
22401
+ directory = "";
22402
+ dynamicTools = {};
22403
+ constructor() {
22404
+ }
22405
+ static getInstance() {
22406
+ if (!_PluginManager.instance) {
22407
+ _PluginManager.instance = new _PluginManager();
22408
+ }
22409
+ return _PluginManager.instance;
22410
+ }
22411
+ async initialize(directory) {
22412
+ this.directory = directory;
22413
+ await this.loadPlugins();
22414
+ }
22415
+ /**
22416
+ * Load plugins from .opencode/plugins/*.js
22417
+ */
22418
+ async loadPlugins() {
22419
+ if (!this.directory) return;
22420
+ const pluginsDir = path7.join(this.directory, PATHS.PLUGINS);
22421
+ try {
22422
+ await fs8.mkdir(pluginsDir, { recursive: true });
22423
+ const files = await fs8.readdir(pluginsDir);
22424
+ for (const file2 of files) {
22425
+ if (file2.endsWith(".js") || file2.endsWith(".mjs")) {
22426
+ await this.loadPlugin(path7.join(pluginsDir, file2));
22427
+ }
22428
+ }
22429
+ } catch (error45) {
22430
+ log(`[PluginManager] Error reading plugins directory: ${error45}`);
22431
+ }
22432
+ }
22433
+ async loadPlugin(pluginPath) {
22434
+ try {
22435
+ const module = await import(`file://${pluginPath}`);
22436
+ const plugin = module.default || module;
22437
+ if (!plugin.name) {
22438
+ log(`[PluginManager] Plugin at ${pluginPath} missing name, skipping.`);
22439
+ return;
22440
+ }
22441
+ log(`[PluginManager] Loading plugin: ${plugin.name} (v${plugin.version})`);
22442
+ const context = { directory: this.directory };
22443
+ if (plugin.init) {
22444
+ await plugin.init(context);
22445
+ }
22446
+ if (plugin.tools) {
22447
+ for (const [name, tool2] of Object.entries(plugin.tools)) {
22448
+ this.dynamicTools[name] = tool2;
22449
+ log(`[PluginManager] Registered tool: ${name} from plugin ${plugin.name}`);
22450
+ }
22451
+ }
22452
+ if (plugin.hooks) {
22453
+ const registry2 = HookRegistry.getInstance();
22454
+ if (plugin.hooks.preTool) registry2.registerPreTool(plugin.hooks.preTool);
22455
+ if (plugin.hooks.postTool) registry2.registerPostTool(plugin.hooks.postTool);
22456
+ if (plugin.hooks.chat) registry2.registerChat(plugin.hooks.chat);
22457
+ if (plugin.hooks.done) registry2.registerDone(plugin.hooks.done);
22458
+ log(`[PluginManager] Registered hooks from plugin ${plugin.name}`);
22459
+ }
22460
+ this.plugins.set(plugin.name, plugin);
22461
+ } catch (error45) {
22462
+ log(`[PluginManager] Failed to load plugin ${pluginPath}: ${error45}`);
22463
+ }
22464
+ }
22465
+ /**
22466
+ * Get all dynamically registered tools
22467
+ */
22468
+ getDynamicTools() {
22469
+ return this.dynamicTools;
22470
+ }
22471
+ };
22472
+
21326
22473
  // src/plugin-handlers/tool-execute-pre-handler.ts
21327
22474
  function createToolExecuteBeforeHandler(ctx) {
21328
22475
  const { sessions, directory } = ctx;
@@ -21381,17 +22528,17 @@ function createChatMessageHandler(ctx) {
21381
22528
  }
21382
22529
 
21383
22530
  // src/utils/compatibility/claude.ts
21384
- import fs6 from "fs";
21385
- import path5 from "path";
22531
+ import fs9 from "fs";
22532
+ import path8 from "path";
21386
22533
  function findClaudeRules(startDir = process.cwd()) {
21387
22534
  try {
21388
22535
  let currentDir = startDir;
21389
- const root = path5.parse(startDir).root;
22536
+ const root = path8.parse(startDir).root;
21390
22537
  while (true) {
21391
- const claudeMdPath = path5.join(currentDir, "CLAUDE.md");
21392
- if (fs6.existsSync(claudeMdPath)) {
22538
+ const claudeMdPath = path8.join(currentDir, "CLAUDE.md");
22539
+ if (fs9.existsSync(claudeMdPath)) {
21393
22540
  try {
21394
- const content = fs6.readFileSync(claudeMdPath, "utf-8");
22541
+ const content = fs9.readFileSync(claudeMdPath, "utf-8");
21395
22542
  log(`[compatibility] Loaded CLAUDE.md from ${claudeMdPath}`);
21396
22543
  return formatRules("CLAUDE.md", content);
21397
22544
  } catch (e) {
@@ -21399,11 +22546,11 @@ function findClaudeRules(startDir = process.cwd()) {
21399
22546
  }
21400
22547
  }
21401
22548
  if (currentDir === root) break;
21402
- currentDir = path5.dirname(currentDir);
22549
+ currentDir = path8.dirname(currentDir);
21403
22550
  }
21404
- const copilotPath = path5.join(startDir, ".github", "copilot-instructions.md");
21405
- if (fs6.existsSync(copilotPath)) {
21406
- return formatRules("Copilot Instructions", fs6.readFileSync(copilotPath, "utf-8"));
22551
+ const copilotPath = path8.join(startDir, ".github", "copilot-instructions.md");
22552
+ if (fs9.existsSync(copilotPath)) {
22553
+ return formatRules("Copilot Instructions", fs9.readFileSync(copilotPath, "utf-8"));
21407
22554
  }
21408
22555
  return null;
21409
22556
  } catch (error45) {
@@ -21686,7 +22833,7 @@ function createEventHandler(ctx) {
21686
22833
  const duration3 = totalTime < 6e4 ? `${Math.round(totalTime / 1e3)}s` : `${Math.round(totalTime / 6e4)}m`;
21687
22834
  sessions.delete(sessionID);
21688
22835
  state2.sessions.delete(sessionID);
21689
- clearSession(sessionID);
22836
+ clearSession2(sessionID);
21690
22837
  cleanupSessionRecovery(sessionID);
21691
22838
  cleanupSession2(sessionID);
21692
22839
  cleanupSession3(sessionID);
@@ -21998,6 +23145,9 @@ var OrchestratorPlugin = async (input) => {
21998
23145
  const sessions = /* @__PURE__ */ new Map();
21999
23146
  const parallelAgentManager2 = ParallelAgentManager.getInstance(client, directory);
22000
23147
  const asyncAgentTools = createAsyncAgentTools(parallelAgentManager2, client);
23148
+ const pluginManager = PluginManager.getInstance();
23149
+ await pluginManager.initialize(directory);
23150
+ const dynamicTools = pluginManager.getDynamicTools();
22001
23151
  taskToastManager.setConcurrencyController(parallelAgentManager2.getConcurrency());
22002
23152
  const handlerContext = {
22003
23153
  client,
@@ -22044,7 +23194,9 @@ var OrchestratorPlugin = async (input) => {
22044
23194
  [TOOL_NAMES.AST_SEARCH]: astSearchTool(directory),
22045
23195
  [TOOL_NAMES.AST_REPLACE]: astReplaceTool(directory),
22046
23196
  // Async agent tools
22047
- ...asyncAgentTools
23197
+ ...asyncAgentTools,
23198
+ // Dynamic tools from plugins
23199
+ ...dynamicTools
22048
23200
  },
22049
23201
  // -----------------------------------------------------------------
22050
23202
  // Config hook - registers our commands and agents with OpenCode