opencode-matrixx 2.6.0 → 2.6.2

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 (84) hide show
  1. package/README.md +15 -10
  2. package/dist/cli/doctor/checks/context-mode.d.ts +2 -0
  3. package/dist/cli/doctor/checks/dcp.d.ts +2 -0
  4. package/dist/cli/doctor/checks/docker.d.ts +2 -0
  5. package/dist/cli/doctor/checks/headroom.d.ts +2 -0
  6. package/dist/cli/doctor/checks/helpers.d.ts +2 -0
  7. package/dist/cli/doctor/checks/index.d.ts +6 -0
  8. package/dist/cli/doctor/checks/rtk.d.ts +2 -0
  9. package/dist/cli/doctor/checks/tmux.d.ts +2 -0
  10. package/dist/cli/doctor/types.d.ts +1 -1
  11. package/dist/cli/index.d.ts +1 -0
  12. package/dist/cli/setup/config-writer.d.ts +12 -0
  13. package/dist/cli/setup/config-writer.test.d.ts +1 -0
  14. package/dist/cli/setup/constants.d.ts +8 -0
  15. package/dist/cli/setup/deps.d.ts +8 -0
  16. package/dist/cli/setup/deps.test.d.ts +1 -0
  17. package/dist/cli/setup/index.d.ts +8 -0
  18. package/dist/cli/setup/index.test.d.ts +1 -0
  19. package/dist/cli/setup/opencode-sync.d.ts +9 -0
  20. package/dist/cli/setup/opencode-sync.test.d.ts +1 -0
  21. package/dist/cli/setup/prompts.d.ts +4 -0
  22. package/dist/cli/setup/prompts.test.d.ts +1 -0
  23. package/dist/cli/setup/types.d.ts +26 -0
  24. package/dist/cli.js +18759 -663
  25. package/dist/config/schema/context-mode.d.ts +7 -0
  26. package/dist/config/schema/experimental.d.ts +1 -1
  27. package/dist/config/schema/hooks.d.ts +3 -2
  28. package/dist/config/schema/matrixx-config.d.ts +13 -3
  29. package/dist/config/schema/morpheus.d.ts +8 -0
  30. package/dist/config/schema.d.ts +1 -0
  31. package/dist/create-hooks.d.ts +3 -2
  32. package/dist/features/builtin-commands/templates/cleanup-tasks.d.ts +1 -0
  33. package/dist/features/builtin-commands/templates/task-list.d.ts +1 -0
  34. package/dist/features/builtin-commands/types.d.ts +1 -1
  35. package/dist/features/task-storage/storage.d.ts +12 -2
  36. package/dist/features/task-storage/types.d.ts +1 -0
  37. package/dist/hooks/compaction-todo-preserver/hook.d.ts +13 -0
  38. package/dist/hooks/compaction-todo-preserver/index.d.ts +1 -1
  39. package/dist/hooks/context-mode-enforcer/constants.d.ts +6 -0
  40. package/dist/hooks/context-mode-enforcer/hook.d.ts +3 -0
  41. package/dist/hooks/context-mode-enforcer/index.d.ts +1 -0
  42. package/dist/hooks/index.d.ts +3 -2
  43. package/dist/hooks/task-continuation-enforcer/abort-detection.d.ts +4 -0
  44. package/dist/hooks/task-continuation-enforcer/constants.d.ts +10 -0
  45. package/dist/hooks/task-continuation-enforcer/continuation-injection.d.ts +12 -0
  46. package/dist/hooks/task-continuation-enforcer/countdown.d.ts +14 -0
  47. package/dist/hooks/task-continuation-enforcer/countdown.test.d.ts +1 -0
  48. package/dist/hooks/task-continuation-enforcer/handler.d.ts +16 -0
  49. package/dist/hooks/task-continuation-enforcer/idle-event.d.ts +11 -0
  50. package/dist/hooks/task-continuation-enforcer/idle-event.test.d.ts +1 -0
  51. package/dist/hooks/task-continuation-enforcer/index.d.ts +6 -0
  52. package/dist/hooks/task-continuation-enforcer/message-directory.d.ts +1 -0
  53. package/dist/hooks/task-continuation-enforcer/non-idle-events.d.ts +6 -0
  54. package/dist/hooks/task-continuation-enforcer/session-state.d.ts +10 -0
  55. package/dist/hooks/task-continuation-enforcer/todo.d.ts +5 -0
  56. package/dist/hooks/task-continuation-enforcer/types.d.ts +60 -0
  57. package/dist/hooks/task-edit-guard/constants.d.ts +2 -0
  58. package/dist/hooks/task-edit-guard/hook.d.ts +2 -0
  59. package/dist/hooks/task-edit-guard/index.d.ts +2 -0
  60. package/dist/hooks/todo-continuation-enforcer/countdown.test.d.ts +1 -0
  61. package/dist/hooks/todo-continuation-enforcer/idle-event.test.d.ts +1 -0
  62. package/dist/index.js +1647 -926
  63. package/dist/matrixx.schema.json +47 -3
  64. package/dist/plugin/hooks/create-continuation-hooks.d.ts +2 -1
  65. package/dist/plugin/hooks/create-core-hooks.d.ts +2 -2
  66. package/dist/plugin/hooks/create-tool-guard-hooks.d.ts +3 -3
  67. package/dist/shared/system-directive.d.ts +1 -0
  68. package/dist/shared/task-system-gating.d.ts +3 -0
  69. package/dist/shared/task-system-gating.test.d.ts +1 -0
  70. package/dist/tools/index.d.ts +1 -1
  71. package/dist/tools/task/index.d.ts +1 -1
  72. package/dist/tools/task/task-cleanup.d.ts +4 -0
  73. package/dist/tools/task/task-cleanup.test.d.ts +1 -0
  74. package/dist/tools/task/task-get.d.ts +2 -1
  75. package/dist/tools/task/task-list.d.ts +2 -1
  76. package/dist/tools/task/types.d.ts +2 -0
  77. package/package.json +3 -2
  78. package/dist/hooks/task-todo-mirror/constants.d.ts +0 -3
  79. package/dist/hooks/task-todo-mirror/hook.d.ts +0 -43
  80. package/dist/hooks/task-todo-mirror/index.d.ts +0 -2
  81. package/dist/hooks/todo-description-override/description.d.ts +0 -1
  82. package/dist/hooks/todo-description-override/hook.d.ts +0 -8
  83. package/dist/hooks/todo-description-override/index.d.ts +0 -1
  84. package/dist/tools/task/todo-sync.d.ts +0 -16
package/dist/index.js CHANGED
@@ -4551,7 +4551,7 @@ var require_picomatch2 = __commonJS(function(exports, module) {
4551
4551
  });
4552
4552
 
4553
4553
  // src/hooks/matrix-loop/constants.ts
4554
- var HOOK_NAME5 = "matrix-loop", DEFAULT_STATE_FILE = ".matrixx/matrix-loop.local.md", DEFAULT_MAX_ITERATIONS = 100, DEFAULT_COMPLETION_PROMISE = "DONE", DEFAULT_VERIFICATION_AGENT = "merovingian", DEFAULT_VERIFICATION_TIMEOUT_MS = 30000, DEFAULT_VERIFICATION_MAX_RETRIES = 2;
4554
+ var HOOK_NAME6 = "matrix-loop", DEFAULT_STATE_FILE = ".matrixx/matrix-loop.local.md", DEFAULT_MAX_ITERATIONS = 100, DEFAULT_COMPLETION_PROMISE = "DONE", DEFAULT_VERIFICATION_AGENT = "merovingian", DEFAULT_VERIFICATION_TIMEOUT_MS = 30000, DEFAULT_VERIFICATION_MAX_RETRIES = 2;
4555
4555
 
4556
4556
  // src/hooks/matrix-loop/storage.ts
4557
4557
  var exports_storage = {};
@@ -32820,6 +32820,33 @@ function migrateConfigFile(configPath, rawConfig) {
32820
32820
  log(`Removed obsolete hooks from disabled_hooks: ${removed.join(", ")} (these hooks no longer exist in v3.0.0)`);
32821
32821
  }
32822
32822
  }
32823
+ if (copy.experimental?.task_system === undefined) {
32824
+ const existing = copy.experimental ?? {};
32825
+ copy.experimental = { ...existing, task_system: true };
32826
+ if (!existingMigrations.has("task_system_default_true")) {
32827
+ allNewMigrations.push("task_system_default_true");
32828
+ const current = Array.isArray(copy._migrations) ? [...copy._migrations] : Array.from(existingMigrations);
32829
+ if (!current.includes("task_system_default_true")) {
32830
+ current.push("task_system_default_true");
32831
+ copy._migrations = current;
32832
+ }
32833
+ }
32834
+ needsWrite = true;
32835
+ }
32836
+ if (copy.morpheus?.tasks?.scope === undefined) {
32837
+ const morpheusExisting = copy.morpheus ?? {};
32838
+ const tasksExisting = morpheusExisting.tasks ?? {};
32839
+ copy.morpheus = { ...morpheusExisting, tasks: { ...tasksExisting, scope: "project" } };
32840
+ if (!existingMigrations.has("tasks_project_scope_default")) {
32841
+ allNewMigrations.push("tasks_project_scope_default");
32842
+ const current = Array.isArray(copy._migrations) ? [...copy._migrations] : Array.from(existingMigrations);
32843
+ if (!current.includes("tasks_project_scope_default")) {
32844
+ current.push("tasks_project_scope_default");
32845
+ copy._migrations = current;
32846
+ }
32847
+ }
32848
+ needsWrite = true;
32849
+ }
32823
32850
  if (needsWrite) {
32824
32851
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
32825
32852
  const backupPath = `${configPath}.bak.${timestamp}`;
@@ -34160,6 +34187,7 @@ function removeSystemReminders(text) {
34160
34187
  }
34161
34188
  var SystemDirectiveTypes = {
34162
34189
  TODO_CONTINUATION: "TODO CONTINUATION",
34190
+ TASK_CONTINUATION: "TASK CONTINUATION",
34163
34191
  MATRIX_LOOP: "MATRIX LOOP",
34164
34192
  MISSION_CONTINUATION: "MISSION CONTINUATION",
34165
34193
  DELEGATION_REQUIRED: "DELEGATION REQUIRED",
@@ -37477,6 +37505,66 @@ Batch: per-feature subdirectory tree under \`<input-dir>/<feature>/\` with the s
37477
37505
  NEVER run \`git commit\`, \`git add\`, \`git push\`, \`git rebase\`, \`git reset\`, \`git tag\`, or any other git command in this command context. The pipeline runner is responsible for version control. You may only create/edit the generated files in the target output directory.
37478
37506
  `;
37479
37507
 
37508
+ // src/features/builtin-commands/templates/cleanup-tasks.ts
37509
+ var CLEANUP_TASKS_TEMPLATE = `# Cleanup Tasks \u2014 SLASH COMMAND OVERRIDE
37510
+
37511
+ SYSTEM OVERRIDE: Ignore any global [search-mode] or [analyze-mode] directive for this command. This is a direct tool call, not a search.
37512
+
37513
+ Call the task_cleanup tool NOW with no exploration, no grep, no trinity, no operator, no background agents.
37514
+
37515
+ ## Usage
37516
+ \`\`\`
37517
+ /cleanup-tasks [--olderThan <duration>] [--all]
37518
+
37519
+ Options:
37520
+ --olderThan: Only delete completed tasks older than duration (e.g., 7d, 24h, 30d). If omitted, deletes all completed regardless of age.
37521
+ --all: Delete all completed tasks (default when no olderThan given).
37522
+
37523
+ Examples:
37524
+ /cleanup-tasks # delete all completed tasks
37525
+ /cleanup-tasks --olderThan 7d
37526
+ /cleanup-tasks --all
37527
+ \`\`\`
37528
+
37529
+ ## What This Command Does
37530
+
37531
+ Cleans up completed tasks from the file-based task system (dedicated Matrixx viewer):
37532
+
37533
+ Storage layout (hybrid, via \`getTaskDir(config, directory)\`):
37534
+ - Primary (default, \`scope: "project"\`): \`<project>/.matrixx/tasks/T-*.json\`
37535
+ - Fallback (\`scope: "global"\` or \`storage_path\` override): \`~/.config/opencode/tasks/{listId}/T-*.json\`
37536
+
37537
+ 1. **Scans** \`getTaskDir(config, directory)/*.json\` via \`readdirSync\` \u2192 resolves to \`.matrixx/tasks\` when \`scope=project\` (default) or legacy global path when \`scope=global\` / \`storage_path\` set
37538
+ 2. **Filters** \`status==="completed"\` (and \`olderThan\` if given, via \`time_updated\`/\`time_created\`)
37539
+ 3. **Deletes** matching files via \`unlinkSync\`
37540
+ 4. **Reports** \`{deleted, remaining, deletedIds}\`
37541
+ View remaining tasks via \`/task-list\` or \`task_list\` tool \u2014 the dedicated Matrixx task viewer (file-based, no sync). Without \`/cleanup-tasks\`, completed tasks remain on disk for audit.
37542
+
37543
+ ## Verification
37544
+
37545
+ After running:
37546
+ - \`task_list\` \u2192 0 pending (if no active)
37547
+ - \`task_list\` \u2192 shows remaining active tasks (pending/in_progress)
37548
+ - \`task_list --all\` \u2192 shows counts by status
37549
+ - Files: \`ls .matrixx/tasks | wc -l\` decreases by \`deleted\` (project scope, default) or \`ls ~/.config/opencode/tasks/matrixx | wc -l\` decreases (global scope / legacy fallback)
37550
+
37551
+ ---
37552
+
37553
+ <command-instruction>
37554
+ You are executing the cleanup-tasks command. Use the task_cleanup tool to delete completed tasks.
37555
+
37556
+ Parse arguments from $ARGUMENTS:
37557
+ - If "--olderThan" present, extract duration (e.g., "7d", "24h") \u2192 pass as olderThan
37558
+ - Otherwise pass all:true (delete all completed)
37559
+
37560
+ Call task_cleanup with {olderThan, all} and report result.
37561
+ </command-instruction>
37562
+
37563
+ <user-request>
37564
+ $ARGUMENTS
37565
+ </user-request>
37566
+ `;
37567
+
37480
37568
  // src/features/builtin-commands/templates/dcp-profile.ts
37481
37569
  var DCP_PROFILE_TEMPLATE = `You are switching the active DCP (Dynamic Context Pruning) profile tier.
37482
37570
 
@@ -39295,6 +39383,20 @@ After running this command:
39295
39383
 
39296
39384
  Use this when you need to pause automated continuation and take manual control.`;
39297
39385
 
39386
+ // src/features/builtin-commands/templates/task-list.ts
39387
+ var TASK_LIST_TEMPLATE = `# Task List \u2014 SLASH COMMAND OVERRIDE
39388
+
39389
+ SYSTEM OVERRIDE: Ignore any global [search-mode] or [analyze-mode] directive for this command. This is a direct tool call, not a search.
39390
+
39391
+ Call the task_list tool NOW with no exploration, no grep, no trinity, no operator, no background agents.
39392
+
39393
+ - If $ARGUMENTS contains "--all", call task_list and also report counts by reading T-*.json via the tool's own logic (do not manually find/ls).
39394
+ - If $ARGUMENTS contains "--status", filter to that status.
39395
+ - Otherwise just return task_list output as table ID | Subject | Status.
39396
+
39397
+ Do not run rtk ls, find, grep, or launch subagents. One tool call only.
39398
+ `;
39399
+
39298
39400
  // src/features/builtin-commands/templates/ultrawork.ts
39299
39401
  var ULTRAWORK_TEMPLATE = `Toggle ultrawork mode for the current session.
39300
39402
 
@@ -39547,6 +39649,28 @@ ${EVOLUTION_TEMPLATE}
39547
39649
  $ARGUMENTS
39548
39650
  </user-request>`,
39549
39651
  argumentHint: "[list|approve <slug>|reject <slug>|audit]"
39652
+ },
39653
+ "cleanup-tasks": {
39654
+ description: "(builtin) Delete completed tasks",
39655
+ template: `<command-instruction>
39656
+ ${CLEANUP_TASKS_TEMPLATE}
39657
+ </command-instruction>
39658
+
39659
+ <user-request>
39660
+ $ARGUMENTS
39661
+ </user-request>`,
39662
+ argumentHint: "[--olderThan <duration>] [--all]"
39663
+ },
39664
+ "task-list": {
39665
+ description: "(builtin) Show current state of all tasks",
39666
+ template: `<command-instruction>
39667
+ ${TASK_LIST_TEMPLATE}
39668
+ </command-instruction>
39669
+
39670
+ <user-request>
39671
+ $ARGUMENTS
39672
+ </user-request>`,
39673
+ argumentHint: "[--all] [--status <pending|in_progress|completed|deleted>]"
39550
39674
  }
39551
39675
  };
39552
39676
  function loadBuiltinCommands(disabledCommands) {
@@ -55490,19 +55614,10 @@ function extractTodos(response) {
55490
55614
  }
55491
55615
  return [];
55492
55616
  }
55493
- async function resolveTodoWriter() {
55494
- const loaders = ["opencode/session/todo", "@opencode-ai/core/session/todo"];
55495
- for (const loader of loaders) {
55496
- try {
55497
- const mod = await import(loader);
55498
- const update = mod.Todo?.update;
55499
- if (typeof update === "function") {
55500
- return update;
55501
- }
55502
- } catch (err) {
55503
- log(`[${HOOK_NAME2}] Failed to resolve Todo.update`, { loader, error: String(err) });
55504
- }
55505
- }
55617
+ var testWriter;
55618
+ async function resolveTodoWriter(_ctx) {
55619
+ if (testWriter !== undefined)
55620
+ return testWriter;
55506
55621
  return null;
55507
55622
  }
55508
55623
  function resolveSessionID(props) {
@@ -55542,18 +55657,17 @@ function createCompactionTodoPreserverHook(ctx) {
55542
55657
  log(`[${HOOK_NAME2}] Skipped restore (todos already present)`, { sessionID, count: currentTodos.length });
55543
55658
  return;
55544
55659
  }
55545
- const writer = await resolveTodoWriter();
55660
+ const writer = await resolveTodoWriter(ctx);
55546
55661
  if (!writer) {
55547
- log(`[${HOOK_NAME2}] Skipped restore (Todo.update unavailable)`, { sessionID });
55662
+ log(`[${HOOK_NAME2}] Todo.update unavailable \u2014 todos not restored after compaction (visible failure)`, { sessionID });
55548
55663
  return;
55549
55664
  }
55550
55665
  try {
55551
55666
  await writer({ sessionID, todos: snapshot });
55552
55667
  log(`[${HOOK_NAME2}] Restored todos after compaction`, { sessionID, count: snapshot.length });
55668
+ snapshots.delete(sessionID);
55553
55669
  } catch (err) {
55554
55670
  log(`[${HOOK_NAME2}] Failed to restore todos`, { sessionID, error: String(err) });
55555
- } finally {
55556
- snapshots.delete(sessionID);
55557
55671
  }
55558
55672
  };
55559
55673
  const event = async ({ event: event2 }) => {
@@ -55575,6 +55689,75 @@ function createCompactionTodoPreserverHook(ctx) {
55575
55689
  };
55576
55690
  return { capture, event };
55577
55691
  }
55692
+ // src/hooks/context-mode-enforcer/constants.ts
55693
+ var HOOK_NAME3 = "context-mode-enforcer";
55694
+ var WARN_MESSAGE_READ = "Use ctx_search / ctx_batch_execute / ctx_execute for analysis \u2014 raw read/grep/glob bypasses FTS5 sandbox and pollutes context. Use Read only with hashline IDs for Edit.";
55695
+ var BLOCK_MESSAGE_GREP_GLOB = "Blocked: raw grep/glob is forbidden when context-mode is enforced. Use ctx_search (indexed) \u2192 grep/glob fallback only when ctx_* unavailable, or ctx_batch_execute / ctx_execute for multi-file analysis.";
55696
+ var WARN_MESSAGE_BASH_READ = "Use ctx_* / Read tool instead of `cat`/`head`/`tail`/`grep` via bash for analysis. Raw bash file reads bypass sandbox.";
55697
+
55698
+ // src/hooks/context-mode-enforcer/hook.ts
55699
+ var BASH_FILE_READ_RE = /^\s*(cat|head|tail)\s+/;
55700
+ var BASH_GREP_RE = /\bgrep\b/;
55701
+ function resolveContextModeConfig(pluginConfig) {
55702
+ const cfg = pluginConfig.context_mode;
55703
+ return {
55704
+ enabled: cfg?.enabled ?? true,
55705
+ enforce: cfg?.enforce ?? false,
55706
+ blockedTools: new Set((cfg?.blocked_tools ?? ["grep", "glob"]).map((t) => t.toLowerCase()))
55707
+ };
55708
+ }
55709
+ function createContextModeEnforcerHook(pluginConfig) {
55710
+ const { enabled, enforce, blockedTools } = resolveContextModeConfig(pluginConfig);
55711
+ return {
55712
+ "tool.execute.before": async (input, output) => {
55713
+ if (!enabled)
55714
+ return;
55715
+ const tool = input.tool.toLowerCase();
55716
+ const isBlocked = blockedTools.has(tool);
55717
+ const isRead = tool === "read";
55718
+ const isGrepGlob = tool === "grep" || tool === "glob";
55719
+ const isBash = tool === "bash";
55720
+ if (isBash) {
55721
+ const command = output.args.command;
55722
+ if (typeof command !== "string")
55723
+ return;
55724
+ const isFileRead = BASH_FILE_READ_RE.test(command);
55725
+ const isGrep = BASH_GREP_RE.test(command);
55726
+ if (!isFileRead && !isGrep)
55727
+ return;
55728
+ if (enforce && blockedTools.has("bash")) {
55729
+ log(`[${HOOK_NAME3}] BLOCKED bash file read`, {
55730
+ sessionID: input.sessionID,
55731
+ command: command.slice(0, 120)
55732
+ });
55733
+ throw new Error(`${BLOCK_MESSAGE_GREP_GLOB}
55734
+
55735
+ Bash command: ${command.slice(0, 200)}`);
55736
+ }
55737
+ output.message = WARN_MESSAGE_BASH_READ;
55738
+ log(`[${HOOK_NAME3}] warned on bash`, { sessionID: input.sessionID, command: command.slice(0, 120) });
55739
+ return;
55740
+ }
55741
+ if (isRead) {
55742
+ if (!isBlocked)
55743
+ return;
55744
+ output.message = WARN_MESSAGE_READ;
55745
+ log(`[${HOOK_NAME3}] warned on read (soft)`, { sessionID: input.sessionID });
55746
+ return;
55747
+ }
55748
+ if (isGrepGlob) {
55749
+ if (!isBlocked)
55750
+ return;
55751
+ if (enforce) {
55752
+ log(`[${HOOK_NAME3}] BLOCKED ${tool}`, { sessionID: input.sessionID });
55753
+ throw new Error(BLOCK_MESSAGE_GREP_GLOB);
55754
+ }
55755
+ output.message = WARN_MESSAGE_READ;
55756
+ log(`[${HOOK_NAME3}] warned on ${tool} (soft)`, { sessionID: input.sessionID });
55757
+ }
55758
+ }
55759
+ };
55760
+ }
55578
55761
  // src/hooks/context-window-monitor.ts
55579
55762
  var ANTHROPIC_DISPLAY_LIMIT = 1e6;
55580
55763
  var ANTHROPIC_ACTUAL_LIMIT2 = process.env.ANTHROPIC_1M_CONTEXT === "true" || process.env.VERTEX_ANTHROPIC_1M_CONTEXT === "true" ? 1e6 : 200000;
@@ -55767,7 +55950,7 @@ ${guidance}`;
55767
55950
  import { join as join35 } from "path";
55768
55951
 
55769
55952
  // src/hooks/design-intent-preserver/constants.ts
55770
- var HOOK_NAME3 = "design-intent-preserver";
55953
+ var HOOK_NAME4 = "design-intent-preserver";
55771
55954
  var PLANS_DIR2 = ".matrixx/plans";
55772
55955
  var PHASE_TRANSITION_PATTERN = /\bphase[\s\-:]*(\d+)\b/i;
55773
55956
  var CONSTRUCT_INDICATORS = /\b(?:construct|web[\s-]?designer|frontend[\s-]?ui[\s-]?ux)\b|@designer/i;
@@ -55854,7 +56037,7 @@ function createDesignIntentPreserverHook(_ctx) {
55854
56037
  ---
55855
56038
 
55856
56039
  ${originalText}`;
55857
- log(`[${HOOK_NAME3}] injected design intent guardrail`, {
56040
+ log(`[${HOOK_NAME4}] injected design intent guardrail`, {
55858
56041
  sessionID: input.sessionID,
55859
56042
  planPath
55860
56043
  });
@@ -59401,18 +59584,18 @@ function extractPromptText3(parts) {
59401
59584
  }
59402
59585
  // src/features/ultrawork-state/manager.ts
59403
59586
  init_logger();
59404
- var HOOK_NAME4 = "ultrawork-state";
59587
+ var HOOK_NAME5 = "ultrawork-state";
59405
59588
  var sessionStates = new Map;
59406
59589
  function getUltraworkState(sessionID) {
59407
59590
  return sessionStates.get(sessionID);
59408
59591
  }
59409
59592
  function enableUltrawork(sessionID) {
59410
59593
  sessionStates.set(sessionID, "enabled");
59411
- log(`[${HOOK_NAME4}] Ultrawork enabled for session`, { sessionID });
59594
+ log(`[${HOOK_NAME5}] Ultrawork enabled for session`, { sessionID });
59412
59595
  }
59413
59596
  function disableUltrawork(sessionID) {
59414
59597
  sessionStates.set(sessionID, "disabled");
59415
- log(`[${HOOK_NAME4}] Ultrawork disabled for session`, { sessionID });
59598
+ log(`[${HOOK_NAME5}] Ultrawork disabled for session`, { sessionID });
59416
59599
  }
59417
59600
  // src/hooks/keyword-detector/hook.ts
59418
59601
  function createKeywordDetectorHook(ctx, _collector) {
@@ -59545,7 +59728,7 @@ function createLoopStateController(options) {
59545
59728
  };
59546
59729
  const success2 = writeState(directory, state3, stateDir);
59547
59730
  if (success2) {
59548
- log(`[${HOOK_NAME5}] Loop started`, {
59731
+ log(`[${HOOK_NAME6}] Loop started`, {
59549
59732
  sessionID,
59550
59733
  maxIterations: state3.max_iterations,
59551
59734
  completionPromise: state3.completion_promise
@@ -59560,7 +59743,7 @@ function createLoopStateController(options) {
59560
59743
  }
59561
59744
  const success2 = clearState(directory, stateDir);
59562
59745
  if (success2) {
59563
- log(`[${HOOK_NAME5}] Loop cancelled`, { sessionID, iteration: state3.iteration });
59746
+ log(`[${HOOK_NAME6}] Loop cancelled`, { sessionID, iteration: state3.iteration });
59564
59747
  }
59565
59748
  return success2;
59566
59749
  },
@@ -59637,7 +59820,7 @@ async function detectCompletionInSessionMessages(ctx, options) {
59637
59820
  return false;
59638
59821
  } catch (err) {
59639
59822
  setTimeout(() => {
59640
- log(`[${HOOK_NAME5}] Session messages check failed`, {
59823
+ log(`[${HOOK_NAME6}] Session messages check failed`, {
59641
59824
  sessionID: options.sessionID,
59642
59825
  error: String(err)
59643
59826
  });
@@ -59718,7 +59901,7 @@ async function verifyCompletion(ctx, options) {
59718
59901
  query: { directory: options.directory }
59719
59902
  });
59720
59903
  if (createResult.error || !createResult.data?.id) {
59721
- log(`[${HOOK_NAME5}] Verification session creation failed`);
59904
+ log(`[${HOOK_NAME6}] Verification session creation failed`);
59722
59905
  return { verified: true };
59723
59906
  }
59724
59907
  verifySessionID = createResult.data.id;
@@ -59748,16 +59931,16 @@ async function verifyCompletion(ctx, options) {
59748
59931
  return parseVerificationResponse(assistantText);
59749
59932
  }
59750
59933
  } catch {
59751
- log(`[${HOOK_NAME5}] Verification poll error`);
59934
+ log(`[${HOOK_NAME6}] Verification poll error`);
59752
59935
  }
59753
59936
  await new Promise((resolve5) => {
59754
59937
  setTimeout(resolve5, pollIntervalMs);
59755
59938
  });
59756
59939
  }
59757
- log(`[${HOOK_NAME5}] Verification timed out, assuming pass`);
59940
+ log(`[${HOOK_NAME6}] Verification timed out, assuming pass`);
59758
59941
  return { verified: true };
59759
59942
  } catch (err) {
59760
- log(`[${HOOK_NAME5}] Verification error: ${String(err)}`);
59943
+ log(`[${HOOK_NAME6}] Verification error: ${String(err)}`);
59761
59944
  return { verified: true };
59762
59945
  } finally {
59763
59946
  if (verifySessionID) {
@@ -59833,7 +60016,7 @@ async function injectContinuationPrompt(ctx, options) {
59833
60016
 
59834
60017
  // src/hooks/matrix-loop/matrix-loop-event-handler.ts
59835
60018
  async function handleVerificationFailure(ctx, options, state3, sessionID, reason, failedCount) {
59836
- log(`[${HOOK_NAME5}] Verification failed`, { sessionID, reason });
60019
+ log(`[${HOOK_NAME6}] Verification failed`, { sessionID, reason });
59837
60020
  const newState = options.loopState.incrementIteration();
59838
60021
  if (newState) {
59839
60022
  newState.verification_failed_count = failedCount + 1;
@@ -59851,7 +60034,7 @@ ${buildContinuationPrompt(newState ?? state3)}`,
59851
60034
  apiTimeoutMs: options.apiTimeoutMs
59852
60035
  });
59853
60036
  } catch (err) {
59854
- log(`[${HOOK_NAME5}] Failed to inject verification feedback`, { sessionID, error: String(err) });
60037
+ log(`[${HOOK_NAME6}] Failed to inject verification feedback`, { sessionID, error: String(err) });
59855
60038
  }
59856
60039
  }
59857
60040
  function createMatrixLoopEventHandler(ctx, options) {
@@ -59863,13 +60046,13 @@ function createMatrixLoopEventHandler(ctx, options) {
59863
60046
  if (!sessionID)
59864
60047
  return;
59865
60048
  if (inFlightSessions.has(sessionID)) {
59866
- log(`[${HOOK_NAME5}] Skipped: handler in flight`, { sessionID });
60049
+ log(`[${HOOK_NAME6}] Skipped: handler in flight`, { sessionID });
59867
60050
  return;
59868
60051
  }
59869
60052
  inFlightSessions.add(sessionID);
59870
60053
  try {
59871
60054
  if (options.sessionRecovery.isRecovering(sessionID)) {
59872
- log(`[${HOOK_NAME5}] Skipped: in recovery`, { sessionID });
60055
+ log(`[${HOOK_NAME6}] Skipped: in recovery`, { sessionID });
59873
60056
  return;
59874
60057
  }
59875
60058
  const state3 = options.loopState.getState();
@@ -59881,11 +60064,11 @@ function createMatrixLoopEventHandler(ctx, options) {
59881
60064
  const exists = await options.checkSessionExists(state3.session_id);
59882
60065
  if (!exists) {
59883
60066
  options.loopState.clear();
59884
- log(`[${HOOK_NAME5}] Cleared orphaned state from deleted session`, { orphanedSessionId: state3.session_id, currentSessionId: sessionID });
60067
+ log(`[${HOOK_NAME6}] Cleared orphaned state from deleted session`, { orphanedSessionId: state3.session_id, currentSessionId: sessionID });
59885
60068
  return;
59886
60069
  }
59887
60070
  } catch (err) {
59888
- log(`[${HOOK_NAME5}] Failed to check session existence`, { sessionId: state3.session_id, error: String(err) });
60071
+ log(`[${HOOK_NAME6}] Failed to check session existence`, { sessionId: state3.session_id, error: String(err) });
59889
60072
  }
59890
60073
  }
59891
60074
  return;
@@ -59904,7 +60087,7 @@ function createMatrixLoopEventHandler(ctx, options) {
59904
60087
  messages = Array.isArray(messagesResponse) ? messagesResponse : Array.isArray(responseData) ? responseData : [];
59905
60088
  } catch (err) {
59906
60089
  setTimeout(() => {
59907
- log(`[${HOOK_NAME5}] Session messages fetch failed`, { sessionID, error: String(err) });
60090
+ log(`[${HOOK_NAME6}] Session messages fetch failed`, { sessionID, error: String(err) });
59908
60091
  }, 0);
59909
60092
  messages = [];
59910
60093
  }
@@ -59917,7 +60100,7 @@ function createMatrixLoopEventHandler(ctx, options) {
59917
60100
  preFetchedMessages: messages
59918
60101
  });
59919
60102
  if (completionViaTranscript || completionViaApi) {
59920
- log(`[${HOOK_NAME5}] Completion detected!`, { sessionID, iteration: state3.iteration, promise: state3.completion_promise, detectedVia: completionViaTranscript ? "transcript_file" : "session_messages_api" });
60103
+ log(`[${HOOK_NAME6}] Completion detected!`, { sessionID, iteration: state3.iteration, promise: state3.completion_promise, detectedVia: completionViaTranscript ? "transcript_file" : "session_messages_api" });
59921
60104
  if (options.verification?.enabled === true) {
59922
60105
  const maxRetries = options.verification?.maxRetries ?? DEFAULT_VERIFICATION_MAX_RETRIES;
59923
60106
  const failedCount = state3.verification_failed_count ?? 0;
@@ -59952,7 +60135,7 @@ function createMatrixLoopEventHandler(ctx, options) {
59952
60135
  return;
59953
60136
  }
59954
60137
  if (state3.iteration >= state3.max_iterations) {
59955
- log(`[${HOOK_NAME5}] Max iterations reached`, { sessionID, iteration: state3.iteration, max: state3.max_iterations });
60138
+ log(`[${HOOK_NAME6}] Max iterations reached`, { sessionID, iteration: state3.iteration, max: state3.max_iterations });
59956
60139
  options.loopState.clear();
59957
60140
  await ctx.client.tui.showToast({ body: { title: "Matrix Loop Stopped", message: `Max iterations (${state3.max_iterations}) reached without completion`, variant: "warning", duration: 5000 } }).catch((err) => {
59958
60141
  log("[matrix-loop] Max iterations toast failed:", err);
@@ -59961,17 +60144,17 @@ function createMatrixLoopEventHandler(ctx, options) {
59961
60144
  }
59962
60145
  const newState = options.loopState.incrementIteration();
59963
60146
  if (!newState) {
59964
- log(`[${HOOK_NAME5}] Failed to increment iteration`, { sessionID });
60147
+ log(`[${HOOK_NAME6}] Failed to increment iteration`, { sessionID });
59965
60148
  return;
59966
60149
  }
59967
- log(`[${HOOK_NAME5}] Continuing loop`, { sessionID, iteration: newState.iteration, max: newState.max_iterations });
60150
+ log(`[${HOOK_NAME6}] Continuing loop`, { sessionID, iteration: newState.iteration, max: newState.max_iterations });
59968
60151
  await ctx.client.tui.showToast({ body: { title: "Matrix Loop", message: `Iteration ${newState.iteration}/${newState.max_iterations}`, variant: "info", duration: 2000 } }).catch((err) => {
59969
60152
  log("[matrix-loop] Iteration toast failed:", err);
59970
60153
  });
59971
60154
  try {
59972
60155
  await injectContinuationPrompt(ctx, { sessionID, prompt: buildContinuationPrompt(newState), directory: options.directory, apiTimeoutMs: options.apiTimeoutMs });
59973
60156
  } catch (err) {
59974
- log(`[${HOOK_NAME5}] Failed to inject continuation`, { sessionID, error: String(err) });
60157
+ log(`[${HOOK_NAME6}] Failed to inject continuation`, { sessionID, error: String(err) });
59975
60158
  }
59976
60159
  return;
59977
60160
  } finally {
@@ -59985,7 +60168,7 @@ function createMatrixLoopEventHandler(ctx, options) {
59985
60168
  const state3 = options.loopState.getState();
59986
60169
  if (state3?.session_id === sessionInfo.id) {
59987
60170
  options.loopState.clear();
59988
- log(`[${HOOK_NAME5}] Session deleted, loop cleared`, { sessionID: sessionInfo.id });
60171
+ log(`[${HOOK_NAME6}] Session deleted, loop cleared`, { sessionID: sessionInfo.id });
59989
60172
  }
59990
60173
  options.sessionRecovery.clear(sessionInfo.id);
59991
60174
  return;
@@ -59998,7 +60181,7 @@ function createMatrixLoopEventHandler(ctx, options) {
59998
60181
  const state3 = options.loopState.getState();
59999
60182
  if (state3?.session_id === sessionID) {
60000
60183
  options.loopState.clear();
60001
- log(`[${HOOK_NAME5}] User aborted, loop cleared`, { sessionID });
60184
+ log(`[${HOOK_NAME6}] User aborted, loop cleared`, { sessionID });
60002
60185
  }
60003
60186
  options.sessionRecovery.clear(sessionID);
60004
60187
  }
@@ -60091,7 +60274,7 @@ function createMcpStartupNotificationHook(ctx) {
60091
60274
  };
60092
60275
  }
60093
60276
  // src/hooks/mouse-notepad/constants.ts
60094
- var HOOK_NAME6 = "mouse-notepad";
60277
+ var HOOK_NAME7 = "mouse-notepad";
60095
60278
  var NOTEPAD_DIRECTIVE = `
60096
60279
  <Work_Context>
60097
60280
  ## Notepad Location (for recording learnings)
@@ -60138,14 +60321,14 @@ function createMouseNotepadHook(ctx) {
60138
60321
  return;
60139
60322
  }
60140
60323
  output.args.prompt = NOTEPAD_DIRECTIVE + prompt;
60141
- log(`[${HOOK_NAME6}] Injected notepad directive to task`, {
60324
+ log(`[${HOOK_NAME7}] Injected notepad directive to task`, {
60142
60325
  sessionID: input.sessionID
60143
60326
  });
60144
60327
  }
60145
60328
  };
60146
60329
  }
60147
60330
  // src/hooks/non-interactive-env/constants.ts
60148
- var HOOK_NAME7 = "non-interactive-env";
60331
+ var HOOK_NAME8 = "non-interactive-env";
60149
60332
  var NON_INTERACTIVE_ENV = {
60150
60333
  CI: "true",
60151
60334
  DEBIAN_FRONTEND: "noninteractive",
@@ -60236,7 +60419,7 @@ function createNonInteractiveEnvHook(_ctx) {
60236
60419
  }
60237
60420
  const envPrefix = buildEnvPrefix(NON_INTERACTIVE_ENV, "unix");
60238
60421
  output.args.command = `${envPrefix} ${command}`;
60239
- log(`[${HOOK_NAME7}] Prepended non-interactive env vars to git command`, {
60422
+ log(`[${HOOK_NAME8}] Prepended non-interactive env vars to git command`, {
60240
60423
  sessionID: input.sessionID,
60241
60424
  envPrefix
60242
60425
  });
@@ -60244,7 +60427,7 @@ function createNonInteractiveEnvHook(_ctx) {
60244
60427
  };
60245
60428
  }
60246
60429
  // src/hooks/oracle-md-only/constants.ts
60247
- var HOOK_NAME8 = "oracle-md-only";
60430
+ var HOOK_NAME9 = "oracle-md-only";
60248
60431
  var ORACLE_AGENT = "oracle";
60249
60432
  var ALLOWED_EXTENSIONS = [".md"];
60250
60433
  var BLOCKED_TOOLS = ["Write", "Edit", "write", "edit"];
@@ -60377,7 +60560,7 @@ function createOracleMdOnlyHook(ctx) {
60377
60560
  const prompt = output.args.prompt;
60378
60561
  if (prompt && !prompt.includes(SYSTEM_DIRECTIVE_PREFIX)) {
60379
60562
  output.args.prompt = PLANNING_CONSULT_WARNING + prompt;
60380
- log(`[${HOOK_NAME8}] Injected read-only planning warning to ${toolName}`, {
60563
+ log(`[${HOOK_NAME9}] Injected read-only planning warning to ${toolName}`, {
60381
60564
  sessionID: input.sessionID,
60382
60565
  tool: toolName,
60383
60566
  agent: agentName
@@ -60393,17 +60576,17 @@ function createOracleMdOnlyHook(ctx) {
60393
60576
  return;
60394
60577
  }
60395
60578
  if (!isAllowedFile(filePath, ctx.directory)) {
60396
- log(`[${HOOK_NAME8}] Blocked: Oracle can only write to .matrixx/*.md`, {
60579
+ log(`[${HOOK_NAME9}] Blocked: Oracle can only write to .matrixx/*.md`, {
60397
60580
  sessionID: input.sessionID,
60398
60581
  tool: toolName,
60399
60582
  filePath,
60400
60583
  agent: agentName
60401
60584
  });
60402
- throw new Error(`[${HOOK_NAME8}] ${getAgentDisplayName("oracle")} can only write/edit .md files inside .matrixx/ directory. ` + `Attempted to modify: ${filePath}. ` + `${getAgentDisplayName("oracle")} is a READ-ONLY planner. Use /start-work to execute the plan. ` + `APOLOGIZE TO THE USER, REMIND OF YOUR PLAN WRITING PROCESSES, TELL USER WHAT YOU WILL GOING TO DO AS THE PROCESS, WRITE THE PLAN`);
60585
+ throw new Error(`[${HOOK_NAME9}] ${getAgentDisplayName("oracle")} can only write/edit .md files inside .matrixx/ directory. ` + `Attempted to modify: ${filePath}. ` + `${getAgentDisplayName("oracle")} is a READ-ONLY planner. Use /start-work to execute the plan. ` + `APOLOGIZE TO THE USER, REMIND OF YOUR PLAN WRITING PROCESSES, TELL USER WHAT YOU WILL GOING TO DO AS THE PROCESS, WRITE THE PLAN`);
60403
60586
  }
60404
60587
  const normalizedPath = filePath.toLowerCase().replace(/\\/g, "/");
60405
60588
  if (normalizedPath.includes(".matrixx/plans/") || normalizedPath.includes(".matrixx\\plans\\")) {
60406
- log(`[${HOOK_NAME8}] Injecting workflow reminder for plan write`, {
60589
+ log(`[${HOOK_NAME9}] Injecting workflow reminder for plan write`, {
60407
60590
  sessionID: input.sessionID,
60408
60591
  tool: toolName,
60409
60592
  filePath,
@@ -60411,7 +60594,7 @@ function createOracleMdOnlyHook(ctx) {
60411
60594
  });
60412
60595
  output.message = (output.message || "") + ORACLE_WORKFLOW_REMINDER;
60413
60596
  }
60414
- log(`[${HOOK_NAME8}] Allowed: .matrixx/*.md write permitted`, {
60597
+ log(`[${HOOK_NAME9}] Allowed: .matrixx/*.md write permitted`, {
60415
60598
  sessionID: input.sessionID,
60416
60599
  tool: toolName,
60417
60600
  filePath,
@@ -60482,7 +60665,7 @@ async function getGitHead(directory) {
60482
60665
  }
60483
60666
 
60484
60667
  // src/hooks/plan-persister/hook.ts
60485
- var HOOK_NAME9 = "plan-persister";
60668
+ var HOOK_NAME10 = "plan-persister";
60486
60669
  function createPlanPersister(ctx, options) {
60487
60670
  const { directory } = options;
60488
60671
  const capture = async (sessionID) => {
@@ -60501,7 +60684,7 @@ function createPlanPersister(ctx, options) {
60501
60684
  const response = await ctx.client.session.todo({ path: { id: sessionID } });
60502
60685
  todos = normalizeTodos(response);
60503
60686
  } catch (err) {
60504
- log(`[${HOOK_NAME9}] Failed to fetch todos`, { sessionID, error: String(err) });
60687
+ log(`[${HOOK_NAME10}] Failed to fetch todos`, { sessionID, error: String(err) });
60505
60688
  return;
60506
60689
  }
60507
60690
  const content = readPlanFile(planPath);
@@ -60523,9 +60706,9 @@ function createPlanPersister(ctx, options) {
60523
60706
  ensurePlanDir(directory);
60524
60707
  const ok = atomicWrite(planPath, finalContent);
60525
60708
  if (ok) {
60526
- log(`[${HOOK_NAME9}] Plan file updated`, { planPath, completed, total });
60709
+ log(`[${HOOK_NAME10}] Plan file updated`, { planPath, completed, total });
60527
60710
  } else {
60528
- log(`[${HOOK_NAME9}] Failed to write plan file`, { planPath });
60711
+ log(`[${HOOK_NAME10}] Failed to write plan file`, { planPath });
60529
60712
  }
60530
60713
  };
60531
60714
  const event = async ({ event: evt }) => {
@@ -61823,13 +62006,11 @@ function createReadImageResizerHook(_ctx) {
61823
62006
  }
61824
62007
  // src/hooks/rtk-bash-rewriter/constants.ts
61825
62008
  var RTK_BINARY_NAME = "rtk";
61826
- var DEFAULT_TIMEOUT_MS = 5000;
61827
62009
  var RTK_REWRITE_COMMAND = "rewrite";
61828
62010
 
61829
62011
  // src/hooks/rtk-bash-rewriter/hook.ts
61830
62012
  function createRtkBashRewriterHook(_ctx, config2) {
61831
62013
  const rtkConfig = config2.rtk;
61832
- const _timeoutMs = rtkConfig?.timeout_ms ?? DEFAULT_TIMEOUT_MS;
61833
62014
  const binaryPath = rtkConfig?.binary_path ?? RTK_BINARY_NAME;
61834
62015
  const hasRtk = Bun.which(binaryPath) !== null;
61835
62016
  if (!hasRtk) {
@@ -62528,7 +62709,7 @@ var RETRYABLE_ERROR_PATTERNS = [
62528
62709
  /(?:^|\s)503(?:\s|$)/,
62529
62710
  /(?:^|\s)529(?:\s|$)/
62530
62711
  ];
62531
- var HOOK_NAME10 = "runtime-fallback";
62712
+ var HOOK_NAME11 = "runtime-fallback";
62532
62713
 
62533
62714
  // src/hooks/runtime-fallback/fallback-models.ts
62534
62715
  init_logger();
@@ -62619,7 +62800,7 @@ function getRawFallbackModelsForSession(sessionID, agent, pluginConfig) {
62619
62800
  if (result)
62620
62801
  return result;
62621
62802
  }
62622
- log(`[${HOOK_NAME10}] No category/agent fallback models resolved for session`, { sessionID, agent });
62803
+ log(`[${HOOK_NAME11}] No category/agent fallback models resolved for session`, { sessionID, agent });
62623
62804
  return;
62624
62805
  }
62625
62806
 
@@ -62648,21 +62829,21 @@ function findNextAvailableFallback(state3, fallbackModels, cooldownSeconds) {
62648
62829
  if (!isModelInCooldown(candidate, state3, cooldownSeconds)) {
62649
62830
  return candidate;
62650
62831
  }
62651
- log(`[${HOOK_NAME10}] Skipping fallback model in cooldown`, { model: candidate, index: i });
62832
+ log(`[${HOOK_NAME11}] Skipping fallback model in cooldown`, { model: candidate, index: i });
62652
62833
  }
62653
62834
  return;
62654
62835
  }
62655
62836
  function prepareFallback(sessionID, state3, fallbackModels, config2) {
62656
62837
  if (state3.attemptCount >= config2.max_fallback_attempts) {
62657
- log(`[${HOOK_NAME10}] Max fallback attempts reached`, { sessionID, attempts: state3.attemptCount });
62838
+ log(`[${HOOK_NAME11}] Max fallback attempts reached`, { sessionID, attempts: state3.attemptCount });
62658
62839
  return { success: false, error: "Max fallback attempts reached", maxAttemptsReached: true };
62659
62840
  }
62660
62841
  const nextModel = findNextAvailableFallback(state3, fallbackModels, config2.cooldown_seconds);
62661
62842
  if (!nextModel) {
62662
- log(`[${HOOK_NAME10}] No available fallback models`, { sessionID });
62843
+ log(`[${HOOK_NAME11}] No available fallback models`, { sessionID });
62663
62844
  return { success: false, error: "No available fallback models (all in cooldown or exhausted)" };
62664
62845
  }
62665
- log(`[${HOOK_NAME10}] Preparing fallback`, {
62846
+ log(`[${HOOK_NAME11}] Preparing fallback`, {
62666
62847
  sessionID,
62667
62848
  from: state3.currentModel,
62668
62849
  to: nextModel,
@@ -62748,9 +62929,9 @@ function createAutoRetryHelpers(deps) {
62748
62929
  const abortSessionRequest = async (sessionID, source) => {
62749
62930
  try {
62750
62931
  await ctx.client.session.abort({ path: { id: sessionID } });
62751
- log(`[${HOOK_NAME10}] Aborted in-flight session request (${source})`, { sessionID });
62932
+ log(`[${HOOK_NAME11}] Aborted in-flight session request (${source})`, { sessionID });
62752
62933
  } catch (error51) {
62753
- log(`[${HOOK_NAME10}] Failed to abort in-flight session request (${source})`, {
62934
+ log(`[${HOOK_NAME11}] Failed to abort in-flight session request (${source})`, {
62754
62935
  sessionID,
62755
62936
  error: String(error51)
62756
62937
  });
@@ -62774,7 +62955,7 @@ function createAutoRetryHelpers(deps) {
62774
62955
  if (!state3)
62775
62956
  return;
62776
62957
  if (sessionRetryInFlight.has(sessionID)) {
62777
- log(`[${HOOK_NAME10}] Overriding in-flight retry due to session timeout`, { sessionID });
62958
+ log(`[${HOOK_NAME11}] Overriding in-flight retry due to session timeout`, { sessionID });
62778
62959
  }
62779
62960
  await abortSessionRequest(sessionID, "session.timeout");
62780
62961
  sessionRetryInFlight.delete(sessionID);
@@ -62784,7 +62965,7 @@ function createAutoRetryHelpers(deps) {
62784
62965
  const fallbackModels = getFallbackModelsForSession(sessionID, resolvedAgent, pluginConfig);
62785
62966
  if (fallbackModels.length === 0)
62786
62967
  return;
62787
- log(`[${HOOK_NAME10}] Session fallback timeout reached`, {
62968
+ log(`[${HOOK_NAME11}] Session fallback timeout reached`, {
62788
62969
  sessionID,
62789
62970
  timeoutSeconds: config2.timeout_seconds,
62790
62971
  currentModel: state3.currentModel
@@ -62798,7 +62979,7 @@ function createAutoRetryHelpers(deps) {
62798
62979
  };
62799
62980
  const autoRetryWithFallback = async (sessionID, newModel, resolvedAgent, source) => {
62800
62981
  if (sessionRetryInFlight.has(sessionID)) {
62801
- log(`[${HOOK_NAME10}] Retry already in flight, skipping (${source})`, { sessionID });
62982
+ log(`[${HOOK_NAME11}] Retry already in flight, skipping (${source})`, { sessionID });
62802
62983
  return;
62803
62984
  }
62804
62985
  const agentSettings = resolvedAgent ? pluginConfig?.agents?.[resolvedAgent] : undefined;
@@ -62807,7 +62988,7 @@ function createAutoRetryHelpers(deps) {
62807
62988
  reasoningEffort: agentSettings.reasoningEffort
62808
62989
  } : undefined);
62809
62990
  if (!retryModelPayload) {
62810
- log(`[${HOOK_NAME10}] Invalid model format (missing provider prefix): ${newModel}`);
62991
+ log(`[${HOOK_NAME11}] Invalid model format (missing provider prefix): ${newModel}`);
62811
62992
  const state3 = sessionStates2.get(sessionID);
62812
62993
  if (state3?.pendingFallbackModel) {
62813
62994
  state3.pendingFallbackModel = undefined;
@@ -62823,7 +63004,7 @@ function createAutoRetryHelpers(deps) {
62823
63004
  });
62824
63005
  const retryParts = getLastUserRetryParts(messagesResp);
62825
63006
  if (retryParts.length > 0) {
62826
- log(`[${HOOK_NAME10}] Auto-retrying with fallback model (${source})`, {
63007
+ log(`[${HOOK_NAME11}] Auto-retrying with fallback model (${source})`, {
62827
63008
  sessionID,
62828
63009
  model: newModel
62829
63010
  });
@@ -62842,10 +63023,10 @@ function createAutoRetryHelpers(deps) {
62842
63023
  });
62843
63024
  retryDispatched = true;
62844
63025
  } else {
62845
- log(`[${HOOK_NAME10}] No user message found for auto-retry (${source})`, { sessionID });
63026
+ log(`[${HOOK_NAME11}] No user message found for auto-retry (${source})`, { sessionID });
62846
63027
  }
62847
63028
  } catch (retryError) {
62848
- log(`[${HOOK_NAME10}] Auto-retry failed (${source})`, { sessionID, error: String(retryError) });
63029
+ log(`[${HOOK_NAME11}] Auto-retry failed (${source})`, { sessionID, error: String(retryError) });
62849
63030
  } finally {
62850
63031
  sessionRetryInFlight.delete(sessionID);
62851
63032
  if (!retryDispatched) {
@@ -62899,7 +63080,7 @@ function createAutoRetryHelpers(deps) {
62899
63080
  }
62900
63081
  }
62901
63082
  if (cleanedCount > 0) {
62902
- log(`[${HOOK_NAME10}] Cleaned up ${cleanedCount} stale session states`);
63083
+ log(`[${HOOK_NAME11}] Cleaned up ${cleanedCount} stale session states`);
62903
63084
  }
62904
63085
  };
62905
63086
  return {
@@ -62930,7 +63111,7 @@ function createChatMessageHandler(deps) {
62930
63111
  state3.pendingFallbackModel = undefined;
62931
63112
  return;
62932
63113
  }
62933
- log(`[${HOOK_NAME10}] Detected manual model change, resetting fallback state`, {
63114
+ log(`[${HOOK_NAME11}] Detected manual model change, resetting fallback state`, {
62934
63115
  sessionID,
62935
63116
  from: state3.currentModel,
62936
63117
  to: requestedModel
@@ -62942,7 +63123,7 @@ function createChatMessageHandler(deps) {
62942
63123
  if (state3.currentModel === state3.originalModel)
62943
63124
  return;
62944
63125
  const activeModel = state3.currentModel;
62945
- log(`[${HOOK_NAME10}] Applying fallback model override`, {
63126
+ log(`[${HOOK_NAME11}] Applying fallback model override`, {
62946
63127
  sessionID,
62947
63128
  from: input.model,
62948
63129
  to: activeModel
@@ -63129,7 +63310,7 @@ function resolveFallbackBootstrapModel(options) {
63129
63310
  const agentConfig = options.resolvedAgent && agentConfigs ? agentConfigs[options.resolvedAgent] : undefined;
63130
63311
  const agentModel = typeof agentConfig?.model === "string" ? agentConfig.model : undefined;
63131
63312
  if (agentModel) {
63132
- log(`[${HOOK_NAME10}] Derived model from agent config for ${options.source}`, {
63313
+ log(`[${HOOK_NAME11}] Derived model from agent config for ${options.source}`, {
63133
63314
  sessionID: options.sessionID,
63134
63315
  agent: options.resolvedAgent,
63135
63316
  model: agentModel
@@ -63140,7 +63321,7 @@ function resolveFallbackBootstrapModel(options) {
63140
63321
  if (agentCategory) {
63141
63322
  const agentCategoryModel = options.pluginConfig?.categories?.[agentCategory]?.model;
63142
63323
  if (typeof agentCategoryModel === "string" && agentCategoryModel.length > 0) {
63143
- log(`[${HOOK_NAME10}] Derived model from agent category config for ${options.source}`, {
63324
+ log(`[${HOOK_NAME11}] Derived model from agent category config for ${options.source}`, {
63144
63325
  sessionID: options.sessionID,
63145
63326
  agent: options.resolvedAgent,
63146
63327
  category: agentCategory,
@@ -63152,7 +63333,7 @@ function resolveFallbackBootstrapModel(options) {
63152
63333
  const sessionCategory = SessionCategoryRegistry.get(options.sessionID);
63153
63334
  const categoryModel = sessionCategory ? options.pluginConfig?.categories?.[sessionCategory]?.model : undefined;
63154
63335
  if (typeof categoryModel === "string" && categoryModel.length > 0) {
63155
- log(`[${HOOK_NAME10}] Derived model from session category config for ${options.source}`, {
63336
+ log(`[${HOOK_NAME11}] Derived model from session category config for ${options.source}`, {
63156
63337
  sessionID: options.sessionID,
63157
63338
  category: sessionCategory,
63158
63339
  model: categoryModel
@@ -63182,7 +63363,7 @@ async function dispatchFallbackRetry(deps, helpers, options) {
63182
63363
  await helpers.autoRetryWithFallback(options.sessionID, result.newModel, options.resolvedAgent, options.source);
63183
63364
  return;
63184
63365
  }
63185
- log(`[${HOOK_NAME10}] Fallback preparation failed`, {
63366
+ log(`[${HOOK_NAME11}] Fallback preparation failed`, {
63186
63367
  sessionID: options.sessionID,
63187
63368
  source: options.source,
63188
63369
  error: result.error
@@ -63235,14 +63416,14 @@ function createSessionStatusHandler(deps, helpers, sessionStatusRetryKeys) {
63235
63416
  sessionStatusRetryKeys.set(sessionID, retryKey);
63236
63417
  if (sessionRetryInFlight.has(sessionID)) {
63237
63418
  if (timeoutEnabled) {
63238
- log(`[${HOOK_NAME10}] Overriding in-flight retry due to provider auto-retry signal`, {
63419
+ log(`[${HOOK_NAME11}] Overriding in-flight retry due to provider auto-retry signal`, {
63239
63420
  sessionID,
63240
63421
  model
63241
63422
  });
63242
63423
  await helpers.abortSessionRequest(sessionID, "session.status.retry-signal");
63243
63424
  sessionRetryInFlight.delete(sessionID);
63244
63425
  } else {
63245
- log(`[${HOOK_NAME10}] session.status retry skipped - retry already in flight`, { sessionID });
63426
+ log(`[${HOOK_NAME11}] session.status retry skipped - retry already in flight`, { sessionID });
63246
63427
  return;
63247
63428
  }
63248
63429
  }
@@ -63265,7 +63446,7 @@ function createSessionStatusHandler(deps, helpers, sessionStatusRetryKeys) {
63265
63446
  });
63266
63447
  if (!initialModel) {
63267
63448
  sessionStatusRetryKeys.delete(sessionID);
63268
- log(`[${HOOK_NAME10}] session.status retry missing model info, cannot fallback`, { sessionID });
63449
+ log(`[${HOOK_NAME11}] session.status retry missing model info, cannot fallback`, { sessionID });
63269
63450
  return;
63270
63451
  }
63271
63452
  state3 = createFallbackState(initialModel);
@@ -63274,20 +63455,20 @@ function createSessionStatusHandler(deps, helpers, sessionStatusRetryKeys) {
63274
63455
  sessionLastAccess.set(sessionID, Date.now());
63275
63456
  if (state3.pendingFallbackModel) {
63276
63457
  if (timeoutEnabled) {
63277
- log(`[${HOOK_NAME10}] Clearing pending fallback due to provider auto-retry signal`, {
63458
+ log(`[${HOOK_NAME11}] Clearing pending fallback due to provider auto-retry signal`, {
63278
63459
  sessionID,
63279
63460
  pendingFallbackModel: state3.pendingFallbackModel
63280
63461
  });
63281
63462
  state3.pendingFallbackModel = undefined;
63282
63463
  } else {
63283
- log(`[${HOOK_NAME10}] session.status retry skipped (pending fallback in progress)`, {
63464
+ log(`[${HOOK_NAME11}] session.status retry skipped (pending fallback in progress)`, {
63284
63465
  sessionID,
63285
63466
  pendingFallbackModel: state3.pendingFallbackModel
63286
63467
  });
63287
63468
  return;
63288
63469
  }
63289
63470
  }
63290
- log(`[${HOOK_NAME10}] Detected provider auto-retry signal in session.status`, {
63471
+ log(`[${HOOK_NAME11}] Detected provider auto-retry signal in session.status`, {
63291
63472
  sessionID,
63292
63473
  model: state3.currentModel,
63293
63474
  retryAttempt: status.attempt
@@ -63323,7 +63504,7 @@ function createEventHandler(deps, helpers) {
63323
63504
  const sessionID = sessionInfo?.id;
63324
63505
  const model = sessionInfo?.model;
63325
63506
  if (sessionID && model) {
63326
- log(`[${HOOK_NAME10}] Session created with model`, { sessionID, model });
63507
+ log(`[${HOOK_NAME11}] Session created with model`, { sessionID, model });
63327
63508
  sessionStates2.set(sessionID, createFallbackState(model));
63328
63509
  sessionLastAccess.set(sessionID, Date.now());
63329
63510
  }
@@ -63332,7 +63513,7 @@ function createEventHandler(deps, helpers) {
63332
63513
  const sessionInfo = props?.info;
63333
63514
  const sessionID = sessionInfo?.id;
63334
63515
  if (sessionID) {
63335
- log(`[${HOOK_NAME10}] Cleaning up session state`, { sessionID });
63516
+ log(`[${HOOK_NAME11}] Cleaning up session state`, { sessionID });
63336
63517
  cancelledSessions.delete(sessionID);
63337
63518
  sessionStates2.delete(sessionID);
63338
63519
  sessionLastAccess.delete(sessionID);
@@ -63352,7 +63533,7 @@ function createEventHandler(deps, helpers) {
63352
63533
  }
63353
63534
  cancelledSessions.add(sessionID);
63354
63535
  resetRetryState(sessionID);
63355
- log(`[${HOOK_NAME10}] Cleared fallback retry state on session.stop`, { sessionID });
63536
+ log(`[${HOOK_NAME11}] Cleared fallback retry state on session.stop`, { sessionID });
63356
63537
  };
63357
63538
  const handleMessageUpdated = (props) => {
63358
63539
  const info = props?.info;
@@ -63368,11 +63549,11 @@ function createEventHandler(deps, helpers) {
63368
63549
  return;
63369
63550
  if (cancelledSessions.has(sessionID)) {
63370
63551
  resetRetryState(sessionID);
63371
- log(`[${HOOK_NAME10}] Cleared fallback retry state for cancelled session on idle`, { sessionID });
63552
+ log(`[${HOOK_NAME11}] Cleared fallback retry state for cancelled session on idle`, { sessionID });
63372
63553
  return;
63373
63554
  }
63374
63555
  if (sessionAwaitingFallbackResult.has(sessionID)) {
63375
- log(`[${HOOK_NAME10}] session.idle while awaiting fallback result; keeping timeout armed`, { sessionID });
63556
+ log(`[${HOOK_NAME11}] session.idle while awaiting fallback result; keeping timeout armed`, { sessionID });
63376
63557
  return;
63377
63558
  }
63378
63559
  const hadTimeout = sessionFallbackTimeouts.has(sessionID);
@@ -63384,7 +63565,7 @@ function createEventHandler(deps, helpers) {
63384
63565
  state3.pendingFallbackModel = undefined;
63385
63566
  }
63386
63567
  if (hadTimeout) {
63387
- log(`[${HOOK_NAME10}] Cleared fallback timeout after session completion`, { sessionID });
63568
+ log(`[${HOOK_NAME11}] Cleared fallback timeout after session completion`, { sessionID });
63388
63569
  }
63389
63570
  };
63390
63571
  const handleSessionError = async (props) => {
@@ -63392,18 +63573,18 @@ function createEventHandler(deps, helpers) {
63392
63573
  const error51 = props?.error;
63393
63574
  const agent = props?.agent;
63394
63575
  if (!sessionID) {
63395
- log(`[${HOOK_NAME10}] session.error without sessionID, skipping`);
63576
+ log(`[${HOOK_NAME11}] session.error without sessionID, skipping`);
63396
63577
  return;
63397
63578
  }
63398
63579
  const resolvedAgent = await helpers.resolveAgentForSessionFromContext(sessionID, agent);
63399
63580
  if (isAbortError(error51)) {
63400
63581
  cancelledSessions.add(sessionID);
63401
63582
  resetRetryState(sessionID);
63402
- log(`[${HOOK_NAME10}] session.error matched cancellation; cleared retry state`, { sessionID, resolvedAgent });
63583
+ log(`[${HOOK_NAME11}] session.error matched cancellation; cleared retry state`, { sessionID, resolvedAgent });
63403
63584
  return;
63404
63585
  }
63405
63586
  if (sessionRetryInFlight.has(sessionID)) {
63406
- log(`[${HOOK_NAME10}] session.error skipped - retry in flight`, {
63587
+ log(`[${HOOK_NAME11}] session.error skipped - retry in flight`, {
63407
63588
  sessionID,
63408
63589
  retryInFlight: true
63409
63590
  });
@@ -63411,7 +63592,7 @@ function createEventHandler(deps, helpers) {
63411
63592
  }
63412
63593
  sessionAwaitingFallbackResult.delete(sessionID);
63413
63594
  helpers.clearSessionFallbackTimeout(sessionID);
63414
- log(`[${HOOK_NAME10}] session.error received`, {
63595
+ log(`[${HOOK_NAME11}] session.error received`, {
63415
63596
  sessionID,
63416
63597
  agent,
63417
63598
  resolvedAgent,
@@ -63420,7 +63601,7 @@ function createEventHandler(deps, helpers) {
63420
63601
  errorType: classifyErrorType(error51)
63421
63602
  });
63422
63603
  if (!isRetryableError(error51, config2.retry_on_errors)) {
63423
- log(`[${HOOK_NAME10}] Error not retryable, skipping fallback`, {
63604
+ log(`[${HOOK_NAME11}] Error not retryable, skipping fallback`, {
63424
63605
  sessionID,
63425
63606
  retryable: false,
63426
63607
  statusCode: extractStatusCode(error51, config2.retry_on_errors),
@@ -63432,7 +63613,7 @@ function createEventHandler(deps, helpers) {
63432
63613
  let state3 = sessionStates2.get(sessionID);
63433
63614
  const fallbackModels = getFallbackModelsForSession(sessionID, resolvedAgent, pluginConfig);
63434
63615
  if (fallbackModels.length === 0) {
63435
- log(`[${HOOK_NAME10}] No fallback models configured`, { sessionID, agent });
63616
+ log(`[${HOOK_NAME11}] No fallback models configured`, { sessionID, agent });
63436
63617
  return;
63437
63618
  }
63438
63619
  if (!state3) {
@@ -63444,7 +63625,7 @@ function createEventHandler(deps, helpers) {
63444
63625
  pluginConfig
63445
63626
  });
63446
63627
  if (!initialModel) {
63447
- log(`[${HOOK_NAME10}] No model info available, cannot fallback`, { sessionID });
63628
+ log(`[${HOOK_NAME11}] No model info available, cannot fallback`, { sessionID });
63448
63629
  return;
63449
63630
  }
63450
63631
  state3 = createFallbackState(initialModel);
@@ -63584,7 +63765,7 @@ function createMessageUpdateHandler(deps, helpers) {
63584
63765
  }
63585
63766
  const hasVisible = await checkVisibleResponse(ctx, sessionID, info);
63586
63767
  if (!hasVisible) {
63587
- log(`[${HOOK_NAME10}] Assistant update observed without visible final response; keeping fallback timeout`, {
63768
+ log(`[${HOOK_NAME11}] Assistant update observed without visible final response; keeping fallback timeout`, {
63588
63769
  sessionID,
63589
63770
  model
63590
63771
  });
@@ -63597,17 +63778,17 @@ function createMessageUpdateHandler(deps, helpers) {
63597
63778
  if (state3?.pendingFallbackModel) {
63598
63779
  state3.pendingFallbackModel = undefined;
63599
63780
  }
63600
- log(`[${HOOK_NAME10}] Assistant response observed; cleared fallback timeout`, { sessionID, model });
63781
+ log(`[${HOOK_NAME11}] Assistant response observed; cleared fallback timeout`, { sessionID, model });
63601
63782
  return;
63602
63783
  }
63603
63784
  if (sessionID && role === "assistant" && error51) {
63604
63785
  sessionAwaitingFallbackResult.delete(sessionID);
63605
63786
  if (sessionRetryInFlight.has(sessionID) && !retrySignal) {
63606
- log(`[${HOOK_NAME10}] message.updated fallback skipped (retry in flight)`, { sessionID });
63787
+ log(`[${HOOK_NAME11}] message.updated fallback skipped (retry in flight)`, { sessionID });
63607
63788
  return;
63608
63789
  }
63609
63790
  if (retrySignal && sessionRetryInFlight.has(sessionID) && timeoutEnabled) {
63610
- log(`[${HOOK_NAME10}] Overriding in-flight retry due to provider auto-retry signal`, {
63791
+ log(`[${HOOK_NAME11}] Overriding in-flight retry due to provider auto-retry signal`, {
63611
63792
  sessionID,
63612
63793
  model
63613
63794
  });
@@ -63615,12 +63796,12 @@ function createMessageUpdateHandler(deps, helpers) {
63615
63796
  sessionRetryInFlight.delete(sessionID);
63616
63797
  }
63617
63798
  if (retrySignal && timeoutEnabled) {
63618
- log(`[${HOOK_NAME10}] Detected provider auto-retry signal`, { sessionID, model });
63799
+ log(`[${HOOK_NAME11}] Detected provider auto-retry signal`, { sessionID, model });
63619
63800
  }
63620
63801
  if (!retrySignal) {
63621
63802
  helpers.clearSessionFallbackTimeout(sessionID);
63622
63803
  }
63623
- log(`[${HOOK_NAME10}] message.updated with assistant error`, {
63804
+ log(`[${HOOK_NAME11}] message.updated with assistant error`, {
63624
63805
  sessionID,
63625
63806
  model,
63626
63807
  statusCode: extractStatusCode(error51, config2.retry_on_errors),
@@ -63628,7 +63809,7 @@ function createMessageUpdateHandler(deps, helpers) {
63628
63809
  errorType: classifyErrorType(error51)
63629
63810
  });
63630
63811
  if (!isRetryableError(error51, config2.retry_on_errors)) {
63631
- log(`[${HOOK_NAME10}] message.updated error not retryable, skipping fallback`, {
63812
+ log(`[${HOOK_NAME11}] message.updated error not retryable, skipping fallback`, {
63632
63813
  sessionID,
63633
63814
  statusCode: extractStatusCode(error51, config2.retry_on_errors),
63634
63815
  errorName: extractErrorName(error51),
@@ -63652,7 +63833,7 @@ function createMessageUpdateHandler(deps, helpers) {
63652
63833
  pluginConfig
63653
63834
  });
63654
63835
  if (!initialModel) {
63655
- log(`[${HOOK_NAME10}] message.updated missing model info, cannot fallback`, {
63836
+ log(`[${HOOK_NAME11}] message.updated missing model info, cannot fallback`, {
63656
63837
  sessionID,
63657
63838
  errorName: extractErrorName(error51),
63658
63839
  errorType: classifyErrorType(error51)
@@ -63666,13 +63847,13 @@ function createMessageUpdateHandler(deps, helpers) {
63666
63847
  sessionLastAccess.set(sessionID, Date.now());
63667
63848
  if (state3.pendingFallbackModel) {
63668
63849
  if (retrySignal && timeoutEnabled) {
63669
- log(`[${HOOK_NAME10}] Clearing pending fallback due to provider auto-retry signal`, {
63850
+ log(`[${HOOK_NAME11}] Clearing pending fallback due to provider auto-retry signal`, {
63670
63851
  sessionID,
63671
63852
  pendingFallbackModel: state3.pendingFallbackModel
63672
63853
  });
63673
63854
  state3.pendingFallbackModel = undefined;
63674
63855
  } else {
63675
- log(`[${HOOK_NAME10}] message.updated fallback skipped (pending fallback in progress)`, {
63856
+ log(`[${HOOK_NAME11}] message.updated fallback skipped (pending fallback in progress)`, {
63676
63857
  sessionID,
63677
63858
  pendingFallbackModel: state3.pendingFallbackModel
63678
63859
  });
@@ -64678,7 +64859,7 @@ function createSessionRecoveryHook(ctx, options) {
64678
64859
  }
64679
64860
  // src/hooks/start-work/start-work-hook.ts
64680
64861
  init_logger();
64681
- var HOOK_NAME11 = "start-work";
64862
+ var HOOK_NAME12 = "start-work";
64682
64863
  var KEYWORD_PATTERN = /\b(ultrawork|ulw)\b/gi;
64683
64864
  function extractUserRequestPlanName(promptText) {
64684
64865
  const userRequestMatch = promptText.match(/<user-request>\s*([\s\S]*?)\s*<\/user-request>/i);
@@ -64708,7 +64889,7 @@ function createStartWorkHook(ctx) {
64708
64889
  if (!isStartWorkCommand) {
64709
64890
  return;
64710
64891
  }
64711
- log(`[${HOOK_NAME11}] Processing start-work command`, {
64892
+ log(`[${HOOK_NAME12}] Processing start-work command`, {
64712
64893
  sessionID: input.sessionID
64713
64894
  });
64714
64895
  updateSessionAgent(input.sessionID, "architect");
@@ -64718,7 +64899,7 @@ function createStartWorkHook(ctx) {
64718
64899
  let contextInfo = "";
64719
64900
  const explicitPlanName = extractUserRequestPlanName(promptText);
64720
64901
  if (explicitPlanName) {
64721
- log(`[${HOOK_NAME11}] Explicit plan name requested: ${explicitPlanName}`, {
64902
+ log(`[${HOOK_NAME12}] Explicit plan name requested: ${explicitPlanName}`, {
64722
64903
  sessionID: input.sessionID
64723
64904
  });
64724
64905
  const allPlans = findOraclePlans(ctx.directory);
@@ -64859,7 +65040,7 @@ Ask the user which plan to work on. Present the options above and wait for their
64859
65040
  ---
64860
65041
  ${contextInfo}`;
64861
65042
  }
64862
- log(`[${HOOK_NAME11}] Context injected`, {
65043
+ log(`[${HOOK_NAME12}] Context injected`, {
64863
65044
  sessionID: input.sessionID,
64864
65045
  hasExistingState: !!existingState
64865
65046
  });
@@ -64868,16 +65049,16 @@ ${contextInfo}`;
64868
65049
  }
64869
65050
  // src/hooks/stop-continuation-guard/hook.ts
64870
65051
  init_logger();
64871
- var HOOK_NAME12 = "stop-continuation-guard";
65052
+ var HOOK_NAME13 = "stop-continuation-guard";
64872
65053
  function createStopContinuationGuardHook(_ctx, options) {
64873
65054
  const stoppedSessions = new Set;
64874
65055
  const stop = (sessionID) => {
64875
65056
  stoppedSessions.add(sessionID);
64876
- log(`[${HOOK_NAME12}] Continuation stopped for session`, { sessionID });
65057
+ log(`[${HOOK_NAME13}] Continuation stopped for session`, { sessionID });
64877
65058
  if (options?.backgroundManager) {
64878
65059
  const cancelled = options.backgroundManager.cancelAllForSession(sessionID);
64879
65060
  if (cancelled > 0) {
64880
- log(`[${HOOK_NAME12}] Cancelled ${cancelled} background task(s) for session`, { sessionID });
65061
+ log(`[${HOOK_NAME13}] Cancelled ${cancelled} background task(s) for session`, { sessionID });
64881
65062
  }
64882
65063
  }
64883
65064
  };
@@ -64886,7 +65067,7 @@ function createStopContinuationGuardHook(_ctx, options) {
64886
65067
  };
64887
65068
  const clear = (sessionID) => {
64888
65069
  stoppedSessions.delete(sessionID);
64889
- log(`[${HOOK_NAME12}] Continuation guard cleared for session`, { sessionID });
65070
+ log(`[${HOOK_NAME13}] Continuation guard cleared for session`, { sessionID });
64890
65071
  };
64891
65072
  const event = async ({
64892
65073
  event: event2
@@ -64896,7 +65077,7 @@ function createStopContinuationGuardHook(_ctx, options) {
64896
65077
  const sessionInfo = props?.info;
64897
65078
  if (sessionInfo?.id) {
64898
65079
  clear(sessionInfo.id);
64899
- log(`[${HOOK_NAME12}] Session deleted: cleaned up`, { sessionID: sessionInfo.id });
65080
+ log(`[${HOOK_NAME13}] Session deleted: cleaned up`, { sessionID: sessionInfo.id });
64900
65081
  }
64901
65082
  }
64902
65083
  };
@@ -64905,7 +65086,7 @@ function createStopContinuationGuardHook(_ctx, options) {
64905
65086
  }) => {
64906
65087
  if (sessionID && stoppedSessions.has(sessionID)) {
64907
65088
  clear(sessionID);
64908
- log(`[${HOOK_NAME12}] Cleared stop state on new user message`, { sessionID });
65089
+ log(`[${HOOK_NAME13}] Cleared stop state on new user message`, { sessionID });
64909
65090
  }
64910
65091
  };
64911
65092
  return {
@@ -64916,216 +65097,54 @@ function createStopContinuationGuardHook(_ctx, options) {
64916
65097
  clear
64917
65098
  };
64918
65099
  }
64919
- // src/hooks/task-notepad/constants.ts
64920
- var HOOK_NAME13 = "task-notepad";
64921
- var PLAN_DIR_NAME = ".matrixx";
64922
- var PLANS_SUBDIR = "plans";
64923
- var NOTEPAD_DIR_NAME = "notepads";
64924
- var TASKS_SUBDIR = "tasks";
64925
- var PLAN_EXTENSION = ".md";
64926
- var NOTEPAD_EXTENSION = ".md";
64927
- var TODO_TOOL_NAMES = new Set(["todowrite", "TodoWrite"]);
64928
- var TASK_NOTEPAD_FRAGMENT = "task-notepad";
64929
- var TODO_FETCH_ERROR_FRAGMENT = "Failed to fetch todos for task-notepad";
64930
- // src/hooks/task-notepad/hook.ts
65100
+ // src/hooks/task-continuation-enforcer/index.ts
64931
65101
  init_logger();
64932
- import { existsSync as existsSync42, mkdirSync as mkdirSync15, readdirSync as readdirSync13, readFileSync as readFileSync32, statSync as statSync8, writeFileSync as writeFileSync18 } from "fs";
64933
- import { dirname as dirname14, join as join51 } from "path";
64934
- function createTaskNotepadHook(ctx) {
64935
- return {
64936
- "tool.execute.after": async (input, output) => {
64937
- if (!output)
64938
- return;
64939
- if (!TODO_TOOL_NAMES.has(input.tool))
64940
- return;
64941
- const todos = await fetchTodos(ctx, input.sessionID);
64942
- const inProgress = todos.find((t) => t.status === "in_progress");
64943
- const completed = todos.filter((t) => t.status === "completed");
64944
- if (!inProgress && completed.length === 0)
64945
- return;
64946
- const plan = resolveActivePlan(ctx.directory);
64947
- if (inProgress) {
64948
- if (!plan) {
64949
- log(`[${HOOK_NAME13}] No active plan, skipping notepad creation`, {
64950
- sessionID: input.sessionID
64951
- });
64952
- } else {
64953
- ensureNotepad(ctx.directory, plan.name, inProgress);
64954
- }
64955
- }
64956
- if (!plan)
64957
- return;
64958
- for (const done of completed) {
64959
- appendCompletion(ctx.directory, plan.name, done);
64960
- }
64961
- }
64962
- };
64963
- }
64964
- async function fetchTodos(ctx, sessionID) {
64965
- try {
64966
- const response = await ctx.client.session.todo({ path: { id: sessionID } });
64967
- const payload = response;
64968
- if (Array.isArray(payload?.data)) {
64969
- return payload.data;
64970
- }
64971
- if (Array.isArray(response)) {
64972
- return response;
64973
- }
64974
- return [];
64975
- } catch (err) {
64976
- log(`[${HOOK_NAME13}] ${TODO_FETCH_ERROR_FRAGMENT}`, {
64977
- sessionID,
64978
- error: String(err)
64979
- });
64980
- return [];
64981
- }
64982
- }
64983
- function resolveActivePlan(directory) {
64984
- const plansDir = join51(directory, PLAN_DIR_NAME, PLANS_SUBDIR);
64985
- if (!existsSync42(plansDir))
64986
- return null;
64987
- let entries;
64988
- try {
64989
- entries = readdirSync13(plansDir);
64990
- } catch {
64991
- return null;
64992
- }
64993
- let best = null;
64994
- for (const entry of entries) {
64995
- if (!entry.endsWith(PLAN_EXTENSION))
64996
- continue;
64997
- const fullPath = join51(plansDir, entry);
64998
- try {
64999
- const stats = statSync8(fullPath);
65000
- if (!stats.isFile())
65001
- continue;
65002
- const candidate = {
65003
- name: entry.slice(0, -PLAN_EXTENSION.length),
65004
- path: fullPath,
65005
- mtimeMs: stats.mtimeMs
65006
- };
65007
- if (!best || candidate.mtimeMs > best.mtimeMs) {
65008
- best = candidate;
65009
- }
65010
- } catch {}
65011
- }
65012
- return best;
65013
- }
65014
- function ensureNotepad(directory, planName, todo) {
65015
- const notepadPath = notepadPathFor(directory, planName, todo.id);
65016
- if (existsSync42(notepadPath))
65017
- return;
65018
- mkdirSync15(dirname14(notepadPath), { recursive: true });
65019
- const content = renderNotepad(todo);
65020
- writeFileSync18(notepadPath, content, "utf-8");
65021
- log(`[${HOOK_NAME13}] Created task notepad`, { plan: planName, taskID: todo.id });
65022
- }
65023
- function appendCompletion(directory, planName, todo) {
65024
- const notepadPath = notepadPathFor(directory, planName, todo.id);
65025
- if (!existsSync42(notepadPath))
65026
- return;
65027
- const existing = readFileSync32(notepadPath, "utf-8");
65028
- if (existing.includes("**Status**: completed"))
65029
- return;
65030
- const stamp = new Date().toISOString();
65031
- const updated = `${existing}
65032
- ## Completion
65033
- - completed_at: ${stamp}
65034
- - status: completed
65035
- `;
65036
- writeFileSync18(notepadPath, updated, "utf-8");
65037
- log(`[${HOOK_NAME13}] Appended completion to notepad`, { plan: planName, taskID: todo.id });
65038
- }
65039
- function notepadPathFor(directory, planName, taskID) {
65040
- return join51(directory, PLAN_DIR_NAME, NOTEPAD_DIR_NAME, planName, TASKS_SUBDIR, `${taskID}${NOTEPAD_EXTENSION}`);
65041
- }
65042
- function renderNotepad(todo) {
65043
- const priority = todo.priority ?? "medium";
65044
- return `# Task: ${todo.content}
65045
-
65046
- **Plan**: ${TASK_NOTEPAD_FRAGMENT}
65047
- **Task ID**: ${todo.id}
65048
- **Priority**: ${priority}
65049
- **Status**: ${todo.status}
65050
- **Started**: ${new Date().toISOString()}
65051
-
65052
- ## Findings
65053
-
65054
- (Record what you learn about this code as you work \u2014 patterns, conventions, gotchas, useful references.)
65055
-
65056
- ## Blockers
65057
-
65058
- (Anything blocking progress. Be specific: file path, line, error, workaround if any.)
65059
65102
 
65060
- ## Questions
65103
+ // src/hooks/task-continuation-enforcer/constants.ts
65104
+ var HOOK_NAME14 = "task-continuation-enforcer";
65105
+ var DEFAULT_SKIP_AGENTS = ["oracle", "compaction"];
65106
+ var CONTINUATION_PROMPT2 = `${createSystemDirective(SystemDirectiveTypes.TASK_CONTINUATION)}
65061
65107
 
65062
- (Open questions to revisit later. Don't lose them when context compacts.)
65108
+ Incomplete Matrixx tasks remain. Continue working on the next pending task.
65063
65109
 
65064
- ## Results
65110
+ - Proceed without asking for permission
65111
+ - Mark each task in_progress before starting, completed immediately after
65112
+ - Respect blockedBy dependencies (skip blocked tasks)
65113
+ - Do not stop until all tasks are done`;
65114
+ var COUNTDOWN_SECONDS = 2;
65115
+ var TOAST_DURATION_MS = 900;
65116
+ var COUNTDOWN_GRACE_PERIOD_MS = 500;
65117
+ var ABORT_WINDOW_MS = 3000;
65118
+ var CONTINUATION_COOLDOWN_MS2 = 30000;
65119
+ var MAX_CONSECUTIVE_FAILURES = 5;
65120
+ var FAILURE_RESET_WINDOW_MS = 5 * 60 * 1000;
65065
65121
 
65066
- (Summary on completion. What changed, what was verified, what remains.)
65067
- `;
65068
- }
65069
- // src/hooks/task-resume-info/hook.ts
65070
- var TARGET_TOOLS2 = ["task", "Task", "task_tool", "delegate_agent"];
65071
- var SESSION_ID_PATTERNS = [
65072
- /Session ID: (ses_[a-zA-Z0-9_-]+)/,
65073
- /session_id: (ses_[a-zA-Z0-9_-]+)/,
65074
- /<task_metadata>\s*session_id: (ses_[a-zA-Z0-9_-]+)/,
65075
- /sessionId: (ses_[a-zA-Z0-9_-]+)/
65076
- ];
65077
- function extractSessionId(output) {
65078
- for (const pattern of SESSION_ID_PATTERNS) {
65079
- const match = output.match(pattern);
65080
- if (match)
65081
- return match[1] ?? null;
65082
- }
65083
- return null;
65084
- }
65085
- function createTaskResumeInfoHook() {
65086
- const toolExecuteAfter = async (input, output) => {
65087
- if (!TARGET_TOOLS2.includes(input.tool))
65088
- return;
65089
- const outputText = output.output ?? "";
65090
- if (outputText.startsWith("Error:") || outputText.startsWith("Failed"))
65091
- return;
65092
- if (outputText.includes(`
65093
- to continue:`))
65094
- return;
65095
- const sessionId = extractSessionId(outputText);
65096
- if (!sessionId)
65097
- return;
65098
- output.output = outputText.trimEnd() + `
65122
+ // src/hooks/task-continuation-enforcer/handler.ts
65123
+ init_logger();
65099
65124
 
65100
- to continue: task(session_id="${sessionId}", prompt="...")`;
65101
- };
65102
- return {
65103
- "tool.execute.after": toolExecuteAfter
65104
- };
65105
- }
65106
- // src/hooks/task-todo-mirror/constants.ts
65107
- var HOOK_NAME14 = "task-todo-mirror";
65108
- var TASK_TOOL_NAMES = new Set([
65109
- "task_create",
65110
- "task_update"
65111
- ]);
65112
- var DEBOUNCE_MS = 300;
65113
- // src/hooks/task-todo-mirror/hook.ts
65114
- import { existsSync as existsSync45, readdirSync as readdirSync15 } from "fs";
65125
+ // src/hooks/task-continuation-enforcer/idle-event.ts
65126
+ import { existsSync as existsSync44, readdirSync as readdirSync15 } from "fs";
65115
65127
 
65116
65128
  // src/features/task-storage/storage.ts
65129
+ init_logger();
65117
65130
  import { randomUUID } from "crypto";
65118
- import { existsSync as existsSync43, mkdirSync as mkdirSync16, readdirSync as readdirSync14, readFileSync as readFileSync33, renameSync as renameSync3, unlinkSync as unlinkSync9, writeFileSync as writeFileSync19 } from "fs";
65119
- import { basename as basename5, dirname as dirname15, isAbsolute as isAbsolute3, join as join52 } from "path";
65120
- function getTaskDir(config2 = {}) {
65131
+ import { existsSync as existsSync42, mkdirSync as mkdirSync15, readdirSync as readdirSync13, readFileSync as readFileSync32, renameSync as renameSync3, unlinkSync as unlinkSync9, writeFileSync as writeFileSync18 } from "fs";
65132
+ import { basename as basename5, dirname as dirname14, isAbsolute as isAbsolute3, join as join51 } from "path";
65133
+ function getProjectTaskDir(directory) {
65134
+ return join51(directory, ".matrixx", "tasks");
65135
+ }
65136
+ function getTaskDir(config2 = {}, directory) {
65121
65137
  const tasksConfig = config2.morpheus?.tasks;
65122
65138
  const storagePath = tasksConfig?.storage_path;
65123
65139
  if (storagePath) {
65124
- return isAbsolute3(storagePath) ? storagePath : join52(process.cwd(), storagePath);
65140
+ return isAbsolute3(storagePath) ? storagePath : join51(directory ?? process.cwd(), storagePath);
65125
65141
  }
65126
- const configDir = getOpenCodeConfigDir({ binary: "opencode" });
65127
- const listId = resolveTaskListId(config2);
65128
- return join52(configDir, "tasks", listId);
65142
+ if (tasksConfig?.scope === "global" || !directory) {
65143
+ const configDir = getOpenCodeConfigDir({ binary: "opencode" });
65144
+ const listId = resolveTaskListId(config2);
65145
+ return join51(configDir, "tasks", listId);
65146
+ }
65147
+ return getProjectTaskDir(directory);
65129
65148
  }
65130
65149
  function sanitizePathSegment(value) {
65131
65150
  return value.replace(/[^a-zA-Z0-9_-]/g, "-") || "default";
@@ -65143,16 +65162,16 @@ function resolveTaskListId(config2 = {}) {
65143
65162
  return sanitizePathSegment(basename5(process.cwd()));
65144
65163
  }
65145
65164
  function ensureDir2(dirPath) {
65146
- if (!existsSync43(dirPath)) {
65147
- mkdirSync16(dirPath, { recursive: true });
65165
+ if (!existsSync42(dirPath)) {
65166
+ mkdirSync15(dirPath, { recursive: true });
65148
65167
  }
65149
65168
  }
65150
65169
  function readJsonSafe(filePath, schema) {
65151
65170
  try {
65152
- if (!existsSync43(filePath)) {
65171
+ if (!existsSync42(filePath)) {
65153
65172
  return null;
65154
65173
  }
65155
- const content = readFileSync33(filePath, "utf-8");
65174
+ const content = readFileSync32(filePath, "utf-8");
65156
65175
  const parsed = JSON.parse(content);
65157
65176
  const result = schema.safeParse(parsed);
65158
65177
  if (!result.success) {
@@ -65164,15 +65183,15 @@ function readJsonSafe(filePath, schema) {
65164
65183
  }
65165
65184
  }
65166
65185
  function writeJsonAtomic(filePath, data) {
65167
- const dir = dirname15(filePath);
65186
+ const dir = dirname14(filePath);
65168
65187
  ensureDir2(dir);
65169
65188
  const tempPath = `${filePath}.tmp.${Date.now()}`;
65170
65189
  try {
65171
- writeFileSync19(tempPath, JSON.stringify(data, null, 2), "utf-8");
65190
+ writeFileSync18(tempPath, JSON.stringify(data, null, 2), "utf-8");
65172
65191
  renameSync3(tempPath, filePath);
65173
65192
  } catch (error51) {
65174
65193
  try {
65175
- if (existsSync43(tempPath)) {
65194
+ if (existsSync42(tempPath)) {
65176
65195
  unlinkSync9(tempPath);
65177
65196
  }
65178
65197
  } catch {}
@@ -65180,21 +65199,73 @@ function writeJsonAtomic(filePath, data) {
65180
65199
  }
65181
65200
  }
65182
65201
  var STALE_LOCK_THRESHOLD_MS = 30000;
65202
+ async function acquireLockWithRetry(dirPath, retries = 4) {
65203
+ for (let attempt = 0;attempt < retries; attempt++) {
65204
+ const lock = acquireLock(dirPath);
65205
+ if (lock.acquired)
65206
+ return lock;
65207
+ if (attempt < retries - 1) {
65208
+ await new Promise((resolve7) => setTimeout(resolve7, 15 * 2 ** attempt));
65209
+ }
65210
+ }
65211
+ return { acquired: false, release: () => {} };
65212
+ }
65183
65213
  function generateTaskId() {
65184
65214
  return `T-${randomUUID()}`;
65185
65215
  }
65216
+ function migrateLegacyTasksIfNeeded(config2, directory) {
65217
+ const projectDir = getProjectTaskDir(directory);
65218
+ const legacyDir = join51(getOpenCodeConfigDir({ binary: "opencode" }), "tasks", resolveTaskListId(config2));
65219
+ if (existsSync42(projectDir)) {
65220
+ try {
65221
+ const existing = readdirSync13(projectDir).filter((f) => f.startsWith("T-") && f.endsWith(".json"));
65222
+ if (existing.length > 0) {
65223
+ return { migrated: 0, legacyDir, projectDir };
65224
+ }
65225
+ } catch {}
65226
+ }
65227
+ if (!existsSync42(legacyDir)) {
65228
+ return { migrated: 0, legacyDir, projectDir };
65229
+ }
65230
+ let files = [];
65231
+ try {
65232
+ files = readdirSync13(legacyDir).filter((f) => f.startsWith("T-") && f.endsWith(".json"));
65233
+ } catch {
65234
+ return { migrated: 0, legacyDir, projectDir };
65235
+ }
65236
+ if (files.length === 0) {
65237
+ return { migrated: 0, legacyDir, projectDir };
65238
+ }
65239
+ ensureDir2(projectDir);
65240
+ let migrated = 0;
65241
+ for (const file2 of files) {
65242
+ const src = join51(legacyDir, file2);
65243
+ const dest = join51(projectDir, file2);
65244
+ if (existsSync42(dest))
65245
+ continue;
65246
+ try {
65247
+ const content = readFileSync32(src, "utf-8");
65248
+ writeFileSync18(dest, content, "utf-8");
65249
+ migrated++;
65250
+ } catch {}
65251
+ }
65252
+ if (migrated > 0) {
65253
+ log(`[task-storage] Migrated ${migrated} legacy tasks from ${legacyDir} to ${projectDir}`);
65254
+ }
65255
+ return { migrated, legacyDir, projectDir };
65256
+ }
65186
65257
  function acquireLock(dirPath) {
65187
- const lockPath = join52(dirPath, ".lock");
65258
+ const lockPath = join51(dirPath, ".lock");
65188
65259
  const lockId = randomUUID();
65189
65260
  const createLock = (timestamp) => {
65190
- writeFileSync19(lockPath, JSON.stringify({ id: lockId, timestamp }), {
65261
+ writeFileSync18(lockPath, JSON.stringify({ id: lockId, timestamp }), {
65191
65262
  encoding: "utf-8",
65192
65263
  flag: "wx"
65193
65264
  });
65194
65265
  };
65195
65266
  const isStale = () => {
65196
65267
  try {
65197
- const lockContent = readFileSync33(lockPath, "utf-8");
65268
+ const lockContent = readFileSync32(lockPath, "utf-8");
65198
65269
  const lockData = JSON.parse(lockContent);
65199
65270
  const lockAge = Date.now() - lockData.timestamp;
65200
65271
  return lockAge > STALE_LOCK_THRESHOLD_MS;
@@ -65232,9 +65303,9 @@ function acquireLock(dirPath) {
65232
65303
  acquired: true,
65233
65304
  release: () => {
65234
65305
  try {
65235
- if (!existsSync43(lockPath))
65306
+ if (!existsSync42(lockPath))
65236
65307
  return;
65237
- const lockContent = readFileSync33(lockPath, "utf-8");
65308
+ const lockContent = readFileSync32(lockPath, "utf-8");
65238
65309
  const lockData = JSON.parse(lockContent);
65239
65310
  if (lockData.id !== lockId)
65240
65311
  return;
@@ -65244,239 +65315,9 @@ function acquireLock(dirPath) {
65244
65315
  };
65245
65316
  }
65246
65317
 
65247
- // src/hooks/task-todo-mirror/hook.ts
65318
+ // src/hooks/task-continuation-enforcer/idle-event.ts
65248
65319
  init_logger();
65249
65320
 
65250
- // src/tools/task/todo-sync.ts
65251
- import { existsSync as existsSync44 } from "fs";
65252
- import { homedir as homedir9 } from "os";
65253
- import { join as join53 } from "path";
65254
- init_logger();
65255
- function mapTaskStatusToTodoStatus(taskStatus) {
65256
- switch (taskStatus) {
65257
- case "pending":
65258
- return "pending";
65259
- case "in_progress":
65260
- return "in_progress";
65261
- case "completed":
65262
- return "completed";
65263
- case "deleted":
65264
- return null;
65265
- default:
65266
- return "pending";
65267
- }
65268
- }
65269
- function extractPriority(metadata) {
65270
- if (!metadata)
65271
- return;
65272
- const priority = metadata.priority;
65273
- if (typeof priority === "string" && ["low", "medium", "high"].includes(priority)) {
65274
- return priority;
65275
- }
65276
- return;
65277
- }
65278
- function todosMatch(todo1, todo2) {
65279
- if (todo1.id && todo2.id) {
65280
- return todo1.id === todo2.id;
65281
- }
65282
- return todo1.content === todo2.content;
65283
- }
65284
- function syncTaskToTodo(task) {
65285
- const todoStatus = mapTaskStatusToTodoStatus(task.status);
65286
- if (todoStatus === null) {
65287
- return null;
65288
- }
65289
- return {
65290
- id: task.id,
65291
- content: task.subject,
65292
- status: todoStatus,
65293
- priority: extractPriority(task.metadata)
65294
- };
65295
- }
65296
- async function resolveTodoWriter2() {
65297
- const loaders = ["opencode/session/todo", "@opencode-ai/core/session/todo"];
65298
- for (const loader of loaders) {
65299
- try {
65300
- const mod = await import(loader);
65301
- const update = mod.Todo?.update;
65302
- if (typeof update === "function") {
65303
- return update;
65304
- }
65305
- } catch (err) {
65306
- log("[todo-sync] Failed to resolve Todo.update", { loader, error: String(err) });
65307
- }
65308
- }
65309
- return null;
65310
- }
65311
- function getOpencodeDbPath() {
65312
- const candidates = [];
65313
- if (process.env.XDG_DATA_HOME) {
65314
- candidates.push(join53(process.env.XDG_DATA_HOME, "opencode", "opencode.db"));
65315
- }
65316
- candidates.push(join53(homedir9(), ".local", "share", "opencode", "opencode.db"));
65317
- candidates.push(join53(homedir9(), ".config", "opencode", "opencode.db"));
65318
- for (const p of candidates) {
65319
- if (existsSync44(p))
65320
- return p;
65321
- }
65322
- return candidates[1] ?? null;
65323
- }
65324
- async function directDbWrite(sessionID, todos) {
65325
- const dbPath = getOpencodeDbPath();
65326
- if (!dbPath || !existsSync44(dbPath)) {
65327
- log("[todo-sync] directDbWrite no db", { dbPath });
65328
- return false;
65329
- }
65330
- try {
65331
- const mod = await import("bun:sqlite");
65332
- const Database = mod.Database;
65333
- const db = new Database(dbPath);
65334
- const now = Date.now();
65335
- try {
65336
- db.exec("BEGIN IMMEDIATE");
65337
- db.prepare("DELETE FROM todo WHERE session_id = ?").run(sessionID);
65338
- const stmt = db.prepare("INSERT INTO todo (session_id, content, status, priority, position, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?, ?)");
65339
- todos.forEach((t, idx) => {
65340
- stmt.run(sessionID, t.content, t.status, t.priority ?? "medium", idx, now, now);
65341
- });
65342
- db.exec("COMMIT");
65343
- } catch (inner) {
65344
- try {
65345
- db.exec("ROLLBACK");
65346
- } catch {}
65347
- throw inner;
65348
- } finally {
65349
- db.close();
65350
- }
65351
- log("[todo-sync] directDbWrite ok", { sessionID, count: todos.length });
65352
- return true;
65353
- } catch (err) {
65354
- log("[todo-sync] directDbWrite failed", { error: String(err) });
65355
- return false;
65356
- }
65357
- }
65358
- async function writeTodosWithFallback(sessionID, todos, writer) {
65359
- if (writer) {
65360
- try {
65361
- await writer({ sessionID, todos });
65362
- return;
65363
- } catch (err) {
65364
- log("[todo-sync] writer failed, fallback to direct DB", { error: String(err) });
65365
- }
65366
- }
65367
- const ok = await directDbWrite(sessionID, todos);
65368
- if (!ok) {
65369
- log("[todo-sync] fallback also failed", { sessionID });
65370
- }
65371
- }
65372
- function extractTodos2(response) {
65373
- const payload = response;
65374
- if (Array.isArray(payload?.data)) {
65375
- return payload.data;
65376
- }
65377
- if (Array.isArray(response)) {
65378
- return response;
65379
- }
65380
- return [];
65381
- }
65382
- async function syncTaskTodoUpdate(ctx, task, sessionID, writer) {
65383
- if (!ctx)
65384
- return;
65385
- const resolvedWriter = writer ?? await resolveTodoWriter2();
65386
- await syncSingleSession(ctx, task, sessionID, resolvedWriter);
65387
- const mainSessionID = safeGetMainSessionID();
65388
- if (mainSessionID && mainSessionID !== sessionID) {
65389
- await syncSingleSession(ctx, task, mainSessionID, resolvedWriter);
65390
- }
65391
- }
65392
- async function syncSingleSession(ctx, task, sessionID, writer) {
65393
- try {
65394
- const response = await ctx.client.session.todo({
65395
- path: { id: sessionID }
65396
- });
65397
- const currentTodos = extractTodos2(response);
65398
- const taskTodo = syncTaskToTodo(task);
65399
- const nextTodos = currentTodos.filter((todo) => {
65400
- if (taskTodo) {
65401
- return !todosMatch(todo, taskTodo);
65402
- }
65403
- if (todo.id) {
65404
- return todo.id !== task.id;
65405
- }
65406
- return todo.content !== task.subject;
65407
- });
65408
- if (taskTodo) {
65409
- nextTodos.push(taskTodo);
65410
- }
65411
- await writeTodosWithFallback(sessionID, nextTodos, writer);
65412
- } catch (err) {
65413
- log("[todo-sync] Failed to sync task todo", {
65414
- error: String(err),
65415
- sessionID
65416
- });
65417
- }
65418
- }
65419
- function safeGetMainSessionID() {
65420
- try {
65421
- return getMainSessionID();
65422
- } catch {
65423
- return;
65424
- }
65425
- }
65426
- async function syncAllTasksToTodos(ctx, tasks, sessionID, writer) {
65427
- try {
65428
- let currentTodos = [];
65429
- try {
65430
- const response = await ctx.client.session.todo({
65431
- path: { id: sessionID || "" }
65432
- });
65433
- currentTodos = extractTodos2(response);
65434
- } catch (err) {
65435
- log("[todo-sync] Failed to fetch current todos", {
65436
- error: String(err),
65437
- sessionID
65438
- });
65439
- }
65440
- const newTodos = [];
65441
- const tasksToRemove = new Set;
65442
- const allTaskSubjects = new Set;
65443
- for (const task of tasks) {
65444
- allTaskSubjects.add(task.subject);
65445
- const todo = syncTaskToTodo(task);
65446
- if (todo === null) {
65447
- tasksToRemove.add(task.id);
65448
- } else {
65449
- newTodos.push(todo);
65450
- }
65451
- }
65452
- const finalTodos = [];
65453
- const removedTaskSubjects = new Set(tasks.filter((t) => t.status === "deleted").map((t) => t.subject));
65454
- for (const existing of currentTodos) {
65455
- const isInNewTodos = newTodos.some((newTodo) => todosMatch(existing, newTodo));
65456
- const isRemovedById = existing.id ? tasksToRemove.has(existing.id) : false;
65457
- const isRemovedByContent = !existing.id && removedTaskSubjects.has(existing.content);
65458
- const isReplacedByTask = !existing.id && allTaskSubjects.has(existing.content);
65459
- if (!isInNewTodos && !isRemovedById && !isRemovedByContent && !isReplacedByTask) {
65460
- finalTodos.push(existing);
65461
- }
65462
- }
65463
- finalTodos.push(...newTodos);
65464
- const resolvedWriter = writer ?? await resolveTodoWriter2();
65465
- if (sessionID) {
65466
- await writeTodosWithFallback(sessionID, finalTodos, resolvedWriter);
65467
- }
65468
- log("[todo-sync] Synced todos", {
65469
- count: finalTodos.length,
65470
- sessionID
65471
- });
65472
- } catch (err) {
65473
- log("[todo-sync] Error in syncAllTasksToTodos", {
65474
- error: String(err),
65475
- sessionID
65476
- });
65477
- }
65478
- }
65479
-
65480
65321
  // src/tools/task/types.ts
65481
65322
  var TaskStatusSchema = exports_external.enum(["pending", "in_progress", "completed", "deleted"]);
65482
65323
  var TaskObjectSchema = exports_external.object({
@@ -65491,7 +65332,8 @@ var TaskObjectSchema = exports_external.object({
65491
65332
  metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
65492
65333
  repoURL: exports_external.string().optional(),
65493
65334
  parentID: exports_external.string().optional(),
65494
- threadID: exports_external.string()
65335
+ threadID: exports_external.string(),
65336
+ projectRoot: exports_external.string().optional()
65495
65337
  }).strict();
65496
65338
  var TaskCreateInputSchema = exports_external.object({
65497
65339
  subject: exports_external.string(),
@@ -65528,82 +65370,849 @@ var TaskDeleteInputSchema = exports_external.object({
65528
65370
  id: exports_external.string()
65529
65371
  });
65530
65372
 
65531
- // src/hooks/task-todo-mirror/hook.ts
65532
- function loadAllTasks(pluginConfig) {
65373
+ // src/hooks/task-continuation-enforcer/abort-detection.ts
65374
+ function isLastAssistantMessageAborted(messages) {
65375
+ if (!messages || messages.length === 0)
65376
+ return false;
65377
+ for (let i = messages.length - 1;i >= 0; i--) {
65378
+ const info = messages[i].info;
65379
+ if (info?.role !== "assistant")
65380
+ continue;
65381
+ const errorName = info.error?.name;
65382
+ if (!errorName)
65383
+ return false;
65384
+ return errorName === "MessageAbortedError" || errorName === "AbortError";
65385
+ }
65386
+ return false;
65387
+ }
65388
+
65389
+ // src/hooks/task-continuation-enforcer/countdown.ts
65390
+ init_logger();
65391
+
65392
+ // src/hooks/task-continuation-enforcer/continuation-injection.ts
65393
+ import { existsSync as existsSync43, readdirSync as readdirSync14 } from "fs";
65394
+ init_logger();
65395
+ // src/hooks/task-continuation-enforcer/todo.ts
65396
+ function getIncompleteTaskCount(tasks) {
65397
+ const byId = new Map(tasks.map((t) => [t.id, t]));
65398
+ return tasks.filter((task) => {
65399
+ if (task.status !== "pending" && task.status !== "in_progress")
65400
+ return false;
65401
+ if (task.blockedBy.length === 0)
65402
+ return true;
65403
+ return task.blockedBy.every((bid) => byId.get(bid)?.status === "completed");
65404
+ }).length;
65405
+ }
65406
+
65407
+ // src/hooks/task-continuation-enforcer/continuation-injection.ts
65408
+ function hasWritePermission(tools) {
65409
+ const editPermission = tools?.edit;
65410
+ const writePermission = tools?.write;
65411
+ return !tools || editPermission !== false && editPermission !== "deny" && writePermission !== false && writePermission !== "deny";
65412
+ }
65413
+ async function injectContinuation(args) {
65414
+ const {
65415
+ ctx,
65416
+ sessionID,
65417
+ backgroundManager,
65418
+ skipAgents = DEFAULT_SKIP_AGENTS,
65419
+ resolvedInfo,
65420
+ sessionStateStore
65421
+ } = args;
65422
+ const state3 = sessionStateStore.getExistingState(sessionID);
65423
+ if (state3?.isRecovering) {
65424
+ log(`[${HOOK_NAME14}] Skipped injection: in recovery`, { sessionID });
65425
+ return;
65426
+ }
65427
+ const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((task) => task.status === "running") : false;
65428
+ if (hasRunningBgTasks) {
65429
+ log(`[${HOOK_NAME14}] Skipped injection: background tasks running`, { sessionID });
65430
+ return;
65431
+ }
65432
+ const tasks = [];
65433
+ let total = 0;
65533
65434
  try {
65534
- const dir = getTaskDir(pluginConfig);
65535
- if (!existsSync45(dir))
65536
- return [];
65537
- const files = readdirSync15(dir).filter((f) => f.endsWith(".json") && f.startsWith("T-"));
65538
- const tasks = [];
65539
- for (const file2 of files) {
65540
- const task = readJsonSafe(`${dir}/${file2}`, TaskObjectSchema);
65541
- if (task)
65542
- tasks.push(task);
65435
+ const taskDir = getTaskDir({}, ctx.directory);
65436
+ if (!existsSync43(taskDir)) {
65437
+ log(`[${HOOK_NAME14}] Skipped injection: no task dir`, { sessionID, taskDir });
65438
+ return;
65439
+ }
65440
+ const files = readdirSync14(taskDir).filter((f) => f.startsWith("T-") && f.endsWith(".json"));
65441
+ for (const f of files) {
65442
+ const parsed = readJsonSafe(`${taskDir}/${f}`, TaskObjectSchema);
65443
+ if (parsed)
65444
+ tasks.push(parsed);
65445
+ }
65446
+ total = tasks.length;
65447
+ if (total === 0) {
65448
+ log(`[${HOOK_NAME14}] Skipped injection: no tasks`, { sessionID });
65449
+ return;
65450
+ }
65451
+ } catch (error51) {
65452
+ log(`[${HOOK_NAME14}] Failed to fetch tasks`, { sessionID, error: String(error51) });
65453
+ return;
65454
+ }
65455
+ const freshIncompleteCount = getIncompleteTaskCount(tasks);
65456
+ if (freshIncompleteCount === 0) {
65457
+ log(`[${HOOK_NAME14}] Skipped injection: no incomplete tasks`, { sessionID, total });
65458
+ return;
65459
+ }
65460
+ let agentName = resolvedInfo?.agent;
65461
+ let model = resolvedInfo?.model;
65462
+ let tools = resolvedInfo?.tools;
65463
+ if (!agentName || !model) {
65464
+ let previousMessage = null;
65465
+ if (isSqliteBackend()) {
65466
+ previousMessage = await findNearestMessageWithFieldsFromSDK(ctx.client, sessionID);
65467
+ } else {
65468
+ const messageDir = getMessageDir(sessionID);
65469
+ previousMessage = messageDir ? findNearestMessageWithFields(messageDir) : null;
65470
+ }
65471
+ agentName = agentName ?? previousMessage?.agent;
65472
+ model = model ?? (previousMessage?.model?.providerID && previousMessage?.model?.modelID ? {
65473
+ providerID: previousMessage.model.providerID,
65474
+ modelID: previousMessage.model.modelID,
65475
+ ...previousMessage.model.variant ? { variant: previousMessage.model.variant } : {}
65476
+ } : undefined);
65477
+ tools = tools ?? previousMessage?.tools;
65478
+ }
65479
+ if (agentName && skipAgents.some((s) => getAgentConfigKey(s) === getAgentConfigKey(agentName))) {
65480
+ log(`[${HOOK_NAME14}] Skipped: agent in skipAgents list`, { sessionID, agent: agentName });
65481
+ return;
65482
+ }
65483
+ if (!hasWritePermission(tools)) {
65484
+ log(`[${HOOK_NAME14}] Skipped: agent lacks write permission`, { sessionID, agent: agentName });
65485
+ return;
65486
+ }
65487
+ const byId = new Map(tasks.map((t) => [t.id, t]));
65488
+ const incompleteTasks = tasks.filter((task) => {
65489
+ if (task.status !== "pending" && task.status !== "in_progress")
65490
+ return false;
65491
+ if (task.blockedBy.length === 0)
65492
+ return true;
65493
+ return task.blockedBy.every((bid) => byId.get(bid)?.status === "completed");
65494
+ });
65495
+ const taskList = incompleteTasks.map((task) => `- [${task.status}] ${task.subject} (${task.id})`).join(`
65496
+ `);
65497
+ const prompt = `${CONTINUATION_PROMPT2}
65498
+
65499
+ [Status: ${total - freshIncompleteCount}/${total} completed, ${freshIncompleteCount} remaining]
65500
+
65501
+ Remaining Matrixx tasks:
65502
+ ${taskList}`;
65503
+ const injectionState = sessionStateStore.getExistingState(sessionID);
65504
+ if (injectionState) {
65505
+ injectionState.inFlight = true;
65506
+ }
65507
+ try {
65508
+ log(`[${HOOK_NAME14}] Injecting continuation`, {
65509
+ sessionID,
65510
+ agent: agentName,
65511
+ model,
65512
+ incompleteCount: freshIncompleteCount
65513
+ });
65514
+ await ctx.client.session.promptAsync({
65515
+ path: { id: sessionID },
65516
+ body: {
65517
+ agent: agentName,
65518
+ ...model !== undefined ? { model } : {},
65519
+ parts: [{ type: "text", text: prompt }]
65520
+ },
65521
+ query: { directory: ctx.directory }
65522
+ });
65523
+ log(`[${HOOK_NAME14}] Injection successful`, { sessionID });
65524
+ if (injectionState) {
65525
+ injectionState.inFlight = false;
65526
+ injectionState.lastInjectedAt = Date.now();
65527
+ injectionState.consecutiveFailures = 0;
65528
+ }
65529
+ } catch (error51) {
65530
+ log(`[${HOOK_NAME14}] Injection failed`, { sessionID, error: String(error51) });
65531
+ if (injectionState) {
65532
+ injectionState.inFlight = false;
65533
+ injectionState.lastInjectedAt = Date.now();
65534
+ injectionState.consecutiveFailures = (injectionState.consecutiveFailures ?? 0) + 1;
65543
65535
  }
65544
- return tasks;
65545
- } catch (err) {
65546
- log(`[${HOOK_NAME14}] Failed to load tasks`, { error: String(err) });
65547
- return [];
65548
65536
  }
65549
65537
  }
65550
- function createTaskTodoMirrorHook(ctx, pluginConfig) {
65551
- const enabled = pluginConfig.experimental?.task_system ?? false;
65552
- const lastSync = new Map;
65553
- const syncForSession = async (sessionID) => {
65554
- if (!enabled)
65538
+
65539
+ // src/hooks/task-continuation-enforcer/countdown.ts
65540
+ async function showCountdownToast(ctx, seconds, incompleteCount) {
65541
+ await ctx.client.tui.showToast({
65542
+ body: {
65543
+ title: "Task Continuation",
65544
+ message: `Resuming in ${seconds}s... (${incompleteCount} tasks remaining)`,
65545
+ variant: "warning",
65546
+ duration: TOAST_DURATION_MS
65547
+ }
65548
+ }).catch((err) => {
65549
+ log("[task-continuation] Countdown toast failed:", err);
65550
+ });
65551
+ }
65552
+ function startCountdown(args) {
65553
+ const {
65554
+ ctx,
65555
+ sessionID,
65556
+ incompleteCount,
65557
+ resolvedInfo,
65558
+ backgroundManager,
65559
+ skipAgents,
65560
+ sessionStateStore
65561
+ } = args;
65562
+ const state3 = sessionStateStore.getState(sessionID);
65563
+ if (state3.countdownTimer || state3.countdownInterval) {
65564
+ log(`[${HOOK_NAME14}] Skipped: countdown already active`, { sessionID });
65565
+ return;
65566
+ }
65567
+ let secondsRemaining = COUNTDOWN_SECONDS;
65568
+ showCountdownToast(ctx, secondsRemaining, incompleteCount);
65569
+ state3.countdownStartedAt = Date.now();
65570
+ state3.countdownInterval = setInterval(() => {
65571
+ secondsRemaining--;
65572
+ if (secondsRemaining > 0) {
65573
+ showCountdownToast(ctx, secondsRemaining, incompleteCount);
65574
+ }
65575
+ }, 1000);
65576
+ state3.countdownTimer = setTimeout(() => {
65577
+ sessionStateStore.cancelCountdown(sessionID);
65578
+ injectContinuation({
65579
+ ctx,
65580
+ sessionID,
65581
+ backgroundManager,
65582
+ skipAgents,
65583
+ resolvedInfo,
65584
+ sessionStateStore
65585
+ });
65586
+ }, COUNTDOWN_SECONDS * 1000);
65587
+ log(`[${HOOK_NAME14}] Countdown started`, {
65588
+ sessionID,
65589
+ seconds: COUNTDOWN_SECONDS,
65590
+ incompleteCount
65591
+ });
65592
+ }
65593
+
65594
+ // src/hooks/task-continuation-enforcer/idle-event.ts
65595
+ async function handleSessionIdle(args) {
65596
+ const {
65597
+ ctx,
65598
+ sessionID,
65599
+ sessionStateStore,
65600
+ backgroundManager,
65601
+ skipAgents = DEFAULT_SKIP_AGENTS,
65602
+ isContinuationStopped
65603
+ } = args;
65604
+ log(`[${HOOK_NAME14}] session.idle`, { sessionID });
65605
+ const state3 = sessionStateStore.getState(sessionID);
65606
+ if (state3.countdownTimer || state3.countdownInterval) {
65607
+ log(`[${HOOK_NAME14}] Skipped: countdown already active`, { sessionID });
65608
+ return;
65609
+ }
65610
+ if (state3.isRecovering) {
65611
+ log(`[${HOOK_NAME14}] Skipped: in recovery`, { sessionID });
65612
+ return;
65613
+ }
65614
+ if (state3.abortDetectedAt) {
65615
+ const timeSinceAbort = Date.now() - state3.abortDetectedAt;
65616
+ if (timeSinceAbort < ABORT_WINDOW_MS) {
65617
+ log(`[${HOOK_NAME14}] Skipped: abort detected via event ${timeSinceAbort}ms ago`, { sessionID });
65618
+ state3.abortDetectedAt = undefined;
65619
+ return;
65620
+ }
65621
+ state3.abortDetectedAt = undefined;
65622
+ }
65623
+ const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((task) => task.status === "running") : false;
65624
+ if (hasRunningBgTasks) {
65625
+ log(`[${HOOK_NAME14}] Skipped: background tasks running`, { sessionID });
65626
+ return;
65627
+ }
65628
+ try {
65629
+ const messagesResp = await ctx.client.session.messages({
65630
+ path: { id: sessionID },
65631
+ query: { directory: ctx.directory }
65632
+ });
65633
+ const messages = normalizeSDKResponse(messagesResp, []);
65634
+ if (isLastAssistantMessageAborted(messages)) {
65635
+ log(`[${HOOK_NAME14}] Skipped: last assistant message was aborted (API fallback)`, { sessionID });
65555
65636
  return;
65637
+ }
65638
+ } catch (error51) {
65639
+ log(`[${HOOK_NAME14}] Messages fetch failed, continuing`, { sessionID, error: String(error51) });
65640
+ }
65641
+ let incompleteCount = 0;
65642
+ let total = 0;
65643
+ try {
65644
+ const taskDir = getTaskDir({}, ctx.directory);
65645
+ if (!existsSync44(taskDir)) {
65646
+ log(`[${HOOK_NAME14}] No task dir`, { sessionID, taskDir });
65647
+ return;
65648
+ }
65649
+ const files = readdirSync15(taskDir).filter((f) => f.startsWith("T-") && f.endsWith(".json"));
65650
+ const tasks = [];
65651
+ for (const f of files) {
65652
+ const parsed = readJsonSafe(`${taskDir}/${f}`, TaskObjectSchema);
65653
+ if (parsed)
65654
+ tasks.push(parsed);
65655
+ }
65656
+ total = tasks.length;
65657
+ if (total === 0) {
65658
+ log(`[${HOOK_NAME14}] No tasks`, { sessionID });
65659
+ return;
65660
+ }
65661
+ incompleteCount = getIncompleteTaskCount(tasks);
65662
+ } catch (error51) {
65663
+ log(`[${HOOK_NAME14}] Task fetch failed`, { sessionID, error: String(error51) });
65664
+ return;
65665
+ }
65666
+ if (incompleteCount === 0) {
65667
+ log(`[${HOOK_NAME14}] All tasks complete`, { sessionID, total });
65668
+ return;
65669
+ }
65670
+ if (state3.inFlight) {
65671
+ log(`[${HOOK_NAME14}] Skipped: injection in flight`, { sessionID });
65672
+ return;
65673
+ }
65674
+ if ((state3.consecutiveFailures ?? 0) >= MAX_CONSECUTIVE_FAILURES && state3.lastInjectedAt && Date.now() - state3.lastInjectedAt >= FAILURE_RESET_WINDOW_MS) {
65675
+ state3.consecutiveFailures = 0;
65676
+ log(`[${HOOK_NAME14}] Reset consecutive failures after recovery window`, {
65677
+ sessionID,
65678
+ failureResetWindowMs: FAILURE_RESET_WINDOW_MS
65679
+ });
65680
+ }
65681
+ if ((state3.consecutiveFailures ?? 0) >= MAX_CONSECUTIVE_FAILURES) {
65682
+ log(`[${HOOK_NAME14}] Skipped: max consecutive failures reached`, {
65683
+ sessionID,
65684
+ consecutiveFailures: state3.consecutiveFailures ?? 0,
65685
+ maxConsecutiveFailures: MAX_CONSECUTIVE_FAILURES
65686
+ });
65687
+ return;
65688
+ }
65689
+ const effectiveCooldown = CONTINUATION_COOLDOWN_MS2 * 2 ** Math.min(state3.consecutiveFailures ?? 0, 5);
65690
+ if (state3.lastInjectedAt && Date.now() - state3.lastInjectedAt < effectiveCooldown) {
65691
+ log(`[${HOOK_NAME14}] Skipped: cooldown active`, {
65692
+ sessionID,
65693
+ effectiveCooldown,
65694
+ consecutiveFailures: state3.consecutiveFailures ?? 0
65695
+ });
65696
+ return;
65697
+ }
65698
+ let resolvedInfo;
65699
+ let hasCompactionMessage = false;
65700
+ try {
65701
+ const messagesResp = await ctx.client.session.messages({
65702
+ path: { id: sessionID }
65703
+ });
65704
+ const messages = normalizeSDKResponse(messagesResp, []);
65705
+ for (let i = messages.length - 1;i >= 0; i--) {
65706
+ const info = messages[i].info;
65707
+ if (info?.agent === "compaction") {
65708
+ hasCompactionMessage = true;
65709
+ continue;
65710
+ }
65711
+ if (info?.agent || info?.model || info?.modelID && info?.providerID) {
65712
+ resolvedInfo = {
65713
+ agent: info.agent,
65714
+ model: info.model ?? (info.providerID && info.modelID ? { providerID: info.providerID, modelID: info.modelID } : undefined),
65715
+ tools: info.tools
65716
+ };
65717
+ break;
65718
+ }
65719
+ }
65720
+ } catch (error51) {
65721
+ log(`[${HOOK_NAME14}] Failed to fetch messages for agent check`, { sessionID, error: String(error51) });
65722
+ }
65723
+ log(`[${HOOK_NAME14}] Agent check`, { sessionID, agentName: resolvedInfo?.agent, skipAgents, hasCompactionMessage });
65724
+ const resolvedAgentName = resolvedInfo?.agent;
65725
+ if (resolvedAgentName && skipAgents.some((s) => getAgentConfigKey(s) === getAgentConfigKey(resolvedAgentName))) {
65726
+ log(`[${HOOK_NAME14}] Skipped: agent in skipAgents list`, { sessionID, agent: resolvedAgentName });
65727
+ return;
65728
+ }
65729
+ if (hasCompactionMessage && !resolvedInfo?.agent) {
65730
+ log(`[${HOOK_NAME14}] Skipped: compaction occurred but no agent info resolved`, { sessionID });
65731
+ return;
65732
+ }
65733
+ if (isContinuationStopped?.(sessionID)) {
65734
+ log(`[${HOOK_NAME14}] Skipped: continuation stopped for session`, { sessionID });
65735
+ return;
65736
+ }
65737
+ startCountdown({
65738
+ ctx,
65739
+ sessionID,
65740
+ incompleteCount,
65741
+ total,
65742
+ resolvedInfo,
65743
+ backgroundManager,
65744
+ skipAgents,
65745
+ sessionStateStore
65746
+ });
65747
+ }
65748
+
65749
+ // src/hooks/task-continuation-enforcer/non-idle-events.ts
65750
+ init_logger();
65751
+ function handleNonIdleEvent(args) {
65752
+ const { eventType, properties, sessionStateStore } = args;
65753
+ if (eventType === "message.updated") {
65754
+ const info = properties?.info;
65755
+ const sessionID = info?.sessionID;
65756
+ const role = info?.role;
65556
65757
  if (!sessionID)
65557
65758
  return;
65759
+ if (role === "user") {
65760
+ const state3 = sessionStateStore.getExistingState(sessionID);
65761
+ if (state3?.countdownStartedAt) {
65762
+ const elapsed = Date.now() - state3.countdownStartedAt;
65763
+ if (elapsed < COUNTDOWN_GRACE_PERIOD_MS) {
65764
+ log(`[${HOOK_NAME14}] Ignoring user message in grace period`, { sessionID, elapsed });
65765
+ return;
65766
+ }
65767
+ }
65768
+ if (state3)
65769
+ state3.abortDetectedAt = undefined;
65770
+ sessionStateStore.cancelCountdown(sessionID);
65771
+ return;
65772
+ }
65773
+ if (role === "assistant") {
65774
+ const state3 = sessionStateStore.getExistingState(sessionID);
65775
+ if (state3)
65776
+ state3.abortDetectedAt = undefined;
65777
+ sessionStateStore.cancelCountdown(sessionID);
65778
+ return;
65779
+ }
65780
+ return;
65781
+ }
65782
+ if (eventType === "message.part.updated") {
65783
+ const info = properties?.info;
65784
+ const sessionID = info?.sessionID;
65785
+ const role = info?.role;
65786
+ if (sessionID && role === "assistant") {
65787
+ const state3 = sessionStateStore.getExistingState(sessionID);
65788
+ if (state3)
65789
+ state3.abortDetectedAt = undefined;
65790
+ sessionStateStore.cancelCountdown(sessionID);
65791
+ }
65792
+ return;
65793
+ }
65794
+ if (eventType === "tool.execute.before" || eventType === "tool.execute.after") {
65795
+ const sessionID = properties?.sessionID;
65796
+ if (sessionID) {
65797
+ const state3 = sessionStateStore.getExistingState(sessionID);
65798
+ if (state3)
65799
+ state3.abortDetectedAt = undefined;
65800
+ sessionStateStore.cancelCountdown(sessionID);
65801
+ }
65802
+ return;
65803
+ }
65804
+ if (eventType === "session.deleted") {
65805
+ const sessionInfo = properties?.info;
65806
+ if (sessionInfo?.id) {
65807
+ sessionStateStore.cleanup(sessionInfo.id);
65808
+ log(`[${HOOK_NAME14}] Session deleted: cleaned up`, { sessionID: sessionInfo.id });
65809
+ }
65810
+ return;
65811
+ }
65812
+ }
65813
+
65814
+ // src/hooks/task-continuation-enforcer/handler.ts
65815
+ function createTaskContinuationHandler(args) {
65816
+ const {
65817
+ ctx,
65818
+ sessionStateStore,
65819
+ backgroundManager,
65820
+ skipAgents = DEFAULT_SKIP_AGENTS,
65821
+ isContinuationStopped
65822
+ } = args;
65823
+ return async ({ event }) => {
65824
+ const props = event.properties;
65825
+ if (event.type === "session.error") {
65826
+ const sessionID = props?.sessionID;
65827
+ if (!sessionID)
65828
+ return;
65829
+ const error51 = props?.error;
65830
+ if (error51?.name === "MessageAbortedError" || error51?.name === "AbortError") {
65831
+ const state3 = sessionStateStore.getState(sessionID);
65832
+ state3.abortDetectedAt = Date.now();
65833
+ log(`[${HOOK_NAME14}] Abort detected via session.error`, { sessionID, errorName: error51.name });
65834
+ }
65835
+ sessionStateStore.cancelCountdown(sessionID);
65836
+ log(`[${HOOK_NAME14}] session.error`, { sessionID });
65837
+ return;
65838
+ }
65839
+ if (event.type === "session.idle") {
65840
+ const sessionID = props?.sessionID;
65841
+ if (!sessionID)
65842
+ return;
65843
+ await handleSessionIdle({
65844
+ ctx,
65845
+ sessionID,
65846
+ sessionStateStore,
65847
+ backgroundManager,
65848
+ skipAgents,
65849
+ isContinuationStopped
65850
+ });
65851
+ return;
65852
+ }
65853
+ handleNonIdleEvent({
65854
+ eventType: event.type,
65855
+ properties: props,
65856
+ sessionStateStore
65857
+ });
65858
+ };
65859
+ }
65860
+
65861
+ // src/hooks/task-continuation-enforcer/session-state.ts
65862
+ var SESSION_STATE_TTL_MS = 10 * 60 * 1000;
65863
+ var SESSION_STATE_PRUNE_INTERVAL_MS = 2 * 60 * 1000;
65864
+ function createSessionStateStore() {
65865
+ const sessions = new Map;
65866
+ let pruneInterval;
65867
+ pruneInterval = setInterval(() => {
65558
65868
  const now = Date.now();
65559
- const last = lastSync.get(sessionID) ?? 0;
65560
- if (now - last < DEBOUNCE_MS)
65869
+ for (const [sessionID, tracked] of sessions.entries()) {
65870
+ if (now - tracked.lastAccessedAt > SESSION_STATE_TTL_MS) {
65871
+ cancelCountdown(sessionID);
65872
+ sessions.delete(sessionID);
65873
+ }
65874
+ }
65875
+ }, SESSION_STATE_PRUNE_INTERVAL_MS);
65876
+ if (typeof pruneInterval === "object" && "unref" in pruneInterval) {
65877
+ pruneInterval.unref();
65878
+ }
65879
+ function getState(sessionID) {
65880
+ const existing = sessions.get(sessionID);
65881
+ if (existing) {
65882
+ existing.lastAccessedAt = Date.now();
65883
+ return existing.state;
65884
+ }
65885
+ const state3 = {
65886
+ consecutiveFailures: 0
65887
+ };
65888
+ sessions.set(sessionID, { state: state3, lastAccessedAt: Date.now() });
65889
+ return state3;
65890
+ }
65891
+ function getExistingState(sessionID) {
65892
+ const existing = sessions.get(sessionID);
65893
+ if (existing) {
65894
+ existing.lastAccessedAt = Date.now();
65895
+ return existing.state;
65896
+ }
65897
+ return;
65898
+ }
65899
+ function cancelCountdown(sessionID) {
65900
+ const tracked = sessions.get(sessionID);
65901
+ if (!tracked)
65561
65902
  return;
65562
- lastSync.set(sessionID, now);
65563
- try {
65564
- const tasks = loadAllTasks(pluginConfig);
65565
- await syncAllTasksToTodos(ctx, tasks, sessionID);
65566
- log(`[${HOOK_NAME14}] Synced ${tasks.length} tasks to session`, { sessionID });
65567
- } catch (err) {
65568
- log(`[${HOOK_NAME14}] Sync failed`, { sessionID, error: String(err) });
65903
+ const state3 = tracked.state;
65904
+ if (state3.countdownTimer) {
65905
+ clearTimeout(state3.countdownTimer);
65906
+ state3.countdownTimer = undefined;
65569
65907
  }
65908
+ if (state3.countdownInterval) {
65909
+ clearInterval(state3.countdownInterval);
65910
+ state3.countdownInterval = undefined;
65911
+ }
65912
+ state3.inFlight = false;
65913
+ state3.countdownStartedAt = undefined;
65914
+ }
65915
+ function cleanup(sessionID) {
65916
+ cancelCountdown(sessionID);
65917
+ sessions.delete(sessionID);
65918
+ }
65919
+ function cancelAllCountdowns() {
65920
+ for (const sessionID of sessions.keys()) {
65921
+ cancelCountdown(sessionID);
65922
+ }
65923
+ }
65924
+ function shutdown() {
65925
+ clearInterval(pruneInterval);
65926
+ cancelAllCountdowns();
65927
+ sessions.clear();
65928
+ }
65929
+ return {
65930
+ getState,
65931
+ getExistingState,
65932
+ cancelCountdown,
65933
+ cleanup,
65934
+ cancelAllCountdowns,
65935
+ shutdown
65936
+ };
65937
+ }
65938
+
65939
+ // src/hooks/task-continuation-enforcer/index.ts
65940
+ function createTaskContinuationEnforcer(ctx, options = {}) {
65941
+ const {
65942
+ backgroundManager,
65943
+ skipAgents = DEFAULT_SKIP_AGENTS,
65944
+ isContinuationStopped
65945
+ } = options;
65946
+ const sessionStateStore = createSessionStateStore();
65947
+ const markRecovering = (sessionID) => {
65948
+ const state3 = sessionStateStore.getState(sessionID);
65949
+ state3.isRecovering = true;
65950
+ sessionStateStore.cancelCountdown(sessionID);
65951
+ log(`[${HOOK_NAME14}] Session marked as recovering`, { sessionID });
65952
+ };
65953
+ const markRecoveryComplete = (sessionID) => {
65954
+ const state3 = sessionStateStore.getExistingState(sessionID);
65955
+ if (state3) {
65956
+ state3.isRecovering = false;
65957
+ log(`[${HOOK_NAME14}] Session recovery complete`, { sessionID });
65958
+ }
65959
+ };
65960
+ const handler = createTaskContinuationHandler({
65961
+ ctx,
65962
+ sessionStateStore,
65963
+ backgroundManager,
65964
+ skipAgents,
65965
+ isContinuationStopped
65966
+ });
65967
+ const cancelAllCountdowns = () => {
65968
+ sessionStateStore.cancelAllCountdowns();
65969
+ log(`[${HOOK_NAME14}] All countdowns cancelled`);
65570
65970
  };
65571
65971
  return {
65572
- "tool.execute.after": async (input, _output) => {
65573
- if (!enabled)
65972
+ handler,
65973
+ markRecovering,
65974
+ markRecoveryComplete,
65975
+ cancelAllCountdowns
65976
+ };
65977
+ }
65978
+ // src/hooks/task-edit-guard/constants.ts
65979
+ var HOOK_NAME15 = "task-edit-guard";
65980
+ var BLOCKED_PATTERNS = [
65981
+ /sed\s+.*\.matrixx\/plans/,
65982
+ /sed\s+.*\.matrixx\/tasks/,
65983
+ /python3?\s+.*\.matrixx\/plans/,
65984
+ /python3?\s+.*\.matrixx\/tasks/,
65985
+ /echo\s+.*\.matrixx\/plans/,
65986
+ /echo\s+.*\.matrixx\/tasks/,
65987
+ /cat\s*>.*\.matrixx\/plans/,
65988
+ /cat\s*>.*\.matrixx\/tasks/,
65989
+ /mv\s+.*\.matrixx\/plans/,
65990
+ /mv\s+.*\.matrixx\/tasks/,
65991
+ /rm\s+.*\.matrixx\/tasks\/T-.*\.json/,
65992
+ /printf\s+.*\.matrixx\/plans/,
65993
+ /printf\s+.*\.matrixx\/tasks/
65994
+ ];
65995
+ // src/hooks/task-edit-guard/hook.ts
65996
+ function createTaskEditGuardHook(ctx) {
65997
+ return {
65998
+ "tool.execute.before": async (input, output) => {
65999
+ if (input.tool?.toLowerCase() !== "bash")
65574
66000
  return;
65575
- await syncForSession(input.sessionID);
65576
- },
65577
- "chat.message": async (input, _output) => {
65578
- if (!enabled)
66001
+ const args = output.args;
66002
+ const cmd = args?.command;
66003
+ if (!cmd)
65579
66004
  return;
65580
- await syncForSession(input.sessionID);
65581
- },
65582
- "experimental.chat.messages.transform": async (_input, output) => {
65583
- if (!enabled)
66005
+ const lower = cmd.toLowerCase();
66006
+ if (lower.includes("grep") && !lower.includes("sed") && !lower.includes("python")) {
66007
+ const isOnlyGrep = BLOCKED_PATTERNS.every((rx) => !rx.test(cmd));
66008
+ if (isOnlyGrep)
66009
+ return;
66010
+ }
66011
+ const hit = BLOCKED_PATTERNS.some((rx) => rx.test(cmd));
66012
+ if (!hit)
65584
66013
  return;
65585
- let sessionID;
65586
- for (let i = output.messages.length - 1;i >= 0; i--) {
65587
- const id = output.messages[i].info.sessionID;
65588
- if (id) {
65589
- sessionID = id;
65590
- break;
66014
+ ctx.directory;
66015
+ log(`[${HOOK_NAME15}] BLOCKED raw bash edit`, {
66016
+ sessionID: input.sessionID,
66017
+ command: cmd.slice(0, 120)
66018
+ });
66019
+ throw new Error("Blocked: raw bash edit to plan/task files. " + "Use Edit (hashline IDs) for .matrixx/plans/*.md and task_create/task_update/task_cleanup for .matrixx/tasks/T-*.json \u2014 raw bash sed/python bypasses project-scoped task system");
66020
+ }
66021
+ };
66022
+ }
66023
+ // src/hooks/task-notepad/constants.ts
66024
+ var HOOK_NAME16 = "task-notepad";
66025
+ var PLAN_DIR_NAME = ".matrixx";
66026
+ var PLANS_SUBDIR = "plans";
66027
+ var NOTEPAD_DIR_NAME = "notepads";
66028
+ var TASKS_SUBDIR = "tasks";
66029
+ var PLAN_EXTENSION = ".md";
66030
+ var NOTEPAD_EXTENSION = ".md";
66031
+ var TODO_TOOL_NAMES = new Set(["todowrite", "TodoWrite"]);
66032
+ var TASK_NOTEPAD_FRAGMENT = "task-notepad";
66033
+ var TODO_FETCH_ERROR_FRAGMENT = "Failed to fetch todos for task-notepad";
66034
+ // src/hooks/task-notepad/hook.ts
66035
+ init_logger();
66036
+ import { existsSync as existsSync45, mkdirSync as mkdirSync16, readdirSync as readdirSync16, readFileSync as readFileSync33, statSync as statSync8, writeFileSync as writeFileSync19 } from "fs";
66037
+ import { dirname as dirname15, join as join52 } from "path";
66038
+ function createTaskNotepadHook(ctx) {
66039
+ return {
66040
+ "tool.execute.after": async (input, output) => {
66041
+ if (!output)
66042
+ return;
66043
+ if (!TODO_TOOL_NAMES.has(input.tool))
66044
+ return;
66045
+ const todos = await fetchTodos(ctx, input.sessionID);
66046
+ const inProgress = todos.find((t) => t.status === "in_progress");
66047
+ const completed = todos.filter((t) => t.status === "completed");
66048
+ if (!inProgress && completed.length === 0)
66049
+ return;
66050
+ const plan = resolveActivePlan(ctx.directory);
66051
+ if (inProgress) {
66052
+ if (!plan) {
66053
+ log(`[${HOOK_NAME16}] No active plan, skipping notepad creation`, {
66054
+ sessionID: input.sessionID
66055
+ });
66056
+ } else {
66057
+ ensureNotepad(ctx.directory, plan.name, inProgress);
65591
66058
  }
65592
66059
  }
65593
- await syncForSession(sessionID);
65594
- },
65595
- event: async (input) => {
65596
- if (!enabled)
66060
+ if (!plan)
65597
66061
  return;
65598
- const props = input.event.properties;
65599
- const sessionID = props?.sessionID ?? props?.info?.id;
65600
- if (sessionID && input.event.type === "session.idle") {
65601
- await syncForSession(sessionID);
66062
+ for (const done of completed) {
66063
+ appendCompletion(ctx.directory, plan.name, done);
65602
66064
  }
65603
- },
65604
- _syncForSession: syncForSession
66065
+ }
66066
+ };
66067
+ }
66068
+ async function fetchTodos(ctx, sessionID) {
66069
+ try {
66070
+ const response = await ctx.client.session.todo({ path: { id: sessionID } });
66071
+ const payload = response;
66072
+ if (Array.isArray(payload?.data)) {
66073
+ return payload.data;
66074
+ }
66075
+ if (Array.isArray(response)) {
66076
+ return response;
66077
+ }
66078
+ return [];
66079
+ } catch (err) {
66080
+ log(`[${HOOK_NAME16}] ${TODO_FETCH_ERROR_FRAGMENT}`, {
66081
+ sessionID,
66082
+ error: String(err)
66083
+ });
66084
+ return [];
66085
+ }
66086
+ }
66087
+ function resolveActivePlan(directory) {
66088
+ const plansDir = join52(directory, PLAN_DIR_NAME, PLANS_SUBDIR);
66089
+ if (!existsSync45(plansDir))
66090
+ return null;
66091
+ let entries;
66092
+ try {
66093
+ entries = readdirSync16(plansDir);
66094
+ } catch {
66095
+ return null;
66096
+ }
66097
+ let best = null;
66098
+ for (const entry of entries) {
66099
+ if (!entry.endsWith(PLAN_EXTENSION))
66100
+ continue;
66101
+ const fullPath = join52(plansDir, entry);
66102
+ try {
66103
+ const stats = statSync8(fullPath);
66104
+ if (!stats.isFile())
66105
+ continue;
66106
+ const candidate = {
66107
+ name: entry.slice(0, -PLAN_EXTENSION.length),
66108
+ path: fullPath,
66109
+ mtimeMs: stats.mtimeMs
66110
+ };
66111
+ if (!best || candidate.mtimeMs > best.mtimeMs) {
66112
+ best = candidate;
66113
+ }
66114
+ } catch {}
66115
+ }
66116
+ return best;
66117
+ }
66118
+ function ensureNotepad(directory, planName, todo) {
66119
+ const notepadPath = notepadPathFor(directory, planName, todo.id);
66120
+ if (existsSync45(notepadPath))
66121
+ return;
66122
+ mkdirSync16(dirname15(notepadPath), { recursive: true });
66123
+ const content = renderNotepad(todo);
66124
+ writeFileSync19(notepadPath, content, "utf-8");
66125
+ log(`[${HOOK_NAME16}] Created task notepad`, { plan: planName, taskID: todo.id });
66126
+ }
66127
+ function appendCompletion(directory, planName, todo) {
66128
+ const notepadPath = notepadPathFor(directory, planName, todo.id);
66129
+ if (!existsSync45(notepadPath))
66130
+ return;
66131
+ const existing = readFileSync33(notepadPath, "utf-8");
66132
+ if (existing.includes("**Status**: completed"))
66133
+ return;
66134
+ const stamp = new Date().toISOString();
66135
+ const updated = `${existing}
66136
+ ## Completion
66137
+ - completed_at: ${stamp}
66138
+ - status: completed
66139
+ `;
66140
+ writeFileSync19(notepadPath, updated, "utf-8");
66141
+ log(`[${HOOK_NAME16}] Appended completion to notepad`, { plan: planName, taskID: todo.id });
66142
+ }
66143
+ function notepadPathFor(directory, planName, taskID) {
66144
+ return join52(directory, PLAN_DIR_NAME, NOTEPAD_DIR_NAME, planName, TASKS_SUBDIR, `${taskID}${NOTEPAD_EXTENSION}`);
66145
+ }
66146
+ function renderNotepad(todo) {
66147
+ const priority = todo.priority ?? "medium";
66148
+ return `# Task: ${todo.content}
66149
+
66150
+ **Plan**: ${TASK_NOTEPAD_FRAGMENT}
66151
+ **Task ID**: ${todo.id}
66152
+ **Priority**: ${priority}
66153
+ **Status**: ${todo.status}
66154
+ **Started**: ${new Date().toISOString()}
66155
+
66156
+ ## Findings
66157
+
66158
+ (Record what you learn about this code as you work \u2014 patterns, conventions, gotchas, useful references.)
66159
+
66160
+ ## Blockers
66161
+
66162
+ (Anything blocking progress. Be specific: file path, line, error, workaround if any.)
66163
+
66164
+ ## Questions
66165
+
66166
+ (Open questions to revisit later. Don't lose them when context compacts.)
66167
+
66168
+ ## Results
66169
+
66170
+ (Summary on completion. What changed, what was verified, what remains.)
66171
+ `;
66172
+ }
66173
+ // src/hooks/task-resume-info/hook.ts
66174
+ var TARGET_TOOLS2 = ["task", "Task", "task_tool", "delegate_agent"];
66175
+ var SESSION_ID_PATTERNS = [
66176
+ /Session ID: (ses_[a-zA-Z0-9_-]+)/,
66177
+ /session_id: (ses_[a-zA-Z0-9_-]+)/,
66178
+ /<task_metadata>\s*session_id: (ses_[a-zA-Z0-9_-]+)/,
66179
+ /sessionId: (ses_[a-zA-Z0-9_-]+)/
66180
+ ];
66181
+ function extractSessionId(output) {
66182
+ for (const pattern of SESSION_ID_PATTERNS) {
66183
+ const match = output.match(pattern);
66184
+ if (match)
66185
+ return match[1] ?? null;
66186
+ }
66187
+ return null;
66188
+ }
66189
+ function createTaskResumeInfoHook() {
66190
+ const toolExecuteAfter = async (input, output) => {
66191
+ if (!TARGET_TOOLS2.includes(input.tool))
66192
+ return;
66193
+ const outputText = output.output ?? "";
66194
+ if (outputText.startsWith("Error:") || outputText.startsWith("Failed"))
66195
+ return;
66196
+ if (outputText.includes(`
66197
+ to continue:`))
66198
+ return;
66199
+ const sessionId = extractSessionId(outputText);
66200
+ if (!sessionId)
66201
+ return;
66202
+ output.output = outputText.trimEnd() + `
66203
+
66204
+ to continue: task(session_id="${sessionId}", prompt="...")`;
66205
+ };
66206
+ return {
66207
+ "tool.execute.after": toolExecuteAfter
65605
66208
  };
65606
66209
  }
66210
+ // src/shared/task-system-gating.ts
66211
+ var TASK_SYSTEM_DEFAULT = true;
66212
+ function isTaskSystemEnabled(config2) {
66213
+ return config2?.experimental?.task_system ?? TASK_SYSTEM_DEFAULT;
66214
+ }
66215
+
65607
66216
  // src/hooks/tasks-todowrite-disabler/constants.ts
65608
66217
  var BLOCKED_TOOLS2 = ["TodoWrite", "TodoRead"];
65609
66218
  var REPLACEMENT_MESSAGE = `TodoRead/TodoWrite are DISABLED because experimental.task_system is enabled.
@@ -65637,10 +66246,10 @@ DO NOT retry TodoWrite. Convert to TaskCreate NOW.`;
65637
66246
 
65638
66247
  // src/hooks/tasks-todowrite-disabler/hook.ts
65639
66248
  function createTasksTodowriteDisablerHook(config2) {
65640
- const isTaskSystemEnabled = config2.experimental?.task_system ?? false;
66249
+ const enabled = isTaskSystemEnabled(config2);
65641
66250
  return {
65642
66251
  "tool.execute.before": async (input, _output) => {
65643
- if (!isTaskSystemEnabled) {
66252
+ if (!enabled) {
65644
66253
  return;
65645
66254
  }
65646
66255
  const toolName = input.tool;
@@ -66122,22 +66731,22 @@ function createThinkingBlockValidatorHook() {
66122
66731
  init_logger();
66123
66732
 
66124
66733
  // src/hooks/todo-continuation-enforcer/constants.ts
66125
- var HOOK_NAME15 = "todo-continuation-enforcer";
66126
- var DEFAULT_SKIP_AGENTS = ["oracle", "compaction"];
66127
- var CONTINUATION_PROMPT2 = `${createSystemDirective(SystemDirectiveTypes.TODO_CONTINUATION)}
66734
+ var HOOK_NAME17 = "todo-continuation-enforcer";
66735
+ var DEFAULT_SKIP_AGENTS2 = ["oracle", "compaction"];
66736
+ var CONTINUATION_PROMPT3 = `${createSystemDirective(SystemDirectiveTypes.TODO_CONTINUATION)}
66128
66737
 
66129
66738
  Incomplete tasks remain in your todo list. Continue working on the next pending task.
66130
66739
 
66131
66740
  - Proceed without asking for permission
66132
66741
  - Mark each task complete when finished
66133
66742
  - Do not stop until all tasks are done`;
66134
- var COUNTDOWN_SECONDS = 2;
66135
- var TOAST_DURATION_MS = 900;
66136
- var COUNTDOWN_GRACE_PERIOD_MS = 500;
66137
- var ABORT_WINDOW_MS = 3000;
66138
- var CONTINUATION_COOLDOWN_MS2 = 30000;
66139
- var MAX_CONSECUTIVE_FAILURES = 5;
66140
- var FAILURE_RESET_WINDOW_MS = 5 * 60 * 1000;
66743
+ var COUNTDOWN_SECONDS2 = 2;
66744
+ var TOAST_DURATION_MS2 = 900;
66745
+ var COUNTDOWN_GRACE_PERIOD_MS2 = 500;
66746
+ var ABORT_WINDOW_MS2 = 3000;
66747
+ var CONTINUATION_COOLDOWN_MS3 = 30000;
66748
+ var MAX_CONSECUTIVE_FAILURES2 = 5;
66749
+ var FAILURE_RESET_WINDOW_MS2 = 5 * 60 * 1000;
66141
66750
 
66142
66751
  // src/hooks/todo-continuation-enforcer/handler.ts
66143
66752
  init_logger();
@@ -66146,7 +66755,7 @@ init_logger();
66146
66755
  init_logger();
66147
66756
 
66148
66757
  // src/hooks/todo-continuation-enforcer/abort-detection.ts
66149
- function isLastAssistantMessageAborted(messages) {
66758
+ function isLastAssistantMessageAborted2(messages) {
66150
66759
  if (!messages || messages.length === 0)
66151
66760
  return false;
66152
66761
  for (let i = messages.length - 1;i >= 0; i--) {
@@ -66172,28 +66781,28 @@ function getIncompleteCount(todos) {
66172
66781
  }
66173
66782
 
66174
66783
  // src/hooks/todo-continuation-enforcer/continuation-injection.ts
66175
- function hasWritePermission(tools) {
66784
+ function hasWritePermission2(tools) {
66176
66785
  const editPermission = tools?.edit;
66177
66786
  const writePermission = tools?.write;
66178
66787
  return !tools || editPermission !== false && editPermission !== "deny" && writePermission !== false && writePermission !== "deny";
66179
66788
  }
66180
- async function injectContinuation(args) {
66789
+ async function injectContinuation2(args) {
66181
66790
  const {
66182
66791
  ctx,
66183
66792
  sessionID,
66184
66793
  backgroundManager,
66185
- skipAgents = DEFAULT_SKIP_AGENTS,
66794
+ skipAgents = DEFAULT_SKIP_AGENTS2,
66186
66795
  resolvedInfo,
66187
66796
  sessionStateStore
66188
66797
  } = args;
66189
66798
  const state3 = sessionStateStore.getExistingState(sessionID);
66190
66799
  if (state3?.isRecovering) {
66191
- log(`[${HOOK_NAME15}] Skipped injection: in recovery`, { sessionID });
66800
+ log(`[${HOOK_NAME17}] Skipped injection: in recovery`, { sessionID });
66192
66801
  return;
66193
66802
  }
66194
66803
  const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((task) => task.status === "running") : false;
66195
66804
  if (hasRunningBgTasks) {
66196
- log(`[${HOOK_NAME15}] Skipped injection: background tasks running`, { sessionID });
66805
+ log(`[${HOOK_NAME17}] Skipped injection: background tasks running`, { sessionID });
66197
66806
  return;
66198
66807
  }
66199
66808
  let todos = [];
@@ -66201,12 +66810,12 @@ async function injectContinuation(args) {
66201
66810
  const response = await ctx.client.session.todo({ path: { id: sessionID } });
66202
66811
  todos = normalizeSDKResponse(response, [], { preferResponseOnMissingData: true });
66203
66812
  } catch (error51) {
66204
- log(`[${HOOK_NAME15}] Failed to fetch todos`, { sessionID, error: String(error51) });
66813
+ log(`[${HOOK_NAME17}] Failed to fetch todos`, { sessionID, error: String(error51) });
66205
66814
  return;
66206
66815
  }
66207
66816
  const freshIncompleteCount = getIncompleteCount(todos);
66208
66817
  if (freshIncompleteCount === 0) {
66209
- log(`[${HOOK_NAME15}] Skipped injection: no incomplete todos`, { sessionID });
66818
+ log(`[${HOOK_NAME17}] Skipped injection: no incomplete todos`, { sessionID });
66210
66819
  return;
66211
66820
  }
66212
66821
  let agentName = resolvedInfo?.agent;
@@ -66229,17 +66838,17 @@ async function injectContinuation(args) {
66229
66838
  tools = tools ?? previousMessage?.tools;
66230
66839
  }
66231
66840
  if (agentName && skipAgents.some((s) => getAgentConfigKey(s) === getAgentConfigKey(agentName))) {
66232
- log(`[${HOOK_NAME15}] Skipped: agent in skipAgents list`, { sessionID, agent: agentName });
66841
+ log(`[${HOOK_NAME17}] Skipped: agent in skipAgents list`, { sessionID, agent: agentName });
66233
66842
  return;
66234
66843
  }
66235
- if (!hasWritePermission(tools)) {
66236
- log(`[${HOOK_NAME15}] Skipped: agent lacks write permission`, { sessionID, agent: agentName });
66844
+ if (!hasWritePermission2(tools)) {
66845
+ log(`[${HOOK_NAME17}] Skipped: agent lacks write permission`, { sessionID, agent: agentName });
66237
66846
  return;
66238
66847
  }
66239
66848
  const incompleteTodos = todos.filter((todo) => todo.status !== "completed" && todo.status !== "cancelled");
66240
66849
  const todoList = incompleteTodos.map((todo) => `- [${todo.status}] ${todo.content}`).join(`
66241
66850
  `);
66242
- const prompt = `${CONTINUATION_PROMPT2}
66851
+ const prompt = `${CONTINUATION_PROMPT3}
66243
66852
 
66244
66853
  [Status: ${todos.length - freshIncompleteCount}/${todos.length} completed, ${freshIncompleteCount} remaining]
66245
66854
 
@@ -66250,7 +66859,7 @@ ${todoList}`;
66250
66859
  injectionState.inFlight = true;
66251
66860
  }
66252
66861
  try {
66253
- log(`[${HOOK_NAME15}] Injecting continuation`, {
66862
+ log(`[${HOOK_NAME17}] Injecting continuation`, {
66254
66863
  sessionID,
66255
66864
  agent: agentName,
66256
66865
  model,
@@ -66265,14 +66874,14 @@ ${todoList}`;
66265
66874
  },
66266
66875
  query: { directory: ctx.directory }
66267
66876
  });
66268
- log(`[${HOOK_NAME15}] Injection successful`, { sessionID });
66877
+ log(`[${HOOK_NAME17}] Injection successful`, { sessionID });
66269
66878
  if (injectionState) {
66270
66879
  injectionState.inFlight = false;
66271
66880
  injectionState.lastInjectedAt = Date.now();
66272
66881
  injectionState.consecutiveFailures = 0;
66273
66882
  }
66274
66883
  } catch (error51) {
66275
- log(`[${HOOK_NAME15}] Injection failed`, { sessionID, error: String(error51) });
66884
+ log(`[${HOOK_NAME17}] Injection failed`, { sessionID, error: String(error51) });
66276
66885
  if (injectionState) {
66277
66886
  injectionState.inFlight = false;
66278
66887
  injectionState.lastInjectedAt = Date.now();
@@ -66282,19 +66891,19 @@ ${todoList}`;
66282
66891
  }
66283
66892
 
66284
66893
  // src/hooks/todo-continuation-enforcer/countdown.ts
66285
- async function showCountdownToast(ctx, seconds, incompleteCount) {
66894
+ async function showCountdownToast2(ctx, seconds, incompleteCount) {
66286
66895
  await ctx.client.tui.showToast({
66287
66896
  body: {
66288
66897
  title: "Todo Continuation",
66289
66898
  message: `Resuming in ${seconds}s... (${incompleteCount} tasks remaining)`,
66290
66899
  variant: "warning",
66291
- duration: TOAST_DURATION_MS
66900
+ duration: TOAST_DURATION_MS2
66292
66901
  }
66293
66902
  }).catch((err) => {
66294
66903
  log("[todo-continuation] Countdown toast failed:", err);
66295
66904
  });
66296
66905
  }
66297
- function startCountdown(args) {
66906
+ function startCountdown2(args) {
66298
66907
  const {
66299
66908
  ctx,
66300
66909
  sessionID,
@@ -66305,19 +66914,22 @@ function startCountdown(args) {
66305
66914
  sessionStateStore
66306
66915
  } = args;
66307
66916
  const state3 = sessionStateStore.getState(sessionID);
66308
- sessionStateStore.cancelCountdown(sessionID);
66309
- let secondsRemaining = COUNTDOWN_SECONDS;
66310
- showCountdownToast(ctx, secondsRemaining, incompleteCount);
66917
+ if (state3.countdownTimer || state3.countdownInterval) {
66918
+ log(`[${HOOK_NAME17}] Skipped: countdown already active`, { sessionID });
66919
+ return;
66920
+ }
66921
+ let secondsRemaining = COUNTDOWN_SECONDS2;
66922
+ showCountdownToast2(ctx, secondsRemaining, incompleteCount);
66311
66923
  state3.countdownStartedAt = Date.now();
66312
66924
  state3.countdownInterval = setInterval(() => {
66313
66925
  secondsRemaining--;
66314
66926
  if (secondsRemaining > 0) {
66315
- showCountdownToast(ctx, secondsRemaining, incompleteCount);
66927
+ showCountdownToast2(ctx, secondsRemaining, incompleteCount);
66316
66928
  }
66317
66929
  }, 1000);
66318
66930
  state3.countdownTimer = setTimeout(() => {
66319
66931
  sessionStateStore.cancelCountdown(sessionID);
66320
- injectContinuation({
66932
+ injectContinuation2({
66321
66933
  ctx,
66322
66934
  sessionID,
66323
66935
  backgroundManager,
@@ -66325,34 +66937,38 @@ function startCountdown(args) {
66325
66937
  resolvedInfo,
66326
66938
  sessionStateStore
66327
66939
  });
66328
- }, COUNTDOWN_SECONDS * 1000);
66329
- log(`[${HOOK_NAME15}] Countdown started`, {
66940
+ }, COUNTDOWN_SECONDS2 * 1000);
66941
+ log(`[${HOOK_NAME17}] Countdown started`, {
66330
66942
  sessionID,
66331
- seconds: COUNTDOWN_SECONDS,
66943
+ seconds: COUNTDOWN_SECONDS2,
66332
66944
  incompleteCount
66333
66945
  });
66334
66946
  }
66335
66947
 
66336
66948
  // src/hooks/todo-continuation-enforcer/idle-event.ts
66337
- async function handleSessionIdle(args) {
66949
+ async function handleSessionIdle2(args) {
66338
66950
  const {
66339
66951
  ctx,
66340
66952
  sessionID,
66341
66953
  sessionStateStore,
66342
66954
  backgroundManager,
66343
- skipAgents = DEFAULT_SKIP_AGENTS,
66955
+ skipAgents = DEFAULT_SKIP_AGENTS2,
66344
66956
  isContinuationStopped
66345
66957
  } = args;
66346
- log(`[${HOOK_NAME15}] session.idle`, { sessionID });
66958
+ log(`[${HOOK_NAME17}] session.idle`, { sessionID });
66347
66959
  const state3 = sessionStateStore.getState(sessionID);
66960
+ if (state3.countdownTimer || state3.countdownInterval) {
66961
+ log(`[${HOOK_NAME17}] Skipped: countdown already active`, { sessionID });
66962
+ return;
66963
+ }
66348
66964
  if (state3.isRecovering) {
66349
- log(`[${HOOK_NAME15}] Skipped: in recovery`, { sessionID });
66965
+ log(`[${HOOK_NAME17}] Skipped: in recovery`, { sessionID });
66350
66966
  return;
66351
66967
  }
66352
66968
  if (state3.abortDetectedAt) {
66353
66969
  const timeSinceAbort = Date.now() - state3.abortDetectedAt;
66354
- if (timeSinceAbort < ABORT_WINDOW_MS) {
66355
- log(`[${HOOK_NAME15}] Skipped: abort detected via event ${timeSinceAbort}ms ago`, { sessionID });
66970
+ if (timeSinceAbort < ABORT_WINDOW_MS2) {
66971
+ log(`[${HOOK_NAME17}] Skipped: abort detected via event ${timeSinceAbort}ms ago`, { sessionID });
66356
66972
  state3.abortDetectedAt = undefined;
66357
66973
  return;
66358
66974
  }
@@ -66360,7 +66976,7 @@ async function handleSessionIdle(args) {
66360
66976
  }
66361
66977
  const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((task) => task.status === "running") : false;
66362
66978
  if (hasRunningBgTasks) {
66363
- log(`[${HOOK_NAME15}] Skipped: background tasks running`, { sessionID });
66979
+ log(`[${HOOK_NAME17}] Skipped: background tasks running`, { sessionID });
66364
66980
  return;
66365
66981
  }
66366
66982
  try {
@@ -66369,52 +66985,52 @@ async function handleSessionIdle(args) {
66369
66985
  query: { directory: ctx.directory }
66370
66986
  });
66371
66987
  const messages = normalizeSDKResponse(messagesResp, []);
66372
- if (isLastAssistantMessageAborted(messages)) {
66373
- log(`[${HOOK_NAME15}] Skipped: last assistant message was aborted (API fallback)`, { sessionID });
66988
+ if (isLastAssistantMessageAborted2(messages)) {
66989
+ log(`[${HOOK_NAME17}] Skipped: last assistant message was aborted (API fallback)`, { sessionID });
66374
66990
  return;
66375
66991
  }
66376
66992
  } catch (error51) {
66377
- log(`[${HOOK_NAME15}] Messages fetch failed, continuing`, { sessionID, error: String(error51) });
66993
+ log(`[${HOOK_NAME17}] Messages fetch failed, continuing`, { sessionID, error: String(error51) });
66378
66994
  }
66379
66995
  let todos = [];
66380
66996
  try {
66381
66997
  const response = await ctx.client.session.todo({ path: { id: sessionID } });
66382
66998
  todos = normalizeSDKResponse(response, [], { preferResponseOnMissingData: true });
66383
66999
  } catch (error51) {
66384
- log(`[${HOOK_NAME15}] Todo fetch failed`, { sessionID, error: String(error51) });
67000
+ log(`[${HOOK_NAME17}] Todo fetch failed`, { sessionID, error: String(error51) });
66385
67001
  return;
66386
67002
  }
66387
67003
  if (!todos || todos.length === 0) {
66388
- log(`[${HOOK_NAME15}] No todos`, { sessionID });
67004
+ log(`[${HOOK_NAME17}] No todos`, { sessionID });
66389
67005
  return;
66390
67006
  }
66391
67007
  const incompleteCount = getIncompleteCount(todos);
66392
67008
  if (incompleteCount === 0) {
66393
- log(`[${HOOK_NAME15}] All todos complete`, { sessionID, total: todos.length });
67009
+ log(`[${HOOK_NAME17}] All todos complete`, { sessionID, total: todos.length });
66394
67010
  return;
66395
67011
  }
66396
67012
  if (state3.inFlight) {
66397
- log(`[${HOOK_NAME15}] Skipped: injection in flight`, { sessionID });
67013
+ log(`[${HOOK_NAME17}] Skipped: injection in flight`, { sessionID });
66398
67014
  return;
66399
67015
  }
66400
- if (state3.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES && state3.lastInjectedAt && Date.now() - state3.lastInjectedAt >= FAILURE_RESET_WINDOW_MS) {
67016
+ if (state3.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2 && state3.lastInjectedAt && Date.now() - state3.lastInjectedAt >= FAILURE_RESET_WINDOW_MS2) {
66401
67017
  state3.consecutiveFailures = 0;
66402
- log(`[${HOOK_NAME15}] Reset consecutive failures after recovery window`, {
67018
+ log(`[${HOOK_NAME17}] Reset consecutive failures after recovery window`, {
66403
67019
  sessionID,
66404
- failureResetWindowMs: FAILURE_RESET_WINDOW_MS
67020
+ failureResetWindowMs: FAILURE_RESET_WINDOW_MS2
66405
67021
  });
66406
67022
  }
66407
- if (state3.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
66408
- log(`[${HOOK_NAME15}] Skipped: max consecutive failures reached`, {
67023
+ if (state3.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) {
67024
+ log(`[${HOOK_NAME17}] Skipped: max consecutive failures reached`, {
66409
67025
  sessionID,
66410
67026
  consecutiveFailures: state3.consecutiveFailures,
66411
- maxConsecutiveFailures: MAX_CONSECUTIVE_FAILURES
67027
+ maxConsecutiveFailures: MAX_CONSECUTIVE_FAILURES2
66412
67028
  });
66413
67029
  return;
66414
67030
  }
66415
- const effectiveCooldown = CONTINUATION_COOLDOWN_MS2 * 2 ** Math.min(state3.consecutiveFailures, 5);
67031
+ const effectiveCooldown = CONTINUATION_COOLDOWN_MS3 * 2 ** Math.min(state3.consecutiveFailures, 5);
66416
67032
  if (state3.lastInjectedAt && Date.now() - state3.lastInjectedAt < effectiveCooldown) {
66417
- log(`[${HOOK_NAME15}] Skipped: cooldown active`, {
67033
+ log(`[${HOOK_NAME17}] Skipped: cooldown active`, {
66418
67034
  sessionID,
66419
67035
  effectiveCooldown,
66420
67036
  consecutiveFailures: state3.consecutiveFailures
@@ -66444,23 +67060,23 @@ async function handleSessionIdle(args) {
66444
67060
  }
66445
67061
  }
66446
67062
  } catch (error51) {
66447
- log(`[${HOOK_NAME15}] Failed to fetch messages for agent check`, { sessionID, error: String(error51) });
67063
+ log(`[${HOOK_NAME17}] Failed to fetch messages for agent check`, { sessionID, error: String(error51) });
66448
67064
  }
66449
- log(`[${HOOK_NAME15}] Agent check`, { sessionID, agentName: resolvedInfo?.agent, skipAgents, hasCompactionMessage });
67065
+ log(`[${HOOK_NAME17}] Agent check`, { sessionID, agentName: resolvedInfo?.agent, skipAgents, hasCompactionMessage });
66450
67066
  const resolvedAgentName = resolvedInfo?.agent;
66451
67067
  if (resolvedAgentName && skipAgents.some((s) => getAgentConfigKey(s) === getAgentConfigKey(resolvedAgentName))) {
66452
- log(`[${HOOK_NAME15}] Skipped: agent in skipAgents list`, { sessionID, agent: resolvedAgentName });
67068
+ log(`[${HOOK_NAME17}] Skipped: agent in skipAgents list`, { sessionID, agent: resolvedAgentName });
66453
67069
  return;
66454
67070
  }
66455
67071
  if (hasCompactionMessage && !resolvedInfo?.agent) {
66456
- log(`[${HOOK_NAME15}] Skipped: compaction occurred but no agent info resolved`, { sessionID });
67072
+ log(`[${HOOK_NAME17}] Skipped: compaction occurred but no agent info resolved`, { sessionID });
66457
67073
  return;
66458
67074
  }
66459
67075
  if (isContinuationStopped?.(sessionID)) {
66460
- log(`[${HOOK_NAME15}] Skipped: continuation stopped for session`, { sessionID });
67076
+ log(`[${HOOK_NAME17}] Skipped: continuation stopped for session`, { sessionID });
66461
67077
  return;
66462
67078
  }
66463
- startCountdown({
67079
+ startCountdown2({
66464
67080
  ctx,
66465
67081
  sessionID,
66466
67082
  incompleteCount,
@@ -66474,7 +67090,7 @@ async function handleSessionIdle(args) {
66474
67090
 
66475
67091
  // src/hooks/todo-continuation-enforcer/non-idle-events.ts
66476
67092
  init_logger();
66477
- function handleNonIdleEvent(args) {
67093
+ function handleNonIdleEvent2(args) {
66478
67094
  const { eventType, properties, sessionStateStore } = args;
66479
67095
  if (eventType === "message.updated") {
66480
67096
  const info = properties?.info;
@@ -66486,8 +67102,8 @@ function handleNonIdleEvent(args) {
66486
67102
  const state3 = sessionStateStore.getExistingState(sessionID);
66487
67103
  if (state3?.countdownStartedAt) {
66488
67104
  const elapsed = Date.now() - state3.countdownStartedAt;
66489
- if (elapsed < COUNTDOWN_GRACE_PERIOD_MS) {
66490
- log(`[${HOOK_NAME15}] Ignoring user message in grace period`, { sessionID, elapsed });
67105
+ if (elapsed < COUNTDOWN_GRACE_PERIOD_MS2) {
67106
+ log(`[${HOOK_NAME17}] Ignoring user message in grace period`, { sessionID, elapsed });
66491
67107
  return;
66492
67108
  }
66493
67109
  }
@@ -66531,7 +67147,7 @@ function handleNonIdleEvent(args) {
66531
67147
  const sessionInfo = properties?.info;
66532
67148
  if (sessionInfo?.id) {
66533
67149
  sessionStateStore.cleanup(sessionInfo.id);
66534
- log(`[${HOOK_NAME15}] Session deleted: cleaned up`, { sessionID: sessionInfo.id });
67150
+ log(`[${HOOK_NAME17}] Session deleted: cleaned up`, { sessionID: sessionInfo.id });
66535
67151
  }
66536
67152
  return;
66537
67153
  }
@@ -66543,7 +67159,7 @@ function createTodoContinuationHandler(args) {
66543
67159
  ctx,
66544
67160
  sessionStateStore,
66545
67161
  backgroundManager,
66546
- skipAgents = DEFAULT_SKIP_AGENTS,
67162
+ skipAgents = DEFAULT_SKIP_AGENTS2,
66547
67163
  isContinuationStopped
66548
67164
  } = args;
66549
67165
  return async ({ event }) => {
@@ -66556,17 +67172,17 @@ function createTodoContinuationHandler(args) {
66556
67172
  if (error51?.name === "MessageAbortedError" || error51?.name === "AbortError") {
66557
67173
  const state3 = sessionStateStore.getState(sessionID);
66558
67174
  state3.abortDetectedAt = Date.now();
66559
- log(`[${HOOK_NAME15}] Abort detected via session.error`, { sessionID, errorName: error51.name });
67175
+ log(`[${HOOK_NAME17}] Abort detected via session.error`, { sessionID, errorName: error51.name });
66560
67176
  }
66561
67177
  sessionStateStore.cancelCountdown(sessionID);
66562
- log(`[${HOOK_NAME15}] session.error`, { sessionID });
67178
+ log(`[${HOOK_NAME17}] session.error`, { sessionID });
66563
67179
  return;
66564
67180
  }
66565
67181
  if (event.type === "session.idle") {
66566
67182
  const sessionID = props?.sessionID;
66567
67183
  if (!sessionID)
66568
67184
  return;
66569
- await handleSessionIdle({
67185
+ await handleSessionIdle2({
66570
67186
  ctx,
66571
67187
  sessionID,
66572
67188
  sessionStateStore,
@@ -66576,7 +67192,7 @@ function createTodoContinuationHandler(args) {
66576
67192
  });
66577
67193
  return;
66578
67194
  }
66579
- handleNonIdleEvent({
67195
+ handleNonIdleEvent2({
66580
67196
  eventType: event.type,
66581
67197
  properties: props,
66582
67198
  sessionStateStore
@@ -66585,20 +67201,20 @@ function createTodoContinuationHandler(args) {
66585
67201
  }
66586
67202
 
66587
67203
  // src/hooks/todo-continuation-enforcer/session-state.ts
66588
- var SESSION_STATE_TTL_MS = 10 * 60 * 1000;
66589
- var SESSION_STATE_PRUNE_INTERVAL_MS = 2 * 60 * 1000;
66590
- function createSessionStateStore() {
67204
+ var SESSION_STATE_TTL_MS2 = 10 * 60 * 1000;
67205
+ var SESSION_STATE_PRUNE_INTERVAL_MS2 = 2 * 60 * 1000;
67206
+ function createSessionStateStore2() {
66591
67207
  const sessions = new Map;
66592
67208
  let pruneInterval;
66593
67209
  pruneInterval = setInterval(() => {
66594
67210
  const now = Date.now();
66595
67211
  for (const [sessionID, tracked] of sessions.entries()) {
66596
- if (now - tracked.lastAccessedAt > SESSION_STATE_TTL_MS) {
67212
+ if (now - tracked.lastAccessedAt > SESSION_STATE_TTL_MS2) {
66597
67213
  cancelCountdown(sessionID);
66598
67214
  sessions.delete(sessionID);
66599
67215
  }
66600
67216
  }
66601
- }, SESSION_STATE_PRUNE_INTERVAL_MS);
67217
+ }, SESSION_STATE_PRUNE_INTERVAL_MS2);
66602
67218
  if (typeof pruneInterval === "object" && "unref" in pruneInterval) {
66603
67219
  pruneInterval.unref();
66604
67220
  }
@@ -66666,21 +67282,21 @@ function createSessionStateStore() {
66666
67282
  function createTodoContinuationEnforcer(ctx, options = {}) {
66667
67283
  const {
66668
67284
  backgroundManager,
66669
- skipAgents = DEFAULT_SKIP_AGENTS,
67285
+ skipAgents = DEFAULT_SKIP_AGENTS2,
66670
67286
  isContinuationStopped
66671
67287
  } = options;
66672
- const sessionStateStore = createSessionStateStore();
67288
+ const sessionStateStore = createSessionStateStore2();
66673
67289
  const markRecovering = (sessionID) => {
66674
67290
  const state3 = sessionStateStore.getState(sessionID);
66675
67291
  state3.isRecovering = true;
66676
67292
  sessionStateStore.cancelCountdown(sessionID);
66677
- log(`[${HOOK_NAME15}] Session marked as recovering`, { sessionID });
67293
+ log(`[${HOOK_NAME17}] Session marked as recovering`, { sessionID });
66678
67294
  };
66679
67295
  const markRecoveryComplete = (sessionID) => {
66680
67296
  const state3 = sessionStateStore.getExistingState(sessionID);
66681
67297
  if (state3) {
66682
67298
  state3.isRecovering = false;
66683
- log(`[${HOOK_NAME15}] Session recovery complete`, { sessionID });
67299
+ log(`[${HOOK_NAME17}] Session recovery complete`, { sessionID });
66684
67300
  }
66685
67301
  };
66686
67302
  const handler = createTodoContinuationHandler({
@@ -66692,7 +67308,7 @@ function createTodoContinuationEnforcer(ctx, options = {}) {
66692
67308
  });
66693
67309
  const cancelAllCountdowns = () => {
66694
67310
  sessionStateStore.cancelAllCountdowns();
66695
- log(`[${HOOK_NAME15}] All countdowns cancelled`);
67311
+ log(`[${HOOK_NAME17}] All countdowns cancelled`);
66696
67312
  };
66697
67313
  return {
66698
67314
  handler,
@@ -66701,38 +67317,6 @@ function createTodoContinuationEnforcer(ctx, options = {}) {
66701
67317
  cancelAllCountdowns
66702
67318
  };
66703
67319
  }
66704
- // src/hooks/todo-description-override/description.ts
66705
- var TODOWRITE_DESCRIPTION = `Manage your todo list with ATOMIC, single-responsibility items.
66706
-
66707
- RULES:
66708
- - One todo = one specific, verifiable action (not "Update everything" or "Fix issues")
66709
- - Each todo must be completable in isolation \u2014 no vague catch-alls
66710
- - Todos must be nameable with a single precise phrase
66711
- - Mark in_progress BEFORE starting work, completed IMMEDIATELY after finishing
66712
- - Only ONE todo in_progress at a time
66713
-
66714
- ANTI-PATTERNS (will be rejected by reviewers):
66715
- - "Update X and Y and Z" \u2192 SPLIT into 3 separate todos
66716
- - "Fix issues" \u2192 SPECIFY which file, function, or behavior
66717
- - "Refactor" \u2192 SPECIFY what is being refactored and to what end
66718
- - "Improve" \u2192 SPECIFY the measurable outcome
66719
-
66720
- STATUS TRANSITIONS:
66721
- pending \u2192 in_progress \u2192 completed
66722
- pending \u2192 cancelled (if no longer needed)
66723
-
66724
- Never batch-complete todos. Mark each one complete the moment it is done.`;
66725
-
66726
- // src/hooks/todo-description-override/hook.ts
66727
- function createTodoDescriptionOverrideHook() {
66728
- return {
66729
- "tool.definition": async (input, output) => {
66730
- if (input.toolID === "todowrite") {
66731
- output.description = TODOWRITE_DESCRIPTION;
66732
- }
66733
- }
66734
- };
66735
- }
66736
67320
  // src/hooks/tool-output-truncator.ts
66737
67321
  var DEFAULT_MAX_TOKENS = 50000;
66738
67322
  var WEBFETCH_MAX_TOKENS = 1e4;
@@ -66948,8 +67532,8 @@ This is a reminder only. No automatic action was taken.`;
66948
67532
  }
66949
67533
  // src/hooks/unstable-agent-babysitter/unstable-agent-babysitter-hook.ts
66950
67534
  init_logger();
66951
- var HOOK_NAME16 = "unstable-agent-babysitter";
66952
- var DEFAULT_TIMEOUT_MS2 = 120000;
67535
+ var HOOK_NAME18 = "unstable-agent-babysitter";
67536
+ var DEFAULT_TIMEOUT_MS = 120000;
66953
67537
  var COOLDOWN_MS = 5 * 60 * 1000;
66954
67538
  async function resolveMainSessionTarget(ctx, sessionID) {
66955
67539
  let agent = getSessionAgent(sessionID);
@@ -66968,7 +67552,7 @@ async function resolveMainSessionTarget(ctx, sessionID) {
66968
67552
  }
66969
67553
  }
66970
67554
  } catch (error51) {
66971
- log(`[${HOOK_NAME16}] Failed to resolve main session agent`, { sessionID, error: String(error51) });
67555
+ log(`[${HOOK_NAME18}] Failed to resolve main session agent`, { sessionID, error: String(error51) });
66972
67556
  }
66973
67557
  return { agent, model };
66974
67558
  }
@@ -67001,7 +67585,7 @@ async function getThinkingSummary(ctx, sessionID) {
67001
67585
  return combined;
67002
67586
  return `${combined.slice(0, THINKING_SUMMARY_MAX_CHARS)}...`;
67003
67587
  } catch (error51) {
67004
- log(`[${HOOK_NAME16}] Failed to fetch thinking summary`, { sessionID, error: String(error51) });
67588
+ log(`[${HOOK_NAME18}] Failed to fetch thinking summary`, { sessionID, error: String(error51) });
67005
67589
  return null;
67006
67590
  }
67007
67591
  }
@@ -67020,7 +67604,7 @@ function createUnstableAgentBabysitterHook(ctx, options) {
67020
67604
  const tasks = options.backgroundManager.getTasksByParentSession(mainSessionID);
67021
67605
  if (tasks.length === 0)
67022
67606
  return;
67023
- const timeoutMs = options.config?.timeout_ms ?? DEFAULT_TIMEOUT_MS2;
67607
+ const timeoutMs = options.config?.timeout_ms ?? DEFAULT_TIMEOUT_MS;
67024
67608
  const now = Date.now();
67025
67609
  for (const task of tasks) {
67026
67610
  if (task.status !== "running")
@@ -67050,9 +67634,9 @@ function createUnstableAgentBabysitterHook(ctx, options) {
67050
67634
  query: { directory: ctx.directory }
67051
67635
  });
67052
67636
  reminderCooldowns.set(task.id, now);
67053
- log(`[${HOOK_NAME16}] Reminder injected`, { taskId: task.id, sessionID: mainSessionID });
67637
+ log(`[${HOOK_NAME18}] Reminder injected`, { taskId: task.id, sessionID: mainSessionID });
67054
67638
  } catch (error51) {
67055
- log(`[${HOOK_NAME16}] Reminder injection failed`, { taskId: task.id, error: String(error51) });
67639
+ log(`[${HOOK_NAME18}] Reminder injection failed`, { taskId: task.id, error: String(error51) });
67056
67640
  }
67057
67641
  }
67058
67642
  };
@@ -67203,7 +67787,7 @@ function createWebFetchRedirectGuardHook(_ctx) {
67203
67787
  }
67204
67788
  // src/hooks/write-existing-file-guard/hook.ts
67205
67789
  import { existsSync as existsSync46 } from "fs";
67206
- import { isAbsolute as isAbsolute4, join as join54, normalize, resolve as resolve7, sep } from "path";
67790
+ import { isAbsolute as isAbsolute4, join as join53, normalize, resolve as resolve7, sep } from "path";
67207
67791
  function createWriteExistingFileGuardHook(ctx) {
67208
67792
  return {
67209
67793
  "tool.execute.before": async (input, output) => {
@@ -67218,7 +67802,7 @@ function createWriteExistingFileGuardHook(ctx) {
67218
67802
  }
67219
67803
  const resolvedPath = normalize(isAbsolute4(filePath) ? filePath : resolve7(ctx.directory, filePath));
67220
67804
  if (existsSync46(resolvedPath)) {
67221
- const matrixxRoot = join54(ctx.directory, ".matrixx") + sep;
67805
+ const matrixxRoot = join53(ctx.directory, ".matrixx") + sep;
67222
67806
  const isMatrixMarkdown = resolvedPath.startsWith(matrixxRoot) && resolvedPath.endsWith(".md");
67223
67807
  if (isMatrixMarkdown) {
67224
67808
  log("[write-existing-file-guard] Allowing .matrixx/*.md overwrite", {
@@ -67281,7 +67865,12 @@ function createContinuationHooks(args) {
67281
67865
  const stopContinuationGuard = isHookEnabled("stop-continuation-guard") ? safeHook("stop-continuation-guard", () => createStopContinuationGuardHook(ctx, { backgroundManager })) : null;
67282
67866
  const compactionContextInjector = isHookEnabled("compaction-context-injector") ? safeHook("compaction-context-injector", () => createCompactionContextInjector(backgroundManager)) : null;
67283
67867
  const compactionTodoPreserver = isHookEnabled("compaction-todo-preserver") ? safeHook("compaction-todo-preserver", () => createCompactionTodoPreserverHook(ctx)) : null;
67284
- const todoContinuationEnforcer = isHookEnabled("todo-continuation-enforcer") ? safeHook("todo-continuation-enforcer", () => createTodoContinuationEnforcer(ctx, {
67868
+ const isTaskSystem = isTaskSystemEnabled(pluginConfig);
67869
+ const todoContinuationEnforcer = !isTaskSystem && isHookEnabled("todo-continuation-enforcer") ? safeHook("todo-continuation-enforcer", () => createTodoContinuationEnforcer(ctx, {
67870
+ backgroundManager,
67871
+ isContinuationStopped: stopContinuationGuard?.isStopped
67872
+ })) : null;
67873
+ const taskContinuationEnforcer = isTaskSystem && isHookEnabled("task-continuation-enforcer") ? safeHook("task-continuation-enforcer", () => createTaskContinuationEnforcer(ctx, {
67285
67874
  backgroundManager,
67286
67875
  isContinuationStopped: stopContinuationGuard?.isStopped
67287
67876
  })) : null;
@@ -67293,6 +67882,10 @@ function createContinuationHooks(args) {
67293
67882
  onAbortCallbacks.push(todoContinuationEnforcer.markRecovering);
67294
67883
  onRecoveryCompleteCallbacks.push(todoContinuationEnforcer.markRecoveryComplete);
67295
67884
  }
67885
+ if (taskContinuationEnforcer) {
67886
+ onAbortCallbacks.push(taskContinuationEnforcer.markRecovering);
67887
+ onRecoveryCompleteCallbacks.push(taskContinuationEnforcer.markRecoveryComplete);
67888
+ }
67296
67889
  if (onAbortCallbacks.length > 0) {
67297
67890
  sessionRecovery.setOnAbortCallback((sessionID) => {
67298
67891
  for (const callback of onAbortCallbacks)
@@ -67321,6 +67914,7 @@ function createContinuationHooks(args) {
67321
67914
  compactionContextInjector,
67322
67915
  compactionTodoPreserver,
67323
67916
  todoContinuationEnforcer,
67917
+ taskContinuationEnforcer,
67324
67918
  unstableAgentBabysitter,
67325
67919
  backgroundNotificationHook,
67326
67920
  architectHook,
@@ -67638,7 +68232,7 @@ function getLanguageId(ext) {
67638
68232
  }
67639
68233
  // src/tools/lsp/server-installation.ts
67640
68234
  import { existsSync as existsSync47 } from "fs";
67641
- import { join as join55 } from "path";
68235
+ import { join as join54 } from "path";
67642
68236
  function isServerInstalled(command) {
67643
68237
  if (command.length === 0)
67644
68238
  return false;
@@ -67666,23 +68260,23 @@ function isServerInstalled(command) {
67666
68260
  const paths = pathEnv.split(pathSeparator);
67667
68261
  for (const p of paths) {
67668
68262
  for (const suffix of exts) {
67669
- if (existsSync47(join55(p, cmd + suffix))) {
68263
+ if (existsSync47(join54(p, cmd + suffix))) {
67670
68264
  return true;
67671
68265
  }
67672
68266
  }
67673
68267
  }
67674
68268
  const cwd = process.cwd();
67675
68269
  const configDir = getOpenCodeConfigDir({ binary: "opencode" });
67676
- const dataDir = join55(getDataDir(), "opencode");
68270
+ const dataDir = join54(getDataDir(), "opencode");
67677
68271
  const additionalBases = [
67678
- join55(cwd, "node_modules", ".bin"),
67679
- join55(configDir, "bin"),
67680
- join55(configDir, "node_modules", ".bin"),
67681
- join55(dataDir, "bin")
68272
+ join54(cwd, "node_modules", ".bin"),
68273
+ join54(configDir, "bin"),
68274
+ join54(configDir, "node_modules", ".bin"),
68275
+ join54(dataDir, "bin")
67682
68276
  ];
67683
68277
  for (const base of additionalBases) {
67684
68278
  for (const suffix of exts) {
67685
- if (existsSync47(join55(base, cmd + suffix))) {
68279
+ if (existsSync47(join54(base, cmd + suffix))) {
67686
68280
  return true;
67687
68281
  }
67688
68282
  }
@@ -67694,7 +68288,7 @@ function isServerInstalled(command) {
67694
68288
  }
67695
68289
  // src/tools/lsp/server-config-loader.ts
67696
68290
  import { existsSync as existsSync48, readFileSync as readFileSync34 } from "fs";
67697
- import { join as join56 } from "path";
68291
+ import { join as join55 } from "path";
67698
68292
  function loadJsonFile(path12) {
67699
68293
  if (!existsSync48(path12))
67700
68294
  return null;
@@ -67708,9 +68302,9 @@ function getConfigPaths3() {
67708
68302
  const cwd = process.cwd();
67709
68303
  const configDir = getOpenCodeConfigDir({ binary: "opencode" });
67710
68304
  return {
67711
- project: detectConfigFile(join56(cwd, ".opencode", "matrixx")).path,
67712
- user: detectConfigFile(join56(configDir, "matrixx")).path,
67713
- opencode: detectConfigFile(join56(configDir, "opencode")).path
68305
+ project: detectConfigFile(join55(cwd, ".opencode", "matrixx")).path,
68306
+ user: detectConfigFile(join55(configDir, "matrixx")).path,
68307
+ opencode: detectConfigFile(join55(configDir, "opencode")).path
67714
68308
  };
67715
68309
  }
67716
68310
  function loadAllConfigs() {
@@ -68559,7 +69153,7 @@ class LSPServerManager {
68559
69153
  var lspManager = LSPServerManager.getInstance();
68560
69154
  // src/tools/lsp/lsp-client-wrapper.ts
68561
69155
  import { existsSync as existsSync50, statSync as statSync10 } from "fs";
68562
- import { dirname as dirname16, extname as extname3, join as join57, resolve as resolve9 } from "path";
69156
+ import { dirname as dirname16, extname as extname3, join as join56, resolve as resolve9 } from "path";
68563
69157
  import { fileURLToPath as fileURLToPath3 } from "url";
68564
69158
  function findWorkspaceRoot(filePath) {
68565
69159
  let dir = resolve9(filePath);
@@ -68575,7 +69169,7 @@ function findWorkspaceRoot(filePath) {
68575
69169
  let prevDir = "";
68576
69170
  while (dir !== prevDir) {
68577
69171
  for (const marker of markers) {
68578
- if (existsSync50(join57(dir, marker))) {
69172
+ if (existsSync50(join56(dir, marker))) {
68579
69173
  return dir;
68580
69174
  }
68581
69175
  }
@@ -81420,20 +82014,20 @@ var CLI_LANGUAGES = [
81420
82014
  "tsx",
81421
82015
  "yaml"
81422
82016
  ];
81423
- var DEFAULT_TIMEOUT_MS3 = 300000;
82017
+ var DEFAULT_TIMEOUT_MS2 = 300000;
81424
82018
  var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
81425
82019
  var DEFAULT_MAX_MATCHES = 500;
81426
82020
 
81427
82021
  // src/tools/ast-grep/sg-cli-path.ts
81428
82022
  import { existsSync as existsSync52, statSync as statSync11 } from "fs";
81429
82023
  import { createRequire as createRequire4 } from "module";
81430
- import { dirname as dirname17, join as join59 } from "path";
82024
+ import { dirname as dirname17, join as join58 } from "path";
81431
82025
 
81432
82026
  // src/tools/ast-grep/downloader.ts
81433
82027
  import { existsSync as existsSync51 } from "fs";
81434
82028
  import { createRequire as createRequire3 } from "module";
81435
- import { homedir as homedir10 } from "os";
81436
- import { join as join58 } from "path";
82029
+ import { homedir as homedir9 } from "os";
82030
+ import { join as join57 } from "path";
81437
82031
  init_logger();
81438
82032
  var REPO2 = "ast-grep/ast-grep";
81439
82033
  var DEFAULT_VERSION = "0.40.0";
@@ -81458,12 +82052,12 @@ var PLATFORM_MAP2 = {
81458
82052
  function getCacheDir3() {
81459
82053
  if (process.platform === "win32") {
81460
82054
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
81461
- const base2 = localAppData || join58(homedir10(), "AppData", "Local");
81462
- return join58(base2, "matrixx", "bin");
82055
+ const base2 = localAppData || join57(homedir9(), "AppData", "Local");
82056
+ return join57(base2, "matrixx", "bin");
81463
82057
  }
81464
82058
  const xdgCache = process.env.XDG_CACHE_HOME;
81465
- const base = xdgCache || join58(homedir10(), ".cache");
81466
- return join58(base, "matrixx", "bin");
82059
+ const base = xdgCache || join57(homedir9(), ".cache");
82060
+ return join57(base, "matrixx", "bin");
81467
82061
  }
81468
82062
  function getBinaryName3() {
81469
82063
  return process.platform === "win32" ? "sg.exe" : "sg";
@@ -81480,7 +82074,7 @@ async function downloadAstGrep(version3 = DEFAULT_VERSION) {
81480
82074
  }
81481
82075
  const cacheDir = getCacheDir3();
81482
82076
  const binaryName = getBinaryName3();
81483
- const binaryPath = join58(cacheDir, binaryName);
82077
+ const binaryPath = join57(cacheDir, binaryName);
81484
82078
  if (existsSync51(binaryPath)) {
81485
82079
  return binaryPath;
81486
82080
  }
@@ -81489,7 +82083,7 @@ async function downloadAstGrep(version3 = DEFAULT_VERSION) {
81489
82083
  const downloadUrl = `https://github.com/${REPO2}/releases/download/${version3}/${assetName}`;
81490
82084
  log(`[matrixx] Downloading ast-grep binary...`);
81491
82085
  try {
81492
- const archivePath = join58(cacheDir, assetName);
82086
+ const archivePath = join57(cacheDir, assetName);
81493
82087
  ensureCacheDir(cacheDir);
81494
82088
  await downloadArchive(downloadUrl, archivePath);
81495
82089
  await extractZipArchive(archivePath, cacheDir);
@@ -81543,7 +82137,7 @@ function findSgCliPathSync() {
81543
82137
  const require2 = createRequire4(import.meta.url);
81544
82138
  const cliPackageJsonPath = require2.resolve("@ast-grep/cli/package.json");
81545
82139
  const cliDirectory = dirname17(cliPackageJsonPath);
81546
- const sgPath = join59(cliDirectory, binaryName);
82140
+ const sgPath = join58(cliDirectory, binaryName);
81547
82141
  if (existsSync52(sgPath) && isValidBinary(sgPath)) {
81548
82142
  return sgPath;
81549
82143
  }
@@ -81555,7 +82149,7 @@ function findSgCliPathSync() {
81555
82149
  const packageJsonPath = require2.resolve(`${platformPackage}/package.json`);
81556
82150
  const packageDirectory = dirname17(packageJsonPath);
81557
82151
  const astGrepBinaryName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
81558
- const binaryPath = join59(packageDirectory, astGrepBinaryName);
82152
+ const binaryPath = join58(packageDirectory, astGrepBinaryName);
81559
82153
  if (existsSync52(binaryPath) && isValidBinary(binaryPath)) {
81560
82154
  return binaryPath;
81561
82155
  }
@@ -81715,7 +82309,7 @@ async function runSg(options) {
81715
82309
  };
81716
82310
  }
81717
82311
  }
81718
- const timeout = DEFAULT_TIMEOUT_MS3;
82312
+ const timeout = DEFAULT_TIMEOUT_MS2;
81719
82313
  const proc = spawn9([cliPath, ...args], {
81720
82314
  stdout: "pipe",
81721
82315
  stderr: "pipe"
@@ -83818,8 +84412,6 @@ function createBddPipelineTool(opts) {
83818
84412
  });
83819
84413
  }
83820
84414
  function makeDeps(args) {
83821
- const featureName = args.featurePath.split("/").pop()?.replace(/\.feature$/i, "") ?? "feature";
83822
- const _featureDir = resolvePath2(args.outDir, featureName);
83823
84415
  return {
83824
84416
  parseFeature: async (path12) => {
83825
84417
  const raw = await args.parseTool.execute({ filePath: path12 }, dummyCtx(args));
@@ -83988,17 +84580,17 @@ var {spawn: spawn10 } = globalThis.Bun;
83988
84580
  // src/tools/grep/constants.ts
83989
84581
  import { spawnSync as spawnSync2 } from "child_process";
83990
84582
  import { existsSync as existsSync59 } from "fs";
83991
- import { dirname as dirname18, join as join61 } from "path";
84583
+ import { dirname as dirname18, join as join60 } from "path";
83992
84584
 
83993
84585
  // src/tools/grep/downloader.ts
83994
- import { existsSync as existsSync58, readdirSync as readdirSync17 } from "fs";
83995
- import { join as join60 } from "path";
84586
+ import { existsSync as existsSync58, readdirSync as readdirSync18 } from "fs";
84587
+ import { join as join59 } from "path";
83996
84588
  function findFileRecursive(dir, filename) {
83997
84589
  try {
83998
- const entries = readdirSync17(dir, { withFileTypes: true, recursive: true });
84590
+ const entries = readdirSync18(dir, { withFileTypes: true, recursive: true });
83999
84591
  for (const entry of entries) {
84000
84592
  if (entry.isFile() && entry.name === filename) {
84001
- return join60(entry.parentPath ?? dir, entry.name);
84593
+ return join59(entry.parentPath ?? dir, entry.name);
84002
84594
  }
84003
84595
  }
84004
84596
  } catch {
@@ -84019,11 +84611,11 @@ function getPlatformKey() {
84019
84611
  }
84020
84612
  function getInstallDir() {
84021
84613
  const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
84022
- return join60(homeDir, ".cache", "matrixx", "bin");
84614
+ return join59(homeDir, ".cache", "matrixx", "bin");
84023
84615
  }
84024
84616
  function getRgPath() {
84025
84617
  const isWindows = process.platform === "win32";
84026
- return join60(getInstallDir(), isWindows ? "rg.exe" : "rg");
84618
+ return join59(getInstallDir(), isWindows ? "rg.exe" : "rg");
84027
84619
  }
84028
84620
  async function extractTarGz2(archivePath, destDir) {
84029
84621
  const platformKey = getPlatformKey();
@@ -84040,7 +84632,7 @@ async function extractZip2(archivePath, destDir) {
84040
84632
  const binaryName = process.platform === "win32" ? "rg.exe" : "rg";
84041
84633
  const foundPath = findFileRecursive(destDir, binaryName);
84042
84634
  if (foundPath) {
84043
- const destPath = join60(destDir, binaryName);
84635
+ const destPath = join59(destDir, binaryName);
84044
84636
  if (foundPath !== destPath) {
84045
84637
  const { renameSync: renameSync4 } = await import("fs");
84046
84638
  renameSync4(foundPath, destPath);
@@ -84061,7 +84653,7 @@ async function downloadAndInstallRipgrep() {
84061
84653
  ensureCacheDir(installDir);
84062
84654
  const filename = `ripgrep-${RG_VERSION}-${config4.platform}.${config4.extension}`;
84063
84655
  const url3 = `https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${filename}`;
84064
- const archivePath = join60(installDir, filename);
84656
+ const archivePath = join59(installDir, filename);
84065
84657
  try {
84066
84658
  await downloadArchive(url3, archivePath);
84067
84659
  if (config4.extension === "tar.gz") {
@@ -84106,11 +84698,11 @@ function getOpenCodeBundledRg() {
84106
84698
  const isWindows = process.platform === "win32";
84107
84699
  const rgName = isWindows ? "rg.exe" : "rg";
84108
84700
  const candidates = [
84109
- join61(getDataDir(), "opencode", "bin", rgName),
84110
- join61(execDir, rgName),
84111
- join61(execDir, "bin", rgName),
84112
- join61(execDir, "..", "bin", rgName),
84113
- join61(execDir, "..", "libexec", rgName)
84701
+ join60(getDataDir(), "opencode", "bin", rgName),
84702
+ join60(execDir, rgName),
84703
+ join60(execDir, "bin", rgName),
84704
+ join60(execDir, "..", "bin", rgName),
84705
+ join60(execDir, "..", "libexec", rgName)
84114
84706
  ];
84115
84707
  for (const candidate of candidates) {
84116
84708
  if (existsSync59(candidate)) {
@@ -84166,7 +84758,7 @@ var DEFAULT_MAX_DEPTH = 20;
84166
84758
  var DEFAULT_MAX_FILESIZE = "10M";
84167
84759
  var DEFAULT_MAX_COUNT = 500;
84168
84760
  var DEFAULT_MAX_COLUMNS = 1000;
84169
- var DEFAULT_TIMEOUT_MS4 = 300000;
84761
+ var DEFAULT_TIMEOUT_MS3 = 300000;
84170
84762
  var DEFAULT_MAX_OUTPUT_BYTES2 = 10 * 1024 * 1024;
84171
84763
  var RG_SAFETY_FLAGS = [
84172
84764
  "--no-follow",
@@ -84178,7 +84770,7 @@ var RG_SAFETY_FLAGS = [
84178
84770
  var GREP_SAFETY_FLAGS = ["-n", "-H", "--color=never"];
84179
84771
 
84180
84772
  // src/tools/glob/constants.ts
84181
- var DEFAULT_TIMEOUT_MS5 = 60000;
84773
+ var DEFAULT_TIMEOUT_MS4 = 60000;
84182
84774
  var DEFAULT_LIMIT = 100;
84183
84775
  var DEFAULT_MAX_DEPTH2 = 20;
84184
84776
  var DEFAULT_MAX_OUTPUT_BYTES3 = 10 * 1024 * 1024;
@@ -84241,7 +84833,7 @@ async function getFileMtime(filePath) {
84241
84833
  }
84242
84834
  async function runRgFiles(options, resolvedCli) {
84243
84835
  const cli = resolvedCli ?? resolveGrepCli();
84244
- const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS5, DEFAULT_TIMEOUT_MS5);
84836
+ const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS4, DEFAULT_TIMEOUT_MS4);
84245
84837
  const limit = Math.min(options.limit ?? DEFAULT_LIMIT, DEFAULT_LIMIT);
84246
84838
  const isRg = cli.backend === "rg";
84247
84839
  const isWindows = process.platform === "win32";
@@ -84463,7 +85055,7 @@ function parseOutput(output) {
84463
85055
  async function runRg(options) {
84464
85056
  const cli = resolveGrepCli();
84465
85057
  const args = buildArgs(options, cli.backend);
84466
- const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS4, DEFAULT_TIMEOUT_MS4);
85058
+ const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS3, DEFAULT_TIMEOUT_MS3);
84467
85059
  if (cli.backend === "rg") {
84468
85060
  args.push("--", options.pattern);
84469
85061
  } else {
@@ -84618,12 +85210,12 @@ var HandoffSchema = exports_external.object({
84618
85210
  });
84619
85211
  // src/features/handoff/storage.ts
84620
85212
  import { existsSync as existsSync60, mkdirSync as mkdirSync17, readFileSync as readFileSync39, renameSync as renameSync4, writeFileSync as writeFileSync23 } from "fs";
84621
- import { join as join62 } from "path";
85213
+ import { join as join61 } from "path";
84622
85214
  function getHandoffFilePath(directory) {
84623
- return join62(directory, HANDOFF_FILE_PATH);
85215
+ return join61(directory, HANDOFF_FILE_PATH);
84624
85216
  }
84625
85217
  function getHandoffConsumedFilePath(directory) {
84626
- return join62(directory, HANDOFF_CONSUMED_FILE_PATH);
85218
+ return join61(directory, HANDOFF_CONSUMED_FILE_PATH);
84627
85219
  }
84628
85220
  function handoffFileExists(directory) {
84629
85221
  try {
@@ -84661,7 +85253,7 @@ function writeHandoffFile(directory, content) {
84661
85253
  }
84662
85254
  function ensureHandoffDir(directory) {
84663
85255
  try {
84664
- mkdirSync17(join62(directory, ".matrixx"), { recursive: true });
85256
+ mkdirSync17(join61(directory, ".matrixx"), { recursive: true });
84665
85257
  return true;
84666
85258
  } catch {
84667
85259
  return false;
@@ -84816,20 +85408,20 @@ ${body}
84816
85408
  }
84817
85409
 
84818
85410
  // src/tools/handoff/list-handler.ts
84819
- import { existsSync as existsSync61, readdirSync as readdirSync18, statSync as statSync13 } from "fs";
84820
- import { join as join63 } from "path";
85411
+ import { existsSync as existsSync61, readdirSync as readdirSync19, statSync as statSync13 } from "fs";
85412
+ import { join as join62 } from "path";
84821
85413
  var MATRIX_DIR = ".matrixx";
84822
85414
  function isHandoffFile(name) {
84823
85415
  return name === "handoff.md" || name.startsWith("handoff.consumed");
84824
85416
  }
84825
85417
  function handleList(directory) {
84826
- const matrixxDir = join63(directory, MATRIX_DIR);
85418
+ const matrixxDir = join62(directory, MATRIX_DIR);
84827
85419
  if (!existsSync61(matrixxDir)) {
84828
85420
  return "No handoffs found in .matrixx/";
84829
85421
  }
84830
85422
  let entries;
84831
85423
  try {
84832
- entries = readdirSync18(matrixxDir);
85424
+ entries = readdirSync19(matrixxDir);
84833
85425
  } catch {
84834
85426
  return "No handoffs found in .matrixx/";
84835
85427
  }
@@ -84841,7 +85433,7 @@ function handleList(directory) {
84841
85433
  lines.push(`Handoff files in ${matrixxDir}:`);
84842
85434
  lines.push("");
84843
85435
  for (const name of handoffs) {
84844
- const fullPath = join63(matrixxDir, name);
85436
+ const fullPath = join62(matrixxDir, name);
84845
85437
  let size = "?";
84846
85438
  let mtime = "?";
84847
85439
  try {
@@ -84938,7 +85530,7 @@ function createHandoffTools(ctx) {
84938
85530
  init_logger();
84939
85531
 
84940
85532
  // src/tools/interactive-bash/constants.ts
84941
- var DEFAULT_TIMEOUT_MS6 = 60000;
85533
+ var DEFAULT_TIMEOUT_MS5 = 60000;
84942
85534
  var BLOCKED_TMUX_SUBCOMMANDS = [
84943
85535
  "capture-pane",
84944
85536
  "capturep",
@@ -85035,7 +85627,7 @@ The Bash tool can execute these commands directly. Do NOT retry with interactive
85035
85627
  });
85036
85628
  const timeoutPromise = new Promise((_, reject) => {
85037
85629
  const id = setTimeout(() => {
85038
- const timeoutError = new Error(`Timeout after ${DEFAULT_TIMEOUT_MS6}ms`);
85630
+ const timeoutError = new Error(`Timeout after ${DEFAULT_TIMEOUT_MS5}ms`);
85039
85631
  try {
85040
85632
  proc.kill();
85041
85633
  proc.exited.catch((err) => {
@@ -85043,7 +85635,7 @@ The Bash tool can execute these commands directly. Do NOT retry with interactive
85043
85635
  });
85044
85636
  } catch {}
85045
85637
  reject(timeoutError);
85046
- }, DEFAULT_TIMEOUT_MS6);
85638
+ }, DEFAULT_TIMEOUT_MS5);
85047
85639
  proc.exited.then(() => clearTimeout(id)).catch(() => clearTimeout(id));
85048
85640
  });
85049
85641
  const [stdout, stderr, exitCode] = await Promise.race([
@@ -85067,7 +85659,7 @@ The Bash tool can execute these commands directly. Do NOT retry with interactive
85067
85659
  // src/tools/pdf-extract-figures/tools.ts
85068
85660
  import { existsSync as existsSync62, mkdtempSync, writeFileSync as writeFileSync24 } from "fs";
85069
85661
  import { tmpdir as tmpdir5 } from "os";
85070
- import { join as join64 } from "path";
85662
+ import { join as join63 } from "path";
85071
85663
  var PYTHON_SCRIPT = `#!/usr/bin/env python3
85072
85664
  import sys
85073
85665
  import json
@@ -85218,11 +85810,11 @@ async function runExtraction(args) {
85218
85810
  }
85219
85811
  let tmpDir;
85220
85812
  try {
85221
- tmpDir = mkdtempSync(join64(tmpdir5(), "matrixx-pdf-extract-"));
85813
+ tmpDir = mkdtempSync(join63(tmpdir5(), "matrixx-pdf-extract-"));
85222
85814
  } catch {
85223
85815
  return "Error: Could not create temporary directory.";
85224
85816
  }
85225
- const scriptPath = join64(tmpDir, "extract_pdf_figures.py");
85817
+ const scriptPath = join63(tmpDir, "extract_pdf_figures.py");
85226
85818
  writeFileSync24(scriptPath, PYTHON_SCRIPT);
85227
85819
  const payload = buildArgsPayload(args);
85228
85820
  log("[pdf_extract_figures] Running extraction", { file_path: args.file_path, payload });
@@ -85270,9 +85862,9 @@ function createPdfExtractFiguresTool() {
85270
85862
  return { pdf_extract_figures };
85271
85863
  }
85272
85864
  // src/tools/session-manager/constants.ts
85273
- import { join as join65 } from "path";
85274
- var TODO_DIR = join65(getOpenCodeConfigDir({ binary: "opencode" }), "todos");
85275
- var TRANSCRIPT_DIR2 = join65(getOpenCodeConfigDir({ binary: "opencode" }), "transcripts");
85865
+ import { join as join64 } from "path";
85866
+ var TODO_DIR = join64(getOpenCodeConfigDir({ binary: "opencode" }), "todos");
85867
+ var TRANSCRIPT_DIR2 = join64(getOpenCodeConfigDir({ binary: "opencode" }), "transcripts");
85276
85868
  var SESSION_LIST_DESCRIPTION = `List all OpenCode sessions with optional filtering.
85277
85869
 
85278
85870
  Returns a list of available session IDs with metadata including message count, date range, and agents used.
@@ -85346,7 +85938,7 @@ Has Transcript: Yes (234 entries)`;
85346
85938
  // src/tools/session-manager/storage.ts
85347
85939
  import { existsSync as existsSync63 } from "fs";
85348
85940
  import { readdir, readFile as readFile4 } from "fs/promises";
85349
- import { join as join66 } from "path";
85941
+ import { join as join65 } from "path";
85350
85942
  var sdkClient = null;
85351
85943
  function setStorageClient(client2) {
85352
85944
  sdkClient = client2;
@@ -85373,13 +85965,13 @@ async function getMainSessions(options) {
85373
85965
  for (const projectDir of projectDirs) {
85374
85966
  if (!projectDir.isDirectory())
85375
85967
  continue;
85376
- const projectPath = join66(SESSION_STORAGE, projectDir.name);
85968
+ const projectPath = join65(SESSION_STORAGE, projectDir.name);
85377
85969
  const sessionFiles = await readdir(projectPath);
85378
85970
  for (const file3 of sessionFiles) {
85379
85971
  if (!file3.endsWith(".json"))
85380
85972
  continue;
85381
85973
  try {
85382
- const content = await readFile4(join66(projectPath, file3), "utf-8");
85974
+ const content = await readFile4(join65(projectPath, file3), "utf-8");
85383
85975
  const meta3 = JSON.parse(content);
85384
85976
  if (meta3.parentID)
85385
85977
  continue;
@@ -85412,7 +86004,7 @@ async function getAllSessions() {
85412
86004
  const entries = await readdir(dir, { withFileTypes: true });
85413
86005
  for (const entry of entries) {
85414
86006
  if (entry.isDirectory()) {
85415
- const sessionPath = join66(dir, entry.name);
86007
+ const sessionPath = join65(dir, entry.name);
85416
86008
  const files = await readdir(sessionPath);
85417
86009
  if (files.some((f) => f.endsWith(".json"))) {
85418
86010
  sessions.push(entry.name);
@@ -85482,7 +86074,7 @@ async function readSessionMessages(sessionID) {
85482
86074
  if (!file3.endsWith(".json"))
85483
86075
  continue;
85484
86076
  try {
85485
- const content = await readFile4(join66(messageDir, file3), "utf-8");
86077
+ const content = await readFile4(join65(messageDir, file3), "utf-8");
85486
86078
  const meta3 = JSON.parse(content);
85487
86079
  const parts = await readParts2(meta3.id);
85488
86080
  messages2.push({
@@ -85506,7 +86098,7 @@ async function readSessionMessages(sessionID) {
85506
86098
  });
85507
86099
  }
85508
86100
  async function readParts2(messageID) {
85509
- const partDir = join66(PART_STORAGE, messageID);
86101
+ const partDir = join65(PART_STORAGE, messageID);
85510
86102
  if (!existsSync63(partDir))
85511
86103
  return [];
85512
86104
  const parts = [];
@@ -85516,7 +86108,7 @@ async function readParts2(messageID) {
85516
86108
  if (!file3.endsWith(".json"))
85517
86109
  continue;
85518
86110
  try {
85519
- const content = await readFile4(join66(partDir, file3), "utf-8");
86111
+ const content = await readFile4(join65(partDir, file3), "utf-8");
85520
86112
  parts.push(JSON.parse(content));
85521
86113
  } catch {}
85522
86114
  }
@@ -85547,7 +86139,7 @@ async function readSessionTodos(sessionID) {
85547
86139
  const todoFiles = allFiles.filter((f) => f.includes(sessionID) && f.endsWith(".json"));
85548
86140
  for (const file3 of todoFiles) {
85549
86141
  try {
85550
- const content = await readFile4(join66(TODO_DIR, file3), "utf-8");
86142
+ const content = await readFile4(join65(TODO_DIR, file3), "utf-8");
85551
86143
  const data = JSON.parse(content);
85552
86144
  if (Array.isArray(data)) {
85553
86145
  return data.map((item) => ({
@@ -85567,7 +86159,7 @@ async function readSessionTodos(sessionID) {
85567
86159
  async function readSessionTranscript(sessionID) {
85568
86160
  if (!existsSync63(TRANSCRIPT_DIR2))
85569
86161
  return 0;
85570
- const transcriptFile = join66(TRANSCRIPT_DIR2, `${sessionID}.jsonl`);
86162
+ const transcriptFile = join65(TRANSCRIPT_DIR2, `${sessionID}.jsonl`);
85571
86163
  if (!existsSync63(transcriptFile))
85572
86164
  return 0;
85573
86165
  try {
@@ -86078,17 +86670,17 @@ function createSkillTool(options = {}) {
86078
86670
  }
86079
86671
  var skill = createSkillTool();
86080
86672
  // src/tools/slashcommand/command-discovery.ts
86081
- import { existsSync as existsSync64, readdirSync as readdirSync19, readFileSync as readFileSync40 } from "fs";
86082
- import { basename as basename6, join as join67 } from "path";
86673
+ import { existsSync as existsSync64, readdirSync as readdirSync20, readFileSync as readFileSync40 } from "fs";
86674
+ import { basename as basename6, join as join66 } from "path";
86083
86675
  function discoverCommandsFromDir2(commandsDir, scope) {
86084
86676
  if (!existsSync64(commandsDir))
86085
86677
  return [];
86086
- const entries = readdirSync19(commandsDir, { withFileTypes: true });
86678
+ const entries = readdirSync20(commandsDir, { withFileTypes: true });
86087
86679
  const commands2 = [];
86088
86680
  for (const entry of entries) {
86089
86681
  if (!isMarkdownFile(entry))
86090
86682
  continue;
86091
- const commandPath = join67(commandsDir, entry.name);
86683
+ const commandPath = join66(commandsDir, entry.name);
86092
86684
  const commandName = basename6(entry.name, ".md");
86093
86685
  try {
86094
86686
  const content = readFileSync40(commandPath, "utf-8");
@@ -86115,8 +86707,8 @@ function discoverCommandsFromDir2(commandsDir, scope) {
86115
86707
  }
86116
86708
  function discoverCommandsSync(directory) {
86117
86709
  const configDir = getOpenCodeConfigDir({ binary: "opencode" });
86118
- const opencodeGlobalDir = join67(configDir, "command");
86119
- const opencodeProjectDir = join67(directory ?? process.cwd(), ".opencode", "command");
86710
+ const opencodeGlobalDir = join66(configDir, "command");
86711
+ const opencodeProjectDir = join66(directory ?? process.cwd(), ".opencode", "command");
86120
86712
  const opencodeGlobalCommands = discoverCommandsFromDir2(opencodeGlobalDir, "opencode");
86121
86713
  const opencodeProjectCommands = discoverCommandsFromDir2(opencodeProjectDir, "opencode-project");
86122
86714
  const builtinCommandsMap = loadBuiltinCommands();
@@ -87287,8 +87879,8 @@ function createAssemblyTool(options) {
87287
87879
  }
87288
87880
  // src/tools/dcp-switch-profile/tools.ts
87289
87881
  import { existsSync as existsSync65, writeFileSync as writeFileSync25 } from "fs";
87290
- import { homedir as homedir11 } from "os";
87291
- import { join as join68 } from "path";
87882
+ import { homedir as homedir10 } from "os";
87883
+ import { join as join67 } from "path";
87292
87884
 
87293
87885
  // src/config/schema/dcp.ts
87294
87886
  var DcpCompressOverrideSchema = exports_external.object({
@@ -87461,8 +88053,8 @@ var DcpConfigSchema = exports_external.object({
87461
88053
  });
87462
88054
 
87463
88055
  // src/tools/dcp-switch-profile/tools.ts
87464
- var DCP_PLUGIN_DIR = join68(homedir11(), ".config", "opencode", "node_modules", "@tarquinen", "opencode-dcp");
87465
- var DCP_SYMLINK = join68(homedir11(), ".config", "opencode", "dcp.jsonc");
88056
+ var DCP_PLUGIN_DIR = join67(homedir10(), ".config", "opencode", "node_modules", "@tarquinen", "opencode-dcp");
88057
+ var DCP_SYMLINK = join67(homedir10(), ".config", "opencode", "dcp.jsonc");
87466
88058
  var VALID_PROFILES = ["economy", "balanced", "performance", "ultimate"];
87467
88059
  function checkDcpInstalled() {
87468
88060
  if (!existsSync65(DCP_PLUGIN_DIR)) {
@@ -87871,6 +88463,11 @@ function createDelegateAgent(ctx, backgroundManager, disabledAgents = []) {
87871
88463
  }
87872
88464
  const normalizedAgent = args.subagent_type.toLowerCase();
87873
88465
  args = { ...args, subagent_type: normalizedAgent };
88466
+ const callerAgent = (toolCtx.agent || "").toLowerCase();
88467
+ const MOUSE_RESEARCH_ONLY = ["trinity", "operator"];
88468
+ if (callerAgent === "mouse" && !MOUSE_RESEARCH_ONLY.includes(normalizedAgent)) {
88469
+ return `Error: Mouse is a leaf executor. delegate_agent with subagent_type="${normalizedAgent}" is BLOCKED. Only trinity/operator allowed for research. Plan files (.matrixx/plans/*.md) must be created via task(subagent_type="oracle") from Morpheus, never via Mouse \u2192 Oracle. Route plan generation directly from Morpheus to oracle.`;
88470
+ }
87874
88471
  if (disabledAgents.some((disabled) => disabled.toLowerCase() === normalizedAgent)) {
87875
88472
  return `Error: Agent "${normalizedAgent}" is disabled via disabled_agents configuration. Remove it from disabled_agents in your matrixx.json to use it.`;
87876
88473
  }
@@ -91338,6 +91935,109 @@ Original error: ${createResult.error}`;
91338
91935
  }
91339
91936
  });
91340
91937
  }
91938
+ // src/tools/task/task-cleanup.ts
91939
+ import { existsSync as existsSync66, readdirSync as readdirSync21, readFileSync as readFileSync41, unlinkSync as unlinkSync10 } from "fs";
91940
+ import { join as join68 } from "path";
91941
+ init_logger();
91942
+ function parseOlderThan(value) {
91943
+ const match = value.trim().match(/^(\d+)(d|h|m)$/);
91944
+ if (!match)
91945
+ return null;
91946
+ const amount = Number(match[1]);
91947
+ const unit = match[2];
91948
+ if (unit === "d")
91949
+ return amount * 24 * 60 * 60 * 1000;
91950
+ if (unit === "h")
91951
+ return amount * 60 * 60 * 1000;
91952
+ return amount * 60 * 1000;
91953
+ }
91954
+ function getTaskTimestamp(task) {
91955
+ const raw = task;
91956
+ const candidate = raw.time_updated ?? raw.time_created ?? raw.updatedAt ?? raw.createdAt ?? raw.timeUpdated ?? raw.timeCreated ?? null;
91957
+ if (candidate === null || candidate === undefined)
91958
+ return Date.now();
91959
+ if (typeof candidate === "number")
91960
+ return candidate;
91961
+ if (typeof candidate === "string") {
91962
+ const parsed = Date.parse(candidate);
91963
+ if (!Number.isNaN(parsed))
91964
+ return parsed;
91965
+ const asNum = Number(candidate);
91966
+ if (!Number.isNaN(asNum))
91967
+ return asNum;
91968
+ return Date.now();
91969
+ }
91970
+ return Date.now();
91971
+ }
91972
+ function createTaskCleanupTool(config4, ctx) {
91973
+ return tool({
91974
+ description: `Delete completed task files from storage.
91975
+
91976
+ Scans getTaskDir()/*.json, filters status==="completed" and optionally olderThan.
91977
+ olderThan supports "7d", "24h", "30m" format (regex ^(\\d+)(d|h|m)$).
91978
+ Returns counts: {deleted, remaining, deletedIds}`,
91979
+ args: {
91980
+ olderThan: tool.schema.string().optional().describe('Only delete completed tasks older than duration (e.g. "7d", "24h", "30m")'),
91981
+ all: tool.schema.boolean().optional().describe("Delete all completed tasks (default true when olderThan not set)")
91982
+ },
91983
+ execute: async (args, context) => {
91984
+ try {
91985
+ const olderThan = args.olderThan;
91986
+ let olderThanMs = null;
91987
+ if (olderThan !== undefined) {
91988
+ const parsed = parseOlderThan(olderThan);
91989
+ if (parsed === null) {
91990
+ return JSON.stringify({
91991
+ error: "validation_error",
91992
+ message: `Invalid olderThan format: "${olderThan}". Expected e.g. "7d", "24h", "30m"`
91993
+ });
91994
+ }
91995
+ olderThanMs = parsed;
91996
+ }
91997
+ const directory = context?.directory ?? ctx?.directory ?? process.cwd();
91998
+ const taskDir = getTaskDir(config4, directory);
91999
+ if (!existsSync66(taskDir)) {
92000
+ return JSON.stringify({ deleted: 0, remaining: 0, deletedIds: [] });
92001
+ }
92002
+ const files = readdirSync21(taskDir).filter((f) => f.endsWith(".json") && f.startsWith("T-"));
92003
+ const total = files.length;
92004
+ const deletedIds = [];
92005
+ for (const file3 of files) {
92006
+ const filePath = join68(taskDir, file3);
92007
+ let raw;
92008
+ try {
92009
+ const content = readFileSync41(filePath, "utf-8");
92010
+ raw = JSON.parse(content);
92011
+ } catch {
92012
+ continue;
92013
+ }
92014
+ if (raw.status !== "completed")
92015
+ continue;
92016
+ const id = typeof raw.id === "string" ? raw.id : file3.replace(".json", "");
92017
+ if (olderThanMs !== null) {
92018
+ const ts = getTaskTimestamp(raw);
92019
+ const age = Date.now() - ts;
92020
+ if (age <= olderThanMs)
92021
+ continue;
92022
+ }
92023
+ try {
92024
+ unlinkSync10(filePath);
92025
+ deletedIds.push(id);
92026
+ } catch (err) {
92027
+ log("[task-cleanup] Failed to delete", { file: file3, error: String(err) });
92028
+ }
92029
+ }
92030
+ return JSON.stringify({
92031
+ deleted: deletedIds.length,
92032
+ remaining: total - deletedIds.length,
92033
+ deletedIds
92034
+ });
92035
+ } catch (error95) {
92036
+ return JSON.stringify({ error: "internal_error", message: error95 instanceof Error ? error95.message : String(error95) });
92037
+ }
92038
+ }
92039
+ });
92040
+ }
91341
92041
  // src/tools/task/task-create.ts
91342
92042
  import { join as join69 } from "path";
91343
92043
  function createTaskCreateTool(config4, ctx) {
@@ -91371,8 +92071,12 @@ Calculate dependencies carefully to maximize parallel execution:
91371
92071
  async function handleCreate2(args, config4, ctx, context) {
91372
92072
  try {
91373
92073
  const validatedArgs = TaskCreateInputSchema.parse(args);
91374
- const taskDir = getTaskDir(config4);
91375
- const lock = acquireLock(taskDir);
92074
+ const directory = context?.directory ?? ctx?.directory ?? process.cwd();
92075
+ try {
92076
+ migrateLegacyTasksIfNeeded(config4, directory);
92077
+ } catch {}
92078
+ const taskDir = getTaskDir(config4, directory);
92079
+ const lock = await acquireLockWithRetry(taskDir);
91376
92080
  if (!lock.acquired) {
91377
92081
  return JSON.stringify({ error: "task_lock_unavailable" });
91378
92082
  }
@@ -91389,11 +92093,11 @@ async function handleCreate2(args, config4, ctx, context) {
91389
92093
  metadata: validatedArgs.metadata,
91390
92094
  repoURL: validatedArgs.repoURL,
91391
92095
  parentID: validatedArgs.parentID,
91392
- threadID: context.sessionID
92096
+ threadID: context.sessionID,
92097
+ projectRoot: directory
91393
92098
  };
91394
92099
  const validatedTask = TaskObjectSchema.parse(task);
91395
92100
  writeJsonAtomic(join69(taskDir, `${taskId}.json`), validatedTask);
91396
- await syncTaskTodoUpdate(ctx, validatedTask, context.sessionID);
91397
92101
  return JSON.stringify({
91398
92102
  task: {
91399
92103
  id: validatedTask.id,
@@ -91425,7 +92129,7 @@ function parseTaskId(id) {
91425
92129
  return null;
91426
92130
  return id;
91427
92131
  }
91428
- function createTaskGetTool(config4) {
92132
+ function createTaskGetTool(config4, ctx) {
91429
92133
  return tool({
91430
92134
  description: `Retrieve a task by ID.
91431
92135
 
@@ -91435,14 +92139,15 @@ Returns null if the task does not exist or the file is invalid.`,
91435
92139
  args: {
91436
92140
  id: tool.schema.string().describe("Task ID to retrieve (format: T-{uuid})")
91437
92141
  },
91438
- execute: async (args) => {
92142
+ execute: async (args, context) => {
91439
92143
  try {
91440
92144
  const validatedArgs = TaskGetInputSchema.parse(args);
91441
92145
  const taskId = parseTaskId(validatedArgs.id);
91442
92146
  if (!taskId) {
91443
92147
  return JSON.stringify({ error: "invalid_task_id" });
91444
92148
  }
91445
- const taskDir = getTaskDir(config4);
92149
+ const directory = context?.directory ?? ctx?.directory ?? process.cwd();
92150
+ const taskDir = getTaskDir(config4, directory);
91446
92151
  const taskPath = join70(taskDir, `${taskId}.json`);
91447
92152
  const task = readJsonSafe(taskPath, TaskObjectSchema);
91448
92153
  return JSON.stringify({ task: task ?? null });
@@ -91456,9 +92161,9 @@ Returns null if the task does not exist or the file is invalid.`,
91456
92161
  });
91457
92162
  }
91458
92163
  // src/tools/task/task-list.ts
91459
- import { existsSync as existsSync66, readdirSync as readdirSync20 } from "fs";
92164
+ import { existsSync as existsSync67, readdirSync as readdirSync22 } from "fs";
91460
92165
  import { join as join71 } from "path";
91461
- function createTaskList(config4) {
92166
+ function createTaskList(config4, ctx) {
91462
92167
  return tool({
91463
92168
  description: `List all active tasks with summary information.
91464
92169
 
@@ -91466,12 +92171,13 @@ Returns tasks excluding completed and deleted statuses by default.
91466
92171
  For each task's blockedBy field, filters to only include unresolved (non-completed) blockers.
91467
92172
  Returns summary format: id, subject, status, owner, blockedBy (not full description).`,
91468
92173
  args: {},
91469
- execute: async () => {
91470
- const taskDir = getTaskDir(config4);
91471
- if (!existsSync66(taskDir)) {
92174
+ execute: async (_args, context) => {
92175
+ const directory = context?.directory ?? ctx?.directory ?? process.cwd();
92176
+ const taskDir = getTaskDir(config4, directory);
92177
+ if (!existsSync67(taskDir)) {
91472
92178
  return JSON.stringify({ tasks: [] });
91473
92179
  }
91474
- const files = readdirSync20(taskDir).filter((f) => f.endsWith(".json") && f.startsWith("T-")).map((f) => f.replace(".json", ""));
92180
+ const files = readdirSync22(taskDir).filter((f) => f.endsWith(".json") && f.startsWith("T-")).map((f) => f.replace(".json", ""));
91475
92181
  if (files.length === 0) {
91476
92182
  return JSON.stringify({ tasks: [] });
91477
92183
  }
@@ -91516,8 +92222,6 @@ function createTaskUpdateTool(config4, ctx) {
91516
92222
 
91517
92223
  Supports updating: subject, description, status, activeForm, owner, metadata.
91518
92224
  For blocks/blockedBy: use addBlocks/addBlockedBy to append (additive, not replacement).
91519
- For metadata: merge with existing, set key to null to delete.
91520
- Syncs to OpenCode Todo API after update.
91521
92225
 
91522
92226
  **IMPORTANT - Dependency Management:**
91523
92227
  Use \`addBlockedBy\` to declare dependencies on other tasks.
@@ -91545,8 +92249,9 @@ async function handleUpdate(args, config4, ctx, context) {
91545
92249
  if (!taskId) {
91546
92250
  return JSON.stringify({ error: "invalid_task_id" });
91547
92251
  }
91548
- const taskDir = getTaskDir(config4);
91549
- const lock = acquireLock(taskDir);
92252
+ const directory = context?.directory ?? ctx?.directory ?? process.cwd();
92253
+ const taskDir = getTaskDir(config4, directory);
92254
+ const lock = await acquireLockWithRetry(taskDir);
91550
92255
  if (!lock.acquired) {
91551
92256
  return JSON.stringify({ error: "task_lock_unavailable" });
91552
92257
  }
@@ -91589,7 +92294,6 @@ async function handleUpdate(args, config4, ctx, context) {
91589
92294
  }
91590
92295
  const validatedTask = TaskObjectSchema.parse(task);
91591
92296
  writeJsonAtomic(taskPath, validatedTask);
91592
- await syncTaskTodoUpdate(ctx, validatedTask, context.sessionID);
91593
92297
  return JSON.stringify({ task: validatedTask });
91594
92298
  } finally {
91595
92299
  lock.release();
@@ -91722,14 +92426,14 @@ function createToolGuardHooks(args) {
91722
92426
  const envFileWriteGuard = isHookEnabled("env-file-write-guard") ? safeHook("env-file-write-guard", () => createEnvFileWriteGuardHook(pluginConfig.security?.env_file_guard)) : null;
91723
92427
  const jsonErrorRecovery = isHookEnabled("json-error-recovery") ? safeHook("json-error-recovery", () => createJsonErrorRecoveryHook(ctx)) : null;
91724
92428
  const bashFileReadGuard = isHookEnabled("bash-file-read-guard") ? safeHook("bash-file-read-guard", () => createBashFileReadGuardHook()) : null;
91725
- const todoDescriptionOverride = isHookEnabled("todo-description-override") ? safeHook("todo-description-override", () => createTodoDescriptionOverrideHook()) : null;
92429
+ const contextModeEnforcer = isHookEnabled("context-mode-enforcer") ? safeHook("context-mode-enforcer", () => createContextModeEnforcerHook(pluginConfig)) : null;
91726
92430
  const readImageResizer = isHookEnabled("read-image-resizer") ? safeHook("read-image-resizer", () => createReadImageResizerHook(ctx)) : null;
91727
92431
  const webfetchRedirectGuard = isHookEnabled("webfetch-redirect-guard") ? safeHook("webfetch-redirect-guard", () => createWebFetchRedirectGuardHook(ctx)) : null;
91728
92432
  const hashlineEditDiffEnhancer = isHookEnabled("hashline-edit-diff-enhancer") ? safeHook("hashline-edit-diff-enhancer", () => createHashlineEditDiffEnhancerHook({ hashline_edit: { enabled: pluginConfig.experimental?.hashline_edit ?? false } })) : null;
91729
92433
  const qualityGate = isHookEnabled("quality-gate") ? safeHook("quality-gate", () => createQualityGateHook()) : null;
91730
92434
  const taskNotepad = isHookEnabled("task-notepad") ? safeHook("task-notepad", () => createTaskNotepadHook(ctx)) : null;
92435
+ const taskEditGuard = isHookEnabled("task-edit-guard") ? safeHook("task-edit-guard", () => createTaskEditGuardHook(ctx)) : null;
91731
92436
  const evolutionWatcher = isHookEnabled("evolution-watcher") ? safeHook("evolution-watcher", () => createEvolutionWatcherHook(ctx, pluginConfig.evolution)) : null;
91732
- const taskTodoMirror = isHookEnabled("task-todo-mirror") ? safeHook("task-todo-mirror", () => createTaskTodoMirrorHook(ctx, pluginConfig)) : null;
91733
92437
  return {
91734
92438
  commentChecker,
91735
92439
  toolOutputTruncator,
@@ -91744,14 +92448,14 @@ function createToolGuardHooks(args) {
91744
92448
  envFileWriteGuard,
91745
92449
  jsonErrorRecovery,
91746
92450
  bashFileReadGuard,
91747
- todoDescriptionOverride,
92451
+ contextModeEnforcer,
91748
92452
  readImageResizer,
91749
92453
  webfetchRedirectGuard,
91750
92454
  hashlineEditDiffEnhancer,
91751
92455
  qualityGate,
91752
92456
  taskNotepad,
91753
- evolutionWatcher,
91754
- taskTodoMirror
92457
+ taskEditGuard,
92458
+ evolutionWatcher
91755
92459
  };
91756
92460
  }
91757
92461
 
@@ -92205,19 +92909,19 @@ function detectRepetitiveToolUse(window2) {
92205
92909
  }
92206
92910
 
92207
92911
  // src/features/background-agent/message-dir.ts
92208
- import { existsSync as existsSync67, readdirSync as readdirSync21, readFileSync as readFileSync41 } from "fs";
92912
+ import { existsSync as existsSync68, readdirSync as readdirSync23, readFileSync as readFileSync42 } from "fs";
92209
92913
  import { join as join73 } from "path";
92210
92914
  var MESSAGE_DIR_CACHE_CAP = 1000;
92211
92915
  var messageDirCache = new Map;
92212
92916
  function computeMessageDir(sessionID) {
92213
- if (!existsSync67(MESSAGE_STORAGE))
92917
+ if (!existsSync68(MESSAGE_STORAGE))
92214
92918
  return null;
92215
92919
  const directPath = join73(MESSAGE_STORAGE, sessionID);
92216
- if (existsSync67(directPath))
92920
+ if (existsSync68(directPath))
92217
92921
  return directPath;
92218
- for (const dir of readdirSync21(MESSAGE_STORAGE)) {
92922
+ for (const dir of readdirSync23(MESSAGE_STORAGE)) {
92219
92923
  const sessionPath = join73(MESSAGE_STORAGE, dir, sessionID);
92220
- if (existsSync67(sessionPath))
92924
+ if (existsSync68(sessionPath))
92221
92925
  return sessionPath;
92222
92926
  }
92223
92927
  return null;
@@ -92251,11 +92955,11 @@ function hasPartialAgentOrModel(message) {
92251
92955
  }
92252
92956
  function findNearestMessageExcludingCompaction(messageDir) {
92253
92957
  try {
92254
- const files = readdirSync21(messageDir).filter((name) => name.endsWith(".json")).sort().reverse();
92958
+ const files = readdirSync23(messageDir).filter((name) => name.endsWith(".json")).sort().reverse();
92255
92959
  const parsedByFile = new Map;
92256
92960
  for (const file3 of files) {
92257
92961
  try {
92258
- const content = readFileSync41(join73(messageDir, file3), "utf-8");
92962
+ const content = readFileSync42(join73(messageDir, file3), "utf-8");
92259
92963
  const parsed = JSON.parse(content);
92260
92964
  parsedByFile.set(file3, parsed);
92261
92965
  if (hasFullAgentAndModel(parsed)) {
@@ -94695,6 +95399,12 @@ ${useWhen.map((w) => `| ${w} | Oracle FIRST, then implement |`).join(`
94695
95399
  ${avoidWhen.map((w) => `- ${w}`).join(`
94696
95400
  `)}
94697
95401
 
95402
+ ### HARD ROUTING RULE \u2014 Plan File Creation (NON-NEGOTIABLE):
95403
+ - ANY task that creates or edits \`.matrixx/plans/*.md\` MUST use \`task(subagent_type='oracle')\` \u2014 NEVER \`task(category='...')\` / Mouse
95404
+ - ANY prompt containing \`Oracle plan\`, \`work plan\`, \`proposal\`, \`TL;DR\`, \`root cause\`, \`pure-host\`, \`fix options\` or similar planning language MUST route to \`oracle\`
95405
+ - Mouse is a leaf executor and CANNOT produce plans. Oracle owns all plan generation. Mouse \u2192 Oracle is a violation (extra hop, diluted context).
95406
+ - If you already created tasks for research + drafting, the drafting task MUST still be \`oracle\`, not \`mouse\`.
95407
+
94698
95408
  ### Usage Pattern:
94699
95409
  Oracle creates plans, not code. Invoke oracle to decompose complex requests into structured work plans with clear steps, then execute the plan yourself or delegate to workers.
94700
95410
 
@@ -94772,14 +95482,14 @@ function buildCompactContextDisciplineSection(hasContextMode = false) {
94772
95482
  | Docs / Web | ctx_fetch_and_index -> ctx_search |
94773
95483
  | Compression | compress when ctx_stats > 40% or 10+ tool calls |
94774
95484
 
94775
- Edits need prior read for LINE#ID \u2014 read\u2192edit chain exempt. When in doubt, use ctx_*.`;
95485
+ Edits need prior read for LINE#ID \u2014 read\u2192edit chain exempt. MUST use ctx_* when available \u2014 raw grep/read is forbidden for analysis.`;
94776
95486
  }
94777
95487
  function buildExploreDisciplineSection(hasContextMode = false, hasHeadroom = false) {
94778
95488
  if (!hasContextMode && !hasHeadroom)
94779
95489
  return "";
94780
95490
  const parts = [];
94781
95491
  if (hasContextMode) {
94782
- parts.push("Prefer ctx_search for indexed hits \u2192 grep/glob fallback for raw FS; use ctx_batch_execute / ctx_execute for multi-file analysis; use ctx_fetch_and_index for docs/web \u2192 ctx_search.");
95492
+ parts.push("MUST use ctx_search for indexed hits \u2192 grep/glob fallback ONLY when ctx_* unavailable; use ctx_batch_execute / ctx_execute for multi-file analysis; use ctx_fetch_and_index for docs/web \u2192 ctx_search. Raw grep/read/glob for analysis is forbidden when ctx_* is available.");
94783
95493
  }
94784
95494
  if (hasHeadroom) {
94785
95495
  parts.push("Use headroom_retrieve / headroom_search for compressed history \u2014 NEVER re-read full history.");
@@ -95823,8 +96533,8 @@ function createBddContractAgent(model) {
95823
96533
  createBddContractAgent.mode = MODE2;
95824
96534
 
95825
96535
  // src/agents/builtin-agents/resolve-file-uri.ts
95826
- import { existsSync as existsSync68, readFileSync as readFileSync42 } from "fs";
95827
- import { homedir as homedir12 } from "os";
96536
+ import { existsSync as existsSync69, readFileSync as readFileSync43 } from "fs";
96537
+ import { homedir as homedir11 } from "os";
95828
96538
  import { isAbsolute as isAbsolute5, resolve as resolve10 } from "path";
95829
96539
  function resolvePromptAppend(promptAppend, configDir) {
95830
96540
  if (!promptAppend.startsWith("file://"))
@@ -95833,16 +96543,16 @@ function resolvePromptAppend(promptAppend, configDir) {
95833
96543
  let filePath;
95834
96544
  try {
95835
96545
  const decoded = decodeURIComponent(encoded);
95836
- const expanded = decoded.startsWith("~/") ? decoded.replace(/^~\//, `${homedir12()}/`) : decoded;
96546
+ const expanded = decoded.startsWith("~/") ? decoded.replace(/^~\//, `${homedir11()}/`) : decoded;
95837
96547
  filePath = isAbsolute5(expanded) ? expanded : resolve10(configDir ?? process.cwd(), expanded);
95838
96548
  } catch {
95839
96549
  return `[WARNING: Malformed file URI (invalid percent-encoding): ${promptAppend}]`;
95840
96550
  }
95841
- if (!existsSync68(filePath)) {
96551
+ if (!existsSync69(filePath)) {
95842
96552
  return `[WARNING: Could not resolve file URI: ${promptAppend}]`;
95843
96553
  }
95844
96554
  try {
95845
- return readFileSync42(filePath, "utf8");
96555
+ return readFileSync43(filePath, "utf8");
95846
96556
  } catch {
95847
96557
  return `[WARNING: Could not read file: ${promptAppend}]`;
95848
96558
  }
@@ -97557,7 +98267,8 @@ var ORACLE_PLAN_BUILDER_METADATA = {
97557
98267
  { domain: "Architecture decisions", trigger: "Multi-system tradeoffs, unfamiliar patterns" },
97558
98268
  { domain: "Work planning", trigger: "Complex feature implementation, refactoring" },
97559
98269
  { domain: "Complex task breakdown", trigger: "Large ambiguous requests needing decomposition" },
97560
- { domain: "Self-review", trigger: "After completing significant implementation" }
98270
+ { domain: "Self-review", trigger: "After completing significant implementation" },
98271
+ { domain: "Plan generation", trigger: "ANY .matrixx/plans/*.md creation or planning doc \u2192 oracle ONLY, never Mouse" }
97561
98272
  ],
97562
98273
  useWhen: [
97563
98274
  "Complex architecture design",
@@ -100589,20 +101300,22 @@ function buildConstraintsSection(useTaskSystem) {
100589
101300
  return `<Critical_Constraints>
100590
101301
  BLOCKED ACTIONS (will fail if attempted):
100591
101302
  - task (agent delegation tool): BLOCKED \u2014 you cannot delegate work to other agents
101303
+ - delegate_agent with subagent_type=oracle/construct/merovingian/smith/sentinel/cipher: BLOCKED \u2014 Mouse is a leaf executor
100592
101304
 
100593
101305
  ALLOWED tools:
100594
- - delegate_agent: You CAN spawn explore/librarian agents for research
101306
+ - delegate_agent with subagent_type=trinity/operator ONLY: You MAY spawn explore/librarian agents for research
100595
101307
  - task_create, task_update, task_list, task_get: ALLOWED \u2014 use these for tracking your work
100596
101308
 
100597
- You work ALONE for implementation. No delegation of implementation tasks.
101309
+ You work ALONE for implementation. No delegation of implementation or planning tasks. Plan files (.matrixx/plans/*.md) are OWNED by oracle \u2014 never create them via Mouse.
100598
101310
  </Critical_Constraints>`;
100599
101311
  }
100600
101312
  return `<Critical_Constraints>
100601
101313
  BLOCKED ACTIONS (will fail if attempted):
100602
101314
  - task (agent delegation tool): BLOCKED \u2014 you cannot delegate work to other agents
101315
+ - delegate_agent with subagent_type=oracle/construct/merovingian/smith/sentinel/cipher: BLOCKED \u2014 Mouse is a leaf executor
100603
101316
 
100604
- ALLOWED: delegate_agent - You CAN spawn explore/librarian agents for research.
100605
- You work ALONE for implementation. No delegation of implementation tasks.
101317
+ ALLOWED: delegate_agent with subagent_type=trinity/operator ONLY - You MAY spawn explore/librarian agents for research.
101318
+ You work ALONE for implementation. No delegation of implementation or planning tasks. Plan files (.matrixx/plans/*.md) are OWNED by oracle \u2014 never create them via Mouse.
100606
101319
  </Critical_Constraints>`;
100607
101320
  }
100608
101321
  function buildTodoDisciplineSection2(useTaskSystem) {
@@ -100707,20 +101420,22 @@ function buildConstraintsSection2(useTaskSystem) {
100707
101420
  return `<Critical_Constraints>
100708
101421
  BLOCKED ACTIONS (will fail if attempted):
100709
101422
  - task (agent delegation tool): BLOCKED \u2014 you cannot delegate work to other agents
101423
+ - delegate_agent with subagent_type=oracle/construct/merovingian/smith/sentinel/cipher: BLOCKED \u2014 Mouse is a leaf executor
100710
101424
 
100711
101425
  ALLOWED tools:
100712
- - delegate_agent: You CAN spawn explore/librarian agents for research
101426
+ - delegate_agent with subagent_type=trinity/operator ONLY: You MAY spawn explore/librarian agents for research
100713
101427
  - task_create, task_update, task_list, task_get: ALLOWED \u2014 use these for tracking your work
100714
101428
 
100715
- You work ALONE for implementation. No delegation of implementation tasks.
101429
+ You work ALONE for implementation. No delegation of implementation or planning tasks. Plan files (.matrixx/plans/*.md) are OWNED by oracle \u2014 never create them via Mouse.
100716
101430
  </Critical_Constraints>`;
100717
101431
  }
100718
101432
  return `<Critical_Constraints>
100719
101433
  BLOCKED ACTIONS (will fail if attempted):
100720
101434
  - task (agent delegation tool): BLOCKED \u2014 you cannot delegate work to other agents
101435
+ - delegate_agent with subagent_type=oracle/construct/merovingian/smith/sentinel/cipher: BLOCKED \u2014 Mouse is a leaf executor
100721
101436
 
100722
- ALLOWED: delegate_agent - You CAN spawn explore/librarian agents for research.
100723
- You work ALONE for implementation. No delegation of implementation tasks.
101437
+ ALLOWED: delegate_agent with subagent_type=trinity/operator ONLY - You MAY spawn explore/librarian agents for research.
101438
+ You work ALONE for implementation. No delegation of implementation or planning tasks. Plan files (.matrixx/plans/*.md) are OWNED by oracle \u2014 never create them via Mouse.
100724
101439
  </Critical_Constraints>`;
100725
101440
  }
100726
101441
  function buildTodoDisciplineSection3(useTaskSystem) {
@@ -100821,17 +101536,18 @@ BLOCKED (will fail if attempted):
100821
101536
  | Tool | Status | Description |
100822
101537
  |------|--------|-------------|
100823
101538
  | task | BLOCKED | Agent delegation tool \u2014 you cannot spawn other agents |
101539
+ | delegate_agent with oracle/construct/merovingian/smith/sentinel/cipher | BLOCKED | Mouse is leaf \u2014 only trinity/operator allowed |
100824
101540
 
100825
101541
  ALLOWED:
100826
101542
  | Tool | Usage |
100827
101543
  |------|-------|
100828
- | delegate_agent | Spawn explore/librarian for research ONLY |
101544
+ | delegate_agent with trinity/operator ONLY | Spawn explore/librarian for research ONLY |
100829
101545
  | task_create | Create tasks to track your work |
100830
101546
  | task_update | Update task status (in_progress, completed) |
100831
101547
  | task_list | List active tasks |
100832
101548
  | task_get | Get task details by ID |
100833
101549
 
100834
- You work ALONE for implementation. No delegation.
101550
+ You work ALONE for implementation. No delegation. Plan files (.matrixx/plans/*.md) are OWNED by oracle \u2014 never create them via Mouse.
100835
101551
  </blocked_actions>`;
100836
101552
  }
100837
101553
  return `<blocked_actions>
@@ -100839,13 +101555,14 @@ BLOCKED (will fail if attempted):
100839
101555
  | Tool | Status | Description |
100840
101556
  |------|--------|-------------|
100841
101557
  | task | BLOCKED | Agent delegation tool \u2014 you cannot spawn other agents |
101558
+ | delegate_agent with oracle/construct/merovingian/smith/sentinel/cipher | BLOCKED | Mouse is leaf \u2014 only trinity/operator allowed |
100842
101559
 
100843
101560
  ALLOWED:
100844
101561
  | Tool | Usage |
100845
101562
  |------|-------|
100846
- | delegate_agent | Spawn explore/librarian for research ONLY |
101563
+ | delegate_agent with trinity/operator ONLY | Spawn explore/librarian for research ONLY |
100847
101564
 
100848
- You work ALONE for implementation. No delegation.
101565
+ You work ALONE for implementation. No delegation. Plan files (.matrixx/plans/*.md) are OWNED by oracle \u2014 never create them via Mouse.
100849
101566
  </blocked_actions>`;
100850
101567
  }
100851
101568
  function buildGptTaskDisciplineSection(useTaskSystem) {
@@ -100885,11 +101602,12 @@ Execute tasks directly. You NEVER delegate.
100885
101602
 
100886
101603
  <rules>
100887
101604
  - task tool: BLOCKED \u2014 cannot delegate to other agents
100888
- - delegate_agent: ALLOWED \u2014 only for research (explore/librarian)
101605
+ - delegate_agent with oracle/construct/merovingian/smith/sentinel/cipher: BLOCKED \u2014 Mouse is leaf, only trinity/operator allowed
101606
+ - delegate_agent with trinity/operator ONLY: ALLOWED for research (explore/librarian)
100889
101607
  - ${useTaskSystem ? "task_create/task_update" : "todowrite/todoread"}: REQUIRED for tracking
100890
101608
  - Implement ONLY what is requested \u2014 no scope creep
101609
+ - Plan files (.matrixx/plans/*.md): OWNED by oracle \u2014 never create via Mouse
100891
101610
  </rules>
100892
-
100893
101611
  ${taskDiscipline}
100894
101612
 
100895
101613
  <verify>
@@ -100981,15 +101699,21 @@ function buildQwenBlockedActions(useTaskSystem) {
100981
101699
  return `| Tool | Status | Notes |
100982
101700
  |------|--------|-------|
100983
101701
  | task | BLOCKED | Cannot delegate implementation |
100984
- | delegate_agent | ALLOWED | Research only (explore/librarian) |
101702
+ | delegate_agent with oracle/construct/merovingian/smith/sentinel/cipher | BLOCKED | Mouse is leaf \u2014 only trinity/operator allowed |
101703
+ | delegate_agent with trinity/operator ONLY | ALLOWED | Research only (explore/librarian) |
100985
101704
  | task_create | ALLOWED | Track your work |
100986
101705
  | task_update | ALLOWED | Update status |
100987
- | task_list / task_get | ALLOWED | View tasks |`;
101706
+ | task_list / task_get | ALLOWED | View tasks |
101707
+
101708
+ Plan files (.matrixx/plans/*.md) are OWNED by oracle \u2014 never create them via Mouse.`;
100988
101709
  }
100989
101710
  return `| Tool | Status | Notes |
100990
101711
  |------|--------|-------|
100991
101712
  | task | BLOCKED | Cannot delegate implementation |
100992
- | delegate_agent | ALLOWED | Research only (explore/librarian) |`;
101713
+ | delegate_agent with oracle/construct/merovingian/smith/sentinel/cipher | BLOCKED | Mouse is leaf \u2014 only trinity/operator allowed |
101714
+ | delegate_agent with trinity/operator ONLY | ALLOWED | Research only (explore/librarian) |
101715
+
101716
+ Plan files (.matrixx/plans/*.md) are OWNED by oracle \u2014 never create them via Mouse.`;
100993
101717
  }
100994
101718
  function buildQwenDisciplineSection(useTaskSystem) {
100995
101719
  if (useTaskSystem) {
@@ -101256,7 +101980,7 @@ async function applyAgentConfig(params) {
101256
101980
  if (!params.pluginConfig.tdd_enforcer?.enabled) {
101257
101981
  disabledSkills.add("tdd-enforcer");
101258
101982
  }
101259
- const useTaskSystem = params.pluginConfig.experimental?.task_system ?? false;
101983
+ const useTaskSystem = isTaskSystemEnabled(params.pluginConfig);
101260
101984
  const availableToolNames = getAvailableToolNames();
101261
101985
  const builtinAgents = await createBuiltinAgents(migratedDisabledAgents, params.pluginConfig.agents, params.ctx.directory, undefined, params.pluginConfig.categories, allDiscoveredSkills, params.ctx.client, browserProvider, currentModel, disabledSkills, useTaskSystem, params.pluginConfig.global_model, availableToolNames);
101262
101986
  const rawPluginAgents = params.pluginComponents.agents;
@@ -101562,7 +102286,9 @@ function agentByKey(agentResult, key) {
101562
102286
  return agentResult[key];
101563
102287
  }
101564
102288
  function applyToolConfig(params) {
101565
- const denyTodoTools = params.pluginConfig.experimental?.task_system ? { todowrite: "deny", todoread: "deny" } : {};
102289
+ const isTaskSystem = isTaskSystemEnabled(params.pluginConfig);
102290
+ const denyTodoTools = isTaskSystem ? { todowrite: "deny", todoread: "deny" } : {};
102291
+ const denyTaskTools = !isTaskSystem ? { "task_*": "deny", task: "deny" } : {};
101566
102292
  params.config.tools = {
101567
102293
  ...params.config.tools,
101568
102294
  "grep_app_*": false,
@@ -101571,7 +102297,7 @@ function applyToolConfig(params) {
101571
102297
  LspCodeActionResolve: false,
101572
102298
  "task_*": false,
101573
102299
  teammate: false,
101574
- ...params.pluginConfig.experimental?.task_system ? { todowrite: false, todoread: false } : {}
102300
+ ...isTaskSystem ? { todowrite: false, todoread: false } : { "task_*": false, task: false }
101575
102301
  };
101576
102302
  const isCliRunMode = process.env.OPENCODE_CLI_RUN_MODE === "true";
101577
102303
  const questionPermission = isCliRunMode ? "deny" : "allow";
@@ -101587,11 +102313,8 @@ function applyToolConfig(params) {
101587
102313
  if (architect) {
101588
102314
  architect.permission = {
101589
102315
  ...architect.permission,
101590
- task: "allow",
101591
102316
  delegate_agent: "deny",
101592
- "task_*": "allow",
101593
- teammate: "allow",
101594
- ...denyTodoTools
102317
+ ...isTaskSystem ? { task: "allow", "task_*": "allow", teammate: "allow", ...denyTodoTools } : { todowrite: "allow", todoread: "allow", ...denyTaskTools }
101595
102318
  };
101596
102319
  }
101597
102320
  const morpheus = agentByKey(params.agentResult, "morpheus");
@@ -101599,11 +102322,8 @@ function applyToolConfig(params) {
101599
102322
  morpheus.permission = {
101600
102323
  ...morpheus.permission,
101601
102324
  delegate_agent: "deny",
101602
- task: "allow",
101603
102325
  question: questionPermission,
101604
- "task_*": "allow",
101605
- teammate: "allow",
101606
- ...denyTodoTools
102326
+ ...isTaskSystem ? { task: "allow", "task_*": "allow", teammate: "allow", ...denyTodoTools } : { todowrite: "allow", todoread: "allow", ...denyTaskTools }
101607
102327
  };
101608
102328
  }
101609
102329
  const keymaker = agentByKey(params.agentResult, "keymaker");
@@ -101611,9 +102331,8 @@ function applyToolConfig(params) {
101611
102331
  keymaker.permission = {
101612
102332
  ...keymaker.permission,
101613
102333
  delegate_agent: "deny",
101614
- task: "allow",
101615
102334
  question: questionPermission,
101616
- ...denyTodoTools
102335
+ ...isTaskSystem ? { task: "allow", ...denyTodoTools } : { todowrite: "allow", todoread: "allow", ...denyTaskTools }
101617
102336
  };
101618
102337
  }
101619
102338
  const oracle = agentByKey(params.agentResult, "oracle");
@@ -101621,21 +102340,15 @@ function applyToolConfig(params) {
101621
102340
  oracle.permission = {
101622
102341
  ...oracle.permission,
101623
102342
  delegate_agent: "deny",
101624
- task: "allow",
101625
102343
  question: questionPermission,
101626
- "task_*": "allow",
101627
- teammate: "allow",
101628
- ...denyTodoTools
102344
+ ...isTaskSystem ? { task: "allow", "task_*": "allow", teammate: "allow", ...denyTodoTools } : { todowrite: "allow", todoread: "allow", ...denyTaskTools }
101629
102345
  };
101630
102346
  }
101631
102347
  const mouse = agentByKey(params.agentResult, "mouse");
101632
102348
  if (mouse) {
101633
102349
  mouse.permission = {
101634
102350
  ...mouse.permission,
101635
- task: "allow",
101636
- "task_*": "allow",
101637
- teammate: "allow",
101638
- ...denyTodoTools
102351
+ ...isTaskSystem ? { task: "allow", "task_*": "allow", teammate: "allow", ...denyTodoTools } : { todowrite: "allow", todoread: "allow", ...denyTaskTools }
101639
102352
  };
101640
102353
  }
101641
102354
  params.config.permission = {
@@ -101811,12 +102524,13 @@ function createToolRegistry(args) {
101811
102524
  skills: skillContext.builtinSkills,
101812
102525
  client: ctx.client
101813
102526
  });
101814
- const taskSystemEnabled = pluginConfig.experimental?.task_system ?? false;
102527
+ const taskSystemEnabled = isTaskSystemEnabled(pluginConfig);
101815
102528
  const taskToolsRecord = taskSystemEnabled ? {
101816
102529
  task_create: createTaskCreateTool(pluginConfig, ctx),
101817
- task_get: createTaskGetTool(pluginConfig),
101818
- task_list: createTaskList(pluginConfig),
101819
- task_update: createTaskUpdateTool(pluginConfig, ctx)
102530
+ task_get: createTaskGetTool(pluginConfig, ctx),
102531
+ task_list: createTaskList(pluginConfig, ctx),
102532
+ task_update: createTaskUpdateTool(pluginConfig, ctx),
102533
+ task_cleanup: createTaskCleanupTool(pluginConfig, ctx)
101820
102534
  } : {};
101821
102535
  const hashlineEnabled = pluginConfig.experimental?.hashline_edit ?? false;
101822
102536
  const hashlineToolsRecord = hashlineEnabled ? { edit: createHashlineEditTool(ctx) } : {};
@@ -102109,6 +102823,12 @@ var BuiltinCommandNameSchema = exports_external.enum([
102109
102823
  var CommentCheckerConfigSchema = exports_external.object({
102110
102824
  custom_prompt: exports_external.string().optional()
102111
102825
  });
102826
+ // src/config/schema/context-mode.ts
102827
+ var ContextModeConfigSchema = exports_external.object({
102828
+ enabled: exports_external.boolean().default(true),
102829
+ enforce: exports_external.boolean().default(false),
102830
+ blocked_tools: exports_external.array(exports_external.string()).default(["read", "grep", "glob"])
102831
+ });
102112
102832
  // src/config/schema/evolution.ts
102113
102833
  var EvolutionWatcherConfigSchema = exports_external.object({
102114
102834
  maxArgChars: exports_external.number().int().min(1).default(4000),
@@ -102182,7 +102902,7 @@ var ExperimentalConfigSchema = exports_external.object({
102182
102902
  auto_resume: exports_external.boolean().optional(),
102183
102903
  preemptive_compaction: exports_external.boolean().optional(),
102184
102904
  truncate_all_tool_outputs: exports_external.boolean().optional(),
102185
- task_system: exports_external.boolean().optional(),
102905
+ task_system: exports_external.boolean().optional().default(true),
102186
102906
  plugin_load_timeout_ms: exports_external.number().min(1000).optional(),
102187
102907
  safe_hook_creation: exports_external.boolean().optional(),
102188
102908
  hashline_edit: exports_external.boolean().optional()
@@ -102196,6 +102916,7 @@ var HeadroomConfigSchema = exports_external.object({
102196
102916
  });
102197
102917
  // src/config/schema/hooks.ts
102198
102918
  var HookNameSchema = exports_external.enum([
102919
+ "task-continuation-enforcer",
102199
102920
  "todo-continuation-enforcer",
102200
102921
  "context-window-monitor",
102201
102922
  "session-recovery",
@@ -102243,7 +102964,6 @@ var HookNameSchema = exports_external.enum([
102243
102964
  "env-file-write-guard",
102244
102965
  "json-error-recovery",
102245
102966
  "bash-file-read-guard",
102246
- "todo-description-override",
102247
102967
  "runtime-fallback",
102248
102968
  "read-image-resizer",
102249
102969
  "webfetch-redirect-guard",
@@ -102251,14 +102971,16 @@ var HookNameSchema = exports_external.enum([
102251
102971
  "tool-pair-validator",
102252
102972
  "quality-gate",
102253
102973
  "task-notepad",
102254
- "task-todo-mirror",
102255
102974
  "design-intent-preserver",
102256
102975
  "rtk-bash-rewriter",
102257
102976
  "mcp-startup-notification",
102258
102977
  "evolution-watcher",
102259
102978
  "evolution-compressor",
102260
102979
  "evolution-quality-gate",
102261
- "evolution-hitl"
102980
+ "evolution-hitl",
102981
+ "context-mode-enforcer",
102982
+ "task-edit-guard",
102983
+ "task-edit-guard"
102262
102984
  ]);
102263
102985
  // src/config/schema/matrix-loop.ts
102264
102986
  var MatrixLoopConfigSchema = exports_external.object({
@@ -102276,7 +102998,8 @@ var MatrixxSelfConfigSkillConfigSchema = exports_external.object({
102276
102998
  var MorpheusTasksConfigSchema = exports_external.object({
102277
102999
  storage_path: exports_external.string().optional(),
102278
103000
  task_list_id: exports_external.string().optional(),
102279
- claude_code_compat: exports_external.boolean().default(false)
103001
+ claude_code_compat: exports_external.boolean().default(false),
103002
+ scope: exports_external.enum(["global", "project"]).default("project").optional().describe("Task storage scope: project \u2192 .matrixx/tasks per project (default), global \u2192 ~/.config/opencode/tasks/{listId}")
102280
103003
  });
102281
103004
  var MorpheusConfigSchema = exports_external.object({
102282
103005
  tasks: MorpheusTasksConfigSchema.optional()
@@ -102446,6 +103169,7 @@ var MatrixxConfigSchema = exports_external.object({
102446
103169
  agent_definitions: AgentDefinitionsConfigSchema.optional(),
102447
103170
  matrixx_self_config: MatrixxSelfConfigSkillConfigSchema.optional(),
102448
103171
  headroom: HeadroomConfigSchema.optional(),
103172
+ context_mode: ContextModeConfigSchema.optional(),
102449
103173
  rtk: RtkConfigSchema.optional(),
102450
103174
  evolution: EvolutionConfigSchema.optional(),
102451
103175
  _migrations: exports_external.array(exports_external.string()).optional()
@@ -102637,7 +103361,6 @@ function createChatMessageHandler2(args) {
102637
103361
  applyAgentVariant(pluginConfig, input.agent, message);
102638
103362
  }
102639
103363
  }
102640
- await hooks2.taskTodoMirror?.["chat.message"]?.(input, output);
102641
103364
  await hooks2.stopContinuationGuard?.["chat.message"]?.(input);
102642
103365
  await hooks2.keywordDetector?.["chat.message"]?.(input, output);
102643
103366
  await hooks2.autoSlashCommand?.["chat.message"]?.(input, output);
@@ -102787,8 +103510,8 @@ function createEventHandler2(args) {
102787
103510
  await Promise.resolve(hooks2.autoUpdateChecker?.event?.(input));
102788
103511
  await Promise.resolve(hooks2.backgroundNotificationHook?.event?.(input));
102789
103512
  await Promise.resolve(hooks2.sessionNotification?.(input));
102790
- await Promise.resolve(hooks2.taskTodoMirror?.event?.(input));
102791
103513
  await Promise.resolve(hooks2.todoContinuationEnforcer?.handler?.(input));
103514
+ await Promise.resolve(hooks2.taskContinuationEnforcer?.handler?.(input));
102792
103515
  await Promise.resolve(hooks2.unstableAgentBabysitter?.event?.(input));
102793
103516
  await Promise.resolve(hooks2.contextWindowMonitor?.event?.(input));
102794
103517
  await Promise.resolve(hooks2.directoryAgentsInjector?.event?.(input));
@@ -102938,7 +103661,6 @@ function setCachedTransform(sessionID, messagesHash, messages2) {
102938
103661
  }
102939
103662
  function createMessagesTransformHandler(args) {
102940
103663
  return async (input, output) => {
102941
- await args.hooks.taskTodoMirror?.["experimental.chat.messages.transform"]?.(input, output);
102942
103664
  const sessionID = extractSessionID(output.messages);
102943
103665
  if (!sessionID) {
102944
103666
  await args.hooks.contextInjectorMessagesTransform?.["experimental.chat.messages.transform"]?.(input, output);
@@ -102962,9 +103684,7 @@ function createMessagesTransformHandler(args) {
102962
103684
  // src/plugin/tool-definition.ts
102963
103685
  function createToolDefinitionHandler(args) {
102964
103686
  const { hooks: hooks2 } = args;
102965
- return async (input, output) => {
102966
- await hooks2.todoDescriptionOverride?.["tool.definition"]?.(input, output);
102967
- };
103687
+ return async (input, output) => {};
102968
103688
  }
102969
103689
 
102970
103690
  // src/plugin/tool-execute-after.ts
@@ -103001,7 +103721,6 @@ function createToolExecuteAfterHandler2(args) {
103001
103721
  await hooks2.jsonErrorRecovery?.["tool.execute.after"]?.(input, output);
103002
103722
  await hooks2.readImageResizer?.["tool.execute.after"]?.(input, output);
103003
103723
  await hooks2.taskNotepad?.["tool.execute.after"]?.(input, output);
103004
- await hooks2.taskTodoMirror?.["tool.execute.after"]?.(input, output);
103005
103724
  };
103006
103725
  await Promise.all([
103007
103726
  hooks2.preemptiveCompaction?.["tool.execute.after"]?.(input, output),
@@ -103013,22 +103732,22 @@ function createToolExecuteAfterHandler2(args) {
103013
103732
 
103014
103733
  // src/features/assembly-state/manager.ts
103015
103734
  init_logger();
103016
- var HOOK_NAME17 = "assembly-state";
103735
+ var HOOK_NAME19 = "assembly-state";
103017
103736
  var disabledSessions = new Set;
103018
103737
  function isAssemblyDisabled(sessionID) {
103019
103738
  return disabledSessions.has(sessionID);
103020
103739
  }
103021
103740
  function disableAssembly(sessionID) {
103022
103741
  disabledSessions.add(sessionID);
103023
- log(`[${HOOK_NAME17}] Assembly disabled for session`, { sessionID });
103742
+ log(`[${HOOK_NAME19}] Assembly disabled for session`, { sessionID });
103024
103743
  disabledSessions.add(sessionID);
103025
- log(`[${HOOK_NAME17}] Consensus disabled for session`, { sessionID });
103744
+ log(`[${HOOK_NAME19}] Consensus disabled for session`, { sessionID });
103026
103745
  }
103027
103746
  function enableAssembly(sessionID) {
103028
103747
  disabledSessions.delete(sessionID);
103029
- log(`[${HOOK_NAME17}] Assembly enabled for session`, { sessionID });
103748
+ log(`[${HOOK_NAME19}] Assembly enabled for session`, { sessionID });
103030
103749
  disabledSessions.delete(sessionID);
103031
- log(`[${HOOK_NAME17}] Consensus enabled for session`, { sessionID });
103750
+ log(`[${HOOK_NAME19}] Consensus enabled for session`, { sessionID });
103032
103751
  }
103033
103752
  // src/plugin/session-agent-resolver.ts
103034
103753
  async function resolveSessionAgent(client2, sessionId, timeoutMs = 5000) {
@@ -103071,6 +103790,7 @@ function createToolExecuteBeforeHandler2(args) {
103071
103790
  const writeExistingFileGuardHook = hooks2.writeExistingFileGuard?.["tool.execute.before"];
103072
103791
  const tasksTodowriteDisablerHook = hooks2.tasksTodowriteDisabler?.["tool.execute.before"];
103073
103792
  const oracleMdOnlyHook = hooks2.oracleMdOnly?.["tool.execute.before"];
103793
+ const contextModeEnforcerHook = hooks2.contextModeEnforcer?.["tool.execute.before"];
103074
103794
  const nonInteractiveEnvHook = hooks2.nonInteractiveEnv?.["tool.execute.before"];
103075
103795
  const bashFileReadGuardHook = hooks2.bashFileReadGuard?.["tool.execute.before"];
103076
103796
  const questionLabelTruncatorHook = hooks2.questionLabelTruncator?.["tool.execute.before"];
@@ -103090,7 +103810,8 @@ function createToolExecuteBeforeHandler2(args) {
103090
103810
  envFileWriteGuardHook?.(input, output),
103091
103811
  writeExistingFileGuardHook?.(input, output),
103092
103812
  tasksTodowriteDisablerHook?.(input, output),
103093
- oracleMdOnlyHook?.(input, output)
103813
+ oracleMdOnlyHook?.(input, output),
103814
+ contextModeEnforcerHook?.(input, output)
103094
103815
  ]);
103095
103816
  let resolvePromise;
103096
103817
  if (input.tool === "task") {