oh-my-opencode-slim 2.0.0-beta.1 → 2.0.0-beta.10

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 (67) hide show
  1. package/README.ja-JP.md +635 -0
  2. package/README.md +19 -9
  3. package/README.zh-CN.md +624 -0
  4. package/dist/cli/index.js +9 -3
  5. package/dist/config/constants.d.ts +1 -1
  6. package/dist/config/schema.d.ts +3 -3
  7. package/dist/hooks/deepwork/index.d.ts +13 -0
  8. package/dist/hooks/index.d.ts +1 -1
  9. package/dist/hooks/phase-reminder/index.d.ts +1 -1
  10. package/dist/index.js +766 -1271
  11. package/dist/multiplexer/session-manager.d.ts +4 -1
  12. package/dist/tools/ast-grep/tools.d.ts +1 -1
  13. package/dist/tools/cancel-task.d.ts +10 -0
  14. package/dist/tools/index.d.ts +1 -2
  15. package/dist/tui.js +10 -4
  16. package/dist/utils/background-job-board.d.ts +34 -1
  17. package/dist/utils/index.d.ts +0 -1
  18. package/oh-my-opencode-slim.schema.json +1 -1
  19. package/package.json +6 -4
  20. package/src/skills/codemap.md +3 -2
  21. package/src/skills/deepwork/SKILL.md +89 -0
  22. package/dist/agents/council-master.d.ts +0 -2
  23. package/dist/background/background-manager.d.ts +0 -203
  24. package/dist/background/index.d.ts +0 -3
  25. package/dist/background/multiplexer-session-manager.d.ts +0 -70
  26. package/dist/background/subagent-depth.d.ts +0 -35
  27. package/dist/cli/divoom.d.ts +0 -23
  28. package/dist/goal/index.d.ts +0 -3
  29. package/dist/goal/manager.d.ts +0 -41
  30. package/dist/goal/prompts.d.ts +0 -4
  31. package/dist/goal/store.d.ts +0 -15
  32. package/dist/goal/types.d.ts +0 -28
  33. package/dist/hooks/session-goal/index.d.ts +0 -38
  34. package/dist/integrations/divoom/index.d.ts +0 -3
  35. package/dist/integrations/divoom/status-manager.d.ts +0 -31
  36. package/dist/integrations/divoom/swift-helper-source.d.ts +0 -1
  37. package/dist/integrations/divoom/swift-transport.d.ts +0 -26
  38. package/dist/integrations/divoom/types.d.ts +0 -41
  39. package/dist/tools/background.d.ts +0 -13
  40. package/dist/tools/fork/command.d.ts +0 -28
  41. package/dist/tools/fork/files.d.ts +0 -33
  42. package/dist/tools/fork/index.d.ts +0 -10
  43. package/dist/tools/fork/state.d.ts +0 -7
  44. package/dist/tools/fork/tools.d.ts +0 -23
  45. package/dist/tools/fork/vendor.d.ts +0 -28
  46. package/dist/tools/handoff/command.d.ts +0 -29
  47. package/dist/tools/handoff/files.d.ts +0 -33
  48. package/dist/tools/handoff/index.d.ts +0 -10
  49. package/dist/tools/handoff/state.d.ts +0 -7
  50. package/dist/tools/handoff/tools.d.ts +0 -23
  51. package/dist/tools/handoff/vendor.d.ts +0 -28
  52. package/dist/tools/lsp/client.d.ts +0 -81
  53. package/dist/tools/lsp/config-store.d.ts +0 -29
  54. package/dist/tools/lsp/config.d.ts +0 -5
  55. package/dist/tools/lsp/constants.d.ts +0 -24
  56. package/dist/tools/lsp/index.d.ts +0 -4
  57. package/dist/tools/lsp/tools.d.ts +0 -5
  58. package/dist/tools/lsp/types.d.ts +0 -45
  59. package/dist/tools/lsp/utils.d.ts +0 -34
  60. package/dist/tools/subtask/command.d.ts +0 -30
  61. package/dist/tools/subtask/files.d.ts +0 -34
  62. package/dist/tools/subtask/index.d.ts +0 -11
  63. package/dist/tools/subtask/state.d.ts +0 -7
  64. package/dist/tools/subtask/tools.d.ts +0 -23
  65. package/dist/tools/subtask/vendor.d.ts +0 -27
  66. package/dist/utils/session-manager.d.ts +0 -55
  67. package/dist/utils/tmux-debug-log.d.ts +0 -2
package/dist/index.js CHANGED
@@ -18226,6 +18226,12 @@ var CUSTOM_SKILLS = [
18226
18226
  description: "Clone important dependency source for local inspection",
18227
18227
  allowedAgents: ["orchestrator"],
18228
18228
  sourcePath: "src/skills/clonedeps"
18229
+ },
18230
+ {
18231
+ name: "deepwork",
18232
+ description: "Heavy/complex coding sessions and large modifications workflow",
18233
+ allowedAgents: ["orchestrator"],
18234
+ sourcePath: "src/skills/deepwork"
18229
18235
  }
18230
18236
  ];
18231
18237
 
@@ -18302,7 +18308,7 @@ var POLL_INTERVAL_BACKGROUND_MS = 2000;
18302
18308
  var DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
18303
18309
  var MAX_POLL_TIME_MS = 5 * 60 * 1000;
18304
18310
  var DEFAULT_MAX_SUBAGENT_DEPTH = 3;
18305
- var PHASE_REMINDER_TEXT = `!IMPORTANT! Scheduler workflow: plan lanes/dependencies → dispatch background specialists → track task IDs → poll task_status → reconcile terminal results → verify. Do not consume running-job output or advance dependent work. !END!`;
18311
+ var PHASE_REMINDER_TEXT = `!IMPORTANT! Scheduler workflow: plan lanes/dependencies → dispatch background specialists → track task IDs → wait for hook-driven completion or use task_status only when needed → reconcile terminal results → verify. Do not consume running-job output or advance dependent work. !END!`;
18306
18312
  var TMUX_SPAWN_DELAY_MS = 500;
18307
18313
  var COUNCILLOR_STAGGER_MS = 250;
18308
18314
  var DEFAULT_DISABLED_AGENTS = ["observer"];
@@ -18560,7 +18566,7 @@ var InterviewConfigSchema = z2.object({
18560
18566
  port: z2.number().int().min(0).max(65535).default(0),
18561
18567
  dashboard: z2.boolean().default(false)
18562
18568
  });
18563
- var SessionManagerConfigSchema = z2.object({
18569
+ var BackgroundJobsConfigSchema = z2.object({
18564
18570
  maxSessionsPerAgent: z2.number().int().min(1).max(10).default(2),
18565
18571
  readContextMinLines: z2.number().int().min(0).max(1000).default(10),
18566
18572
  readContextMaxFiles: z2.number().int().min(0).max(50).default(8)
@@ -18626,7 +18632,7 @@ var PluginConfigSchema = z2.object({
18626
18632
  tmux: TmuxConfigSchema.optional(),
18627
18633
  websearch: WebsearchConfigSchema.optional(),
18628
18634
  interview: InterviewConfigSchema.optional(),
18629
- sessionManager: SessionManagerConfigSchema.optional(),
18635
+ backgroundJobs: BackgroundJobsConfigSchema.optional(),
18630
18636
  divoom: DivoomConfigSchema.optional(),
18631
18637
  todoContinuation: TodoContinuationConfigSchema.optional(),
18632
18638
  fallback: FailoverConfigSchema.optional(),
@@ -18727,7 +18733,7 @@ function mergePluginConfigs(base, override) {
18727
18733
  tmux: deepMerge(base.tmux, override.tmux),
18728
18734
  multiplexer: deepMerge(base.multiplexer, override.multiplexer),
18729
18735
  interview: deepMerge(base.interview, override.interview),
18730
- sessionManager: deepMerge(base.sessionManager, override.sessionManager),
18736
+ backgroundJobs: deepMerge(base.backgroundJobs, override.backgroundJobs),
18731
18737
  divoom: deepMerge(base.divoom, override.divoom),
18732
18738
  fallback: deepMerge(base.fallback, override.fallback),
18733
18739
  council: deepMerge(base.council, override.council)
@@ -18958,49 +18964,48 @@ ${customAppendPrompt}`;
18958
18964
  }
18959
18965
  var AGENT_DESCRIPTIONS = {
18960
18966
  explorer: `@explorer
18961
- - Lane: Codebase discovery and reconnaissance
18962
- - Role: Parallel search specialist for discovering unknowns across the codebase
18963
- - Permissions: Read files
18967
+ - Lane: Fast codebase recon that returns compressed context
18968
+ - Permissions: read_files
18964
18969
  - Stats: 2x faster codebase search than orchestrator, 1/2 cost of orchestrator
18965
18970
  - Capabilities: Glob, grep, AST queries to locate files, symbols, patterns
18966
18971
  - **Delegate when:** Need to discover what exists before planning • Parallel searches speed discovery • Need summarized map vs full contents • Broad/uncertain scope
18967
18972
  - **Don't delegate when:** Know the path and need actual content • Need full file anyway • Single specific lookup • About to edit the file`,
18968
18973
  librarian: `@librarian
18969
- - Lane: External knowledge and library research
18970
- - Role: Authoritative source for current library docs and API references
18971
- - Permissions: External docs/search MCPs; no file edits
18972
- - Stats: 10x better finding up-to-date library docs than orchestrator, 1/2 cost of orchestrator
18973
- - Capabilities: Fetches latest official docs, examples, API signatures, version-specific behavior via grep_app MCP
18974
- - **Delegate when:** Libraries with frequent API changes (React, Next.js, AI SDKs) • Complex APIs needing official examples (ORMs, auth) • Version-specific behavior matters • Unfamiliar library • Edge cases or advanced features • Nuanced best practices
18974
+ - Lane: External knowledge and library research, fast web research
18975
+ - Role: Authoritative source for current library docs, API references, examples, bug investigations, and web retrieval
18976
+ - Stats: 2x faster web research than orchestrator, 1/2 cost of orchestrator
18977
+ - **Delegate when:** Libraries with frequent API changes (React, Next.js, AI SDKs) • Complex APIs needing official examples (ORMs, auth) • Version-specific behavior matters • Unfamiliar library Edge cases or advanced features • Nuanced best practices • Working on fixing tricky bug or problem and need latest web research information
18975
18978
  - **Don't delegate when:** Standard usage you're confident • Simple stable APIs • General programming knowledge • Info already in conversation • Built-in language features
18976
- - **Rule of thumb:** "How does this library work?" → @librarian. "How does programming work?" → answer directly.`,
18979
+ - **Rule of thumb:** "How does this library work?" → @librarian. "How does programming work?" → answer directly. How does others solve or workaround this tricky issue?" → @librarian.`,
18977
18980
  oracle: `@oracle
18978
18981
  - Lane: Architecture, risk, debugging strategy, and review
18979
18982
  - Role: Strategic advisor for high-stakes decisions and persistent problems, code reviewer
18980
- - Permissions: Read files
18983
+ - Permissions: read_files
18981
18984
  - Stats: 5x better decision maker, problem solver, investigator than orchestrator, 0.8x speed of orchestrator, same cost.
18982
18985
  - Capabilities: Deep architectural reasoning, system-level trade-offs, complex debugging, code review, simplification, maintainability review
18983
18986
  - **Delegate when:** Major architectural decisions with long-term impact • Problems persisting after 2+ fix attempts • High-risk multi-system refactors • Costly trade-offs (performance vs maintainability) • Complex debugging with unclear root cause • Security/scalability/data integrity decisions • Genuinely uncertain and cost of wrong choice is high • When a workflow calls for a **reviewer** subagent • Code needs simplification or YAGNI scrutiny
18984
18987
  - **Don't delegate when:** Routine decisions you're confident about • First bug fix attempt • Straightforward trade-offs • Tactical "how" vs strategic "should" • Time-sensitive good-enough decisions • Quick research/testing can answer
18985
18988
  - **Rule of thumb:** Need senior architect review? → @oracle. Need code review or simplification? → @oracle. Routine coordination or final synthesis? → handle directly.`,
18986
18989
  designer: `@designer
18987
- - Lane: User-facing UI/UX design, polish, and review
18988
- - Role: UI/UX specialist for intentional, polished experiences
18989
- - Permissions: Read/write files
18990
+ - Lane: UI/UX design, related edits, design polish and review
18991
+ - Permissions: read_files, write_files
18990
18992
  - Stats: 10x better UI/UX than orchestrator
18991
- - Capabilities: Visual relevant edits, interactions, responsive layouts, design systems with aesthetic intent, deep UI/UX knowledge.
18993
+ - Capabilities: Goot design taste, visual relevant edits, interactions, responsive layouts, design systems with aesthetic intent, deep UI/UX knowledge.
18994
+ - Weakness: copywriting, when calling ask to use grounded, normal wording
18995
+ - Avoid: "Let me us designer how it should look and implement yourself" → instead: "Let me ask designer to design and implement the UI/UX changes for me"
18992
18996
  - **Delegate when:** User-facing interfaces needing polish • Responsive layouts • UX-critical components (forms, nav, dashboards) • Visual consistency systems • Animations/micro-interactions • Landing/marketing pages • Refining functional→delightful • Reviewing existing UI/UX quality
18993
- - **Don't delegate when:** Backend/logic with no visual • Quick prototypes where design doesn't matter yet
18997
+ - **Don't delegate when:** Backend/logic with no visual • Quick prototypes where design doesn't matter yet.
18994
18998
  - **Rule of thumb:** Users see it and polish matters? → @designer. Headless/functional implementation? → schedule @fixer.`,
18995
18999
  fixer: `@fixer
18996
- - Lane: Bounded implementation and test execution
18997
- - Role: Fast execution specialist for well-defined tasks, which empowers orchestrator with parallel, speedy executions
18998
- - Permissions: Read/write files
18999
- - Stats: 2x faster code edits, 1/2 cost of orchestrator, 0.8x quality of orchestrator
19000
+ - Lane: Bounded implementation and executioner
19001
+ - Role: Fast execution specialist for well-defined tasks
19002
+ - Permissions: read_files, write_files
19003
+ - Stats: 2x faster code edits, 1/2 cost of orchestrator
19004
+ - Weakness: design, taste
19000
19005
  - Tools/Constraints: Execution-focused—no research, no architectural decisions
19001
- - **Delegate when:** For implementation work, think and triage first. If the change is non-trivial or multi-file, hand bounded execution to @fixer • Writing or updating tests • Tasks that touch test files, fixtures, mocks, or test helpers. Parallelization benefits: Task involves multiple folders and multiple files modificaiton, scoping work per folder and spawning parallel @fixers for each folder.
19002
- - **Don't delegate when:** Needs discovery/research/decisions • Single small change (<20 lines, one file) • Unclear requirements needing iteration • Explaining to fixer > doing • Tight integration with your current work • Sequential dependencies
19003
- - **Rule of thumb:** If implementation or tests are needed, schedule @fixer with clear scope. Bigger or lots of edits should be split by ownership and dispatched as parallel background fixer lanes when safe.`,
19006
+ - **Delegate when:** For implementation work, think and triage first. If the change is non-trivial or multi-file, hand bounded execution to @fixer • Parallelization benefits: Task involves multiple folders and multiple files modification, scoping work per folder and spawning parallel @fixers for each folder.
19007
+ - **Don't delegate when:** Needs discovery/research/decisions • Single small change (<20 lines, one file) • Unclear requirements needing iteration • Explaining to fixer > doing • Tight integration with your current work
19008
+ - **Rule of thumb:** Implementation are needed, schedule @fixer with clear scope. Bigger or lots of edits should be split by ownership and dispatched as parallel background @fixer lanes when safe. Editing files which includes design, ui, ux changes → schedule @designer.`,
19004
19009
  council: `@council
19005
19010
  - Lane: High-stakes multi-model decision support
19006
19011
  - Role: Multi-LLM consensus engine that runs several councillors, synthesizes their views, and returns a structured council report.
@@ -19025,8 +19030,8 @@ var AGENT_DESCRIPTIONS = {
19025
19030
  };
19026
19031
  var VALIDATION_ROUTING = [
19027
19032
  "- Route UI/UX validation and review to @designer",
19028
- "- Route code review, simplification, maintainability review, and YAGNI checks to @oracle",
19029
- "- Route test writing, test updates, and changes touching test files to @fixer",
19033
+ "- Route code review, code simplification and maintainability review checks to @oracle",
19034
+ "- Route implementation to @fixer or multiple @fixer instances for maximum parallel execution",
19030
19035
  "- Route visual/media analysis and interpretation to @observer",
19031
19036
  "- If a request spans multiple lanes, delegate only the lanes that add clear value"
19032
19037
  ];
@@ -19058,6 +19063,7 @@ function buildOrchestratorPrompt(disabledAgents) {
19058
19063
  You are a workflow manager for coding work. Your job is to plan, schedule, delegate, monitor, reconcile, and verify specialist-agent work. You are not the default implementation worker.
19059
19064
 
19060
19065
  Optimize for quality, speed, cost, and reliability by dispatching the right specialist lanes, tracking background task state, and integrating terminal results into one coherent outcome.
19066
+ You have perfect understanding of agent's context management, understand well the cost of building content and reusing context of existing agents when it's best or when it's best to spawn a new agent.
19061
19067
  </Role>
19062
19068
 
19063
19069
  <Agents>
@@ -19072,22 +19078,26 @@ ${enabledAgents}
19072
19078
  Parse request: explicit requirements + implicit needs.
19073
19079
 
19074
19080
  ## 2. Path Selection
19075
- Evaluate approach by: quality, speed, cost, reliability.
19081
+ Evaluate approach by: quality, speed and cost.
19076
19082
  Choose the path that optimizes all four.
19077
19083
 
19078
- Classify work into lanes: discovery, external knowledge, implementation, UI/UX, review/risk, visual analysis, and final verification.
19079
-
19080
19084
  ## 3. Delegation Check
19081
- **STOP. Review specialists before acting.**
19082
-
19083
- !!! Review available agents and lane rules. Decide what to schedule, what depends on what, and what minimal direct coordination is needed. !!!
19085
+ Review available agents and lane rules.
19084
19086
 
19085
19087
  **Dispatch efficiency:**
19086
19088
  - Reference paths/lines, don't paste files (\`src/app.ts:42\` not full contents)
19087
- - Provide context summaries, let specialists read what they need
19088
19089
  - Brief user on delegation goal before each call
19089
- - Keep direct work limited to clarification, minimal routing context, todos, synthesis, and final checks
19090
19090
  - For trivial conversational answers or tiny mechanical edits, direct execution is allowed when scheduling overhead would clearly dominate
19091
+ - Record task IDs, state, and advisory ownership/dependency labels
19092
+ - Do not immediately wait after spawning independent background tasks unless the next step truly depends on their result
19093
+ - Reconcile results, resolve conflicts, and gate dependent lanes
19094
+
19095
+ **File operations rules:**
19096
+ - Always use dedicated file tools for file I/O.
19097
+ - Search files/code with \`glob\`, \`grep\`, or \`ast_grep_search\`.
19098
+ - Read files with \`read\`. Never use \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or bash commands to read file contents.
19099
+ - Edit files with \`apply_patch\`. Never use shell redirection, \`echo\`, \`printf\`, or heredocs for file content unless no file tool can do the job.
19100
+ - Use \`bash\` only for execution: git, package managers, tests, builds, scripts, or diagnostics.
19091
19101
 
19092
19102
  ## 4. Plan and Parallelize
19093
19103
  Build a short work graph before dispatching:
@@ -19101,46 +19111,28 @@ ${enabledParallelExamples}
19101
19111
 
19102
19112
  Balance: respect dependencies, avoid parallelizing what must be sequential, and avoid overlapping write ownership.
19103
19113
 
19104
- ### Context Isolation
19105
- If no specialist delegation is needed, consider \`subtask\` before doing
19106
- context-heavy work directly.
19107
-
19108
- Ask whether the parent context needs the details or only the result. Use
19109
- \`subtask\` when the work is bounded, context-heavy, and the parent only needs a
19110
- compact outcome.
19111
-
19112
- Use \`subtask\` for focused investigation, bounded analysis, cleanup, or
19113
- verification across files/logs/messages.
19114
-
19115
- Prefer native background \`task(..., background: true)\` plus \`task_status\` for independent specialist lanes. Use \`subtask\` only for bounded parent-local context isolation when native background specialist scheduling is not the right fit.
19116
-
19117
- Do not use \`subtask\` for tiny tasks, open-ended work, interactive decisions,
19118
- work better handled by a named specialist, or cases where the parent must reason
19119
- over the details.
19120
-
19121
- When calling \`subtask\`, give a self-contained prompt with objective,
19122
- constraints, relevant context, deliverable, and validation. Pass only clearly
19123
- relevant files. Wait for the summary, then integrate and verify it.
19124
-
19125
19114
  ### OpenCode scheduler model
19126
19115
  - Delegated specialists should be launched as background tasks whenever work can run independently: use \`task(..., background: true)\`.
19127
19116
  - A dispatch returns a task/session ID immediately; it does not mean completion.
19128
19117
  - Track each task ID with specialist, objective, state, and any advisory ownership/dependency labels from the dispatch plan.
19129
- - Continue orchestration while tasks run: planning, scheduling independent lanes, preparing synthesis, and asking needed user questions.
19130
- - Poll or wait with \`task_status(wait: true, timeout_ms: ...)\` before consuming outputs or starting dependent work.
19118
+ - Background completion is event/hook-driven: when a background task finishes, OpenCode injects a follow-up message with the terminal result.
19119
+ - Continue orchestration while tasks run only when useful: planning, scheduling independent lanes, preparing synthesis, or asking needed user questions.
19120
+ - If no useful independent work remains, stop after a brief status response; do not call \`task_status\` just to wait. OpenCode will resume you when the background completion event arrives.
19121
+ - Use \`task_status(wait: true, timeout_ms: ...)\` only when you actively need a result before a dependent step or final response and no completion event has arrived yet.
19131
19122
  - Parallel background tasks are allowed only when their write scopes do not conflict.
19132
19123
  - Final response requires relevant tasks to be terminal and reconciled.
19133
19124
 
19134
- ## 5. Dispatch
19135
- 1. Split work into independent and dependency-ordered lanes
19136
- 2. Plan advisory ownership for write-capable lanes
19137
- 3. Dispatch independent specialists as background tasks
19138
- 4. Record task IDs, state, and advisory ownership/dependency labels
19139
- 5. Continue only independent orchestration while jobs run
19140
- 6. Poll/wait for terminal results with \`task_status(wait: true, timeout_ms: ...)\`
19141
- 7. Reconcile results, resolve conflicts, and gate dependent lanes
19142
- 8. Dispatch follow-up jobs if needed
19143
- 9. Verify final state
19125
+ ### Background Job Discipline
19126
+ - Every background task owns its declared lane until terminal.
19127
+ - Do not duplicate, undermine, or race a running lane.
19128
+ - After dispatch, classify the next step:
19129
+ 1. independent: continue,
19130
+ 2. dependent: wait/poll,
19131
+ 3. no useful independent work: stop and let hook-driven completion resume.
19132
+ - Before editing files or spawning another writer, compare against running job scopes.
19133
+ - Use \`cancel_task\` only when the user asks, or when a running lane is obsolete, wrong, or conflicts with a safer replacement plan.
19134
+ - Cancellation is not rollback: if cancelling a writer, inspect and reconcile partial file changes before launching a replacement lane.
19135
+ - Never finalize work that depends on unresolved background jobs.
19144
19136
 
19145
19137
  ### Session Reuse
19146
19138
  - Smartly reuse an available specialist session - context reuse saves time and tokens
@@ -19255,6 +19247,12 @@ var COUNCIL_AGENT_PROMPT = `You are the Council agent — a multi-LLM orchestrat
19255
19247
  - Be transparent about trade-offs when different approaches have valid pros/cons
19256
19248
  - Don't just average responses — choose the best approach and improve upon it
19257
19249
 
19250
+ **File Operations Rules**:
19251
+ - Use dedicated tools for file I/O if local files must be inspected
19252
+ - Search files/code with glob, grep, or ast_grep_search
19253
+ - Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
19254
+ - Use bash only for execution/diagnostics, never for file I/O
19255
+
19258
19256
  **Required Output Format**:
19259
19257
  Always include these sections in your final response:
19260
19258
 
@@ -19366,6 +19364,12 @@ var COUNCILLOR_PROMPT = `You are a councillor in a multi-model council.
19366
19364
 
19367
19365
  You CANNOT edit files, write files, run shell commands, or delegate to other agents. You are an advisor, not an implementer.
19368
19366
 
19367
+ **File Operations Rules**:
19368
+ - READ-ONLY: do not modify files
19369
+ - Search files/code with glob, grep, or ast_grep_search
19370
+ - Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
19371
+ - Do not use bash or shell commands
19372
+
19369
19373
  **Behavior**:
19370
19374
  - **Examine the codebase** before answering — your read access is what makes council valuable. Don't guess at code you can see.
19371
19375
  - Analyze the problem thoroughly
@@ -19453,6 +19457,13 @@ var DESIGNER_PROMPT = `You are a Designer - a frontend UI/UX specialist who crea
19453
19457
  - Leverage component libraries where available
19454
19458
  - Prioritize visual excellence—code perfection comes second
19455
19459
 
19460
+ ## File Operations Rules
19461
+ - Always use dedicated file tools for file I/O
19462
+ - Search files/code with glob, grep, or ast_grep_search
19463
+ - Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
19464
+ - Edit/write files with write, edit, or apply_patch. Never use shell redirection, echo, printf, or heredocs for file content unless no file tool can do the job
19465
+ - Use bash only for execution: git, package managers, tests, builds, scripts, or diagnostics
19466
+
19456
19467
  ## Review Responsibilities
19457
19468
  - Review existing UI for usability, responsiveness, visual consistency, and polish when asked
19458
19469
  - Call out concrete UX issues and improvements, not just abstract design advice
@@ -19490,6 +19501,12 @@ var EXPLORER_PROMPT = `You are Explorer - a fast codebase navigation specialist.
19490
19501
  - **Structural patterns** (function shapes, class structures): ast_grep_search
19491
19502
  - **File discovery** (find by name/extension): glob
19492
19503
 
19504
+ **File Operations Rules**:
19505
+ - READ-ONLY: Search and report, don't modify files
19506
+ - Search files/code with glob, grep, or ast_grep_search
19507
+ - Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
19508
+ - Use bash only for execution/diagnostics, never for file I/O
19509
+
19493
19510
  **Behavior**:
19494
19511
  - Be fast and thorough
19495
19512
  - Fire multiple searches in parallel if needed
@@ -19544,6 +19561,13 @@ var FIXER_PROMPT = `You are Fixer - a fast, focused implementation specialist.
19544
19561
  - Run relevant validation when requested or clearly applicable (otherwise note as skipped with reason)
19545
19562
  - Report completion with summary of changes
19546
19563
 
19564
+ **File Operations Rules**:
19565
+ - Always use dedicated file tools for file I/O
19566
+ - Search files/code with glob, grep, or ast_grep_search
19567
+ - Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
19568
+ - Edit/write files with write, edit, or apply_patch. Never use shell redirection, echo, printf, or heredocs for file content unless no file tool can do the job
19569
+ - Use bash only for execution: git, package managers, tests, builds, scripts, or diagnostics
19570
+
19547
19571
  **Constraints**:
19548
19572
  - NO external research (no websearch, context7, grep_app)
19549
19573
  - NO delegation or spawning subagents
@@ -19609,6 +19633,12 @@ var LIBRARIAN_PROMPT = `You are Librarian - a research specialist for codebases
19609
19633
  - grep_app: Search GitHub repositories
19610
19634
  - websearch: General web search for docs
19611
19635
 
19636
+ **File Operations Rules**:
19637
+ - Use dedicated tools for file I/O when local files must be inspected
19638
+ - Search files/code with glob, grep, or ast_grep_search
19639
+ - Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
19640
+ - Use bash only for execution/diagnostics, never for file I/O
19641
+
19612
19642
  **Behavior**:
19613
19643
  - Provide evidence-based answers with sources
19614
19644
  - Quote relevant code snippets
@@ -19653,6 +19683,11 @@ var OBSERVER_PROMPT = `You are Observer — a visual analysis specialist.
19653
19683
  - Save context tokens — the Orchestrator never processes the raw file
19654
19684
  - Match the language of the request
19655
19685
  - If info not found, state clearly what's missing
19686
+
19687
+ **File Operations Rules**:
19688
+ - READ-ONLY: do not modify files
19689
+ - Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
19690
+ - Use bash only for execution/diagnostics, never for file I/O
19656
19691
  `;
19657
19692
  function createObserverAgent(model, customPrompt, customAppendPrompt) {
19658
19693
  let prompt = OBSERVER_PROMPT;
@@ -19697,6 +19732,12 @@ var ORACLE_PROMPT = `You are Oracle - a strategic technical advisor and code rev
19697
19732
  - READ-ONLY: You advise, you don't implement
19698
19733
  - Focus on strategy, not execution
19699
19734
  - Point to specific files/lines when relevant
19735
+
19736
+ **File Operations Rules**:
19737
+ - READ-ONLY: do not modify files
19738
+ - Search files/code with glob, grep, or ast_grep_search
19739
+ - Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
19740
+ - Use bash only for execution/diagnostics, never for file I/O
19700
19741
  `;
19701
19742
  function createOracleAgent(model, customPrompt, customAppendPrompt) {
19702
19743
  let prompt = ORACLE_PROMPT;
@@ -19720,6 +19761,7 @@ ${customAppendPrompt}`;
19720
19761
 
19721
19762
  // src/agents/index.ts
19722
19763
  var COUNCIL_TOOL_ALLOWED_AGENTS = new Set(["council"]);
19764
+ var CANCEL_TASK_ALLOWED_AGENTS = new Set(["orchestrator"]);
19723
19765
  var SAFE_AGENT_ALIAS_RE = /^[a-z][a-z0-9_-]*$/i;
19724
19766
  function normalizeDisplayName(displayName) {
19725
19767
  const trimmed = displayName.trim();
@@ -19796,10 +19838,12 @@ function applyDefaultPermissions(agent, configuredSkills) {
19796
19838
  const skillPermissions = getSkillPermissionsForAgent(agent.name, configuredSkills);
19797
19839
  const questionPerm = existing.question === "deny" ? "deny" : "allow";
19798
19840
  const councilSessionPerm = COUNCIL_TOOL_ALLOWED_AGENTS.has(agent.name) ? existing.council_session ?? "allow" : "deny";
19841
+ const cancelTaskPerm = CANCEL_TASK_ALLOWED_AGENTS.has(agent.name) ? existing.cancel_task ?? "allow" : "deny";
19799
19842
  agent.config.permission = {
19800
19843
  ...existing,
19801
19844
  question: questionPerm,
19802
19845
  council_session: councilSessionPerm,
19846
+ cancel_task: cancelTaskPerm,
19803
19847
  skill: {
19804
19848
  ...typeof existing.skill === "object" ? existing.skill : {},
19805
19849
  ...skillPermissions
@@ -22683,6 +22727,14 @@ var AGENT_PREFIX = {
22683
22727
  class BackgroundJobBoard {
22684
22728
  jobs = new Map;
22685
22729
  counters = new Map;
22730
+ maxReusablePerAgent;
22731
+ readContextMinLines;
22732
+ readContextMaxFiles;
22733
+ constructor(options = {}) {
22734
+ this.maxReusablePerAgent = options.maxReusablePerAgent ?? 2;
22735
+ this.readContextMinLines = options.readContextMinLines ?? 10;
22736
+ this.readContextMaxFiles = options.readContextMaxFiles ?? 8;
22737
+ }
22686
22738
  registerLaunch(input) {
22687
22739
  const now = input.now ?? Date.now();
22688
22740
  const existing = this.jobs.get(input.taskID);
@@ -22697,6 +22749,9 @@ class BackgroundJobBoard {
22697
22749
  terminalUnreconciled: false,
22698
22750
  completedAt: undefined,
22699
22751
  resultSummary: undefined,
22752
+ terminalState: undefined,
22753
+ lastLaunchedAt: now,
22754
+ lastUsedAt: now,
22700
22755
  updatedAt: now
22701
22756
  };
22702
22757
  this.jobs.set(input.taskID, updated);
@@ -22712,8 +22767,11 @@ class BackgroundJobBoard {
22712
22767
  timedOut: false,
22713
22768
  terminalUnreconciled: false,
22714
22769
  launchedAt: now,
22770
+ lastLaunchedAt: now,
22771
+ lastUsedAt: now,
22715
22772
  updatedAt: now,
22716
- alias: this.nextAlias(input.parentSessionID, input.agent)
22773
+ alias: this.nextAlias(input.parentSessionID, input.agent),
22774
+ contextFiles: []
22717
22775
  };
22718
22776
  this.jobs.set(input.taskID, record);
22719
22777
  return record;
@@ -22722,7 +22780,7 @@ class BackgroundJobBoard {
22722
22780
  const existing = this.jobs.get(input.taskID);
22723
22781
  if (!existing)
22724
22782
  return;
22725
- if (existing.state === "reconciled") {
22783
+ if (existing.state === "reconciled" || existing.state === "cancelled" && input.state !== "cancelled" || TERMINAL_STATES.has(existing.state) && input.state === "running") {
22726
22784
  return existing;
22727
22785
  }
22728
22786
  const now = input.now ?? Date.now();
@@ -22734,9 +22792,11 @@ class BackgroundJobBoard {
22734
22792
  terminalUnreconciled: terminal ? true : existing.terminalUnreconciled,
22735
22793
  updatedAt: now,
22736
22794
  completedAt: terminal ? existing.completedAt ?? now : existing.completedAt,
22795
+ terminalState: terminal ? input.state : existing.terminalState,
22737
22796
  resultSummary: input.resultSummary ?? existing.resultSummary
22738
22797
  };
22739
22798
  this.jobs.set(input.taskID, updated);
22799
+ this.trimReusable(input.taskID);
22740
22800
  return updated;
22741
22801
  }
22742
22802
  updateFromStatusOutput(output) {
@@ -22761,7 +22821,32 @@ class BackgroundJobBoard {
22761
22821
  ...existing,
22762
22822
  state: "reconciled",
22763
22823
  terminalUnreconciled: false,
22764
- updatedAt: now
22824
+ updatedAt: now,
22825
+ lastUsedAt: now,
22826
+ terminalState: existing.terminalState ?? terminalStateOf(existing.state)
22827
+ };
22828
+ this.jobs.set(taskID, updated);
22829
+ this.trimReusable(taskID);
22830
+ return updated;
22831
+ }
22832
+ markCancelled(taskID, reason, now = Date.now()) {
22833
+ const existing = this.jobs.get(taskID);
22834
+ if (!existing)
22835
+ return;
22836
+ if (existing.state === "reconciled")
22837
+ return existing;
22838
+ if (TERMINAL_STATES.has(existing.state))
22839
+ return existing;
22840
+ const summary = normalizeCancelReason(reason);
22841
+ const updated = {
22842
+ ...existing,
22843
+ state: "cancelled",
22844
+ timedOut: false,
22845
+ terminalUnreconciled: true,
22846
+ updatedAt: now,
22847
+ completedAt: existing.completedAt ?? now,
22848
+ terminalState: "cancelled",
22849
+ resultSummary: summary
22765
22850
  };
22766
22851
  this.jobs.set(taskID, updated);
22767
22852
  return updated;
@@ -22769,6 +22854,49 @@ class BackgroundJobBoard {
22769
22854
  get(taskID) {
22770
22855
  return this.jobs.get(taskID);
22771
22856
  }
22857
+ resolve(parentSessionID, taskIDOrAlias) {
22858
+ const value = taskIDOrAlias.trim();
22859
+ return this.list(parentSessionID).find((job) => job.taskID === value || job.alias === value);
22860
+ }
22861
+ resolveForStatus(parentSessionID, taskIDOrAlias) {
22862
+ return this.resolve(parentSessionID, taskIDOrAlias);
22863
+ }
22864
+ resolveReusable(parentSessionID, taskIDOrAlias, agent) {
22865
+ const job = this.resolve(parentSessionID, taskIDOrAlias);
22866
+ if (!job || !isReusable(job))
22867
+ return;
22868
+ if (agent && job.agent !== agent)
22869
+ return;
22870
+ return job;
22871
+ }
22872
+ markUsed(parentSessionID, key, now = Date.now()) {
22873
+ const job = this.resolve(parentSessionID, key);
22874
+ if (!job)
22875
+ return;
22876
+ this.jobs.set(job.taskID, { ...job, lastUsedAt: now, updatedAt: now });
22877
+ }
22878
+ taskIDs() {
22879
+ return new Set(this.jobs.keys());
22880
+ }
22881
+ addContext(taskID, files) {
22882
+ if (files.length === 0)
22883
+ return;
22884
+ const job = this.jobs.get(taskID);
22885
+ if (!job)
22886
+ return;
22887
+ const existing = new Map(job.contextFiles.map((file) => [file.path, file]));
22888
+ for (const file of files) {
22889
+ const previous = existing.get(file.path);
22890
+ if (previous) {
22891
+ previous.lineCount = Math.max(previous.lineCount, file.lineCount);
22892
+ previous.lastReadAt = Math.max(previous.lastReadAt, file.lastReadAt);
22893
+ } else {
22894
+ existing.set(file.path, { ...file });
22895
+ }
22896
+ }
22897
+ const contextFiles = [...existing.values()].filter((file) => file.lineCount >= this.readContextMinLines).sort((a, b) => b.lastReadAt - a.lastReadAt).slice(0, this.readContextMaxFiles + 1);
22898
+ this.jobs.set(taskID, { ...job, contextFiles });
22899
+ }
22772
22900
  list(parentSessionID) {
22773
22901
  const jobs = [...this.jobs.values()];
22774
22902
  const filtered = parentSessionID ? jobs.filter((job) => job.parentSessionID === parentSessionID) : jobs;
@@ -22780,15 +22908,21 @@ class BackgroundJobBoard {
22780
22908
  hasTerminalUnreconciled(parentSessionID) {
22781
22909
  return this.list(parentSessionID).some((job) => job.terminalUnreconciled);
22782
22910
  }
22783
- formatForPrompt(parentSessionID) {
22784
- const jobs = this.list(parentSessionID).filter((job) => job.state === "running" || job.terminalUnreconciled);
22785
- if (jobs.length === 0)
22911
+ formatForPrompt(parentSessionID, now = Date.now()) {
22912
+ const active = this.list(parentSessionID).filter((job) => job.state === "running" || job.terminalUnreconciled);
22913
+ const reusable = this.list(parentSessionID).filter(isReusable);
22914
+ if (active.length === 0 && reusable.length === 0)
22786
22915
  return;
22787
22916
  return [
22788
22917
  "### Background Job Board",
22789
- "Use task_status before consuming running jobs. Reconcile terminal jobs before final response.",
22918
+ "SENTINEL: background-job-board-v2",
22919
+ "Use task_status for running jobs. Reconcile terminal jobs before final response. Reuse only completed/reconciled sessions for the same specialist/context.",
22920
+ "",
22921
+ "#### Active / Unreconciled",
22922
+ ...active.length > 0 ? active.map((job) => formatJob(job, now)) : ["- none"],
22790
22923
  "",
22791
- ...jobs.map(formatJob)
22924
+ "#### Reusable Sessions",
22925
+ ...reusable.length > 0 ? reusable.map((job) => this.formatReusableJob(job)) : ["- none"]
22792
22926
  ].join(`
22793
22927
  `);
22794
22928
  }
@@ -22800,6 +22934,26 @@ class BackgroundJobBoard {
22800
22934
  drop(taskID) {
22801
22935
  this.jobs.delete(taskID);
22802
22936
  }
22937
+ trimReusable(taskID) {
22938
+ const job = this.jobs.get(taskID);
22939
+ if (!job || !isReusable(job))
22940
+ return;
22941
+ const reusable = this.list(job.parentSessionID).filter((candidate) => candidate.agent === job.agent && isReusable(candidate)).sort((a, b) => b.lastUsedAt - a.lastUsedAt);
22942
+ for (const stale of reusable.slice(this.maxReusablePerAgent)) {
22943
+ this.jobs.delete(stale.taskID);
22944
+ }
22945
+ }
22946
+ formatReusableJob(job) {
22947
+ const lines = [
22948
+ `- ${job.alias} / ${job.taskID} / ${job.agent} / completed, reconciled`,
22949
+ ` Objective: ${job.objective || job.description}`
22950
+ ];
22951
+ const context = formatContextFiles(job.contextFiles, this.readContextMaxFiles);
22952
+ if (context)
22953
+ lines.push(` Context read by ${job.alias}: ${context}`);
22954
+ return lines.join(`
22955
+ `);
22956
+ }
22803
22957
  nextAlias(parentSessionID, agent) {
22804
22958
  const prefix2 = AGENT_PREFIX[agent] ?? (agent.slice(0, 3) || "job");
22805
22959
  const key = `${parentSessionID}:${prefix2}`;
@@ -22808,8 +22962,35 @@ class BackgroundJobBoard {
22808
22962
  return `${prefix2}-${next}`;
22809
22963
  }
22810
22964
  }
22811
- function formatJob(job) {
22812
- const status = job.terminalUnreconciled ? `${job.state}, unreconciled` : job.timedOut ? `${job.state}, timed out` : job.state;
22965
+ function deriveTaskSessionLabel(input) {
22966
+ const preferred = normalizeWhitespace(input.description ?? "");
22967
+ if (preferred)
22968
+ return preferred.slice(0, 48);
22969
+ const firstPromptLine = (input.prompt ?? "").split(/\r?\n/).map((line) => normalizeWhitespace(line)).find(Boolean);
22970
+ return firstPromptLine ? firstPromptLine.slice(0, 48) : `recent ${input.agentType} task`;
22971
+ }
22972
+ function isReusable(job) {
22973
+ return job.state === "reconciled" && job.terminalState === "completed";
22974
+ }
22975
+ function terminalStateOf(state) {
22976
+ return state === "completed" || state === "error" || state === "cancelled" ? state : undefined;
22977
+ }
22978
+ function formatContextFiles(files, maxFiles) {
22979
+ if (maxFiles === 0)
22980
+ return "";
22981
+ const shown = files.slice(0, maxFiles);
22982
+ const rest = files.length - shown.length;
22983
+ const rendered = shown.map((file) => `${file.path} (${file.lineCount} lines)`);
22984
+ return `${rendered.join(", ")}${rest > 0 ? ` (+${rest} more)` : ""}`;
22985
+ }
22986
+ function normalizeWhitespace(value) {
22987
+ return value.replace(/\s+/g, " ").trim();
22988
+ }
22989
+ function formatJob(job, now = Date.now()) {
22990
+ const ageMs = now - job.lastLaunchedAt;
22991
+ const isResume = job.lastLaunchedAt !== job.launchedAt;
22992
+ const ageLabel = job.state === "running" && ageMs < 30000 ? ` [${isResume ? "resumed" : "just launched"}, ${Math.floor(ageMs / 1000)}s ago]` : "";
22993
+ const status = job.terminalUnreconciled ? `${job.state}, unreconciled` : job.timedOut ? `${job.state}, timed out` : `${job.state}${ageLabel}`;
22813
22994
  const lines = [
22814
22995
  `- ${job.alias} / ${job.taskID} / ${job.agent} / ${status}`,
22815
22996
  ` Objective: ${job.objective || job.description}`
@@ -22826,6 +23007,10 @@ function singleLine(value) {
22826
23007
  return normalized;
22827
23008
  return `${normalized.slice(0, 157)}...`;
22828
23009
  }
23010
+ function normalizeCancelReason(reason) {
23011
+ const normalized = reason?.replace(/\s+/g, " ").trim();
23012
+ return normalized ? `cancelled: ${normalized}` : "cancelled";
23013
+ }
22829
23014
  // src/utils/internal-initiator.ts
22830
23015
  var SLIM_INTERNAL_INITIATOR_MARKER = "<!-- SLIM_INTERNAL_INITIATOR -->";
22831
23016
  function isRecord(value) {
@@ -22847,236 +23032,6 @@ function hasInternalInitiatorMarker(part) {
22847
23032
  }
22848
23033
  return part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER);
22849
23034
  }
22850
- // src/utils/session-manager.ts
22851
- var MIN_CONTEXT_FILE_LINES = 10;
22852
- var MAX_CONTEXT_FILES_PER_SESSION = 8;
22853
- function aliasPrefix(agentType) {
22854
- switch (agentType) {
22855
- case "explorer":
22856
- return "exp";
22857
- case "librarian":
22858
- return "lib";
22859
- case "oracle":
22860
- return "ora";
22861
- case "designer":
22862
- return "des";
22863
- case "fixer":
22864
- return "fix";
22865
- case "observer":
22866
- return "obs";
22867
- case "council":
22868
- return "cnc";
22869
- case "councillor":
22870
- return "clr";
22871
- case "orchestrator":
22872
- return "orc";
22873
- }
22874
- }
22875
- function normalizeWhitespace(value) {
22876
- return value.replace(/\s+/g, " ").trim();
22877
- }
22878
- function deriveTaskSessionLabel(input) {
22879
- const preferred = normalizeWhitespace(input.description ?? "");
22880
- if (preferred) {
22881
- return preferred.slice(0, 48);
22882
- }
22883
- const firstPromptLine = (input.prompt ?? "").split(/\r?\n/).map((line) => normalizeWhitespace(line)).find(Boolean);
22884
- if (firstPromptLine) {
22885
- return firstPromptLine.slice(0, 48);
22886
- }
22887
- return `recent ${input.agentType} task`;
22888
- }
22889
-
22890
- class SessionManager {
22891
- maxSessionsPerAgent;
22892
- readContextMinLines;
22893
- readContextMaxFiles;
22894
- sessionsByParent = new Map;
22895
- nextAliasIndexByParent = new Map;
22896
- orderCounter = 0;
22897
- constructor(maxSessionsPerAgent, options = {}) {
22898
- this.maxSessionsPerAgent = maxSessionsPerAgent;
22899
- this.readContextMinLines = options.readContextMinLines ?? MIN_CONTEXT_FILE_LINES;
22900
- this.readContextMaxFiles = options.readContextMaxFiles ?? MAX_CONTEXT_FILES_PER_SESSION;
22901
- }
22902
- remember(input) {
22903
- const now = this.nextOrder();
22904
- const group = this.getAgentGroup(input.parentSessionId, input.agentType, true);
22905
- if (!group) {
22906
- throw new Error("Failed to initialize session group");
22907
- }
22908
- const existing = group.find((entry) => entry.taskId === input.taskId);
22909
- if (existing) {
22910
- existing.label = input.label;
22911
- existing.lastUsedAt = this.nextOrder();
22912
- return existing;
22913
- }
22914
- const remembered = {
22915
- alias: this.nextAlias(input.parentSessionId, input.agentType),
22916
- taskId: input.taskId,
22917
- agentType: input.agentType,
22918
- label: input.label,
22919
- contextFiles: [],
22920
- createdAt: now,
22921
- lastUsedAt: now
22922
- };
22923
- group.push(remembered);
22924
- this.trimGroup(group);
22925
- return remembered;
22926
- }
22927
- markUsed(parentSessionId, agentType, key) {
22928
- const group = this.getAgentGroup(parentSessionId, agentType, false);
22929
- const match = group?.find((entry) => entry.alias === key || entry.taskId === key);
22930
- if (match) {
22931
- match.lastUsedAt = this.nextOrder();
22932
- }
22933
- }
22934
- resolve(parentSessionId, agentType, key) {
22935
- const group = this.getAgentGroup(parentSessionId, agentType, false);
22936
- return group?.find((entry) => entry.alias === key || entry.taskId === key);
22937
- }
22938
- drop(parentSessionId, agentType, key) {
22939
- const group = this.getAgentGroup(parentSessionId, agentType, false);
22940
- if (!group)
22941
- return;
22942
- const next = group.filter((entry) => entry.alias !== key && entry.taskId !== key);
22943
- this.setAgentGroup(parentSessionId, agentType, next);
22944
- }
22945
- dropTask(taskId) {
22946
- for (const [parentSessionId, groups] of this.sessionsByParent.entries()) {
22947
- for (const [agentType, group] of groups.entries()) {
22948
- const next = group.filter((entry) => entry.taskId !== taskId);
22949
- this.setAgentGroup(parentSessionId, agentType, next);
22950
- }
22951
- }
22952
- }
22953
- taskIds() {
22954
- const ids = new Set;
22955
- for (const groups of this.sessionsByParent.values()) {
22956
- for (const group of groups.values()) {
22957
- for (const entry of group) {
22958
- ids.add(entry.taskId);
22959
- }
22960
- }
22961
- }
22962
- return ids;
22963
- }
22964
- addContext(taskId, files) {
22965
- if (files.length === 0)
22966
- return;
22967
- for (const groups of this.sessionsByParent.values()) {
22968
- for (const group of groups.values()) {
22969
- const match = group.find((entry) => entry.taskId === taskId);
22970
- if (!match)
22971
- continue;
22972
- const existing = new Map(match.contextFiles.map((file) => [file.path, file]));
22973
- for (const file of files) {
22974
- const previous = existing.get(file.path);
22975
- if (previous) {
22976
- previous.lineCount = Math.max(previous.lineCount, file.lineCount);
22977
- previous.lastReadAt = Math.max(previous.lastReadAt, file.lastReadAt);
22978
- continue;
22979
- }
22980
- match.contextFiles.push({ ...file });
22981
- }
22982
- this.trimContextFiles(match);
22983
- }
22984
- }
22985
- }
22986
- clearParent(parentSessionId) {
22987
- this.sessionsByParent.delete(parentSessionId);
22988
- this.nextAliasIndexByParent.delete(parentSessionId);
22989
- }
22990
- formatForPrompt(parentSessionId) {
22991
- const groups = this.sessionsByParent.get(parentSessionId);
22992
- if (!groups || groups.size === 0)
22993
- return;
22994
- const lines = [...groups.entries()].map(([agentType, entries]) => [
22995
- agentType,
22996
- [...entries].sort((a, b) => b.lastUsedAt - a.lastUsedAt)
22997
- ]).filter(([, entries]) => entries.length > 0).sort((a, b) => b[1][0].lastUsedAt - a[1][0].lastUsedAt).map(([agentType, entries]) => [
22998
- `- ${agentType}: ${entries.map((entry) => `${entry.alias} ${entry.label}`).join("; ")}`,
22999
- ...entries.map((entry) => [
23000
- entry,
23001
- formatContextFiles(entry.contextFiles, {
23002
- minLines: this.readContextMinLines,
23003
- maxFiles: this.readContextMaxFiles
23004
- })
23005
- ]).filter(([, context]) => context.length > 0).map(([entry, context]) => ` Context read by ${entry.alias}: ${context}`)
23006
- ].join(`
23007
- `));
23008
- if (lines.length === 0)
23009
- return;
23010
- return [
23011
- "### Resumable Sessions",
23012
- "Reuse only completed/reconciled threads. Poll running jobs from Background Job Board.",
23013
- "",
23014
- ...lines
23015
- ].join(`
23016
- `);
23017
- }
23018
- getAgentGroup(parentSessionId, agentType, create) {
23019
- let groups = this.sessionsByParent.get(parentSessionId);
23020
- if (!groups && create) {
23021
- groups = new Map;
23022
- this.sessionsByParent.set(parentSessionId, groups);
23023
- }
23024
- let group = groups?.get(agentType);
23025
- if (!group && create && groups) {
23026
- group = [];
23027
- groups.set(agentType, group);
23028
- }
23029
- return group;
23030
- }
23031
- setAgentGroup(parentSessionId, agentType, entries) {
23032
- const groups = this.sessionsByParent.get(parentSessionId);
23033
- if (!groups)
23034
- return;
23035
- if (entries.length === 0) {
23036
- groups.delete(agentType);
23037
- if (groups.size === 0) {
23038
- this.sessionsByParent.delete(parentSessionId);
23039
- this.nextAliasIndexByParent.delete(parentSessionId);
23040
- }
23041
- return;
23042
- }
23043
- groups.set(agentType, entries);
23044
- }
23045
- nextAlias(parentSessionId, agentType) {
23046
- let counters = this.nextAliasIndexByParent.get(parentSessionId);
23047
- if (!counters) {
23048
- counters = new Map;
23049
- this.nextAliasIndexByParent.set(parentSessionId, counters);
23050
- }
23051
- const next = (counters.get(agentType) ?? 0) + 1;
23052
- counters.set(agentType, next);
23053
- return `${aliasPrefix(agentType)}-${next}`;
23054
- }
23055
- trimGroup(group) {
23056
- group.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
23057
- if (group.length > this.maxSessionsPerAgent) {
23058
- group.length = this.maxSessionsPerAgent;
23059
- }
23060
- }
23061
- trimContextFiles(entry) {
23062
- if (this.readContextMaxFiles === 0) {
23063
- entry.contextFiles = [];
23064
- return;
23065
- }
23066
- entry.contextFiles = entry.contextFiles.filter((file) => file.lineCount >= this.readContextMinLines).sort((a, b) => b.lastReadAt - a.lastReadAt).slice(0, this.readContextMaxFiles + 1);
23067
- }
23068
- nextOrder() {
23069
- this.orderCounter += 1;
23070
- return this.orderCounter;
23071
- }
23072
- }
23073
- function formatContextFiles(files, options) {
23074
- const eligible = files.filter((file) => file.lineCount >= options.minLines).sort((a, b) => b.lastReadAt - a.lastReadAt);
23075
- const shown = eligible.slice(0, options.maxFiles);
23076
- const rest = eligible.length - shown.length;
23077
- const rendered = shown.map((file) => `${file.path} (${file.lineCount} lines)`);
23078
- return `${rendered.join(", ")}${rest > 0 ? ` (+${rest} more)` : ""}`;
23079
- }
23080
23035
  // src/utils/zip-extractor.ts
23081
23036
  import { spawnSync } from "node:child_process";
23082
23037
  import { release } from "node:os";
@@ -23207,6 +23162,53 @@ function createChatHeadersHook(ctx) {
23207
23162
  }
23208
23163
  };
23209
23164
  }
23165
+ // src/hooks/deepwork/index.ts
23166
+ var COMMAND_NAME = "deepwork";
23167
+ function activationPrompt(task2) {
23168
+ return [
23169
+ "Use the deepwork skill for this task. Treat it as a heavy coding session.",
23170
+ "",
23171
+ "Deepwork requirements:",
23172
+ "- create/update a `.slim/deepwork/` progress file;",
23173
+ "- keep OpenCode todos synced with the current phase;",
23174
+ "- draft a plan and get `@oracle` review before implementation;",
23175
+ "- create and review a phased implementation/delegation plan;",
23176
+ "- execute phase by phase with background specialists where useful;",
23177
+ "- poll `task_status`, reconcile results, validate, and ask `@oracle` to review each phase;",
23178
+ "- ask `@oracle` to include simplify/readability feedback in phase reviews;",
23179
+ "- fix actionable review issues before continuing.",
23180
+ "",
23181
+ "Task:",
23182
+ task2
23183
+ ].join(`
23184
+ `);
23185
+ }
23186
+ function createDeepworkCommandHook() {
23187
+ return {
23188
+ registerCommand: (opencodeConfig) => {
23189
+ const commandConfig = opencodeConfig.command;
23190
+ if (commandConfig?.[COMMAND_NAME])
23191
+ return;
23192
+ if (!opencodeConfig.command)
23193
+ opencodeConfig.command = {};
23194
+ opencodeConfig.command[COMMAND_NAME] = {
23195
+ template: "Start a deepwork session for a complex coding task",
23196
+ description: "Use the deepwork workflow for heavy multi-phase coding work"
23197
+ };
23198
+ },
23199
+ handleCommandExecuteBefore: async (input, output) => {
23200
+ if (input.command !== COMMAND_NAME)
23201
+ return;
23202
+ output.parts.length = 0;
23203
+ const task2 = input.arguments.trim();
23204
+ if (!task2) {
23205
+ output.parts.push(createInternalAgentTextPart("What task should deepwork manage? Run `/deepwork <task>`."));
23206
+ return;
23207
+ }
23208
+ output.parts.push({ type: "text", text: activationPrompt(task2) });
23209
+ }
23210
+ };
23211
+ }
23210
23212
  // src/hooks/delegate-task-retry/patterns.ts
23211
23213
  var DELEGATE_TASK_ERROR_PATTERNS = [
23212
23214
  {
@@ -23407,7 +23409,7 @@ var RATE_LIMIT_PATTERNS = [
23407
23409
  /usage limit/i,
23408
23410
  /overloaded/i,
23409
23411
  /resource.?exhausted/i,
23410
- /insufficient.?quota/i,
23412
+ /insufficient.?(quota|balance)/i,
23411
23413
  /high concurrency/i,
23412
23414
  /reduce concurrency/i
23413
23415
  ];
@@ -23483,7 +23485,7 @@ class ForegroundFallbackManager {
23483
23485
  if (!props?.sessionID || props.status?.type !== "retry")
23484
23486
  break;
23485
23487
  const msg = props.status.message?.toLowerCase() ?? "";
23486
- if (msg.includes("rate limit") || msg.includes("usage limit") || msg.includes("usage exceeded") || msg.includes("quota exceeded") || msg.includes("exceededbudget") || msg.includes("over budget") || msg.includes("high concurrency") || msg.includes("reduce concurrency")) {
23488
+ if (msg.includes("rate limit") || msg.includes("usage limit") || msg.includes("usage exceeded") || msg.includes("quota exceeded") || msg.includes("exceededbudget") || msg.includes("over budget") || msg.includes("insufficient") || msg.includes("high concurrency") || msg.includes("reduce concurrency")) {
23487
23489
  await this.tryFallback(props.sessionID);
23488
23490
  }
23489
23491
  break;
@@ -23932,382 +23934,51 @@ function createPostFileToolNudgeHook(options = {}) {
23932
23934
  }
23933
23935
  };
23934
23936
  }
23935
- // src/hooks/session-goal/index.ts
23936
- import * as fs7 from "node:fs/promises";
23937
-
23938
- // src/interview/document.ts
23939
- import * as fsSync from "node:fs";
23940
- import * as fs6 from "node:fs/promises";
23941
- import * as path9 from "node:path";
23942
- var DEFAULT_OUTPUT_FOLDER = "interview";
23943
- function normalizeOutputFolder(outputFolder) {
23944
- const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
23945
- return normalized || DEFAULT_OUTPUT_FOLDER;
23946
- }
23947
- function createInterviewDirectoryPath(directory, outputFolder) {
23948
- return path9.join(directory, normalizeOutputFolder(outputFolder));
23949
- }
23950
- function createInterviewFilePath(directory, outputFolder, idea) {
23951
- const fileName = `${slugify(idea) || "interview"}.md`;
23952
- return path9.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
23953
- }
23954
- function relativeInterviewPath(directory, filePath) {
23955
- return path9.relative(directory, filePath) || path9.basename(filePath);
23937
+ // src/hooks/task-session-manager/index.ts
23938
+ import path9 from "node:path";
23939
+ var AGENT_NAME_SET = new Set([
23940
+ "orchestrator",
23941
+ "oracle",
23942
+ "designer",
23943
+ "explorer",
23944
+ "librarian",
23945
+ "fixer",
23946
+ "observer",
23947
+ "council",
23948
+ "councillor"
23949
+ ]);
23950
+ var MAX_PENDING_TASK_CALLS = 100;
23951
+ var BACKGROUND_JOB_BOARD_SENTINEL = "SENTINEL: background-job-board-v2";
23952
+ var BACKGROUND_COMPLETION_COMPLETED = /^Background task completed: /;
23953
+ var BACKGROUND_COMPLETION_FAILED = /^Background task failed: /;
23954
+ var MAX_PROCESSED_INJECTED_COMPLETIONS = 500;
23955
+ function djb2Hash(str) {
23956
+ let hash = 5381;
23957
+ for (let i = 0;i < str.length; i++) {
23958
+ hash = (hash << 5) + hash + str.charCodeAt(i);
23959
+ }
23960
+ return (hash >>> 0).toString(16).padStart(8, "0");
23956
23961
  }
23957
- function resolveExistingInterviewPath(directory, outputFolder, value) {
23958
- const trimmed = value.trim();
23959
- if (!trimmed) {
23960
- return null;
23962
+ function createOccurrenceId(part, message, partIndex) {
23963
+ if (typeof part.id === "string") {
23964
+ return part.id;
23961
23965
  }
23962
- const outputDir = createInterviewDirectoryPath(directory, outputFolder);
23963
- const candidates = new Set;
23964
- const resolvedRoot = path9.resolve(directory);
23965
- if (path9.isAbsolute(trimmed)) {
23966
- candidates.add(trimmed);
23967
- } else {
23968
- candidates.add(path9.resolve(directory, trimmed));
23969
- candidates.add(path9.join(outputDir, trimmed));
23970
- if (!trimmed.endsWith(".md")) {
23971
- candidates.add(path9.join(outputDir, `${trimmed}.md`));
23972
- }
23966
+ if (typeof message.info.id === "string") {
23967
+ return `${message.info.id}:${partIndex}`;
23973
23968
  }
23974
- for (const candidate of candidates) {
23975
- if (path9.extname(candidate) !== ".md") {
23976
- continue;
23977
- }
23978
- const resolved = path9.resolve(candidate);
23979
- if (!resolved.startsWith(resolvedRoot + path9.sep) && resolved !== resolvedRoot) {
23980
- continue;
23981
- }
23982
- if (fsSync.existsSync(candidate)) {
23983
- return candidate;
23984
- }
23969
+ const sessionID = message.info.sessionID ?? "unknown";
23970
+ const content = typeof part.text === "string" ? part.text : "";
23971
+ const status = parseTaskStatusOutput(content);
23972
+ if (status) {
23973
+ const stableKey = `${sessionID}:${status.taskID}:${status.state}:${status.result ?? ""}`;
23974
+ const hash2 = djb2Hash(stableKey);
23975
+ return `anon:${hash2}`;
23985
23976
  }
23986
- return null;
23987
- }
23988
- function slugify(value) {
23989
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
23977
+ const hash = djb2Hash(`${sessionID}:${content}`);
23978
+ return `anon:${hash}`;
23990
23979
  }
23991
- function extractHistorySection(document) {
23992
- const marker = `## Q&A history
23993
-
23994
- `;
23995
- const index = document.indexOf(marker);
23996
- return index >= 0 ? document.slice(index + marker.length).trim() : "";
23997
- }
23998
- function extractSummarySection(document) {
23999
- const marker = `## Current spec
24000
-
24001
- `;
24002
- const historyMarker = `
24003
-
24004
- ## Q&A history`;
24005
- const start = document.indexOf(marker);
24006
- if (start < 0) {
24007
- return "";
24008
- }
24009
- const summaryStart = start + marker.length;
24010
- const summaryEnd = document.indexOf(historyMarker, summaryStart);
24011
- return document.slice(summaryStart, summaryEnd >= 0 ? summaryEnd : undefined).trim();
24012
- }
24013
- function extractTitle(document) {
24014
- const match = document.match(/^#\s+(.+)$/m);
24015
- return match?.[1]?.trim() ?? "";
24016
- }
24017
- function buildInterviewDocument(idea, summary, history, meta) {
24018
- const normalizedSummary = summary.trim() || "Waiting for interview answers.";
24019
- const normalizedHistory = history.trim() || "No answers yet.";
24020
- const frontmatter = meta?.sessionID ? [
24021
- "---",
24022
- `sessionID: ${meta.sessionID}`,
24023
- `baseMessageCount: ${meta.baseMessageCount ?? 0}`,
24024
- `updatedAt: ${new Date().toISOString()}`,
24025
- "---",
24026
- ""
24027
- ].join(`
24028
- `) : "";
24029
- return [
24030
- frontmatter,
24031
- `# ${idea}`,
24032
- "",
24033
- "## Current spec",
24034
- "",
24035
- normalizedSummary,
24036
- "",
24037
- "## Q&A history",
24038
- "",
24039
- normalizedHistory,
24040
- ""
24041
- ].join(`
24042
- `);
24043
- }
24044
- function parseFrontmatter(content) {
24045
- const match = content.match(/^---\n([\s\S]*?)\n---\n/);
24046
- if (!match)
24047
- return null;
24048
- const result = {};
24049
- for (const line of match[1].split(`
24050
- `)) {
24051
- const colonIdx = line.indexOf(":");
24052
- if (colonIdx > 0) {
24053
- result[line.slice(0, colonIdx).trim()] = line.slice(colonIdx + 1).trim();
24054
- }
24055
- }
24056
- return result;
24057
- }
24058
- async function ensureInterviewFile(record) {
24059
- await fs6.mkdir(path9.dirname(record.markdownPath), { recursive: true });
24060
- try {
24061
- await fs6.access(record.markdownPath);
24062
- } catch {
24063
- await fs6.writeFile(record.markdownPath, buildInterviewDocument(record.idea, "", "", {
24064
- sessionID: record.sessionID,
24065
- baseMessageCount: record.baseMessageCount
24066
- }), "utf8");
24067
- }
24068
- }
24069
- async function readInterviewDocument(record) {
24070
- try {
24071
- return await fs6.readFile(record.markdownPath, "utf8");
24072
- } catch {}
24073
- await ensureInterviewFile(record);
24074
- return fs6.readFile(record.markdownPath, "utf8");
24075
- }
24076
- async function rewriteInterviewDocument(record, summary) {
24077
- const existing = await readInterviewDocument(record);
24078
- const history = extractHistorySection(existing);
24079
- const next = buildInterviewDocument(record.idea, summary, history, {
24080
- sessionID: record.sessionID,
24081
- baseMessageCount: record.baseMessageCount
24082
- });
24083
- await fs6.writeFile(record.markdownPath, next, "utf8");
24084
- return next;
24085
- }
24086
- async function appendInterviewAnswers(record, questions, answers) {
24087
- const existing = await readInterviewDocument(record);
24088
- const summary = extractSummarySection(existing);
24089
- const history = extractHistorySection(existing);
24090
- const questionMap = new Map(questions.map((question) => [question.id, question]));
24091
- const appended = answers.map((answer) => {
24092
- const question = questionMap.get(answer.questionId);
24093
- return question ? `Q: ${question.question}
24094
- A: ${answer.answer.trim()}` : null;
24095
- }).filter((value) => value !== null).join(`
24096
-
24097
- `);
24098
- const nextHistory = [history === "No answers yet." ? "" : history, appended].filter(Boolean).join(`
24099
-
24100
- `);
24101
- await fs6.writeFile(record.markdownPath, buildInterviewDocument(record.idea, summary, nextHistory, {
24102
- sessionID: record.sessionID,
24103
- baseMessageCount: record.baseMessageCount
24104
- }), "utf8");
24105
- }
24106
-
24107
- // src/hooks/session-goal/index.ts
24108
- var COMMAND_NAME = "goal";
24109
- var MAX_GOAL_LENGTH = 4000;
24110
- function normalizeGoalText(text) {
24111
- return text.trim().replace(/\s+/g, " ").slice(0, MAX_GOAL_LENGTH);
24112
- }
24113
- function trimGoalText(text) {
24114
- return text.trim().slice(0, MAX_GOAL_LENGTH);
24115
- }
24116
- function pushText(output, text) {
24117
- output.parts.push(createInternalAgentTextPart(text));
24118
- }
24119
- function formatGoal(state, inherited) {
24120
- const tag = inherited ? "parent_goal" : "active_goal";
24121
- const guidance = inherited ? "This is context only. Your delegated prompt remains the bounded task." : "Use todos as the execution ledger. Keep planning, delegation, edits, and verification aligned to this goal. Do not broaden scope unless the user changes the goal.";
24122
- return `<${tag}>
24123
- Objective: ${state.text}
24124
- ${guidance}
24125
- </${tag}>`;
24126
- }
24127
- async function readInterviewGoal(directory, outputFolder, value) {
24128
- try {
24129
- const sourcePath = resolveExistingInterviewPath(directory, outputFolder, value);
24130
- if (!sourcePath)
24131
- return null;
24132
- const content = await fs7.readFile(sourcePath, "utf8");
24133
- const title = extractTitle(content);
24134
- const summary = extractSummarySection(content);
24135
- const text = trimGoalText([title ? `From interview: ${title}` : "", summary].filter(Boolean).join(`
24136
-
24137
- `));
24138
- return text ? { text, sourcePath } : null;
24139
- } catch {
24140
- return null;
24141
- }
24142
- }
24143
- function resolveGoal(goals, sessionID) {
24144
- const seen = new Set;
24145
- let currentSessionID = sessionID;
24146
- let inherited = false;
24147
- while (true) {
24148
- if (seen.has(currentSessionID)) {
24149
- goals.delete(sessionID);
24150
- return null;
24151
- }
24152
- seen.add(currentSessionID);
24153
- const goal = goals.get(currentSessionID);
24154
- if (!goal) {
24155
- goals.delete(sessionID);
24156
- return null;
24157
- }
24158
- if (!goal.inheritedFrom) {
24159
- return { goal, inherited };
24160
- }
24161
- inherited = true;
24162
- currentSessionID = goal.inheritedFrom;
24163
- }
24164
- }
24165
- function createSessionGoalHook(ctx, config, options) {
24166
- const goals = new Map;
24167
- const outputFolder = config.interview?.outputFolder ?? "interview";
24168
- return {
24169
- registerCommand: (opencodeConfig) => {
24170
- const commandConfig = opencodeConfig.command;
24171
- if (commandConfig?.[COMMAND_NAME])
24172
- return;
24173
- if (!opencodeConfig.command)
24174
- opencodeConfig.command = {};
24175
- opencodeConfig.command[COMMAND_NAME] = {
24176
- template: "Set or show the current session goal",
24177
- description: "Pin a session objective that keeps todos, delegation, and verification aligned"
24178
- };
24179
- },
24180
- handleCommandExecuteBefore: async (input, output) => {
24181
- if (input.command !== COMMAND_NAME)
24182
- return;
24183
- output.parts.length = 0;
24184
- const args = input.arguments.trim();
24185
- if (!args) {
24186
- const resolved = resolveGoal(goals, input.sessionID);
24187
- pushText(output, resolved ? `Active goal:
24188
- ${resolved.goal.text}
24189
-
24190
- Use todos for execution steps. Auto-continuation continues only while todos remain.` : "No active goal. Set one with /goal <objective>.");
24191
- return;
24192
- }
24193
- if (args === "clear") {
24194
- goals.delete(input.sessionID);
24195
- pushText(output, "Cleared the active goal for this session.");
24196
- return;
24197
- }
24198
- if (args.startsWith("from ")) {
24199
- const value = args.slice("from ".length).trim();
24200
- const interviewGoal = await readInterviewGoal(ctx.directory, outputFolder, value);
24201
- if (!interviewGoal) {
24202
- pushText(output, `Could not find a readable interview spec for "${value}".`);
24203
- return;
24204
- }
24205
- goals.set(input.sessionID, {
24206
- text: interviewGoal.text,
24207
- source: "interview",
24208
- sourcePath: interviewGoal.sourcePath,
24209
- createdAt: Date.now()
24210
- });
24211
- pushText(output, `Set active goal from interview:
24212
- ${interviewGoal.text}`);
24213
- return;
24214
- }
24215
- const text = normalizeGoalText(args);
24216
- goals.set(input.sessionID, {
24217
- text,
24218
- source: "manual",
24219
- createdAt: Date.now()
24220
- });
24221
- pushText(output, `Set active goal:
24222
- ${text}`);
24223
- },
24224
- handleEvent: (input) => {
24225
- const event = input.event;
24226
- if (event.type === "session.created") {
24227
- const info = event.properties?.info;
24228
- if (!info?.id || !info.parentID)
24229
- return;
24230
- const parentGoal = goals.get(info.parentID);
24231
- if (!parentGoal)
24232
- return;
24233
- goals.set(info.id, {
24234
- inheritedFrom: info.parentID,
24235
- createdAt: Date.now(),
24236
- text: ""
24237
- });
24238
- return;
24239
- }
24240
- if (event.type === "session.deleted") {
24241
- const props = event.properties;
24242
- const sessionID = props?.info?.id ?? props?.sessionID;
24243
- if (sessionID)
24244
- goals.delete(sessionID);
24245
- }
24246
- },
24247
- handleSystemTransform: (input, output) => {
24248
- if (!input.sessionID)
24249
- return;
24250
- const resolved = resolveGoal(goals, input.sessionID);
24251
- if (!resolved)
24252
- return;
24253
- const agentName = options?.getAgentName?.(input.sessionID);
24254
- const { goal, inherited } = resolved;
24255
- if (!inherited && agentName && agentName !== "orchestrator")
24256
- return;
24257
- const block = formatGoal(goal, inherited);
24258
- if (output.system.some((entry) => entry.includes(block)))
24259
- return;
24260
- output.system.push(block);
24261
- },
24262
- getGoal: (sessionID) => resolveGoal(goals, sessionID)?.goal
24263
- };
24264
- }
24265
- // src/hooks/task-session-manager/index.ts
24266
- import path10 from "node:path";
24267
- var AGENT_NAME_SET = new Set([
24268
- "orchestrator",
24269
- "oracle",
24270
- "designer",
24271
- "explorer",
24272
- "librarian",
24273
- "fixer",
24274
- "observer",
24275
- "council",
24276
- "councillor"
24277
- ]);
24278
- var MAX_PENDING_TASK_CALLS = 100;
24279
- var RESUMABLE_SESSIONS_START = "<resumable_sessions>";
24280
- var RESUMABLE_SESSIONS_END = "</resumable_sessions>";
24281
- var BACKGROUND_COMPLETION_COMPLETED = /^Background task completed: /;
24282
- var BACKGROUND_COMPLETION_FAILED = /^Background task failed: /;
24283
- var MAX_PROCESSED_INJECTED_COMPLETIONS = 500;
24284
- function djb2Hash(str) {
24285
- let hash = 5381;
24286
- for (let i = 0;i < str.length; i++) {
24287
- hash = (hash << 5) + hash + str.charCodeAt(i);
24288
- }
24289
- return (hash >>> 0).toString(16).padStart(8, "0");
24290
- }
24291
- function createOccurrenceId(part, message, partIndex) {
24292
- if (typeof part.id === "string") {
24293
- return part.id;
24294
- }
24295
- if (typeof message.info.id === "string") {
24296
- return `${message.info.id}:${partIndex}`;
24297
- }
24298
- const sessionID = message.info.sessionID ?? "unknown";
24299
- const content = typeof part.text === "string" ? part.text : "";
24300
- const status = parseTaskStatusOutput(content);
24301
- if (status) {
24302
- const stableKey = `${sessionID}:${status.taskID}:${status.state}:${status.result ?? ""}`;
24303
- const hash2 = djb2Hash(stableKey);
24304
- return `anon:${hash2}`;
24305
- }
24306
- const hash = djb2Hash(`${sessionID}:${content}`);
24307
- return `anon:${hash}`;
24308
- }
24309
- function isAgentName(value) {
24310
- return typeof value === "string" && AGENT_NAME_SET.has(value);
23980
+ function isAgentName(value) {
23981
+ return typeof value === "string" && AGENT_NAME_SET.has(value);
24311
23982
  }
24312
23983
  function isObjectRecord(value) {
24313
23984
  return typeof value === "object" && value !== null;
@@ -24316,11 +23987,11 @@ function extractPath(output) {
24316
23987
  return /<path>([^<]+)<\/path>/.exec(output)?.[1];
24317
23988
  }
24318
23989
  function normalizePath(root, file) {
24319
- const relative2 = path10.relative(root, file);
24320
- if (!relative2 || relative2.startsWith("..") || path10.isAbsolute(relative2)) {
23990
+ const relative = path9.relative(root, file);
23991
+ if (!relative || relative.startsWith("..") || path9.isAbsolute(relative)) {
24321
23992
  return file;
24322
23993
  }
24323
- return relative2;
23994
+ return relative;
24324
23995
  }
24325
23996
  function extractReadFiles(root, output) {
24326
23997
  if (typeof output.output !== "string")
@@ -24345,11 +24016,11 @@ function countReadLines(output) {
24345
24016
  return [...lines];
24346
24017
  }
24347
24018
  function createTaskSessionManagerHook(_ctx, options) {
24348
- const sessionManager = new SessionManager(options.maxSessionsPerAgent, {
24019
+ const backgroundJobBoard = options.backgroundJobBoard ?? new BackgroundJobBoard({
24020
+ maxReusablePerAgent: options.maxSessionsPerAgent,
24349
24021
  readContextMinLines: options.readContextMinLines,
24350
24022
  readContextMaxFiles: options.readContextMaxFiles
24351
24023
  });
24352
- const backgroundJobBoard = options.backgroundJobBoard ?? new BackgroundJobBoard;
24353
24024
  const pendingCalls = new Map;
24354
24025
  const pendingCallOrder = [];
24355
24026
  const contextByTask = new Map;
@@ -24378,7 +24049,7 @@ function createTaskSessionManagerHook(_ctx, options) {
24378
24049
  pending.lastReadAt = Math.max(pending.lastReadAt, file.lastReadAt);
24379
24050
  context.set(file.path, pending);
24380
24051
  }
24381
- sessionManager.addContext(taskId, contextFilesForPrompt(context));
24052
+ backgroundJobBoard.addContext(taskId, contextFilesForPrompt(context));
24382
24053
  }
24383
24054
  function contextFilesForPrompt(context) {
24384
24055
  if (!context)
@@ -24390,10 +24061,10 @@ function createTaskSessionManagerHook(_ctx, options) {
24390
24061
  }));
24391
24062
  }
24392
24063
  function canTrackTaskContext(taskId) {
24393
- return pendingManagedTaskIds.has(taskId) || sessionManager.taskIds().has(taskId);
24064
+ return pendingManagedTaskIds.has(taskId) || backgroundJobBoard.taskIDs().has(taskId);
24394
24065
  }
24395
24066
  function pruneContext() {
24396
- const remembered = sessionManager.taskIds();
24067
+ const remembered = backgroundJobBoard.taskIDs();
24397
24068
  for (const taskId of contextByTask.keys()) {
24398
24069
  if (!pendingManagedTaskIds.has(taskId) && !remembered.has(taskId)) {
24399
24070
  contextByTask.delete(taskId);
@@ -24416,7 +24087,7 @@ function createTaskSessionManagerHook(_ctx, options) {
24416
24087
  return;
24417
24088
  if (updated.terminalUnreconciled) {
24418
24089
  pendingManagedTaskIds.delete(updated.taskID);
24419
- contextByTask.delete(updated.taskID);
24090
+ backgroundJobBoard.addContext(updated.taskID, contextFilesForPrompt(contextByTask.get(updated.taskID)));
24420
24091
  pruneContext();
24421
24092
  }
24422
24093
  return updated;
@@ -24516,16 +24187,31 @@ function createTaskSessionManagerHook(_ctx, options) {
24516
24187
  }
24517
24188
  return {
24518
24189
  "tool.execute.before": async (input, output) => {
24519
- if (input.tool.toLowerCase() !== "task")
24190
+ const toolName = input.tool.toLowerCase();
24191
+ if (toolName !== "task" && toolName !== "task_status")
24520
24192
  return;
24521
24193
  if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
24522
24194
  return;
24523
24195
  }
24524
24196
  if (!isObjectRecord(output.args))
24525
24197
  return;
24198
+ if (toolName === "task_status") {
24199
+ const args2 = output.args;
24200
+ if (typeof args2.task_id !== "string" || args2.task_id.trim() === "") {
24201
+ return;
24202
+ }
24203
+ const resolved = backgroundJobBoard.resolveForStatus(input.sessionID, args2.task_id.trim());
24204
+ if (resolved)
24205
+ args2.task_id = resolved.taskID;
24206
+ return;
24207
+ }
24526
24208
  const args = output.args;
24527
- if (!isAgentName(args.subagent_type))
24209
+ if (!isAgentName(args.subagent_type)) {
24210
+ if (typeof args.task_id === "string" && args.task_id.trim() !== "") {
24211
+ delete args.task_id;
24212
+ }
24528
24213
  return;
24214
+ }
24529
24215
  const label = deriveTaskSessionLabel({
24530
24216
  description: typeof args.description === "string" ? args.description : undefined,
24531
24217
  prompt: typeof args.prompt === "string" ? args.prompt : undefined,
@@ -24545,15 +24231,15 @@ function createTaskSessionManagerHook(_ctx, options) {
24545
24231
  return;
24546
24232
  }
24547
24233
  const requested = args.task_id.trim();
24548
- const remembered = sessionManager.resolve(input.sessionID, args.subagent_type, requested);
24234
+ const remembered = backgroundJobBoard.resolveReusable(input.sessionID, requested, args.subagent_type);
24549
24235
  if (!remembered) {
24550
24236
  delete args.task_id;
24551
24237
  return;
24552
24238
  }
24553
- args.task_id = remembered.taskId;
24554
- pendingManagedTaskIds.add(remembered.taskId);
24555
- sessionManager.markUsed(input.sessionID, args.subagent_type, remembered.taskId);
24556
- pendingCall.resumedTaskId = remembered.taskId;
24239
+ args.task_id = remembered.taskID;
24240
+ pendingManagedTaskIds.add(remembered.taskID);
24241
+ backgroundJobBoard.markUsed(input.sessionID, remembered.taskID);
24242
+ pendingCall.resumedTaskId = remembered.taskID;
24557
24243
  rememberPendingCall(pendingCall);
24558
24244
  },
24559
24245
  "tool.execute.after": async (input, output) => {
@@ -24584,29 +24270,23 @@ function createTaskSessionManagerHook(_ctx, options) {
24584
24270
  description: pending.label,
24585
24271
  objective: pending.label
24586
24272
  });
24587
- sessionManager.drop(pending.parentSessionId, pending.agentType, pending.resumedTaskId ?? launch.taskID);
24273
+ backgroundJobBoard.addContext(launch.taskID, contextFilesForPrompt(contextByTask.get(launch.taskID)));
24588
24274
  pendingManagedTaskIds.add(launch.taskID);
24589
24275
  return;
24590
24276
  }
24591
24277
  const taskId = parseTaskIdFromTaskOutput(output.output);
24592
24278
  if (!taskId) {
24593
24279
  if (pending.resumedTaskId && isMissingRememberedSessionError(output.output)) {
24594
- sessionManager.drop(pending.parentSessionId, pending.agentType, pending.resumedTaskId);
24280
+ backgroundJobBoard.drop(pending.resumedTaskId);
24595
24281
  }
24596
24282
  return;
24597
24283
  }
24598
24284
  if (pending.resumedTaskId && pending.resumedTaskId !== taskId) {
24599
- sessionManager.drop(pending.parentSessionId, pending.agentType, pending.resumedTaskId);
24285
+ backgroundJobBoard.drop(pending.resumedTaskId);
24600
24286
  }
24601
- sessionManager.remember({
24602
- parentSessionId: pending.parentSessionId,
24603
- taskId,
24604
- agentType: pending.agentType,
24605
- label: pending.label
24606
- });
24607
24287
  pendingManagedTaskIds.delete(taskId);
24608
24288
  const contextFiles = contextFilesForPrompt(contextByTask.get(taskId));
24609
- sessionManager.addContext(taskId, contextFiles);
24289
+ backgroundJobBoard.addContext(taskId, contextFiles);
24610
24290
  pruneContext();
24611
24291
  },
24612
24292
  "experimental.chat.messages.transform": async (_input, output) => {
@@ -24633,8 +24313,7 @@ function createTaskSessionManagerHook(_ctx, options) {
24633
24313
  return;
24634
24314
  }
24635
24315
  const reminders = [
24636
- backgroundJobBoard.formatForPrompt(message.info.sessionID),
24637
- sessionManager.formatForPrompt(message.info.sessionID)
24316
+ backgroundJobBoard.formatForPrompt(message.info.sessionID)
24638
24317
  ].filter((item) => Boolean(item));
24639
24318
  if (reminders.length === 0)
24640
24319
  return;
@@ -24643,18 +24322,12 @@ function createTaskSessionManagerHook(_ctx, options) {
24643
24322
  return;
24644
24323
  if (textPart.text?.includes(SLIM_INTERNAL_INITIATOR_MARKER))
24645
24324
  return;
24646
- if (textPart.text?.includes(RESUMABLE_SESSIONS_START))
24325
+ if (textPart.text?.includes(BACKGROUND_JOB_BOARD_SENTINEL))
24647
24326
  return;
24648
24327
  rememberInjectedTerminalJobs(message.info.sessionID);
24649
- textPart.text = [
24650
- textPart.text ?? "",
24651
- "",
24652
- RESUMABLE_SESSIONS_START,
24653
- reminders.join(`
24328
+ textPart.text = [textPart.text ?? "", "", reminders.join(`
24654
24329
 
24655
- `),
24656
- RESUMABLE_SESSIONS_END
24657
- ].join(`
24330
+ `)].join(`
24658
24331
  `);
24659
24332
  return;
24660
24333
  }
@@ -24686,8 +24359,7 @@ function createTaskSessionManagerHook(_ctx, options) {
24686
24359
  const sessionId = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
24687
24360
  if (!sessionId)
24688
24361
  return;
24689
- sessionManager.dropTask(sessionId);
24690
- sessionManager.clearParent(sessionId);
24362
+ backgroundJobBoard.drop(sessionId);
24691
24363
  backgroundJobBoard.clearParent(sessionId);
24692
24364
  terminalJobsInjectedByParent.delete(sessionId);
24693
24365
  contextByTask.delete(sessionId);
@@ -24703,7 +24375,7 @@ function createTaskSessionManagerHook(_ctx, options) {
24703
24375
  };
24704
24376
  }
24705
24377
  // src/hooks/todo-continuation/index.ts
24706
- import { tool } from "@opencode-ai/plugin/tool";
24378
+ import { tool } from "@opencode-ai/plugin";
24707
24379
 
24708
24380
  // src/hooks/todo-continuation/todo-hygiene.ts
24709
24381
  var TODO_HYGIENE_REMINDER = "If the active task changed or finished, update the todo list to match the current work state.";
@@ -25402,28 +25074,197 @@ function createTodoContinuationHook(ctx, config) {
25402
25074
  output.parts.push(createInternalAgentTextPart(`[Auto-continue: enabled for up to ${maxContinuations} continuations. No incomplete todos right now.]`));
25403
25075
  }
25404
25076
  }
25405
- return {
25406
- tool: { auto_continue: autoContinue },
25407
- handleToolExecuteAfter: hygiene.handleToolExecuteAfter,
25408
- handleMessagesTransform,
25409
- handleEvent,
25410
- handleChatMessage,
25411
- handleCommandExecuteBefore
25412
- };
25077
+ return {
25078
+ tool: { auto_continue: autoContinue },
25079
+ handleToolExecuteAfter: hygiene.handleToolExecuteAfter,
25080
+ handleMessagesTransform,
25081
+ handleEvent,
25082
+ handleChatMessage,
25083
+ handleCommandExecuteBefore
25084
+ };
25085
+ }
25086
+ // src/interview/manager.ts
25087
+ import path13 from "node:path";
25088
+
25089
+ // src/interview/dashboard.ts
25090
+ import crypto from "node:crypto";
25091
+ import * as fsSync2 from "node:fs";
25092
+ import fs7 from "node:fs/promises";
25093
+ import {
25094
+ createServer
25095
+ } from "node:http";
25096
+ import os4 from "node:os";
25097
+ import path11 from "node:path";
25098
+ import { URL as URL2 } from "node:url";
25099
+
25100
+ // src/interview/document.ts
25101
+ import * as fsSync from "node:fs";
25102
+ import * as fs6 from "node:fs/promises";
25103
+ import * as path10 from "node:path";
25104
+ var DEFAULT_OUTPUT_FOLDER = "interview";
25105
+ function normalizeOutputFolder(outputFolder) {
25106
+ const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
25107
+ return normalized || DEFAULT_OUTPUT_FOLDER;
25108
+ }
25109
+ function createInterviewDirectoryPath(directory, outputFolder) {
25110
+ return path10.join(directory, normalizeOutputFolder(outputFolder));
25111
+ }
25112
+ function createInterviewFilePath(directory, outputFolder, idea) {
25113
+ const fileName = `${slugify(idea) || "interview"}.md`;
25114
+ return path10.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
25115
+ }
25116
+ function relativeInterviewPath(directory, filePath) {
25117
+ return path10.relative(directory, filePath) || path10.basename(filePath);
25118
+ }
25119
+ function resolveExistingInterviewPath(directory, outputFolder, value) {
25120
+ const trimmed = value.trim();
25121
+ if (!trimmed) {
25122
+ return null;
25123
+ }
25124
+ const outputDir = createInterviewDirectoryPath(directory, outputFolder);
25125
+ const candidates = new Set;
25126
+ const resolvedRoot = path10.resolve(directory);
25127
+ if (path10.isAbsolute(trimmed)) {
25128
+ candidates.add(trimmed);
25129
+ } else {
25130
+ candidates.add(path10.resolve(directory, trimmed));
25131
+ candidates.add(path10.join(outputDir, trimmed));
25132
+ if (!trimmed.endsWith(".md")) {
25133
+ candidates.add(path10.join(outputDir, `${trimmed}.md`));
25134
+ }
25135
+ }
25136
+ for (const candidate of candidates) {
25137
+ if (path10.extname(candidate) !== ".md") {
25138
+ continue;
25139
+ }
25140
+ const resolved = path10.resolve(candidate);
25141
+ if (!resolved.startsWith(resolvedRoot + path10.sep) && resolved !== resolvedRoot) {
25142
+ continue;
25143
+ }
25144
+ if (fsSync.existsSync(candidate)) {
25145
+ return candidate;
25146
+ }
25147
+ }
25148
+ return null;
25149
+ }
25150
+ function slugify(value) {
25151
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
25152
+ }
25153
+ function extractHistorySection(document) {
25154
+ const marker = `## Q&A history
25155
+
25156
+ `;
25157
+ const index = document.indexOf(marker);
25158
+ return index >= 0 ? document.slice(index + marker.length).trim() : "";
25159
+ }
25160
+ function extractSummarySection(document) {
25161
+ const marker = `## Current spec
25162
+
25163
+ `;
25164
+ const historyMarker = `
25165
+
25166
+ ## Q&A history`;
25167
+ const start = document.indexOf(marker);
25168
+ if (start < 0) {
25169
+ return "";
25170
+ }
25171
+ const summaryStart = start + marker.length;
25172
+ const summaryEnd = document.indexOf(historyMarker, summaryStart);
25173
+ return document.slice(summaryStart, summaryEnd >= 0 ? summaryEnd : undefined).trim();
25174
+ }
25175
+ function extractTitle(document) {
25176
+ const match = document.match(/^#\s+(.+)$/m);
25177
+ return match?.[1]?.trim() ?? "";
25178
+ }
25179
+ function buildInterviewDocument(idea, summary, history, meta) {
25180
+ const normalizedSummary = summary.trim() || "Waiting for interview answers.";
25181
+ const normalizedHistory = history.trim() || "No answers yet.";
25182
+ const frontmatter = meta?.sessionID ? [
25183
+ "---",
25184
+ `sessionID: ${meta.sessionID}`,
25185
+ `baseMessageCount: ${meta.baseMessageCount ?? 0}`,
25186
+ `updatedAt: ${new Date().toISOString()}`,
25187
+ "---",
25188
+ ""
25189
+ ].join(`
25190
+ `) : "";
25191
+ return [
25192
+ frontmatter,
25193
+ `# ${idea}`,
25194
+ "",
25195
+ "## Current spec",
25196
+ "",
25197
+ normalizedSummary,
25198
+ "",
25199
+ "## Q&A history",
25200
+ "",
25201
+ normalizedHistory,
25202
+ ""
25203
+ ].join(`
25204
+ `);
25205
+ }
25206
+ function parseFrontmatter(content) {
25207
+ const match = content.match(/^---\n([\s\S]*?)\n---\n/);
25208
+ if (!match)
25209
+ return null;
25210
+ const result = {};
25211
+ for (const line of match[1].split(`
25212
+ `)) {
25213
+ const colonIdx = line.indexOf(":");
25214
+ if (colonIdx > 0) {
25215
+ result[line.slice(0, colonIdx).trim()] = line.slice(colonIdx + 1).trim();
25216
+ }
25217
+ }
25218
+ return result;
25219
+ }
25220
+ async function ensureInterviewFile(record) {
25221
+ await fs6.mkdir(path10.dirname(record.markdownPath), { recursive: true });
25222
+ try {
25223
+ await fs6.access(record.markdownPath);
25224
+ } catch {
25225
+ await fs6.writeFile(record.markdownPath, buildInterviewDocument(record.idea, "", "", {
25226
+ sessionID: record.sessionID,
25227
+ baseMessageCount: record.baseMessageCount
25228
+ }), "utf8");
25229
+ }
25413
25230
  }
25414
- // src/interview/manager.ts
25415
- import path13 from "node:path";
25231
+ async function readInterviewDocument(record) {
25232
+ try {
25233
+ return await fs6.readFile(record.markdownPath, "utf8");
25234
+ } catch {}
25235
+ await ensureInterviewFile(record);
25236
+ return fs6.readFile(record.markdownPath, "utf8");
25237
+ }
25238
+ async function rewriteInterviewDocument(record, summary) {
25239
+ const existing = await readInterviewDocument(record);
25240
+ const history = extractHistorySection(existing);
25241
+ const next = buildInterviewDocument(record.idea, summary, history, {
25242
+ sessionID: record.sessionID,
25243
+ baseMessageCount: record.baseMessageCount
25244
+ });
25245
+ await fs6.writeFile(record.markdownPath, next, "utf8");
25246
+ return next;
25247
+ }
25248
+ async function appendInterviewAnswers(record, questions, answers) {
25249
+ const existing = await readInterviewDocument(record);
25250
+ const summary = extractSummarySection(existing);
25251
+ const history = extractHistorySection(existing);
25252
+ const questionMap = new Map(questions.map((question) => [question.id, question]));
25253
+ const appended = answers.map((answer) => {
25254
+ const question = questionMap.get(answer.questionId);
25255
+ return question ? `Q: ${question.question}
25256
+ A: ${answer.answer.trim()}` : null;
25257
+ }).filter((value) => value !== null).join(`
25416
25258
 
25417
- // src/interview/dashboard.ts
25418
- import crypto from "node:crypto";
25419
- import * as fsSync2 from "node:fs";
25420
- import fs8 from "node:fs/promises";
25421
- import {
25422
- createServer
25423
- } from "node:http";
25424
- import os4 from "node:os";
25425
- import path11 from "node:path";
25426
- import { URL as URL2 } from "node:url";
25259
+ `);
25260
+ const nextHistory = [history === "No answers yet." ? "" : history, appended].filter(Boolean).join(`
25261
+
25262
+ `);
25263
+ await fs6.writeFile(record.markdownPath, buildInterviewDocument(record.idea, summary, nextHistory, {
25264
+ sessionID: record.sessionID,
25265
+ baseMessageCount: record.baseMessageCount
25266
+ }), "utf8");
25267
+ }
25427
25268
 
25428
25269
  // src/interview/helpers.ts
25429
25270
  function sendJson(response, status, value) {
@@ -27069,7 +26910,7 @@ function removeAuthFile(port) {
27069
26910
  }
27070
26911
  async function readDashboardAuthFile(port) {
27071
26912
  try {
27072
- const content = await fs8.readFile(getAuthFilePath(port), "utf8");
26913
+ const content = await fs7.readFile(getAuthFilePath(port), "utf8");
27073
26914
  const data = JSON.parse(content);
27074
26915
  try {
27075
26916
  process.kill(data.pid, 0);
@@ -27192,7 +27033,7 @@ function createDashboardServer(config) {
27192
27033
  const interviewDir = path11.join(dir, config.outputFolder);
27193
27034
  let entries;
27194
27035
  try {
27195
- entries = await fs8.readdir(interviewDir);
27036
+ entries = await fs7.readdir(interviewDir);
27196
27037
  } catch {
27197
27038
  continue;
27198
27039
  }
@@ -27201,7 +27042,7 @@ function createDashboardServer(config) {
27201
27042
  continue;
27202
27043
  let content;
27203
27044
  try {
27204
- content = await fs8.readFile(path11.join(interviewDir, entry), "utf8");
27045
+ content = await fs7.readFile(path11.join(interviewDir, entry), "utf8");
27205
27046
  } catch {
27206
27047
  continue;
27207
27048
  }
@@ -27230,7 +27071,7 @@ function createDashboardServer(config) {
27230
27071
  const interviewDir = path11.join(dir, config.outputFolder);
27231
27072
  let entries;
27232
27073
  try {
27233
- entries = await fs8.readdir(interviewDir);
27074
+ entries = await fs7.readdir(interviewDir);
27234
27075
  } catch {
27235
27076
  continue;
27236
27077
  }
@@ -27239,7 +27080,7 @@ function createDashboardServer(config) {
27239
27080
  continue;
27240
27081
  let content;
27241
27082
  try {
27242
- content = await fs8.readFile(path11.join(interviewDir, entry), "utf8");
27083
+ content = await fs7.readFile(path11.join(interviewDir, entry), "utf8");
27243
27084
  } catch {
27244
27085
  continue;
27245
27086
  }
@@ -27523,7 +27364,7 @@ function createDashboardServer(config) {
27523
27364
  let markdownPath = entry.filePath;
27524
27365
  if (entry.filePath) {
27525
27366
  try {
27526
- document = await fs8.readFile(entry.filePath, "utf8");
27367
+ document = await fs7.readFile(entry.filePath, "utf8");
27527
27368
  } catch {}
27528
27369
  } else {
27529
27370
  const dirs = getKnownDirectories();
@@ -27531,7 +27372,7 @@ function createDashboardServer(config) {
27531
27372
  const slug = extractResumeSlug(interviewId);
27532
27373
  const candidate = path11.join(dir, config.outputFolder, `${slug}.md`);
27533
27374
  try {
27534
- document = await fs8.readFile(candidate, "utf8");
27375
+ document = await fs7.readFile(candidate, "utf8");
27535
27376
  markdownPath = candidate;
27536
27377
  entry.filePath = candidate;
27537
27378
  break;
@@ -28056,7 +27897,7 @@ function createInterviewServer(deps) {
28056
27897
 
28057
27898
  // src/interview/service.ts
28058
27899
  import { spawn as spawn2 } from "node:child_process";
28059
- import * as fs9 from "node:fs/promises";
27900
+ import * as fs8 from "node:fs/promises";
28060
27901
  import * as path12 from "node:path";
28061
27902
 
28062
27903
  // src/interview/types.ts
@@ -28331,11 +28172,11 @@ function createInterviewService(ctx, config, deps) {
28331
28172
  const dir = path12.dirname(interview.markdownPath);
28332
28173
  const newPath = path12.join(dir, `${newSlug}.md`);
28333
28174
  try {
28334
- await fs9.access(newPath);
28175
+ await fs8.access(newPath);
28335
28176
  return;
28336
28177
  } catch {}
28337
28178
  try {
28338
- await fs9.rename(interview.markdownPath, newPath);
28179
+ await fs8.rename(interview.markdownPath, newPath);
28339
28180
  interview.markdownPath = newPath;
28340
28181
  log("[interview] renamed file with assistant title:", {
28341
28182
  from: currentFileName,
@@ -28401,7 +28242,7 @@ function createInterviewService(ctx, config, deps) {
28401
28242
  active.status = "abandoned";
28402
28243
  }
28403
28244
  }
28404
- const document = await fs9.readFile(markdownPath, "utf8");
28245
+ const document = await fs8.readFile(markdownPath, "utf8");
28405
28246
  const messages = await loadMessages(sessionID);
28406
28247
  const title = extractTitle(document);
28407
28248
  const record = {
@@ -28573,7 +28414,7 @@ function createInterviewService(ctx, config, deps) {
28573
28414
  const resumePath = resolveExistingInterviewPath(ctx.directory, outputFolder, idea);
28574
28415
  if (resumePath) {
28575
28416
  const interview2 = await resumeInterview(input.sessionID, resumePath);
28576
- const document = await fs9.readFile(interview2.markdownPath, "utf8");
28417
+ const document = await fs8.readFile(interview2.markdownPath, "utf8");
28577
28418
  await notifyInterviewUrl(input.sessionID, interview2);
28578
28419
  output.parts.push(createInternalAgentTextPart(buildResumePrompt(document, maxQuestions)));
28579
28420
  return;
@@ -28637,7 +28478,7 @@ function createInterviewService(ctx, config, deps) {
28637
28478
  const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) => path12.resolve(i.markdownPath)));
28638
28479
  let entries;
28639
28480
  try {
28640
- entries = await fs9.readdir(outputDir);
28481
+ entries = await fs8.readdir(outputDir);
28641
28482
  } catch {
28642
28483
  return [];
28643
28484
  }
@@ -28650,7 +28491,7 @@ function createInterviewService(ctx, config, deps) {
28650
28491
  continue;
28651
28492
  let content;
28652
28493
  try {
28653
- content = await fs9.readFile(fullPath, "utf8");
28494
+ content = await fs8.readFile(fullPath, "utf8");
28654
28495
  } catch {
28655
28496
  continue;
28656
28497
  }
@@ -29721,7 +29562,6 @@ function startAvailabilityCheck(config) {
29721
29562
  }
29722
29563
  }
29723
29564
  // src/multiplexer/session-manager.ts
29724
- var SESSION_TIMEOUT_MS = 10 * 60 * 1000;
29725
29565
  var SESSION_MISSING_GRACE_MS = POLL_INTERVAL_BACKGROUND_MS * 3;
29726
29566
  var SHARED_STATE_KEY = Symbol.for("oh-my-opencode-slim.multiplexer-session-manager.state");
29727
29567
  function getSharedState() {
@@ -29735,6 +29575,7 @@ function getSharedState() {
29735
29575
  return globalWithState[SHARED_STATE_KEY];
29736
29576
  }
29737
29577
  class MultiplexerSessionManager {
29578
+ backgroundJobBoard;
29738
29579
  instanceId = Math.random().toString(36).slice(2, 8);
29739
29580
  serverUrl;
29740
29581
  directory;
@@ -29745,7 +29586,8 @@ class MultiplexerSessionManager {
29745
29586
  closingSessions;
29746
29587
  pollInterval;
29747
29588
  enabled = false;
29748
- constructor(ctx, config) {
29589
+ constructor(ctx, config, backgroundJobBoard) {
29590
+ this.backgroundJobBoard = backgroundJobBoard;
29749
29591
  const sharedState = getSharedState();
29750
29592
  this.sessions = sharedState.sessions;
29751
29593
  this.knownSessions = sharedState.knownSessions;
@@ -29840,7 +29682,8 @@ class MultiplexerSessionManager {
29840
29682
  title,
29841
29683
  directory,
29842
29684
  createdAt: now,
29843
- lastSeenAt: now
29685
+ lastSeenAt: now,
29686
+ seenInStatus: false
29844
29687
  });
29845
29688
  log("[multiplexer-session-manager] pane spawned", {
29846
29689
  instanceId: this.instanceId,
@@ -29932,16 +29775,26 @@ class MultiplexerSessionManager {
29932
29775
  const isIdle = status?.type === "idle";
29933
29776
  if (status) {
29934
29777
  tracked.lastSeenAt = now;
29778
+ tracked.seenInStatus = true;
29935
29779
  tracked.missingSince = undefined;
29936
29780
  } else if (!tracked.missingSince) {
29937
29781
  tracked.missingSince = now;
29938
29782
  }
29939
29783
  const missingTooLong = !!tracked.missingSince && now - tracked.missingSince >= SESSION_MISSING_GRACE_MS;
29940
- const isTimedOut = now - tracked.createdAt > SESSION_TIMEOUT_MS;
29941
- if (isIdle || missingTooLong || isTimedOut) {
29784
+ const shouldKeepRunningBackgroundJob = missingTooLong && this.isRunningBackgroundJob(sessionId);
29785
+ if (isIdle || missingTooLong) {
29786
+ if (shouldKeepRunningBackgroundJob) {
29787
+ log("[multiplexer-session-manager] keeping running background pane", {
29788
+ instanceId: this.instanceId,
29789
+ sessionId,
29790
+ paneId: tracked.paneId,
29791
+ seenInStatus: tracked.seenInStatus
29792
+ });
29793
+ continue;
29794
+ }
29942
29795
  sessionsToClose.push({
29943
29796
  sessionId,
29944
- reason: isIdle ? "idle" : isTimedOut ? "timeout" : "missing"
29797
+ reason: isIdle ? "idle" : "missing"
29945
29798
  });
29946
29799
  }
29947
29800
  }
@@ -29958,7 +29811,15 @@ class MultiplexerSessionManager {
29958
29811
  if (!response.ok) {
29959
29812
  throw new Error(`session status request failed: ${response.status} ${response.statusText}`);
29960
29813
  }
29961
- return await response.json();
29814
+ const body = await response.text();
29815
+ if (body.trim() === "") {
29816
+ throw new Error("session status response was empty");
29817
+ }
29818
+ try {
29819
+ return JSON.parse(body);
29820
+ } catch (err) {
29821
+ throw new Error(`session status response was not valid JSON: ${err}`);
29822
+ }
29962
29823
  }
29963
29824
  async closeSession(sessionId, reason) {
29964
29825
  if (reason === "deleted") {
@@ -30058,7 +29919,8 @@ class MultiplexerSessionManager {
30058
29919
  title: known.title,
30059
29920
  directory: known.directory,
30060
29921
  createdAt: now,
30061
- lastSeenAt: now
29922
+ lastSeenAt: now,
29923
+ seenInStatus: false
30062
29924
  });
30063
29925
  log("[multiplexer-session-manager] pane respawned on busy", {
30064
29926
  instanceId: this.instanceId,
@@ -30083,6 +29945,9 @@ class MultiplexerSessionManager {
30083
29945
  getSessionId(event) {
30084
29946
  return event.properties?.info?.id ?? event.properties?.sessionID;
30085
29947
  }
29948
+ isRunningBackgroundJob(sessionId) {
29949
+ return this.backgroundJobBoard?.get(sessionId)?.state === "running";
29950
+ }
30086
29951
  async cleanup() {
30087
29952
  this.stopPolling();
30088
29953
  if (this.closingSessions.size > 0) {
@@ -30128,7 +29993,7 @@ async function isServerRunning(serverUrl, timeoutMs = 3000, maxAttempts = 2) {
30128
29993
  return false;
30129
29994
  }
30130
29995
  // src/tools/ast-grep/tools.ts
30131
- import { tool as tool2 } from "@opencode-ai/plugin/tool";
29996
+ import { tool as tool2 } from "@opencode-ai/plugin";
30132
29997
 
30133
29998
  // src/tools/ast-grep/cli.ts
30134
29999
  import { existsSync as existsSync9 } from "node:fs";
@@ -30673,11 +30538,96 @@ var ast_grep_replace = tool2({
30673
30538
  }
30674
30539
  }
30675
30540
  });
30676
- // src/tools/council.ts
30541
+ // src/tools/cancel-task.ts
30677
30542
  import {
30678
30543
  tool as tool3
30679
30544
  } from "@opencode-ai/plugin";
30680
30545
  var z4 = tool3.schema;
30546
+ function createCancelTaskTool(options) {
30547
+ const cancel_task = tool3({
30548
+ description: `Cancel a tracked background specialist task.
30549
+
30550
+ Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accepts either the native task_id/session ID or the parent-scoped alias shown in the Background Job Board. Cancellation is not rollback: if cancelling a writer, inspect and reconcile partial file changes before replacing the lane.`,
30551
+ args: {
30552
+ task_id: z4.string().describe("Tracked background task ID or Background Job Board alias"),
30553
+ reason: z4.string().optional().describe("Short cancellation reason")
30554
+ },
30555
+ async execute(args, toolContext) {
30556
+ const parentSessionID = toolContext?.sessionID;
30557
+ if (!parentSessionID)
30558
+ throw new Error("cancel_task requires sessionID");
30559
+ if (toolContext.agent && toolContext.agent !== "orchestrator") {
30560
+ throw new Error("cancel_task can only be used by orchestrator");
30561
+ }
30562
+ if (!options.shouldManageSession(parentSessionID)) {
30563
+ throw new Error("cancel_task can only be used in orchestrator sessions");
30564
+ }
30565
+ const requested = args.task_id.trim();
30566
+ if (!requested)
30567
+ throw new Error("cancel_task requires task_id");
30568
+ const job = options.backgroundJobBoard.resolve(parentSessionID, requested);
30569
+ if (!job) {
30570
+ return [
30571
+ `task_id: ${requested}`,
30572
+ "state: unknown",
30573
+ "",
30574
+ "<task_error>",
30575
+ "unknown or unowned background task",
30576
+ "</task_error>"
30577
+ ].join(`
30578
+ `);
30579
+ }
30580
+ if (job.state !== "running") {
30581
+ return [
30582
+ `task_id: ${job.taskID}`,
30583
+ `state: ${job.state}`,
30584
+ "",
30585
+ "<task_result>",
30586
+ `not cancelled: task is already ${job.state}`,
30587
+ "</task_result>"
30588
+ ].join(`
30589
+ `);
30590
+ }
30591
+ try {
30592
+ await abortSessionWithTimeout(options.client, job.taskID, options.abortTimeoutMs ?? 1e4);
30593
+ } catch (error) {
30594
+ const timedOut = error instanceof OperationTimeoutError;
30595
+ if (timedOut) {
30596
+ options.backgroundJobBoard.updateStatus({
30597
+ taskID: job.taskID,
30598
+ state: "error",
30599
+ resultSummary: error instanceof Error ? error.message : "cancel request timed out"
30600
+ });
30601
+ }
30602
+ return [
30603
+ `task_id: ${job.taskID}`,
30604
+ `state: ${timedOut ? "error" : "cancel_error"}`,
30605
+ "",
30606
+ "<task_error>",
30607
+ error instanceof Error ? error.message : String(error),
30608
+ "</task_error>"
30609
+ ].join(`
30610
+ `);
30611
+ }
30612
+ const cancelled = options.backgroundJobBoard.markCancelled(job.taskID, args.reason);
30613
+ return [
30614
+ `task_id: ${job.taskID}`,
30615
+ `state: ${cancelled?.state ?? "cancelled"}`,
30616
+ "",
30617
+ "<task_error>",
30618
+ cancelled?.resultSummary ?? "cancelled",
30619
+ "</task_error>"
30620
+ ].join(`
30621
+ `);
30622
+ }
30623
+ });
30624
+ return { cancel_task };
30625
+ }
30626
+ // src/tools/council.ts
30627
+ import {
30628
+ tool as tool4
30629
+ } from "@opencode-ai/plugin";
30630
+ var z5 = tool4.schema;
30681
30631
  function formatModelComposition(councillorResults) {
30682
30632
  return councillorResults.map((cr) => {
30683
30633
  const shortModel = shortModelLabel(cr.model);
@@ -30685,15 +30635,15 @@ function formatModelComposition(councillorResults) {
30685
30635
  }).join(", ");
30686
30636
  }
30687
30637
  function createCouncilTool(_ctx, councilManager) {
30688
- const council_session = tool3({
30638
+ const council_session = tool4({
30689
30639
  description: `Launch a multi-LLM council session for consensus-based analysis.
30690
30640
 
30691
30641
  Sends the prompt to multiple models (councillors) in parallel and returns their formatted responses for you to synthesize.
30692
30642
 
30693
30643
  Returns the councillor responses with a summary footer.`,
30694
30644
  args: {
30695
- prompt: z4.string().describe("The prompt to send to all councillors"),
30696
- preset: z4.string().optional().describe('Council preset to use (default: "default"). Must match a preset in the council config.')
30645
+ prompt: z5.string().describe("The prompt to send to all councillors"),
30646
+ preset: z5.string().optional().describe('Council preset to use (default: "default"). Must match a preset in the council config.')
30697
30647
  },
30698
30648
  async execute(args, toolContext) {
30699
30649
  if (!toolContext || typeof toolContext !== "object" || !("sessionID" in toolContext)) {
@@ -30740,7 +30690,7 @@ Returns the councillor responses with a summary footer.`,
30740
30690
  return { council_session };
30741
30691
  }
30742
30692
  // src/tui-state.ts
30743
- import * as fs10 from "node:fs";
30693
+ import * as fs9 from "node:fs";
30744
30694
  import * as os5 from "node:os";
30745
30695
  import * as path14 from "node:path";
30746
30696
  var STATE_DIR = "oh-my-opencode-slim";
@@ -30770,14 +30720,14 @@ function parseSnapshot(value) {
30770
30720
  }
30771
30721
  function readTuiSnapshot() {
30772
30722
  try {
30773
- return parseSnapshot(fs10.readFileSync(getTuiStatePath(), "utf8"));
30723
+ return parseSnapshot(fs9.readFileSync(getTuiStatePath(), "utf8"));
30774
30724
  } catch {
30775
30725
  return emptySnapshot();
30776
30726
  }
30777
30727
  }
30778
30728
  async function readTuiSnapshotAsync() {
30779
30729
  try {
30780
- return parseSnapshot(await fs10.promises.readFile(getTuiStatePath(), "utf8"));
30730
+ return parseSnapshot(await fs9.promises.readFile(getTuiStatePath(), "utf8"));
30781
30731
  } catch {
30782
30732
  return emptySnapshot();
30783
30733
  }
@@ -30785,8 +30735,8 @@ async function readTuiSnapshotAsync() {
30785
30735
  function writeTuiSnapshot(snapshot) {
30786
30736
  try {
30787
30737
  const filePath = getTuiStatePath();
30788
- fs10.mkdirSync(path14.dirname(filePath), { recursive: true });
30789
- fs10.writeFileSync(filePath, `${JSON.stringify(snapshot)}
30738
+ fs9.mkdirSync(path14.dirname(filePath), { recursive: true });
30739
+ fs9.writeFileSync(filePath, `${JSON.stringify(snapshot)}
30790
30740
  `);
30791
30741
  } catch {}
30792
30742
  }
@@ -31004,7 +30954,7 @@ var WEBFETCH_DESCRIPTION = "Fetch a URL with better extraction for static/docs p
31004
30954
  import os6 from "node:os";
31005
30955
  import path18 from "node:path";
31006
30956
  import {
31007
- tool as tool4
30957
+ tool as tool5
31008
30958
  } from "@opencode-ai/plugin";
31009
30959
 
31010
30960
  // src/tools/smartfetch/binary.ts
@@ -31172,7 +31122,7 @@ var M = class u2 {
31172
31122
  return this.#S;
31173
31123
  }
31174
31124
  constructor(e) {
31175
- let { max: t = 0, ttl: i, ttlResolution: s = 1, ttlAutopurge: n, updateAgeOnGet: o, updateAgeOnHas: r, allowStale: h, dispose: l, onInsert: c, disposeAfter: f, noDisposeOnSet: g, noUpdateTTL: p, maxSize: T = 0, maxEntrySize: w = 0, sizeCalculation: y, fetchMethod: a, memoMethod: m, noDeleteOnFetchRejection: _, noDeleteOnStaleGet: b, allowStaleOnFetchRejection: d, allowStaleOnFetchAbort: A, ignoreFetchAbort: z5, perf: x } = e;
31125
+ let { max: t = 0, ttl: i, ttlResolution: s = 1, ttlAutopurge: n, updateAgeOnGet: o, updateAgeOnHas: r, allowStale: h, dispose: l, onInsert: c, disposeAfter: f, noDisposeOnSet: g, noUpdateTTL: p, maxSize: T = 0, maxEntrySize: w = 0, sizeCalculation: y, fetchMethod: a, memoMethod: m, noDeleteOnFetchRejection: _, noDeleteOnStaleGet: b, allowStaleOnFetchRejection: d, allowStaleOnFetchAbort: A, ignoreFetchAbort: z6, perf: x } = e;
31176
31126
  if (x !== undefined && typeof x?.now != "function")
31177
31127
  throw new TypeError("perf option must have a now() method if specified");
31178
31128
  if (this.#m = x ?? C, t !== 0 && !F(t))
@@ -31190,7 +31140,7 @@ var M = class u2 {
31190
31140
  throw new TypeError("memoMethod must be a function if defined");
31191
31141
  if (this.#U = m, a !== undefined && typeof a != "function")
31192
31142
  throw new TypeError("fetchMethod must be a function if specified");
31193
- if (this.#M = a, this.#W = !!a, this.#s = new Map, this.#i = Array.from({ length: t }).fill(undefined), this.#t = Array.from({ length: t }).fill(undefined), this.#a = new v(t), this.#c = new v(t), this.#l = 0, this.#h = 0, this.#y = R.create(t), this.#n = 0, this.#b = 0, typeof l == "function" && (this.#w = l), typeof c == "function" && (this.#x = c), typeof f == "function" ? (this.#S = f, this.#r = []) : (this.#S = undefined, this.#r = undefined), this.#T = !!this.#w, this.#j = !!this.#x, this.#f = !!this.#S, this.noDisposeOnSet = !!g, this.noUpdateTTL = !!p, this.noDeleteOnFetchRejection = !!_, this.allowStaleOnFetchRejection = !!d, this.allowStaleOnFetchAbort = !!A, this.ignoreFetchAbort = !!z5, this.maxEntrySize !== 0) {
31143
+ if (this.#M = a, this.#W = !!a, this.#s = new Map, this.#i = Array.from({ length: t }).fill(undefined), this.#t = Array.from({ length: t }).fill(undefined), this.#a = new v(t), this.#c = new v(t), this.#l = 0, this.#h = 0, this.#y = R.create(t), this.#n = 0, this.#b = 0, typeof l == "function" && (this.#w = l), typeof c == "function" && (this.#x = c), typeof f == "function" ? (this.#S = f, this.#r = []) : (this.#S = undefined, this.#r = undefined), this.#T = !!this.#w, this.#j = !!this.#x, this.#f = !!this.#S, this.noDisposeOnSet = !!g, this.noUpdateTTL = !!p, this.noDeleteOnFetchRejection = !!_, this.allowStaleOnFetchRejection = !!d, this.allowStaleOnFetchAbort = !!A, this.ignoreFetchAbort = !!z6, this.maxEntrySize !== 0) {
31194
31144
  if (this.#u !== 0 && !F(this.#u))
31195
31145
  throw new TypeError("maxSize must be a positive integer if specified");
31196
31146
  if (!F(this.maxEntrySize))
@@ -31570,8 +31520,8 @@ var M = class u2 {
31570
31520
  let A = this.#p(b);
31571
31521
  if (!y && !A)
31572
31522
  return a && (a.fetch = "hit"), this.#L(b), s && this.#D(b), a && this.#E(a, b), d;
31573
- let z5 = this.#P(e, b, _, w), v = z5.__staleWhileFetching !== undefined && i;
31574
- return a && (a.fetch = A ? "stale" : "refresh", v && A && (a.returnedStale = true)), v ? z5.__staleWhileFetching : z5.__returned = z5;
31523
+ let z6 = this.#P(e, b, _, w), v = z6.__staleWhileFetching !== undefined && i;
31524
+ return a && (a.fetch = A ? "stale" : "refresh", v && A && (a.returnedStale = true)), v ? z6.__staleWhileFetching : z6.__returned = z6;
31575
31525
  }
31576
31526
  }
31577
31527
  forceFetch(e, t = {}) {
@@ -31785,7 +31735,7 @@ function extractStructuredText(root) {
31785
31735
  ]);
31786
31736
  const isText = (node) => node.nodeType === node.TEXT_NODE;
31787
31737
  const isElement = (node) => node.nodeType === node.ELEMENT_NODE;
31788
- const pushText2 = (value) => {
31738
+ const pushText = (value) => {
31789
31739
  const normalized = value.replace(/\s+/g, " ");
31790
31740
  if (!normalized.trim())
31791
31741
  return;
@@ -31811,7 +31761,7 @@ function extractStructuredText(root) {
31811
31761
  };
31812
31762
  const visit = (node) => {
31813
31763
  if (isText(node)) {
31814
- pushText2(node.textContent || "");
31764
+ pushText(node.textContent || "");
31815
31765
  return;
31816
31766
  }
31817
31767
  if (!isElement(node))
@@ -32583,7 +32533,7 @@ function isInvalidLlmsResult(fetchResult) {
32583
32533
 
32584
32534
  // src/tools/smartfetch/secondary-model.ts
32585
32535
  import { existsSync as existsSync10 } from "node:fs";
32586
- import { readFile as readFile5 } from "node:fs/promises";
32536
+ import { readFile as readFile4 } from "node:fs/promises";
32587
32537
  import path17 from "node:path";
32588
32538
  function parseModelRef(value) {
32589
32539
  if (!value)
@@ -32620,7 +32570,7 @@ async function readOpenCodeConfigFile(configPath) {
32620
32570
  if (!configPath)
32621
32571
  return;
32622
32572
  try {
32623
- const content = await readFile5(configPath, "utf8");
32573
+ const content = await readFile4(configPath, "utf8");
32624
32574
  return JSON.parse(stripJsonComments(content));
32625
32575
  } catch {
32626
32576
  return;
@@ -32786,20 +32736,20 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
32786
32736
  }
32787
32737
 
32788
32738
  // src/tools/smartfetch/tool.ts
32789
- var z5 = tool4.schema;
32739
+ var z6 = tool5.schema;
32790
32740
  function createWebfetchTool(pluginCtx, options = {}) {
32791
32741
  const binaryDir = options.binaryDir || path18.join(os6.tmpdir(), "opencode-smartfetch");
32792
- return tool4({
32742
+ return tool5({
32793
32743
  description: WEBFETCH_DESCRIPTION,
32794
32744
  args: {
32795
- url: z5.httpUrl(),
32796
- format: z5.enum(["text", "markdown", "html"]).default("markdown"),
32797
- timeout: z5.number().positive().max(MAX_TIMEOUT_SECONDS).optional().describe("Timeout in seconds, max 120."),
32798
- prompt: z5.string().optional().describe("Optional extraction task to run on the fetched content using a cheap secondary model."),
32799
- extract_main: z5.boolean().default(true),
32800
- prefer_llms_txt: z5.enum(["auto", "always", "never"]).default("auto"),
32801
- include_metadata: z5.boolean().default(true),
32802
- save_binary: z5.boolean().default(false).describe("Save binary payload to disk when it fits within the active download limit.")
32745
+ url: z6.httpUrl(),
32746
+ format: z6.enum(["text", "markdown", "html"]).default("markdown"),
32747
+ timeout: z6.number().positive().max(MAX_TIMEOUT_SECONDS).optional().describe("Timeout in seconds, max 120."),
32748
+ prompt: z6.string().optional().describe("Optional extraction task to run on the fetched content using a cheap secondary model."),
32749
+ extract_main: z6.boolean().default(true),
32750
+ prefer_llms_txt: z6.enum(["auto", "always", "never"]).default("auto"),
32751
+ include_metadata: z6.boolean().default(true),
32752
+ save_binary: z6.boolean().default(false).describe("Save binary payload to disk when it fits within the active download limit.")
32803
32753
  },
32804
32754
  async execute(args, ctx) {
32805
32755
  const secondaryModels = await readSecondaryModelFromConfig(ctx.directory || pluginCtx.directory);
@@ -33269,460 +33219,6 @@ function createWebfetchTool(pluginCtx, options = {}) {
33269
33219
  }
33270
33220
  });
33271
33221
  }
33272
- // src/tools/subtask/command.ts
33273
- var COMMAND_NAME5 = "subtask";
33274
- var SUBTASK_COMMAND_TEMPLATE = `Start a focused subtask worker.
33275
-
33276
- The user's request below is the full scope for the worker. Do not broaden it.
33277
- Create a self-contained worker prompt that includes:
33278
- - the exact objective
33279
- - relevant context from this conversation
33280
- - specific files/paths that matter
33281
- - expected deliverables
33282
- - validation the worker should run, if applicable
33283
-
33284
- USER REQUEST:
33285
- $ARGUMENTS
33286
-
33287
- Then call the subtask tool:
33288
- \`subtask(prompt="...", files=["src/foo.ts", "docs/bar.md"])\`
33289
-
33290
- Only include files that are clearly relevant. If no files are needed, omit files.`;
33291
- function createSubtaskCommandManager(_ctx, state) {
33292
- function registerCommand(opencodeConfig) {
33293
- const configCommand = opencodeConfig.command;
33294
- if (!configCommand?.[COMMAND_NAME5]) {
33295
- if (!opencodeConfig.command) {
33296
- opencodeConfig.command = {};
33297
- }
33298
- opencodeConfig.command[COMMAND_NAME5] = {
33299
- description: "Create a focused subtask prompt for a new session",
33300
- template: SUBTASK_COMMAND_TEMPLATE
33301
- };
33302
- }
33303
- }
33304
- return {
33305
- registerCommand,
33306
- handleEvent(input) {
33307
- if (input.event.type === "session.created") {
33308
- const info = input.event.properties?.info;
33309
- if (!info?.id || !info.parentID)
33310
- return;
33311
- const source = state.sourceFor(info.parentID);
33312
- if (source)
33313
- state.markSession(info.id, source);
33314
- return;
33315
- }
33316
- if (input.event.type !== "session.deleted")
33317
- return;
33318
- const sessionID = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
33319
- if (sessionID)
33320
- state.unmarkSession(sessionID);
33321
- }
33322
- };
33323
- }
33324
- // src/tools/subtask/files.ts
33325
- import * as fs12 from "node:fs/promises";
33326
- import * as path20 from "node:path";
33327
-
33328
- // src/tools/subtask/vendor.ts
33329
- import * as fs11 from "node:fs/promises";
33330
- import * as path19 from "node:path";
33331
- var DEFAULT_READ_LIMIT = 2000;
33332
- var MAX_LINE_LENGTH = 2000;
33333
- var MAX_BYTES = 50 * 1024;
33334
- var MAX_LINE_SUFFIX = `... (line truncated to ${MAX_LINE_LENGTH} chars)`;
33335
- var MAX_BYTES_LABEL = `${MAX_BYTES / 1024} KB`;
33336
- var SAMPLE_BYTES = 4096;
33337
- var BINARY_EXTENSIONS = new Set([
33338
- ".zip",
33339
- ".tar",
33340
- ".gz",
33341
- ".exe",
33342
- ".dll",
33343
- ".so",
33344
- ".class",
33345
- ".jar",
33346
- ".war",
33347
- ".7z",
33348
- ".doc",
33349
- ".docx",
33350
- ".xls",
33351
- ".xlsx",
33352
- ".ppt",
33353
- ".pptx",
33354
- ".odt",
33355
- ".ods",
33356
- ".odp",
33357
- ".bin",
33358
- ".dat",
33359
- ".obj",
33360
- ".o",
33361
- ".a",
33362
- ".lib",
33363
- ".wasm",
33364
- ".pyc",
33365
- ".pyo"
33366
- ]);
33367
- async function isBinaryFile(filepath) {
33368
- const ext = path19.extname(filepath).toLowerCase();
33369
- if (BINARY_EXTENSIONS.has(ext)) {
33370
- return true;
33371
- }
33372
- try {
33373
- const file = await fs11.open(filepath, "r");
33374
- try {
33375
- const buffer = Buffer.alloc(SAMPLE_BYTES);
33376
- const result = await file.read(buffer, 0, SAMPLE_BYTES, 0);
33377
- if (result.bytesRead === 0)
33378
- return false;
33379
- const bytes = buffer.subarray(0, result.bytesRead);
33380
- let nonPrintableCount = 0;
33381
- for (let i = 0;i < bytes.length; i++) {
33382
- const byte = bytes[i];
33383
- if (byte === undefined)
33384
- continue;
33385
- if (byte === 0)
33386
- return true;
33387
- if (byte < 9 || byte > 13 && byte < 32) {
33388
- nonPrintableCount++;
33389
- }
33390
- }
33391
- return nonPrintableCount / bytes.length > 0.3;
33392
- } finally {
33393
- await file.close();
33394
- }
33395
- } catch {
33396
- return false;
33397
- }
33398
- }
33399
- function formatFileContent(_filepath, content) {
33400
- const cappedContent = Buffer.byteLength(content, "utf8") > MAX_BYTES;
33401
- const contentToFormat = cappedContent ? content.slice(0, MAX_BYTES) : content;
33402
- const lines = contentToFormat.split(`
33403
- `);
33404
- const limit = DEFAULT_READ_LIMIT;
33405
- const offset = 0;
33406
- const raw = lines.slice(offset, offset + limit).map((line) => {
33407
- return line.length > MAX_LINE_LENGTH ? `${line.substring(0, MAX_LINE_LENGTH)}${MAX_LINE_SUFFIX}` : line;
33408
- });
33409
- const formatted = raw.map((line, index) => {
33410
- return `${index + offset + 1}: ${line}`;
33411
- });
33412
- let output = [
33413
- `<path>${_filepath}</path>`,
33414
- "<type>file</type>",
33415
- `<content>
33416
- `
33417
- ].join(`
33418
- `);
33419
- output += formatted.join(`
33420
- `);
33421
- const totalLines = lines.length;
33422
- const lastReadLine = offset + formatted.length;
33423
- const hasMoreLines = totalLines > lastReadLine;
33424
- if (cappedContent) {
33425
- output += `
33426
-
33427
- (Output capped at ${MAX_BYTES_LABEL}. Showing lines 1-${lastReadLine}. Use offset=${lastReadLine + 1} to continue.)`;
33428
- } else if (hasMoreLines) {
33429
- output += `
33430
-
33431
- (Showing lines 1-${lastReadLine} of ${totalLines}. Use offset=${lastReadLine + 1} to continue.)`;
33432
- } else {
33433
- output += `
33434
-
33435
- (End of file - total ${totalLines} lines)`;
33436
- }
33437
- output += `
33438
- </content>`;
33439
- return output;
33440
- }
33441
-
33442
- // src/tools/subtask/files.ts
33443
- var FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g;
33444
- var TRAILING_PATH_PUNCTUATION = /[!?:;]+$/;
33445
- function cleanFileReference(ref) {
33446
- return ref.replace(/^@/, "").replace(TRAILING_PATH_PUNCTUATION, "");
33447
- }
33448
- function parseFileReferences(text) {
33449
- const fileRefs = new Set;
33450
- for (const match of text.matchAll(FILE_REGEX)) {
33451
- if (match[1]) {
33452
- fileRefs.add(cleanFileReference(match[1]));
33453
- }
33454
- }
33455
- return fileRefs;
33456
- }
33457
- async function buildSyntheticFileParts(directory, refs) {
33458
- const parts = [];
33459
- const realDirectory = await fs12.realpath(directory);
33460
- for (const ref of refs) {
33461
- const filepath = path20.resolve(directory, ref);
33462
- const relative3 = path20.relative(directory, filepath);
33463
- if (relative3.startsWith("..") || path20.isAbsolute(relative3))
33464
- continue;
33465
- try {
33466
- const realFilepath = await fs12.realpath(filepath);
33467
- const realRelative = path20.relative(realDirectory, realFilepath);
33468
- if (realRelative.startsWith("..") || path20.isAbsolute(realRelative)) {
33469
- continue;
33470
- }
33471
- const stats = await fs12.stat(realFilepath);
33472
- if (!stats.isFile())
33473
- continue;
33474
- if (await isBinaryFile(realFilepath))
33475
- continue;
33476
- const content = await fs12.readFile(realFilepath, "utf-8");
33477
- parts.push({
33478
- type: "text",
33479
- synthetic: true,
33480
- text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: realFilepath })}`
33481
- });
33482
- parts.push({
33483
- type: "text",
33484
- synthetic: true,
33485
- text: formatFileContent(realFilepath, content)
33486
- });
33487
- } catch {}
33488
- }
33489
- return parts;
33490
- }
33491
- // src/tools/subtask/state.ts
33492
- function createSubtaskState() {
33493
- const sourceBySession = new Map;
33494
- return {
33495
- markSession(sessionID, sourceSessionID) {
33496
- sourceBySession.set(sessionID, sourceSessionID);
33497
- },
33498
- unmarkSession(sessionID) {
33499
- sourceBySession.delete(sessionID);
33500
- },
33501
- isSubtaskSession(sessionID) {
33502
- return sourceBySession.has(sessionID);
33503
- },
33504
- sourceFor(sessionID) {
33505
- return sourceBySession.get(sessionID);
33506
- }
33507
- };
33508
- }
33509
- // src/tools/subtask/tools.ts
33510
- import { tool as tool5 } from "@opencode-ai/plugin";
33511
- var SUBTASK_TIMEOUT_MS = 5 * 60 * 1000;
33512
- var SUBTASK_SUMMARY_TAG_REGEX = /<\/?subtask_summary>/g;
33513
- function normalizeSubtaskSummary(text) {
33514
- return text.replace(SUBTASK_SUMMARY_TAG_REGEX, "").trim();
33515
- }
33516
- function getAbortSignal(context) {
33517
- if (!context || typeof context !== "object" || !("abort" in context)) {
33518
- return;
33519
- }
33520
- const signal = context.abort;
33521
- return signal && typeof signal === "object" && "addEventListener" in signal && "removeEventListener" in signal && "aborted" in signal ? signal : undefined;
33522
- }
33523
- function createSubtaskTool(ctx, state, depthTracker) {
33524
- const client = ctx.client;
33525
- return tool5({
33526
- description: "Run a child worker session and return its completion summary to the caller",
33527
- args: {
33528
- prompt: tool5.schema.string().describe("The generated subtask prompt"),
33529
- files: tool5.schema.array(tool5.schema.string()).optional().describe("Array of file paths to load into the new session's context")
33530
- },
33531
- async execute(args, context) {
33532
- const directory = context && typeof context === "object" && "directory" in context && typeof context.directory === "string" ? context.directory : ctx.directory;
33533
- const sessionID = context && typeof context === "object" && "sessionID" in context ? context.sessionID : "unknown";
33534
- const abortSignal = getAbortSignal(context);
33535
- if (state.isSubtaskSession(sessionID)) {
33536
- return "Nested subtask is disabled: this session is already a subtask worker. Finish this worker and return its summary to the parent session instead.";
33537
- }
33538
- if (sessionID !== "unknown" && depthTracker && depthTracker.getDepth(sessionID) + 1 > depthTracker.maxDepth) {
33539
- return `Subtask worker blocked: max subagent depth ${depthTracker.maxDepth} would be exceeded.`;
33540
- }
33541
- const sessionReference = `You are a subtask worker spawned by parent session ${sessionID}.
33542
-
33543
- Your job is bounded: complete only the task below. Do not expand scope.
33544
- If needed context is missing, use read_session to inspect the parent session.
33545
- Do not spawn another subtask.`;
33546
- const files = new Set([
33547
- ...parseFileReferences(args.prompt),
33548
- ...(args.files ?? []).map(cleanFileReference)
33549
- ]);
33550
- const fileRefs = files.size > 0 ? [...files].map((f) => `@${f}`).join(" ") : "";
33551
- const fullPrompt = fileRefs ? `${sessionReference}
33552
-
33553
- TASK:
33554
- ${args.prompt}
33555
-
33556
- FILES PROVIDED:
33557
- ${fileRefs}` : `${sessionReference}
33558
-
33559
- TASK:
33560
- ${args.prompt}`;
33561
- let childSessionID;
33562
- try {
33563
- const session2 = await client.session.create({
33564
- responseStyle: "data",
33565
- throwOnError: true,
33566
- query: { directory },
33567
- body: {
33568
- parentID: sessionID === "unknown" ? undefined : sessionID,
33569
- title: `Subtask worker from ${sessionID}`
33570
- }
33571
- });
33572
- childSessionID = session2?.data?.id ?? session2?.id;
33573
- if (!childSessionID) {
33574
- throw new Error("Subtask worker session did not return an id");
33575
- }
33576
- if (sessionID !== "unknown" && depthTracker) {
33577
- const registered = depthTracker.registerChild(sessionID, childSessionID);
33578
- if (!registered) {
33579
- throw new Error("Subtask worker blocked: max subagent depth exceeded");
33580
- }
33581
- }
33582
- state.markSession(childSessionID, sessionID);
33583
- await promptWithTimeout(client, {
33584
- responseStyle: "data",
33585
- throwOnError: true,
33586
- query: { directory },
33587
- path: { id: childSessionID },
33588
- body: {
33589
- agent: "orchestrator",
33590
- parts: [
33591
- {
33592
- type: "text",
33593
- text: `${fullPrompt}
33594
-
33595
- Instructions:
33596
- 1. Understand the task and relevant file context.
33597
- 2. Make only necessary changes.
33598
- 3. Run the most relevant validation checks when practical.
33599
- 4. Stop when the requested task is done.
33600
-
33601
- Return your final response in this format:
33602
-
33603
- <subtask_summary>
33604
- Status: completed | blocked | partial
33605
-
33606
- What changed:
33607
- - ...
33608
-
33609
- Files touched:
33610
- - ...
33611
-
33612
- Validation:
33613
- - ...
33614
-
33615
- Risks / follow-up:
33616
- - ...
33617
- </subtask_summary>`
33618
- },
33619
- ...await buildSyntheticFileParts(directory, files)
33620
- ]
33621
- }
33622
- }, SUBTASK_TIMEOUT_MS, abortSignal);
33623
- const extraction = await extractSessionResult(client, childSessionID, {
33624
- directory,
33625
- includeReasoning: false
33626
- });
33627
- if (extraction.empty) {
33628
- throw new Error("Subtask worker returned no summary");
33629
- }
33630
- const summary = normalizeSubtaskSummary(extraction.text);
33631
- return [
33632
- `task_id: ${childSessionID}`,
33633
- "",
33634
- "<subtask_summary>",
33635
- summary,
33636
- "</subtask_summary>"
33637
- ].join(`
33638
- `);
33639
- } finally {
33640
- if (childSessionID) {
33641
- try {
33642
- await client.session.abort({
33643
- path: { id: childSessionID },
33644
- query: { directory }
33645
- });
33646
- state.unmarkSession(childSessionID);
33647
- } catch {}
33648
- }
33649
- }
33650
- }
33651
- });
33652
- }
33653
- function formatTranscript(messages, limit) {
33654
- const lines = [];
33655
- for (const msg of messages) {
33656
- const role = msg.info?.role;
33657
- const parts = msg.parts;
33658
- if (role === "user") {
33659
- lines.push("## User");
33660
- for (const part of parts) {
33661
- if (part.type === "text" && !part.ignored && typeof part.text === "string") {
33662
- lines.push(part.text);
33663
- }
33664
- if (part.type === "file") {
33665
- lines.push(`[Attached: ${part.filename || "file"}]`);
33666
- }
33667
- }
33668
- lines.push("");
33669
- }
33670
- if (role === "assistant") {
33671
- lines.push("## Assistant");
33672
- for (const part of parts) {
33673
- if (part.type === "text" && typeof part.text === "string") {
33674
- lines.push(part.text);
33675
- }
33676
- if (part.type === "tool" && part.state?.status === "completed" && part.tool) {
33677
- lines.push(`[Tool: ${part.tool}] ${part.state.title ?? ""}`);
33678
- }
33679
- }
33680
- lines.push("");
33681
- }
33682
- }
33683
- const output = lines.join(`
33684
- `).trim();
33685
- if (messages.length >= (limit ?? 100)) {
33686
- return output + `
33687
-
33688
- (Showing ${messages.length} most recent messages. Use a higher 'limit' to see more.)`;
33689
- }
33690
- return `${output}
33691
-
33692
- (End of session - ${messages.length} messages)`;
33693
- }
33694
- function createReadSessionTool(client, state) {
33695
- return tool5({
33696
- description: "Read the conversation transcript from a previous session. Use this when you need specific information from the source session that wasn't included in the subtask summary.",
33697
- args: {
33698
- sessionID: tool5.schema.string().describe("The full session ID (e.g., sess_01jxyz...)"),
33699
- limit: tool5.schema.number().optional().describe("Maximum number of messages to read (defaults to 100, max 500)")
33700
- },
33701
- async execute(args, context) {
33702
- const limit = Math.min(args.limit ?? 100, 500);
33703
- const directory = context && typeof context === "object" && "directory" in context && typeof context.directory === "string" ? context.directory : undefined;
33704
- const callerSessionID = context && typeof context === "object" && "sessionID" in context ? context.sessionID : undefined;
33705
- if (!callerSessionID || !state.isSubtaskSession(callerSessionID)) {
33706
- return "read_session is only available from subtask worker sessions.";
33707
- }
33708
- if (state.sourceFor(callerSessionID) !== args.sessionID) {
33709
- return "read_session can only read the source session for this subtask worker.";
33710
- }
33711
- try {
33712
- const response = await client.session.messages({
33713
- path: { id: args.sessionID },
33714
- query: { limit, ...directory ? { directory } : {} }
33715
- });
33716
- if (!response.data || response.data.length === 0) {
33717
- return "Session has no messages or does not exist.";
33718
- }
33719
- return formatTranscript(response.data, limit);
33720
- } catch (error) {
33721
- return `Could not read session ${args.sessionID}: ${error instanceof Error ? error.message : "Unknown error"}`;
33722
- }
33723
- }
33724
- });
33725
- }
33726
33222
  // src/utils/subagent-depth.ts
33727
33223
  class SubagentDepthTracker {
33728
33224
  depthBySession = new Map;
@@ -33835,17 +33331,16 @@ var OhMyOpenCodeLite = async (ctx) => {
33835
33331
  let jsonErrorRecoveryHook;
33836
33332
  let foregroundFallback;
33837
33333
  let todoContinuationHook;
33838
- let sessionGoalHook;
33334
+ let deepworkCommandHook;
33839
33335
  let taskSessionManagerHook;
33840
33336
  let backgroundJobBoard;
33841
33337
  let interviewManager;
33842
33338
  let presetManager;
33843
33339
  let divoomManager;
33844
33340
  let councilTools;
33341
+ let cancelTaskTools;
33845
33342
  let webfetch;
33846
33343
  let rewriteDisplayNameMentions;
33847
- let subtaskCommandManager;
33848
- let subtaskState;
33849
33344
  let toolCount = 0;
33850
33345
  try {
33851
33346
  config = loadPluginConfig(ctx.directory);
@@ -33908,7 +33403,12 @@ var OhMyOpenCodeLite = async (ctx) => {
33908
33403
  councilTools = config.council ? createCouncilTool(ctx, new CouncilManager(ctx, config, depthTracker, multiplexerEnabled)) : {};
33909
33404
  mcps = createBuiltinMcps(config.disabled_mcps, config.websearch);
33910
33405
  webfetch = createWebfetchTool(ctx);
33911
- multiplexerSessionManager = new MultiplexerSessionManager(ctx, multiplexerConfig);
33406
+ backgroundJobBoard = new BackgroundJobBoard({
33407
+ maxReusablePerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? 2,
33408
+ readContextMinLines: config.backgroundJobs?.readContextMinLines ?? 10,
33409
+ readContextMaxFiles: config.backgroundJobs?.readContextMaxFiles ?? 8
33410
+ });
33411
+ multiplexerSessionManager = new MultiplexerSessionManager(ctx, multiplexerConfig, backgroundJobBoard);
33912
33412
  autoUpdateChecker = createAutoUpdateCheckerHook(ctx, {
33913
33413
  autoUpdate: config.autoUpdate ?? true
33914
33414
  });
@@ -33923,7 +33423,6 @@ var OhMyOpenCodeLite = async (ctx) => {
33923
33423
  applyPatchHook = createApplyPatchHook(ctx);
33924
33424
  jsonErrorRecoveryHook = createJsonErrorRecoveryHook(ctx);
33925
33425
  foregroundFallback = new ForegroundFallbackManager(ctx.client, runtimeChains, config.fallback?.enabled !== false && Object.keys(runtimeChains).length > 0);
33926
- backgroundJobBoard = new BackgroundJobBoard;
33927
33426
  todoContinuationHook = createTodoContinuationHook(ctx, {
33928
33427
  maxContinuations: config.todoContinuation?.maxContinuations ?? 5,
33929
33428
  cooldownMs: config.todoContinuation?.cooldownMs ?? 3000,
@@ -33931,22 +33430,23 @@ var OhMyOpenCodeLite = async (ctx) => {
33931
33430
  autoEnableThreshold: config.todoContinuation?.autoEnableThreshold ?? 4,
33932
33431
  backgroundJobBoard
33933
33432
  });
33934
- sessionGoalHook = createSessionGoalHook(ctx, config, {
33935
- getAgentName: (sessionID) => sessionAgentMap.get(sessionID)
33936
- });
33433
+ deepworkCommandHook = createDeepworkCommandHook();
33937
33434
  taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
33938
- maxSessionsPerAgent: config.sessionManager?.maxSessionsPerAgent ?? 2,
33939
- readContextMinLines: config.sessionManager?.readContextMinLines ?? 10,
33940
- readContextMaxFiles: config.sessionManager?.readContextMaxFiles ?? 8,
33435
+ maxSessionsPerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? 2,
33436
+ readContextMinLines: config.backgroundJobs?.readContextMinLines ?? 10,
33437
+ readContextMaxFiles: config.backgroundJobs?.readContextMaxFiles ?? 8,
33941
33438
  backgroundJobBoard,
33942
33439
  shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
33943
33440
  });
33944
33441
  interviewManager = createInterviewManager(ctx, config);
33945
33442
  presetManager = createPresetManager(ctx, config);
33946
33443
  divoomManager = createDivoomManager(config.divoom);
33947
- subtaskState = createSubtaskState();
33948
- subtaskCommandManager = createSubtaskCommandManager(ctx, subtaskState);
33949
- toolCount = Object.keys(councilTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2 + 2;
33444
+ cancelTaskTools = createCancelTaskTool({
33445
+ client: ctx.client,
33446
+ backgroundJobBoard,
33447
+ shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
33448
+ });
33449
+ toolCount = Object.keys(councilTools).length + Object.keys(cancelTaskTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2;
33950
33450
  } catch (err) {
33951
33451
  log("[plugin] FATAL: init failed", String(err));
33952
33452
  await appLog(ctx, "error", `INIT FAILED: ${String(err)}. Report at github.com/alvinunreal/oh-my-opencode-slim/issues/310`);
@@ -33988,12 +33488,11 @@ var OhMyOpenCodeLite = async (ctx) => {
33988
33488
  agent: agents,
33989
33489
  tool: {
33990
33490
  ...councilTools,
33491
+ ...cancelTaskTools,
33991
33492
  webfetch,
33992
33493
  ...todoContinuationHook.tool,
33993
33494
  ast_grep_search,
33994
- ast_grep_replace,
33995
- subtask: createSubtaskTool(ctx, subtaskState, depthTracker),
33996
- read_session: createReadSessionTool(ctx.client, subtaskState)
33495
+ ast_grep_replace
33997
33496
  },
33998
33497
  mcp: mcps,
33999
33498
  config: async (opencodeConfig) => {
@@ -34189,9 +33688,8 @@ var OhMyOpenCodeLite = async (ctx) => {
34189
33688
  };
34190
33689
  }
34191
33690
  interviewManager.registerCommand(opencodeConfig);
34192
- sessionGoalHook.registerCommand(opencodeConfig);
33691
+ deepworkCommandHook.registerCommand(opencodeConfig);
34193
33692
  presetManager.registerCommand(opencodeConfig);
34194
- subtaskCommandManager.registerCommand(opencodeConfig);
34195
33693
  },
34196
33694
  event: async (input) => {
34197
33695
  const event = input.event;
@@ -34216,11 +33714,9 @@ var OhMyOpenCodeLite = async (ctx) => {
34216
33714
  await multiplexerSessionManager.onSessionDeleted(event);
34217
33715
  await foregroundFallback.handleEvent(input.event);
34218
33716
  await todoContinuationHook.handleEvent(input);
34219
- sessionGoalHook.handleEvent(input);
34220
33717
  await autoUpdateChecker.event(input);
34221
33718
  await interviewManager.handleEvent(input);
34222
33719
  await taskSessionManagerHook.event(input);
34223
- subtaskCommandManager.handleEvent(input);
34224
33720
  if (event.type === "permission.asked" || event.type === "question.asked") {
34225
33721
  const props = event.properties;
34226
33722
  divoomManager.onUserInputRequired({
@@ -34278,7 +33774,7 @@ var OhMyOpenCodeLite = async (ctx) => {
34278
33774
  await todoContinuationHook.handleCommandExecuteBefore(input, output);
34279
33775
  await interviewManager.handleCommandExecuteBefore(input, output);
34280
33776
  await presetManager.handleCommandExecuteBefore(input, output);
34281
- await sessionGoalHook.handleCommandExecuteBefore(input, output);
33777
+ await deepworkCommandHook.handleCommandExecuteBefore(input, output);
34282
33778
  },
34283
33779
  "chat.headers": chatHeadersHook["chat.headers"],
34284
33780
  "chat.message": async (input, output) => {
@@ -34307,7 +33803,6 @@ var OhMyOpenCodeLite = async (ctx) => {
34307
33803
  ${output.system[0]}` : "");
34308
33804
  }
34309
33805
  }
34310
- sessionGoalHook.handleSystemTransform(input, output);
34311
33806
  collapseSystemInPlace(output.system);
34312
33807
  },
34313
33808
  "experimental.chat.messages.transform": async (input, output) => {