opencode-matrixx 2.6.0 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +3 -0
  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 +18719 -663
  25. package/dist/config/schema/experimental.d.ts +1 -1
  26. package/dist/config/schema/hooks.d.ts +2 -2
  27. package/dist/config/schema/matrixx-config.d.ts +7 -3
  28. package/dist/config/schema/morpheus.d.ts +8 -0
  29. package/dist/create-hooks.d.ts +2 -2
  30. package/dist/features/builtin-commands/templates/cleanup-tasks.d.ts +1 -0
  31. package/dist/features/builtin-commands/templates/task-list.d.ts +1 -0
  32. package/dist/features/builtin-commands/types.d.ts +1 -1
  33. package/dist/features/task-storage/storage.d.ts +12 -2
  34. package/dist/features/task-storage/types.d.ts +1 -0
  35. package/dist/hooks/compaction-todo-preserver/hook.d.ts +13 -0
  36. package/dist/hooks/compaction-todo-preserver/index.d.ts +1 -1
  37. package/dist/hooks/index.d.ts +2 -2
  38. package/dist/hooks/task-continuation-enforcer/abort-detection.d.ts +4 -0
  39. package/dist/hooks/task-continuation-enforcer/constants.d.ts +10 -0
  40. package/dist/hooks/task-continuation-enforcer/continuation-injection.d.ts +12 -0
  41. package/dist/hooks/task-continuation-enforcer/countdown.d.ts +14 -0
  42. package/dist/hooks/task-continuation-enforcer/handler.d.ts +16 -0
  43. package/dist/hooks/task-continuation-enforcer/idle-event.d.ts +11 -0
  44. package/dist/hooks/task-continuation-enforcer/index.d.ts +6 -0
  45. package/dist/hooks/task-continuation-enforcer/message-directory.d.ts +1 -0
  46. package/dist/hooks/task-continuation-enforcer/non-idle-events.d.ts +6 -0
  47. package/dist/hooks/task-continuation-enforcer/session-state.d.ts +10 -0
  48. package/dist/hooks/task-continuation-enforcer/todo.d.ts +5 -0
  49. package/dist/hooks/task-continuation-enforcer/types.d.ts +60 -0
  50. package/dist/hooks/task-edit-guard/constants.d.ts +2 -0
  51. package/dist/hooks/task-edit-guard/hook.d.ts +2 -0
  52. package/dist/hooks/task-edit-guard/index.d.ts +2 -0
  53. package/dist/index.js +1445 -820
  54. package/dist/matrixx.schema.json +16 -3
  55. package/dist/plugin/hooks/create-continuation-hooks.d.ts +2 -1
  56. package/dist/plugin/hooks/create-core-hooks.d.ts +1 -2
  57. package/dist/plugin/hooks/create-tool-guard-hooks.d.ts +2 -3
  58. package/dist/shared/system-directive.d.ts +1 -0
  59. package/dist/shared/task-system-gating.d.ts +3 -0
  60. package/dist/shared/task-system-gating.test.d.ts +1 -0
  61. package/dist/tools/index.d.ts +1 -1
  62. package/dist/tools/task/index.d.ts +1 -1
  63. package/dist/tools/task/task-cleanup.d.ts +4 -0
  64. package/dist/tools/task/task-cleanup.test.d.ts +1 -0
  65. package/dist/tools/task/task-get.d.ts +2 -1
  66. package/dist/tools/task/task-list.d.ts +2 -1
  67. package/dist/tools/task/types.d.ts +2 -0
  68. package/package.json +3 -2
  69. package/dist/hooks/task-todo-mirror/constants.d.ts +0 -3
  70. package/dist/hooks/task-todo-mirror/hook.d.ts +0 -43
  71. package/dist/hooks/task-todo-mirror/index.d.ts +0 -2
  72. package/dist/hooks/todo-description-override/description.d.ts +0 -1
  73. package/dist/hooks/todo-description-override/hook.d.ts +0 -8
  74. package/dist/hooks/todo-description-override/index.d.ts +0 -1
  75. package/dist/tools/task/todo-sync.d.ts +0 -16
package/dist/index.js CHANGED
@@ -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 }) => {
@@ -61823,13 +61937,11 @@ function createReadImageResizerHook(_ctx) {
61823
61937
  }
61824
61938
  // src/hooks/rtk-bash-rewriter/constants.ts
61825
61939
  var RTK_BINARY_NAME = "rtk";
61826
- var DEFAULT_TIMEOUT_MS = 5000;
61827
61940
  var RTK_REWRITE_COMMAND = "rewrite";
61828
61941
 
61829
61942
  // src/hooks/rtk-bash-rewriter/hook.ts
61830
61943
  function createRtkBashRewriterHook(_ctx, config2) {
61831
61944
  const rtkConfig = config2.rtk;
61832
- const _timeoutMs = rtkConfig?.timeout_ms ?? DEFAULT_TIMEOUT_MS;
61833
61945
  const binaryPath = rtkConfig?.binary_path ?? RTK_BINARY_NAME;
61834
61946
  const hasRtk = Bun.which(binaryPath) !== null;
61835
61947
  if (!hasRtk) {
@@ -64916,216 +65028,54 @@ function createStopContinuationGuardHook(_ctx, options) {
64916
65028
  clear
64917
65029
  };
64918
65030
  }
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
65031
+ // src/hooks/task-continuation-enforcer/index.ts
64931
65032
  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
65033
 
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
-
65060
- ## Questions
65034
+ // src/hooks/task-continuation-enforcer/constants.ts
65035
+ var HOOK_NAME13 = "task-continuation-enforcer";
65036
+ var DEFAULT_SKIP_AGENTS = ["oracle", "compaction"];
65037
+ var CONTINUATION_PROMPT2 = `${createSystemDirective(SystemDirectiveTypes.TASK_CONTINUATION)}
65061
65038
 
65062
- (Open questions to revisit later. Don't lose them when context compacts.)
65039
+ Incomplete Matrixx tasks remain. Continue working on the next pending task.
65063
65040
 
65064
- ## Results
65041
+ - Proceed without asking for permission
65042
+ - Mark each task in_progress before starting, completed immediately after
65043
+ - Respect blockedBy dependencies (skip blocked tasks)
65044
+ - Do not stop until all tasks are done`;
65045
+ var COUNTDOWN_SECONDS = 2;
65046
+ var TOAST_DURATION_MS = 900;
65047
+ var COUNTDOWN_GRACE_PERIOD_MS = 500;
65048
+ var ABORT_WINDOW_MS = 3000;
65049
+ var CONTINUATION_COOLDOWN_MS2 = 30000;
65050
+ var MAX_CONSECUTIVE_FAILURES = 5;
65051
+ var FAILURE_RESET_WINDOW_MS = 5 * 60 * 1000;
65065
65052
 
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() + `
65053
+ // src/hooks/task-continuation-enforcer/handler.ts
65054
+ init_logger();
65099
65055
 
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";
65056
+ // src/hooks/task-continuation-enforcer/idle-event.ts
65057
+ import { existsSync as existsSync44, readdirSync as readdirSync15 } from "fs";
65115
65058
 
65116
65059
  // src/features/task-storage/storage.ts
65060
+ init_logger();
65117
65061
  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 = {}) {
65062
+ import { existsSync as existsSync42, mkdirSync as mkdirSync15, readdirSync as readdirSync13, readFileSync as readFileSync32, renameSync as renameSync3, unlinkSync as unlinkSync9, writeFileSync as writeFileSync18 } from "fs";
65063
+ import { basename as basename5, dirname as dirname14, isAbsolute as isAbsolute3, join as join51 } from "path";
65064
+ function getProjectTaskDir(directory) {
65065
+ return join51(directory, ".matrixx", "tasks");
65066
+ }
65067
+ function getTaskDir(config2 = {}, directory) {
65121
65068
  const tasksConfig = config2.morpheus?.tasks;
65122
65069
  const storagePath = tasksConfig?.storage_path;
65123
65070
  if (storagePath) {
65124
- return isAbsolute3(storagePath) ? storagePath : join52(process.cwd(), storagePath);
65071
+ return isAbsolute3(storagePath) ? storagePath : join51(directory ?? process.cwd(), storagePath);
65125
65072
  }
65126
- const configDir = getOpenCodeConfigDir({ binary: "opencode" });
65127
- const listId = resolveTaskListId(config2);
65128
- return join52(configDir, "tasks", listId);
65073
+ if (tasksConfig?.scope === "global" || !directory) {
65074
+ const configDir = getOpenCodeConfigDir({ binary: "opencode" });
65075
+ const listId = resolveTaskListId(config2);
65076
+ return join51(configDir, "tasks", listId);
65077
+ }
65078
+ return getProjectTaskDir(directory);
65129
65079
  }
65130
65080
  function sanitizePathSegment(value) {
65131
65081
  return value.replace(/[^a-zA-Z0-9_-]/g, "-") || "default";
@@ -65143,16 +65093,16 @@ function resolveTaskListId(config2 = {}) {
65143
65093
  return sanitizePathSegment(basename5(process.cwd()));
65144
65094
  }
65145
65095
  function ensureDir2(dirPath) {
65146
- if (!existsSync43(dirPath)) {
65147
- mkdirSync16(dirPath, { recursive: true });
65096
+ if (!existsSync42(dirPath)) {
65097
+ mkdirSync15(dirPath, { recursive: true });
65148
65098
  }
65149
65099
  }
65150
65100
  function readJsonSafe(filePath, schema) {
65151
65101
  try {
65152
- if (!existsSync43(filePath)) {
65102
+ if (!existsSync42(filePath)) {
65153
65103
  return null;
65154
65104
  }
65155
- const content = readFileSync33(filePath, "utf-8");
65105
+ const content = readFileSync32(filePath, "utf-8");
65156
65106
  const parsed = JSON.parse(content);
65157
65107
  const result = schema.safeParse(parsed);
65158
65108
  if (!result.success) {
@@ -65164,15 +65114,15 @@ function readJsonSafe(filePath, schema) {
65164
65114
  }
65165
65115
  }
65166
65116
  function writeJsonAtomic(filePath, data) {
65167
- const dir = dirname15(filePath);
65117
+ const dir = dirname14(filePath);
65168
65118
  ensureDir2(dir);
65169
65119
  const tempPath = `${filePath}.tmp.${Date.now()}`;
65170
65120
  try {
65171
- writeFileSync19(tempPath, JSON.stringify(data, null, 2), "utf-8");
65121
+ writeFileSync18(tempPath, JSON.stringify(data, null, 2), "utf-8");
65172
65122
  renameSync3(tempPath, filePath);
65173
65123
  } catch (error51) {
65174
65124
  try {
65175
- if (existsSync43(tempPath)) {
65125
+ if (existsSync42(tempPath)) {
65176
65126
  unlinkSync9(tempPath);
65177
65127
  }
65178
65128
  } catch {}
@@ -65180,21 +65130,73 @@ function writeJsonAtomic(filePath, data) {
65180
65130
  }
65181
65131
  }
65182
65132
  var STALE_LOCK_THRESHOLD_MS = 30000;
65133
+ async function acquireLockWithRetry(dirPath, retries = 4) {
65134
+ for (let attempt = 0;attempt < retries; attempt++) {
65135
+ const lock = acquireLock(dirPath);
65136
+ if (lock.acquired)
65137
+ return lock;
65138
+ if (attempt < retries - 1) {
65139
+ await new Promise((resolve7) => setTimeout(resolve7, 15 * 2 ** attempt));
65140
+ }
65141
+ }
65142
+ return { acquired: false, release: () => {} };
65143
+ }
65183
65144
  function generateTaskId() {
65184
65145
  return `T-${randomUUID()}`;
65185
65146
  }
65147
+ function migrateLegacyTasksIfNeeded(config2, directory) {
65148
+ const projectDir = getProjectTaskDir(directory);
65149
+ const legacyDir = join51(getOpenCodeConfigDir({ binary: "opencode" }), "tasks", resolveTaskListId(config2));
65150
+ if (existsSync42(projectDir)) {
65151
+ try {
65152
+ const existing = readdirSync13(projectDir).filter((f) => f.startsWith("T-") && f.endsWith(".json"));
65153
+ if (existing.length > 0) {
65154
+ return { migrated: 0, legacyDir, projectDir };
65155
+ }
65156
+ } catch {}
65157
+ }
65158
+ if (!existsSync42(legacyDir)) {
65159
+ return { migrated: 0, legacyDir, projectDir };
65160
+ }
65161
+ let files = [];
65162
+ try {
65163
+ files = readdirSync13(legacyDir).filter((f) => f.startsWith("T-") && f.endsWith(".json"));
65164
+ } catch {
65165
+ return { migrated: 0, legacyDir, projectDir };
65166
+ }
65167
+ if (files.length === 0) {
65168
+ return { migrated: 0, legacyDir, projectDir };
65169
+ }
65170
+ ensureDir2(projectDir);
65171
+ let migrated = 0;
65172
+ for (const file2 of files) {
65173
+ const src = join51(legacyDir, file2);
65174
+ const dest = join51(projectDir, file2);
65175
+ if (existsSync42(dest))
65176
+ continue;
65177
+ try {
65178
+ const content = readFileSync32(src, "utf-8");
65179
+ writeFileSync18(dest, content, "utf-8");
65180
+ migrated++;
65181
+ } catch {}
65182
+ }
65183
+ if (migrated > 0) {
65184
+ log(`[task-storage] Migrated ${migrated} legacy tasks from ${legacyDir} to ${projectDir}`);
65185
+ }
65186
+ return { migrated, legacyDir, projectDir };
65187
+ }
65186
65188
  function acquireLock(dirPath) {
65187
- const lockPath = join52(dirPath, ".lock");
65189
+ const lockPath = join51(dirPath, ".lock");
65188
65190
  const lockId = randomUUID();
65189
65191
  const createLock = (timestamp) => {
65190
- writeFileSync19(lockPath, JSON.stringify({ id: lockId, timestamp }), {
65192
+ writeFileSync18(lockPath, JSON.stringify({ id: lockId, timestamp }), {
65191
65193
  encoding: "utf-8",
65192
65194
  flag: "wx"
65193
65195
  });
65194
65196
  };
65195
65197
  const isStale = () => {
65196
65198
  try {
65197
- const lockContent = readFileSync33(lockPath, "utf-8");
65199
+ const lockContent = readFileSync32(lockPath, "utf-8");
65198
65200
  const lockData = JSON.parse(lockContent);
65199
65201
  const lockAge = Date.now() - lockData.timestamp;
65200
65202
  return lockAge > STALE_LOCK_THRESHOLD_MS;
@@ -65232,9 +65234,9 @@ function acquireLock(dirPath) {
65232
65234
  acquired: true,
65233
65235
  release: () => {
65234
65236
  try {
65235
- if (!existsSync43(lockPath))
65237
+ if (!existsSync42(lockPath))
65236
65238
  return;
65237
- const lockContent = readFileSync33(lockPath, "utf-8");
65239
+ const lockContent = readFileSync32(lockPath, "utf-8");
65238
65240
  const lockData = JSON.parse(lockContent);
65239
65241
  if (lockData.id !== lockId)
65240
65242
  return;
@@ -65244,239 +65246,9 @@ function acquireLock(dirPath) {
65244
65246
  };
65245
65247
  }
65246
65248
 
65247
- // src/hooks/task-todo-mirror/hook.ts
65249
+ // src/hooks/task-continuation-enforcer/idle-event.ts
65248
65250
  init_logger();
65249
65251
 
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
65252
  // src/tools/task/types.ts
65481
65253
  var TaskStatusSchema = exports_external.enum(["pending", "in_progress", "completed", "deleted"]);
65482
65254
  var TaskObjectSchema = exports_external.object({
@@ -65491,7 +65263,8 @@ var TaskObjectSchema = exports_external.object({
65491
65263
  metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
65492
65264
  repoURL: exports_external.string().optional(),
65493
65265
  parentID: exports_external.string().optional(),
65494
- threadID: exports_external.string()
65266
+ threadID: exports_external.string(),
65267
+ projectRoot: exports_external.string().optional()
65495
65268
  }).strict();
65496
65269
  var TaskCreateInputSchema = exports_external.object({
65497
65270
  subject: exports_external.string(),
@@ -65528,82 +65301,842 @@ var TaskDeleteInputSchema = exports_external.object({
65528
65301
  id: exports_external.string()
65529
65302
  });
65530
65303
 
65531
- // src/hooks/task-todo-mirror/hook.ts
65532
- function loadAllTasks(pluginConfig) {
65304
+ // src/hooks/task-continuation-enforcer/abort-detection.ts
65305
+ function isLastAssistantMessageAborted(messages) {
65306
+ if (!messages || messages.length === 0)
65307
+ return false;
65308
+ for (let i = messages.length - 1;i >= 0; i--) {
65309
+ const info = messages[i].info;
65310
+ if (info?.role !== "assistant")
65311
+ continue;
65312
+ const errorName = info.error?.name;
65313
+ if (!errorName)
65314
+ return false;
65315
+ return errorName === "MessageAbortedError" || errorName === "AbortError";
65316
+ }
65317
+ return false;
65318
+ }
65319
+
65320
+ // src/hooks/task-continuation-enforcer/countdown.ts
65321
+ init_logger();
65322
+
65323
+ // src/hooks/task-continuation-enforcer/continuation-injection.ts
65324
+ import { existsSync as existsSync43, readdirSync as readdirSync14 } from "fs";
65325
+ init_logger();
65326
+ // src/hooks/task-continuation-enforcer/todo.ts
65327
+ function getIncompleteTaskCount(tasks) {
65328
+ const byId = new Map(tasks.map((t) => [t.id, t]));
65329
+ return tasks.filter((task) => {
65330
+ if (task.status !== "pending" && task.status !== "in_progress")
65331
+ return false;
65332
+ if (task.blockedBy.length === 0)
65333
+ return true;
65334
+ return task.blockedBy.every((bid) => byId.get(bid)?.status === "completed");
65335
+ }).length;
65336
+ }
65337
+
65338
+ // src/hooks/task-continuation-enforcer/continuation-injection.ts
65339
+ function hasWritePermission(tools) {
65340
+ const editPermission = tools?.edit;
65341
+ const writePermission = tools?.write;
65342
+ return !tools || editPermission !== false && editPermission !== "deny" && writePermission !== false && writePermission !== "deny";
65343
+ }
65344
+ async function injectContinuation(args) {
65345
+ const {
65346
+ ctx,
65347
+ sessionID,
65348
+ backgroundManager,
65349
+ skipAgents = DEFAULT_SKIP_AGENTS,
65350
+ resolvedInfo,
65351
+ sessionStateStore
65352
+ } = args;
65353
+ const state3 = sessionStateStore.getExistingState(sessionID);
65354
+ if (state3?.isRecovering) {
65355
+ log(`[${HOOK_NAME13}] Skipped injection: in recovery`, { sessionID });
65356
+ return;
65357
+ }
65358
+ const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((task) => task.status === "running") : false;
65359
+ if (hasRunningBgTasks) {
65360
+ log(`[${HOOK_NAME13}] Skipped injection: background tasks running`, { sessionID });
65361
+ return;
65362
+ }
65363
+ const tasks = [];
65364
+ let total = 0;
65533
65365
  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);
65366
+ const taskDir = getTaskDir({}, ctx.directory);
65367
+ if (!existsSync43(taskDir)) {
65368
+ log(`[${HOOK_NAME13}] Skipped injection: no task dir`, { sessionID, taskDir });
65369
+ return;
65370
+ }
65371
+ const files = readdirSync14(taskDir).filter((f) => f.startsWith("T-") && f.endsWith(".json"));
65372
+ for (const f of files) {
65373
+ const parsed = readJsonSafe(`${taskDir}/${f}`, TaskObjectSchema);
65374
+ if (parsed)
65375
+ tasks.push(parsed);
65376
+ }
65377
+ total = tasks.length;
65378
+ if (total === 0) {
65379
+ log(`[${HOOK_NAME13}] Skipped injection: no tasks`, { sessionID });
65380
+ return;
65381
+ }
65382
+ } catch (error51) {
65383
+ log(`[${HOOK_NAME13}] Failed to fetch tasks`, { sessionID, error: String(error51) });
65384
+ return;
65385
+ }
65386
+ const freshIncompleteCount = getIncompleteTaskCount(tasks);
65387
+ if (freshIncompleteCount === 0) {
65388
+ log(`[${HOOK_NAME13}] Skipped injection: no incomplete tasks`, { sessionID, total });
65389
+ return;
65390
+ }
65391
+ let agentName = resolvedInfo?.agent;
65392
+ let model = resolvedInfo?.model;
65393
+ let tools = resolvedInfo?.tools;
65394
+ if (!agentName || !model) {
65395
+ let previousMessage = null;
65396
+ if (isSqliteBackend()) {
65397
+ previousMessage = await findNearestMessageWithFieldsFromSDK(ctx.client, sessionID);
65398
+ } else {
65399
+ const messageDir = getMessageDir(sessionID);
65400
+ previousMessage = messageDir ? findNearestMessageWithFields(messageDir) : null;
65401
+ }
65402
+ agentName = agentName ?? previousMessage?.agent;
65403
+ model = model ?? (previousMessage?.model?.providerID && previousMessage?.model?.modelID ? {
65404
+ providerID: previousMessage.model.providerID,
65405
+ modelID: previousMessage.model.modelID,
65406
+ ...previousMessage.model.variant ? { variant: previousMessage.model.variant } : {}
65407
+ } : undefined);
65408
+ tools = tools ?? previousMessage?.tools;
65409
+ }
65410
+ if (agentName && skipAgents.some((s) => getAgentConfigKey(s) === getAgentConfigKey(agentName))) {
65411
+ log(`[${HOOK_NAME13}] Skipped: agent in skipAgents list`, { sessionID, agent: agentName });
65412
+ return;
65413
+ }
65414
+ if (!hasWritePermission(tools)) {
65415
+ log(`[${HOOK_NAME13}] Skipped: agent lacks write permission`, { sessionID, agent: agentName });
65416
+ return;
65417
+ }
65418
+ const byId = new Map(tasks.map((t) => [t.id, t]));
65419
+ const incompleteTasks = tasks.filter((task) => {
65420
+ if (task.status !== "pending" && task.status !== "in_progress")
65421
+ return false;
65422
+ if (task.blockedBy.length === 0)
65423
+ return true;
65424
+ return task.blockedBy.every((bid) => byId.get(bid)?.status === "completed");
65425
+ });
65426
+ const taskList = incompleteTasks.map((task) => `- [${task.status}] ${task.subject} (${task.id})`).join(`
65427
+ `);
65428
+ const prompt = `${CONTINUATION_PROMPT2}
65429
+
65430
+ [Status: ${total - freshIncompleteCount}/${total} completed, ${freshIncompleteCount} remaining]
65431
+
65432
+ Remaining Matrixx tasks:
65433
+ ${taskList}`;
65434
+ const injectionState = sessionStateStore.getExistingState(sessionID);
65435
+ if (injectionState) {
65436
+ injectionState.inFlight = true;
65437
+ }
65438
+ try {
65439
+ log(`[${HOOK_NAME13}] Injecting continuation`, {
65440
+ sessionID,
65441
+ agent: agentName,
65442
+ model,
65443
+ incompleteCount: freshIncompleteCount
65444
+ });
65445
+ await ctx.client.session.promptAsync({
65446
+ path: { id: sessionID },
65447
+ body: {
65448
+ agent: agentName,
65449
+ ...model !== undefined ? { model } : {},
65450
+ parts: [{ type: "text", text: prompt }]
65451
+ },
65452
+ query: { directory: ctx.directory }
65453
+ });
65454
+ log(`[${HOOK_NAME13}] Injection successful`, { sessionID });
65455
+ if (injectionState) {
65456
+ injectionState.inFlight = false;
65457
+ injectionState.lastInjectedAt = Date.now();
65458
+ injectionState.consecutiveFailures = 0;
65459
+ }
65460
+ } catch (error51) {
65461
+ log(`[${HOOK_NAME13}] Injection failed`, { sessionID, error: String(error51) });
65462
+ if (injectionState) {
65463
+ injectionState.inFlight = false;
65464
+ injectionState.lastInjectedAt = Date.now();
65465
+ injectionState.consecutiveFailures = (injectionState.consecutiveFailures ?? 0) + 1;
65543
65466
  }
65544
- return tasks;
65545
- } catch (err) {
65546
- log(`[${HOOK_NAME14}] Failed to load tasks`, { error: String(err) });
65547
- return [];
65548
65467
  }
65549
65468
  }
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)
65469
+
65470
+ // src/hooks/task-continuation-enforcer/countdown.ts
65471
+ async function showCountdownToast(ctx, seconds, incompleteCount) {
65472
+ await ctx.client.tui.showToast({
65473
+ body: {
65474
+ title: "Task Continuation",
65475
+ message: `Resuming in ${seconds}s... (${incompleteCount} tasks remaining)`,
65476
+ variant: "warning",
65477
+ duration: TOAST_DURATION_MS
65478
+ }
65479
+ }).catch((err) => {
65480
+ log("[task-continuation] Countdown toast failed:", err);
65481
+ });
65482
+ }
65483
+ function startCountdown(args) {
65484
+ const {
65485
+ ctx,
65486
+ sessionID,
65487
+ incompleteCount,
65488
+ resolvedInfo,
65489
+ backgroundManager,
65490
+ skipAgents,
65491
+ sessionStateStore
65492
+ } = args;
65493
+ const state3 = sessionStateStore.getState(sessionID);
65494
+ sessionStateStore.cancelCountdown(sessionID);
65495
+ let secondsRemaining = COUNTDOWN_SECONDS;
65496
+ showCountdownToast(ctx, secondsRemaining, incompleteCount);
65497
+ state3.countdownStartedAt = Date.now();
65498
+ state3.countdownInterval = setInterval(() => {
65499
+ secondsRemaining--;
65500
+ if (secondsRemaining > 0) {
65501
+ showCountdownToast(ctx, secondsRemaining, incompleteCount);
65502
+ }
65503
+ }, 1000);
65504
+ state3.countdownTimer = setTimeout(() => {
65505
+ sessionStateStore.cancelCountdown(sessionID);
65506
+ injectContinuation({
65507
+ ctx,
65508
+ sessionID,
65509
+ backgroundManager,
65510
+ skipAgents,
65511
+ resolvedInfo,
65512
+ sessionStateStore
65513
+ });
65514
+ }, COUNTDOWN_SECONDS * 1000);
65515
+ log(`[${HOOK_NAME13}] Countdown started`, {
65516
+ sessionID,
65517
+ seconds: COUNTDOWN_SECONDS,
65518
+ incompleteCount
65519
+ });
65520
+ }
65521
+
65522
+ // src/hooks/task-continuation-enforcer/idle-event.ts
65523
+ async function handleSessionIdle(args) {
65524
+ const {
65525
+ ctx,
65526
+ sessionID,
65527
+ sessionStateStore,
65528
+ backgroundManager,
65529
+ skipAgents = DEFAULT_SKIP_AGENTS,
65530
+ isContinuationStopped
65531
+ } = args;
65532
+ log(`[${HOOK_NAME13}] session.idle`, { sessionID });
65533
+ const state3 = sessionStateStore.getState(sessionID);
65534
+ if (state3.isRecovering) {
65535
+ log(`[${HOOK_NAME13}] Skipped: in recovery`, { sessionID });
65536
+ return;
65537
+ }
65538
+ if (state3.abortDetectedAt) {
65539
+ const timeSinceAbort = Date.now() - state3.abortDetectedAt;
65540
+ if (timeSinceAbort < ABORT_WINDOW_MS) {
65541
+ log(`[${HOOK_NAME13}] Skipped: abort detected via event ${timeSinceAbort}ms ago`, { sessionID });
65542
+ state3.abortDetectedAt = undefined;
65543
+ return;
65544
+ }
65545
+ state3.abortDetectedAt = undefined;
65546
+ }
65547
+ const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((task) => task.status === "running") : false;
65548
+ if (hasRunningBgTasks) {
65549
+ log(`[${HOOK_NAME13}] Skipped: background tasks running`, { sessionID });
65550
+ return;
65551
+ }
65552
+ try {
65553
+ const messagesResp = await ctx.client.session.messages({
65554
+ path: { id: sessionID },
65555
+ query: { directory: ctx.directory }
65556
+ });
65557
+ const messages = normalizeSDKResponse(messagesResp, []);
65558
+ if (isLastAssistantMessageAborted(messages)) {
65559
+ log(`[${HOOK_NAME13}] Skipped: last assistant message was aborted (API fallback)`, { sessionID });
65560
+ return;
65561
+ }
65562
+ } catch (error51) {
65563
+ log(`[${HOOK_NAME13}] Messages fetch failed, continuing`, { sessionID, error: String(error51) });
65564
+ }
65565
+ let incompleteCount = 0;
65566
+ let total = 0;
65567
+ try {
65568
+ const taskDir = getTaskDir({}, ctx.directory);
65569
+ if (!existsSync44(taskDir)) {
65570
+ log(`[${HOOK_NAME13}] No task dir`, { sessionID, taskDir });
65571
+ return;
65572
+ }
65573
+ const files = readdirSync15(taskDir).filter((f) => f.startsWith("T-") && f.endsWith(".json"));
65574
+ const tasks = [];
65575
+ for (const f of files) {
65576
+ const parsed = readJsonSafe(`${taskDir}/${f}`, TaskObjectSchema);
65577
+ if (parsed)
65578
+ tasks.push(parsed);
65579
+ }
65580
+ total = tasks.length;
65581
+ if (total === 0) {
65582
+ log(`[${HOOK_NAME13}] No tasks`, { sessionID });
65555
65583
  return;
65584
+ }
65585
+ incompleteCount = getIncompleteTaskCount(tasks);
65586
+ } catch (error51) {
65587
+ log(`[${HOOK_NAME13}] Task fetch failed`, { sessionID, error: String(error51) });
65588
+ return;
65589
+ }
65590
+ if (incompleteCount === 0) {
65591
+ log(`[${HOOK_NAME13}] All tasks complete`, { sessionID, total });
65592
+ return;
65593
+ }
65594
+ if (state3.inFlight) {
65595
+ log(`[${HOOK_NAME13}] Skipped: injection in flight`, { sessionID });
65596
+ return;
65597
+ }
65598
+ if ((state3.consecutiveFailures ?? 0) >= MAX_CONSECUTIVE_FAILURES && state3.lastInjectedAt && Date.now() - state3.lastInjectedAt >= FAILURE_RESET_WINDOW_MS) {
65599
+ state3.consecutiveFailures = 0;
65600
+ log(`[${HOOK_NAME13}] Reset consecutive failures after recovery window`, {
65601
+ sessionID,
65602
+ failureResetWindowMs: FAILURE_RESET_WINDOW_MS
65603
+ });
65604
+ }
65605
+ if ((state3.consecutiveFailures ?? 0) >= MAX_CONSECUTIVE_FAILURES) {
65606
+ log(`[${HOOK_NAME13}] Skipped: max consecutive failures reached`, {
65607
+ sessionID,
65608
+ consecutiveFailures: state3.consecutiveFailures ?? 0,
65609
+ maxConsecutiveFailures: MAX_CONSECUTIVE_FAILURES
65610
+ });
65611
+ return;
65612
+ }
65613
+ const effectiveCooldown = CONTINUATION_COOLDOWN_MS2 * 2 ** Math.min(state3.consecutiveFailures ?? 0, 5);
65614
+ if (state3.lastInjectedAt && Date.now() - state3.lastInjectedAt < effectiveCooldown) {
65615
+ log(`[${HOOK_NAME13}] Skipped: cooldown active`, {
65616
+ sessionID,
65617
+ effectiveCooldown,
65618
+ consecutiveFailures: state3.consecutiveFailures ?? 0
65619
+ });
65620
+ return;
65621
+ }
65622
+ let resolvedInfo;
65623
+ let hasCompactionMessage = false;
65624
+ try {
65625
+ const messagesResp = await ctx.client.session.messages({
65626
+ path: { id: sessionID }
65627
+ });
65628
+ const messages = normalizeSDKResponse(messagesResp, []);
65629
+ for (let i = messages.length - 1;i >= 0; i--) {
65630
+ const info = messages[i].info;
65631
+ if (info?.agent === "compaction") {
65632
+ hasCompactionMessage = true;
65633
+ continue;
65634
+ }
65635
+ if (info?.agent || info?.model || info?.modelID && info?.providerID) {
65636
+ resolvedInfo = {
65637
+ agent: info.agent,
65638
+ model: info.model ?? (info.providerID && info.modelID ? { providerID: info.providerID, modelID: info.modelID } : undefined),
65639
+ tools: info.tools
65640
+ };
65641
+ break;
65642
+ }
65643
+ }
65644
+ } catch (error51) {
65645
+ log(`[${HOOK_NAME13}] Failed to fetch messages for agent check`, { sessionID, error: String(error51) });
65646
+ }
65647
+ log(`[${HOOK_NAME13}] Agent check`, { sessionID, agentName: resolvedInfo?.agent, skipAgents, hasCompactionMessage });
65648
+ const resolvedAgentName = resolvedInfo?.agent;
65649
+ if (resolvedAgentName && skipAgents.some((s) => getAgentConfigKey(s) === getAgentConfigKey(resolvedAgentName))) {
65650
+ log(`[${HOOK_NAME13}] Skipped: agent in skipAgents list`, { sessionID, agent: resolvedAgentName });
65651
+ return;
65652
+ }
65653
+ if (hasCompactionMessage && !resolvedInfo?.agent) {
65654
+ log(`[${HOOK_NAME13}] Skipped: compaction occurred but no agent info resolved`, { sessionID });
65655
+ return;
65656
+ }
65657
+ if (isContinuationStopped?.(sessionID)) {
65658
+ log(`[${HOOK_NAME13}] Skipped: continuation stopped for session`, { sessionID });
65659
+ return;
65660
+ }
65661
+ startCountdown({
65662
+ ctx,
65663
+ sessionID,
65664
+ incompleteCount,
65665
+ total,
65666
+ resolvedInfo,
65667
+ backgroundManager,
65668
+ skipAgents,
65669
+ sessionStateStore
65670
+ });
65671
+ }
65672
+
65673
+ // src/hooks/task-continuation-enforcer/non-idle-events.ts
65674
+ init_logger();
65675
+ function handleNonIdleEvent(args) {
65676
+ const { eventType, properties, sessionStateStore } = args;
65677
+ if (eventType === "message.updated") {
65678
+ const info = properties?.info;
65679
+ const sessionID = info?.sessionID;
65680
+ const role = info?.role;
65556
65681
  if (!sessionID)
65557
65682
  return;
65683
+ if (role === "user") {
65684
+ const state3 = sessionStateStore.getExistingState(sessionID);
65685
+ if (state3?.countdownStartedAt) {
65686
+ const elapsed = Date.now() - state3.countdownStartedAt;
65687
+ if (elapsed < COUNTDOWN_GRACE_PERIOD_MS) {
65688
+ log(`[${HOOK_NAME13}] Ignoring user message in grace period`, { sessionID, elapsed });
65689
+ return;
65690
+ }
65691
+ }
65692
+ if (state3)
65693
+ state3.abortDetectedAt = undefined;
65694
+ sessionStateStore.cancelCountdown(sessionID);
65695
+ return;
65696
+ }
65697
+ if (role === "assistant") {
65698
+ const state3 = sessionStateStore.getExistingState(sessionID);
65699
+ if (state3)
65700
+ state3.abortDetectedAt = undefined;
65701
+ sessionStateStore.cancelCountdown(sessionID);
65702
+ return;
65703
+ }
65704
+ return;
65705
+ }
65706
+ if (eventType === "message.part.updated") {
65707
+ const info = properties?.info;
65708
+ const sessionID = info?.sessionID;
65709
+ const role = info?.role;
65710
+ if (sessionID && role === "assistant") {
65711
+ const state3 = sessionStateStore.getExistingState(sessionID);
65712
+ if (state3)
65713
+ state3.abortDetectedAt = undefined;
65714
+ sessionStateStore.cancelCountdown(sessionID);
65715
+ }
65716
+ return;
65717
+ }
65718
+ if (eventType === "tool.execute.before" || eventType === "tool.execute.after") {
65719
+ const sessionID = properties?.sessionID;
65720
+ if (sessionID) {
65721
+ const state3 = sessionStateStore.getExistingState(sessionID);
65722
+ if (state3)
65723
+ state3.abortDetectedAt = undefined;
65724
+ sessionStateStore.cancelCountdown(sessionID);
65725
+ }
65726
+ return;
65727
+ }
65728
+ if (eventType === "session.deleted") {
65729
+ const sessionInfo = properties?.info;
65730
+ if (sessionInfo?.id) {
65731
+ sessionStateStore.cleanup(sessionInfo.id);
65732
+ log(`[${HOOK_NAME13}] Session deleted: cleaned up`, { sessionID: sessionInfo.id });
65733
+ }
65734
+ return;
65735
+ }
65736
+ }
65737
+
65738
+ // src/hooks/task-continuation-enforcer/handler.ts
65739
+ function createTaskContinuationHandler(args) {
65740
+ const {
65741
+ ctx,
65742
+ sessionStateStore,
65743
+ backgroundManager,
65744
+ skipAgents = DEFAULT_SKIP_AGENTS,
65745
+ isContinuationStopped
65746
+ } = args;
65747
+ return async ({ event }) => {
65748
+ const props = event.properties;
65749
+ if (event.type === "session.error") {
65750
+ const sessionID = props?.sessionID;
65751
+ if (!sessionID)
65752
+ return;
65753
+ const error51 = props?.error;
65754
+ if (error51?.name === "MessageAbortedError" || error51?.name === "AbortError") {
65755
+ const state3 = sessionStateStore.getState(sessionID);
65756
+ state3.abortDetectedAt = Date.now();
65757
+ log(`[${HOOK_NAME13}] Abort detected via session.error`, { sessionID, errorName: error51.name });
65758
+ }
65759
+ sessionStateStore.cancelCountdown(sessionID);
65760
+ log(`[${HOOK_NAME13}] session.error`, { sessionID });
65761
+ return;
65762
+ }
65763
+ if (event.type === "session.idle") {
65764
+ const sessionID = props?.sessionID;
65765
+ if (!sessionID)
65766
+ return;
65767
+ await handleSessionIdle({
65768
+ ctx,
65769
+ sessionID,
65770
+ sessionStateStore,
65771
+ backgroundManager,
65772
+ skipAgents,
65773
+ isContinuationStopped
65774
+ });
65775
+ return;
65776
+ }
65777
+ handleNonIdleEvent({
65778
+ eventType: event.type,
65779
+ properties: props,
65780
+ sessionStateStore
65781
+ });
65782
+ };
65783
+ }
65784
+
65785
+ // src/hooks/task-continuation-enforcer/session-state.ts
65786
+ var SESSION_STATE_TTL_MS = 10 * 60 * 1000;
65787
+ var SESSION_STATE_PRUNE_INTERVAL_MS = 2 * 60 * 1000;
65788
+ function createSessionStateStore() {
65789
+ const sessions = new Map;
65790
+ let pruneInterval;
65791
+ pruneInterval = setInterval(() => {
65558
65792
  const now = Date.now();
65559
- const last = lastSync.get(sessionID) ?? 0;
65560
- if (now - last < DEBOUNCE_MS)
65793
+ for (const [sessionID, tracked] of sessions.entries()) {
65794
+ if (now - tracked.lastAccessedAt > SESSION_STATE_TTL_MS) {
65795
+ cancelCountdown(sessionID);
65796
+ sessions.delete(sessionID);
65797
+ }
65798
+ }
65799
+ }, SESSION_STATE_PRUNE_INTERVAL_MS);
65800
+ if (typeof pruneInterval === "object" && "unref" in pruneInterval) {
65801
+ pruneInterval.unref();
65802
+ }
65803
+ function getState(sessionID) {
65804
+ const existing = sessions.get(sessionID);
65805
+ if (existing) {
65806
+ existing.lastAccessedAt = Date.now();
65807
+ return existing.state;
65808
+ }
65809
+ const state3 = {
65810
+ consecutiveFailures: 0
65811
+ };
65812
+ sessions.set(sessionID, { state: state3, lastAccessedAt: Date.now() });
65813
+ return state3;
65814
+ }
65815
+ function getExistingState(sessionID) {
65816
+ const existing = sessions.get(sessionID);
65817
+ if (existing) {
65818
+ existing.lastAccessedAt = Date.now();
65819
+ return existing.state;
65820
+ }
65821
+ return;
65822
+ }
65823
+ function cancelCountdown(sessionID) {
65824
+ const tracked = sessions.get(sessionID);
65825
+ if (!tracked)
65561
65826
  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) });
65827
+ const state3 = tracked.state;
65828
+ if (state3.countdownTimer) {
65829
+ clearTimeout(state3.countdownTimer);
65830
+ state3.countdownTimer = undefined;
65831
+ }
65832
+ if (state3.countdownInterval) {
65833
+ clearInterval(state3.countdownInterval);
65834
+ state3.countdownInterval = undefined;
65835
+ }
65836
+ state3.inFlight = false;
65837
+ state3.countdownStartedAt = undefined;
65838
+ }
65839
+ function cleanup(sessionID) {
65840
+ cancelCountdown(sessionID);
65841
+ sessions.delete(sessionID);
65842
+ }
65843
+ function cancelAllCountdowns() {
65844
+ for (const sessionID of sessions.keys()) {
65845
+ cancelCountdown(sessionID);
65846
+ }
65847
+ }
65848
+ function shutdown() {
65849
+ clearInterval(pruneInterval);
65850
+ cancelAllCountdowns();
65851
+ sessions.clear();
65852
+ }
65853
+ return {
65854
+ getState,
65855
+ getExistingState,
65856
+ cancelCountdown,
65857
+ cleanup,
65858
+ cancelAllCountdowns,
65859
+ shutdown
65860
+ };
65861
+ }
65862
+
65863
+ // src/hooks/task-continuation-enforcer/index.ts
65864
+ function createTaskContinuationEnforcer(ctx, options = {}) {
65865
+ const {
65866
+ backgroundManager,
65867
+ skipAgents = DEFAULT_SKIP_AGENTS,
65868
+ isContinuationStopped
65869
+ } = options;
65870
+ const sessionStateStore = createSessionStateStore();
65871
+ const markRecovering = (sessionID) => {
65872
+ const state3 = sessionStateStore.getState(sessionID);
65873
+ state3.isRecovering = true;
65874
+ sessionStateStore.cancelCountdown(sessionID);
65875
+ log(`[${HOOK_NAME13}] Session marked as recovering`, { sessionID });
65876
+ };
65877
+ const markRecoveryComplete = (sessionID) => {
65878
+ const state3 = sessionStateStore.getExistingState(sessionID);
65879
+ if (state3) {
65880
+ state3.isRecovering = false;
65881
+ log(`[${HOOK_NAME13}] Session recovery complete`, { sessionID });
65569
65882
  }
65570
65883
  };
65884
+ const handler = createTaskContinuationHandler({
65885
+ ctx,
65886
+ sessionStateStore,
65887
+ backgroundManager,
65888
+ skipAgents,
65889
+ isContinuationStopped
65890
+ });
65891
+ const cancelAllCountdowns = () => {
65892
+ sessionStateStore.cancelAllCountdowns();
65893
+ log(`[${HOOK_NAME13}] All countdowns cancelled`);
65894
+ };
65571
65895
  return {
65572
- "tool.execute.after": async (input, _output) => {
65573
- if (!enabled)
65896
+ handler,
65897
+ markRecovering,
65898
+ markRecoveryComplete,
65899
+ cancelAllCountdowns
65900
+ };
65901
+ }
65902
+ // src/hooks/task-edit-guard/constants.ts
65903
+ var HOOK_NAME14 = "task-edit-guard";
65904
+ var BLOCKED_PATTERNS = [
65905
+ /sed\s+.*\.matrixx\/plans/,
65906
+ /sed\s+.*\.matrixx\/tasks/,
65907
+ /python3?\s+.*\.matrixx\/plans/,
65908
+ /python3?\s+.*\.matrixx\/tasks/,
65909
+ /echo\s+.*\.matrixx\/plans/,
65910
+ /echo\s+.*\.matrixx\/tasks/,
65911
+ /cat\s*>.*\.matrixx\/plans/,
65912
+ /cat\s*>.*\.matrixx\/tasks/,
65913
+ /mv\s+.*\.matrixx\/plans/,
65914
+ /mv\s+.*\.matrixx\/tasks/,
65915
+ /rm\s+.*\.matrixx\/tasks\/T-.*\.json/,
65916
+ /printf\s+.*\.matrixx\/plans/,
65917
+ /printf\s+.*\.matrixx\/tasks/
65918
+ ];
65919
+ // src/hooks/task-edit-guard/hook.ts
65920
+ function createTaskEditGuardHook(ctx) {
65921
+ return {
65922
+ "tool.execute.before": async (input, output) => {
65923
+ if (input.tool?.toLowerCase() !== "bash")
65574
65924
  return;
65575
- await syncForSession(input.sessionID);
65576
- },
65577
- "chat.message": async (input, _output) => {
65578
- if (!enabled)
65925
+ const args = output.args;
65926
+ const cmd = args?.command;
65927
+ if (!cmd)
65579
65928
  return;
65580
- await syncForSession(input.sessionID);
65581
- },
65582
- "experimental.chat.messages.transform": async (_input, output) => {
65583
- if (!enabled)
65929
+ const lower = cmd.toLowerCase();
65930
+ if (lower.includes("grep") && !lower.includes("sed") && !lower.includes("python")) {
65931
+ const isOnlyGrep = BLOCKED_PATTERNS.every((rx) => !rx.test(cmd));
65932
+ if (isOnlyGrep)
65933
+ return;
65934
+ }
65935
+ const hit = BLOCKED_PATTERNS.some((rx) => rx.test(cmd));
65936
+ if (!hit)
65584
65937
  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;
65938
+ ctx.directory;
65939
+ log(`[${HOOK_NAME14}] BLOCKED raw bash edit`, {
65940
+ sessionID: input.sessionID,
65941
+ command: cmd.slice(0, 120)
65942
+ });
65943
+ 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");
65944
+ }
65945
+ };
65946
+ }
65947
+ // src/hooks/task-notepad/constants.ts
65948
+ var HOOK_NAME15 = "task-notepad";
65949
+ var PLAN_DIR_NAME = ".matrixx";
65950
+ var PLANS_SUBDIR = "plans";
65951
+ var NOTEPAD_DIR_NAME = "notepads";
65952
+ var TASKS_SUBDIR = "tasks";
65953
+ var PLAN_EXTENSION = ".md";
65954
+ var NOTEPAD_EXTENSION = ".md";
65955
+ var TODO_TOOL_NAMES = new Set(["todowrite", "TodoWrite"]);
65956
+ var TASK_NOTEPAD_FRAGMENT = "task-notepad";
65957
+ var TODO_FETCH_ERROR_FRAGMENT = "Failed to fetch todos for task-notepad";
65958
+ // src/hooks/task-notepad/hook.ts
65959
+ init_logger();
65960
+ import { existsSync as existsSync45, mkdirSync as mkdirSync16, readdirSync as readdirSync16, readFileSync as readFileSync33, statSync as statSync8, writeFileSync as writeFileSync19 } from "fs";
65961
+ import { dirname as dirname15, join as join52 } from "path";
65962
+ function createTaskNotepadHook(ctx) {
65963
+ return {
65964
+ "tool.execute.after": async (input, output) => {
65965
+ if (!output)
65966
+ return;
65967
+ if (!TODO_TOOL_NAMES.has(input.tool))
65968
+ return;
65969
+ const todos = await fetchTodos(ctx, input.sessionID);
65970
+ const inProgress = todos.find((t) => t.status === "in_progress");
65971
+ const completed = todos.filter((t) => t.status === "completed");
65972
+ if (!inProgress && completed.length === 0)
65973
+ return;
65974
+ const plan = resolveActivePlan(ctx.directory);
65975
+ if (inProgress) {
65976
+ if (!plan) {
65977
+ log(`[${HOOK_NAME15}] No active plan, skipping notepad creation`, {
65978
+ sessionID: input.sessionID
65979
+ });
65980
+ } else {
65981
+ ensureNotepad(ctx.directory, plan.name, inProgress);
65591
65982
  }
65592
65983
  }
65593
- await syncForSession(sessionID);
65594
- },
65595
- event: async (input) => {
65596
- if (!enabled)
65984
+ if (!plan)
65597
65985
  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);
65986
+ for (const done of completed) {
65987
+ appendCompletion(ctx.directory, plan.name, done);
65602
65988
  }
65603
- },
65604
- _syncForSession: syncForSession
65989
+ }
65990
+ };
65991
+ }
65992
+ async function fetchTodos(ctx, sessionID) {
65993
+ try {
65994
+ const response = await ctx.client.session.todo({ path: { id: sessionID } });
65995
+ const payload = response;
65996
+ if (Array.isArray(payload?.data)) {
65997
+ return payload.data;
65998
+ }
65999
+ if (Array.isArray(response)) {
66000
+ return response;
66001
+ }
66002
+ return [];
66003
+ } catch (err) {
66004
+ log(`[${HOOK_NAME15}] ${TODO_FETCH_ERROR_FRAGMENT}`, {
66005
+ sessionID,
66006
+ error: String(err)
66007
+ });
66008
+ return [];
66009
+ }
66010
+ }
66011
+ function resolveActivePlan(directory) {
66012
+ const plansDir = join52(directory, PLAN_DIR_NAME, PLANS_SUBDIR);
66013
+ if (!existsSync45(plansDir))
66014
+ return null;
66015
+ let entries;
66016
+ try {
66017
+ entries = readdirSync16(plansDir);
66018
+ } catch {
66019
+ return null;
66020
+ }
66021
+ let best = null;
66022
+ for (const entry of entries) {
66023
+ if (!entry.endsWith(PLAN_EXTENSION))
66024
+ continue;
66025
+ const fullPath = join52(plansDir, entry);
66026
+ try {
66027
+ const stats = statSync8(fullPath);
66028
+ if (!stats.isFile())
66029
+ continue;
66030
+ const candidate = {
66031
+ name: entry.slice(0, -PLAN_EXTENSION.length),
66032
+ path: fullPath,
66033
+ mtimeMs: stats.mtimeMs
66034
+ };
66035
+ if (!best || candidate.mtimeMs > best.mtimeMs) {
66036
+ best = candidate;
66037
+ }
66038
+ } catch {}
66039
+ }
66040
+ return best;
66041
+ }
66042
+ function ensureNotepad(directory, planName, todo) {
66043
+ const notepadPath = notepadPathFor(directory, planName, todo.id);
66044
+ if (existsSync45(notepadPath))
66045
+ return;
66046
+ mkdirSync16(dirname15(notepadPath), { recursive: true });
66047
+ const content = renderNotepad(todo);
66048
+ writeFileSync19(notepadPath, content, "utf-8");
66049
+ log(`[${HOOK_NAME15}] Created task notepad`, { plan: planName, taskID: todo.id });
66050
+ }
66051
+ function appendCompletion(directory, planName, todo) {
66052
+ const notepadPath = notepadPathFor(directory, planName, todo.id);
66053
+ if (!existsSync45(notepadPath))
66054
+ return;
66055
+ const existing = readFileSync33(notepadPath, "utf-8");
66056
+ if (existing.includes("**Status**: completed"))
66057
+ return;
66058
+ const stamp = new Date().toISOString();
66059
+ const updated = `${existing}
66060
+ ## Completion
66061
+ - completed_at: ${stamp}
66062
+ - status: completed
66063
+ `;
66064
+ writeFileSync19(notepadPath, updated, "utf-8");
66065
+ log(`[${HOOK_NAME15}] Appended completion to notepad`, { plan: planName, taskID: todo.id });
66066
+ }
66067
+ function notepadPathFor(directory, planName, taskID) {
66068
+ return join52(directory, PLAN_DIR_NAME, NOTEPAD_DIR_NAME, planName, TASKS_SUBDIR, `${taskID}${NOTEPAD_EXTENSION}`);
66069
+ }
66070
+ function renderNotepad(todo) {
66071
+ const priority = todo.priority ?? "medium";
66072
+ return `# Task: ${todo.content}
66073
+
66074
+ **Plan**: ${TASK_NOTEPAD_FRAGMENT}
66075
+ **Task ID**: ${todo.id}
66076
+ **Priority**: ${priority}
66077
+ **Status**: ${todo.status}
66078
+ **Started**: ${new Date().toISOString()}
66079
+
66080
+ ## Findings
66081
+
66082
+ (Record what you learn about this code as you work \u2014 patterns, conventions, gotchas, useful references.)
66083
+
66084
+ ## Blockers
66085
+
66086
+ (Anything blocking progress. Be specific: file path, line, error, workaround if any.)
66087
+
66088
+ ## Questions
66089
+
66090
+ (Open questions to revisit later. Don't lose them when context compacts.)
66091
+
66092
+ ## Results
66093
+
66094
+ (Summary on completion. What changed, what was verified, what remains.)
66095
+ `;
66096
+ }
66097
+ // src/hooks/task-resume-info/hook.ts
66098
+ var TARGET_TOOLS2 = ["task", "Task", "task_tool", "delegate_agent"];
66099
+ var SESSION_ID_PATTERNS = [
66100
+ /Session ID: (ses_[a-zA-Z0-9_-]+)/,
66101
+ /session_id: (ses_[a-zA-Z0-9_-]+)/,
66102
+ /<task_metadata>\s*session_id: (ses_[a-zA-Z0-9_-]+)/,
66103
+ /sessionId: (ses_[a-zA-Z0-9_-]+)/
66104
+ ];
66105
+ function extractSessionId(output) {
66106
+ for (const pattern of SESSION_ID_PATTERNS) {
66107
+ const match = output.match(pattern);
66108
+ if (match)
66109
+ return match[1] ?? null;
66110
+ }
66111
+ return null;
66112
+ }
66113
+ function createTaskResumeInfoHook() {
66114
+ const toolExecuteAfter = async (input, output) => {
66115
+ if (!TARGET_TOOLS2.includes(input.tool))
66116
+ return;
66117
+ const outputText = output.output ?? "";
66118
+ if (outputText.startsWith("Error:") || outputText.startsWith("Failed"))
66119
+ return;
66120
+ if (outputText.includes(`
66121
+ to continue:`))
66122
+ return;
66123
+ const sessionId = extractSessionId(outputText);
66124
+ if (!sessionId)
66125
+ return;
66126
+ output.output = outputText.trimEnd() + `
66127
+
66128
+ to continue: task(session_id="${sessionId}", prompt="...")`;
65605
66129
  };
66130
+ return {
66131
+ "tool.execute.after": toolExecuteAfter
66132
+ };
66133
+ }
66134
+ // src/shared/task-system-gating.ts
66135
+ var TASK_SYSTEM_DEFAULT = true;
66136
+ function isTaskSystemEnabled(config2) {
66137
+ return config2?.experimental?.task_system ?? TASK_SYSTEM_DEFAULT;
65606
66138
  }
66139
+
65607
66140
  // src/hooks/tasks-todowrite-disabler/constants.ts
65608
66141
  var BLOCKED_TOOLS2 = ["TodoWrite", "TodoRead"];
65609
66142
  var REPLACEMENT_MESSAGE = `TodoRead/TodoWrite are DISABLED because experimental.task_system is enabled.
@@ -65637,10 +66170,10 @@ DO NOT retry TodoWrite. Convert to TaskCreate NOW.`;
65637
66170
 
65638
66171
  // src/hooks/tasks-todowrite-disabler/hook.ts
65639
66172
  function createTasksTodowriteDisablerHook(config2) {
65640
- const isTaskSystemEnabled = config2.experimental?.task_system ?? false;
66173
+ const enabled = isTaskSystemEnabled(config2);
65641
66174
  return {
65642
66175
  "tool.execute.before": async (input, _output) => {
65643
- if (!isTaskSystemEnabled) {
66176
+ if (!enabled) {
65644
66177
  return;
65645
66178
  }
65646
66179
  const toolName = input.tool;
@@ -66122,22 +66655,22 @@ function createThinkingBlockValidatorHook() {
66122
66655
  init_logger();
66123
66656
 
66124
66657
  // 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)}
66658
+ var HOOK_NAME16 = "todo-continuation-enforcer";
66659
+ var DEFAULT_SKIP_AGENTS2 = ["oracle", "compaction"];
66660
+ var CONTINUATION_PROMPT3 = `${createSystemDirective(SystemDirectiveTypes.TODO_CONTINUATION)}
66128
66661
 
66129
66662
  Incomplete tasks remain in your todo list. Continue working on the next pending task.
66130
66663
 
66131
66664
  - Proceed without asking for permission
66132
66665
  - Mark each task complete when finished
66133
66666
  - 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;
66667
+ var COUNTDOWN_SECONDS2 = 2;
66668
+ var TOAST_DURATION_MS2 = 900;
66669
+ var COUNTDOWN_GRACE_PERIOD_MS2 = 500;
66670
+ var ABORT_WINDOW_MS2 = 3000;
66671
+ var CONTINUATION_COOLDOWN_MS3 = 30000;
66672
+ var MAX_CONSECUTIVE_FAILURES2 = 5;
66673
+ var FAILURE_RESET_WINDOW_MS2 = 5 * 60 * 1000;
66141
66674
 
66142
66675
  // src/hooks/todo-continuation-enforcer/handler.ts
66143
66676
  init_logger();
@@ -66146,7 +66679,7 @@ init_logger();
66146
66679
  init_logger();
66147
66680
 
66148
66681
  // src/hooks/todo-continuation-enforcer/abort-detection.ts
66149
- function isLastAssistantMessageAborted(messages) {
66682
+ function isLastAssistantMessageAborted2(messages) {
66150
66683
  if (!messages || messages.length === 0)
66151
66684
  return false;
66152
66685
  for (let i = messages.length - 1;i >= 0; i--) {
@@ -66172,28 +66705,28 @@ function getIncompleteCount(todos) {
66172
66705
  }
66173
66706
 
66174
66707
  // src/hooks/todo-continuation-enforcer/continuation-injection.ts
66175
- function hasWritePermission(tools) {
66708
+ function hasWritePermission2(tools) {
66176
66709
  const editPermission = tools?.edit;
66177
66710
  const writePermission = tools?.write;
66178
66711
  return !tools || editPermission !== false && editPermission !== "deny" && writePermission !== false && writePermission !== "deny";
66179
66712
  }
66180
- async function injectContinuation(args) {
66713
+ async function injectContinuation2(args) {
66181
66714
  const {
66182
66715
  ctx,
66183
66716
  sessionID,
66184
66717
  backgroundManager,
66185
- skipAgents = DEFAULT_SKIP_AGENTS,
66718
+ skipAgents = DEFAULT_SKIP_AGENTS2,
66186
66719
  resolvedInfo,
66187
66720
  sessionStateStore
66188
66721
  } = args;
66189
66722
  const state3 = sessionStateStore.getExistingState(sessionID);
66190
66723
  if (state3?.isRecovering) {
66191
- log(`[${HOOK_NAME15}] Skipped injection: in recovery`, { sessionID });
66724
+ log(`[${HOOK_NAME16}] Skipped injection: in recovery`, { sessionID });
66192
66725
  return;
66193
66726
  }
66194
66727
  const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((task) => task.status === "running") : false;
66195
66728
  if (hasRunningBgTasks) {
66196
- log(`[${HOOK_NAME15}] Skipped injection: background tasks running`, { sessionID });
66729
+ log(`[${HOOK_NAME16}] Skipped injection: background tasks running`, { sessionID });
66197
66730
  return;
66198
66731
  }
66199
66732
  let todos = [];
@@ -66201,12 +66734,12 @@ async function injectContinuation(args) {
66201
66734
  const response = await ctx.client.session.todo({ path: { id: sessionID } });
66202
66735
  todos = normalizeSDKResponse(response, [], { preferResponseOnMissingData: true });
66203
66736
  } catch (error51) {
66204
- log(`[${HOOK_NAME15}] Failed to fetch todos`, { sessionID, error: String(error51) });
66737
+ log(`[${HOOK_NAME16}] Failed to fetch todos`, { sessionID, error: String(error51) });
66205
66738
  return;
66206
66739
  }
66207
66740
  const freshIncompleteCount = getIncompleteCount(todos);
66208
66741
  if (freshIncompleteCount === 0) {
66209
- log(`[${HOOK_NAME15}] Skipped injection: no incomplete todos`, { sessionID });
66742
+ log(`[${HOOK_NAME16}] Skipped injection: no incomplete todos`, { sessionID });
66210
66743
  return;
66211
66744
  }
66212
66745
  let agentName = resolvedInfo?.agent;
@@ -66229,17 +66762,17 @@ async function injectContinuation(args) {
66229
66762
  tools = tools ?? previousMessage?.tools;
66230
66763
  }
66231
66764
  if (agentName && skipAgents.some((s) => getAgentConfigKey(s) === getAgentConfigKey(agentName))) {
66232
- log(`[${HOOK_NAME15}] Skipped: agent in skipAgents list`, { sessionID, agent: agentName });
66765
+ log(`[${HOOK_NAME16}] Skipped: agent in skipAgents list`, { sessionID, agent: agentName });
66233
66766
  return;
66234
66767
  }
66235
- if (!hasWritePermission(tools)) {
66236
- log(`[${HOOK_NAME15}] Skipped: agent lacks write permission`, { sessionID, agent: agentName });
66768
+ if (!hasWritePermission2(tools)) {
66769
+ log(`[${HOOK_NAME16}] Skipped: agent lacks write permission`, { sessionID, agent: agentName });
66237
66770
  return;
66238
66771
  }
66239
66772
  const incompleteTodos = todos.filter((todo) => todo.status !== "completed" && todo.status !== "cancelled");
66240
66773
  const todoList = incompleteTodos.map((todo) => `- [${todo.status}] ${todo.content}`).join(`
66241
66774
  `);
66242
- const prompt = `${CONTINUATION_PROMPT2}
66775
+ const prompt = `${CONTINUATION_PROMPT3}
66243
66776
 
66244
66777
  [Status: ${todos.length - freshIncompleteCount}/${todos.length} completed, ${freshIncompleteCount} remaining]
66245
66778
 
@@ -66250,7 +66783,7 @@ ${todoList}`;
66250
66783
  injectionState.inFlight = true;
66251
66784
  }
66252
66785
  try {
66253
- log(`[${HOOK_NAME15}] Injecting continuation`, {
66786
+ log(`[${HOOK_NAME16}] Injecting continuation`, {
66254
66787
  sessionID,
66255
66788
  agent: agentName,
66256
66789
  model,
@@ -66265,14 +66798,14 @@ ${todoList}`;
66265
66798
  },
66266
66799
  query: { directory: ctx.directory }
66267
66800
  });
66268
- log(`[${HOOK_NAME15}] Injection successful`, { sessionID });
66801
+ log(`[${HOOK_NAME16}] Injection successful`, { sessionID });
66269
66802
  if (injectionState) {
66270
66803
  injectionState.inFlight = false;
66271
66804
  injectionState.lastInjectedAt = Date.now();
66272
66805
  injectionState.consecutiveFailures = 0;
66273
66806
  }
66274
66807
  } catch (error51) {
66275
- log(`[${HOOK_NAME15}] Injection failed`, { sessionID, error: String(error51) });
66808
+ log(`[${HOOK_NAME16}] Injection failed`, { sessionID, error: String(error51) });
66276
66809
  if (injectionState) {
66277
66810
  injectionState.inFlight = false;
66278
66811
  injectionState.lastInjectedAt = Date.now();
@@ -66282,19 +66815,19 @@ ${todoList}`;
66282
66815
  }
66283
66816
 
66284
66817
  // src/hooks/todo-continuation-enforcer/countdown.ts
66285
- async function showCountdownToast(ctx, seconds, incompleteCount) {
66818
+ async function showCountdownToast2(ctx, seconds, incompleteCount) {
66286
66819
  await ctx.client.tui.showToast({
66287
66820
  body: {
66288
66821
  title: "Todo Continuation",
66289
66822
  message: `Resuming in ${seconds}s... (${incompleteCount} tasks remaining)`,
66290
66823
  variant: "warning",
66291
- duration: TOAST_DURATION_MS
66824
+ duration: TOAST_DURATION_MS2
66292
66825
  }
66293
66826
  }).catch((err) => {
66294
66827
  log("[todo-continuation] Countdown toast failed:", err);
66295
66828
  });
66296
66829
  }
66297
- function startCountdown(args) {
66830
+ function startCountdown2(args) {
66298
66831
  const {
66299
66832
  ctx,
66300
66833
  sessionID,
@@ -66306,18 +66839,18 @@ function startCountdown(args) {
66306
66839
  } = args;
66307
66840
  const state3 = sessionStateStore.getState(sessionID);
66308
66841
  sessionStateStore.cancelCountdown(sessionID);
66309
- let secondsRemaining = COUNTDOWN_SECONDS;
66310
- showCountdownToast(ctx, secondsRemaining, incompleteCount);
66842
+ let secondsRemaining = COUNTDOWN_SECONDS2;
66843
+ showCountdownToast2(ctx, secondsRemaining, incompleteCount);
66311
66844
  state3.countdownStartedAt = Date.now();
66312
66845
  state3.countdownInterval = setInterval(() => {
66313
66846
  secondsRemaining--;
66314
66847
  if (secondsRemaining > 0) {
66315
- showCountdownToast(ctx, secondsRemaining, incompleteCount);
66848
+ showCountdownToast2(ctx, secondsRemaining, incompleteCount);
66316
66849
  }
66317
66850
  }, 1000);
66318
66851
  state3.countdownTimer = setTimeout(() => {
66319
66852
  sessionStateStore.cancelCountdown(sessionID);
66320
- injectContinuation({
66853
+ injectContinuation2({
66321
66854
  ctx,
66322
66855
  sessionID,
66323
66856
  backgroundManager,
@@ -66325,34 +66858,34 @@ function startCountdown(args) {
66325
66858
  resolvedInfo,
66326
66859
  sessionStateStore
66327
66860
  });
66328
- }, COUNTDOWN_SECONDS * 1000);
66329
- log(`[${HOOK_NAME15}] Countdown started`, {
66861
+ }, COUNTDOWN_SECONDS2 * 1000);
66862
+ log(`[${HOOK_NAME16}] Countdown started`, {
66330
66863
  sessionID,
66331
- seconds: COUNTDOWN_SECONDS,
66864
+ seconds: COUNTDOWN_SECONDS2,
66332
66865
  incompleteCount
66333
66866
  });
66334
66867
  }
66335
66868
 
66336
66869
  // src/hooks/todo-continuation-enforcer/idle-event.ts
66337
- async function handleSessionIdle(args) {
66870
+ async function handleSessionIdle2(args) {
66338
66871
  const {
66339
66872
  ctx,
66340
66873
  sessionID,
66341
66874
  sessionStateStore,
66342
66875
  backgroundManager,
66343
- skipAgents = DEFAULT_SKIP_AGENTS,
66876
+ skipAgents = DEFAULT_SKIP_AGENTS2,
66344
66877
  isContinuationStopped
66345
66878
  } = args;
66346
- log(`[${HOOK_NAME15}] session.idle`, { sessionID });
66879
+ log(`[${HOOK_NAME16}] session.idle`, { sessionID });
66347
66880
  const state3 = sessionStateStore.getState(sessionID);
66348
66881
  if (state3.isRecovering) {
66349
- log(`[${HOOK_NAME15}] Skipped: in recovery`, { sessionID });
66882
+ log(`[${HOOK_NAME16}] Skipped: in recovery`, { sessionID });
66350
66883
  return;
66351
66884
  }
66352
66885
  if (state3.abortDetectedAt) {
66353
66886
  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 });
66887
+ if (timeSinceAbort < ABORT_WINDOW_MS2) {
66888
+ log(`[${HOOK_NAME16}] Skipped: abort detected via event ${timeSinceAbort}ms ago`, { sessionID });
66356
66889
  state3.abortDetectedAt = undefined;
66357
66890
  return;
66358
66891
  }
@@ -66360,7 +66893,7 @@ async function handleSessionIdle(args) {
66360
66893
  }
66361
66894
  const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((task) => task.status === "running") : false;
66362
66895
  if (hasRunningBgTasks) {
66363
- log(`[${HOOK_NAME15}] Skipped: background tasks running`, { sessionID });
66896
+ log(`[${HOOK_NAME16}] Skipped: background tasks running`, { sessionID });
66364
66897
  return;
66365
66898
  }
66366
66899
  try {
@@ -66369,52 +66902,52 @@ async function handleSessionIdle(args) {
66369
66902
  query: { directory: ctx.directory }
66370
66903
  });
66371
66904
  const messages = normalizeSDKResponse(messagesResp, []);
66372
- if (isLastAssistantMessageAborted(messages)) {
66373
- log(`[${HOOK_NAME15}] Skipped: last assistant message was aborted (API fallback)`, { sessionID });
66905
+ if (isLastAssistantMessageAborted2(messages)) {
66906
+ log(`[${HOOK_NAME16}] Skipped: last assistant message was aborted (API fallback)`, { sessionID });
66374
66907
  return;
66375
66908
  }
66376
66909
  } catch (error51) {
66377
- log(`[${HOOK_NAME15}] Messages fetch failed, continuing`, { sessionID, error: String(error51) });
66910
+ log(`[${HOOK_NAME16}] Messages fetch failed, continuing`, { sessionID, error: String(error51) });
66378
66911
  }
66379
66912
  let todos = [];
66380
66913
  try {
66381
66914
  const response = await ctx.client.session.todo({ path: { id: sessionID } });
66382
66915
  todos = normalizeSDKResponse(response, [], { preferResponseOnMissingData: true });
66383
66916
  } catch (error51) {
66384
- log(`[${HOOK_NAME15}] Todo fetch failed`, { sessionID, error: String(error51) });
66917
+ log(`[${HOOK_NAME16}] Todo fetch failed`, { sessionID, error: String(error51) });
66385
66918
  return;
66386
66919
  }
66387
66920
  if (!todos || todos.length === 0) {
66388
- log(`[${HOOK_NAME15}] No todos`, { sessionID });
66921
+ log(`[${HOOK_NAME16}] No todos`, { sessionID });
66389
66922
  return;
66390
66923
  }
66391
66924
  const incompleteCount = getIncompleteCount(todos);
66392
66925
  if (incompleteCount === 0) {
66393
- log(`[${HOOK_NAME15}] All todos complete`, { sessionID, total: todos.length });
66926
+ log(`[${HOOK_NAME16}] All todos complete`, { sessionID, total: todos.length });
66394
66927
  return;
66395
66928
  }
66396
66929
  if (state3.inFlight) {
66397
- log(`[${HOOK_NAME15}] Skipped: injection in flight`, { sessionID });
66930
+ log(`[${HOOK_NAME16}] Skipped: injection in flight`, { sessionID });
66398
66931
  return;
66399
66932
  }
66400
- if (state3.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES && state3.lastInjectedAt && Date.now() - state3.lastInjectedAt >= FAILURE_RESET_WINDOW_MS) {
66933
+ if (state3.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2 && state3.lastInjectedAt && Date.now() - state3.lastInjectedAt >= FAILURE_RESET_WINDOW_MS2) {
66401
66934
  state3.consecutiveFailures = 0;
66402
- log(`[${HOOK_NAME15}] Reset consecutive failures after recovery window`, {
66935
+ log(`[${HOOK_NAME16}] Reset consecutive failures after recovery window`, {
66403
66936
  sessionID,
66404
- failureResetWindowMs: FAILURE_RESET_WINDOW_MS
66937
+ failureResetWindowMs: FAILURE_RESET_WINDOW_MS2
66405
66938
  });
66406
66939
  }
66407
- if (state3.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
66408
- log(`[${HOOK_NAME15}] Skipped: max consecutive failures reached`, {
66940
+ if (state3.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) {
66941
+ log(`[${HOOK_NAME16}] Skipped: max consecutive failures reached`, {
66409
66942
  sessionID,
66410
66943
  consecutiveFailures: state3.consecutiveFailures,
66411
- maxConsecutiveFailures: MAX_CONSECUTIVE_FAILURES
66944
+ maxConsecutiveFailures: MAX_CONSECUTIVE_FAILURES2
66412
66945
  });
66413
66946
  return;
66414
66947
  }
66415
- const effectiveCooldown = CONTINUATION_COOLDOWN_MS2 * 2 ** Math.min(state3.consecutiveFailures, 5);
66948
+ const effectiveCooldown = CONTINUATION_COOLDOWN_MS3 * 2 ** Math.min(state3.consecutiveFailures, 5);
66416
66949
  if (state3.lastInjectedAt && Date.now() - state3.lastInjectedAt < effectiveCooldown) {
66417
- log(`[${HOOK_NAME15}] Skipped: cooldown active`, {
66950
+ log(`[${HOOK_NAME16}] Skipped: cooldown active`, {
66418
66951
  sessionID,
66419
66952
  effectiveCooldown,
66420
66953
  consecutiveFailures: state3.consecutiveFailures
@@ -66444,23 +66977,23 @@ async function handleSessionIdle(args) {
66444
66977
  }
66445
66978
  }
66446
66979
  } catch (error51) {
66447
- log(`[${HOOK_NAME15}] Failed to fetch messages for agent check`, { sessionID, error: String(error51) });
66980
+ log(`[${HOOK_NAME16}] Failed to fetch messages for agent check`, { sessionID, error: String(error51) });
66448
66981
  }
66449
- log(`[${HOOK_NAME15}] Agent check`, { sessionID, agentName: resolvedInfo?.agent, skipAgents, hasCompactionMessage });
66982
+ log(`[${HOOK_NAME16}] Agent check`, { sessionID, agentName: resolvedInfo?.agent, skipAgents, hasCompactionMessage });
66450
66983
  const resolvedAgentName = resolvedInfo?.agent;
66451
66984
  if (resolvedAgentName && skipAgents.some((s) => getAgentConfigKey(s) === getAgentConfigKey(resolvedAgentName))) {
66452
- log(`[${HOOK_NAME15}] Skipped: agent in skipAgents list`, { sessionID, agent: resolvedAgentName });
66985
+ log(`[${HOOK_NAME16}] Skipped: agent in skipAgents list`, { sessionID, agent: resolvedAgentName });
66453
66986
  return;
66454
66987
  }
66455
66988
  if (hasCompactionMessage && !resolvedInfo?.agent) {
66456
- log(`[${HOOK_NAME15}] Skipped: compaction occurred but no agent info resolved`, { sessionID });
66989
+ log(`[${HOOK_NAME16}] Skipped: compaction occurred but no agent info resolved`, { sessionID });
66457
66990
  return;
66458
66991
  }
66459
66992
  if (isContinuationStopped?.(sessionID)) {
66460
- log(`[${HOOK_NAME15}] Skipped: continuation stopped for session`, { sessionID });
66993
+ log(`[${HOOK_NAME16}] Skipped: continuation stopped for session`, { sessionID });
66461
66994
  return;
66462
66995
  }
66463
- startCountdown({
66996
+ startCountdown2({
66464
66997
  ctx,
66465
66998
  sessionID,
66466
66999
  incompleteCount,
@@ -66474,7 +67007,7 @@ async function handleSessionIdle(args) {
66474
67007
 
66475
67008
  // src/hooks/todo-continuation-enforcer/non-idle-events.ts
66476
67009
  init_logger();
66477
- function handleNonIdleEvent(args) {
67010
+ function handleNonIdleEvent2(args) {
66478
67011
  const { eventType, properties, sessionStateStore } = args;
66479
67012
  if (eventType === "message.updated") {
66480
67013
  const info = properties?.info;
@@ -66486,8 +67019,8 @@ function handleNonIdleEvent(args) {
66486
67019
  const state3 = sessionStateStore.getExistingState(sessionID);
66487
67020
  if (state3?.countdownStartedAt) {
66488
67021
  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 });
67022
+ if (elapsed < COUNTDOWN_GRACE_PERIOD_MS2) {
67023
+ log(`[${HOOK_NAME16}] Ignoring user message in grace period`, { sessionID, elapsed });
66491
67024
  return;
66492
67025
  }
66493
67026
  }
@@ -66531,7 +67064,7 @@ function handleNonIdleEvent(args) {
66531
67064
  const sessionInfo = properties?.info;
66532
67065
  if (sessionInfo?.id) {
66533
67066
  sessionStateStore.cleanup(sessionInfo.id);
66534
- log(`[${HOOK_NAME15}] Session deleted: cleaned up`, { sessionID: sessionInfo.id });
67067
+ log(`[${HOOK_NAME16}] Session deleted: cleaned up`, { sessionID: sessionInfo.id });
66535
67068
  }
66536
67069
  return;
66537
67070
  }
@@ -66543,7 +67076,7 @@ function createTodoContinuationHandler(args) {
66543
67076
  ctx,
66544
67077
  sessionStateStore,
66545
67078
  backgroundManager,
66546
- skipAgents = DEFAULT_SKIP_AGENTS,
67079
+ skipAgents = DEFAULT_SKIP_AGENTS2,
66547
67080
  isContinuationStopped
66548
67081
  } = args;
66549
67082
  return async ({ event }) => {
@@ -66556,17 +67089,17 @@ function createTodoContinuationHandler(args) {
66556
67089
  if (error51?.name === "MessageAbortedError" || error51?.name === "AbortError") {
66557
67090
  const state3 = sessionStateStore.getState(sessionID);
66558
67091
  state3.abortDetectedAt = Date.now();
66559
- log(`[${HOOK_NAME15}] Abort detected via session.error`, { sessionID, errorName: error51.name });
67092
+ log(`[${HOOK_NAME16}] Abort detected via session.error`, { sessionID, errorName: error51.name });
66560
67093
  }
66561
67094
  sessionStateStore.cancelCountdown(sessionID);
66562
- log(`[${HOOK_NAME15}] session.error`, { sessionID });
67095
+ log(`[${HOOK_NAME16}] session.error`, { sessionID });
66563
67096
  return;
66564
67097
  }
66565
67098
  if (event.type === "session.idle") {
66566
67099
  const sessionID = props?.sessionID;
66567
67100
  if (!sessionID)
66568
67101
  return;
66569
- await handleSessionIdle({
67102
+ await handleSessionIdle2({
66570
67103
  ctx,
66571
67104
  sessionID,
66572
67105
  sessionStateStore,
@@ -66576,7 +67109,7 @@ function createTodoContinuationHandler(args) {
66576
67109
  });
66577
67110
  return;
66578
67111
  }
66579
- handleNonIdleEvent({
67112
+ handleNonIdleEvent2({
66580
67113
  eventType: event.type,
66581
67114
  properties: props,
66582
67115
  sessionStateStore
@@ -66585,20 +67118,20 @@ function createTodoContinuationHandler(args) {
66585
67118
  }
66586
67119
 
66587
67120
  // 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() {
67121
+ var SESSION_STATE_TTL_MS2 = 10 * 60 * 1000;
67122
+ var SESSION_STATE_PRUNE_INTERVAL_MS2 = 2 * 60 * 1000;
67123
+ function createSessionStateStore2() {
66591
67124
  const sessions = new Map;
66592
67125
  let pruneInterval;
66593
67126
  pruneInterval = setInterval(() => {
66594
67127
  const now = Date.now();
66595
67128
  for (const [sessionID, tracked] of sessions.entries()) {
66596
- if (now - tracked.lastAccessedAt > SESSION_STATE_TTL_MS) {
67129
+ if (now - tracked.lastAccessedAt > SESSION_STATE_TTL_MS2) {
66597
67130
  cancelCountdown(sessionID);
66598
67131
  sessions.delete(sessionID);
66599
67132
  }
66600
67133
  }
66601
- }, SESSION_STATE_PRUNE_INTERVAL_MS);
67134
+ }, SESSION_STATE_PRUNE_INTERVAL_MS2);
66602
67135
  if (typeof pruneInterval === "object" && "unref" in pruneInterval) {
66603
67136
  pruneInterval.unref();
66604
67137
  }
@@ -66666,21 +67199,21 @@ function createSessionStateStore() {
66666
67199
  function createTodoContinuationEnforcer(ctx, options = {}) {
66667
67200
  const {
66668
67201
  backgroundManager,
66669
- skipAgents = DEFAULT_SKIP_AGENTS,
67202
+ skipAgents = DEFAULT_SKIP_AGENTS2,
66670
67203
  isContinuationStopped
66671
67204
  } = options;
66672
- const sessionStateStore = createSessionStateStore();
67205
+ const sessionStateStore = createSessionStateStore2();
66673
67206
  const markRecovering = (sessionID) => {
66674
67207
  const state3 = sessionStateStore.getState(sessionID);
66675
67208
  state3.isRecovering = true;
66676
67209
  sessionStateStore.cancelCountdown(sessionID);
66677
- log(`[${HOOK_NAME15}] Session marked as recovering`, { sessionID });
67210
+ log(`[${HOOK_NAME16}] Session marked as recovering`, { sessionID });
66678
67211
  };
66679
67212
  const markRecoveryComplete = (sessionID) => {
66680
67213
  const state3 = sessionStateStore.getExistingState(sessionID);
66681
67214
  if (state3) {
66682
67215
  state3.isRecovering = false;
66683
- log(`[${HOOK_NAME15}] Session recovery complete`, { sessionID });
67216
+ log(`[${HOOK_NAME16}] Session recovery complete`, { sessionID });
66684
67217
  }
66685
67218
  };
66686
67219
  const handler = createTodoContinuationHandler({
@@ -66692,7 +67225,7 @@ function createTodoContinuationEnforcer(ctx, options = {}) {
66692
67225
  });
66693
67226
  const cancelAllCountdowns = () => {
66694
67227
  sessionStateStore.cancelAllCountdowns();
66695
- log(`[${HOOK_NAME15}] All countdowns cancelled`);
67228
+ log(`[${HOOK_NAME16}] All countdowns cancelled`);
66696
67229
  };
66697
67230
  return {
66698
67231
  handler,
@@ -66701,38 +67234,6 @@ function createTodoContinuationEnforcer(ctx, options = {}) {
66701
67234
  cancelAllCountdowns
66702
67235
  };
66703
67236
  }
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
67237
  // src/hooks/tool-output-truncator.ts
66737
67238
  var DEFAULT_MAX_TOKENS = 50000;
66738
67239
  var WEBFETCH_MAX_TOKENS = 1e4;
@@ -66948,8 +67449,8 @@ This is a reminder only. No automatic action was taken.`;
66948
67449
  }
66949
67450
  // src/hooks/unstable-agent-babysitter/unstable-agent-babysitter-hook.ts
66950
67451
  init_logger();
66951
- var HOOK_NAME16 = "unstable-agent-babysitter";
66952
- var DEFAULT_TIMEOUT_MS2 = 120000;
67452
+ var HOOK_NAME17 = "unstable-agent-babysitter";
67453
+ var DEFAULT_TIMEOUT_MS = 120000;
66953
67454
  var COOLDOWN_MS = 5 * 60 * 1000;
66954
67455
  async function resolveMainSessionTarget(ctx, sessionID) {
66955
67456
  let agent = getSessionAgent(sessionID);
@@ -66968,7 +67469,7 @@ async function resolveMainSessionTarget(ctx, sessionID) {
66968
67469
  }
66969
67470
  }
66970
67471
  } catch (error51) {
66971
- log(`[${HOOK_NAME16}] Failed to resolve main session agent`, { sessionID, error: String(error51) });
67472
+ log(`[${HOOK_NAME17}] Failed to resolve main session agent`, { sessionID, error: String(error51) });
66972
67473
  }
66973
67474
  return { agent, model };
66974
67475
  }
@@ -67001,7 +67502,7 @@ async function getThinkingSummary(ctx, sessionID) {
67001
67502
  return combined;
67002
67503
  return `${combined.slice(0, THINKING_SUMMARY_MAX_CHARS)}...`;
67003
67504
  } catch (error51) {
67004
- log(`[${HOOK_NAME16}] Failed to fetch thinking summary`, { sessionID, error: String(error51) });
67505
+ log(`[${HOOK_NAME17}] Failed to fetch thinking summary`, { sessionID, error: String(error51) });
67005
67506
  return null;
67006
67507
  }
67007
67508
  }
@@ -67020,7 +67521,7 @@ function createUnstableAgentBabysitterHook(ctx, options) {
67020
67521
  const tasks = options.backgroundManager.getTasksByParentSession(mainSessionID);
67021
67522
  if (tasks.length === 0)
67022
67523
  return;
67023
- const timeoutMs = options.config?.timeout_ms ?? DEFAULT_TIMEOUT_MS2;
67524
+ const timeoutMs = options.config?.timeout_ms ?? DEFAULT_TIMEOUT_MS;
67024
67525
  const now = Date.now();
67025
67526
  for (const task of tasks) {
67026
67527
  if (task.status !== "running")
@@ -67050,9 +67551,9 @@ function createUnstableAgentBabysitterHook(ctx, options) {
67050
67551
  query: { directory: ctx.directory }
67051
67552
  });
67052
67553
  reminderCooldowns.set(task.id, now);
67053
- log(`[${HOOK_NAME16}] Reminder injected`, { taskId: task.id, sessionID: mainSessionID });
67554
+ log(`[${HOOK_NAME17}] Reminder injected`, { taskId: task.id, sessionID: mainSessionID });
67054
67555
  } catch (error51) {
67055
- log(`[${HOOK_NAME16}] Reminder injection failed`, { taskId: task.id, error: String(error51) });
67556
+ log(`[${HOOK_NAME17}] Reminder injection failed`, { taskId: task.id, error: String(error51) });
67056
67557
  }
67057
67558
  }
67058
67559
  };
@@ -67203,7 +67704,7 @@ function createWebFetchRedirectGuardHook(_ctx) {
67203
67704
  }
67204
67705
  // src/hooks/write-existing-file-guard/hook.ts
67205
67706
  import { existsSync as existsSync46 } from "fs";
67206
- import { isAbsolute as isAbsolute4, join as join54, normalize, resolve as resolve7, sep } from "path";
67707
+ import { isAbsolute as isAbsolute4, join as join53, normalize, resolve as resolve7, sep } from "path";
67207
67708
  function createWriteExistingFileGuardHook(ctx) {
67208
67709
  return {
67209
67710
  "tool.execute.before": async (input, output) => {
@@ -67218,7 +67719,7 @@ function createWriteExistingFileGuardHook(ctx) {
67218
67719
  }
67219
67720
  const resolvedPath = normalize(isAbsolute4(filePath) ? filePath : resolve7(ctx.directory, filePath));
67220
67721
  if (existsSync46(resolvedPath)) {
67221
- const matrixxRoot = join54(ctx.directory, ".matrixx") + sep;
67722
+ const matrixxRoot = join53(ctx.directory, ".matrixx") + sep;
67222
67723
  const isMatrixMarkdown = resolvedPath.startsWith(matrixxRoot) && resolvedPath.endsWith(".md");
67223
67724
  if (isMatrixMarkdown) {
67224
67725
  log("[write-existing-file-guard] Allowing .matrixx/*.md overwrite", {
@@ -67281,7 +67782,12 @@ function createContinuationHooks(args) {
67281
67782
  const stopContinuationGuard = isHookEnabled("stop-continuation-guard") ? safeHook("stop-continuation-guard", () => createStopContinuationGuardHook(ctx, { backgroundManager })) : null;
67282
67783
  const compactionContextInjector = isHookEnabled("compaction-context-injector") ? safeHook("compaction-context-injector", () => createCompactionContextInjector(backgroundManager)) : null;
67283
67784
  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, {
67785
+ const isTaskSystem = isTaskSystemEnabled(pluginConfig);
67786
+ const todoContinuationEnforcer = !isTaskSystem && isHookEnabled("todo-continuation-enforcer") ? safeHook("todo-continuation-enforcer", () => createTodoContinuationEnforcer(ctx, {
67787
+ backgroundManager,
67788
+ isContinuationStopped: stopContinuationGuard?.isStopped
67789
+ })) : null;
67790
+ const taskContinuationEnforcer = isTaskSystem && isHookEnabled("task-continuation-enforcer") ? safeHook("task-continuation-enforcer", () => createTaskContinuationEnforcer(ctx, {
67285
67791
  backgroundManager,
67286
67792
  isContinuationStopped: stopContinuationGuard?.isStopped
67287
67793
  })) : null;
@@ -67293,6 +67799,10 @@ function createContinuationHooks(args) {
67293
67799
  onAbortCallbacks.push(todoContinuationEnforcer.markRecovering);
67294
67800
  onRecoveryCompleteCallbacks.push(todoContinuationEnforcer.markRecoveryComplete);
67295
67801
  }
67802
+ if (taskContinuationEnforcer) {
67803
+ onAbortCallbacks.push(taskContinuationEnforcer.markRecovering);
67804
+ onRecoveryCompleteCallbacks.push(taskContinuationEnforcer.markRecoveryComplete);
67805
+ }
67296
67806
  if (onAbortCallbacks.length > 0) {
67297
67807
  sessionRecovery.setOnAbortCallback((sessionID) => {
67298
67808
  for (const callback of onAbortCallbacks)
@@ -67321,6 +67831,7 @@ function createContinuationHooks(args) {
67321
67831
  compactionContextInjector,
67322
67832
  compactionTodoPreserver,
67323
67833
  todoContinuationEnforcer,
67834
+ taskContinuationEnforcer,
67324
67835
  unstableAgentBabysitter,
67325
67836
  backgroundNotificationHook,
67326
67837
  architectHook,
@@ -67638,7 +68149,7 @@ function getLanguageId(ext) {
67638
68149
  }
67639
68150
  // src/tools/lsp/server-installation.ts
67640
68151
  import { existsSync as existsSync47 } from "fs";
67641
- import { join as join55 } from "path";
68152
+ import { join as join54 } from "path";
67642
68153
  function isServerInstalled(command) {
67643
68154
  if (command.length === 0)
67644
68155
  return false;
@@ -67666,23 +68177,23 @@ function isServerInstalled(command) {
67666
68177
  const paths = pathEnv.split(pathSeparator);
67667
68178
  for (const p of paths) {
67668
68179
  for (const suffix of exts) {
67669
- if (existsSync47(join55(p, cmd + suffix))) {
68180
+ if (existsSync47(join54(p, cmd + suffix))) {
67670
68181
  return true;
67671
68182
  }
67672
68183
  }
67673
68184
  }
67674
68185
  const cwd = process.cwd();
67675
68186
  const configDir = getOpenCodeConfigDir({ binary: "opencode" });
67676
- const dataDir = join55(getDataDir(), "opencode");
68187
+ const dataDir = join54(getDataDir(), "opencode");
67677
68188
  const additionalBases = [
67678
- join55(cwd, "node_modules", ".bin"),
67679
- join55(configDir, "bin"),
67680
- join55(configDir, "node_modules", ".bin"),
67681
- join55(dataDir, "bin")
68189
+ join54(cwd, "node_modules", ".bin"),
68190
+ join54(configDir, "bin"),
68191
+ join54(configDir, "node_modules", ".bin"),
68192
+ join54(dataDir, "bin")
67682
68193
  ];
67683
68194
  for (const base of additionalBases) {
67684
68195
  for (const suffix of exts) {
67685
- if (existsSync47(join55(base, cmd + suffix))) {
68196
+ if (existsSync47(join54(base, cmd + suffix))) {
67686
68197
  return true;
67687
68198
  }
67688
68199
  }
@@ -67694,7 +68205,7 @@ function isServerInstalled(command) {
67694
68205
  }
67695
68206
  // src/tools/lsp/server-config-loader.ts
67696
68207
  import { existsSync as existsSync48, readFileSync as readFileSync34 } from "fs";
67697
- import { join as join56 } from "path";
68208
+ import { join as join55 } from "path";
67698
68209
  function loadJsonFile(path12) {
67699
68210
  if (!existsSync48(path12))
67700
68211
  return null;
@@ -67708,9 +68219,9 @@ function getConfigPaths3() {
67708
68219
  const cwd = process.cwd();
67709
68220
  const configDir = getOpenCodeConfigDir({ binary: "opencode" });
67710
68221
  return {
67711
- project: detectConfigFile(join56(cwd, ".opencode", "matrixx")).path,
67712
- user: detectConfigFile(join56(configDir, "matrixx")).path,
67713
- opencode: detectConfigFile(join56(configDir, "opencode")).path
68222
+ project: detectConfigFile(join55(cwd, ".opencode", "matrixx")).path,
68223
+ user: detectConfigFile(join55(configDir, "matrixx")).path,
68224
+ opencode: detectConfigFile(join55(configDir, "opencode")).path
67714
68225
  };
67715
68226
  }
67716
68227
  function loadAllConfigs() {
@@ -68559,7 +69070,7 @@ class LSPServerManager {
68559
69070
  var lspManager = LSPServerManager.getInstance();
68560
69071
  // src/tools/lsp/lsp-client-wrapper.ts
68561
69072
  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";
69073
+ import { dirname as dirname16, extname as extname3, join as join56, resolve as resolve9 } from "path";
68563
69074
  import { fileURLToPath as fileURLToPath3 } from "url";
68564
69075
  function findWorkspaceRoot(filePath) {
68565
69076
  let dir = resolve9(filePath);
@@ -68575,7 +69086,7 @@ function findWorkspaceRoot(filePath) {
68575
69086
  let prevDir = "";
68576
69087
  while (dir !== prevDir) {
68577
69088
  for (const marker of markers) {
68578
- if (existsSync50(join57(dir, marker))) {
69089
+ if (existsSync50(join56(dir, marker))) {
68579
69090
  return dir;
68580
69091
  }
68581
69092
  }
@@ -81420,20 +81931,20 @@ var CLI_LANGUAGES = [
81420
81931
  "tsx",
81421
81932
  "yaml"
81422
81933
  ];
81423
- var DEFAULT_TIMEOUT_MS3 = 300000;
81934
+ var DEFAULT_TIMEOUT_MS2 = 300000;
81424
81935
  var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
81425
81936
  var DEFAULT_MAX_MATCHES = 500;
81426
81937
 
81427
81938
  // src/tools/ast-grep/sg-cli-path.ts
81428
81939
  import { existsSync as existsSync52, statSync as statSync11 } from "fs";
81429
81940
  import { createRequire as createRequire4 } from "module";
81430
- import { dirname as dirname17, join as join59 } from "path";
81941
+ import { dirname as dirname17, join as join58 } from "path";
81431
81942
 
81432
81943
  // src/tools/ast-grep/downloader.ts
81433
81944
  import { existsSync as existsSync51 } from "fs";
81434
81945
  import { createRequire as createRequire3 } from "module";
81435
- import { homedir as homedir10 } from "os";
81436
- import { join as join58 } from "path";
81946
+ import { homedir as homedir9 } from "os";
81947
+ import { join as join57 } from "path";
81437
81948
  init_logger();
81438
81949
  var REPO2 = "ast-grep/ast-grep";
81439
81950
  var DEFAULT_VERSION = "0.40.0";
@@ -81458,12 +81969,12 @@ var PLATFORM_MAP2 = {
81458
81969
  function getCacheDir3() {
81459
81970
  if (process.platform === "win32") {
81460
81971
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
81461
- const base2 = localAppData || join58(homedir10(), "AppData", "Local");
81462
- return join58(base2, "matrixx", "bin");
81972
+ const base2 = localAppData || join57(homedir9(), "AppData", "Local");
81973
+ return join57(base2, "matrixx", "bin");
81463
81974
  }
81464
81975
  const xdgCache = process.env.XDG_CACHE_HOME;
81465
- const base = xdgCache || join58(homedir10(), ".cache");
81466
- return join58(base, "matrixx", "bin");
81976
+ const base = xdgCache || join57(homedir9(), ".cache");
81977
+ return join57(base, "matrixx", "bin");
81467
81978
  }
81468
81979
  function getBinaryName3() {
81469
81980
  return process.platform === "win32" ? "sg.exe" : "sg";
@@ -81480,7 +81991,7 @@ async function downloadAstGrep(version3 = DEFAULT_VERSION) {
81480
81991
  }
81481
81992
  const cacheDir = getCacheDir3();
81482
81993
  const binaryName = getBinaryName3();
81483
- const binaryPath = join58(cacheDir, binaryName);
81994
+ const binaryPath = join57(cacheDir, binaryName);
81484
81995
  if (existsSync51(binaryPath)) {
81485
81996
  return binaryPath;
81486
81997
  }
@@ -81489,7 +82000,7 @@ async function downloadAstGrep(version3 = DEFAULT_VERSION) {
81489
82000
  const downloadUrl = `https://github.com/${REPO2}/releases/download/${version3}/${assetName}`;
81490
82001
  log(`[matrixx] Downloading ast-grep binary...`);
81491
82002
  try {
81492
- const archivePath = join58(cacheDir, assetName);
82003
+ const archivePath = join57(cacheDir, assetName);
81493
82004
  ensureCacheDir(cacheDir);
81494
82005
  await downloadArchive(downloadUrl, archivePath);
81495
82006
  await extractZipArchive(archivePath, cacheDir);
@@ -81543,7 +82054,7 @@ function findSgCliPathSync() {
81543
82054
  const require2 = createRequire4(import.meta.url);
81544
82055
  const cliPackageJsonPath = require2.resolve("@ast-grep/cli/package.json");
81545
82056
  const cliDirectory = dirname17(cliPackageJsonPath);
81546
- const sgPath = join59(cliDirectory, binaryName);
82057
+ const sgPath = join58(cliDirectory, binaryName);
81547
82058
  if (existsSync52(sgPath) && isValidBinary(sgPath)) {
81548
82059
  return sgPath;
81549
82060
  }
@@ -81555,7 +82066,7 @@ function findSgCliPathSync() {
81555
82066
  const packageJsonPath = require2.resolve(`${platformPackage}/package.json`);
81556
82067
  const packageDirectory = dirname17(packageJsonPath);
81557
82068
  const astGrepBinaryName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
81558
- const binaryPath = join59(packageDirectory, astGrepBinaryName);
82069
+ const binaryPath = join58(packageDirectory, astGrepBinaryName);
81559
82070
  if (existsSync52(binaryPath) && isValidBinary(binaryPath)) {
81560
82071
  return binaryPath;
81561
82072
  }
@@ -81715,7 +82226,7 @@ async function runSg(options) {
81715
82226
  };
81716
82227
  }
81717
82228
  }
81718
- const timeout = DEFAULT_TIMEOUT_MS3;
82229
+ const timeout = DEFAULT_TIMEOUT_MS2;
81719
82230
  const proc = spawn9([cliPath, ...args], {
81720
82231
  stdout: "pipe",
81721
82232
  stderr: "pipe"
@@ -83818,8 +84329,6 @@ function createBddPipelineTool(opts) {
83818
84329
  });
83819
84330
  }
83820
84331
  function makeDeps(args) {
83821
- const featureName = args.featurePath.split("/").pop()?.replace(/\.feature$/i, "") ?? "feature";
83822
- const _featureDir = resolvePath2(args.outDir, featureName);
83823
84332
  return {
83824
84333
  parseFeature: async (path12) => {
83825
84334
  const raw = await args.parseTool.execute({ filePath: path12 }, dummyCtx(args));
@@ -83988,17 +84497,17 @@ var {spawn: spawn10 } = globalThis.Bun;
83988
84497
  // src/tools/grep/constants.ts
83989
84498
  import { spawnSync as spawnSync2 } from "child_process";
83990
84499
  import { existsSync as existsSync59 } from "fs";
83991
- import { dirname as dirname18, join as join61 } from "path";
84500
+ import { dirname as dirname18, join as join60 } from "path";
83992
84501
 
83993
84502
  // src/tools/grep/downloader.ts
83994
- import { existsSync as existsSync58, readdirSync as readdirSync17 } from "fs";
83995
- import { join as join60 } from "path";
84503
+ import { existsSync as existsSync58, readdirSync as readdirSync18 } from "fs";
84504
+ import { join as join59 } from "path";
83996
84505
  function findFileRecursive(dir, filename) {
83997
84506
  try {
83998
- const entries = readdirSync17(dir, { withFileTypes: true, recursive: true });
84507
+ const entries = readdirSync18(dir, { withFileTypes: true, recursive: true });
83999
84508
  for (const entry of entries) {
84000
84509
  if (entry.isFile() && entry.name === filename) {
84001
- return join60(entry.parentPath ?? dir, entry.name);
84510
+ return join59(entry.parentPath ?? dir, entry.name);
84002
84511
  }
84003
84512
  }
84004
84513
  } catch {
@@ -84019,11 +84528,11 @@ function getPlatformKey() {
84019
84528
  }
84020
84529
  function getInstallDir() {
84021
84530
  const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
84022
- return join60(homeDir, ".cache", "matrixx", "bin");
84531
+ return join59(homeDir, ".cache", "matrixx", "bin");
84023
84532
  }
84024
84533
  function getRgPath() {
84025
84534
  const isWindows = process.platform === "win32";
84026
- return join60(getInstallDir(), isWindows ? "rg.exe" : "rg");
84535
+ return join59(getInstallDir(), isWindows ? "rg.exe" : "rg");
84027
84536
  }
84028
84537
  async function extractTarGz2(archivePath, destDir) {
84029
84538
  const platformKey = getPlatformKey();
@@ -84040,7 +84549,7 @@ async function extractZip2(archivePath, destDir) {
84040
84549
  const binaryName = process.platform === "win32" ? "rg.exe" : "rg";
84041
84550
  const foundPath = findFileRecursive(destDir, binaryName);
84042
84551
  if (foundPath) {
84043
- const destPath = join60(destDir, binaryName);
84552
+ const destPath = join59(destDir, binaryName);
84044
84553
  if (foundPath !== destPath) {
84045
84554
  const { renameSync: renameSync4 } = await import("fs");
84046
84555
  renameSync4(foundPath, destPath);
@@ -84061,7 +84570,7 @@ async function downloadAndInstallRipgrep() {
84061
84570
  ensureCacheDir(installDir);
84062
84571
  const filename = `ripgrep-${RG_VERSION}-${config4.platform}.${config4.extension}`;
84063
84572
  const url3 = `https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${filename}`;
84064
- const archivePath = join60(installDir, filename);
84573
+ const archivePath = join59(installDir, filename);
84065
84574
  try {
84066
84575
  await downloadArchive(url3, archivePath);
84067
84576
  if (config4.extension === "tar.gz") {
@@ -84106,11 +84615,11 @@ function getOpenCodeBundledRg() {
84106
84615
  const isWindows = process.platform === "win32";
84107
84616
  const rgName = isWindows ? "rg.exe" : "rg";
84108
84617
  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)
84618
+ join60(getDataDir(), "opencode", "bin", rgName),
84619
+ join60(execDir, rgName),
84620
+ join60(execDir, "bin", rgName),
84621
+ join60(execDir, "..", "bin", rgName),
84622
+ join60(execDir, "..", "libexec", rgName)
84114
84623
  ];
84115
84624
  for (const candidate of candidates) {
84116
84625
  if (existsSync59(candidate)) {
@@ -84166,7 +84675,7 @@ var DEFAULT_MAX_DEPTH = 20;
84166
84675
  var DEFAULT_MAX_FILESIZE = "10M";
84167
84676
  var DEFAULT_MAX_COUNT = 500;
84168
84677
  var DEFAULT_MAX_COLUMNS = 1000;
84169
- var DEFAULT_TIMEOUT_MS4 = 300000;
84678
+ var DEFAULT_TIMEOUT_MS3 = 300000;
84170
84679
  var DEFAULT_MAX_OUTPUT_BYTES2 = 10 * 1024 * 1024;
84171
84680
  var RG_SAFETY_FLAGS = [
84172
84681
  "--no-follow",
@@ -84178,7 +84687,7 @@ var RG_SAFETY_FLAGS = [
84178
84687
  var GREP_SAFETY_FLAGS = ["-n", "-H", "--color=never"];
84179
84688
 
84180
84689
  // src/tools/glob/constants.ts
84181
- var DEFAULT_TIMEOUT_MS5 = 60000;
84690
+ var DEFAULT_TIMEOUT_MS4 = 60000;
84182
84691
  var DEFAULT_LIMIT = 100;
84183
84692
  var DEFAULT_MAX_DEPTH2 = 20;
84184
84693
  var DEFAULT_MAX_OUTPUT_BYTES3 = 10 * 1024 * 1024;
@@ -84241,7 +84750,7 @@ async function getFileMtime(filePath) {
84241
84750
  }
84242
84751
  async function runRgFiles(options, resolvedCli) {
84243
84752
  const cli = resolvedCli ?? resolveGrepCli();
84244
- const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS5, DEFAULT_TIMEOUT_MS5);
84753
+ const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS4, DEFAULT_TIMEOUT_MS4);
84245
84754
  const limit = Math.min(options.limit ?? DEFAULT_LIMIT, DEFAULT_LIMIT);
84246
84755
  const isRg = cli.backend === "rg";
84247
84756
  const isWindows = process.platform === "win32";
@@ -84463,7 +84972,7 @@ function parseOutput(output) {
84463
84972
  async function runRg(options) {
84464
84973
  const cli = resolveGrepCli();
84465
84974
  const args = buildArgs(options, cli.backend);
84466
- const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS4, DEFAULT_TIMEOUT_MS4);
84975
+ const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS3, DEFAULT_TIMEOUT_MS3);
84467
84976
  if (cli.backend === "rg") {
84468
84977
  args.push("--", options.pattern);
84469
84978
  } else {
@@ -84618,12 +85127,12 @@ var HandoffSchema = exports_external.object({
84618
85127
  });
84619
85128
  // src/features/handoff/storage.ts
84620
85129
  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";
85130
+ import { join as join61 } from "path";
84622
85131
  function getHandoffFilePath(directory) {
84623
- return join62(directory, HANDOFF_FILE_PATH);
85132
+ return join61(directory, HANDOFF_FILE_PATH);
84624
85133
  }
84625
85134
  function getHandoffConsumedFilePath(directory) {
84626
- return join62(directory, HANDOFF_CONSUMED_FILE_PATH);
85135
+ return join61(directory, HANDOFF_CONSUMED_FILE_PATH);
84627
85136
  }
84628
85137
  function handoffFileExists(directory) {
84629
85138
  try {
@@ -84661,7 +85170,7 @@ function writeHandoffFile(directory, content) {
84661
85170
  }
84662
85171
  function ensureHandoffDir(directory) {
84663
85172
  try {
84664
- mkdirSync17(join62(directory, ".matrixx"), { recursive: true });
85173
+ mkdirSync17(join61(directory, ".matrixx"), { recursive: true });
84665
85174
  return true;
84666
85175
  } catch {
84667
85176
  return false;
@@ -84816,20 +85325,20 @@ ${body}
84816
85325
  }
84817
85326
 
84818
85327
  // 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";
85328
+ import { existsSync as existsSync61, readdirSync as readdirSync19, statSync as statSync13 } from "fs";
85329
+ import { join as join62 } from "path";
84821
85330
  var MATRIX_DIR = ".matrixx";
84822
85331
  function isHandoffFile(name) {
84823
85332
  return name === "handoff.md" || name.startsWith("handoff.consumed");
84824
85333
  }
84825
85334
  function handleList(directory) {
84826
- const matrixxDir = join63(directory, MATRIX_DIR);
85335
+ const matrixxDir = join62(directory, MATRIX_DIR);
84827
85336
  if (!existsSync61(matrixxDir)) {
84828
85337
  return "No handoffs found in .matrixx/";
84829
85338
  }
84830
85339
  let entries;
84831
85340
  try {
84832
- entries = readdirSync18(matrixxDir);
85341
+ entries = readdirSync19(matrixxDir);
84833
85342
  } catch {
84834
85343
  return "No handoffs found in .matrixx/";
84835
85344
  }
@@ -84841,7 +85350,7 @@ function handleList(directory) {
84841
85350
  lines.push(`Handoff files in ${matrixxDir}:`);
84842
85351
  lines.push("");
84843
85352
  for (const name of handoffs) {
84844
- const fullPath = join63(matrixxDir, name);
85353
+ const fullPath = join62(matrixxDir, name);
84845
85354
  let size = "?";
84846
85355
  let mtime = "?";
84847
85356
  try {
@@ -84938,7 +85447,7 @@ function createHandoffTools(ctx) {
84938
85447
  init_logger();
84939
85448
 
84940
85449
  // src/tools/interactive-bash/constants.ts
84941
- var DEFAULT_TIMEOUT_MS6 = 60000;
85450
+ var DEFAULT_TIMEOUT_MS5 = 60000;
84942
85451
  var BLOCKED_TMUX_SUBCOMMANDS = [
84943
85452
  "capture-pane",
84944
85453
  "capturep",
@@ -85035,7 +85544,7 @@ The Bash tool can execute these commands directly. Do NOT retry with interactive
85035
85544
  });
85036
85545
  const timeoutPromise = new Promise((_, reject) => {
85037
85546
  const id = setTimeout(() => {
85038
- const timeoutError = new Error(`Timeout after ${DEFAULT_TIMEOUT_MS6}ms`);
85547
+ const timeoutError = new Error(`Timeout after ${DEFAULT_TIMEOUT_MS5}ms`);
85039
85548
  try {
85040
85549
  proc.kill();
85041
85550
  proc.exited.catch((err) => {
@@ -85043,7 +85552,7 @@ The Bash tool can execute these commands directly. Do NOT retry with interactive
85043
85552
  });
85044
85553
  } catch {}
85045
85554
  reject(timeoutError);
85046
- }, DEFAULT_TIMEOUT_MS6);
85555
+ }, DEFAULT_TIMEOUT_MS5);
85047
85556
  proc.exited.then(() => clearTimeout(id)).catch(() => clearTimeout(id));
85048
85557
  });
85049
85558
  const [stdout, stderr, exitCode] = await Promise.race([
@@ -85067,7 +85576,7 @@ The Bash tool can execute these commands directly. Do NOT retry with interactive
85067
85576
  // src/tools/pdf-extract-figures/tools.ts
85068
85577
  import { existsSync as existsSync62, mkdtempSync, writeFileSync as writeFileSync24 } from "fs";
85069
85578
  import { tmpdir as tmpdir5 } from "os";
85070
- import { join as join64 } from "path";
85579
+ import { join as join63 } from "path";
85071
85580
  var PYTHON_SCRIPT = `#!/usr/bin/env python3
85072
85581
  import sys
85073
85582
  import json
@@ -85218,11 +85727,11 @@ async function runExtraction(args) {
85218
85727
  }
85219
85728
  let tmpDir;
85220
85729
  try {
85221
- tmpDir = mkdtempSync(join64(tmpdir5(), "matrixx-pdf-extract-"));
85730
+ tmpDir = mkdtempSync(join63(tmpdir5(), "matrixx-pdf-extract-"));
85222
85731
  } catch {
85223
85732
  return "Error: Could not create temporary directory.";
85224
85733
  }
85225
- const scriptPath = join64(tmpDir, "extract_pdf_figures.py");
85734
+ const scriptPath = join63(tmpDir, "extract_pdf_figures.py");
85226
85735
  writeFileSync24(scriptPath, PYTHON_SCRIPT);
85227
85736
  const payload = buildArgsPayload(args);
85228
85737
  log("[pdf_extract_figures] Running extraction", { file_path: args.file_path, payload });
@@ -85270,9 +85779,9 @@ function createPdfExtractFiguresTool() {
85270
85779
  return { pdf_extract_figures };
85271
85780
  }
85272
85781
  // 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");
85782
+ import { join as join64 } from "path";
85783
+ var TODO_DIR = join64(getOpenCodeConfigDir({ binary: "opencode" }), "todos");
85784
+ var TRANSCRIPT_DIR2 = join64(getOpenCodeConfigDir({ binary: "opencode" }), "transcripts");
85276
85785
  var SESSION_LIST_DESCRIPTION = `List all OpenCode sessions with optional filtering.
85277
85786
 
85278
85787
  Returns a list of available session IDs with metadata including message count, date range, and agents used.
@@ -85346,7 +85855,7 @@ Has Transcript: Yes (234 entries)`;
85346
85855
  // src/tools/session-manager/storage.ts
85347
85856
  import { existsSync as existsSync63 } from "fs";
85348
85857
  import { readdir, readFile as readFile4 } from "fs/promises";
85349
- import { join as join66 } from "path";
85858
+ import { join as join65 } from "path";
85350
85859
  var sdkClient = null;
85351
85860
  function setStorageClient(client2) {
85352
85861
  sdkClient = client2;
@@ -85373,13 +85882,13 @@ async function getMainSessions(options) {
85373
85882
  for (const projectDir of projectDirs) {
85374
85883
  if (!projectDir.isDirectory())
85375
85884
  continue;
85376
- const projectPath = join66(SESSION_STORAGE, projectDir.name);
85885
+ const projectPath = join65(SESSION_STORAGE, projectDir.name);
85377
85886
  const sessionFiles = await readdir(projectPath);
85378
85887
  for (const file3 of sessionFiles) {
85379
85888
  if (!file3.endsWith(".json"))
85380
85889
  continue;
85381
85890
  try {
85382
- const content = await readFile4(join66(projectPath, file3), "utf-8");
85891
+ const content = await readFile4(join65(projectPath, file3), "utf-8");
85383
85892
  const meta3 = JSON.parse(content);
85384
85893
  if (meta3.parentID)
85385
85894
  continue;
@@ -85412,7 +85921,7 @@ async function getAllSessions() {
85412
85921
  const entries = await readdir(dir, { withFileTypes: true });
85413
85922
  for (const entry of entries) {
85414
85923
  if (entry.isDirectory()) {
85415
- const sessionPath = join66(dir, entry.name);
85924
+ const sessionPath = join65(dir, entry.name);
85416
85925
  const files = await readdir(sessionPath);
85417
85926
  if (files.some((f) => f.endsWith(".json"))) {
85418
85927
  sessions.push(entry.name);
@@ -85482,7 +85991,7 @@ async function readSessionMessages(sessionID) {
85482
85991
  if (!file3.endsWith(".json"))
85483
85992
  continue;
85484
85993
  try {
85485
- const content = await readFile4(join66(messageDir, file3), "utf-8");
85994
+ const content = await readFile4(join65(messageDir, file3), "utf-8");
85486
85995
  const meta3 = JSON.parse(content);
85487
85996
  const parts = await readParts2(meta3.id);
85488
85997
  messages2.push({
@@ -85506,7 +86015,7 @@ async function readSessionMessages(sessionID) {
85506
86015
  });
85507
86016
  }
85508
86017
  async function readParts2(messageID) {
85509
- const partDir = join66(PART_STORAGE, messageID);
86018
+ const partDir = join65(PART_STORAGE, messageID);
85510
86019
  if (!existsSync63(partDir))
85511
86020
  return [];
85512
86021
  const parts = [];
@@ -85516,7 +86025,7 @@ async function readParts2(messageID) {
85516
86025
  if (!file3.endsWith(".json"))
85517
86026
  continue;
85518
86027
  try {
85519
- const content = await readFile4(join66(partDir, file3), "utf-8");
86028
+ const content = await readFile4(join65(partDir, file3), "utf-8");
85520
86029
  parts.push(JSON.parse(content));
85521
86030
  } catch {}
85522
86031
  }
@@ -85547,7 +86056,7 @@ async function readSessionTodos(sessionID) {
85547
86056
  const todoFiles = allFiles.filter((f) => f.includes(sessionID) && f.endsWith(".json"));
85548
86057
  for (const file3 of todoFiles) {
85549
86058
  try {
85550
- const content = await readFile4(join66(TODO_DIR, file3), "utf-8");
86059
+ const content = await readFile4(join65(TODO_DIR, file3), "utf-8");
85551
86060
  const data = JSON.parse(content);
85552
86061
  if (Array.isArray(data)) {
85553
86062
  return data.map((item) => ({
@@ -85567,7 +86076,7 @@ async function readSessionTodos(sessionID) {
85567
86076
  async function readSessionTranscript(sessionID) {
85568
86077
  if (!existsSync63(TRANSCRIPT_DIR2))
85569
86078
  return 0;
85570
- const transcriptFile = join66(TRANSCRIPT_DIR2, `${sessionID}.jsonl`);
86079
+ const transcriptFile = join65(TRANSCRIPT_DIR2, `${sessionID}.jsonl`);
85571
86080
  if (!existsSync63(transcriptFile))
85572
86081
  return 0;
85573
86082
  try {
@@ -86078,17 +86587,17 @@ function createSkillTool(options = {}) {
86078
86587
  }
86079
86588
  var skill = createSkillTool();
86080
86589
  // 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";
86590
+ import { existsSync as existsSync64, readdirSync as readdirSync20, readFileSync as readFileSync40 } from "fs";
86591
+ import { basename as basename6, join as join66 } from "path";
86083
86592
  function discoverCommandsFromDir2(commandsDir, scope) {
86084
86593
  if (!existsSync64(commandsDir))
86085
86594
  return [];
86086
- const entries = readdirSync19(commandsDir, { withFileTypes: true });
86595
+ const entries = readdirSync20(commandsDir, { withFileTypes: true });
86087
86596
  const commands2 = [];
86088
86597
  for (const entry of entries) {
86089
86598
  if (!isMarkdownFile(entry))
86090
86599
  continue;
86091
- const commandPath = join67(commandsDir, entry.name);
86600
+ const commandPath = join66(commandsDir, entry.name);
86092
86601
  const commandName = basename6(entry.name, ".md");
86093
86602
  try {
86094
86603
  const content = readFileSync40(commandPath, "utf-8");
@@ -86115,8 +86624,8 @@ function discoverCommandsFromDir2(commandsDir, scope) {
86115
86624
  }
86116
86625
  function discoverCommandsSync(directory) {
86117
86626
  const configDir = getOpenCodeConfigDir({ binary: "opencode" });
86118
- const opencodeGlobalDir = join67(configDir, "command");
86119
- const opencodeProjectDir = join67(directory ?? process.cwd(), ".opencode", "command");
86627
+ const opencodeGlobalDir = join66(configDir, "command");
86628
+ const opencodeProjectDir = join66(directory ?? process.cwd(), ".opencode", "command");
86120
86629
  const opencodeGlobalCommands = discoverCommandsFromDir2(opencodeGlobalDir, "opencode");
86121
86630
  const opencodeProjectCommands = discoverCommandsFromDir2(opencodeProjectDir, "opencode-project");
86122
86631
  const builtinCommandsMap = loadBuiltinCommands();
@@ -87287,8 +87796,8 @@ function createAssemblyTool(options) {
87287
87796
  }
87288
87797
  // src/tools/dcp-switch-profile/tools.ts
87289
87798
  import { existsSync as existsSync65, writeFileSync as writeFileSync25 } from "fs";
87290
- import { homedir as homedir11 } from "os";
87291
- import { join as join68 } from "path";
87799
+ import { homedir as homedir10 } from "os";
87800
+ import { join as join67 } from "path";
87292
87801
 
87293
87802
  // src/config/schema/dcp.ts
87294
87803
  var DcpCompressOverrideSchema = exports_external.object({
@@ -87461,8 +87970,8 @@ var DcpConfigSchema = exports_external.object({
87461
87970
  });
87462
87971
 
87463
87972
  // 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");
87973
+ var DCP_PLUGIN_DIR = join67(homedir10(), ".config", "opencode", "node_modules", "@tarquinen", "opencode-dcp");
87974
+ var DCP_SYMLINK = join67(homedir10(), ".config", "opencode", "dcp.jsonc");
87466
87975
  var VALID_PROFILES = ["economy", "balanced", "performance", "ultimate"];
87467
87976
  function checkDcpInstalled() {
87468
87977
  if (!existsSync65(DCP_PLUGIN_DIR)) {
@@ -87871,6 +88380,11 @@ function createDelegateAgent(ctx, backgroundManager, disabledAgents = []) {
87871
88380
  }
87872
88381
  const normalizedAgent = args.subagent_type.toLowerCase();
87873
88382
  args = { ...args, subagent_type: normalizedAgent };
88383
+ const callerAgent = (toolCtx.agent || "").toLowerCase();
88384
+ const MOUSE_RESEARCH_ONLY = ["trinity", "operator"];
88385
+ if (callerAgent === "mouse" && !MOUSE_RESEARCH_ONLY.includes(normalizedAgent)) {
88386
+ 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.`;
88387
+ }
87874
88388
  if (disabledAgents.some((disabled) => disabled.toLowerCase() === normalizedAgent)) {
87875
88389
  return `Error: Agent "${normalizedAgent}" is disabled via disabled_agents configuration. Remove it from disabled_agents in your matrixx.json to use it.`;
87876
88390
  }
@@ -91338,6 +91852,109 @@ Original error: ${createResult.error}`;
91338
91852
  }
91339
91853
  });
91340
91854
  }
91855
+ // src/tools/task/task-cleanup.ts
91856
+ import { existsSync as existsSync66, readdirSync as readdirSync21, readFileSync as readFileSync41, unlinkSync as unlinkSync10 } from "fs";
91857
+ import { join as join68 } from "path";
91858
+ init_logger();
91859
+ function parseOlderThan(value) {
91860
+ const match = value.trim().match(/^(\d+)(d|h|m)$/);
91861
+ if (!match)
91862
+ return null;
91863
+ const amount = Number(match[1]);
91864
+ const unit = match[2];
91865
+ if (unit === "d")
91866
+ return amount * 24 * 60 * 60 * 1000;
91867
+ if (unit === "h")
91868
+ return amount * 60 * 60 * 1000;
91869
+ return amount * 60 * 1000;
91870
+ }
91871
+ function getTaskTimestamp(task) {
91872
+ const raw = task;
91873
+ const candidate = raw.time_updated ?? raw.time_created ?? raw.updatedAt ?? raw.createdAt ?? raw.timeUpdated ?? raw.timeCreated ?? null;
91874
+ if (candidate === null || candidate === undefined)
91875
+ return Date.now();
91876
+ if (typeof candidate === "number")
91877
+ return candidate;
91878
+ if (typeof candidate === "string") {
91879
+ const parsed = Date.parse(candidate);
91880
+ if (!Number.isNaN(parsed))
91881
+ return parsed;
91882
+ const asNum = Number(candidate);
91883
+ if (!Number.isNaN(asNum))
91884
+ return asNum;
91885
+ return Date.now();
91886
+ }
91887
+ return Date.now();
91888
+ }
91889
+ function createTaskCleanupTool(config4, ctx) {
91890
+ return tool({
91891
+ description: `Delete completed task files from storage.
91892
+
91893
+ Scans getTaskDir()/*.json, filters status==="completed" and optionally olderThan.
91894
+ olderThan supports "7d", "24h", "30m" format (regex ^(\\d+)(d|h|m)$).
91895
+ Returns counts: {deleted, remaining, deletedIds}`,
91896
+ args: {
91897
+ olderThan: tool.schema.string().optional().describe('Only delete completed tasks older than duration (e.g. "7d", "24h", "30m")'),
91898
+ all: tool.schema.boolean().optional().describe("Delete all completed tasks (default true when olderThan not set)")
91899
+ },
91900
+ execute: async (args, context) => {
91901
+ try {
91902
+ const olderThan = args.olderThan;
91903
+ let olderThanMs = null;
91904
+ if (olderThan !== undefined) {
91905
+ const parsed = parseOlderThan(olderThan);
91906
+ if (parsed === null) {
91907
+ return JSON.stringify({
91908
+ error: "validation_error",
91909
+ message: `Invalid olderThan format: "${olderThan}". Expected e.g. "7d", "24h", "30m"`
91910
+ });
91911
+ }
91912
+ olderThanMs = parsed;
91913
+ }
91914
+ const directory = context?.directory ?? ctx?.directory ?? process.cwd();
91915
+ const taskDir = getTaskDir(config4, directory);
91916
+ if (!existsSync66(taskDir)) {
91917
+ return JSON.stringify({ deleted: 0, remaining: 0, deletedIds: [] });
91918
+ }
91919
+ const files = readdirSync21(taskDir).filter((f) => f.endsWith(".json") && f.startsWith("T-"));
91920
+ const total = files.length;
91921
+ const deletedIds = [];
91922
+ for (const file3 of files) {
91923
+ const filePath = join68(taskDir, file3);
91924
+ let raw;
91925
+ try {
91926
+ const content = readFileSync41(filePath, "utf-8");
91927
+ raw = JSON.parse(content);
91928
+ } catch {
91929
+ continue;
91930
+ }
91931
+ if (raw.status !== "completed")
91932
+ continue;
91933
+ const id = typeof raw.id === "string" ? raw.id : file3.replace(".json", "");
91934
+ if (olderThanMs !== null) {
91935
+ const ts = getTaskTimestamp(raw);
91936
+ const age = Date.now() - ts;
91937
+ if (age <= olderThanMs)
91938
+ continue;
91939
+ }
91940
+ try {
91941
+ unlinkSync10(filePath);
91942
+ deletedIds.push(id);
91943
+ } catch (err) {
91944
+ log("[task-cleanup] Failed to delete", { file: file3, error: String(err) });
91945
+ }
91946
+ }
91947
+ return JSON.stringify({
91948
+ deleted: deletedIds.length,
91949
+ remaining: total - deletedIds.length,
91950
+ deletedIds
91951
+ });
91952
+ } catch (error95) {
91953
+ return JSON.stringify({ error: "internal_error", message: error95 instanceof Error ? error95.message : String(error95) });
91954
+ }
91955
+ }
91956
+ });
91957
+ }
91341
91958
  // src/tools/task/task-create.ts
91342
91959
  import { join as join69 } from "path";
91343
91960
  function createTaskCreateTool(config4, ctx) {
@@ -91371,8 +91988,12 @@ Calculate dependencies carefully to maximize parallel execution:
91371
91988
  async function handleCreate2(args, config4, ctx, context) {
91372
91989
  try {
91373
91990
  const validatedArgs = TaskCreateInputSchema.parse(args);
91374
- const taskDir = getTaskDir(config4);
91375
- const lock = acquireLock(taskDir);
91991
+ const directory = context?.directory ?? ctx?.directory ?? process.cwd();
91992
+ try {
91993
+ migrateLegacyTasksIfNeeded(config4, directory);
91994
+ } catch {}
91995
+ const taskDir = getTaskDir(config4, directory);
91996
+ const lock = await acquireLockWithRetry(taskDir);
91376
91997
  if (!lock.acquired) {
91377
91998
  return JSON.stringify({ error: "task_lock_unavailable" });
91378
91999
  }
@@ -91389,11 +92010,11 @@ async function handleCreate2(args, config4, ctx, context) {
91389
92010
  metadata: validatedArgs.metadata,
91390
92011
  repoURL: validatedArgs.repoURL,
91391
92012
  parentID: validatedArgs.parentID,
91392
- threadID: context.sessionID
92013
+ threadID: context.sessionID,
92014
+ projectRoot: directory
91393
92015
  };
91394
92016
  const validatedTask = TaskObjectSchema.parse(task);
91395
92017
  writeJsonAtomic(join69(taskDir, `${taskId}.json`), validatedTask);
91396
- await syncTaskTodoUpdate(ctx, validatedTask, context.sessionID);
91397
92018
  return JSON.stringify({
91398
92019
  task: {
91399
92020
  id: validatedTask.id,
@@ -91425,7 +92046,7 @@ function parseTaskId(id) {
91425
92046
  return null;
91426
92047
  return id;
91427
92048
  }
91428
- function createTaskGetTool(config4) {
92049
+ function createTaskGetTool(config4, ctx) {
91429
92050
  return tool({
91430
92051
  description: `Retrieve a task by ID.
91431
92052
 
@@ -91435,14 +92056,15 @@ Returns null if the task does not exist or the file is invalid.`,
91435
92056
  args: {
91436
92057
  id: tool.schema.string().describe("Task ID to retrieve (format: T-{uuid})")
91437
92058
  },
91438
- execute: async (args) => {
92059
+ execute: async (args, context) => {
91439
92060
  try {
91440
92061
  const validatedArgs = TaskGetInputSchema.parse(args);
91441
92062
  const taskId = parseTaskId(validatedArgs.id);
91442
92063
  if (!taskId) {
91443
92064
  return JSON.stringify({ error: "invalid_task_id" });
91444
92065
  }
91445
- const taskDir = getTaskDir(config4);
92066
+ const directory = context?.directory ?? ctx?.directory ?? process.cwd();
92067
+ const taskDir = getTaskDir(config4, directory);
91446
92068
  const taskPath = join70(taskDir, `${taskId}.json`);
91447
92069
  const task = readJsonSafe(taskPath, TaskObjectSchema);
91448
92070
  return JSON.stringify({ task: task ?? null });
@@ -91456,9 +92078,9 @@ Returns null if the task does not exist or the file is invalid.`,
91456
92078
  });
91457
92079
  }
91458
92080
  // src/tools/task/task-list.ts
91459
- import { existsSync as existsSync66, readdirSync as readdirSync20 } from "fs";
92081
+ import { existsSync as existsSync67, readdirSync as readdirSync22 } from "fs";
91460
92082
  import { join as join71 } from "path";
91461
- function createTaskList(config4) {
92083
+ function createTaskList(config4, ctx) {
91462
92084
  return tool({
91463
92085
  description: `List all active tasks with summary information.
91464
92086
 
@@ -91466,12 +92088,13 @@ Returns tasks excluding completed and deleted statuses by default.
91466
92088
  For each task's blockedBy field, filters to only include unresolved (non-completed) blockers.
91467
92089
  Returns summary format: id, subject, status, owner, blockedBy (not full description).`,
91468
92090
  args: {},
91469
- execute: async () => {
91470
- const taskDir = getTaskDir(config4);
91471
- if (!existsSync66(taskDir)) {
92091
+ execute: async (_args, context) => {
92092
+ const directory = context?.directory ?? ctx?.directory ?? process.cwd();
92093
+ const taskDir = getTaskDir(config4, directory);
92094
+ if (!existsSync67(taskDir)) {
91472
92095
  return JSON.stringify({ tasks: [] });
91473
92096
  }
91474
- const files = readdirSync20(taskDir).filter((f) => f.endsWith(".json") && f.startsWith("T-")).map((f) => f.replace(".json", ""));
92097
+ const files = readdirSync22(taskDir).filter((f) => f.endsWith(".json") && f.startsWith("T-")).map((f) => f.replace(".json", ""));
91475
92098
  if (files.length === 0) {
91476
92099
  return JSON.stringify({ tasks: [] });
91477
92100
  }
@@ -91516,8 +92139,6 @@ function createTaskUpdateTool(config4, ctx) {
91516
92139
 
91517
92140
  Supports updating: subject, description, status, activeForm, owner, metadata.
91518
92141
  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
92142
 
91522
92143
  **IMPORTANT - Dependency Management:**
91523
92144
  Use \`addBlockedBy\` to declare dependencies on other tasks.
@@ -91545,8 +92166,9 @@ async function handleUpdate(args, config4, ctx, context) {
91545
92166
  if (!taskId) {
91546
92167
  return JSON.stringify({ error: "invalid_task_id" });
91547
92168
  }
91548
- const taskDir = getTaskDir(config4);
91549
- const lock = acquireLock(taskDir);
92169
+ const directory = context?.directory ?? ctx?.directory ?? process.cwd();
92170
+ const taskDir = getTaskDir(config4, directory);
92171
+ const lock = await acquireLockWithRetry(taskDir);
91550
92172
  if (!lock.acquired) {
91551
92173
  return JSON.stringify({ error: "task_lock_unavailable" });
91552
92174
  }
@@ -91589,7 +92211,6 @@ async function handleUpdate(args, config4, ctx, context) {
91589
92211
  }
91590
92212
  const validatedTask = TaskObjectSchema.parse(task);
91591
92213
  writeJsonAtomic(taskPath, validatedTask);
91592
- await syncTaskTodoUpdate(ctx, validatedTask, context.sessionID);
91593
92214
  return JSON.stringify({ task: validatedTask });
91594
92215
  } finally {
91595
92216
  lock.release();
@@ -91722,14 +92343,13 @@ function createToolGuardHooks(args) {
91722
92343
  const envFileWriteGuard = isHookEnabled("env-file-write-guard") ? safeHook("env-file-write-guard", () => createEnvFileWriteGuardHook(pluginConfig.security?.env_file_guard)) : null;
91723
92344
  const jsonErrorRecovery = isHookEnabled("json-error-recovery") ? safeHook("json-error-recovery", () => createJsonErrorRecoveryHook(ctx)) : null;
91724
92345
  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;
91726
92346
  const readImageResizer = isHookEnabled("read-image-resizer") ? safeHook("read-image-resizer", () => createReadImageResizerHook(ctx)) : null;
91727
92347
  const webfetchRedirectGuard = isHookEnabled("webfetch-redirect-guard") ? safeHook("webfetch-redirect-guard", () => createWebFetchRedirectGuardHook(ctx)) : null;
91728
92348
  const hashlineEditDiffEnhancer = isHookEnabled("hashline-edit-diff-enhancer") ? safeHook("hashline-edit-diff-enhancer", () => createHashlineEditDiffEnhancerHook({ hashline_edit: { enabled: pluginConfig.experimental?.hashline_edit ?? false } })) : null;
91729
92349
  const qualityGate = isHookEnabled("quality-gate") ? safeHook("quality-gate", () => createQualityGateHook()) : null;
91730
92350
  const taskNotepad = isHookEnabled("task-notepad") ? safeHook("task-notepad", () => createTaskNotepadHook(ctx)) : null;
92351
+ const taskEditGuard = isHookEnabled("task-edit-guard") ? safeHook("task-edit-guard", () => createTaskEditGuardHook(ctx)) : null;
91731
92352
  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
92353
  return {
91734
92354
  commentChecker,
91735
92355
  toolOutputTruncator,
@@ -91744,14 +92364,13 @@ function createToolGuardHooks(args) {
91744
92364
  envFileWriteGuard,
91745
92365
  jsonErrorRecovery,
91746
92366
  bashFileReadGuard,
91747
- todoDescriptionOverride,
91748
92367
  readImageResizer,
91749
92368
  webfetchRedirectGuard,
91750
92369
  hashlineEditDiffEnhancer,
91751
92370
  qualityGate,
91752
92371
  taskNotepad,
91753
- evolutionWatcher,
91754
- taskTodoMirror
92372
+ taskEditGuard,
92373
+ evolutionWatcher
91755
92374
  };
91756
92375
  }
91757
92376
 
@@ -92205,19 +92824,19 @@ function detectRepetitiveToolUse(window2) {
92205
92824
  }
92206
92825
 
92207
92826
  // src/features/background-agent/message-dir.ts
92208
- import { existsSync as existsSync67, readdirSync as readdirSync21, readFileSync as readFileSync41 } from "fs";
92827
+ import { existsSync as existsSync68, readdirSync as readdirSync23, readFileSync as readFileSync42 } from "fs";
92209
92828
  import { join as join73 } from "path";
92210
92829
  var MESSAGE_DIR_CACHE_CAP = 1000;
92211
92830
  var messageDirCache = new Map;
92212
92831
  function computeMessageDir(sessionID) {
92213
- if (!existsSync67(MESSAGE_STORAGE))
92832
+ if (!existsSync68(MESSAGE_STORAGE))
92214
92833
  return null;
92215
92834
  const directPath = join73(MESSAGE_STORAGE, sessionID);
92216
- if (existsSync67(directPath))
92835
+ if (existsSync68(directPath))
92217
92836
  return directPath;
92218
- for (const dir of readdirSync21(MESSAGE_STORAGE)) {
92837
+ for (const dir of readdirSync23(MESSAGE_STORAGE)) {
92219
92838
  const sessionPath = join73(MESSAGE_STORAGE, dir, sessionID);
92220
- if (existsSync67(sessionPath))
92839
+ if (existsSync68(sessionPath))
92221
92840
  return sessionPath;
92222
92841
  }
92223
92842
  return null;
@@ -92251,11 +92870,11 @@ function hasPartialAgentOrModel(message) {
92251
92870
  }
92252
92871
  function findNearestMessageExcludingCompaction(messageDir) {
92253
92872
  try {
92254
- const files = readdirSync21(messageDir).filter((name) => name.endsWith(".json")).sort().reverse();
92873
+ const files = readdirSync23(messageDir).filter((name) => name.endsWith(".json")).sort().reverse();
92255
92874
  const parsedByFile = new Map;
92256
92875
  for (const file3 of files) {
92257
92876
  try {
92258
- const content = readFileSync41(join73(messageDir, file3), "utf-8");
92877
+ const content = readFileSync42(join73(messageDir, file3), "utf-8");
92259
92878
  const parsed = JSON.parse(content);
92260
92879
  parsedByFile.set(file3, parsed);
92261
92880
  if (hasFullAgentAndModel(parsed)) {
@@ -94695,6 +95314,12 @@ ${useWhen.map((w) => `| ${w} | Oracle FIRST, then implement |`).join(`
94695
95314
  ${avoidWhen.map((w) => `- ${w}`).join(`
94696
95315
  `)}
94697
95316
 
95317
+ ### HARD ROUTING RULE \u2014 Plan File Creation (NON-NEGOTIABLE):
95318
+ - ANY task that creates or edits \`.matrixx/plans/*.md\` MUST use \`task(subagent_type='oracle')\` \u2014 NEVER \`task(category='...')\` / Mouse
95319
+ - ANY prompt containing \`Oracle plan\`, \`work plan\`, \`proposal\`, \`TL;DR\`, \`root cause\`, \`pure-host\`, \`fix options\` or similar planning language MUST route to \`oracle\`
95320
+ - Mouse is a leaf executor and CANNOT produce plans. Oracle owns all plan generation. Mouse \u2192 Oracle is a violation (extra hop, diluted context).
95321
+ - If you already created tasks for research + drafting, the drafting task MUST still be \`oracle\`, not \`mouse\`.
95322
+
94698
95323
  ### Usage Pattern:
94699
95324
  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
95325
 
@@ -95823,8 +96448,8 @@ function createBddContractAgent(model) {
95823
96448
  createBddContractAgent.mode = MODE2;
95824
96449
 
95825
96450
  // 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";
96451
+ import { existsSync as existsSync69, readFileSync as readFileSync43 } from "fs";
96452
+ import { homedir as homedir11 } from "os";
95828
96453
  import { isAbsolute as isAbsolute5, resolve as resolve10 } from "path";
95829
96454
  function resolvePromptAppend(promptAppend, configDir) {
95830
96455
  if (!promptAppend.startsWith("file://"))
@@ -95833,16 +96458,16 @@ function resolvePromptAppend(promptAppend, configDir) {
95833
96458
  let filePath;
95834
96459
  try {
95835
96460
  const decoded = decodeURIComponent(encoded);
95836
- const expanded = decoded.startsWith("~/") ? decoded.replace(/^~\//, `${homedir12()}/`) : decoded;
96461
+ const expanded = decoded.startsWith("~/") ? decoded.replace(/^~\//, `${homedir11()}/`) : decoded;
95837
96462
  filePath = isAbsolute5(expanded) ? expanded : resolve10(configDir ?? process.cwd(), expanded);
95838
96463
  } catch {
95839
96464
  return `[WARNING: Malformed file URI (invalid percent-encoding): ${promptAppend}]`;
95840
96465
  }
95841
- if (!existsSync68(filePath)) {
96466
+ if (!existsSync69(filePath)) {
95842
96467
  return `[WARNING: Could not resolve file URI: ${promptAppend}]`;
95843
96468
  }
95844
96469
  try {
95845
- return readFileSync42(filePath, "utf8");
96470
+ return readFileSync43(filePath, "utf8");
95846
96471
  } catch {
95847
96472
  return `[WARNING: Could not read file: ${promptAppend}]`;
95848
96473
  }
@@ -97557,7 +98182,8 @@ var ORACLE_PLAN_BUILDER_METADATA = {
97557
98182
  { domain: "Architecture decisions", trigger: "Multi-system tradeoffs, unfamiliar patterns" },
97558
98183
  { domain: "Work planning", trigger: "Complex feature implementation, refactoring" },
97559
98184
  { domain: "Complex task breakdown", trigger: "Large ambiguous requests needing decomposition" },
97560
- { domain: "Self-review", trigger: "After completing significant implementation" }
98185
+ { domain: "Self-review", trigger: "After completing significant implementation" },
98186
+ { domain: "Plan generation", trigger: "ANY .matrixx/plans/*.md creation or planning doc \u2192 oracle ONLY, never Mouse" }
97561
98187
  ],
97562
98188
  useWhen: [
97563
98189
  "Complex architecture design",
@@ -100589,20 +101215,22 @@ function buildConstraintsSection(useTaskSystem) {
100589
101215
  return `<Critical_Constraints>
100590
101216
  BLOCKED ACTIONS (will fail if attempted):
100591
101217
  - task (agent delegation tool): BLOCKED \u2014 you cannot delegate work to other agents
101218
+ - delegate_agent with subagent_type=oracle/construct/merovingian/smith/sentinel/cipher: BLOCKED \u2014 Mouse is a leaf executor
100592
101219
 
100593
101220
  ALLOWED tools:
100594
- - delegate_agent: You CAN spawn explore/librarian agents for research
101221
+ - delegate_agent with subagent_type=trinity/operator ONLY: You MAY spawn explore/librarian agents for research
100595
101222
  - task_create, task_update, task_list, task_get: ALLOWED \u2014 use these for tracking your work
100596
101223
 
100597
- You work ALONE for implementation. No delegation of implementation tasks.
101224
+ 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
101225
  </Critical_Constraints>`;
100599
101226
  }
100600
101227
  return `<Critical_Constraints>
100601
101228
  BLOCKED ACTIONS (will fail if attempted):
100602
101229
  - task (agent delegation tool): BLOCKED \u2014 you cannot delegate work to other agents
101230
+ - delegate_agent with subagent_type=oracle/construct/merovingian/smith/sentinel/cipher: BLOCKED \u2014 Mouse is a leaf executor
100603
101231
 
100604
- ALLOWED: delegate_agent - You CAN spawn explore/librarian agents for research.
100605
- You work ALONE for implementation. No delegation of implementation tasks.
101232
+ ALLOWED: delegate_agent with subagent_type=trinity/operator ONLY - You MAY spawn explore/librarian agents for research.
101233
+ 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
101234
  </Critical_Constraints>`;
100607
101235
  }
100608
101236
  function buildTodoDisciplineSection2(useTaskSystem) {
@@ -100707,20 +101335,22 @@ function buildConstraintsSection2(useTaskSystem) {
100707
101335
  return `<Critical_Constraints>
100708
101336
  BLOCKED ACTIONS (will fail if attempted):
100709
101337
  - task (agent delegation tool): BLOCKED \u2014 you cannot delegate work to other agents
101338
+ - delegate_agent with subagent_type=oracle/construct/merovingian/smith/sentinel/cipher: BLOCKED \u2014 Mouse is a leaf executor
100710
101339
 
100711
101340
  ALLOWED tools:
100712
- - delegate_agent: You CAN spawn explore/librarian agents for research
101341
+ - delegate_agent with subagent_type=trinity/operator ONLY: You MAY spawn explore/librarian agents for research
100713
101342
  - task_create, task_update, task_list, task_get: ALLOWED \u2014 use these for tracking your work
100714
101343
 
100715
- You work ALONE for implementation. No delegation of implementation tasks.
101344
+ 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
101345
  </Critical_Constraints>`;
100717
101346
  }
100718
101347
  return `<Critical_Constraints>
100719
101348
  BLOCKED ACTIONS (will fail if attempted):
100720
101349
  - task (agent delegation tool): BLOCKED \u2014 you cannot delegate work to other agents
101350
+ - delegate_agent with subagent_type=oracle/construct/merovingian/smith/sentinel/cipher: BLOCKED \u2014 Mouse is a leaf executor
100721
101351
 
100722
- ALLOWED: delegate_agent - You CAN spawn explore/librarian agents for research.
100723
- You work ALONE for implementation. No delegation of implementation tasks.
101352
+ ALLOWED: delegate_agent with subagent_type=trinity/operator ONLY - You MAY spawn explore/librarian agents for research.
101353
+ 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
101354
  </Critical_Constraints>`;
100725
101355
  }
100726
101356
  function buildTodoDisciplineSection3(useTaskSystem) {
@@ -100821,17 +101451,18 @@ BLOCKED (will fail if attempted):
100821
101451
  | Tool | Status | Description |
100822
101452
  |------|--------|-------------|
100823
101453
  | task | BLOCKED | Agent delegation tool \u2014 you cannot spawn other agents |
101454
+ | delegate_agent with oracle/construct/merovingian/smith/sentinel/cipher | BLOCKED | Mouse is leaf \u2014 only trinity/operator allowed |
100824
101455
 
100825
101456
  ALLOWED:
100826
101457
  | Tool | Usage |
100827
101458
  |------|-------|
100828
- | delegate_agent | Spawn explore/librarian for research ONLY |
101459
+ | delegate_agent with trinity/operator ONLY | Spawn explore/librarian for research ONLY |
100829
101460
  | task_create | Create tasks to track your work |
100830
101461
  | task_update | Update task status (in_progress, completed) |
100831
101462
  | task_list | List active tasks |
100832
101463
  | task_get | Get task details by ID |
100833
101464
 
100834
- You work ALONE for implementation. No delegation.
101465
+ You work ALONE for implementation. No delegation. Plan files (.matrixx/plans/*.md) are OWNED by oracle \u2014 never create them via Mouse.
100835
101466
  </blocked_actions>`;
100836
101467
  }
100837
101468
  return `<blocked_actions>
@@ -100839,13 +101470,14 @@ BLOCKED (will fail if attempted):
100839
101470
  | Tool | Status | Description |
100840
101471
  |------|--------|-------------|
100841
101472
  | task | BLOCKED | Agent delegation tool \u2014 you cannot spawn other agents |
101473
+ | delegate_agent with oracle/construct/merovingian/smith/sentinel/cipher | BLOCKED | Mouse is leaf \u2014 only trinity/operator allowed |
100842
101474
 
100843
101475
  ALLOWED:
100844
101476
  | Tool | Usage |
100845
101477
  |------|-------|
100846
- | delegate_agent | Spawn explore/librarian for research ONLY |
101478
+ | delegate_agent with trinity/operator ONLY | Spawn explore/librarian for research ONLY |
100847
101479
 
100848
- You work ALONE for implementation. No delegation.
101480
+ You work ALONE for implementation. No delegation. Plan files (.matrixx/plans/*.md) are OWNED by oracle \u2014 never create them via Mouse.
100849
101481
  </blocked_actions>`;
100850
101482
  }
100851
101483
  function buildGptTaskDisciplineSection(useTaskSystem) {
@@ -100885,11 +101517,12 @@ Execute tasks directly. You NEVER delegate.
100885
101517
 
100886
101518
  <rules>
100887
101519
  - task tool: BLOCKED \u2014 cannot delegate to other agents
100888
- - delegate_agent: ALLOWED \u2014 only for research (explore/librarian)
101520
+ - delegate_agent with oracle/construct/merovingian/smith/sentinel/cipher: BLOCKED \u2014 Mouse is leaf, only trinity/operator allowed
101521
+ - delegate_agent with trinity/operator ONLY: ALLOWED for research (explore/librarian)
100889
101522
  - ${useTaskSystem ? "task_create/task_update" : "todowrite/todoread"}: REQUIRED for tracking
100890
101523
  - Implement ONLY what is requested \u2014 no scope creep
101524
+ - Plan files (.matrixx/plans/*.md): OWNED by oracle \u2014 never create via Mouse
100891
101525
  </rules>
100892
-
100893
101526
  ${taskDiscipline}
100894
101527
 
100895
101528
  <verify>
@@ -100981,15 +101614,21 @@ function buildQwenBlockedActions(useTaskSystem) {
100981
101614
  return `| Tool | Status | Notes |
100982
101615
  |------|--------|-------|
100983
101616
  | task | BLOCKED | Cannot delegate implementation |
100984
- | delegate_agent | ALLOWED | Research only (explore/librarian) |
101617
+ | delegate_agent with oracle/construct/merovingian/smith/sentinel/cipher | BLOCKED | Mouse is leaf \u2014 only trinity/operator allowed |
101618
+ | delegate_agent with trinity/operator ONLY | ALLOWED | Research only (explore/librarian) |
100985
101619
  | task_create | ALLOWED | Track your work |
100986
101620
  | task_update | ALLOWED | Update status |
100987
- | task_list / task_get | ALLOWED | View tasks |`;
101621
+ | task_list / task_get | ALLOWED | View tasks |
101622
+
101623
+ Plan files (.matrixx/plans/*.md) are OWNED by oracle \u2014 never create them via Mouse.`;
100988
101624
  }
100989
101625
  return `| Tool | Status | Notes |
100990
101626
  |------|--------|-------|
100991
101627
  | task | BLOCKED | Cannot delegate implementation |
100992
- | delegate_agent | ALLOWED | Research only (explore/librarian) |`;
101628
+ | delegate_agent with oracle/construct/merovingian/smith/sentinel/cipher | BLOCKED | Mouse is leaf \u2014 only trinity/operator allowed |
101629
+ | delegate_agent with trinity/operator ONLY | ALLOWED | Research only (explore/librarian) |
101630
+
101631
+ Plan files (.matrixx/plans/*.md) are OWNED by oracle \u2014 never create them via Mouse.`;
100993
101632
  }
100994
101633
  function buildQwenDisciplineSection(useTaskSystem) {
100995
101634
  if (useTaskSystem) {
@@ -101256,7 +101895,7 @@ async function applyAgentConfig(params) {
101256
101895
  if (!params.pluginConfig.tdd_enforcer?.enabled) {
101257
101896
  disabledSkills.add("tdd-enforcer");
101258
101897
  }
101259
- const useTaskSystem = params.pluginConfig.experimental?.task_system ?? false;
101898
+ const useTaskSystem = isTaskSystemEnabled(params.pluginConfig);
101260
101899
  const availableToolNames = getAvailableToolNames();
101261
101900
  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
101901
  const rawPluginAgents = params.pluginComponents.agents;
@@ -101562,7 +102201,9 @@ function agentByKey(agentResult, key) {
101562
102201
  return agentResult[key];
101563
102202
  }
101564
102203
  function applyToolConfig(params) {
101565
- const denyTodoTools = params.pluginConfig.experimental?.task_system ? { todowrite: "deny", todoread: "deny" } : {};
102204
+ const isTaskSystem = isTaskSystemEnabled(params.pluginConfig);
102205
+ const denyTodoTools = isTaskSystem ? { todowrite: "deny", todoread: "deny" } : {};
102206
+ const denyTaskTools = !isTaskSystem ? { "task_*": "deny", task: "deny" } : {};
101566
102207
  params.config.tools = {
101567
102208
  ...params.config.tools,
101568
102209
  "grep_app_*": false,
@@ -101571,7 +102212,7 @@ function applyToolConfig(params) {
101571
102212
  LspCodeActionResolve: false,
101572
102213
  "task_*": false,
101573
102214
  teammate: false,
101574
- ...params.pluginConfig.experimental?.task_system ? { todowrite: false, todoread: false } : {}
102215
+ ...isTaskSystem ? { todowrite: false, todoread: false } : { "task_*": false, task: false }
101575
102216
  };
101576
102217
  const isCliRunMode = process.env.OPENCODE_CLI_RUN_MODE === "true";
101577
102218
  const questionPermission = isCliRunMode ? "deny" : "allow";
@@ -101587,11 +102228,8 @@ function applyToolConfig(params) {
101587
102228
  if (architect) {
101588
102229
  architect.permission = {
101589
102230
  ...architect.permission,
101590
- task: "allow",
101591
102231
  delegate_agent: "deny",
101592
- "task_*": "allow",
101593
- teammate: "allow",
101594
- ...denyTodoTools
102232
+ ...isTaskSystem ? { task: "allow", "task_*": "allow", teammate: "allow", ...denyTodoTools } : { todowrite: "allow", todoread: "allow", ...denyTaskTools }
101595
102233
  };
101596
102234
  }
101597
102235
  const morpheus = agentByKey(params.agentResult, "morpheus");
@@ -101599,11 +102237,8 @@ function applyToolConfig(params) {
101599
102237
  morpheus.permission = {
101600
102238
  ...morpheus.permission,
101601
102239
  delegate_agent: "deny",
101602
- task: "allow",
101603
102240
  question: questionPermission,
101604
- "task_*": "allow",
101605
- teammate: "allow",
101606
- ...denyTodoTools
102241
+ ...isTaskSystem ? { task: "allow", "task_*": "allow", teammate: "allow", ...denyTodoTools } : { todowrite: "allow", todoread: "allow", ...denyTaskTools }
101607
102242
  };
101608
102243
  }
101609
102244
  const keymaker = agentByKey(params.agentResult, "keymaker");
@@ -101611,9 +102246,8 @@ function applyToolConfig(params) {
101611
102246
  keymaker.permission = {
101612
102247
  ...keymaker.permission,
101613
102248
  delegate_agent: "deny",
101614
- task: "allow",
101615
102249
  question: questionPermission,
101616
- ...denyTodoTools
102250
+ ...isTaskSystem ? { task: "allow", ...denyTodoTools } : { todowrite: "allow", todoread: "allow", ...denyTaskTools }
101617
102251
  };
101618
102252
  }
101619
102253
  const oracle = agentByKey(params.agentResult, "oracle");
@@ -101621,21 +102255,15 @@ function applyToolConfig(params) {
101621
102255
  oracle.permission = {
101622
102256
  ...oracle.permission,
101623
102257
  delegate_agent: "deny",
101624
- task: "allow",
101625
102258
  question: questionPermission,
101626
- "task_*": "allow",
101627
- teammate: "allow",
101628
- ...denyTodoTools
102259
+ ...isTaskSystem ? { task: "allow", "task_*": "allow", teammate: "allow", ...denyTodoTools } : { todowrite: "allow", todoread: "allow", ...denyTaskTools }
101629
102260
  };
101630
102261
  }
101631
102262
  const mouse = agentByKey(params.agentResult, "mouse");
101632
102263
  if (mouse) {
101633
102264
  mouse.permission = {
101634
102265
  ...mouse.permission,
101635
- task: "allow",
101636
- "task_*": "allow",
101637
- teammate: "allow",
101638
- ...denyTodoTools
102266
+ ...isTaskSystem ? { task: "allow", "task_*": "allow", teammate: "allow", ...denyTodoTools } : { todowrite: "allow", todoread: "allow", ...denyTaskTools }
101639
102267
  };
101640
102268
  }
101641
102269
  params.config.permission = {
@@ -101811,12 +102439,13 @@ function createToolRegistry(args) {
101811
102439
  skills: skillContext.builtinSkills,
101812
102440
  client: ctx.client
101813
102441
  });
101814
- const taskSystemEnabled = pluginConfig.experimental?.task_system ?? false;
102442
+ const taskSystemEnabled = isTaskSystemEnabled(pluginConfig);
101815
102443
  const taskToolsRecord = taskSystemEnabled ? {
101816
102444
  task_create: createTaskCreateTool(pluginConfig, ctx),
101817
- task_get: createTaskGetTool(pluginConfig),
101818
- task_list: createTaskList(pluginConfig),
101819
- task_update: createTaskUpdateTool(pluginConfig, ctx)
102445
+ task_get: createTaskGetTool(pluginConfig, ctx),
102446
+ task_list: createTaskList(pluginConfig, ctx),
102447
+ task_update: createTaskUpdateTool(pluginConfig, ctx),
102448
+ task_cleanup: createTaskCleanupTool(pluginConfig, ctx)
101820
102449
  } : {};
101821
102450
  const hashlineEnabled = pluginConfig.experimental?.hashline_edit ?? false;
101822
102451
  const hashlineToolsRecord = hashlineEnabled ? { edit: createHashlineEditTool(ctx) } : {};
@@ -102182,7 +102811,7 @@ var ExperimentalConfigSchema = exports_external.object({
102182
102811
  auto_resume: exports_external.boolean().optional(),
102183
102812
  preemptive_compaction: exports_external.boolean().optional(),
102184
102813
  truncate_all_tool_outputs: exports_external.boolean().optional(),
102185
- task_system: exports_external.boolean().optional(),
102814
+ task_system: exports_external.boolean().optional().default(true),
102186
102815
  plugin_load_timeout_ms: exports_external.number().min(1000).optional(),
102187
102816
  safe_hook_creation: exports_external.boolean().optional(),
102188
102817
  hashline_edit: exports_external.boolean().optional()
@@ -102196,6 +102825,7 @@ var HeadroomConfigSchema = exports_external.object({
102196
102825
  });
102197
102826
  // src/config/schema/hooks.ts
102198
102827
  var HookNameSchema = exports_external.enum([
102828
+ "task-continuation-enforcer",
102199
102829
  "todo-continuation-enforcer",
102200
102830
  "context-window-monitor",
102201
102831
  "session-recovery",
@@ -102243,7 +102873,6 @@ var HookNameSchema = exports_external.enum([
102243
102873
  "env-file-write-guard",
102244
102874
  "json-error-recovery",
102245
102875
  "bash-file-read-guard",
102246
- "todo-description-override",
102247
102876
  "runtime-fallback",
102248
102877
  "read-image-resizer",
102249
102878
  "webfetch-redirect-guard",
@@ -102251,14 +102880,14 @@ var HookNameSchema = exports_external.enum([
102251
102880
  "tool-pair-validator",
102252
102881
  "quality-gate",
102253
102882
  "task-notepad",
102254
- "task-todo-mirror",
102255
102883
  "design-intent-preserver",
102256
102884
  "rtk-bash-rewriter",
102257
102885
  "mcp-startup-notification",
102258
102886
  "evolution-watcher",
102259
102887
  "evolution-compressor",
102260
102888
  "evolution-quality-gate",
102261
- "evolution-hitl"
102889
+ "evolution-hitl",
102890
+ "task-edit-guard"
102262
102891
  ]);
102263
102892
  // src/config/schema/matrix-loop.ts
102264
102893
  var MatrixLoopConfigSchema = exports_external.object({
@@ -102276,7 +102905,8 @@ var MatrixxSelfConfigSkillConfigSchema = exports_external.object({
102276
102905
  var MorpheusTasksConfigSchema = exports_external.object({
102277
102906
  storage_path: exports_external.string().optional(),
102278
102907
  task_list_id: exports_external.string().optional(),
102279
- claude_code_compat: exports_external.boolean().default(false)
102908
+ claude_code_compat: exports_external.boolean().default(false),
102909
+ 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
102910
  });
102281
102911
  var MorpheusConfigSchema = exports_external.object({
102282
102912
  tasks: MorpheusTasksConfigSchema.optional()
@@ -102637,7 +103267,6 @@ function createChatMessageHandler2(args) {
102637
103267
  applyAgentVariant(pluginConfig, input.agent, message);
102638
103268
  }
102639
103269
  }
102640
- await hooks2.taskTodoMirror?.["chat.message"]?.(input, output);
102641
103270
  await hooks2.stopContinuationGuard?.["chat.message"]?.(input);
102642
103271
  await hooks2.keywordDetector?.["chat.message"]?.(input, output);
102643
103272
  await hooks2.autoSlashCommand?.["chat.message"]?.(input, output);
@@ -102787,8 +103416,8 @@ function createEventHandler2(args) {
102787
103416
  await Promise.resolve(hooks2.autoUpdateChecker?.event?.(input));
102788
103417
  await Promise.resolve(hooks2.backgroundNotificationHook?.event?.(input));
102789
103418
  await Promise.resolve(hooks2.sessionNotification?.(input));
102790
- await Promise.resolve(hooks2.taskTodoMirror?.event?.(input));
102791
103419
  await Promise.resolve(hooks2.todoContinuationEnforcer?.handler?.(input));
103420
+ await Promise.resolve(hooks2.taskContinuationEnforcer?.handler?.(input));
102792
103421
  await Promise.resolve(hooks2.unstableAgentBabysitter?.event?.(input));
102793
103422
  await Promise.resolve(hooks2.contextWindowMonitor?.event?.(input));
102794
103423
  await Promise.resolve(hooks2.directoryAgentsInjector?.event?.(input));
@@ -102938,7 +103567,6 @@ function setCachedTransform(sessionID, messagesHash, messages2) {
102938
103567
  }
102939
103568
  function createMessagesTransformHandler(args) {
102940
103569
  return async (input, output) => {
102941
- await args.hooks.taskTodoMirror?.["experimental.chat.messages.transform"]?.(input, output);
102942
103570
  const sessionID = extractSessionID(output.messages);
102943
103571
  if (!sessionID) {
102944
103572
  await args.hooks.contextInjectorMessagesTransform?.["experimental.chat.messages.transform"]?.(input, output);
@@ -102962,9 +103590,7 @@ function createMessagesTransformHandler(args) {
102962
103590
  // src/plugin/tool-definition.ts
102963
103591
  function createToolDefinitionHandler(args) {
102964
103592
  const { hooks: hooks2 } = args;
102965
- return async (input, output) => {
102966
- await hooks2.todoDescriptionOverride?.["tool.definition"]?.(input, output);
102967
- };
103593
+ return async (input, output) => {};
102968
103594
  }
102969
103595
 
102970
103596
  // src/plugin/tool-execute-after.ts
@@ -103001,7 +103627,6 @@ function createToolExecuteAfterHandler2(args) {
103001
103627
  await hooks2.jsonErrorRecovery?.["tool.execute.after"]?.(input, output);
103002
103628
  await hooks2.readImageResizer?.["tool.execute.after"]?.(input, output);
103003
103629
  await hooks2.taskNotepad?.["tool.execute.after"]?.(input, output);
103004
- await hooks2.taskTodoMirror?.["tool.execute.after"]?.(input, output);
103005
103630
  };
103006
103631
  await Promise.all([
103007
103632
  hooks2.preemptiveCompaction?.["tool.execute.after"]?.(input, output),
@@ -103013,22 +103638,22 @@ function createToolExecuteAfterHandler2(args) {
103013
103638
 
103014
103639
  // src/features/assembly-state/manager.ts
103015
103640
  init_logger();
103016
- var HOOK_NAME17 = "assembly-state";
103641
+ var HOOK_NAME18 = "assembly-state";
103017
103642
  var disabledSessions = new Set;
103018
103643
  function isAssemblyDisabled(sessionID) {
103019
103644
  return disabledSessions.has(sessionID);
103020
103645
  }
103021
103646
  function disableAssembly(sessionID) {
103022
103647
  disabledSessions.add(sessionID);
103023
- log(`[${HOOK_NAME17}] Assembly disabled for session`, { sessionID });
103648
+ log(`[${HOOK_NAME18}] Assembly disabled for session`, { sessionID });
103024
103649
  disabledSessions.add(sessionID);
103025
- log(`[${HOOK_NAME17}] Consensus disabled for session`, { sessionID });
103650
+ log(`[${HOOK_NAME18}] Consensus disabled for session`, { sessionID });
103026
103651
  }
103027
103652
  function enableAssembly(sessionID) {
103028
103653
  disabledSessions.delete(sessionID);
103029
- log(`[${HOOK_NAME17}] Assembly enabled for session`, { sessionID });
103654
+ log(`[${HOOK_NAME18}] Assembly enabled for session`, { sessionID });
103030
103655
  disabledSessions.delete(sessionID);
103031
- log(`[${HOOK_NAME17}] Consensus enabled for session`, { sessionID });
103656
+ log(`[${HOOK_NAME18}] Consensus enabled for session`, { sessionID });
103032
103657
  }
103033
103658
  // src/plugin/session-agent-resolver.ts
103034
103659
  async function resolveSessionAgent(client2, sessionId, timeoutMs = 5000) {