opencode-matrixx 2.6.4 → 2.6.5

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 (50) hide show
  1. package/dist/cli/setup/index.d.ts +1 -0
  2. package/dist/cli/setup/preset-wizard.d.ts +36 -0
  3. package/dist/cli/setup/types.d.ts +5 -0
  4. package/dist/cli.js +486 -59
  5. package/dist/config/index.d.ts +0 -1
  6. package/dist/config/migrations/model-migration.d.ts +1 -2
  7. package/dist/config/preset-applier.d.ts +17 -0
  8. package/dist/config/schema/agent-overrides.d.ts +0 -133
  9. package/dist/config/schema/categories.d.ts +0 -14
  10. package/dist/config/schema/commands.d.ts +1 -1
  11. package/dist/config/schema/matrixx-config.d.ts +28 -144
  12. package/dist/config/schema/model-config.d.ts +0 -22
  13. package/dist/config/schema/model-presets.d.ts +40 -0
  14. package/dist/config/schema/task.d.ts +6 -0
  15. package/dist/config/schema/tasks.d.ts +25 -0
  16. package/dist/config/schema.d.ts +3 -0
  17. package/dist/features/builtin-commands/templates/preset.d.ts +1 -0
  18. package/dist/features/builtin-commands/types.d.ts +1 -1
  19. package/dist/features/preset-state/index.d.ts +1 -0
  20. package/dist/features/preset-state/manager.d.ts +5 -0
  21. package/dist/features/session-state/state.d.ts +6 -0
  22. package/dist/features/task-storage/types.d.ts +2 -0
  23. package/dist/hooks/json-error-recovery/hook.d.ts +1 -1
  24. package/dist/hooks/task-continuation-enforcer/continuation-injection.test.d.ts +1 -0
  25. package/dist/hooks/task-continuation-enforcer/staleness.d.ts +7 -1
  26. package/dist/hooks/task-continuation-enforcer/todo.d.ts +8 -0
  27. package/dist/hooks/task-continuation-enforcer/todo.test.d.ts +1 -0
  28. package/dist/hooks/tasks-todowrite-disabler/constants.d.ts +1 -1
  29. package/dist/index.js +673 -598
  30. package/dist/matrixx.schema.json +113 -244
  31. package/dist/mcp/types.d.ts +1 -1
  32. package/dist/mcp/{grep-app.d.ts → white-rabbit.d.ts} +1 -1
  33. package/dist/shared/config-errors.d.ts +3 -0
  34. package/dist/shared/merge-categories.d.ts +1 -16
  35. package/dist/shared/task-system-gating.d.ts +10 -0
  36. package/dist/tools/delegate-task/categories.d.ts +3 -18
  37. package/dist/tools/delegate-task/complexity-constants.d.ts +2 -7
  38. package/dist/tools/delegate-task/executor-types.d.ts +7 -2
  39. package/dist/tools/delegate-task/timing.d.ts +5 -0
  40. package/dist/tools/delegate-task/types.d.ts +5 -2
  41. package/dist/tools/index.d.ts +1 -0
  42. package/dist/tools/preset/index.d.ts +1 -0
  43. package/dist/tools/preset/tools.d.ts +7 -0
  44. package/dist/tools/task/constants.d.ts +1 -0
  45. package/package.json +1 -1
  46. package/dist/config/presets.d.ts +0 -4
  47. package/dist/config/resolve-tiers.d.ts +0 -32
  48. package/dist/features/builtin-commands/templates/profile.d.ts +0 -1
  49. package/dist/shared/model-tiers.d.ts +0 -44
  50. package/dist/shared/tier-resolver.d.ts +0 -26
package/dist/index.js CHANGED
@@ -29560,7 +29560,7 @@ var init_ulw_research = __esm(() => {
29560
29560
  "look_at",
29561
29561
  "delegate_agent",
29562
29562
  "websearch_web_search_exa",
29563
- "grep_app_searchGitHub",
29563
+ "white_rabbit_searchGitHub",
29564
29564
  "context7_resolve-library-id",
29565
29565
  "context7_query-docs"
29566
29566
  ],
@@ -29671,7 +29671,7 @@ task(
29671
29671
  prompt="""
29672
29672
  Search open source repositories for implementations related to: <CURRENT_LEAD>
29673
29673
 
29674
- Use grep_app_searchGitHub to find real-world code patterns.
29674
+ Use white_rabbit_searchGitHub to find real-world code patterns.
29675
29675
  Focus on:
29676
29676
  - Production implementations in similar projects
29677
29677
  - Reference implementations or canonical examples
@@ -44410,31 +44410,13 @@ var ModelFallbackEntrySchema = exports_external.object({
44410
44410
  model: exports_external.string().min(1),
44411
44411
  variant: exports_external.string().optional()
44412
44412
  });
44413
- var TierSpecSchema = exports_external.object({
44414
- providerPriority: exports_external.array(exports_external.string()),
44415
- modelPattern: exports_external.string().refine((s) => {
44416
- try {
44417
- new RegExp(s);
44418
- return true;
44419
- } catch {
44420
- return false;
44421
- }
44422
- }, { message: "Invalid regex pattern" }),
44423
- fallbackTier: exports_external.string().optional(),
44424
- fallback: exports_external.array(ModelFallbackEntrySchema).optional()
44425
- });
44426
- var TiersSchema = exports_external.record(exports_external.string(), TierSpecSchema);
44427
44413
  var complexityDowngradeValueSchema = exports_external.string().refine((s) => {
44428
- if (s.startsWith("tier:")) {
44429
- const name = s.slice(5);
44430
- return name.length > 0;
44431
- }
44432
44414
  if (s.includes("/")) {
44433
44415
  const parts = s.split("/");
44434
44416
  return parts.length === 2 && parts[0].length > 0 && parts[1].length > 0;
44435
44417
  }
44436
44418
  return false;
44437
- }, { message: "Must be tier:<name> or <provider>/<model>" });
44419
+ }, { message: "Must be <provider>/<model>" });
44438
44420
  var ComplexityDowngradesSchema = exports_external.record(exports_external.string(), exports_external.record(exports_external.string(), complexityDowngradeValueSchema));
44439
44421
  var AgentModelRequirementsSchema = exports_external.object({
44440
44422
  fallbackChain: exports_external.array(ModelFallbackEntrySchema),
@@ -44861,6 +44843,12 @@ async function resolveCommandsInText(text, depth = 0, maxDepth = 3) {
44861
44843
  return resolved;
44862
44844
  }
44863
44845
  // src/shared/config-errors.ts
44846
+ class ConfigError extends Error {
44847
+ constructor(message) {
44848
+ super(message);
44849
+ this.name = "ConfigError";
44850
+ }
44851
+ }
44864
44852
  var configLoadErrors = [];
44865
44853
  function getConfigLoadErrors() {
44866
44854
  return configLoadErrors;
@@ -48112,7 +48100,7 @@ var TARGET_TOOLS = new Set([
48112
48100
  "context7_query-docs",
48113
48101
  "websearch_web_search_exa",
48114
48102
  "context7_get-library-docs",
48115
- "grep_app_searchgithub"
48103
+ "white_rabbit_searchgithub"
48116
48104
  ]);
48117
48105
  var AGENT_TOOLS = new Set([
48118
48106
  "task",
@@ -48498,10 +48486,14 @@ function registerSubagentSession(sessionID, parentSessionID) {
48498
48486
  subagentSessions.add(sessionID);
48499
48487
  subagentParentMap.set(sessionID, parentSessionID);
48500
48488
  }
48489
+ function unregisterSubagentSession(sessionID) {
48490
+ subagentSessions.delete(sessionID);
48491
+ subagentParentMap.delete(sessionID);
48492
+ }
48501
48493
  function getSubagentSessionIDs(parentSessionID) {
48502
48494
  const ids = [];
48503
- for (const [subID, parentID] of subagentParentMap) {
48504
- if (parentID === parentSessionID)
48495
+ for (const subID of subagentSessions) {
48496
+ if (subagentParentMap.get(subID) === parentSessionID)
48505
48497
  ids.push(subID);
48506
48498
  }
48507
48499
  return ids;
@@ -50255,79 +50247,49 @@ This prevents it from being picked up again.
50255
50247
  - DO begin working immediately if the user included a task with /pickup
50256
50248
  `;
50257
50249
 
50258
- // src/features/builtin-commands/templates/profile.ts
50259
- var PROFILE_TEMPLATE = `# /profile Command
50250
+ // src/features/builtin-commands/templates/preset.ts
50251
+ var PRESET_TEMPLATE = `You are managing the active model preset.
50252
+
50253
+ This command is provided by the Matrixx plugin. It uses the built-in \`preset\` tool to list, show, or switch model presets \u2014 no external scripts or direct config edits needed.
50260
50254
 
50261
50255
  ## Usage
50262
50256
 
50263
50257
  \`\`\`
50264
- /profile \u2192 List available profiles and show current
50265
- /profile list \u2192 List available profiles
50266
- /profile show \u2192 Show currently active profile
50267
- /profile set <name> \u2192 Change profile (edits project config, instructs to start new session)
50268
- /profile set <name> --global \u2192 Change profile globally (~/.config/opencode/matrixx.jsonc)
50269
- /profile set <name> --project \u2192 Change profile project-scoped (.opencode/matrixx.jsonc)
50258
+ /preset \u2192 List available presets and mark the active one
50259
+ /preset list \u2192 List available presets
50260
+ /preset show [<name>] \u2192 Show a preset's agent/category model assignments (defaults to active)
50261
+ /preset set <name> \u2192 Switch the active preset for the current session (live)
50262
+ /preset set <name> --save [--global|--project] \u2192 Also persist active_preset to config
50270
50263
  \`\`\`
50271
50264
 
50272
- ## Available Profiles
50273
-
50274
- | Profile | Description | Key Characteristic |
50275
- |---------|-------------|-------------------|
50276
- | free | Zero-cost, uses only free/open models | Kimi K2.5 Free, Grok (free), GLM |
50277
- | budget | Cost-optimized for tight budgets | Sonnet/Haiku mix, no Opus |
50278
- | economy | Balanced cost and capability | Primarily Sonnet/Haiku |
50279
- | balanced | Default \u2014 best overall value | Opus/Sonnet/Haiku tiered |
50280
- | performance | Maximum quality, no compromises | Opus for all heavy agents |
50281
- | go | Premium OpenCode Go subscription | Kimi K2.6, DeepSeek V4 Pro, GLM 5.1 |
50282
-
50283
- ## How It Works
50284
-
50285
- Each profile sets the \`model\` field on every agent and category in Matrixx's configuration. When you switch profiles, the agent models are swapped accordingly.
50286
-
50287
- The profile name is stored in your \`matrixx.json\` or \`matrixx.jsonc\` config file. On the next session, Matrixx reads it and applies the profile's model mapping, merging it with any explicit per-agent overrides you may have set.
50288
-
50289
- ## Task: Change the Profile
50290
-
50291
- Follow these steps:
50292
-
50293
- ### Step 1: Know the Current Config Location
50294
-
50295
- Matrixx reads config from two locations (user config wins over project config):
50296
- - **Project**: \`.opencode/matrixx.json\` or \`.opencode/matrixx.jsonc\`
50297
- - **Global**: \`~/.config/opencode/matrixx.json\` or \`~/.config/opencode/matrixx.jsonc\`
50298
-
50299
- ### Step 2: Determine Which Config File to Edit
50265
+ ## Step 1: Determine the action
50300
50266
 
50301
- - \`--global\` \u2192 Edit \`~/.config/opencode/matrixx.jsonc\`
50302
- - \`--project\` (default) \u2192 Edit \`.opencode/matrixx.jsonc\`
50303
- - If no flag and neither file exists, use \`--project\`
50267
+ Parse the arguments passed to this command:
50304
50268
 
50305
- ### Step 3: Read the Target Config File
50269
+ - No arguments or \`list\` \u2192 call \`preset\` with \`action: "list"\`.
50270
+ - \`show\` \u2192 call \`preset\` with \`action: "show"\` (optionally \`name\`).
50271
+ - \`set <name>\` \u2192 call \`preset\` with \`action: "set"\`, \`name: "<name>"\`.
50272
+ - \`set <name> --save\` \u2192 also pass \`save: true\`; \`--global\` \u2192 \`scope: "global"\`, \`--project\` (default) \u2192 \`scope: "project"\`.
50306
50273
 
50307
- Use the \`read\` tool to read the config file.
50274
+ ## Step 2: Call the built-in \`preset\` tool
50308
50275
 
50309
- ### Step 4: Validate the Requested Profile
50276
+ Use the \`preset\` tool with the resolved arguments. The tool validates the preset name, applies the session overlay (delegate-task categories switch immediately), and \u2014 with \`--save\` \u2014 persists \`active_preset\` to the project (\`.opencode/matrixx.jsonc\`) or global (\`~/.config/opencode/matrixx.jsonc\`) config.
50310
50277
 
50311
- If the user asked for \`/profile set <name>\`, verify \`<name>\` is in the table above. If not, report the valid profiles and stop.
50278
+ The tool handles all validation and file operations \u2014 do NOT edit config files directly.
50312
50279
 
50313
- ### Step 5: Update the \`profile\` Key
50280
+ ## Step 3: Confirm and instruct
50314
50281
 
50315
- Set \`"profile": "<name>"\` in the config JSON. If the \`profile\` key already exists, update it. If it doesn't exist, add it at the top level. Do NOT modify any other keys.
50316
-
50317
- ### Step 6: Write the Config File Back
50318
-
50319
- Use the \`write\` tool (or \`edit\` tool for minimal changes) to save the updated config.
50320
-
50321
- ### Step 7: Confirm and Advise
50282
+ After a successful switch:
50322
50283
 
50323
- Tell the user:
50324
- 1. The profile has been changed to \`<name>\`
50325
- 2. For the change to take effect, they MUST start a new session (\`/new\` or restart OpenCode)
50326
- 3. If they want session-level switching without restarting, suggest using the profile with \`--global\` or \`--project\` as appropriate
50284
+ 1. Report the tool's output to the user.
50285
+ 2. Note that delegate-task categories use the new preset immediately (session-scoped live switch).
50286
+ 3. If \`--save\` was used, tell the user the persisted \`active_preset\` takes effect for builtin agents on the next session.
50327
50287
 
50328
- ### Step 8: If \`/profile\` Was Called Without Arguments
50288
+ ## Important constraints
50329
50289
 
50330
- Just list the available profiles and show the current configuration's profile value.`;
50290
+ - Use the built-in \`preset\` tool. Do NOT bypass it by editing config files directly.
50291
+ - Do not invent preset names \u2014 if the requested name is unknown, report the available presets and stop.
50292
+ - \`--save\` without a scope defaults to project scope.`;
50331
50293
 
50332
50294
  // src/features/builtin-commands/templates/refactor.ts
50333
50295
  var REFACTOR_TEMPLATE = `# Intelligent Refactor Command
@@ -51596,16 +51558,16 @@ ${REMOVE_DEADCODE_TEMPLATE}
51596
51558
  </command-instruction>`,
51597
51559
  argumentHint: "[target-path] [--scope=<file|module|project>] [--dry-run]"
51598
51560
  },
51599
- profile: {
51600
- description: '(builtin) View recommended tier configurations (replaces the v2.x profile system; use `tier: "..."` in matrixx.jsonc instead)',
51561
+ preset: {
51562
+ description: "(builtin) Switch the active model preset (session-scoped live switch; --save persists to config)",
51601
51563
  template: `<command-instruction>
51602
- ${PROFILE_TEMPLATE}
51564
+ ${PRESET_TEMPLATE}
51603
51565
  </command-instruction>
51604
51566
 
51605
51567
  <user-request>
51606
51568
  $ARGUMENTS
51607
51569
  </user-request>`,
51608
- argumentHint: "[list|show|set <name> [--global|--project]]"
51570
+ argumentHint: "[list|show|set <name> [--save [--global|--project]]]"
51609
51571
  },
51610
51572
  "end-ultrawork": {
51611
51573
  description: "(builtin) Deactivate ultrawork mode and return to default Matrixx behavior for the current session",
@@ -59099,7 +59061,7 @@ var JSON_ERROR_TOOL_EXCLUDE_LIST = [
59099
59061
  "grep",
59100
59062
  "webfetch",
59101
59063
  "look_at",
59102
- "grep_app_searchgithub",
59064
+ "white_rabbit_searchgithub",
59103
59065
  "websearch_web_search_exa"
59104
59066
  ];
59105
59067
  var JSON_ERROR_PATTERNS = [
@@ -60125,16 +60087,38 @@ init_logger();
60125
60087
  import { randomUUID } from "crypto";
60126
60088
  import { existsSync as existsSync36, mkdirSync as mkdirSync14, readdirSync as readdirSync11, readFileSync as readFileSync30, renameSync as renameSync3, unlinkSync as unlinkSync8, writeFileSync as writeFileSync16 } from "fs";
60127
60089
  import { basename as basename5, dirname as dirname10, isAbsolute as isAbsolute3, join as join44 } from "path";
60090
+
60091
+ // src/shared/task-system-gating.ts
60092
+ var TASK_SYSTEM_DEFAULT = true;
60093
+ function resolveTasksConfig(config2) {
60094
+ const canonical = config2?.tasks;
60095
+ const legacyTasks = config2?.morpheus?.tasks;
60096
+ const legacyPoll = config2?.task?.pollTimeoutMs;
60097
+ return {
60098
+ enabled: canonical?.enabled ?? config2?.experimental?.task_system ?? config2?.new_task_system_enabled ?? TASK_SYSTEM_DEFAULT,
60099
+ scope: canonical?.scope ?? legacyTasks?.scope ?? "project",
60100
+ storage_path: canonical?.storage_path ?? legacyTasks?.storage_path,
60101
+ task_list_id: canonical?.task_list_id ?? legacyTasks?.task_list_id,
60102
+ stale_after_hours: canonical?.stale_after_hours ?? legacyTasks?.stale_after_hours,
60103
+ session_scoped: canonical?.session_scoped ?? legacyTasks?.session_scoped ?? true,
60104
+ pollTimeoutMs: canonical?.pollTimeoutMs ?? legacyPoll
60105
+ };
60106
+ }
60107
+ function isTaskSystemEnabled(config2) {
60108
+ return resolveTasksConfig(config2).enabled;
60109
+ }
60110
+
60111
+ // src/features/task-storage/storage.ts
60128
60112
  function getProjectTaskDir(directory) {
60129
60113
  return join44(directory, ".matrixx", "tasks");
60130
60114
  }
60131
60115
  function getTaskDir(config2 = {}, directory) {
60132
- const tasksConfig = config2.morpheus?.tasks;
60133
- const storagePath = tasksConfig?.storage_path;
60116
+ const tasksConfig = resolveTasksConfig(config2);
60117
+ const storagePath = tasksConfig.storage_path;
60134
60118
  if (storagePath) {
60135
60119
  return isAbsolute3(storagePath) ? storagePath : join44(directory ?? process.cwd(), storagePath);
60136
60120
  }
60137
- if (tasksConfig?.scope === "global" || !directory) {
60121
+ if (tasksConfig.scope === "global" || !directory) {
60138
60122
  const configDir = getOpenCodeConfigDir({ binary: "opencode" });
60139
60123
  const listId = resolveTaskListId(config2);
60140
60124
  return join44(configDir, "tasks", listId);
@@ -60148,7 +60132,7 @@ function resolveTaskListId(config2 = {}) {
60148
60132
  const envId = process.env.ULTRAWORK_TASK_LIST_ID?.trim();
60149
60133
  if (envId)
60150
60134
  return sanitizePathSegment(envId);
60151
- const configId = config2.morpheus?.tasks?.task_list_id?.trim();
60135
+ const configId = resolveTasksConfig(config2).task_list_id?.trim();
60152
60136
  if (configId)
60153
60137
  return sanitizePathSegment(configId);
60154
60138
  return sanitizePathSegment(basename5(process.cwd()));
@@ -60369,6 +60353,7 @@ async function getGitHead(directory) {
60369
60353
 
60370
60354
  // src/tools/task/constants.ts
60371
60355
  var TASK_ID_PATTERN = /^T-[A-Za-z0-9]+(-[A-Za-z0-9]+)*$/;
60356
+ var DEDUP_WINDOW_MS = 10 * 60 * 1000;
60372
60357
 
60373
60358
  // src/tools/task/types.ts
60374
60359
  var TaskStatusSchema = exports_external.enum(["pending", "in_progress", "completed", "deleted"]);
@@ -64921,7 +64906,7 @@ init_logger();
64921
64906
 
64922
64907
  // src/hooks/task-continuation-enforcer/idle-event.ts
64923
64908
  import { existsSync as existsSync43, readdirSync as readdirSync15 } from "fs";
64924
- import { join as join51 } from "path";
64909
+ import { join as join52 } from "path";
64925
64910
  init_logger();
64926
64911
 
64927
64912
  // src/hooks/task-continuation-enforcer/abort-detection.ts
@@ -64945,14 +64930,15 @@ init_logger();
64945
64930
 
64946
64931
  // src/hooks/task-continuation-enforcer/continuation-injection.ts
64947
64932
  import { existsSync as existsSync42, readdirSync as readdirSync14 } from "fs";
64948
- import { join as join50 } from "path";
64933
+ import { join as join51 } from "path";
64949
64934
  init_logger();
64950
64935
  // src/hooks/task-continuation-enforcer/staleness.ts
64951
64936
  import { statSync as statSync8 } from "fs";
64937
+ import { join as join50 } from "path";
64952
64938
  var DEFAULT_STALE_AFTER_HOURS = 24;
64953
64939
  var HOUR_MS = 60 * 60 * 1000;
64954
64940
  function getStaleAfterMs(config2) {
64955
- const hours = config2?.morpheus?.tasks?.stale_after_hours ?? DEFAULT_STALE_AFTER_HOURS;
64941
+ const hours = resolveTasksConfig(config2).stale_after_hours ?? DEFAULT_STALE_AFTER_HOURS;
64956
64942
  return hours * HOUR_MS;
64957
64943
  }
64958
64944
  function getTaskAgeMs(taskPath) {
@@ -64967,6 +64953,9 @@ function isTaskStale(taskPath, staleAfterMs) {
64967
64953
  const ageMs = getTaskAgeMs(taskPath);
64968
64954
  return ageMs !== null && ageMs > staleAfterMs;
64969
64955
  }
64956
+ function filterFreshIncompleteTasks(tasks, taskDir, staleAfterMs) {
64957
+ return tasks.filter((t) => !isTaskStale(join50(taskDir, `${t.id}.json`), staleAfterMs));
64958
+ }
64970
64959
  function formatTaskAge(ageMs) {
64971
64960
  const hours = Math.floor(ageMs / HOUR_MS);
64972
64961
  if (hours >= 24) {
@@ -64987,6 +64976,17 @@ function getIncompleteTasks(tasks) {
64987
64976
  return task.blockedBy.every((bid) => byId.get(bid)?.status === "completed");
64988
64977
  });
64989
64978
  }
64979
+ function dropSubtasksWithResolvedParent(tasks) {
64980
+ const byId = new Map(tasks.map((t) => [t.id, t]));
64981
+ return tasks.filter((task) => {
64982
+ if (!task.parentID)
64983
+ return true;
64984
+ const parent = byId.get(task.parentID);
64985
+ if (!parent)
64986
+ return true;
64987
+ return parent.status !== "completed" && parent.status !== "deleted";
64988
+ });
64989
+ }
64990
64990
  function filterTasksBySession(tasks, options) {
64991
64991
  if (options.sessionScoped === false)
64992
64992
  return tasks;
@@ -65059,10 +65059,10 @@ async function injectContinuation(args) {
65059
65059
  if (parsed)
65060
65060
  tasks.push(parsed);
65061
65061
  }
65062
- filteredTasks = filterTasksBySession(tasks, {
65062
+ filteredTasks = filterTasksBySession(dropSubtasksWithResolvedParent(tasks), {
65063
65063
  sessionID,
65064
65064
  subagentIDs: getSubagentSessionIDs(sessionID),
65065
- sessionScoped: config2?.morpheus?.tasks?.session_scoped !== false
65065
+ sessionScoped: resolveTasksConfig(config2).session_scoped
65066
65066
  });
65067
65067
  total = filteredTasks.length;
65068
65068
  if (total === 0) {
@@ -65085,9 +65085,10 @@ async function injectContinuation(args) {
65085
65085
  isBootstrap = true;
65086
65086
  stateForBootstrap._bootstrap = undefined;
65087
65087
  }
65088
- const freshIncompleteCount = isBootstrap ? 1 : getIncompleteTasks(filteredTasks).length;
65088
+ const staleAfterMs = getStaleAfterMs(config2);
65089
+ const freshIncompleteCount = isBootstrap ? 1 : filterFreshIncompleteTasks(getIncompleteTasks(filteredTasks), taskDir, staleAfterMs).length;
65089
65090
  if (!isBootstrap && freshIncompleteCount === 0) {
65090
- log(`[${HOOK_NAME14}] Skipped injection: no incomplete tasks`, { sessionID, total });
65091
+ log(`[${HOOK_NAME14}] Skipped injection: only stale tasks remain`, { sessionID, total });
65091
65092
  return;
65092
65093
  }
65093
65094
  let agentName = resolvedInfo?.agent;
@@ -65122,14 +65123,14 @@ async function injectContinuation(args) {
65122
65123
  prompt = BOOTSTRAP_PROMPT;
65123
65124
  } else {
65124
65125
  const incompleteTasks = getIncompleteTasks(filteredTasks);
65125
- const staleAfterMs = getStaleAfterMs(config2);
65126
+ const freshIncompleteTasks = filterFreshIncompleteTasks(incompleteTasks, taskDir, staleAfterMs);
65126
65127
  const taskList = incompleteTasks.map((task) => {
65127
- const ageMs = getTaskAgeMs(join50(taskDir, `${task.id}.json`));
65128
+ const ageMs = getTaskAgeMs(join51(taskDir, `${task.id}.json`));
65128
65129
  const staleSuffix = ageMs !== null && ageMs > staleAfterMs ? ` (stale: ${formatTaskAge(ageMs)})` : "";
65129
65130
  return `- [${task.status}] ${task.subject} (${task.id})${staleSuffix}`;
65130
65131
  }).join(`
65131
65132
  `);
65132
- const staleCount = incompleteTasks.filter((t) => isTaskStale(join50(taskDir, `${t.id}.json`), staleAfterMs)).length;
65133
+ const staleCount = incompleteTasks.length - freshIncompleteTasks.length;
65133
65134
  const staleNote = staleCount > 0 ? `
65134
65135
 
65135
65136
  Note: ${staleCount} remaining task(s) have had no activity for >${Math.round(staleAfterMs / 3600000)}h and may be orphaned. Mark them completed/deleted if superseded.` : "";
@@ -65322,10 +65323,10 @@ async function handleSessionIdle(args) {
65322
65323
  if (parsed)
65323
65324
  tasks.push(parsed);
65324
65325
  }
65325
- const filteredTasks = filterTasksBySession(tasks, {
65326
+ const filteredTasks = filterTasksBySession(dropSubtasksWithResolvedParent(tasks), {
65326
65327
  sessionID,
65327
65328
  subagentIDs: getSubagentSessionIDs(sessionID),
65328
- sessionScoped: config2?.morpheus?.tasks?.session_scoped !== false
65329
+ sessionScoped: resolveTasksConfig(config2).session_scoped
65329
65330
  });
65330
65331
  total = filteredTasks.length;
65331
65332
  if (total === 0) {
@@ -65342,7 +65343,7 @@ async function handleSessionIdle(args) {
65342
65343
  const incompleteTasks = getIncompleteTasks(filteredTasks);
65343
65344
  incompleteCount = incompleteTasks.length;
65344
65345
  const staleAfterMs = getStaleAfterMs(config2);
65345
- const staleIncompleteCount = incompleteTasks.filter((t) => isTaskStale(join51(taskDir, `${t.id}.json`), staleAfterMs)).length;
65346
+ const staleIncompleteCount = incompleteTasks.filter((t) => isTaskStale(join52(taskDir, `${t.id}.json`), staleAfterMs)).length;
65346
65347
  const activeIncompleteCount = incompleteCount - staleIncompleteCount;
65347
65348
  if (activeIncompleteCount === 0 && staleIncompleteCount > 0) {
65348
65349
  log(`[${HOOK_NAME14}] Skipped: only stale tasks remain`, {
@@ -65790,7 +65791,7 @@ var TODO_FETCH_ERROR_FRAGMENT = "Failed to fetch todos for task-notepad";
65790
65791
  // src/hooks/task-notepad/hook.ts
65791
65792
  init_logger();
65792
65793
  import { existsSync as existsSync44, mkdirSync as mkdirSync16, readdirSync as readdirSync16, readFileSync as readFileSync33, statSync as statSync9, writeFileSync as writeFileSync18 } from "fs";
65793
- import { dirname as dirname13, join as join52 } from "path";
65794
+ import { dirname as dirname13, join as join53 } from "path";
65794
65795
  function createTaskNotepadHook(ctx) {
65795
65796
  return {
65796
65797
  "tool.execute.after": async (input, output) => {
@@ -65841,7 +65842,7 @@ async function fetchTodos(ctx, sessionID) {
65841
65842
  }
65842
65843
  }
65843
65844
  function resolveActivePlan(directory) {
65844
- const plansDir = join52(directory, PLAN_DIR_NAME, PLANS_SUBDIR);
65845
+ const plansDir = join53(directory, PLAN_DIR_NAME, PLANS_SUBDIR);
65845
65846
  if (!existsSync44(plansDir))
65846
65847
  return null;
65847
65848
  let entries;
@@ -65854,7 +65855,7 @@ function resolveActivePlan(directory) {
65854
65855
  for (const entry of entries) {
65855
65856
  if (!entry.endsWith(PLAN_EXTENSION))
65856
65857
  continue;
65857
- const fullPath = join52(plansDir, entry);
65858
+ const fullPath = join53(plansDir, entry);
65858
65859
  try {
65859
65860
  const stats = statSync9(fullPath);
65860
65861
  if (!stats.isFile())
@@ -65897,7 +65898,7 @@ function appendCompletion(directory, planName, todo) {
65897
65898
  log(`[${HOOK_NAME16}] Appended completion to notepad`, { plan: planName, taskID: todo.id });
65898
65899
  }
65899
65900
  function notepadPathFor(directory, planName, taskID) {
65900
- return join52(directory, PLAN_DIR_NAME, NOTEPAD_DIR_NAME, planName, TASKS_SUBDIR, `${taskID}${NOTEPAD_EXTENSION}`);
65901
+ return join53(directory, PLAN_DIR_NAME, NOTEPAD_DIR_NAME, planName, TASKS_SUBDIR, `${taskID}${NOTEPAD_EXTENSION}`);
65901
65902
  }
65902
65903
  function renderNotepad(todo) {
65903
65904
  const priority = todo.priority ?? "medium";
@@ -65963,15 +65964,9 @@ to continue: task(session_id="${sessionId}", prompt="...")`;
65963
65964
  "tool.execute.after": toolExecuteAfter
65964
65965
  };
65965
65966
  }
65966
- // src/shared/task-system-gating.ts
65967
- var TASK_SYSTEM_DEFAULT = true;
65968
- function isTaskSystemEnabled(config2) {
65969
- return config2?.experimental?.task_system ?? TASK_SYSTEM_DEFAULT;
65970
- }
65971
-
65972
65967
  // src/hooks/tasks-todowrite-disabler/constants.ts
65973
65968
  var BLOCKED_TOOLS2 = ["TodoWrite", "TodoRead"];
65974
- var REPLACEMENT_MESSAGE = `TodoRead/TodoWrite are DISABLED because experimental.task_system is enabled.
65969
+ var REPLACEMENT_MESSAGE = `TodoRead/TodoWrite are DISABLED because tasks.enabled is true.
65975
65970
 
65976
65971
  **ACTION REQUIRED**: RE-REGISTER what you were about to write as Todo using Task tools NOW. Then ASSIGN yourself and START WORKING immediately.
65977
65972
 
@@ -67579,7 +67574,7 @@ function createWebFetchRedirectGuardHook(_ctx) {
67579
67574
  }
67580
67575
  // src/hooks/write-existing-file-guard/hook.ts
67581
67576
  import { existsSync as existsSync45 } from "fs";
67582
- import { isAbsolute as isAbsolute4, join as join53, normalize, resolve as resolve6, sep } from "path";
67577
+ import { isAbsolute as isAbsolute4, join as join54, normalize, resolve as resolve6, sep } from "path";
67583
67578
  function createWriteExistingFileGuardHook(ctx) {
67584
67579
  return {
67585
67580
  "tool.execute.before": async (input, output) => {
@@ -67594,7 +67589,7 @@ function createWriteExistingFileGuardHook(ctx) {
67594
67589
  }
67595
67590
  const resolvedPath = normalize(isAbsolute4(filePath) ? filePath : resolve6(ctx.directory, filePath));
67596
67591
  if (existsSync45(resolvedPath)) {
67597
- const matrixxRoot = join53(ctx.directory, ".matrixx") + sep;
67592
+ const matrixxRoot = join54(ctx.directory, ".matrixx") + sep;
67598
67593
  const isMatrixMarkdown = resolvedPath.startsWith(matrixxRoot) && resolvedPath.endsWith(".md");
67599
67594
  const normalizedPath = resolvedPath.replace(/\\/g, "/");
67600
67595
  const isPlansPath = normalizedPath.includes(".matrixx/plans");
@@ -68040,7 +68035,7 @@ function getLanguageId(ext) {
68040
68035
  }
68041
68036
  // src/tools/lsp/server-installation.ts
68042
68037
  import { existsSync as existsSync46 } from "fs";
68043
- import { join as join54 } from "path";
68038
+ import { join as join55 } from "path";
68044
68039
  function isServerInstalled(command) {
68045
68040
  if (command.length === 0)
68046
68041
  return false;
@@ -68068,23 +68063,23 @@ function isServerInstalled(command) {
68068
68063
  const paths = pathEnv.split(pathSeparator);
68069
68064
  for (const p of paths) {
68070
68065
  for (const suffix of exts) {
68071
- if (existsSync46(join54(p, cmd + suffix))) {
68066
+ if (existsSync46(join55(p, cmd + suffix))) {
68072
68067
  return true;
68073
68068
  }
68074
68069
  }
68075
68070
  }
68076
68071
  const cwd = process.cwd();
68077
68072
  const configDir = getOpenCodeConfigDir({ binary: "opencode" });
68078
- const dataDir = join54(getDataDir(), "opencode");
68073
+ const dataDir = join55(getDataDir(), "opencode");
68079
68074
  const additionalBases = [
68080
- join54(cwd, "node_modules", ".bin"),
68081
- join54(configDir, "bin"),
68082
- join54(configDir, "node_modules", ".bin"),
68083
- join54(dataDir, "bin")
68075
+ join55(cwd, "node_modules", ".bin"),
68076
+ join55(configDir, "bin"),
68077
+ join55(configDir, "node_modules", ".bin"),
68078
+ join55(dataDir, "bin")
68084
68079
  ];
68085
68080
  for (const base of additionalBases) {
68086
68081
  for (const suffix of exts) {
68087
- if (existsSync46(join54(base, cmd + suffix))) {
68082
+ if (existsSync46(join55(base, cmd + suffix))) {
68088
68083
  return true;
68089
68084
  }
68090
68085
  }
@@ -68096,7 +68091,7 @@ function isServerInstalled(command) {
68096
68091
  }
68097
68092
  // src/tools/lsp/server-config-loader.ts
68098
68093
  import { existsSync as existsSync47, readFileSync as readFileSync34 } from "fs";
68099
- import { join as join55 } from "path";
68094
+ import { join as join56 } from "path";
68100
68095
  function loadJsonFile(path12) {
68101
68096
  if (!existsSync47(path12))
68102
68097
  return null;
@@ -68110,9 +68105,9 @@ function getConfigPaths3() {
68110
68105
  const cwd = process.cwd();
68111
68106
  const configDir = getOpenCodeConfigDir({ binary: "opencode" });
68112
68107
  return {
68113
- project: detectConfigFile(join55(cwd, ".opencode", "matrixx")).path,
68114
- user: detectConfigFile(join55(configDir, "matrixx")).path,
68115
- opencode: detectConfigFile(join55(configDir, "opencode")).path
68108
+ project: detectConfigFile(join56(cwd, ".opencode", "matrixx")).path,
68109
+ user: detectConfigFile(join56(configDir, "matrixx")).path,
68110
+ opencode: detectConfigFile(join56(configDir, "opencode")).path
68116
68111
  };
68117
68112
  }
68118
68113
  function loadAllConfigs() {
@@ -68961,7 +68956,7 @@ class LSPServerManager {
68961
68956
  var lspManager = LSPServerManager.getInstance();
68962
68957
  // src/tools/lsp/lsp-client-wrapper.ts
68963
68958
  import { existsSync as existsSync49, statSync as statSync11 } from "fs";
68964
- import { dirname as dirname14, extname as extname3, join as join56, resolve as resolve8 } from "path";
68959
+ import { dirname as dirname14, extname as extname3, join as join57, resolve as resolve8 } from "path";
68965
68960
  import { fileURLToPath as fileURLToPath3 } from "url";
68966
68961
  function findWorkspaceRoot(filePath) {
68967
68962
  let dir = resolve8(filePath);
@@ -68977,7 +68972,7 @@ function findWorkspaceRoot(filePath) {
68977
68972
  let prevDir = "";
68978
68973
  while (dir !== prevDir) {
68979
68974
  for (const marker of markers) {
68980
- if (existsSync49(join56(dir, marker))) {
68975
+ if (existsSync49(join57(dir, marker))) {
68981
68976
  return dir;
68982
68977
  }
68983
68978
  }
@@ -81829,13 +81824,13 @@ var DEFAULT_MAX_MATCHES = 500;
81829
81824
  // src/tools/ast-grep/sg-cli-path.ts
81830
81825
  import { existsSync as existsSync51, statSync as statSync12 } from "fs";
81831
81826
  import { createRequire as createRequire4 } from "module";
81832
- import { dirname as dirname15, join as join58 } from "path";
81827
+ import { dirname as dirname15, join as join59 } from "path";
81833
81828
 
81834
81829
  // src/tools/ast-grep/downloader.ts
81835
81830
  import { existsSync as existsSync50 } from "fs";
81836
81831
  import { createRequire as createRequire3 } from "module";
81837
81832
  import { homedir as homedir8 } from "os";
81838
- import { join as join57 } from "path";
81833
+ import { join as join58 } from "path";
81839
81834
  init_logger();
81840
81835
  var REPO2 = "ast-grep/ast-grep";
81841
81836
  var DEFAULT_VERSION = "0.40.0";
@@ -81860,12 +81855,12 @@ var PLATFORM_MAP2 = {
81860
81855
  function getCacheDir3() {
81861
81856
  if (process.platform === "win32") {
81862
81857
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
81863
- const base2 = localAppData || join57(homedir8(), "AppData", "Local");
81864
- return join57(base2, "matrixx", "bin");
81858
+ const base2 = localAppData || join58(homedir8(), "AppData", "Local");
81859
+ return join58(base2, "matrixx", "bin");
81865
81860
  }
81866
81861
  const xdgCache = process.env.XDG_CACHE_HOME;
81867
- const base = xdgCache || join57(homedir8(), ".cache");
81868
- return join57(base, "matrixx", "bin");
81862
+ const base = xdgCache || join58(homedir8(), ".cache");
81863
+ return join58(base, "matrixx", "bin");
81869
81864
  }
81870
81865
  function getBinaryName3() {
81871
81866
  return process.platform === "win32" ? "sg.exe" : "sg";
@@ -81882,7 +81877,7 @@ async function downloadAstGrep(version3 = DEFAULT_VERSION) {
81882
81877
  }
81883
81878
  const cacheDir = getCacheDir3();
81884
81879
  const binaryName = getBinaryName3();
81885
- const binaryPath = join57(cacheDir, binaryName);
81880
+ const binaryPath = join58(cacheDir, binaryName);
81886
81881
  if (existsSync50(binaryPath)) {
81887
81882
  return binaryPath;
81888
81883
  }
@@ -81891,7 +81886,7 @@ async function downloadAstGrep(version3 = DEFAULT_VERSION) {
81891
81886
  const downloadUrl = `https://github.com/${REPO2}/releases/download/${version3}/${assetName}`;
81892
81887
  log(`[matrixx] Downloading ast-grep binary...`);
81893
81888
  try {
81894
- const archivePath = join57(cacheDir, assetName);
81889
+ const archivePath = join58(cacheDir, assetName);
81895
81890
  ensureCacheDir(cacheDir);
81896
81891
  await downloadArchive(downloadUrl, archivePath);
81897
81892
  await extractZipArchive(archivePath, cacheDir);
@@ -81945,7 +81940,7 @@ function findSgCliPathSync() {
81945
81940
  const require2 = createRequire4(import.meta.url);
81946
81941
  const cliPackageJsonPath = require2.resolve("@ast-grep/cli/package.json");
81947
81942
  const cliDirectory = dirname15(cliPackageJsonPath);
81948
- const sgPath = join58(cliDirectory, binaryName);
81943
+ const sgPath = join59(cliDirectory, binaryName);
81949
81944
  if (existsSync51(sgPath) && isValidBinary(sgPath)) {
81950
81945
  return sgPath;
81951
81946
  }
@@ -81957,7 +81952,7 @@ function findSgCliPathSync() {
81957
81952
  const packageJsonPath = require2.resolve(`${platformPackage}/package.json`);
81958
81953
  const packageDirectory = dirname15(packageJsonPath);
81959
81954
  const astGrepBinaryName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
81960
- const binaryPath = join58(packageDirectory, astGrepBinaryName);
81955
+ const binaryPath = join59(packageDirectory, astGrepBinaryName);
81961
81956
  if (existsSync51(binaryPath) && isValidBinary(binaryPath)) {
81962
81957
  return binaryPath;
81963
81958
  }
@@ -84388,17 +84383,17 @@ var {spawn: spawn10 } = globalThis.Bun;
84388
84383
  // src/tools/grep/constants.ts
84389
84384
  import { spawnSync as spawnSync2 } from "child_process";
84390
84385
  import { existsSync as existsSync58 } from "fs";
84391
- import { dirname as dirname16, join as join60 } from "path";
84386
+ import { dirname as dirname16, join as join61 } from "path";
84392
84387
 
84393
84388
  // src/tools/grep/downloader.ts
84394
84389
  import { existsSync as existsSync57, readdirSync as readdirSync18 } from "fs";
84395
- import { join as join59 } from "path";
84390
+ import { join as join60 } from "path";
84396
84391
  function findFileRecursive(dir, filename) {
84397
84392
  try {
84398
84393
  const entries = readdirSync18(dir, { withFileTypes: true, recursive: true });
84399
84394
  for (const entry of entries) {
84400
84395
  if (entry.isFile() && entry.name === filename) {
84401
- return join59(entry.parentPath ?? dir, entry.name);
84396
+ return join60(entry.parentPath ?? dir, entry.name);
84402
84397
  }
84403
84398
  }
84404
84399
  } catch {
@@ -84419,11 +84414,11 @@ function getPlatformKey() {
84419
84414
  }
84420
84415
  function getInstallDir() {
84421
84416
  const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
84422
- return join59(homeDir, ".cache", "matrixx", "bin");
84417
+ return join60(homeDir, ".cache", "matrixx", "bin");
84423
84418
  }
84424
84419
  function getRgPath() {
84425
84420
  const isWindows = process.platform === "win32";
84426
- return join59(getInstallDir(), isWindows ? "rg.exe" : "rg");
84421
+ return join60(getInstallDir(), isWindows ? "rg.exe" : "rg");
84427
84422
  }
84428
84423
  async function extractTarGz2(archivePath, destDir) {
84429
84424
  const platformKey = getPlatformKey();
@@ -84440,7 +84435,7 @@ async function extractZip2(archivePath, destDir) {
84440
84435
  const binaryName = process.platform === "win32" ? "rg.exe" : "rg";
84441
84436
  const foundPath = findFileRecursive(destDir, binaryName);
84442
84437
  if (foundPath) {
84443
- const destPath = join59(destDir, binaryName);
84438
+ const destPath = join60(destDir, binaryName);
84444
84439
  if (foundPath !== destPath) {
84445
84440
  const { renameSync: renameSync4 } = await import("fs");
84446
84441
  renameSync4(foundPath, destPath);
@@ -84461,7 +84456,7 @@ async function downloadAndInstallRipgrep() {
84461
84456
  ensureCacheDir(installDir);
84462
84457
  const filename = `ripgrep-${RG_VERSION}-${config4.platform}.${config4.extension}`;
84463
84458
  const url3 = `https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${filename}`;
84464
- const archivePath = join59(installDir, filename);
84459
+ const archivePath = join60(installDir, filename);
84465
84460
  try {
84466
84461
  await downloadArchive(url3, archivePath);
84467
84462
  if (config4.extension === "tar.gz") {
@@ -84506,11 +84501,11 @@ function getOpenCodeBundledRg() {
84506
84501
  const isWindows = process.platform === "win32";
84507
84502
  const rgName = isWindows ? "rg.exe" : "rg";
84508
84503
  const candidates = [
84509
- join60(getDataDir(), "opencode", "bin", rgName),
84510
- join60(execDir, rgName),
84511
- join60(execDir, "bin", rgName),
84512
- join60(execDir, "..", "bin", rgName),
84513
- join60(execDir, "..", "libexec", rgName)
84504
+ join61(getDataDir(), "opencode", "bin", rgName),
84505
+ join61(execDir, rgName),
84506
+ join61(execDir, "bin", rgName),
84507
+ join61(execDir, "..", "bin", rgName),
84508
+ join61(execDir, "..", "libexec", rgName)
84514
84509
  ];
84515
84510
  for (const candidate of candidates) {
84516
84511
  if (existsSync58(candidate)) {
@@ -85018,12 +85013,12 @@ var HandoffSchema = exports_external.object({
85018
85013
  });
85019
85014
  // src/features/handoff/storage.ts
85020
85015
  import { existsSync as existsSync59, mkdirSync as mkdirSync17, readFileSync as readFileSync39, renameSync as renameSync4, writeFileSync as writeFileSync22 } from "fs";
85021
- import { join as join61 } from "path";
85016
+ import { join as join62 } from "path";
85022
85017
  function getHandoffFilePath(directory) {
85023
- return join61(directory, HANDOFF_FILE_PATH);
85018
+ return join62(directory, HANDOFF_FILE_PATH);
85024
85019
  }
85025
85020
  function getHandoffConsumedFilePath(directory) {
85026
- return join61(directory, HANDOFF_CONSUMED_FILE_PATH);
85021
+ return join62(directory, HANDOFF_CONSUMED_FILE_PATH);
85027
85022
  }
85028
85023
  function handoffFileExists(directory) {
85029
85024
  try {
@@ -85061,7 +85056,7 @@ function writeHandoffFile(directory, content) {
85061
85056
  }
85062
85057
  function ensureHandoffDir(directory) {
85063
85058
  try {
85064
- mkdirSync17(join61(directory, ".matrixx"), { recursive: true });
85059
+ mkdirSync17(join62(directory, ".matrixx"), { recursive: true });
85065
85060
  return true;
85066
85061
  } catch {
85067
85062
  return false;
@@ -85217,13 +85212,13 @@ ${body}
85217
85212
 
85218
85213
  // src/tools/handoff/list-handler.ts
85219
85214
  import { existsSync as existsSync60, readdirSync as readdirSync19, statSync as statSync14 } from "fs";
85220
- import { join as join62 } from "path";
85215
+ import { join as join63 } from "path";
85221
85216
  var MATRIX_DIR = ".matrixx";
85222
85217
  function isHandoffFile(name) {
85223
85218
  return name === "handoff.md" || name.startsWith("handoff.consumed");
85224
85219
  }
85225
85220
  function handleList(directory) {
85226
- const matrixxDir = join62(directory, MATRIX_DIR);
85221
+ const matrixxDir = join63(directory, MATRIX_DIR);
85227
85222
  if (!existsSync60(matrixxDir)) {
85228
85223
  return "No handoffs found in .matrixx/";
85229
85224
  }
@@ -85241,7 +85236,7 @@ function handleList(directory) {
85241
85236
  lines.push(`Handoff files in ${matrixxDir}:`);
85242
85237
  lines.push("");
85243
85238
  for (const name of handoffs) {
85244
- const fullPath = join62(matrixxDir, name);
85239
+ const fullPath = join63(matrixxDir, name);
85245
85240
  let size = "?";
85246
85241
  let mtime = "?";
85247
85242
  try {
@@ -85467,7 +85462,7 @@ The Bash tool can execute these commands directly. Do NOT retry with interactive
85467
85462
  // src/tools/pdf-extract-figures/tools.ts
85468
85463
  import { existsSync as existsSync61, mkdtempSync, writeFileSync as writeFileSync23 } from "fs";
85469
85464
  import { tmpdir as tmpdir5 } from "os";
85470
- import { join as join63 } from "path";
85465
+ import { join as join64 } from "path";
85471
85466
  var PYTHON_SCRIPT = `#!/usr/bin/env python3
85472
85467
  import sys
85473
85468
  import json
@@ -85618,11 +85613,11 @@ async function runExtraction(args) {
85618
85613
  }
85619
85614
  let tmpDir;
85620
85615
  try {
85621
- tmpDir = mkdtempSync(join63(tmpdir5(), "matrixx-pdf-extract-"));
85616
+ tmpDir = mkdtempSync(join64(tmpdir5(), "matrixx-pdf-extract-"));
85622
85617
  } catch {
85623
85618
  return "Error: Could not create temporary directory.";
85624
85619
  }
85625
- const scriptPath = join63(tmpDir, "extract_pdf_figures.py");
85620
+ const scriptPath = join64(tmpDir, "extract_pdf_figures.py");
85626
85621
  writeFileSync23(scriptPath, PYTHON_SCRIPT);
85627
85622
  const payload = buildArgsPayload(args);
85628
85623
  log("[pdf_extract_figures] Running extraction", { file_path: args.file_path, payload });
@@ -85674,7 +85669,7 @@ import { existsSync as existsSync62 } from "fs";
85674
85669
  import { basename as basename7 } from "path";
85675
85670
 
85676
85671
  // src/tools/plan/types.ts
85677
- import { basename as basename6, isAbsolute as isAbsolute5, join as join64, relative as relative4, resolve as resolve9 } from "path";
85672
+ import { basename as basename6, isAbsolute as isAbsolute5, join as join65, relative as relative4, resolve as resolve9 } from "path";
85678
85673
 
85679
85674
  // src/tools/plan/constants.ts
85680
85675
  var PLANS_DIR3 = ".matrixx/plans";
@@ -85700,8 +85695,8 @@ function validatePlanFilePath(filePath, directory) {
85700
85695
  if (!PLAN_FILENAME_KEBAB_REGEX.test(base)) {
85701
85696
  return { error: `fileName must match kebab-case ^[a-z0-9-]+\\.md$: ${base}` };
85702
85697
  }
85703
- const resolved = isAbsolute5(filePath) ? resolve9(filePath) : resolve9(join64(directory, filePath));
85704
- const plansDir = resolve9(join64(directory, PLANS_DIR3));
85698
+ const resolved = isAbsolute5(filePath) ? resolve9(filePath) : resolve9(join65(directory, filePath));
85699
+ const plansDir = resolve9(join65(directory, PLANS_DIR3));
85705
85700
  const relToPlans = relative4(plansDir, resolved);
85706
85701
  if (relToPlans.startsWith("..") || isAbsolute5(relToPlans)) {
85707
85702
  return { error: `filePath must be inside ${PLANS_DIR3}: ${filePath}` };
@@ -85805,7 +85800,7 @@ function createPlanDeleteTool(ctx) {
85805
85800
  }
85806
85801
  // src/tools/plan/plan-list.ts
85807
85802
  import { existsSync as existsSync64, readdirSync as readdirSync20, statSync as statSync15 } from "fs";
85808
- import { join as join65 } from "path";
85803
+ import { join as join66 } from "path";
85809
85804
  function createPlanListTool(ctx) {
85810
85805
  return tool({
85811
85806
  description: `List plan files under .matrixx/plans/*.md. Filters *.md with kebab-case, sorts by mtime.`,
@@ -85813,13 +85808,13 @@ function createPlanListTool(ctx) {
85813
85808
  execute: async (_args, context) => {
85814
85809
  try {
85815
85810
  const directory = resolveDirectory(context?.directory, ctx?.directory);
85816
- const plansDir = join65(directory, PLANS_DIR3);
85811
+ const plansDir = join66(directory, PLANS_DIR3);
85817
85812
  if (!existsSync64(plansDir)) {
85818
85813
  return JSON.stringify({ plans: [] });
85819
85814
  }
85820
85815
  const entries = readdirSync20(plansDir).filter((f) => f.endsWith(".md") && PLAN_FILENAME_KEBAB_REGEX.test(f));
85821
85816
  const plans = entries.map((fileName) => {
85822
- const filePath = join65(plansDir, fileName);
85817
+ const filePath = join66(plansDir, fileName);
85823
85818
  try {
85824
85819
  const stat2 = statSync15(filePath);
85825
85820
  return {
@@ -86965,10 +86960,152 @@ function createPlanUpdateTool(ctx) {
86965
86960
  }
86966
86961
  });
86967
86962
  }
86963
+ // src/tools/preset/tools.ts
86964
+ import { existsSync as existsSync67, mkdirSync as mkdirSync18, readFileSync as readFileSync40, writeFileSync as writeFileSync24 } from "fs";
86965
+ import { homedir as homedir9 } from "os";
86966
+ import { dirname as dirname17, join as join67 } from "path";
86967
+
86968
+ // src/features/preset-state/manager.ts
86969
+ init_logger();
86970
+ var HOOK_NAME19 = "preset-state";
86971
+ var presetBySession = new Map;
86972
+ function setSessionPreset(sessionID, presetName) {
86973
+ presetBySession.set(sessionID, presetName);
86974
+ log(`[${HOOK_NAME19}] Preset set for session`, { sessionID, presetName });
86975
+ }
86976
+ function getSessionPreset(sessionID) {
86977
+ return presetBySession.get(sessionID);
86978
+ }
86979
+ // src/tools/preset/tools.ts
86980
+ init_logger();
86981
+ var ACTIONS = ["list", "show", "set"];
86982
+ var SCOPES = ["global", "project"];
86983
+ function resolveConfigPath(scope, directory) {
86984
+ if (scope === "global") {
86985
+ return join67(homedir9(), ".config", "opencode", "matrixx.jsonc");
86986
+ }
86987
+ return join67(directory ?? process.cwd(), ".opencode", "matrixx.jsonc");
86988
+ }
86989
+ function persistActivePreset(presetName, scope, directory) {
86990
+ const configPath = resolveConfigPath(scope, directory);
86991
+ let raw = {};
86992
+ if (existsSync67(configPath)) {
86993
+ const content = readFileSync40(configPath, "utf-8");
86994
+ const parsed = parseJsoncSafe(content);
86995
+ if (parsed.errors.length > 0) {
86996
+ return {
86997
+ ok: false,
86998
+ error: `Cannot parse existing config at ${configPath}: ${parsed.errors[0]?.message}`
86999
+ };
87000
+ }
87001
+ raw = parsed.data ?? {};
87002
+ } else {
87003
+ mkdirSync18(dirname17(configPath), { recursive: true });
87004
+ }
87005
+ raw.active_preset = presetName;
87006
+ writeFileSync24(configPath, `${JSON.stringify(raw, null, 2)}
87007
+ `);
87008
+ log("[preset] persisted active_preset", { presetName, scope, configPath });
87009
+ return { ok: true, path: configPath };
87010
+ }
87011
+ function unknownPresetError(name, presetNames) {
87012
+ const available = presetNames.length > 0 ? presetNames.join(", ") : "(none defined)";
87013
+ return `Error: Unknown preset "${name}". Available presets: ${available}`;
87014
+ }
87015
+ function formatPreset(name, preset) {
87016
+ const lines = [`Preset "${name}":`];
87017
+ if (preset.default_model) {
87018
+ lines.push(` default_model: ${preset.default_model}`);
87019
+ }
87020
+ const agentEntries = Object.entries(preset.agents ?? {});
87021
+ if (agentEntries.length > 0) {
87022
+ lines.push(" agents:");
87023
+ for (const [agent, entry] of agentEntries) {
87024
+ lines.push(` - ${agent}: ${entry.model}${entry.variant ? ` (variant: ${entry.variant})` : ""}`);
87025
+ }
87026
+ }
87027
+ const categoryEntries = Object.entries(preset.categories ?? {});
87028
+ if (categoryEntries.length > 0) {
87029
+ lines.push(" categories:");
87030
+ for (const [category, entry] of categoryEntries) {
87031
+ lines.push(` - ${category}: ${entry.model}${entry.variant ? ` (variant: ${entry.variant})` : ""}`);
87032
+ }
87033
+ }
87034
+ return lines.join(`
87035
+ `);
87036
+ }
87037
+ function runPresetCommand(input) {
87038
+ const { action, name, save, scope, sessionID, options } = input;
87039
+ const presets = options?.pluginConfig?.model_presets ?? {};
87040
+ const presetNames = Object.keys(presets);
87041
+ const activePreset = options?.pluginConfig?.active_preset;
87042
+ const sessionPreset = getSessionPreset(sessionID);
87043
+ const effective = sessionPreset ?? activePreset;
87044
+ if (action === "list") {
87045
+ if (presetNames.length === 0) {
87046
+ return "No model presets defined. Add a `model_presets` section to your matrixx config.";
87047
+ }
87048
+ const lines = presetNames.map((p) => `- ${p}${p === effective ? " (active)" : ""}`);
87049
+ const activeLine = effective ? `Active: ${effective}${sessionPreset ? ` (session overlay for ${sessionID})` : ""}` : "Active: (none)";
87050
+ return `Available presets:
87051
+ ${lines.join(`
87052
+ `)}
87053
+
87054
+ ${activeLine}`;
87055
+ }
87056
+ if (action === "show") {
87057
+ const target = name ?? effective;
87058
+ if (!target) {
87059
+ return "No preset specified and no active preset set. Usage: preset show <name>";
87060
+ }
87061
+ const preset = presets[target];
87062
+ if (!preset)
87063
+ return unknownPresetError(target, presetNames);
87064
+ return formatPreset(target, preset);
87065
+ }
87066
+ if (!name) {
87067
+ return "Error: `preset set` requires a preset name. Usage: preset set <name> [--save [--global|--project]]";
87068
+ }
87069
+ if (!presets[name])
87070
+ return unknownPresetError(name, presetNames);
87071
+ setSessionPreset(sessionID, name);
87072
+ let message = `\u2713 Active preset set to "${name}" for this session.
87073
+
87074
+ ` + `- delegate-task categories switch immediately
87075
+ ` + `- builtin agents apply on next session`;
87076
+ if (save) {
87077
+ const result = persistActivePreset(name, scope, options?.directory);
87078
+ if (!result.ok)
87079
+ return `Error: ${result.error}`;
87080
+ message += `
87081
+
87082
+ Persisted active_preset to ${result.path} (scope: ${scope}). Next session starts on "${name}".`;
87083
+ }
87084
+ return message;
87085
+ }
87086
+ function createPresetTool(options) {
87087
+ const preset = tool({
87088
+ description: "Manage model presets: list available presets, show a preset's model assignments, " + "or set the active preset for the current session. `set <name>` switches delegate-task " + "categories immediately and applies to builtin agents on the next session; " + "`set <name> --save [--global|--project]` additionally persists `active_preset` to config.",
87089
+ args: {
87090
+ action: tool.schema.enum(ACTIONS).describe("Operation: list available presets, show a preset's details, or set the active preset"),
87091
+ name: tool.schema.string().optional().describe("Preset name (required for set; optional for show \u2014 defaults to the active preset)"),
87092
+ save: tool.schema.boolean().optional().describe("Persist the active preset to config (default: false \u2014 session-only overlay)"),
87093
+ scope: tool.schema.enum(SCOPES).optional().describe("Where to persist when --save: project (.opencode/matrixx.jsonc, default) or global (~/.config/opencode/matrixx.jsonc)")
87094
+ },
87095
+ async execute(args, context) {
87096
+ const action = args.action;
87097
+ const name = args.name;
87098
+ const save = args.save;
87099
+ const scope = args.scope ?? "project";
87100
+ return runPresetCommand({ action, name, save, scope, sessionID: context.sessionID, options });
87101
+ }
87102
+ });
87103
+ return { preset };
87104
+ }
86968
87105
  // src/tools/session-manager/constants.ts
86969
- import { join as join66 } from "path";
86970
- var TODO_DIR = join66(getOpenCodeConfigDir({ binary: "opencode" }), "todos");
86971
- var TRANSCRIPT_DIR2 = join66(getOpenCodeConfigDir({ binary: "opencode" }), "transcripts");
87106
+ import { join as join68 } from "path";
87107
+ var TODO_DIR = join68(getOpenCodeConfigDir({ binary: "opencode" }), "todos");
87108
+ var TRANSCRIPT_DIR2 = join68(getOpenCodeConfigDir({ binary: "opencode" }), "transcripts");
86972
87109
  var SESSION_LIST_DESCRIPTION = `List all OpenCode sessions with optional filtering.
86973
87110
 
86974
87111
  Returns a list of available session IDs with metadata including message count, date range, and agents used.
@@ -87040,9 +87177,9 @@ Agents Used: build, oracle, librarian
87040
87177
  Has Todos: Yes (12 items, 8 completed)
87041
87178
  Has Transcript: Yes (234 entries)`;
87042
87179
  // src/tools/session-manager/storage.ts
87043
- import { existsSync as existsSync67 } from "fs";
87180
+ import { existsSync as existsSync68 } from "fs";
87044
87181
  import { readdir, readFile as readFile3 } from "fs/promises";
87045
- import { join as join67 } from "path";
87182
+ import { join as join69 } from "path";
87046
87183
  var sdkClient = null;
87047
87184
  function setStorageClient(client2) {
87048
87185
  sdkClient = client2;
@@ -87061,7 +87198,7 @@ async function getMainSessions(options) {
87061
87198
  return [];
87062
87199
  }
87063
87200
  }
87064
- if (!existsSync67(SESSION_STORAGE))
87201
+ if (!existsSync68(SESSION_STORAGE))
87065
87202
  return [];
87066
87203
  const sessions = [];
87067
87204
  try {
@@ -87069,13 +87206,13 @@ async function getMainSessions(options) {
87069
87206
  for (const projectDir of projectDirs) {
87070
87207
  if (!projectDir.isDirectory())
87071
87208
  continue;
87072
- const projectPath = join67(SESSION_STORAGE, projectDir.name);
87209
+ const projectPath = join69(SESSION_STORAGE, projectDir.name);
87073
87210
  const sessionFiles = await readdir(projectPath);
87074
87211
  for (const file3 of sessionFiles) {
87075
87212
  if (!file3.endsWith(".json"))
87076
87213
  continue;
87077
87214
  try {
87078
- const content = await readFile3(join67(projectPath, file3), "utf-8");
87215
+ const content = await readFile3(join69(projectPath, file3), "utf-8");
87079
87216
  const meta3 = JSON.parse(content);
87080
87217
  if (meta3.parentID)
87081
87218
  continue;
@@ -87100,7 +87237,7 @@ async function getAllSessions() {
87100
87237
  return [];
87101
87238
  }
87102
87239
  }
87103
- if (!existsSync67(MESSAGE_STORAGE))
87240
+ if (!existsSync68(MESSAGE_STORAGE))
87104
87241
  return [];
87105
87242
  const sessions = [];
87106
87243
  async function scanDirectory(dir) {
@@ -87108,7 +87245,7 @@ async function getAllSessions() {
87108
87245
  const entries = await readdir(dir, { withFileTypes: true });
87109
87246
  for (const entry of entries) {
87110
87247
  if (entry.isDirectory()) {
87111
- const sessionPath = join67(dir, entry.name);
87248
+ const sessionPath = join69(dir, entry.name);
87112
87249
  const files = await readdir(sessionPath);
87113
87250
  if (files.some((f) => f.endsWith(".json"))) {
87114
87251
  sessions.push(entry.name);
@@ -87169,7 +87306,7 @@ async function readSessionMessages(sessionID) {
87169
87306
  }
87170
87307
  }
87171
87308
  const messageDir = getMessageDir(sessionID);
87172
- if (!messageDir || !existsSync67(messageDir))
87309
+ if (!messageDir || !existsSync68(messageDir))
87173
87310
  return [];
87174
87311
  const messages2 = [];
87175
87312
  try {
@@ -87178,7 +87315,7 @@ async function readSessionMessages(sessionID) {
87178
87315
  if (!file3.endsWith(".json"))
87179
87316
  continue;
87180
87317
  try {
87181
- const content = await readFile3(join67(messageDir, file3), "utf-8");
87318
+ const content = await readFile3(join69(messageDir, file3), "utf-8");
87182
87319
  const meta3 = JSON.parse(content);
87183
87320
  const parts = await readParts2(meta3.id);
87184
87321
  messages2.push({
@@ -87202,8 +87339,8 @@ async function readSessionMessages(sessionID) {
87202
87339
  });
87203
87340
  }
87204
87341
  async function readParts2(messageID) {
87205
- const partDir = join67(PART_STORAGE, messageID);
87206
- if (!existsSync67(partDir))
87342
+ const partDir = join69(PART_STORAGE, messageID);
87343
+ if (!existsSync68(partDir))
87207
87344
  return [];
87208
87345
  const parts = [];
87209
87346
  try {
@@ -87212,7 +87349,7 @@ async function readParts2(messageID) {
87212
87349
  if (!file3.endsWith(".json"))
87213
87350
  continue;
87214
87351
  try {
87215
- const content = await readFile3(join67(partDir, file3), "utf-8");
87352
+ const content = await readFile3(join69(partDir, file3), "utf-8");
87216
87353
  parts.push(JSON.parse(content));
87217
87354
  } catch {}
87218
87355
  }
@@ -87236,14 +87373,14 @@ async function readSessionTodos(sessionID) {
87236
87373
  return [];
87237
87374
  }
87238
87375
  }
87239
- if (!existsSync67(TODO_DIR))
87376
+ if (!existsSync68(TODO_DIR))
87240
87377
  return [];
87241
87378
  try {
87242
87379
  const allFiles = await readdir(TODO_DIR);
87243
87380
  const todoFiles = allFiles.filter((f) => f.includes(sessionID) && f.endsWith(".json"));
87244
87381
  for (const file3 of todoFiles) {
87245
87382
  try {
87246
- const content = await readFile3(join67(TODO_DIR, file3), "utf-8");
87383
+ const content = await readFile3(join69(TODO_DIR, file3), "utf-8");
87247
87384
  const data = JSON.parse(content);
87248
87385
  if (Array.isArray(data)) {
87249
87386
  return data.map((item) => ({
@@ -87261,10 +87398,10 @@ async function readSessionTodos(sessionID) {
87261
87398
  return [];
87262
87399
  }
87263
87400
  async function readSessionTranscript(sessionID) {
87264
- if (!existsSync67(TRANSCRIPT_DIR2))
87401
+ if (!existsSync68(TRANSCRIPT_DIR2))
87265
87402
  return 0;
87266
- const transcriptFile = join67(TRANSCRIPT_DIR2, `${sessionID}.jsonl`);
87267
- if (!existsSync67(transcriptFile))
87403
+ const transcriptFile = join69(TRANSCRIPT_DIR2, `${sessionID}.jsonl`);
87404
+ if (!existsSync68(transcriptFile))
87268
87405
  return 0;
87269
87406
  try {
87270
87407
  const content = await readFile3(transcriptFile, "utf-8");
@@ -87774,20 +87911,20 @@ function createSkillTool(options = {}) {
87774
87911
  }
87775
87912
  var skill = createSkillTool();
87776
87913
  // src/tools/slashcommand/command-discovery.ts
87777
- import { existsSync as existsSync68, readdirSync as readdirSync21, readFileSync as readFileSync40 } from "fs";
87778
- import { basename as basename8, join as join68 } from "path";
87914
+ import { existsSync as existsSync69, readdirSync as readdirSync21, readFileSync as readFileSync41 } from "fs";
87915
+ import { basename as basename8, join as join70 } from "path";
87779
87916
  function discoverCommandsFromDir2(commandsDir, scope) {
87780
- if (!existsSync68(commandsDir))
87917
+ if (!existsSync69(commandsDir))
87781
87918
  return [];
87782
87919
  const entries = readdirSync21(commandsDir, { withFileTypes: true });
87783
87920
  const commands2 = [];
87784
87921
  for (const entry of entries) {
87785
87922
  if (!isMarkdownFile(entry))
87786
87923
  continue;
87787
- const commandPath = join68(commandsDir, entry.name);
87924
+ const commandPath = join70(commandsDir, entry.name);
87788
87925
  const commandName = basename8(entry.name, ".md");
87789
87926
  try {
87790
- const content = readFileSync40(commandPath, "utf-8");
87927
+ const content = readFileSync41(commandPath, "utf-8");
87791
87928
  const { data, body } = parseFrontmatter(content);
87792
87929
  const isOpencodeSource = scope === "opencode" || scope === "opencode-project";
87793
87930
  const metadata = {
@@ -87811,8 +87948,8 @@ function discoverCommandsFromDir2(commandsDir, scope) {
87811
87948
  }
87812
87949
  function discoverCommandsSync(directory) {
87813
87950
  const configDir = getOpenCodeConfigDir({ binary: "opencode" });
87814
- const opencodeGlobalDir = join68(configDir, "command");
87815
- const opencodeProjectDir = join68(directory ?? process.cwd(), ".opencode", "command");
87951
+ const opencodeGlobalDir = join70(configDir, "command");
87952
+ const opencodeProjectDir = join70(directory ?? process.cwd(), ".opencode", "command");
87816
87953
  const opencodeGlobalCommands = discoverCommandsFromDir2(opencodeGlobalDir, "opencode");
87817
87954
  const opencodeProjectCommands = discoverCommandsFromDir2(opencodeProjectDir, "opencode-project");
87818
87955
  const builtinCommandsMap = loadBuiltinCommands();
@@ -87836,7 +87973,7 @@ function discoverCommandsSync(directory) {
87836
87973
  ];
87837
87974
  }
87838
87975
  // src/tools/slashcommand/command-output-formatter.ts
87839
- import { dirname as dirname17 } from "path";
87976
+ import { dirname as dirname18 } from "path";
87840
87977
  async function formatLoadedCommand(command, userMessage) {
87841
87978
  const sections = [];
87842
87979
  sections.push(`# /${command.name} Command
@@ -87875,7 +88012,7 @@ async function formatLoadedCommand(command, userMessage) {
87875
88012
  if (!content && command.lazyContentLoader) {
87876
88013
  content = await command.lazyContentLoader.load();
87877
88014
  }
87878
- const commandDir = command.path ? dirname17(command.path) : process.cwd();
88015
+ const commandDir = command.path ? dirname18(command.path) : process.cwd();
87879
88016
  const withFileReferences = await resolveFileReferencesInText(content, commandDir);
87880
88017
  const resolvedContent = await resolveCommandsInText(withFileReferences);
87881
88018
  let finalContent = resolvedContent.trim();
@@ -88954,9 +89091,9 @@ function createAssemblyTool(options) {
88954
89091
  });
88955
89092
  }
88956
89093
  // src/tools/dcp-switch-profile/tools.ts
88957
- import { existsSync as existsSync69, writeFileSync as writeFileSync24 } from "fs";
88958
- import { homedir as homedir9 } from "os";
88959
- import { join as join69 } from "path";
89094
+ import { existsSync as existsSync70, writeFileSync as writeFileSync25 } from "fs";
89095
+ import { homedir as homedir10 } from "os";
89096
+ import { join as join71 } from "path";
88960
89097
 
88961
89098
  // src/config/schema/dcp.ts
88962
89099
  var DcpCompressOverrideSchema = exports_external.object({
@@ -89129,11 +89266,11 @@ var DcpConfigSchema = exports_external.object({
89129
89266
  });
89130
89267
 
89131
89268
  // src/tools/dcp-switch-profile/tools.ts
89132
- var DCP_PLUGIN_DIR = join69(homedir9(), ".config", "opencode", "node_modules", "@tarquinen", "opencode-dcp");
89133
- var DCP_SYMLINK = join69(homedir9(), ".config", "opencode", "dcp.jsonc");
89269
+ var DCP_PLUGIN_DIR = join71(homedir10(), ".config", "opencode", "node_modules", "@tarquinen", "opencode-dcp");
89270
+ var DCP_SYMLINK = join71(homedir10(), ".config", "opencode", "dcp.jsonc");
89134
89271
  var VALID_PROFILES = ["economy", "balanced", "performance", "ultimate"];
89135
89272
  function checkDcpInstalled() {
89136
- if (!existsSync69(DCP_PLUGIN_DIR)) {
89273
+ if (!existsSync70(DCP_PLUGIN_DIR)) {
89137
89274
  return `DCP is not installed at ${DCP_PLUGIN_DIR}. Install it with: npm install --prefix ~/.config/opencode @tarquinen/opencode-dcp`;
89138
89275
  }
89139
89276
  return null;
@@ -89211,7 +89348,7 @@ function switchProfile(profile, options) {
89211
89348
  if (dcpError)
89212
89349
  return dcpError;
89213
89350
  const inlineConfig = buildInlineConfig(profile, options);
89214
- writeFileSync24(DCP_SYMLINK, `${JSON.stringify(inlineConfig, null, 2)}
89351
+ writeFileSync25(DCP_SYMLINK, `${JSON.stringify(inlineConfig, null, 2)}
89215
89352
  `);
89216
89353
  return `\u2713 Switched to DCP profile: ${profile}
89217
89354
 
@@ -89557,149 +89694,23 @@ function createDelegateAgent(ctx, backgroundManager, disabledAgents = []) {
89557
89694
  }
89558
89695
  });
89559
89696
  }
89560
- // src/shared/tier-resolver.ts
89561
- init_logger();
89562
-
89563
- // src/shared/model-tiers.ts
89564
- function compilePattern(pattern) {
89565
- try {
89566
- return new RegExp(pattern);
89567
- } catch {
89568
- return /(?!)/;
89569
- }
89570
- }
89571
- var DEFAULT_TIER_SPECS = {
89572
- free: {
89573
- name: "free",
89574
- providerPriority: ["opencode", "xai", "opencode-go", "zai-coding-plan"],
89575
- modelPattern: /-free$|kimi-k2\.5-free|minimax-m2\.5-free|grok-code-fast/,
89576
- fallback: [
89577
- { providers: ["opencode"], model: "kimi-k2.5-free" },
89578
- { providers: ["xai"], model: "grok-code-fast-1" }
89579
- ]
89580
- },
89581
- fast: {
89582
- name: "fast",
89583
- providerPriority: ["anthropic", "openai", "google", "opencode-go"],
89584
- modelPattern: /claude-haiku|gpt-5-nano|gemini-2\.5-flash|deepseek-v4-flash/,
89585
- fallback: [{ providers: ["anthropic"], model: "claude-haiku-4-5" }],
89586
- fallbackTier: "free"
89587
- },
89588
- standard: {
89589
- name: "standard",
89590
- providerPriority: ["anthropic", "openai", "google", "opencode-go"],
89591
- modelPattern: /claude-sonnet|gpt-5\.2|gemini-2\.5-pro/,
89592
- fallback: [{ providers: ["anthropic"], model: "claude-sonnet-4-6" }],
89593
- fallbackTier: "fast"
89594
- },
89595
- premium: {
89596
- name: "premium",
89597
- providerPriority: ["anthropic", "openai", "google", "opencode-go"],
89598
- modelPattern: /claude-opus|gpt-5\.3-codex|gemini-3-pro/,
89599
- fallback: [{ providers: ["anthropic"], model: "claude-opus-4-6" }],
89600
- fallbackTier: "standard"
89601
- },
89602
- frontier: {
89603
- name: "frontier",
89604
- providerPriority: ["anthropic", "openai", "google", "opencode-go"],
89605
- modelPattern: /claude-opus|gpt-5\.3-codex|gemini-3\.1-pro/,
89606
- fallback: [{ providers: ["anthropic"], model: "claude-opus-4-6" }],
89607
- fallbackTier: "premium"
89608
- }
89609
- };
89610
- function buildTierSpecs(config4) {
89611
- if (!config4?.tiers)
89612
- return DEFAULT_TIER_SPECS;
89613
- const result = {};
89614
- for (const [name, raw] of Object.entries(config4.tiers)) {
89615
- if (!raw || typeof raw !== "object")
89616
- continue;
89617
- const providerPriority = Array.isArray(raw.providerPriority) ? raw.providerPriority : [];
89618
- const rawPattern = raw.modelPattern;
89619
- const patternString = typeof rawPattern === "string" ? rawPattern : "";
89620
- result[name] = {
89621
- name,
89622
- providerPriority,
89623
- modelPattern: compilePattern(patternString),
89624
- fallbackTier: raw.fallbackTier,
89625
- fallback: raw.fallback
89626
- };
89627
- }
89628
- return result;
89629
- }
89630
- function getTierSpec(tierName, config4) {
89631
- if (!tierName)
89632
- return;
89633
- const specs = buildTierSpecs(config4);
89634
- return specs[tierName];
89635
- }
89636
-
89637
- // src/shared/tier-resolver.ts
89638
- function resolveTier(tier, ctx, config4) {
89639
- const spec = getTierSpec(tier, config4);
89640
- if (!spec)
89641
- return null;
89642
- if (ctx.availableModels.size > 0) {
89643
- for (const provider of spec.providerPriority) {
89644
- const match = findRegexMatch(spec.modelPattern, provider, ctx.availableModels);
89645
- if (match) {
89646
- log("[tier-resolver] resolved via regex", { tier, provider, match });
89647
- return { model: match, provenance: "tier-resolved", tier };
89648
- }
89649
- }
89650
- log("[tier-resolver] no live match found, recursing to fallback tier", { tier });
89651
- } else if (ctx.connectedProviders) {
89652
- const connectedSet = new Set(ctx.connectedProviders);
89653
- const fallback = spec.fallback ?? [];
89654
- for (const entry of fallback) {
89655
- for (const provider of entry.providers) {
89656
- if (connectedSet.has(provider)) {
89657
- const model = `${provider}/${entry.model}`;
89658
- log("[tier-resolver] using config fallback (cold cache)", { tier, provider, model });
89659
- return { model, provenance: "tier-static-fallback", tier };
89660
- }
89661
- }
89662
- }
89663
- log("[tier-resolver] no config fallback provider is connected", { tier });
89664
- } else {
89665
- log("[tier-resolver] no live models and no connected providers cache", { tier });
89666
- }
89667
- if (spec.fallbackTier) {
89668
- return resolveTier(spec.fallbackTier, ctx, config4);
89669
- }
89670
- return null;
89671
- }
89672
- function findRegexMatch(pattern, provider, available) {
89673
- for (const candidate of available) {
89674
- if (!candidate.startsWith(`${provider}/`))
89675
- continue;
89676
- if (pattern.test(candidate))
89677
- return candidate;
89678
- }
89679
- return null;
89680
- }
89681
-
89682
89697
  // src/tools/delegate-task/complexity-types.ts
89683
89698
  function isDowngradable(level) {
89684
89699
  return level === 1 || level === 2;
89685
89700
  }
89686
89701
 
89687
89702
  // src/tools/delegate-task/complexity-constants.ts
89688
- function resolveComplexityModel(category, complexity, originalModel, userDowngrades, config4, tierContext) {
89703
+ function resolveComplexityModel(category, complexity, originalModel, userDowngrades, config4) {
89689
89704
  if (!isDowngradable(complexity)) {
89690
89705
  return { model: originalModel, downgraded: false };
89691
89706
  }
89692
89707
  let complexityDowngrades;
89693
- let tiersConfig;
89694
89708
  if (config4) {
89695
89709
  const maybe = config4;
89696
- const hasHolderKeys = "complexityDowngrades" in maybe || "tiers" in maybe;
89710
+ const hasHolderKeys = "complexityDowngrades" in maybe;
89697
89711
  if (hasHolderKeys) {
89698
89712
  const holder = config4;
89699
89713
  complexityDowngrades = holder.complexityDowngrades;
89700
- if (holder.tiers) {
89701
- tiersConfig = holder;
89702
- }
89703
89714
  } else {
89704
89715
  complexityDowngrades = config4;
89705
89716
  }
@@ -89709,20 +89720,6 @@ function resolveComplexityModel(category, complexity, originalModel, userDowngra
89709
89720
  if (!rawDowngrade) {
89710
89721
  return { model: originalModel, downgraded: false };
89711
89722
  }
89712
- if (rawDowngrade.startsWith("tier:")) {
89713
- const tierName = rawDowngrade.slice(5).trim();
89714
- if (!tierName) {
89715
- return { model: originalModel, downgraded: false };
89716
- }
89717
- if (!tierContext || !tiersConfig) {
89718
- return { model: originalModel, downgraded: false };
89719
- }
89720
- const resolved = resolveTier(tierName, tierContext, tiersConfig);
89721
- if (!resolved) {
89722
- return { model: originalModel, downgraded: false };
89723
- }
89724
- return { model: resolved.model, downgraded: true };
89725
- }
89726
89723
  return { model: rawDowngrade, downgraded: true };
89727
89724
  }
89728
89725
  // src/tools/delegate-task/complexity-scorer.ts
@@ -89999,14 +89996,14 @@ You are NOT an interactive assistant. You are an autonomous problem-solver.
89999
89996
  - Report completion with summary of changes made
90000
89997
  </Category_Context>`;
90001
89998
  var DEFAULT_CATEGORIES = {
90002
- construct: { tier: "standard" },
90003
- source: { tier: "premium", variant: "max" },
90004
- "deep-jack": { tier: "premium", variant: "max" },
90005
- "matrix-bend": { tier: "standard" },
90006
- "bullet-time": { tier: "fast" },
90007
- "blue-pill": { tier: "standard" },
90008
- "red-pill": { tier: "premium", variant: "max" },
90009
- broadcast: { tier: "standard" }
89999
+ construct: {},
90000
+ source: { variant: "max" },
90001
+ "deep-jack": { variant: "max" },
90002
+ "matrix-bend": {},
90003
+ "bullet-time": {},
90004
+ "blue-pill": {},
90005
+ "red-pill": { variant: "max" },
90006
+ broadcast: {}
90010
90007
  };
90011
90008
  var CATEGORY_PROMPT_APPENDS = {
90012
90009
  construct: VISUAL_CATEGORY_PROMPT_APPEND,
@@ -90339,93 +90336,10 @@ function isPlanFamily(category) {
90339
90336
  // src/tools/delegate-task/tools.ts
90340
90337
  init_logger();
90341
90338
 
90342
- // src/config/resolve-tiers.ts
90343
- init_logger();
90344
- function resolveTiersInConfig(config4, ctx) {
90345
- const defaultTier = config4.default_tier;
90346
- const agents = walkAgentOverrides(config4.agents, defaultTier, ctx, config4);
90347
- const categories = walkCategoryOverrides(config4.categories, defaultTier, ctx, config4);
90348
- const { default_tier: _drop, ...rest } = config4;
90349
- return { ...rest, agents, categories };
90350
- }
90351
- function resolveTiersInCategoryRegistry(registry4, ctx, config4) {
90352
- const result = {};
90353
- for (const [name, entry] of Object.entries(registry4)) {
90354
- if (!entry)
90355
- continue;
90356
- result[name] = resolveEntry(entry, undefined, ctx, config4);
90357
- }
90358
- return result;
90359
- }
90360
- function walkAgentOverrides(agents, defaultTier, ctx, config4) {
90361
- if (!agents)
90362
- return agents;
90363
- const result = {};
90364
- for (const [name, entry] of Object.entries(agents)) {
90365
- if (!entry) {
90366
- result[name] = entry;
90367
- continue;
90368
- }
90369
- result[name] = resolveEntry(entry, defaultTier, ctx, config4);
90370
- }
90371
- return result;
90372
- }
90373
- function walkCategoryOverrides(categories, defaultTier, ctx, config4) {
90374
- if (!categories)
90375
- return categories;
90376
- const result = {};
90377
- for (const [name, entry] of Object.entries(categories)) {
90378
- if (!entry) {
90379
- result[name] = entry;
90380
- continue;
90381
- }
90382
- result[name] = resolveEntry(entry, defaultTier, ctx, config4);
90383
- }
90384
- return result;
90385
- }
90386
- function resolveEntry(entry, defaultTier, ctx, config4) {
90387
- if (entry.model) {
90388
- const { tier: _t2, ...rest2 } = entry;
90389
- return rest2;
90390
- }
90391
- const tier = entry.tier ?? defaultTier;
90392
- if (!tier)
90393
- return entry;
90394
- const resolved = resolveTier(tier, ctx, config4);
90395
- if (!resolved) {
90396
- log("[resolveTiersInConfig] tier could not be resolved", { tier });
90397
- return entry;
90398
- }
90399
- const { tier: _t, ...rest } = entry;
90400
- return { ...rest, model: resolved.model };
90401
- }
90402
-
90403
90339
  // src/shared/merge-categories.ts
90404
- function mergeCategories(userCategories, config4) {
90405
- const resolvedDefaults = resolveDefaultCategories(config4);
90406
- const merged = userCategories ? { ...resolvedDefaults, ...userCategories } : { ...resolvedDefaults };
90407
- return Object.fromEntries(Object.entries(merged).filter(([, config5]) => !config5.disable));
90408
- }
90409
- function resolveDefaultCategories(config4) {
90410
- const providerCache = readProviderModelsCache();
90411
- const availableModels = new Set;
90412
- if (providerCache) {
90413
- const connected = new Set(providerCache.connected);
90414
- for (const [providerId, models] of Object.entries(providerCache.models)) {
90415
- if (!connected.has(providerId))
90416
- continue;
90417
- for (const item of models) {
90418
- const modelID = typeof item === "string" ? item : item?.id;
90419
- if (modelID)
90420
- availableModels.add(`${providerId}/${modelID}`);
90421
- }
90422
- }
90423
- }
90424
- const ctx = {
90425
- availableModels,
90426
- connectedProviders: readConnectedProvidersCache()
90427
- };
90428
- return resolveTiersInCategoryRegistry(DEFAULT_CATEGORIES, ctx, config4);
90340
+ function mergeCategories(userCategories) {
90341
+ const merged = userCategories ? { ...DEFAULT_CATEGORIES, ...userCategories } : { ...DEFAULT_CATEGORIES };
90342
+ return Object.fromEntries(Object.entries(merged).filter(([, config4]) => !config4.disable));
90429
90343
  }
90430
90344
  // src/tools/delegate-task/background-continuation.ts
90431
90345
  async function executeBackgroundContinuation(args, ctx, executorCtx, parentContext) {
@@ -90500,6 +90414,9 @@ function getTimingConfig() {
90500
90414
  SESSION_CONTINUATION_STABILITY_MS
90501
90415
  };
90502
90416
  }
90417
+ function setPollTimeoutMs(ms) {
90418
+ MAX_POLL_TIME_MS = ms;
90419
+ }
90503
90420
 
90504
90421
  // src/tools/delegate-task/background-task.ts
90505
90422
  async function executeBackgroundTask(args, ctx, executorCtx, parentContext, agentToUse, categoryModel, systemContent) {
@@ -90578,6 +90495,67 @@ session_id: ${sessionId}
90578
90495
  });
90579
90496
  }
90580
90497
  }
90498
+ // src/config/preset-applier.ts
90499
+ init_logger();
90500
+ function resolvePresetModel(preset, key) {
90501
+ const entry = preset.agents?.[key] ?? preset.categories?.[key];
90502
+ if (entry)
90503
+ return entry;
90504
+ if (preset.default_model)
90505
+ return { model: preset.default_model };
90506
+ return;
90507
+ }
90508
+ function applyActivePreset(config4, presetName) {
90509
+ const name = presetName ?? config4.active_preset;
90510
+ if (!name)
90511
+ return config4;
90512
+ const preset = config4.model_presets?.[name];
90513
+ if (!preset) {
90514
+ log("[applyActivePreset] unknown active_preset", { active_preset: name });
90515
+ return config4;
90516
+ }
90517
+ const agents = fillRegistry(config4.agents, preset.agents, preset.default_model);
90518
+ const categories = fillRegistry(config4.categories, preset.categories, preset.default_model);
90519
+ const result = { ...config4 };
90520
+ if (agents !== config4.agents)
90521
+ result.agents = agents;
90522
+ if (categories !== config4.categories) {
90523
+ result.categories = categories;
90524
+ }
90525
+ return result;
90526
+ }
90527
+ function fillRegistry(registry4, presetEntries, defaultModel) {
90528
+ if (!registry4)
90529
+ return registry4;
90530
+ let changed = false;
90531
+ const result = {};
90532
+ for (const [name, entry] of Object.entries(registry4)) {
90533
+ if (!entry) {
90534
+ result[name] = entry;
90535
+ continue;
90536
+ }
90537
+ const filled = applyEntry(entry, presetEntries?.[name], defaultModel);
90538
+ if (filled) {
90539
+ changed = true;
90540
+ result[name] = filled;
90541
+ } else {
90542
+ result[name] = entry;
90543
+ }
90544
+ }
90545
+ return changed ? result : registry4;
90546
+ }
90547
+ function applyEntry(entry, presetEntry, defaultModel) {
90548
+ if (entry.model)
90549
+ return null;
90550
+ const model = presetEntry?.model ?? defaultModel;
90551
+ if (!model)
90552
+ return null;
90553
+ const variant = entry.variant ?? presetEntry?.variant;
90554
+ if (variant === undefined)
90555
+ return { ...entry, model };
90556
+ return { ...entry, model, variant };
90557
+ }
90558
+
90581
90559
  // src/tools/delegate-task/category-resolver.ts
90582
90560
  init_logger();
90583
90561
 
@@ -90635,7 +90613,7 @@ async function getAvailableModelsForDelegateTask(client2) {
90635
90613
  // src/tools/delegate-task/categories.ts
90636
90614
  init_logger();
90637
90615
  function resolveCategoryConfig(categoryName, options) {
90638
- const { userCategories, inheritedModel: _inheritedModel, systemDefaultModel, availableModels, tierContext, modelRequirements, tiers } = options;
90616
+ const { userCategories, inheritedModel: _inheritedModel, systemDefaultModel, availableModels, modelRequirements, presetEntry } = options;
90639
90617
  const defaultConfig = DEFAULT_CATEGORIES[categoryName];
90640
90618
  const userConfig = userCategories?.[categoryName];
90641
90619
  const hasExplicitUserConfig = userConfig !== undefined;
@@ -90654,16 +90632,13 @@ function resolveCategoryConfig(categoryName, options) {
90654
90632
  if (!defaultConfig && !userConfig) {
90655
90633
  return null;
90656
90634
  }
90657
- const tierConfig = tiers ? { tiers } : undefined;
90658
- const effectiveUserModel = resolveTierOnEntry(userConfig, tierContext, tierConfig);
90659
- const effectiveDefaultModel = resolveTierOnEntry(defaultConfig, tierContext, tierConfig);
90660
- const model = normalizeModel(effectiveUserModel ?? userConfig?.model) ?? normalizeModel(effectiveDefaultModel ?? defaultConfig?.model) ?? systemDefaultModel;
90661
- const hasUserModelOverride = (effectiveUserModel ?? userConfig?.model) !== undefined;
90635
+ const model = normalizeModel(userConfig?.model) ?? normalizeModel(presetEntry?.model) ?? normalizeModel(defaultConfig?.model) ?? systemDefaultModel;
90636
+ const hasUserModelOverride = (userConfig?.model ?? presetEntry?.model) !== undefined;
90662
90637
  const config4 = {
90663
90638
  ...defaultConfig,
90664
90639
  ...userConfig,
90665
90640
  model,
90666
- variant: userConfig?.variant ?? (hasUserModelOverride ? undefined : defaultConfig?.variant)
90641
+ variant: userConfig?.variant ?? presetEntry?.variant ?? (hasUserModelOverride ? undefined : defaultConfig?.variant)
90667
90642
  };
90668
90643
  let promptAppend = defaultPromptAppend;
90669
90644
  if (userConfig?.prompt_append) {
@@ -90673,12 +90648,6 @@ ${userConfig.prompt_append}` : userConfig.prompt_append;
90673
90648
  }
90674
90649
  return { config: config4, promptAppend, model, temperature: config4.temperature };
90675
90650
  }
90676
- function resolveTierOnEntry(entry, ctx, tiersConfig) {
90677
- if (!entry || entry.model || !entry.tier || !ctx)
90678
- return;
90679
- const resolved = resolveTier(entry.tier, ctx, tiersConfig);
90680
- return resolved?.model;
90681
- }
90682
90651
 
90683
90652
  // src/tools/delegate-task/model-selection.ts
90684
90653
  function normalizeModel2(model) {
@@ -90737,11 +90706,33 @@ function resolveModelForDelegateTask(input) {
90737
90706
  var MOUSE_AGENT2 = "mouse";
90738
90707
 
90739
90708
  // src/tools/delegate-task/category-resolver.ts
90709
+ function resolveCategoryPresetEntry(categoryName, ctx) {
90710
+ const { sessionID, modelPresets, activePreset } = ctx;
90711
+ if (!modelPresets)
90712
+ return;
90713
+ const overlayName = sessionID ? getSessionPreset(sessionID) : undefined;
90714
+ let result;
90715
+ if (overlayName) {
90716
+ const overlayPreset = modelPresets[overlayName];
90717
+ if (overlayPreset) {
90718
+ const entry = resolvePresetModel(overlayPreset, categoryName);
90719
+ if (entry)
90720
+ result = entry;
90721
+ } else {
90722
+ log("[preset] unknown session overlay preset, ignoring", { sessionID, presetName: overlayName });
90723
+ }
90724
+ }
90725
+ if (!result && activePreset) {
90726
+ const preset = modelPresets[activePreset];
90727
+ if (preset)
90728
+ result = resolvePresetModel(preset, categoryName);
90729
+ }
90730
+ return result;
90731
+ }
90740
90732
  async function resolveCategoryExecution(args, executorCtx, inheritedModel, systemDefaultModel) {
90741
- const { client: client2, userCategories, mouseModel, globalModel, modelRequirements, complexityDowngrades, tiers } = executorCtx;
90733
+ const { client: client2, userCategories, mouseModel, globalModel, modelRequirements, complexityDowngrades } = executorCtx;
90742
90734
  const categoryRequirements = getCategoryModelRequirements(modelRequirements ? { modelRequirements } : undefined);
90743
90735
  const availableModels = await getAvailableModelsForDelegateTask(client2);
90744
- const connectedProviders = readConnectedProvidersCache();
90745
90736
  const categoryName = args.category;
90746
90737
  const enabledCategories = mergeCategories(userCategories);
90747
90738
  const categoryExists = enabledCategories[categoryName] !== undefined;
@@ -90750,8 +90741,8 @@ async function resolveCategoryExecution(args, executorCtx, inheritedModel, syste
90750
90741
  inheritedModel,
90751
90742
  systemDefaultModel,
90752
90743
  availableModels,
90753
- tierContext: { availableModels, connectedProviders },
90754
- modelRequirements
90744
+ modelRequirements,
90745
+ presetEntry: resolveCategoryPresetEntry(categoryName, executorCtx)
90755
90746
  });
90756
90747
  if (!resolved) {
90757
90748
  const requirement2 = categoryRequirements[categoryName];
@@ -90847,9 +90838,7 @@ Available categories: ${allCategoryNames}`
90847
90838
  }
90848
90839
  if (actualModel && (complexityLevel === 1 || complexityLevel === 2)) {
90849
90840
  const userDowngrades = userCategories?.[args.category]?.complexity_downgrades;
90850
- const holder = complexityDowngrades || tiers ? { complexityDowngrades, tiers } : undefined;
90851
- const tierCtx = { availableModels, connectedProviders };
90852
- const resolvedDowngrade = resolveComplexityModel(args.category, complexityLevel, actualModel, userDowngrades, holder, tierCtx);
90841
+ const resolvedDowngrade = resolveComplexityModel(args.category, complexityLevel, actualModel, userDowngrades, complexityDowngrades);
90853
90842
  if (resolvedDowngrade.downgraded) {
90854
90843
  complexityDowngraded = true;
90855
90844
  log("[complexity] Auto-downgraded model", {
@@ -91861,7 +91850,7 @@ session_id: ${sessionID}
91861
91850
  });
91862
91851
  } finally {
91863
91852
  if (syncSessionID) {
91864
- subagentSessions.delete(syncSessionID);
91853
+ unregisterSubagentSession(syncSessionID);
91865
91854
  client2.session.abort({ path: { id: syncSessionID } }).catch((err) => {
91866
91855
  log("[delegate-task] Sync session abort failed:", err);
91867
91856
  });
@@ -92190,7 +92179,7 @@ Prompts MUST be in English.`;
92190
92179
  let actualModel;
92191
92180
  let isUnstableAgent = false;
92192
92181
  if (args.category) {
92193
- const resolution = await resolveCategoryExecution(args, options, inheritedModel, systemDefaultModel);
92182
+ const resolution = await resolveCategoryExecution(args, { ...options, sessionID: ctx.sessionID }, inheritedModel, systemDefaultModel);
92194
92183
  if (resolution.error) {
92195
92184
  return resolution.error;
92196
92185
  }
@@ -92591,8 +92580,8 @@ Original error: ${createResult.error}`;
92591
92580
  });
92592
92581
  }
92593
92582
  // src/tools/task/task-cleanup.ts
92594
- import { existsSync as existsSync70, readdirSync as readdirSync22, readFileSync as readFileSync41, unlinkSync as unlinkSync11 } from "fs";
92595
- import { join as join70 } from "path";
92583
+ import { existsSync as existsSync71, readdirSync as readdirSync22, readFileSync as readFileSync42, unlinkSync as unlinkSync11 } from "fs";
92584
+ import { join as join72 } from "path";
92596
92585
  init_logger();
92597
92586
  function parseOlderThan(value) {
92598
92587
  const match = value.trim().match(/^(\d+)(d|h|m)$/);
@@ -92651,17 +92640,17 @@ Returns counts: {deleted, remaining, deletedIds}`,
92651
92640
  }
92652
92641
  const directory = context?.directory ?? ctx?.directory ?? process.cwd();
92653
92642
  const taskDir = getTaskDir(config4, directory);
92654
- if (!existsSync70(taskDir)) {
92643
+ if (!existsSync71(taskDir)) {
92655
92644
  return JSON.stringify({ deleted: 0, remaining: 0, deletedIds: [] });
92656
92645
  }
92657
92646
  const files = readdirSync22(taskDir).filter((f) => f.endsWith(".json") && f.startsWith("T-"));
92658
92647
  const total = files.length;
92659
92648
  const deletedIds = [];
92660
92649
  for (const file3 of files) {
92661
- const filePath = join70(taskDir, file3);
92650
+ const filePath = join72(taskDir, file3);
92662
92651
  let raw;
92663
92652
  try {
92664
- const content = readFileSync41(filePath, "utf-8");
92653
+ const content = readFileSync42(filePath, "utf-8");
92665
92654
  raw = JSON.parse(content);
92666
92655
  } catch {
92667
92656
  continue;
@@ -92694,7 +92683,9 @@ Returns counts: {deleted, remaining, deletedIds}`,
92694
92683
  });
92695
92684
  }
92696
92685
  // src/tools/task/task-create.ts
92697
- import { join as join71 } from "path";
92686
+ import { readdirSync as readdirSync23, statSync as statSync17 } from "fs";
92687
+ import { join as join73 } from "path";
92688
+ init_logger();
92698
92689
  function createTaskCreateTool(config4, ctx) {
92699
92690
  return tool({
92700
92691
  description: `Create a new task with auto-generated ID and threadID recording.
@@ -92736,6 +92727,13 @@ async function handleCreate2(args, config4, ctx, context) {
92736
92727
  return JSON.stringify({ error: "task_lock_unavailable" });
92737
92728
  }
92738
92729
  try {
92730
+ const existingTask = findDuplicateTask(taskDir, validatedArgs.subject, directory);
92731
+ if (existingTask) {
92732
+ return JSON.stringify({
92733
+ task: { id: existingTask.id, subject: existingTask.subject },
92734
+ deduplicated: true
92735
+ });
92736
+ }
92739
92737
  const taskId = generateTaskId();
92740
92738
  const task = {
92741
92739
  id: taskId,
@@ -92752,7 +92750,7 @@ async function handleCreate2(args, config4, ctx, context) {
92752
92750
  projectRoot: directory
92753
92751
  };
92754
92752
  const validatedTask = TaskObjectSchema.parse(task);
92755
- writeJsonAtomic(join71(taskDir, `${taskId}.json`), validatedTask);
92753
+ writeJsonAtomic(join73(taskDir, `${taskId}.json`), validatedTask);
92756
92754
  return JSON.stringify({
92757
92755
  task: {
92758
92756
  id: validatedTask.id,
@@ -92772,8 +92770,47 @@ async function handleCreate2(args, config4, ctx, context) {
92772
92770
  return JSON.stringify({ error: "internal_error" });
92773
92771
  }
92774
92772
  }
92773
+ function findDuplicateTask(taskDir, subject, directory) {
92774
+ let files;
92775
+ try {
92776
+ files = readdirSync23(taskDir).filter((f) => f.endsWith(".json") && f.startsWith("T-"));
92777
+ } catch (error95) {
92778
+ log(`[task-create] Failed to list task dir ${taskDir} during dedup scan`, error95);
92779
+ return null;
92780
+ }
92781
+ for (const file3 of files) {
92782
+ const filePath = join73(taskDir, file3);
92783
+ let existing;
92784
+ try {
92785
+ existing = readJsonSafe(filePath, TaskObjectSchema);
92786
+ } catch (error95) {
92787
+ log(`[task-create] Failed to read task file ${filePath} during dedup scan`, error95);
92788
+ continue;
92789
+ }
92790
+ if (!existing)
92791
+ continue;
92792
+ const isActive = existing.status === "pending" || existing.status === "in_progress";
92793
+ if (!isActive)
92794
+ continue;
92795
+ if (existing.subject.trim() !== subject.trim())
92796
+ continue;
92797
+ if (existing.projectRoot !== directory)
92798
+ continue;
92799
+ let mtimeMs;
92800
+ try {
92801
+ mtimeMs = statSync17(filePath).mtimeMs;
92802
+ } catch (error95) {
92803
+ log(`[task-create] Failed to stat task file ${filePath} during dedup scan`, error95);
92804
+ continue;
92805
+ }
92806
+ if (Date.now() - mtimeMs > DEDUP_WINDOW_MS)
92807
+ continue;
92808
+ return existing;
92809
+ }
92810
+ return null;
92811
+ }
92775
92812
  // src/tools/task/task-get.ts
92776
- import { join as join72 } from "path";
92813
+ import { join as join74 } from "path";
92777
92814
  function parseTaskId(id) {
92778
92815
  if (!TASK_ID_PATTERN.test(id))
92779
92816
  return null;
@@ -92798,7 +92835,7 @@ Returns null if the task does not exist or the file is invalid.`,
92798
92835
  }
92799
92836
  const directory = context?.directory ?? ctx?.directory ?? process.cwd();
92800
92837
  const taskDir = getTaskDir(config4, directory);
92801
- const taskPath = join72(taskDir, `${taskId}.json`);
92838
+ const taskPath = join74(taskDir, `${taskId}.json`);
92802
92839
  const task = readJsonSafe(taskPath, TaskObjectSchema);
92803
92840
  return JSON.stringify({ task: task ?? null });
92804
92841
  } catch (error95) {
@@ -92811,35 +92848,42 @@ Returns null if the task does not exist or the file is invalid.`,
92811
92848
  });
92812
92849
  }
92813
92850
  // src/tools/task/task-list.ts
92814
- import { existsSync as existsSync71, readdirSync as readdirSync23 } from "fs";
92815
- import { join as join73 } from "path";
92851
+ import { existsSync as existsSync72, readdirSync as readdirSync24 } from "fs";
92852
+ import { join as join75 } from "path";
92816
92853
  function createTaskList(config4, ctx) {
92817
92854
  return tool({
92818
92855
  description: `List all active tasks with summary information.
92819
92856
 
92820
92857
  Returns tasks excluding completed and deleted statuses by default.
92821
92858
  For each task's blockedBy field, filters to only include unresolved (non-completed) blockers.
92822
- Returns summary format: id, subject, status, owner, blockedBy (not full description).`,
92823
- args: {},
92824
- execute: async (_args, context) => {
92859
+ Returns summary format: id, subject, status, owner, blockedBy (not full description).
92860
+
92861
+ Optional \`parentID\` filter: when provided, returns only tasks whose parentID matches
92862
+ (i.e. the subtasks of that parent task).`,
92863
+ args: {
92864
+ parentID: tool.schema.string().optional().describe("Filter to subtasks of this parent task ID")
92865
+ },
92866
+ execute: async (args, context) => {
92867
+ const parentID = args.parentID;
92825
92868
  const directory = context?.directory ?? ctx?.directory ?? process.cwd();
92826
92869
  const taskDir = getTaskDir(config4, directory);
92827
- if (!existsSync71(taskDir)) {
92870
+ if (!existsSync72(taskDir)) {
92828
92871
  return JSON.stringify({ tasks: [] });
92829
92872
  }
92830
- const files = readdirSync23(taskDir).filter((f) => f.endsWith(".json") && f.startsWith("T-")).map((f) => f.replace(".json", ""));
92873
+ const files = readdirSync24(taskDir).filter((f) => f.endsWith(".json") && f.startsWith("T-")).map((f) => f.replace(".json", ""));
92831
92874
  if (files.length === 0) {
92832
92875
  return JSON.stringify({ tasks: [] });
92833
92876
  }
92834
92877
  const allTasks = [];
92835
92878
  for (const fileId of files) {
92836
- const task = readJsonSafe(join73(taskDir, `${fileId}.json`), TaskObjectSchema);
92879
+ const task = readJsonSafe(join75(taskDir, `${fileId}.json`), TaskObjectSchema);
92837
92880
  if (task) {
92838
92881
  allTasks.push(task);
92839
92882
  }
92840
92883
  }
92841
92884
  const activeTasks = allTasks.filter((task) => task.status !== "completed" && task.status !== "deleted");
92842
- const summaries = activeTasks.map((task) => {
92885
+ const filteredTasks = parentID ? activeTasks.filter((task) => task.parentID === parentID) : activeTasks;
92886
+ const summaries = filteredTasks.map((task) => {
92843
92887
  const unresolvedBlockers = task.blockedBy.filter((blockerId) => {
92844
92888
  const blockerTask = allTasks.find((t) => t.id === blockerId);
92845
92889
  return blockerTask?.status !== "completed";
@@ -92849,7 +92893,8 @@ Returns summary format: id, subject, status, owner, blockedBy (not full descript
92849
92893
  subject: task.subject,
92850
92894
  status: task.status,
92851
92895
  owner: task.owner,
92852
- blockedBy: unresolvedBlockers
92896
+ blockedBy: unresolvedBlockers,
92897
+ parentID: task.parentID
92853
92898
  };
92854
92899
  });
92855
92900
  return JSON.stringify({
@@ -92860,7 +92905,7 @@ Returns summary format: id, subject, status, owner, blockedBy (not full descript
92860
92905
  });
92861
92906
  }
92862
92907
  // src/tools/task/task-update.ts
92863
- import { join as join74 } from "path";
92908
+ import { join as join76 } from "path";
92864
92909
  function parseTaskId2(id) {
92865
92910
  if (!TASK_ID_PATTERN.test(id))
92866
92911
  return null;
@@ -92906,7 +92951,7 @@ async function handleUpdate(args, config4, ctx, context) {
92906
92951
  return JSON.stringify({ error: "task_lock_unavailable" });
92907
92952
  }
92908
92953
  try {
92909
- const taskPath = join74(taskDir, `${taskId}.json`);
92954
+ const taskPath = join76(taskDir, `${taskId}.json`);
92910
92955
  const task = readJsonSafe(taskPath, TaskObjectSchema);
92911
92956
  if (!task) {
92912
92957
  return JSON.stringify({ error: "task_not_found" });
@@ -93559,19 +93604,19 @@ function detectRepetitiveToolUse(window2) {
93559
93604
  }
93560
93605
 
93561
93606
  // src/features/background-agent/message-dir.ts
93562
- import { existsSync as existsSync72, readdirSync as readdirSync24, readFileSync as readFileSync42 } from "fs";
93563
- import { join as join75 } from "path";
93607
+ import { existsSync as existsSync73, readdirSync as readdirSync25, readFileSync as readFileSync43 } from "fs";
93608
+ import { join as join77 } from "path";
93564
93609
  var MESSAGE_DIR_CACHE_CAP = 1000;
93565
93610
  var messageDirCache = new Map;
93566
93611
  function computeMessageDir(sessionID) {
93567
- if (!existsSync72(MESSAGE_STORAGE))
93612
+ if (!existsSync73(MESSAGE_STORAGE))
93568
93613
  return null;
93569
- const directPath = join75(MESSAGE_STORAGE, sessionID);
93570
- if (existsSync72(directPath))
93614
+ const directPath = join77(MESSAGE_STORAGE, sessionID);
93615
+ if (existsSync73(directPath))
93571
93616
  return directPath;
93572
- for (const dir of readdirSync24(MESSAGE_STORAGE)) {
93573
- const sessionPath = join75(MESSAGE_STORAGE, dir, sessionID);
93574
- if (existsSync72(sessionPath))
93617
+ for (const dir of readdirSync25(MESSAGE_STORAGE)) {
93618
+ const sessionPath = join77(MESSAGE_STORAGE, dir, sessionID);
93619
+ if (existsSync73(sessionPath))
93575
93620
  return sessionPath;
93576
93621
  }
93577
93622
  return null;
@@ -93605,11 +93650,11 @@ function hasPartialAgentOrModel(message) {
93605
93650
  }
93606
93651
  function findNearestMessageExcludingCompaction(messageDir) {
93607
93652
  try {
93608
- const files = readdirSync24(messageDir).filter((name) => name.endsWith(".json")).sort().reverse();
93653
+ const files = readdirSync25(messageDir).filter((name) => name.endsWith(".json")).sort().reverse();
93609
93654
  const parsedByFile = new Map;
93610
93655
  for (const file3 of files) {
93611
93656
  try {
93612
- const content = readFileSync42(join75(messageDir, file3), "utf-8");
93657
+ const content = readFileSync43(join77(messageDir, file3), "utf-8");
93613
93658
  const parsed = JSON.parse(content);
93614
93659
  parsedByFile.set(file3, parsed);
93615
93660
  if (hasFullAgentAndModel(parsed)) {
@@ -94813,7 +94858,7 @@ class BackgroundManager {
94813
94858
  }
94814
94859
  this.tasks.delete(taskId);
94815
94860
  if (task.sessionID) {
94816
- subagentSessions.delete(task.sessionID);
94861
+ unregisterSubagentSession(task.sessionID);
94817
94862
  }
94818
94863
  }
94819
94864
  }
@@ -95791,9 +95836,9 @@ class TmuxSessionManager {
95791
95836
  var SESSION_TIMEOUT_MS3 = 10 * 60 * 1000;
95792
95837
  var MIN_STABILITY_TIME_MS4 = 10 * 1000;
95793
95838
  // src/agents/dynamic-agent-prompt-builder.ts
95794
- import { existsSync as existsSync73, readFileSync as readFileSync43 } from "fs";
95795
- import { homedir as homedir10 } from "os";
95796
- import { join as join76 } from "path";
95839
+ import { existsSync as existsSync74, readFileSync as readFileSync44 } from "fs";
95840
+ import { homedir as homedir11 } from "os";
95841
+ import { join as join78 } from "path";
95797
95842
  init_logger();
95798
95843
  function categorizeTools(toolNames) {
95799
95844
  return toolNames.map((name) => {
@@ -96155,24 +96200,24 @@ function exploreCtxPart(hasGrepGlob) {
96155
96200
  function resolveContextModeDisciplinePath() {
96156
96201
  try {
96157
96202
  const resolved = __require.resolve("context-mode/configs/opencode/AGENTS.md");
96158
- if (existsSync73(resolved))
96203
+ if (existsSync74(resolved))
96159
96204
  return resolved;
96160
96205
  } catch {}
96161
96206
  try {
96162
96207
  const cacheDir = getOpenCodeCacheDir();
96163
- const p = join76(cacheDir, "packages/context-mode@latest/node_modules/context-mode/configs/opencode/AGENTS.md");
96164
- if (existsSync73(p))
96208
+ const p = join78(cacheDir, "packages/context-mode@latest/node_modules/context-mode/configs/opencode/AGENTS.md");
96209
+ if (existsSync74(p))
96165
96210
  return p;
96166
96211
  } catch {}
96167
96212
  try {
96168
- const base = process.env.XDG_CACHE_HOME ?? join76(homedir10(), ".cache");
96169
- const p = join76(base, "opencode/packages/context-mode@latest/node_modules/context-mode/configs/opencode/AGENTS.md");
96170
- if (existsSync73(p))
96213
+ const base = process.env.XDG_CACHE_HOME ?? join78(homedir11(), ".cache");
96214
+ const p = join78(base, "opencode/packages/context-mode@latest/node_modules/context-mode/configs/opencode/AGENTS.md");
96215
+ if (existsSync74(p))
96171
96216
  return p;
96172
96217
  } catch {}
96173
96218
  try {
96174
- const p2 = join76(homedir10(), ".cache/opencode/packages/context-mode@latest/node_modules/context-mode/configs/opencode/AGENTS.md");
96175
- if (existsSync73(p2))
96219
+ const p2 = join78(homedir11(), ".cache/opencode/packages/context-mode@latest/node_modules/context-mode/configs/opencode/AGENTS.md");
96220
+ if (existsSync74(p2))
96176
96221
  return p2;
96177
96222
  } catch {}
96178
96223
  return null;
@@ -96190,11 +96235,11 @@ function loadContextModeDiscipline(hasGrepGlob = true) {
96190
96235
  const p = resolveContextModeDisciplinePath();
96191
96236
  if (!p)
96192
96237
  throw new Error("discipline file not found");
96193
- const raw = readFileSync43(p, "utf8");
96238
+ const raw = readFileSync44(p, "utf8");
96194
96239
  let version4 = "1.0.169";
96195
96240
  try {
96196
96241
  const pkgResolved = __require.resolve("context-mode/package.json");
96197
- const pkgRaw = readFileSync43(pkgResolved, "utf8");
96242
+ const pkgRaw = readFileSync44(pkgResolved, "utf8");
96198
96243
  const pkg = JSON.parse(pkgRaw);
96199
96244
  if (pkg.version)
96200
96245
  version4 = pkg.version;
@@ -96241,11 +96286,11 @@ function loadCompactContextDiscipline(hasGrepGlob = true) {
96241
96286
  const p = resolveContextModeDisciplinePath();
96242
96287
  if (!p)
96243
96288
  throw new Error("discipline file not found");
96244
- const raw = readFileSync43(p, "utf8");
96289
+ const raw = readFileSync44(p, "utf8");
96245
96290
  let version4 = "1.0.169";
96246
96291
  try {
96247
96292
  const pkgResolved = __require.resolve("context-mode/package.json");
96248
- const pkgRaw = readFileSync43(pkgResolved, "utf8");
96293
+ const pkgRaw = readFileSync44(pkgResolved, "utf8");
96249
96294
  const pkg = JSON.parse(pkgRaw);
96250
96295
  if (pkg.version)
96251
96296
  version4 = pkg.version;
@@ -97314,8 +97359,8 @@ function createBddContractAgent(model) {
97314
97359
  createBddContractAgent.mode = MODE2;
97315
97360
 
97316
97361
  // src/agents/builtin-agents/resolve-file-uri.ts
97317
- import { existsSync as existsSync74, readFileSync as readFileSync44 } from "fs";
97318
- import { homedir as homedir11 } from "os";
97362
+ import { existsSync as existsSync75, readFileSync as readFileSync45 } from "fs";
97363
+ import { homedir as homedir12 } from "os";
97319
97364
  import { isAbsolute as isAbsolute6, resolve as resolve10 } from "path";
97320
97365
  function resolvePromptAppend(promptAppend, configDir) {
97321
97366
  if (!promptAppend.startsWith("file://"))
@@ -97324,16 +97369,16 @@ function resolvePromptAppend(promptAppend, configDir) {
97324
97369
  let filePath;
97325
97370
  try {
97326
97371
  const decoded = decodeURIComponent(encoded);
97327
- const expanded = decoded.startsWith("~/") ? decoded.replace(/^~\//, `${homedir11()}/`) : decoded;
97372
+ const expanded = decoded.startsWith("~/") ? decoded.replace(/^~\//, `${homedir12()}/`) : decoded;
97328
97373
  filePath = isAbsolute6(expanded) ? expanded : resolve10(configDir ?? process.cwd(), expanded);
97329
97374
  } catch {
97330
97375
  return `[WARNING: Malformed file URI (invalid percent-encoding): ${promptAppend}]`;
97331
97376
  }
97332
- if (!existsSync74(filePath)) {
97377
+ if (!existsSync75(filePath)) {
97333
97378
  return `[WARNING: Could not resolve file URI: ${promptAppend}]`;
97334
97379
  }
97335
97380
  try {
97336
- return readFileSync44(filePath, "utf8");
97381
+ return readFileSync45(filePath, "utf8");
97337
97382
  } catch {
97338
97383
  return `[WARNING: Could not read file: ${promptAppend}]`;
97339
97384
  }
@@ -99217,16 +99262,19 @@ var OPERATOR_PROMPT_METADATA = {
99217
99262
  category: "exploration",
99218
99263
  cost: "CHEAP",
99219
99264
  promptAlias: "Librarian",
99220
- keyTrigger: "External library/source mentioned \u2192 fire `librarian` background",
99265
+ keyTrigger: "External library/source mentioned or code search needed \u2192 fire `librarian` background",
99221
99266
  triggers: [
99222
- { domain: "Librarian", trigger: "Unfamiliar packages / libraries, struggles at weird behaviour (to find existing implementation of opensource)" }
99267
+ { domain: "Librarian", trigger: "Unfamiliar packages / libraries, struggles at weird behaviour (to find existing implementation of opensource)" },
99268
+ { domain: "Code Search", trigger: "Find real-world code examples, search GitHub repos for patterns, or need OSS implementation references" }
99223
99269
  ],
99224
99270
  useWhen: [
99225
99271
  "How do I use [library]?",
99226
99272
  "What's the best practice for [framework feature]?",
99227
99273
  "Why does [external dependency] behave this way?",
99228
99274
  "Find examples of [library] usage",
99229
- "Working with unfamiliar npm/pip/cargo packages"
99275
+ "Working with unfamiliar npm/pip/cargo packages",
99276
+ "Show me real-world code examples of [pattern]",
99277
+ "Search GitHub for implementations of [feature]"
99230
99278
  ]
99231
99279
  };
99232
99280
  function createOperatorAgent(model) {
@@ -99327,7 +99375,7 @@ context7_query-docs(libraryId: id, query: "specific topic")
99327
99375
  Tool 1: context7_resolve-library-id("library-name")
99328
99376
  \u2192 then context7_query-docs(libraryId: id, query: "specific-topic")
99329
99377
  Tool 2: webfetch(relevant_pages_from_sitemap) // Targeted, not random
99330
- Tool 3: grep_app_searchGitHub(query: "usage pattern", language: ["TypeScript"])
99378
+ Tool 3: white_rabbit_searchGitHub(query: "usage pattern", language: ["TypeScript"])
99331
99379
  \`\`\`
99332
99380
 
99333
99381
  **Output**: Summarize findings with links to official docs (versioned if applicable) and real-world examples.
@@ -99357,7 +99405,7 @@ Step 4: Construct permalink
99357
99405
  **Parallel acceleration (4+ calls)**:
99358
99406
  \`\`\`
99359
99407
  Tool 1: gh repo clone owner/repo \${TMPDIR:-/tmp}/repo -- --depth 1
99360
- Tool 2: grep_app_searchGitHub(query: "function_name", repo: "owner/repo")
99408
+ Tool 2: white_rabbit_searchGitHub(query: "function_name", repo: "owner/repo")
99361
99409
  Tool 3: gh api repos/owner/repo/commits/HEAD --jq '.sha'
99362
99410
  Tool 4: context7_get-library-docs(id, topic: "relevant-api")
99363
99411
  \`\`\`
@@ -99396,8 +99444,8 @@ Tool 1: context7_resolve-library-id \u2192 context7_query-docs
99396
99444
  Tool 2: webfetch(targeted_doc_pages_from_sitemap)
99397
99445
 
99398
99446
  // Code Search
99399
- Tool 3: grep_app_searchGitHub(query: "pattern1", language: [...])
99400
- Tool 4: grep_app_searchGitHub(query: "pattern2", useRegexp: true)
99447
+ Tool 3: white_rabbit_searchGitHub(query: "pattern1", language: [...])
99448
+ Tool 4: white_rabbit_searchGitHub(query: "pattern2", useRegexp: true)
99401
99449
 
99402
99450
  // Source Analysis
99403
99451
  Tool 5: gh repo clone owner/repo \${TMPDIR:-/tmp}/repo -- --depth 1
@@ -99453,7 +99501,7 @@ https://github.com/tanstack/query/blob/abc123def/packages/react-query/src/useQue
99453
99501
  | **Sitemap Discovery** | webfetch | \`webfetch(docs_url + "/sitemap.xml")\` to understand doc structure |
99454
99502
  | **Read Doc Page** | webfetch | \`webfetch(specific_doc_page)\` for targeted documentation |
99455
99503
  | **Latest Info** | websearch_exa | \`websearch_exa_web_search_exa("query ${new Date().getFullYear()}")\` |
99456
- | **Fast Code Search** | grep_app | \`grep_app_searchGitHub(query, language, useRegexp)\` |
99504
+ | **Fast Code Search** | white_rabbit | \`white_rabbit_searchGitHub(query, language, useRegexp)\` |
99457
99505
  | **Deep Code Search** | gh CLI | \`gh search code "query" --repo owner/repo\` |
99458
99506
  | **Clone Repo** | gh CLI | \`gh repo clone owner/repo \${TMPDIR:-/tmp}/name -- --depth 1\` |
99459
99507
  | **Issues/PRs** | gh CLI | \`gh search issues/prs "query" --repo owner/repo\` |
@@ -99489,16 +99537,16 @@ Use OS-appropriate temp directory:
99489
99537
  **Doc Discovery is SEQUENTIAL** (websearch \u2192 version check \u2192 sitemap \u2192 investigate).
99490
99538
  **Main phase is PARALLEL** once you know where to look.
99491
99539
 
99492
- **Always vary queries** when using grep_app:
99540
+ **Always vary queries** when using white_rabbit:
99493
99541
  \`\`\`
99494
99542
  // GOOD: Different angles
99495
- grep_app_searchGitHub(query: "useQuery(", language: ["TypeScript"])
99496
- grep_app_searchGitHub(query: "queryOptions", language: ["TypeScript"])
99497
- grep_app_searchGitHub(query: "staleTime:", language: ["TypeScript"])
99543
+ white_rabbit_searchGitHub(query: "useQuery(", language: ["TypeScript"])
99544
+ white_rabbit_searchGitHub(query: "queryOptions", language: ["TypeScript"])
99545
+ white_rabbit_searchGitHub(query: "staleTime:", language: ["TypeScript"])
99498
99546
 
99499
99547
  // BAD: Same pattern
99500
- grep_app_searchGitHub(query: "useQuery")
99501
- grep_app_searchGitHub(query: "useQuery")
99548
+ white_rabbit_searchGitHub(query: "useQuery")
99549
+ white_rabbit_searchGitHub(query: "useQuery")
99502
99550
  \`\`\`
99503
99551
 
99504
99552
  ---
@@ -99508,7 +99556,7 @@ grep_app_searchGitHub(query: "useQuery")
99508
99556
  | Failure | Recovery Action |
99509
99557
  |---------|-----------------|
99510
99558
  | context7 not found | Clone repo, read source + README directly |
99511
- | grep_app no results | Broaden query, try concept instead of exact name |
99559
+ | white_rabbit no results | Broaden query, try concept instead of exact name |
99512
99560
  | gh API rate limit | Use cloned repo in temp directory |
99513
99561
  | Repo not found | Search for forks or mirrors |
99514
99562
  | Sitemap not found | Try \`/sitemap-0.xml\`, \`/sitemap_index.xml\`, or fetch docs index page and parse navigation |
@@ -99519,7 +99567,7 @@ grep_app_searchGitHub(query: "useQuery")
99519
99567
 
99520
99568
  ## COMMUNICATION RULES
99521
99569
 
99522
- 1. **NO TOOL NAMES**: Say "I'll search the codebase" not "I'll use grep_app"
99570
+ 1. **NO TOOL NAMES**: Say "I'll search the codebase" not "I'll use white_rabbit"
99523
99571
  2. **NO PREAMBLE**: Answer directly, skip "I'll help you with..."
99524
99572
  3. **ALWAYS CITE**: Every code claim needs a permalink
99525
99573
  4. **USE MARKDOWN**: Code blocks with language identifiers
@@ -102891,14 +102939,6 @@ var document_reader = {
102891
102939
  enabled: true
102892
102940
  };
102893
102941
 
102894
- // src/mcp/grep-app.ts
102895
- var grep_app = {
102896
- type: "remote",
102897
- url: "https://mcp.grep.app",
102898
- enabled: true,
102899
- oauth: false
102900
- };
102901
-
102902
102942
  // src/mcp/mcp-validator.ts
102903
102943
  function validateWebsearchConfig(config4) {
102904
102944
  const provider = config4?.provider || "exa";
@@ -102947,8 +102987,16 @@ function createWebsearchConfig(config4) {
102947
102987
  }
102948
102988
  var websearch = createWebsearchConfig();
102949
102989
 
102990
+ // src/mcp/white-rabbit.ts
102991
+ var white_rabbit = {
102992
+ type: "remote",
102993
+ url: "https://mcp.grep.app",
102994
+ enabled: true,
102995
+ oauth: false
102996
+ };
102997
+
102950
102998
  // src/mcp/types.ts
102951
- var McpNameSchema = exports_external.enum(["websearch", "context7", "grep_app", "document_reader"]);
102999
+ var McpNameSchema = exports_external.enum(["websearch", "context7", "white_rabbit", "document_reader"]);
102952
103000
  var AnyMcpNameSchema = exports_external.string().min(1);
102953
103001
 
102954
103002
  // src/mcp/index.ts
@@ -102997,8 +103045,8 @@ function createBuiltinMcps(disabledMcps = [], config4, options = {}) {
102997
103045
  if (!disabledMcps.includes("context7")) {
102998
103046
  mcps.context7 = context7;
102999
103047
  }
103000
- if (!disabledMcps.includes("grep_app")) {
103001
- mcps.grep_app = grep_app;
103048
+ if (!disabledMcps.includes("white_rabbit")) {
103049
+ mcps.white_rabbit = white_rabbit;
103002
103050
  }
103003
103051
  if (!disabledMcps.includes("document_reader")) {
103004
103052
  if (commandAvailable("uvx")) {
@@ -103100,7 +103148,7 @@ function applyToolConfig(params) {
103100
103148
  const denyTaskTools = !isTaskSystem ? { "task_*": "deny", task: "deny" } : {};
103101
103149
  params.config.tools = {
103102
103150
  ...params.config.tools,
103103
- "grep_app_*": false,
103151
+ "white_rabbit_*": false,
103104
103152
  LspHover: false,
103105
103153
  LspCodeActions: false,
103106
103154
  LspCodeActionResolve: false,
@@ -103112,7 +103160,7 @@ function applyToolConfig(params) {
103112
103160
  const questionPermission = isCliRunMode ? "deny" : "allow";
103113
103161
  const operator = agentByKey(params.agentResult, "operator");
103114
103162
  if (operator) {
103115
- operator.permission = { ...operator.permission, "grep_app_*": "allow" };
103163
+ operator.permission = { ...operator.permission, "white_rabbit_*": "allow" };
103116
103164
  }
103117
103165
  const construct = agentByKey(params.agentResult, "construct");
103118
103166
  if (construct) {
@@ -103306,7 +103354,8 @@ function createToolRegistry(args) {
103306
103354
  availableSkills: skillContext.availableSkills,
103307
103355
  modelRequirements: pluginConfig.modelRequirements,
103308
103356
  complexityDowngrades: pluginConfig.complexityDowngrades,
103309
- tiers: pluginConfig.tiers,
103357
+ modelPresets: pluginConfig.model_presets,
103358
+ activePreset: pluginConfig.active_preset,
103310
103359
  agentOverrides: pluginConfig.agents,
103311
103360
  onSyncSessionCreated: async (event) => {
103312
103361
  log("[index] onSyncSessionCreated callback", {
@@ -103367,6 +103416,7 @@ function createToolRegistry(args) {
103367
103416
  ...createHandoffTools(ctx),
103368
103417
  ...createPdfExtractFiguresTool(),
103369
103418
  ...createDcpSwitchProfileTool({ pluginConfig }),
103419
+ ...createPresetTool({ pluginConfig, directory: ctx.directory }),
103370
103420
  ...backgroundTools,
103371
103421
  delegate_agent: delegateAgent,
103372
103422
  ...lookAt ? { look_at: lookAt } : {},
@@ -103393,6 +103443,10 @@ function createToolRegistry(args) {
103393
103443
  // src/create-tools.ts
103394
103444
  async function createTools(args) {
103395
103445
  const { ctx, pluginConfig, managers } = args;
103446
+ const pollTimeout = resolveTasksConfig(pluginConfig).pollTimeoutMs;
103447
+ if (pollTimeout !== undefined) {
103448
+ setPollTimeoutMs(pollTimeout);
103449
+ }
103396
103450
  const skillContext = await createSkillContext({
103397
103451
  directory: ctx.directory,
103398
103452
  pluginConfig
@@ -103494,10 +103548,8 @@ var AgentPermissionSchema = exports_external.object({
103494
103548
  });
103495
103549
 
103496
103550
  // src/config/schema/agent-overrides.ts
103497
- var TierNameSchema = exports_external.enum(["free", "fast", "standard", "premium", "frontier"]);
103498
103551
  var AgentOverrideConfigSchema = exports_external.object({
103499
103552
  model: exports_external.string().optional(),
103500
- tier: TierNameSchema.optional(),
103501
103553
  variant: exports_external.string().optional(),
103502
103554
  category: exports_external.string().optional(),
103503
103555
  skills: exports_external.array(exports_external.string()).optional(),
@@ -103588,7 +103640,6 @@ var BrowserAutomationConfigSchema = exports_external.object({
103588
103640
  var CategoryConfigSchema = exports_external.object({
103589
103641
  description: exports_external.string().optional(),
103590
103642
  model: exports_external.string().optional(),
103591
- tier: TierNameSchema.optional(),
103592
103643
  variant: exports_external.string().optional(),
103593
103644
  temperature: exports_external.number().min(0).max(2).optional(),
103594
103645
  top_p: exports_external.number().min(0).max(1).optional(),
@@ -103626,7 +103677,7 @@ var BuiltinCommandNameSchema = exports_external.enum([
103626
103677
  "refactor",
103627
103678
  "start-work",
103628
103679
  "stop-continuation",
103629
- "profile",
103680
+ "preset",
103630
103681
  "end-ultrawork",
103631
103682
  "handoff",
103632
103683
  "research",
@@ -103823,6 +103874,25 @@ var MatrixxSelfConfigSkillConfigSchema = exports_external.object({
103823
103874
  proactive: exports_external.boolean().default(false)
103824
103875
  });
103825
103876
 
103877
+ // src/config/schema/model-presets.ts
103878
+ var providerModelSchema = exports_external.string().refine((s) => {
103879
+ if (!s.includes("/"))
103880
+ return false;
103881
+ const parts = s.split("/");
103882
+ return parts.length === 2 && parts[0].length > 0 && parts[1].length > 0;
103883
+ }, { message: "Must be <provider>/<model>" });
103884
+ var ModelPresetEntrySchema = exports_external.object({
103885
+ model: providerModelSchema,
103886
+ variant: exports_external.string().optional()
103887
+ });
103888
+ var ModelPresetSchema = exports_external.object({
103889
+ default_model: providerModelSchema.optional(),
103890
+ agents: exports_external.record(exports_external.string(), ModelPresetEntrySchema).optional(),
103891
+ categories: exports_external.record(exports_external.string(), ModelPresetEntrySchema).optional()
103892
+ });
103893
+ var ModelPresetsSchema = exports_external.record(exports_external.string(), ModelPresetSchema);
103894
+ var ActivePresetSchema = exports_external.string().min(1);
103895
+
103826
103896
  // src/config/schema/morpheus.ts
103827
103897
  var MorpheusTasksConfigSchema = exports_external.object({
103828
103898
  storage_path: exports_external.string().optional(),
@@ -103949,6 +104019,22 @@ var SkillsConfigSchema = exports_external.union([
103949
104019
  }).catchall(SkillEntrySchema)
103950
104020
  ]);
103951
104021
 
104022
+ // src/config/schema/task.ts
104023
+ var TaskConfigSchema = exports_external.object({
104024
+ pollTimeoutMs: exports_external.number().min(60000).optional()
104025
+ });
104026
+
104027
+ // src/config/schema/tasks.ts
104028
+ var TasksConfigSchema = exports_external.object({
104029
+ enabled: exports_external.boolean().optional().default(true),
104030
+ scope: exports_external.enum(["global", "project"]).default("project").optional().describe("Task storage scope: project \u2192 .matrixx/tasks per project (default), global \u2192 <opencode-config>/tasks/{listId}"),
104031
+ storage_path: exports_external.string().optional(),
104032
+ task_list_id: exports_external.string().optional(),
104033
+ stale_after_hours: exports_external.number().int().min(1).optional().describe("Pending tasks with no file activity for this many hours are considered stale (default: 24). Stale tasks are excluded from task-continuation directives."),
104034
+ session_scoped: exports_external.boolean().default(true).optional().describe("When true, the task-continuation-enforcer only considers tasks created by the current session (or its subagent sessions). When false, all project tasks are considered regardless of session origin."),
104035
+ pollTimeoutMs: exports_external.number().min(60000).optional()
104036
+ });
104037
+
103952
104038
  // src/config/schema/tdd-enforcer.ts
103953
104039
  var TddEnforcerConfigSchema = exports_external.object({
103954
104040
  enabled: exports_external.boolean().default(false)
@@ -103980,7 +104066,8 @@ var WebsearchConfigSchema = exports_external.object({
103980
104066
  var MatrixxConfigSchema = exports_external.object({
103981
104067
  $schema: exports_external.string().optional(),
103982
104068
  global_model: exports_external.string().optional(),
103983
- default_tier: TierNameSchema.optional(),
104069
+ model_presets: ModelPresetsSchema.optional(),
104070
+ active_preset: ActivePresetSchema.optional(),
103984
104071
  new_task_system_enabled: exports_external.boolean().optional(),
103985
104072
  default_run_agent: exports_external.string().optional(),
103986
104073
  disabled_mcps: exports_external.array(AnyMcpNameSchema).optional(),
@@ -104002,6 +104089,8 @@ var MatrixxConfigSchema = exports_external.object({
104002
104089
  background_task: BackgroundTaskConfigSchema.optional(),
104003
104090
  notification: NotificationConfigSchema.optional(),
104004
104091
  babysitting: BabysittingConfigSchema.optional(),
104092
+ tasks: TasksConfigSchema.optional(),
104093
+ task: TaskConfigSchema.optional(),
104005
104094
  tdd_enforcer: TddEnforcerConfigSchema.optional(),
104006
104095
  browser_automation_engine: BrowserAutomationConfigSchema.optional(),
104007
104096
  websearch: WebsearchConfigSchema.optional(),
@@ -104017,7 +104106,6 @@ var MatrixxConfigSchema = exports_external.object({
104017
104106
  rtk: RtkConfigSchema.optional(),
104018
104107
  evolution: EvolutionConfigSchema.optional(),
104019
104108
  _migrations: exports_external.array(exports_external.string()).optional(),
104020
- tiers: TiersSchema.optional(),
104021
104109
  modelRequirements: ModelRequirementsSchema.optional(),
104022
104110
  complexityDowngrades: ComplexityDowngradesSchema.optional()
104023
104111
  });
@@ -104025,11 +104113,8 @@ var MatrixxConfigSchema = exports_external.object({
104025
104113
  init_logger();
104026
104114
  var MIGRATION_KEY = "model-migration";
104027
104115
  function normalizeModelInput(value) {
104028
- if (value.startsWith("tier:")) {
104029
- return value;
104030
- }
104031
104116
  if (value.includes("/")) {
104032
- log(`[migration] prefixed model '${value}' is deprecated, use 'tier:<name>' or bare '<model>'`);
104117
+ log(`[migration] prefixed model '${value}' is deprecated, use bare '<model>'`);
104033
104118
  }
104034
104119
  return value;
104035
104120
  }
@@ -104041,26 +104126,6 @@ function migrateMatrixxConfig(raw) {
104041
104126
  if (typeof migrated.global_model === "string") {
104042
104127
  migrated = { ...migrated, global_model: normalizeModelInput(migrated.global_model) };
104043
104128
  }
104044
- if (migrated.tiers) {
104045
- const nextTiers = {};
104046
- for (const [tierName, spec] of Object.entries(migrated.tiers)) {
104047
- if (!spec || typeof spec !== "object") {
104048
- nextTiers[tierName] = spec;
104049
- continue;
104050
- }
104051
- const typed = spec;
104052
- if (!typed.fallback) {
104053
- nextTiers[tierName] = spec;
104054
- continue;
104055
- }
104056
- const nextFallback = typed.fallback.map((entry) => ({
104057
- ...entry,
104058
- model: normalizeModelInput(entry.model)
104059
- }));
104060
- nextTiers[tierName] = { ...spec, fallback: nextFallback };
104061
- }
104062
- migrated = { ...migrated, tiers: nextTiers };
104063
- }
104064
104129
  if (migrated.modelRequirements) {
104065
104130
  const nextMR = { ...migrated.modelRequirements };
104066
104131
  for (const scope of ["agents", "categories"]) {
@@ -104205,7 +104270,7 @@ function mergeConfigs(base, override) {
104205
104270
  });
104206
104271
  return merged;
104207
104272
  }
104208
- function resolveConfigPath(baseDir, configName) {
104273
+ function resolveConfigPath2(baseDir, configName) {
104209
104274
  const newBase = path13.join(baseDir, "matrixx");
104210
104275
  const newDetected = detectConfigFile(newBase);
104211
104276
  if (newDetected.format !== "none") {
@@ -104219,29 +104284,39 @@ function resolveConfigPath(baseDir, configName) {
104219
104284
  }
104220
104285
  return `${newBase}.json`;
104221
104286
  }
104287
+ function assertModelsResolvable(config4) {
104288
+ const globalModel = config4.global_model;
104289
+ const unresolved = [];
104290
+ for (const [name, entry] of Object.entries(config4.agents ?? {})) {
104291
+ if (entry && !entry.disable && !entry.model && !globalModel) {
104292
+ unresolved.push(`agent "${name}"`);
104293
+ }
104294
+ }
104295
+ for (const [name, entry] of Object.entries(config4.categories ?? {})) {
104296
+ if (entry && !entry.disable && !entry.model && !globalModel) {
104297
+ unresolved.push(`category "${name}"`);
104298
+ }
104299
+ }
104300
+ if (unresolved.length > 0) {
104301
+ throw new ConfigError(`Matrixx config error: no model resolved for ${unresolved.join(", ")}. ` + `Set an explicit "model" on the entry, define model_presets with an active_preset, ` + `or run "bunx opencode-matrixx setup" to generate a preset from your connected providers.`);
104302
+ }
104303
+ }
104222
104304
  async function loadPluginConfig(directory, ctx) {
104223
104305
  const configDir = getOpenCodeConfigDir({ binary: "opencode" });
104224
- const userConfigPath = resolveConfigPath(configDir, "matrixx");
104306
+ const userConfigPath = resolveConfigPath2(configDir, "matrixx");
104225
104307
  const projectDir = path13.join(directory, ".opencode");
104226
- const projectConfigPath = resolveConfigPath(projectDir, "matrixx");
104308
+ const projectConfigPath = resolveConfigPath2(projectDir, "matrixx");
104227
104309
  let config4 = loadConfigFromPath(userConfigPath, ctx) ?? {};
104228
104310
  const projectConfig = loadConfigFromPath(projectConfigPath, ctx);
104229
104311
  if (projectConfig) {
104230
104312
  config4 = mergeConfigs(config4, projectConfig);
104231
104313
  }
104232
104314
  config4 = migrateMatrixxConfig(config4);
104233
- const connectedProviders = readConnectedProvidersCache();
104234
- const availableModels = await fetchAvailableModels(undefined, { connectedProviders });
104235
- const tierCtx = {
104236
- availableModels: availableModels ?? new Set,
104237
- connectedProviders
104238
- };
104239
- config4 = resolveTiersInConfig(config4, tierCtx);
104240
- const resolvedDefaults = resolveTiersInCategoryRegistry(DEFAULT_CATEGORIES, tierCtx, config4);
104241
- config4 = {
104242
- ...config4,
104243
- categories: { ...resolvedDefaults, ...config4.categories ?? {} }
104244
- };
104315
+ config4 = applyActivePreset(config4, config4.active_preset);
104316
+ if (config4.active_preset && config4.model_presets?.[config4.active_preset]) {
104317
+ log("Active model preset applied", { active_preset: config4.active_preset });
104318
+ }
104319
+ assertModelsResolvable(config4);
104245
104320
  log("Final merged config", {
104246
104321
  agents: config4.agents,
104247
104322
  disabled_agents: config4.disabled_agents,
@@ -104461,19 +104536,19 @@ function createEventHandler2(args) {
104461
104536
  };
104462
104537
  const recentSyntheticIdles = new Map;
104463
104538
  const recentRealIdles = new Map;
104464
- const DEDUP_WINDOW_MS = 500;
104539
+ const DEDUP_WINDOW_MS2 = 500;
104465
104540
  return async (input) => {
104466
104541
  pruneRecentSyntheticIdles({
104467
104542
  recentSyntheticIdles,
104468
104543
  recentRealIdles,
104469
104544
  now: Date.now(),
104470
- dedupWindowMs: DEDUP_WINDOW_MS
104545
+ dedupWindowMs: DEDUP_WINDOW_MS2
104471
104546
  });
104472
104547
  if (input.event.type === "session.idle") {
104473
104548
  const sessionID = input.event.properties?.sessionID;
104474
104549
  if (sessionID) {
104475
104550
  const emittedAt = recentSyntheticIdles.get(sessionID);
104476
- if (emittedAt && Date.now() - emittedAt < DEDUP_WINDOW_MS) {
104551
+ if (emittedAt && Date.now() - emittedAt < DEDUP_WINDOW_MS2) {
104477
104552
  recentSyntheticIdles.delete(sessionID);
104478
104553
  return;
104479
104554
  }
@@ -104485,7 +104560,7 @@ function createEventHandler2(args) {
104485
104560
  if (syntheticIdle) {
104486
104561
  const sessionID = syntheticIdle.event.properties?.sessionID;
104487
104562
  const emittedAt = recentRealIdles.get(sessionID);
104488
- if (emittedAt && Date.now() - emittedAt < DEDUP_WINDOW_MS) {
104563
+ if (emittedAt && Date.now() - emittedAt < DEDUP_WINDOW_MS2) {
104489
104564
  recentRealIdles.delete(sessionID);
104490
104565
  return;
104491
104566
  }
@@ -104661,22 +104736,22 @@ function createToolExecuteAfterHandler2(args) {
104661
104736
 
104662
104737
  // src/features/assembly-state/manager.ts
104663
104738
  init_logger();
104664
- var HOOK_NAME19 = "assembly-state";
104739
+ var HOOK_NAME20 = "assembly-state";
104665
104740
  var disabledSessions = new Set;
104666
104741
  function isAssemblyDisabled(sessionID) {
104667
104742
  return disabledSessions.has(sessionID);
104668
104743
  }
104669
104744
  function disableAssembly(sessionID) {
104670
104745
  disabledSessions.add(sessionID);
104671
- log(`[${HOOK_NAME19}] Assembly disabled for session`, { sessionID });
104746
+ log(`[${HOOK_NAME20}] Assembly disabled for session`, { sessionID });
104672
104747
  disabledSessions.add(sessionID);
104673
- log(`[${HOOK_NAME19}] Consensus disabled for session`, { sessionID });
104748
+ log(`[${HOOK_NAME20}] Consensus disabled for session`, { sessionID });
104674
104749
  }
104675
104750
  function enableAssembly(sessionID) {
104676
104751
  disabledSessions.delete(sessionID);
104677
- log(`[${HOOK_NAME19}] Assembly enabled for session`, { sessionID });
104752
+ log(`[${HOOK_NAME20}] Assembly enabled for session`, { sessionID });
104678
104753
  disabledSessions.delete(sessionID);
104679
- log(`[${HOOK_NAME19}] Consensus enabled for session`, { sessionID });
104754
+ log(`[${HOOK_NAME20}] Consensus enabled for session`, { sessionID });
104680
104755
  }
104681
104756
  // src/plugin/session-agent-resolver.ts
104682
104757
  async function resolveSessionAgent(client2, sessionId, timeoutMs = 5000) {