oh-my-opencode-slim 2.0.0-beta.0 → 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 +773 -1253
  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 -6
  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,9 +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! Recall the workflow rules:
18306
- Understand → build a short work graph with independent lanes, dependencies, and advisory ownership → dispatch independent specialists as background tasks → record task/session IDs → continue orchestration → poll task_status for terminal results → reconcile → verify.
18307
- Only consume outputs or advance dependent work when background results are terminal. !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!`;
18308
18312
  var TMUX_SPAWN_DELAY_MS = 500;
18309
18313
  var COUNCILLOR_STAGGER_MS = 250;
18310
18314
  var DEFAULT_DISABLED_AGENTS = ["observer"];
@@ -18562,7 +18566,7 @@ var InterviewConfigSchema = z2.object({
18562
18566
  port: z2.number().int().min(0).max(65535).default(0),
18563
18567
  dashboard: z2.boolean().default(false)
18564
18568
  });
18565
- var SessionManagerConfigSchema = z2.object({
18569
+ var BackgroundJobsConfigSchema = z2.object({
18566
18570
  maxSessionsPerAgent: z2.number().int().min(1).max(10).default(2),
18567
18571
  readContextMinLines: z2.number().int().min(0).max(1000).default(10),
18568
18572
  readContextMaxFiles: z2.number().int().min(0).max(50).default(8)
@@ -18628,7 +18632,7 @@ var PluginConfigSchema = z2.object({
18628
18632
  tmux: TmuxConfigSchema.optional(),
18629
18633
  websearch: WebsearchConfigSchema.optional(),
18630
18634
  interview: InterviewConfigSchema.optional(),
18631
- sessionManager: SessionManagerConfigSchema.optional(),
18635
+ backgroundJobs: BackgroundJobsConfigSchema.optional(),
18632
18636
  divoom: DivoomConfigSchema.optional(),
18633
18637
  todoContinuation: TodoContinuationConfigSchema.optional(),
18634
18638
  fallback: FailoverConfigSchema.optional(),
@@ -18729,7 +18733,7 @@ function mergePluginConfigs(base, override) {
18729
18733
  tmux: deepMerge(base.tmux, override.tmux),
18730
18734
  multiplexer: deepMerge(base.multiplexer, override.multiplexer),
18731
18735
  interview: deepMerge(base.interview, override.interview),
18732
- sessionManager: deepMerge(base.sessionManager, override.sessionManager),
18736
+ backgroundJobs: deepMerge(base.backgroundJobs, override.backgroundJobs),
18733
18737
  divoom: deepMerge(base.divoom, override.divoom),
18734
18738
  fallback: deepMerge(base.fallback, override.fallback),
18735
18739
  council: deepMerge(base.council, override.council)
@@ -18960,49 +18964,48 @@ ${customAppendPrompt}`;
18960
18964
  }
18961
18965
  var AGENT_DESCRIPTIONS = {
18962
18966
  explorer: `@explorer
18963
- - Lane: Codebase discovery and reconnaissance
18964
- - Role: Parallel search specialist for discovering unknowns across the codebase
18965
- - Permissions: Read files
18967
+ - Lane: Fast codebase recon that returns compressed context
18968
+ - Permissions: read_files
18966
18969
  - Stats: 2x faster codebase search than orchestrator, 1/2 cost of orchestrator
18967
18970
  - Capabilities: Glob, grep, AST queries to locate files, symbols, patterns
18968
18971
  - **Delegate when:** Need to discover what exists before planning • Parallel searches speed discovery • Need summarized map vs full contents • Broad/uncertain scope
18969
18972
  - **Don't delegate when:** Know the path and need actual content • Need full file anyway • Single specific lookup • About to edit the file`,
18970
18973
  librarian: `@librarian
18971
- - Lane: External knowledge and library research
18972
- - Role: Authoritative source for current library docs and API references
18973
- - Permissions: External docs/search MCPs; no file edits
18974
- - Stats: 10x better finding up-to-date library docs than orchestrator, 1/2 cost of orchestrator
18975
- - Capabilities: Fetches latest official docs, examples, API signatures, version-specific behavior via grep_app MCP
18976
- - **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
18977
18978
  - **Don't delegate when:** Standard usage you're confident • Simple stable APIs • General programming knowledge • Info already in conversation • Built-in language features
18978
- - **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.`,
18979
18980
  oracle: `@oracle
18980
18981
  - Lane: Architecture, risk, debugging strategy, and review
18981
18982
  - Role: Strategic advisor for high-stakes decisions and persistent problems, code reviewer
18982
- - Permissions: Read files
18983
+ - Permissions: read_files
18983
18984
  - Stats: 5x better decision maker, problem solver, investigator than orchestrator, 0.8x speed of orchestrator, same cost.
18984
18985
  - Capabilities: Deep architectural reasoning, system-level trade-offs, complex debugging, code review, simplification, maintainability review
18985
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
18986
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
18987
18988
  - **Rule of thumb:** Need senior architect review? → @oracle. Need code review or simplification? → @oracle. Routine coordination or final synthesis? → handle directly.`,
18988
18989
  designer: `@designer
18989
- - Lane: User-facing UI/UX design, polish, and review
18990
- - Role: UI/UX specialist for intentional, polished experiences
18991
- - Permissions: Read/write files
18990
+ - Lane: UI/UX design, related edits, design polish and review
18991
+ - Permissions: read_files, write_files
18992
18992
  - Stats: 10x better UI/UX than orchestrator
18993
- - 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"
18994
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
18995
- - **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.
18996
18998
  - **Rule of thumb:** Users see it and polish matters? → @designer. Headless/functional implementation? → schedule @fixer.`,
18997
18999
  fixer: `@fixer
18998
- - Lane: Bounded implementation and test execution
18999
- - Role: Fast execution specialist for well-defined tasks, which empowers orchestrator with parallel, speedy executions
19000
- - Permissions: Read/write files
19001
- - 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
19002
19005
  - Tools/Constraints: Execution-focused—no research, no architectural decisions
19003
- - **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.
19004
- - **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
19005
- - **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.`,
19006
19009
  council: `@council
19007
19010
  - Lane: High-stakes multi-model decision support
19008
19011
  - Role: Multi-LLM consensus engine that runs several councillors, synthesizes their views, and returns a structured council report.
@@ -19027,8 +19030,8 @@ var AGENT_DESCRIPTIONS = {
19027
19030
  };
19028
19031
  var VALIDATION_ROUTING = [
19029
19032
  "- Route UI/UX validation and review to @designer",
19030
- "- Route code review, simplification, maintainability review, and YAGNI checks to @oracle",
19031
- "- 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",
19032
19035
  "- Route visual/media analysis and interpretation to @observer",
19033
19036
  "- If a request spans multiple lanes, delegate only the lanes that add clear value"
19034
19037
  ];
@@ -19060,6 +19063,7 @@ function buildOrchestratorPrompt(disabledAgents) {
19060
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.
19061
19064
 
19062
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.
19063
19067
  </Role>
19064
19068
 
19065
19069
  <Agents>
@@ -19074,22 +19078,26 @@ ${enabledAgents}
19074
19078
  Parse request: explicit requirements + implicit needs.
19075
19079
 
19076
19080
  ## 2. Path Selection
19077
- Evaluate approach by: quality, speed, cost, reliability.
19081
+ Evaluate approach by: quality, speed and cost.
19078
19082
  Choose the path that optimizes all four.
19079
19083
 
19080
- Classify work into lanes: discovery, external knowledge, implementation, UI/UX, review/risk, visual analysis, and final verification.
19081
-
19082
19084
  ## 3. Delegation Check
19083
- **STOP. Review specialists before acting.**
19084
-
19085
- !!! 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.
19086
19086
 
19087
19087
  **Dispatch efficiency:**
19088
19088
  - Reference paths/lines, don't paste files (\`src/app.ts:42\` not full contents)
19089
- - Provide context summaries, let specialists read what they need
19090
19089
  - Brief user on delegation goal before each call
19091
- - Keep direct work limited to clarification, minimal routing context, todos, synthesis, and final checks
19092
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.
19093
19101
 
19094
19102
  ## 4. Plan and Parallelize
19095
19103
  Build a short work graph before dispatching:
@@ -19103,46 +19111,28 @@ ${enabledParallelExamples}
19103
19111
 
19104
19112
  Balance: respect dependencies, avoid parallelizing what must be sequential, and avoid overlapping write ownership.
19105
19113
 
19106
- ### Context Isolation
19107
- If no specialist delegation is needed, consider \`subtask\` before doing
19108
- context-heavy work directly.
19109
-
19110
- Ask whether the parent context needs the details or only the result. Use
19111
- \`subtask\` when the work is bounded, context-heavy, and the parent only needs a
19112
- compact outcome.
19113
-
19114
- Use \`subtask\` for focused investigation, bounded analysis, cleanup, or
19115
- verification across files/logs/messages.
19116
-
19117
- 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.
19118
-
19119
- Do not use \`subtask\` for tiny tasks, open-ended work, interactive decisions,
19120
- work better handled by a named specialist, or cases where the parent must reason
19121
- over the details.
19122
-
19123
- When calling \`subtask\`, give a self-contained prompt with objective,
19124
- constraints, relevant context, deliverable, and validation. Pass only clearly
19125
- relevant files. Wait for the summary, then integrate and verify it.
19126
-
19127
19114
  ### OpenCode scheduler model
19128
19115
  - Delegated specialists should be launched as background tasks whenever work can run independently: use \`task(..., background: true)\`.
19129
19116
  - A dispatch returns a task/session ID immediately; it does not mean completion.
19130
19117
  - Track each task ID with specialist, objective, state, and any advisory ownership/dependency labels from the dispatch plan.
19131
- - Continue orchestration while tasks run: planning, scheduling independent lanes, preparing synthesis, and asking needed user questions.
19132
- - 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.
19133
19122
  - Parallel background tasks are allowed only when their write scopes do not conflict.
19134
19123
  - Final response requires relevant tasks to be terminal and reconciled.
19135
19124
 
19136
- ## 5. Dispatch
19137
- 1. Split work into independent and dependency-ordered lanes
19138
- 2. Plan advisory ownership for write-capable lanes
19139
- 3. Dispatch independent specialists as background tasks
19140
- 4. Record task IDs, state, and advisory ownership/dependency labels
19141
- 5. Continue only independent orchestration while jobs run
19142
- 6. Poll/wait for terminal results with \`task_status(wait: true, timeout_ms: ...)\`
19143
- 7. Reconcile results, resolve conflicts, and gate dependent lanes
19144
- 8. Dispatch follow-up jobs if needed
19145
- 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.
19146
19136
 
19147
19137
  ### Session Reuse
19148
19138
  - Smartly reuse an available specialist session - context reuse saves time and tokens
@@ -19257,6 +19247,12 @@ var COUNCIL_AGENT_PROMPT = `You are the Council agent — a multi-LLM orchestrat
19257
19247
  - Be transparent about trade-offs when different approaches have valid pros/cons
19258
19248
  - Don't just average responses — choose the best approach and improve upon it
19259
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
+
19260
19256
  **Required Output Format**:
19261
19257
  Always include these sections in your final response:
19262
19258
 
@@ -19368,6 +19364,12 @@ var COUNCILLOR_PROMPT = `You are a councillor in a multi-model council.
19368
19364
 
19369
19365
  You CANNOT edit files, write files, run shell commands, or delegate to other agents. You are an advisor, not an implementer.
19370
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
+
19371
19373
  **Behavior**:
19372
19374
  - **Examine the codebase** before answering — your read access is what makes council valuable. Don't guess at code you can see.
19373
19375
  - Analyze the problem thoroughly
@@ -19455,6 +19457,13 @@ var DESIGNER_PROMPT = `You are a Designer - a frontend UI/UX specialist who crea
19455
19457
  - Leverage component libraries where available
19456
19458
  - Prioritize visual excellence—code perfection comes second
19457
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
+
19458
19467
  ## Review Responsibilities
19459
19468
  - Review existing UI for usability, responsiveness, visual consistency, and polish when asked
19460
19469
  - Call out concrete UX issues and improvements, not just abstract design advice
@@ -19492,6 +19501,12 @@ var EXPLORER_PROMPT = `You are Explorer - a fast codebase navigation specialist.
19492
19501
  - **Structural patterns** (function shapes, class structures): ast_grep_search
19493
19502
  - **File discovery** (find by name/extension): glob
19494
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
+
19495
19510
  **Behavior**:
19496
19511
  - Be fast and thorough
19497
19512
  - Fire multiple searches in parallel if needed
@@ -19546,6 +19561,13 @@ var FIXER_PROMPT = `You are Fixer - a fast, focused implementation specialist.
19546
19561
  - Run relevant validation when requested or clearly applicable (otherwise note as skipped with reason)
19547
19562
  - Report completion with summary of changes
19548
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
+
19549
19571
  **Constraints**:
19550
19572
  - NO external research (no websearch, context7, grep_app)
19551
19573
  - NO delegation or spawning subagents
@@ -19611,6 +19633,12 @@ var LIBRARIAN_PROMPT = `You are Librarian - a research specialist for codebases
19611
19633
  - grep_app: Search GitHub repositories
19612
19634
  - websearch: General web search for docs
19613
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
+
19614
19642
  **Behavior**:
19615
19643
  - Provide evidence-based answers with sources
19616
19644
  - Quote relevant code snippets
@@ -19655,6 +19683,11 @@ var OBSERVER_PROMPT = `You are Observer — a visual analysis specialist.
19655
19683
  - Save context tokens — the Orchestrator never processes the raw file
19656
19684
  - Match the language of the request
19657
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
19658
19691
  `;
19659
19692
  function createObserverAgent(model, customPrompt, customAppendPrompt) {
19660
19693
  let prompt = OBSERVER_PROMPT;
@@ -19699,6 +19732,12 @@ var ORACLE_PROMPT = `You are Oracle - a strategic technical advisor and code rev
19699
19732
  - READ-ONLY: You advise, you don't implement
19700
19733
  - Focus on strategy, not execution
19701
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
19702
19741
  `;
19703
19742
  function createOracleAgent(model, customPrompt, customAppendPrompt) {
19704
19743
  let prompt = ORACLE_PROMPT;
@@ -19722,6 +19761,7 @@ ${customAppendPrompt}`;
19722
19761
 
19723
19762
  // src/agents/index.ts
19724
19763
  var COUNCIL_TOOL_ALLOWED_AGENTS = new Set(["council"]);
19764
+ var CANCEL_TASK_ALLOWED_AGENTS = new Set(["orchestrator"]);
19725
19765
  var SAFE_AGENT_ALIAS_RE = /^[a-z][a-z0-9_-]*$/i;
19726
19766
  function normalizeDisplayName(displayName) {
19727
19767
  const trimmed = displayName.trim();
@@ -19798,10 +19838,12 @@ function applyDefaultPermissions(agent, configuredSkills) {
19798
19838
  const skillPermissions = getSkillPermissionsForAgent(agent.name, configuredSkills);
19799
19839
  const questionPerm = existing.question === "deny" ? "deny" : "allow";
19800
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";
19801
19842
  agent.config.permission = {
19802
19843
  ...existing,
19803
19844
  question: questionPerm,
19804
19845
  council_session: councilSessionPerm,
19846
+ cancel_task: cancelTaskPerm,
19805
19847
  skill: {
19806
19848
  ...typeof existing.skill === "object" ? existing.skill : {},
19807
19849
  ...skillPermissions
@@ -22685,6 +22727,14 @@ var AGENT_PREFIX = {
22685
22727
  class BackgroundJobBoard {
22686
22728
  jobs = new Map;
22687
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
+ }
22688
22738
  registerLaunch(input) {
22689
22739
  const now = input.now ?? Date.now();
22690
22740
  const existing = this.jobs.get(input.taskID);
@@ -22699,6 +22749,9 @@ class BackgroundJobBoard {
22699
22749
  terminalUnreconciled: false,
22700
22750
  completedAt: undefined,
22701
22751
  resultSummary: undefined,
22752
+ terminalState: undefined,
22753
+ lastLaunchedAt: now,
22754
+ lastUsedAt: now,
22702
22755
  updatedAt: now
22703
22756
  };
22704
22757
  this.jobs.set(input.taskID, updated);
@@ -22714,8 +22767,11 @@ class BackgroundJobBoard {
22714
22767
  timedOut: false,
22715
22768
  terminalUnreconciled: false,
22716
22769
  launchedAt: now,
22770
+ lastLaunchedAt: now,
22771
+ lastUsedAt: now,
22717
22772
  updatedAt: now,
22718
- alias: this.nextAlias(input.parentSessionID, input.agent)
22773
+ alias: this.nextAlias(input.parentSessionID, input.agent),
22774
+ contextFiles: []
22719
22775
  };
22720
22776
  this.jobs.set(input.taskID, record);
22721
22777
  return record;
@@ -22724,6 +22780,9 @@ class BackgroundJobBoard {
22724
22780
  const existing = this.jobs.get(input.taskID);
22725
22781
  if (!existing)
22726
22782
  return;
22783
+ if (existing.state === "reconciled" || existing.state === "cancelled" && input.state !== "cancelled" || TERMINAL_STATES.has(existing.state) && input.state === "running") {
22784
+ return existing;
22785
+ }
22727
22786
  const now = input.now ?? Date.now();
22728
22787
  const terminal = TERMINAL_STATES.has(input.state);
22729
22788
  const updated = {
@@ -22733,9 +22792,11 @@ class BackgroundJobBoard {
22733
22792
  terminalUnreconciled: terminal ? true : existing.terminalUnreconciled,
22734
22793
  updatedAt: now,
22735
22794
  completedAt: terminal ? existing.completedAt ?? now : existing.completedAt,
22795
+ terminalState: terminal ? input.state : existing.terminalState,
22736
22796
  resultSummary: input.resultSummary ?? existing.resultSummary
22737
22797
  };
22738
22798
  this.jobs.set(input.taskID, updated);
22799
+ this.trimReusable(input.taskID);
22739
22800
  return updated;
22740
22801
  }
22741
22802
  updateFromStatusOutput(output) {
@@ -22760,7 +22821,32 @@ class BackgroundJobBoard {
22760
22821
  ...existing,
22761
22822
  state: "reconciled",
22762
22823
  terminalUnreconciled: false,
22763
- 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
22764
22850
  };
22765
22851
  this.jobs.set(taskID, updated);
22766
22852
  return updated;
@@ -22768,6 +22854,49 @@ class BackgroundJobBoard {
22768
22854
  get(taskID) {
22769
22855
  return this.jobs.get(taskID);
22770
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
+ }
22771
22900
  list(parentSessionID) {
22772
22901
  const jobs = [...this.jobs.values()];
22773
22902
  const filtered = parentSessionID ? jobs.filter((job) => job.parentSessionID === parentSessionID) : jobs;
@@ -22779,15 +22908,21 @@ class BackgroundJobBoard {
22779
22908
  hasTerminalUnreconciled(parentSessionID) {
22780
22909
  return this.list(parentSessionID).some((job) => job.terminalUnreconciled);
22781
22910
  }
22782
- formatForPrompt(parentSessionID) {
22783
- const jobs = this.list(parentSessionID).filter((job) => job.state === "running" || job.terminalUnreconciled);
22784
- 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)
22785
22915
  return;
22786
22916
  return [
22787
22917
  "### Background Job Board",
22788
- "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"],
22789
22923
  "",
22790
- ...jobs.map(formatJob)
22924
+ "#### Reusable Sessions",
22925
+ ...reusable.length > 0 ? reusable.map((job) => this.formatReusableJob(job)) : ["- none"]
22791
22926
  ].join(`
22792
22927
  `);
22793
22928
  }
@@ -22799,6 +22934,26 @@ class BackgroundJobBoard {
22799
22934
  drop(taskID) {
22800
22935
  this.jobs.delete(taskID);
22801
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
+ }
22802
22957
  nextAlias(parentSessionID, agent) {
22803
22958
  const prefix2 = AGENT_PREFIX[agent] ?? (agent.slice(0, 3) || "job");
22804
22959
  const key = `${parentSessionID}:${prefix2}`;
@@ -22807,8 +22962,35 @@ class BackgroundJobBoard {
22807
22962
  return `${prefix2}-${next}`;
22808
22963
  }
22809
22964
  }
22810
- function formatJob(job) {
22811
- 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}`;
22812
22994
  const lines = [
22813
22995
  `- ${job.alias} / ${job.taskID} / ${job.agent} / ${status}`,
22814
22996
  ` Objective: ${job.objective || job.description}`
@@ -22825,6 +23007,10 @@ function singleLine(value) {
22825
23007
  return normalized;
22826
23008
  return `${normalized.slice(0, 157)}...`;
22827
23009
  }
23010
+ function normalizeCancelReason(reason) {
23011
+ const normalized = reason?.replace(/\s+/g, " ").trim();
23012
+ return normalized ? `cancelled: ${normalized}` : "cancelled";
23013
+ }
22828
23014
  // src/utils/internal-initiator.ts
22829
23015
  var SLIM_INTERNAL_INITIATOR_MARKER = "<!-- SLIM_INTERNAL_INITIATOR -->";
22830
23016
  function isRecord(value) {
@@ -22846,236 +23032,6 @@ function hasInternalInitiatorMarker(part) {
22846
23032
  }
22847
23033
  return part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER);
22848
23034
  }
22849
- // src/utils/session-manager.ts
22850
- var MIN_CONTEXT_FILE_LINES = 10;
22851
- var MAX_CONTEXT_FILES_PER_SESSION = 8;
22852
- function aliasPrefix(agentType) {
22853
- switch (agentType) {
22854
- case "explorer":
22855
- return "exp";
22856
- case "librarian":
22857
- return "lib";
22858
- case "oracle":
22859
- return "ora";
22860
- case "designer":
22861
- return "des";
22862
- case "fixer":
22863
- return "fix";
22864
- case "observer":
22865
- return "obs";
22866
- case "council":
22867
- return "cnc";
22868
- case "councillor":
22869
- return "clr";
22870
- case "orchestrator":
22871
- return "orc";
22872
- }
22873
- }
22874
- function normalizeWhitespace(value) {
22875
- return value.replace(/\s+/g, " ").trim();
22876
- }
22877
- function deriveTaskSessionLabel(input) {
22878
- const preferred = normalizeWhitespace(input.description ?? "");
22879
- if (preferred) {
22880
- return preferred.slice(0, 48);
22881
- }
22882
- const firstPromptLine = (input.prompt ?? "").split(/\r?\n/).map((line) => normalizeWhitespace(line)).find(Boolean);
22883
- if (firstPromptLine) {
22884
- return firstPromptLine.slice(0, 48);
22885
- }
22886
- return `recent ${input.agentType} task`;
22887
- }
22888
-
22889
- class SessionManager {
22890
- maxSessionsPerAgent;
22891
- readContextMinLines;
22892
- readContextMaxFiles;
22893
- sessionsByParent = new Map;
22894
- nextAliasIndexByParent = new Map;
22895
- orderCounter = 0;
22896
- constructor(maxSessionsPerAgent, options = {}) {
22897
- this.maxSessionsPerAgent = maxSessionsPerAgent;
22898
- this.readContextMinLines = options.readContextMinLines ?? MIN_CONTEXT_FILE_LINES;
22899
- this.readContextMaxFiles = options.readContextMaxFiles ?? MAX_CONTEXT_FILES_PER_SESSION;
22900
- }
22901
- remember(input) {
22902
- const now = this.nextOrder();
22903
- const group = this.getAgentGroup(input.parentSessionId, input.agentType, true);
22904
- if (!group) {
22905
- throw new Error("Failed to initialize session group");
22906
- }
22907
- const existing = group.find((entry) => entry.taskId === input.taskId);
22908
- if (existing) {
22909
- existing.label = input.label;
22910
- existing.lastUsedAt = this.nextOrder();
22911
- return existing;
22912
- }
22913
- const remembered = {
22914
- alias: this.nextAlias(input.parentSessionId, input.agentType),
22915
- taskId: input.taskId,
22916
- agentType: input.agentType,
22917
- label: input.label,
22918
- contextFiles: [],
22919
- createdAt: now,
22920
- lastUsedAt: now
22921
- };
22922
- group.push(remembered);
22923
- this.trimGroup(group);
22924
- return remembered;
22925
- }
22926
- markUsed(parentSessionId, agentType, key) {
22927
- const group = this.getAgentGroup(parentSessionId, agentType, false);
22928
- const match = group?.find((entry) => entry.alias === key || entry.taskId === key);
22929
- if (match) {
22930
- match.lastUsedAt = this.nextOrder();
22931
- }
22932
- }
22933
- resolve(parentSessionId, agentType, key) {
22934
- const group = this.getAgentGroup(parentSessionId, agentType, false);
22935
- return group?.find((entry) => entry.alias === key || entry.taskId === key);
22936
- }
22937
- drop(parentSessionId, agentType, key) {
22938
- const group = this.getAgentGroup(parentSessionId, agentType, false);
22939
- if (!group)
22940
- return;
22941
- const next = group.filter((entry) => entry.alias !== key && entry.taskId !== key);
22942
- this.setAgentGroup(parentSessionId, agentType, next);
22943
- }
22944
- dropTask(taskId) {
22945
- for (const [parentSessionId, groups] of this.sessionsByParent.entries()) {
22946
- for (const [agentType, group] of groups.entries()) {
22947
- const next = group.filter((entry) => entry.taskId !== taskId);
22948
- this.setAgentGroup(parentSessionId, agentType, next);
22949
- }
22950
- }
22951
- }
22952
- taskIds() {
22953
- const ids = new Set;
22954
- for (const groups of this.sessionsByParent.values()) {
22955
- for (const group of groups.values()) {
22956
- for (const entry of group) {
22957
- ids.add(entry.taskId);
22958
- }
22959
- }
22960
- }
22961
- return ids;
22962
- }
22963
- addContext(taskId, files) {
22964
- if (files.length === 0)
22965
- return;
22966
- for (const groups of this.sessionsByParent.values()) {
22967
- for (const group of groups.values()) {
22968
- const match = group.find((entry) => entry.taskId === taskId);
22969
- if (!match)
22970
- continue;
22971
- const existing = new Map(match.contextFiles.map((file) => [file.path, file]));
22972
- for (const file of files) {
22973
- const previous = existing.get(file.path);
22974
- if (previous) {
22975
- previous.lineCount = Math.max(previous.lineCount, file.lineCount);
22976
- previous.lastReadAt = Math.max(previous.lastReadAt, file.lastReadAt);
22977
- continue;
22978
- }
22979
- match.contextFiles.push({ ...file });
22980
- }
22981
- this.trimContextFiles(match);
22982
- }
22983
- }
22984
- }
22985
- clearParent(parentSessionId) {
22986
- this.sessionsByParent.delete(parentSessionId);
22987
- this.nextAliasIndexByParent.delete(parentSessionId);
22988
- }
22989
- formatForPrompt(parentSessionId) {
22990
- const groups = this.sessionsByParent.get(parentSessionId);
22991
- if (!groups || groups.size === 0)
22992
- return;
22993
- const lines = [...groups.entries()].map(([agentType, entries]) => [
22994
- agentType,
22995
- [...entries].sort((a, b) => b.lastUsedAt - a.lastUsedAt)
22996
- ]).filter(([, entries]) => entries.length > 0).sort((a, b) => b[1][0].lastUsedAt - a[1][0].lastUsedAt).map(([agentType, entries]) => [
22997
- `- ${agentType}: ${entries.map((entry) => `${entry.alias} ${entry.label}`).join("; ")}`,
22998
- ...entries.map((entry) => [
22999
- entry,
23000
- formatContextFiles(entry.contextFiles, {
23001
- minLines: this.readContextMinLines,
23002
- maxFiles: this.readContextMaxFiles
23003
- })
23004
- ]).filter(([, context]) => context.length > 0).map(([entry, context]) => ` Context read by ${entry.alias}: ${context}`)
23005
- ].join(`
23006
- `));
23007
- if (lines.length === 0)
23008
- return;
23009
- return [
23010
- "### Resumable Sessions",
23011
- "Reuse only for clear continuation of the same thread. Otherwise start fresh.",
23012
- "",
23013
- ...lines
23014
- ].join(`
23015
- `);
23016
- }
23017
- getAgentGroup(parentSessionId, agentType, create) {
23018
- let groups = this.sessionsByParent.get(parentSessionId);
23019
- if (!groups && create) {
23020
- groups = new Map;
23021
- this.sessionsByParent.set(parentSessionId, groups);
23022
- }
23023
- let group = groups?.get(agentType);
23024
- if (!group && create && groups) {
23025
- group = [];
23026
- groups.set(agentType, group);
23027
- }
23028
- return group;
23029
- }
23030
- setAgentGroup(parentSessionId, agentType, entries) {
23031
- const groups = this.sessionsByParent.get(parentSessionId);
23032
- if (!groups)
23033
- return;
23034
- if (entries.length === 0) {
23035
- groups.delete(agentType);
23036
- if (groups.size === 0) {
23037
- this.sessionsByParent.delete(parentSessionId);
23038
- this.nextAliasIndexByParent.delete(parentSessionId);
23039
- }
23040
- return;
23041
- }
23042
- groups.set(agentType, entries);
23043
- }
23044
- nextAlias(parentSessionId, agentType) {
23045
- let counters = this.nextAliasIndexByParent.get(parentSessionId);
23046
- if (!counters) {
23047
- counters = new Map;
23048
- this.nextAliasIndexByParent.set(parentSessionId, counters);
23049
- }
23050
- const next = (counters.get(agentType) ?? 0) + 1;
23051
- counters.set(agentType, next);
23052
- return `${aliasPrefix(agentType)}-${next}`;
23053
- }
23054
- trimGroup(group) {
23055
- group.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
23056
- if (group.length > this.maxSessionsPerAgent) {
23057
- group.length = this.maxSessionsPerAgent;
23058
- }
23059
- }
23060
- trimContextFiles(entry) {
23061
- if (this.readContextMaxFiles === 0) {
23062
- entry.contextFiles = [];
23063
- return;
23064
- }
23065
- entry.contextFiles = entry.contextFiles.filter((file) => file.lineCount >= this.readContextMinLines).sort((a, b) => b.lastReadAt - a.lastReadAt).slice(0, this.readContextMaxFiles + 1);
23066
- }
23067
- nextOrder() {
23068
- this.orderCounter += 1;
23069
- return this.orderCounter;
23070
- }
23071
- }
23072
- function formatContextFiles(files, options) {
23073
- const eligible = files.filter((file) => file.lineCount >= options.minLines).sort((a, b) => b.lastReadAt - a.lastReadAt);
23074
- const shown = eligible.slice(0, options.maxFiles);
23075
- const rest = eligible.length - shown.length;
23076
- const rendered = shown.map((file) => `${file.path} (${file.lineCount} lines)`);
23077
- return `${rendered.join(", ")}${rest > 0 ? ` (+${rest} more)` : ""}`;
23078
- }
23079
23035
  // src/utils/zip-extractor.ts
23080
23036
  import { spawnSync } from "node:child_process";
23081
23037
  import { release } from "node:os";
@@ -23206,6 +23162,53 @@ function createChatHeadersHook(ctx) {
23206
23162
  }
23207
23163
  };
23208
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
+ }
23209
23212
  // src/hooks/delegate-task-retry/patterns.ts
23210
23213
  var DELEGATE_TASK_ERROR_PATTERNS = [
23211
23214
  {
@@ -23406,7 +23409,7 @@ var RATE_LIMIT_PATTERNS = [
23406
23409
  /usage limit/i,
23407
23410
  /overloaded/i,
23408
23411
  /resource.?exhausted/i,
23409
- /insufficient.?quota/i,
23412
+ /insufficient.?(quota|balance)/i,
23410
23413
  /high concurrency/i,
23411
23414
  /reduce concurrency/i
23412
23415
  ];
@@ -23482,7 +23485,7 @@ class ForegroundFallbackManager {
23482
23485
  if (!props?.sessionID || props.status?.type !== "retry")
23483
23486
  break;
23484
23487
  const msg = props.status.message?.toLowerCase() ?? "";
23485
- 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")) {
23486
23489
  await this.tryFallback(props.sessionID);
23487
23490
  }
23488
23491
  break;
@@ -23931,370 +23934,64 @@ function createPostFileToolNudgeHook(options = {}) {
23931
23934
  }
23932
23935
  };
23933
23936
  }
23934
- // src/hooks/session-goal/index.ts
23935
- import * as fs7 from "node:fs/promises";
23936
-
23937
- // src/interview/document.ts
23938
- import * as fsSync from "node:fs";
23939
- import * as fs6 from "node:fs/promises";
23940
- import * as path9 from "node:path";
23941
- var DEFAULT_OUTPUT_FOLDER = "interview";
23942
- function normalizeOutputFolder(outputFolder) {
23943
- const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
23944
- return normalized || DEFAULT_OUTPUT_FOLDER;
23945
- }
23946
- function createInterviewDirectoryPath(directory, outputFolder) {
23947
- return path9.join(directory, normalizeOutputFolder(outputFolder));
23948
- }
23949
- function createInterviewFilePath(directory, outputFolder, idea) {
23950
- const fileName = `${slugify(idea) || "interview"}.md`;
23951
- return path9.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
23952
- }
23953
- function relativeInterviewPath(directory, filePath) {
23954
- 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");
23955
23961
  }
23956
- function resolveExistingInterviewPath(directory, outputFolder, value) {
23957
- const trimmed = value.trim();
23958
- if (!trimmed) {
23959
- return null;
23962
+ function createOccurrenceId(part, message, partIndex) {
23963
+ if (typeof part.id === "string") {
23964
+ return part.id;
23960
23965
  }
23961
- const outputDir = createInterviewDirectoryPath(directory, outputFolder);
23962
- const candidates = new Set;
23963
- const resolvedRoot = path9.resolve(directory);
23964
- if (path9.isAbsolute(trimmed)) {
23965
- candidates.add(trimmed);
23966
- } else {
23967
- candidates.add(path9.resolve(directory, trimmed));
23968
- candidates.add(path9.join(outputDir, trimmed));
23969
- if (!trimmed.endsWith(".md")) {
23970
- candidates.add(path9.join(outputDir, `${trimmed}.md`));
23971
- }
23966
+ if (typeof message.info.id === "string") {
23967
+ return `${message.info.id}:${partIndex}`;
23972
23968
  }
23973
- for (const candidate of candidates) {
23974
- if (path9.extname(candidate) !== ".md") {
23975
- continue;
23976
- }
23977
- const resolved = path9.resolve(candidate);
23978
- if (!resolved.startsWith(resolvedRoot + path9.sep) && resolved !== resolvedRoot) {
23979
- continue;
23980
- }
23981
- if (fsSync.existsSync(candidate)) {
23982
- return candidate;
23983
- }
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}`;
23984
23976
  }
23985
- return null;
23977
+ const hash = djb2Hash(`${sessionID}:${content}`);
23978
+ return `anon:${hash}`;
23986
23979
  }
23987
- function slugify(value) {
23988
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
23980
+ function isAgentName(value) {
23981
+ return typeof value === "string" && AGENT_NAME_SET.has(value);
23989
23982
  }
23990
- function extractHistorySection(document) {
23991
- const marker = `## Q&A history
23992
-
23993
- `;
23994
- const index = document.indexOf(marker);
23995
- return index >= 0 ? document.slice(index + marker.length).trim() : "";
23983
+ function isObjectRecord(value) {
23984
+ return typeof value === "object" && value !== null;
23996
23985
  }
23997
- function extractSummarySection(document) {
23998
- const marker = `## Current spec
23999
-
24000
- `;
24001
- const historyMarker = `
24002
-
24003
- ## Q&A history`;
24004
- const start = document.indexOf(marker);
24005
- if (start < 0) {
24006
- return "";
24007
- }
24008
- const summaryStart = start + marker.length;
24009
- const summaryEnd = document.indexOf(historyMarker, summaryStart);
24010
- return document.slice(summaryStart, summaryEnd >= 0 ? summaryEnd : undefined).trim();
24011
- }
24012
- function extractTitle(document) {
24013
- const match = document.match(/^#\s+(.+)$/m);
24014
- return match?.[1]?.trim() ?? "";
24015
- }
24016
- function buildInterviewDocument(idea, summary, history, meta) {
24017
- const normalizedSummary = summary.trim() || "Waiting for interview answers.";
24018
- const normalizedHistory = history.trim() || "No answers yet.";
24019
- const frontmatter = meta?.sessionID ? [
24020
- "---",
24021
- `sessionID: ${meta.sessionID}`,
24022
- `baseMessageCount: ${meta.baseMessageCount ?? 0}`,
24023
- `updatedAt: ${new Date().toISOString()}`,
24024
- "---",
24025
- ""
24026
- ].join(`
24027
- `) : "";
24028
- return [
24029
- frontmatter,
24030
- `# ${idea}`,
24031
- "",
24032
- "## Current spec",
24033
- "",
24034
- normalizedSummary,
24035
- "",
24036
- "## Q&A history",
24037
- "",
24038
- normalizedHistory,
24039
- ""
24040
- ].join(`
24041
- `);
24042
- }
24043
- function parseFrontmatter(content) {
24044
- const match = content.match(/^---\n([\s\S]*?)\n---\n/);
24045
- if (!match)
24046
- return null;
24047
- const result = {};
24048
- for (const line of match[1].split(`
24049
- `)) {
24050
- const colonIdx = line.indexOf(":");
24051
- if (colonIdx > 0) {
24052
- result[line.slice(0, colonIdx).trim()] = line.slice(colonIdx + 1).trim();
24053
- }
24054
- }
24055
- return result;
24056
- }
24057
- async function ensureInterviewFile(record) {
24058
- await fs6.mkdir(path9.dirname(record.markdownPath), { recursive: true });
24059
- try {
24060
- await fs6.access(record.markdownPath);
24061
- } catch {
24062
- await fs6.writeFile(record.markdownPath, buildInterviewDocument(record.idea, "", "", {
24063
- sessionID: record.sessionID,
24064
- baseMessageCount: record.baseMessageCount
24065
- }), "utf8");
24066
- }
24067
- }
24068
- async function readInterviewDocument(record) {
24069
- try {
24070
- return await fs6.readFile(record.markdownPath, "utf8");
24071
- } catch {}
24072
- await ensureInterviewFile(record);
24073
- return fs6.readFile(record.markdownPath, "utf8");
24074
- }
24075
- async function rewriteInterviewDocument(record, summary) {
24076
- const existing = await readInterviewDocument(record);
24077
- const history = extractHistorySection(existing);
24078
- const next = buildInterviewDocument(record.idea, summary, history, {
24079
- sessionID: record.sessionID,
24080
- baseMessageCount: record.baseMessageCount
24081
- });
24082
- await fs6.writeFile(record.markdownPath, next, "utf8");
24083
- return next;
24084
- }
24085
- async function appendInterviewAnswers(record, questions, answers) {
24086
- const existing = await readInterviewDocument(record);
24087
- const summary = extractSummarySection(existing);
24088
- const history = extractHistorySection(existing);
24089
- const questionMap = new Map(questions.map((question) => [question.id, question]));
24090
- const appended = answers.map((answer) => {
24091
- const question = questionMap.get(answer.questionId);
24092
- return question ? `Q: ${question.question}
24093
- A: ${answer.answer.trim()}` : null;
24094
- }).filter((value) => value !== null).join(`
24095
-
24096
- `);
24097
- const nextHistory = [history === "No answers yet." ? "" : history, appended].filter(Boolean).join(`
24098
-
24099
- `);
24100
- await fs6.writeFile(record.markdownPath, buildInterviewDocument(record.idea, summary, nextHistory, {
24101
- sessionID: record.sessionID,
24102
- baseMessageCount: record.baseMessageCount
24103
- }), "utf8");
24104
- }
24105
-
24106
- // src/hooks/session-goal/index.ts
24107
- var COMMAND_NAME = "goal";
24108
- var MAX_GOAL_LENGTH = 4000;
24109
- function normalizeGoalText(text) {
24110
- return text.trim().replace(/\s+/g, " ").slice(0, MAX_GOAL_LENGTH);
24111
- }
24112
- function trimGoalText(text) {
24113
- return text.trim().slice(0, MAX_GOAL_LENGTH);
24114
- }
24115
- function pushText(output, text) {
24116
- output.parts.push(createInternalAgentTextPart(text));
24117
- }
24118
- function formatGoal(state, inherited) {
24119
- const tag = inherited ? "parent_goal" : "active_goal";
24120
- 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.";
24121
- return `<${tag}>
24122
- Objective: ${state.text}
24123
- ${guidance}
24124
- </${tag}>`;
24125
- }
24126
- async function readInterviewGoal(directory, outputFolder, value) {
24127
- try {
24128
- const sourcePath = resolveExistingInterviewPath(directory, outputFolder, value);
24129
- if (!sourcePath)
24130
- return null;
24131
- const content = await fs7.readFile(sourcePath, "utf8");
24132
- const title = extractTitle(content);
24133
- const summary = extractSummarySection(content);
24134
- const text = trimGoalText([title ? `From interview: ${title}` : "", summary].filter(Boolean).join(`
24135
-
24136
- `));
24137
- return text ? { text, sourcePath } : null;
24138
- } catch {
24139
- return null;
24140
- }
24141
- }
24142
- function resolveGoal(goals, sessionID) {
24143
- const seen = new Set;
24144
- let currentSessionID = sessionID;
24145
- let inherited = false;
24146
- while (true) {
24147
- if (seen.has(currentSessionID)) {
24148
- goals.delete(sessionID);
24149
- return null;
24150
- }
24151
- seen.add(currentSessionID);
24152
- const goal = goals.get(currentSessionID);
24153
- if (!goal) {
24154
- goals.delete(sessionID);
24155
- return null;
24156
- }
24157
- if (!goal.inheritedFrom) {
24158
- return { goal, inherited };
24159
- }
24160
- inherited = true;
24161
- currentSessionID = goal.inheritedFrom;
24162
- }
24163
- }
24164
- function createSessionGoalHook(ctx, config, options) {
24165
- const goals = new Map;
24166
- const outputFolder = config.interview?.outputFolder ?? "interview";
24167
- return {
24168
- registerCommand: (opencodeConfig) => {
24169
- const commandConfig = opencodeConfig.command;
24170
- if (commandConfig?.[COMMAND_NAME])
24171
- return;
24172
- if (!opencodeConfig.command)
24173
- opencodeConfig.command = {};
24174
- opencodeConfig.command[COMMAND_NAME] = {
24175
- template: "Set or show the current session goal",
24176
- description: "Pin a session objective that keeps todos, delegation, and verification aligned"
24177
- };
24178
- },
24179
- handleCommandExecuteBefore: async (input, output) => {
24180
- if (input.command !== COMMAND_NAME)
24181
- return;
24182
- output.parts.length = 0;
24183
- const args = input.arguments.trim();
24184
- if (!args) {
24185
- const resolved = resolveGoal(goals, input.sessionID);
24186
- pushText(output, resolved ? `Active goal:
24187
- ${resolved.goal.text}
24188
-
24189
- Use todos for execution steps. Auto-continuation continues only while todos remain.` : "No active goal. Set one with /goal <objective>.");
24190
- return;
24191
- }
24192
- if (args === "clear") {
24193
- goals.delete(input.sessionID);
24194
- pushText(output, "Cleared the active goal for this session.");
24195
- return;
24196
- }
24197
- if (args.startsWith("from ")) {
24198
- const value = args.slice("from ".length).trim();
24199
- const interviewGoal = await readInterviewGoal(ctx.directory, outputFolder, value);
24200
- if (!interviewGoal) {
24201
- pushText(output, `Could not find a readable interview spec for "${value}".`);
24202
- return;
24203
- }
24204
- goals.set(input.sessionID, {
24205
- text: interviewGoal.text,
24206
- source: "interview",
24207
- sourcePath: interviewGoal.sourcePath,
24208
- createdAt: Date.now()
24209
- });
24210
- pushText(output, `Set active goal from interview:
24211
- ${interviewGoal.text}`);
24212
- return;
24213
- }
24214
- const text = normalizeGoalText(args);
24215
- goals.set(input.sessionID, {
24216
- text,
24217
- source: "manual",
24218
- createdAt: Date.now()
24219
- });
24220
- pushText(output, `Set active goal:
24221
- ${text}`);
24222
- },
24223
- handleEvent: (input) => {
24224
- const event = input.event;
24225
- if (event.type === "session.created") {
24226
- const info = event.properties?.info;
24227
- if (!info?.id || !info.parentID)
24228
- return;
24229
- const parentGoal = goals.get(info.parentID);
24230
- if (!parentGoal)
24231
- return;
24232
- goals.set(info.id, {
24233
- inheritedFrom: info.parentID,
24234
- createdAt: Date.now(),
24235
- text: ""
24236
- });
24237
- return;
24238
- }
24239
- if (event.type === "session.deleted") {
24240
- const props = event.properties;
24241
- const sessionID = props?.info?.id ?? props?.sessionID;
24242
- if (sessionID)
24243
- goals.delete(sessionID);
24244
- }
24245
- },
24246
- handleSystemTransform: (input, output) => {
24247
- if (!input.sessionID)
24248
- return;
24249
- const resolved = resolveGoal(goals, input.sessionID);
24250
- if (!resolved)
24251
- return;
24252
- const agentName = options?.getAgentName?.(input.sessionID);
24253
- const { goal, inherited } = resolved;
24254
- if (!inherited && agentName && agentName !== "orchestrator")
24255
- return;
24256
- const block = formatGoal(goal, inherited);
24257
- if (output.system.some((entry) => entry.includes(block)))
24258
- return;
24259
- output.system.push(block);
24260
- },
24261
- getGoal: (sessionID) => resolveGoal(goals, sessionID)?.goal
24262
- };
24263
- }
24264
- // src/hooks/task-session-manager/index.ts
24265
- import path10 from "node:path";
24266
- var AGENT_NAME_SET = new Set([
24267
- "orchestrator",
24268
- "oracle",
24269
- "designer",
24270
- "explorer",
24271
- "librarian",
24272
- "fixer",
24273
- "observer",
24274
- "council",
24275
- "councillor"
24276
- ]);
24277
- var MAX_PENDING_TASK_CALLS = 100;
24278
- var RESUMABLE_SESSIONS_START = "<resumable_sessions>";
24279
- var RESUMABLE_SESSIONS_END = "</resumable_sessions>";
24280
- var BACKGROUND_COMPLETION_COMPLETED = /^Background task completed: /;
24281
- var BACKGROUND_COMPLETION_FAILED = /^Background task failed: /;
24282
- var MAX_PROCESSED_INJECTED_COMPLETIONS = 500;
24283
- function isAgentName(value) {
24284
- return typeof value === "string" && AGENT_NAME_SET.has(value);
24285
- }
24286
- function isObjectRecord(value) {
24287
- return typeof value === "object" && value !== null;
24288
- }
24289
- function extractPath(output) {
24290
- return /<path>([^<]+)<\/path>/.exec(output)?.[1];
23986
+ function extractPath(output) {
23987
+ return /<path>([^<]+)<\/path>/.exec(output)?.[1];
24291
23988
  }
24292
23989
  function normalizePath(root, file) {
24293
- const relative2 = path10.relative(root, file);
24294
- if (!relative2 || relative2.startsWith("..") || path10.isAbsolute(relative2)) {
23990
+ const relative = path9.relative(root, file);
23991
+ if (!relative || relative.startsWith("..") || path9.isAbsolute(relative)) {
24295
23992
  return file;
24296
23993
  }
24297
- return relative2;
23994
+ return relative;
24298
23995
  }
24299
23996
  function extractReadFiles(root, output) {
24300
23997
  if (typeof output.output !== "string")
@@ -24319,11 +24016,11 @@ function countReadLines(output) {
24319
24016
  return [...lines];
24320
24017
  }
24321
24018
  function createTaskSessionManagerHook(_ctx, options) {
24322
- const sessionManager = new SessionManager(options.maxSessionsPerAgent, {
24019
+ const backgroundJobBoard = options.backgroundJobBoard ?? new BackgroundJobBoard({
24020
+ maxReusablePerAgent: options.maxSessionsPerAgent,
24323
24021
  readContextMinLines: options.readContextMinLines,
24324
24022
  readContextMaxFiles: options.readContextMaxFiles
24325
24023
  });
24326
- const backgroundJobBoard = options.backgroundJobBoard ?? new BackgroundJobBoard;
24327
24024
  const pendingCalls = new Map;
24328
24025
  const pendingCallOrder = [];
24329
24026
  const contextByTask = new Map;
@@ -24352,7 +24049,7 @@ function createTaskSessionManagerHook(_ctx, options) {
24352
24049
  pending.lastReadAt = Math.max(pending.lastReadAt, file.lastReadAt);
24353
24050
  context.set(file.path, pending);
24354
24051
  }
24355
- sessionManager.addContext(taskId, contextFilesForPrompt(context));
24052
+ backgroundJobBoard.addContext(taskId, contextFilesForPrompt(context));
24356
24053
  }
24357
24054
  function contextFilesForPrompt(context) {
24358
24055
  if (!context)
@@ -24364,10 +24061,10 @@ function createTaskSessionManagerHook(_ctx, options) {
24364
24061
  }));
24365
24062
  }
24366
24063
  function canTrackTaskContext(taskId) {
24367
- return pendingManagedTaskIds.has(taskId) || sessionManager.taskIds().has(taskId);
24064
+ return pendingManagedTaskIds.has(taskId) || backgroundJobBoard.taskIDs().has(taskId);
24368
24065
  }
24369
24066
  function pruneContext() {
24370
- const remembered = sessionManager.taskIds();
24067
+ const remembered = backgroundJobBoard.taskIDs();
24371
24068
  for (const taskId of contextByTask.keys()) {
24372
24069
  if (!pendingManagedTaskIds.has(taskId) && !remembered.has(taskId)) {
24373
24070
  contextByTask.delete(taskId);
@@ -24390,12 +24087,12 @@ function createTaskSessionManagerHook(_ctx, options) {
24390
24087
  return;
24391
24088
  if (updated.terminalUnreconciled) {
24392
24089
  pendingManagedTaskIds.delete(updated.taskID);
24393
- contextByTask.delete(updated.taskID);
24090
+ backgroundJobBoard.addContext(updated.taskID, contextFilesForPrompt(contextByTask.get(updated.taskID)));
24394
24091
  pruneContext();
24395
24092
  }
24396
24093
  return updated;
24397
24094
  }
24398
- function updateFromInjectedCompletion(part, message, messageIndex, partIndex) {
24095
+ function updateFromInjectedCompletion(part, message, _messageIndex, partIndex) {
24399
24096
  if (part.type !== "text" || typeof part.text !== "string") {
24400
24097
  return;
24401
24098
  }
@@ -24411,7 +24108,7 @@ function createTaskSessionManagerHook(_ctx, options) {
24411
24108
  return;
24412
24109
  if (isFailed && status.state !== "error")
24413
24110
  return;
24414
- const occurrenceId = typeof part.id === "string" ? part.id : typeof message.info.id === "string" ? `${message.info.id}:${partIndex}` : `${message.info.sessionID ?? "unknown"}:${messageIndex}:${partIndex}`;
24111
+ const occurrenceId = createOccurrenceId(part, message, partIndex);
24415
24112
  if (processedInjectedCompletions.has(occurrenceId))
24416
24113
  return;
24417
24114
  const updated = updateBackgroundJobFromOutput(part.text);
@@ -24490,16 +24187,31 @@ function createTaskSessionManagerHook(_ctx, options) {
24490
24187
  }
24491
24188
  return {
24492
24189
  "tool.execute.before": async (input, output) => {
24493
- if (input.tool.toLowerCase() !== "task")
24190
+ const toolName = input.tool.toLowerCase();
24191
+ if (toolName !== "task" && toolName !== "task_status")
24494
24192
  return;
24495
24193
  if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
24496
24194
  return;
24497
24195
  }
24498
24196
  if (!isObjectRecord(output.args))
24499
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
+ }
24500
24208
  const args = output.args;
24501
- 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
+ }
24502
24213
  return;
24214
+ }
24503
24215
  const label = deriveTaskSessionLabel({
24504
24216
  description: typeof args.description === "string" ? args.description : undefined,
24505
24217
  prompt: typeof args.prompt === "string" ? args.prompt : undefined,
@@ -24519,15 +24231,15 @@ function createTaskSessionManagerHook(_ctx, options) {
24519
24231
  return;
24520
24232
  }
24521
24233
  const requested = args.task_id.trim();
24522
- const remembered = sessionManager.resolve(input.sessionID, args.subagent_type, requested);
24234
+ const remembered = backgroundJobBoard.resolveReusable(input.sessionID, requested, args.subagent_type);
24523
24235
  if (!remembered) {
24524
24236
  delete args.task_id;
24525
24237
  return;
24526
24238
  }
24527
- args.task_id = remembered.taskId;
24528
- pendingManagedTaskIds.add(remembered.taskId);
24529
- sessionManager.markUsed(input.sessionID, args.subagent_type, remembered.taskId);
24530
- 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;
24531
24243
  rememberPendingCall(pendingCall);
24532
24244
  },
24533
24245
  "tool.execute.after": async (input, output) => {
@@ -24558,29 +24270,23 @@ function createTaskSessionManagerHook(_ctx, options) {
24558
24270
  description: pending.label,
24559
24271
  objective: pending.label
24560
24272
  });
24561
- sessionManager.drop(pending.parentSessionId, pending.agentType, pending.resumedTaskId ?? launch.taskID);
24273
+ backgroundJobBoard.addContext(launch.taskID, contextFilesForPrompt(contextByTask.get(launch.taskID)));
24562
24274
  pendingManagedTaskIds.add(launch.taskID);
24563
24275
  return;
24564
24276
  }
24565
24277
  const taskId = parseTaskIdFromTaskOutput(output.output);
24566
24278
  if (!taskId) {
24567
24279
  if (pending.resumedTaskId && isMissingRememberedSessionError(output.output)) {
24568
- sessionManager.drop(pending.parentSessionId, pending.agentType, pending.resumedTaskId);
24280
+ backgroundJobBoard.drop(pending.resumedTaskId);
24569
24281
  }
24570
24282
  return;
24571
24283
  }
24572
24284
  if (pending.resumedTaskId && pending.resumedTaskId !== taskId) {
24573
- sessionManager.drop(pending.parentSessionId, pending.agentType, pending.resumedTaskId);
24285
+ backgroundJobBoard.drop(pending.resumedTaskId);
24574
24286
  }
24575
- sessionManager.remember({
24576
- parentSessionId: pending.parentSessionId,
24577
- taskId,
24578
- agentType: pending.agentType,
24579
- label: pending.label
24580
- });
24581
24287
  pendingManagedTaskIds.delete(taskId);
24582
24288
  const contextFiles = contextFilesForPrompt(contextByTask.get(taskId));
24583
- sessionManager.addContext(taskId, contextFiles);
24289
+ backgroundJobBoard.addContext(taskId, contextFiles);
24584
24290
  pruneContext();
24585
24291
  },
24586
24292
  "experimental.chat.messages.transform": async (_input, output) => {
@@ -24607,8 +24313,7 @@ function createTaskSessionManagerHook(_ctx, options) {
24607
24313
  return;
24608
24314
  }
24609
24315
  const reminders = [
24610
- backgroundJobBoard.formatForPrompt(message.info.sessionID),
24611
- sessionManager.formatForPrompt(message.info.sessionID)
24316
+ backgroundJobBoard.formatForPrompt(message.info.sessionID)
24612
24317
  ].filter((item) => Boolean(item));
24613
24318
  if (reminders.length === 0)
24614
24319
  return;
@@ -24617,18 +24322,12 @@ function createTaskSessionManagerHook(_ctx, options) {
24617
24322
  return;
24618
24323
  if (textPart.text?.includes(SLIM_INTERNAL_INITIATOR_MARKER))
24619
24324
  return;
24620
- if (textPart.text?.includes(RESUMABLE_SESSIONS_START))
24325
+ if (textPart.text?.includes(BACKGROUND_JOB_BOARD_SENTINEL))
24621
24326
  return;
24622
24327
  rememberInjectedTerminalJobs(message.info.sessionID);
24623
- textPart.text = [
24624
- textPart.text ?? "",
24625
- "",
24626
- RESUMABLE_SESSIONS_START,
24627
- reminders.join(`
24328
+ textPart.text = [textPart.text ?? "", "", reminders.join(`
24628
24329
 
24629
- `),
24630
- RESUMABLE_SESSIONS_END
24631
- ].join(`
24330
+ `)].join(`
24632
24331
  `);
24633
24332
  return;
24634
24333
  }
@@ -24660,9 +24359,7 @@ function createTaskSessionManagerHook(_ctx, options) {
24660
24359
  const sessionId = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
24661
24360
  if (!sessionId)
24662
24361
  return;
24663
- sessionManager.dropTask(sessionId);
24664
24362
  backgroundJobBoard.drop(sessionId);
24665
- sessionManager.clearParent(sessionId);
24666
24363
  backgroundJobBoard.clearParent(sessionId);
24667
24364
  terminalJobsInjectedByParent.delete(sessionId);
24668
24365
  contextByTask.delete(sessionId);
@@ -24678,7 +24375,7 @@ function createTaskSessionManagerHook(_ctx, options) {
24678
24375
  };
24679
24376
  }
24680
24377
  // src/hooks/todo-continuation/index.ts
24681
- import { tool } from "@opencode-ai/plugin/tool";
24378
+ import { tool } from "@opencode-ai/plugin";
24682
24379
 
24683
24380
  // src/hooks/todo-continuation/todo-hygiene.ts
24684
24381
  var TODO_HYGIENE_REMINDER = "If the active task changed or finished, update the todo list to match the current work state.";
@@ -25377,28 +25074,197 @@ function createTodoContinuationHook(ctx, config) {
25377
25074
  output.parts.push(createInternalAgentTextPart(`[Auto-continue: enabled for up to ${maxContinuations} continuations. No incomplete todos right now.]`));
25378
25075
  }
25379
25076
  }
25380
- return {
25381
- tool: { auto_continue: autoContinue },
25382
- handleToolExecuteAfter: hygiene.handleToolExecuteAfter,
25383
- handleMessagesTransform,
25384
- handleEvent,
25385
- handleChatMessage,
25386
- handleCommandExecuteBefore
25387
- };
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
+ }
25388
25230
  }
25389
- // src/interview/manager.ts
25390
- 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(`
25391
25258
 
25392
- // src/interview/dashboard.ts
25393
- import crypto from "node:crypto";
25394
- import * as fsSync2 from "node:fs";
25395
- import fs8 from "node:fs/promises";
25396
- import {
25397
- createServer
25398
- } from "node:http";
25399
- import os4 from "node:os";
25400
- import path11 from "node:path";
25401
- 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
+ }
25402
25268
 
25403
25269
  // src/interview/helpers.ts
25404
25270
  function sendJson(response, status, value) {
@@ -27044,7 +26910,7 @@ function removeAuthFile(port) {
27044
26910
  }
27045
26911
  async function readDashboardAuthFile(port) {
27046
26912
  try {
27047
- const content = await fs8.readFile(getAuthFilePath(port), "utf8");
26913
+ const content = await fs7.readFile(getAuthFilePath(port), "utf8");
27048
26914
  const data = JSON.parse(content);
27049
26915
  try {
27050
26916
  process.kill(data.pid, 0);
@@ -27167,7 +27033,7 @@ function createDashboardServer(config) {
27167
27033
  const interviewDir = path11.join(dir, config.outputFolder);
27168
27034
  let entries;
27169
27035
  try {
27170
- entries = await fs8.readdir(interviewDir);
27036
+ entries = await fs7.readdir(interviewDir);
27171
27037
  } catch {
27172
27038
  continue;
27173
27039
  }
@@ -27176,7 +27042,7 @@ function createDashboardServer(config) {
27176
27042
  continue;
27177
27043
  let content;
27178
27044
  try {
27179
- content = await fs8.readFile(path11.join(interviewDir, entry), "utf8");
27045
+ content = await fs7.readFile(path11.join(interviewDir, entry), "utf8");
27180
27046
  } catch {
27181
27047
  continue;
27182
27048
  }
@@ -27205,7 +27071,7 @@ function createDashboardServer(config) {
27205
27071
  const interviewDir = path11.join(dir, config.outputFolder);
27206
27072
  let entries;
27207
27073
  try {
27208
- entries = await fs8.readdir(interviewDir);
27074
+ entries = await fs7.readdir(interviewDir);
27209
27075
  } catch {
27210
27076
  continue;
27211
27077
  }
@@ -27214,7 +27080,7 @@ function createDashboardServer(config) {
27214
27080
  continue;
27215
27081
  let content;
27216
27082
  try {
27217
- content = await fs8.readFile(path11.join(interviewDir, entry), "utf8");
27083
+ content = await fs7.readFile(path11.join(interviewDir, entry), "utf8");
27218
27084
  } catch {
27219
27085
  continue;
27220
27086
  }
@@ -27498,7 +27364,7 @@ function createDashboardServer(config) {
27498
27364
  let markdownPath = entry.filePath;
27499
27365
  if (entry.filePath) {
27500
27366
  try {
27501
- document = await fs8.readFile(entry.filePath, "utf8");
27367
+ document = await fs7.readFile(entry.filePath, "utf8");
27502
27368
  } catch {}
27503
27369
  } else {
27504
27370
  const dirs = getKnownDirectories();
@@ -27506,7 +27372,7 @@ function createDashboardServer(config) {
27506
27372
  const slug = extractResumeSlug(interviewId);
27507
27373
  const candidate = path11.join(dir, config.outputFolder, `${slug}.md`);
27508
27374
  try {
27509
- document = await fs8.readFile(candidate, "utf8");
27375
+ document = await fs7.readFile(candidate, "utf8");
27510
27376
  markdownPath = candidate;
27511
27377
  entry.filePath = candidate;
27512
27378
  break;
@@ -28031,7 +27897,7 @@ function createInterviewServer(deps) {
28031
27897
 
28032
27898
  // src/interview/service.ts
28033
27899
  import { spawn as spawn2 } from "node:child_process";
28034
- import * as fs9 from "node:fs/promises";
27900
+ import * as fs8 from "node:fs/promises";
28035
27901
  import * as path12 from "node:path";
28036
27902
 
28037
27903
  // src/interview/types.ts
@@ -28306,11 +28172,11 @@ function createInterviewService(ctx, config, deps) {
28306
28172
  const dir = path12.dirname(interview.markdownPath);
28307
28173
  const newPath = path12.join(dir, `${newSlug}.md`);
28308
28174
  try {
28309
- await fs9.access(newPath);
28175
+ await fs8.access(newPath);
28310
28176
  return;
28311
28177
  } catch {}
28312
28178
  try {
28313
- await fs9.rename(interview.markdownPath, newPath);
28179
+ await fs8.rename(interview.markdownPath, newPath);
28314
28180
  interview.markdownPath = newPath;
28315
28181
  log("[interview] renamed file with assistant title:", {
28316
28182
  from: currentFileName,
@@ -28376,7 +28242,7 @@ function createInterviewService(ctx, config, deps) {
28376
28242
  active.status = "abandoned";
28377
28243
  }
28378
28244
  }
28379
- const document = await fs9.readFile(markdownPath, "utf8");
28245
+ const document = await fs8.readFile(markdownPath, "utf8");
28380
28246
  const messages = await loadMessages(sessionID);
28381
28247
  const title = extractTitle(document);
28382
28248
  const record = {
@@ -28548,7 +28414,7 @@ function createInterviewService(ctx, config, deps) {
28548
28414
  const resumePath = resolveExistingInterviewPath(ctx.directory, outputFolder, idea);
28549
28415
  if (resumePath) {
28550
28416
  const interview2 = await resumeInterview(input.sessionID, resumePath);
28551
- const document = await fs9.readFile(interview2.markdownPath, "utf8");
28417
+ const document = await fs8.readFile(interview2.markdownPath, "utf8");
28552
28418
  await notifyInterviewUrl(input.sessionID, interview2);
28553
28419
  output.parts.push(createInternalAgentTextPart(buildResumePrompt(document, maxQuestions)));
28554
28420
  return;
@@ -28612,7 +28478,7 @@ function createInterviewService(ctx, config, deps) {
28612
28478
  const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) => path12.resolve(i.markdownPath)));
28613
28479
  let entries;
28614
28480
  try {
28615
- entries = await fs9.readdir(outputDir);
28481
+ entries = await fs8.readdir(outputDir);
28616
28482
  } catch {
28617
28483
  return [];
28618
28484
  }
@@ -28625,7 +28491,7 @@ function createInterviewService(ctx, config, deps) {
28625
28491
  continue;
28626
28492
  let content;
28627
28493
  try {
28628
- content = await fs9.readFile(fullPath, "utf8");
28494
+ content = await fs8.readFile(fullPath, "utf8");
28629
28495
  } catch {
28630
28496
  continue;
28631
28497
  }
@@ -29696,7 +29562,6 @@ function startAvailabilityCheck(config) {
29696
29562
  }
29697
29563
  }
29698
29564
  // src/multiplexer/session-manager.ts
29699
- var SESSION_TIMEOUT_MS = 10 * 60 * 1000;
29700
29565
  var SESSION_MISSING_GRACE_MS = POLL_INTERVAL_BACKGROUND_MS * 3;
29701
29566
  var SHARED_STATE_KEY = Symbol.for("oh-my-opencode-slim.multiplexer-session-manager.state");
29702
29567
  function getSharedState() {
@@ -29710,6 +29575,7 @@ function getSharedState() {
29710
29575
  return globalWithState[SHARED_STATE_KEY];
29711
29576
  }
29712
29577
  class MultiplexerSessionManager {
29578
+ backgroundJobBoard;
29713
29579
  instanceId = Math.random().toString(36).slice(2, 8);
29714
29580
  serverUrl;
29715
29581
  directory;
@@ -29720,7 +29586,8 @@ class MultiplexerSessionManager {
29720
29586
  closingSessions;
29721
29587
  pollInterval;
29722
29588
  enabled = false;
29723
- constructor(ctx, config) {
29589
+ constructor(ctx, config, backgroundJobBoard) {
29590
+ this.backgroundJobBoard = backgroundJobBoard;
29724
29591
  const sharedState = getSharedState();
29725
29592
  this.sessions = sharedState.sessions;
29726
29593
  this.knownSessions = sharedState.knownSessions;
@@ -29815,7 +29682,8 @@ class MultiplexerSessionManager {
29815
29682
  title,
29816
29683
  directory,
29817
29684
  createdAt: now,
29818
- lastSeenAt: now
29685
+ lastSeenAt: now,
29686
+ seenInStatus: false
29819
29687
  });
29820
29688
  log("[multiplexer-session-manager] pane spawned", {
29821
29689
  instanceId: this.instanceId,
@@ -29907,16 +29775,26 @@ class MultiplexerSessionManager {
29907
29775
  const isIdle = status?.type === "idle";
29908
29776
  if (status) {
29909
29777
  tracked.lastSeenAt = now;
29778
+ tracked.seenInStatus = true;
29910
29779
  tracked.missingSince = undefined;
29911
29780
  } else if (!tracked.missingSince) {
29912
29781
  tracked.missingSince = now;
29913
29782
  }
29914
29783
  const missingTooLong = !!tracked.missingSince && now - tracked.missingSince >= SESSION_MISSING_GRACE_MS;
29915
- const isTimedOut = now - tracked.createdAt > SESSION_TIMEOUT_MS;
29916
- 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
+ }
29917
29795
  sessionsToClose.push({
29918
29796
  sessionId,
29919
- reason: isIdle ? "idle" : isTimedOut ? "timeout" : "missing"
29797
+ reason: isIdle ? "idle" : "missing"
29920
29798
  });
29921
29799
  }
29922
29800
  }
@@ -29933,7 +29811,15 @@ class MultiplexerSessionManager {
29933
29811
  if (!response.ok) {
29934
29812
  throw new Error(`session status request failed: ${response.status} ${response.statusText}`);
29935
29813
  }
29936
- 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
+ }
29937
29823
  }
29938
29824
  async closeSession(sessionId, reason) {
29939
29825
  if (reason === "deleted") {
@@ -30033,7 +29919,8 @@ class MultiplexerSessionManager {
30033
29919
  title: known.title,
30034
29920
  directory: known.directory,
30035
29921
  createdAt: now,
30036
- lastSeenAt: now
29922
+ lastSeenAt: now,
29923
+ seenInStatus: false
30037
29924
  });
30038
29925
  log("[multiplexer-session-manager] pane respawned on busy", {
30039
29926
  instanceId: this.instanceId,
@@ -30058,6 +29945,9 @@ class MultiplexerSessionManager {
30058
29945
  getSessionId(event) {
30059
29946
  return event.properties?.info?.id ?? event.properties?.sessionID;
30060
29947
  }
29948
+ isRunningBackgroundJob(sessionId) {
29949
+ return this.backgroundJobBoard?.get(sessionId)?.state === "running";
29950
+ }
30061
29951
  async cleanup() {
30062
29952
  this.stopPolling();
30063
29953
  if (this.closingSessions.size > 0) {
@@ -30103,7 +29993,7 @@ async function isServerRunning(serverUrl, timeoutMs = 3000, maxAttempts = 2) {
30103
29993
  return false;
30104
29994
  }
30105
29995
  // src/tools/ast-grep/tools.ts
30106
- import { tool as tool2 } from "@opencode-ai/plugin/tool";
29996
+ import { tool as tool2 } from "@opencode-ai/plugin";
30107
29997
 
30108
29998
  // src/tools/ast-grep/cli.ts
30109
29999
  import { existsSync as existsSync9 } from "node:fs";
@@ -30648,11 +30538,96 @@ var ast_grep_replace = tool2({
30648
30538
  }
30649
30539
  }
30650
30540
  });
30651
- // src/tools/council.ts
30541
+ // src/tools/cancel-task.ts
30652
30542
  import {
30653
30543
  tool as tool3
30654
30544
  } from "@opencode-ai/plugin";
30655
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;
30656
30631
  function formatModelComposition(councillorResults) {
30657
30632
  return councillorResults.map((cr) => {
30658
30633
  const shortModel = shortModelLabel(cr.model);
@@ -30660,15 +30635,15 @@ function formatModelComposition(councillorResults) {
30660
30635
  }).join(", ");
30661
30636
  }
30662
30637
  function createCouncilTool(_ctx, councilManager) {
30663
- const council_session = tool3({
30638
+ const council_session = tool4({
30664
30639
  description: `Launch a multi-LLM council session for consensus-based analysis.
30665
30640
 
30666
30641
  Sends the prompt to multiple models (councillors) in parallel and returns their formatted responses for you to synthesize.
30667
30642
 
30668
30643
  Returns the councillor responses with a summary footer.`,
30669
30644
  args: {
30670
- prompt: z4.string().describe("The prompt to send to all councillors"),
30671
- 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.')
30672
30647
  },
30673
30648
  async execute(args, toolContext) {
30674
30649
  if (!toolContext || typeof toolContext !== "object" || !("sessionID" in toolContext)) {
@@ -30715,7 +30690,7 @@ Returns the councillor responses with a summary footer.`,
30715
30690
  return { council_session };
30716
30691
  }
30717
30692
  // src/tui-state.ts
30718
- import * as fs10 from "node:fs";
30693
+ import * as fs9 from "node:fs";
30719
30694
  import * as os5 from "node:os";
30720
30695
  import * as path14 from "node:path";
30721
30696
  var STATE_DIR = "oh-my-opencode-slim";
@@ -30745,14 +30720,14 @@ function parseSnapshot(value) {
30745
30720
  }
30746
30721
  function readTuiSnapshot() {
30747
30722
  try {
30748
- return parseSnapshot(fs10.readFileSync(getTuiStatePath(), "utf8"));
30723
+ return parseSnapshot(fs9.readFileSync(getTuiStatePath(), "utf8"));
30749
30724
  } catch {
30750
30725
  return emptySnapshot();
30751
30726
  }
30752
30727
  }
30753
30728
  async function readTuiSnapshotAsync() {
30754
30729
  try {
30755
- return parseSnapshot(await fs10.promises.readFile(getTuiStatePath(), "utf8"));
30730
+ return parseSnapshot(await fs9.promises.readFile(getTuiStatePath(), "utf8"));
30756
30731
  } catch {
30757
30732
  return emptySnapshot();
30758
30733
  }
@@ -30760,8 +30735,8 @@ async function readTuiSnapshotAsync() {
30760
30735
  function writeTuiSnapshot(snapshot) {
30761
30736
  try {
30762
30737
  const filePath = getTuiStatePath();
30763
- fs10.mkdirSync(path14.dirname(filePath), { recursive: true });
30764
- fs10.writeFileSync(filePath, `${JSON.stringify(snapshot)}
30738
+ fs9.mkdirSync(path14.dirname(filePath), { recursive: true });
30739
+ fs9.writeFileSync(filePath, `${JSON.stringify(snapshot)}
30765
30740
  `);
30766
30741
  } catch {}
30767
30742
  }
@@ -30979,7 +30954,7 @@ var WEBFETCH_DESCRIPTION = "Fetch a URL with better extraction for static/docs p
30979
30954
  import os6 from "node:os";
30980
30955
  import path18 from "node:path";
30981
30956
  import {
30982
- tool as tool4
30957
+ tool as tool5
30983
30958
  } from "@opencode-ai/plugin";
30984
30959
 
30985
30960
  // src/tools/smartfetch/binary.ts
@@ -31147,7 +31122,7 @@ var M = class u2 {
31147
31122
  return this.#S;
31148
31123
  }
31149
31124
  constructor(e) {
31150
- 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;
31151
31126
  if (x !== undefined && typeof x?.now != "function")
31152
31127
  throw new TypeError("perf option must have a now() method if specified");
31153
31128
  if (this.#m = x ?? C, t !== 0 && !F(t))
@@ -31165,7 +31140,7 @@ var M = class u2 {
31165
31140
  throw new TypeError("memoMethod must be a function if defined");
31166
31141
  if (this.#U = m, a !== undefined && typeof a != "function")
31167
31142
  throw new TypeError("fetchMethod must be a function if specified");
31168
- 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) {
31169
31144
  if (this.#u !== 0 && !F(this.#u))
31170
31145
  throw new TypeError("maxSize must be a positive integer if specified");
31171
31146
  if (!F(this.maxEntrySize))
@@ -31545,8 +31520,8 @@ var M = class u2 {
31545
31520
  let A = this.#p(b);
31546
31521
  if (!y && !A)
31547
31522
  return a && (a.fetch = "hit"), this.#L(b), s && this.#D(b), a && this.#E(a, b), d;
31548
- let z5 = this.#P(e, b, _, w), v = z5.__staleWhileFetching !== undefined && i;
31549
- 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;
31550
31525
  }
31551
31526
  }
31552
31527
  forceFetch(e, t = {}) {
@@ -31760,7 +31735,7 @@ function extractStructuredText(root) {
31760
31735
  ]);
31761
31736
  const isText = (node) => node.nodeType === node.TEXT_NODE;
31762
31737
  const isElement = (node) => node.nodeType === node.ELEMENT_NODE;
31763
- const pushText2 = (value) => {
31738
+ const pushText = (value) => {
31764
31739
  const normalized = value.replace(/\s+/g, " ");
31765
31740
  if (!normalized.trim())
31766
31741
  return;
@@ -31786,7 +31761,7 @@ function extractStructuredText(root) {
31786
31761
  };
31787
31762
  const visit = (node) => {
31788
31763
  if (isText(node)) {
31789
- pushText2(node.textContent || "");
31764
+ pushText(node.textContent || "");
31790
31765
  return;
31791
31766
  }
31792
31767
  if (!isElement(node))
@@ -32558,7 +32533,7 @@ function isInvalidLlmsResult(fetchResult) {
32558
32533
 
32559
32534
  // src/tools/smartfetch/secondary-model.ts
32560
32535
  import { existsSync as existsSync10 } from "node:fs";
32561
- import { readFile as readFile5 } from "node:fs/promises";
32536
+ import { readFile as readFile4 } from "node:fs/promises";
32562
32537
  import path17 from "node:path";
32563
32538
  function parseModelRef(value) {
32564
32539
  if (!value)
@@ -32595,7 +32570,7 @@ async function readOpenCodeConfigFile(configPath) {
32595
32570
  if (!configPath)
32596
32571
  return;
32597
32572
  try {
32598
- const content = await readFile5(configPath, "utf8");
32573
+ const content = await readFile4(configPath, "utf8");
32599
32574
  return JSON.parse(stripJsonComments(content));
32600
32575
  } catch {
32601
32576
  return;
@@ -32761,20 +32736,20 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
32761
32736
  }
32762
32737
 
32763
32738
  // src/tools/smartfetch/tool.ts
32764
- var z5 = tool4.schema;
32739
+ var z6 = tool5.schema;
32765
32740
  function createWebfetchTool(pluginCtx, options = {}) {
32766
32741
  const binaryDir = options.binaryDir || path18.join(os6.tmpdir(), "opencode-smartfetch");
32767
- return tool4({
32742
+ return tool5({
32768
32743
  description: WEBFETCH_DESCRIPTION,
32769
32744
  args: {
32770
- url: z5.httpUrl(),
32771
- format: z5.enum(["text", "markdown", "html"]).default("markdown"),
32772
- timeout: z5.number().positive().max(MAX_TIMEOUT_SECONDS).optional().describe("Timeout in seconds, max 120."),
32773
- prompt: z5.string().optional().describe("Optional extraction task to run on the fetched content using a cheap secondary model."),
32774
- extract_main: z5.boolean().default(true),
32775
- prefer_llms_txt: z5.enum(["auto", "always", "never"]).default("auto"),
32776
- include_metadata: z5.boolean().default(true),
32777
- 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.")
32778
32753
  },
32779
32754
  async execute(args, ctx) {
32780
32755
  const secondaryModels = await readSecondaryModelFromConfig(ctx.directory || pluginCtx.directory);
@@ -33244,460 +33219,6 @@ function createWebfetchTool(pluginCtx, options = {}) {
33244
33219
  }
33245
33220
  });
33246
33221
  }
33247
- // src/tools/subtask/command.ts
33248
- var COMMAND_NAME5 = "subtask";
33249
- var SUBTASK_COMMAND_TEMPLATE = `Start a focused subtask worker.
33250
-
33251
- The user's request below is the full scope for the worker. Do not broaden it.
33252
- Create a self-contained worker prompt that includes:
33253
- - the exact objective
33254
- - relevant context from this conversation
33255
- - specific files/paths that matter
33256
- - expected deliverables
33257
- - validation the worker should run, if applicable
33258
-
33259
- USER REQUEST:
33260
- $ARGUMENTS
33261
-
33262
- Then call the subtask tool:
33263
- \`subtask(prompt="...", files=["src/foo.ts", "docs/bar.md"])\`
33264
-
33265
- Only include files that are clearly relevant. If no files are needed, omit files.`;
33266
- function createSubtaskCommandManager(_ctx, state) {
33267
- function registerCommand(opencodeConfig) {
33268
- const configCommand = opencodeConfig.command;
33269
- if (!configCommand?.[COMMAND_NAME5]) {
33270
- if (!opencodeConfig.command) {
33271
- opencodeConfig.command = {};
33272
- }
33273
- opencodeConfig.command[COMMAND_NAME5] = {
33274
- description: "Create a focused subtask prompt for a new session",
33275
- template: SUBTASK_COMMAND_TEMPLATE
33276
- };
33277
- }
33278
- }
33279
- return {
33280
- registerCommand,
33281
- handleEvent(input) {
33282
- if (input.event.type === "session.created") {
33283
- const info = input.event.properties?.info;
33284
- if (!info?.id || !info.parentID)
33285
- return;
33286
- const source = state.sourceFor(info.parentID);
33287
- if (source)
33288
- state.markSession(info.id, source);
33289
- return;
33290
- }
33291
- if (input.event.type !== "session.deleted")
33292
- return;
33293
- const sessionID = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
33294
- if (sessionID)
33295
- state.unmarkSession(sessionID);
33296
- }
33297
- };
33298
- }
33299
- // src/tools/subtask/files.ts
33300
- import * as fs12 from "node:fs/promises";
33301
- import * as path20 from "node:path";
33302
-
33303
- // src/tools/subtask/vendor.ts
33304
- import * as fs11 from "node:fs/promises";
33305
- import * as path19 from "node:path";
33306
- var DEFAULT_READ_LIMIT = 2000;
33307
- var MAX_LINE_LENGTH = 2000;
33308
- var MAX_BYTES = 50 * 1024;
33309
- var MAX_LINE_SUFFIX = `... (line truncated to ${MAX_LINE_LENGTH} chars)`;
33310
- var MAX_BYTES_LABEL = `${MAX_BYTES / 1024} KB`;
33311
- var SAMPLE_BYTES = 4096;
33312
- var BINARY_EXTENSIONS = new Set([
33313
- ".zip",
33314
- ".tar",
33315
- ".gz",
33316
- ".exe",
33317
- ".dll",
33318
- ".so",
33319
- ".class",
33320
- ".jar",
33321
- ".war",
33322
- ".7z",
33323
- ".doc",
33324
- ".docx",
33325
- ".xls",
33326
- ".xlsx",
33327
- ".ppt",
33328
- ".pptx",
33329
- ".odt",
33330
- ".ods",
33331
- ".odp",
33332
- ".bin",
33333
- ".dat",
33334
- ".obj",
33335
- ".o",
33336
- ".a",
33337
- ".lib",
33338
- ".wasm",
33339
- ".pyc",
33340
- ".pyo"
33341
- ]);
33342
- async function isBinaryFile(filepath) {
33343
- const ext = path19.extname(filepath).toLowerCase();
33344
- if (BINARY_EXTENSIONS.has(ext)) {
33345
- return true;
33346
- }
33347
- try {
33348
- const file = await fs11.open(filepath, "r");
33349
- try {
33350
- const buffer = Buffer.alloc(SAMPLE_BYTES);
33351
- const result = await file.read(buffer, 0, SAMPLE_BYTES, 0);
33352
- if (result.bytesRead === 0)
33353
- return false;
33354
- const bytes = buffer.subarray(0, result.bytesRead);
33355
- let nonPrintableCount = 0;
33356
- for (let i = 0;i < bytes.length; i++) {
33357
- const byte = bytes[i];
33358
- if (byte === undefined)
33359
- continue;
33360
- if (byte === 0)
33361
- return true;
33362
- if (byte < 9 || byte > 13 && byte < 32) {
33363
- nonPrintableCount++;
33364
- }
33365
- }
33366
- return nonPrintableCount / bytes.length > 0.3;
33367
- } finally {
33368
- await file.close();
33369
- }
33370
- } catch {
33371
- return false;
33372
- }
33373
- }
33374
- function formatFileContent(_filepath, content) {
33375
- const cappedContent = Buffer.byteLength(content, "utf8") > MAX_BYTES;
33376
- const contentToFormat = cappedContent ? content.slice(0, MAX_BYTES) : content;
33377
- const lines = contentToFormat.split(`
33378
- `);
33379
- const limit = DEFAULT_READ_LIMIT;
33380
- const offset = 0;
33381
- const raw = lines.slice(offset, offset + limit).map((line) => {
33382
- return line.length > MAX_LINE_LENGTH ? `${line.substring(0, MAX_LINE_LENGTH)}${MAX_LINE_SUFFIX}` : line;
33383
- });
33384
- const formatted = raw.map((line, index) => {
33385
- return `${index + offset + 1}: ${line}`;
33386
- });
33387
- let output = [
33388
- `<path>${_filepath}</path>`,
33389
- "<type>file</type>",
33390
- `<content>
33391
- `
33392
- ].join(`
33393
- `);
33394
- output += formatted.join(`
33395
- `);
33396
- const totalLines = lines.length;
33397
- const lastReadLine = offset + formatted.length;
33398
- const hasMoreLines = totalLines > lastReadLine;
33399
- if (cappedContent) {
33400
- output += `
33401
-
33402
- (Output capped at ${MAX_BYTES_LABEL}. Showing lines 1-${lastReadLine}. Use offset=${lastReadLine + 1} to continue.)`;
33403
- } else if (hasMoreLines) {
33404
- output += `
33405
-
33406
- (Showing lines 1-${lastReadLine} of ${totalLines}. Use offset=${lastReadLine + 1} to continue.)`;
33407
- } else {
33408
- output += `
33409
-
33410
- (End of file - total ${totalLines} lines)`;
33411
- }
33412
- output += `
33413
- </content>`;
33414
- return output;
33415
- }
33416
-
33417
- // src/tools/subtask/files.ts
33418
- var FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g;
33419
- var TRAILING_PATH_PUNCTUATION = /[!?:;]+$/;
33420
- function cleanFileReference(ref) {
33421
- return ref.replace(/^@/, "").replace(TRAILING_PATH_PUNCTUATION, "");
33422
- }
33423
- function parseFileReferences(text) {
33424
- const fileRefs = new Set;
33425
- for (const match of text.matchAll(FILE_REGEX)) {
33426
- if (match[1]) {
33427
- fileRefs.add(cleanFileReference(match[1]));
33428
- }
33429
- }
33430
- return fileRefs;
33431
- }
33432
- async function buildSyntheticFileParts(directory, refs) {
33433
- const parts = [];
33434
- const realDirectory = await fs12.realpath(directory);
33435
- for (const ref of refs) {
33436
- const filepath = path20.resolve(directory, ref);
33437
- const relative3 = path20.relative(directory, filepath);
33438
- if (relative3.startsWith("..") || path20.isAbsolute(relative3))
33439
- continue;
33440
- try {
33441
- const realFilepath = await fs12.realpath(filepath);
33442
- const realRelative = path20.relative(realDirectory, realFilepath);
33443
- if (realRelative.startsWith("..") || path20.isAbsolute(realRelative)) {
33444
- continue;
33445
- }
33446
- const stats = await fs12.stat(realFilepath);
33447
- if (!stats.isFile())
33448
- continue;
33449
- if (await isBinaryFile(realFilepath))
33450
- continue;
33451
- const content = await fs12.readFile(realFilepath, "utf-8");
33452
- parts.push({
33453
- type: "text",
33454
- synthetic: true,
33455
- text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: realFilepath })}`
33456
- });
33457
- parts.push({
33458
- type: "text",
33459
- synthetic: true,
33460
- text: formatFileContent(realFilepath, content)
33461
- });
33462
- } catch {}
33463
- }
33464
- return parts;
33465
- }
33466
- // src/tools/subtask/state.ts
33467
- function createSubtaskState() {
33468
- const sourceBySession = new Map;
33469
- return {
33470
- markSession(sessionID, sourceSessionID) {
33471
- sourceBySession.set(sessionID, sourceSessionID);
33472
- },
33473
- unmarkSession(sessionID) {
33474
- sourceBySession.delete(sessionID);
33475
- },
33476
- isSubtaskSession(sessionID) {
33477
- return sourceBySession.has(sessionID);
33478
- },
33479
- sourceFor(sessionID) {
33480
- return sourceBySession.get(sessionID);
33481
- }
33482
- };
33483
- }
33484
- // src/tools/subtask/tools.ts
33485
- import { tool as tool5 } from "@opencode-ai/plugin";
33486
- var SUBTASK_TIMEOUT_MS = 5 * 60 * 1000;
33487
- var SUBTASK_SUMMARY_TAG_REGEX = /<\/?subtask_summary>/g;
33488
- function normalizeSubtaskSummary(text) {
33489
- return text.replace(SUBTASK_SUMMARY_TAG_REGEX, "").trim();
33490
- }
33491
- function getAbortSignal(context) {
33492
- if (!context || typeof context !== "object" || !("abort" in context)) {
33493
- return;
33494
- }
33495
- const signal = context.abort;
33496
- return signal && typeof signal === "object" && "addEventListener" in signal && "removeEventListener" in signal && "aborted" in signal ? signal : undefined;
33497
- }
33498
- function createSubtaskTool(ctx, state, depthTracker) {
33499
- const client = ctx.client;
33500
- return tool5({
33501
- description: "Run a child worker session and return its completion summary to the caller",
33502
- args: {
33503
- prompt: tool5.schema.string().describe("The generated subtask prompt"),
33504
- files: tool5.schema.array(tool5.schema.string()).optional().describe("Array of file paths to load into the new session's context")
33505
- },
33506
- async execute(args, context) {
33507
- const directory = context && typeof context === "object" && "directory" in context && typeof context.directory === "string" ? context.directory : ctx.directory;
33508
- const sessionID = context && typeof context === "object" && "sessionID" in context ? context.sessionID : "unknown";
33509
- const abortSignal = getAbortSignal(context);
33510
- if (state.isSubtaskSession(sessionID)) {
33511
- return "Nested subtask is disabled: this session is already a subtask worker. Finish this worker and return its summary to the parent session instead.";
33512
- }
33513
- if (sessionID !== "unknown" && depthTracker && depthTracker.getDepth(sessionID) + 1 > depthTracker.maxDepth) {
33514
- return `Subtask worker blocked: max subagent depth ${depthTracker.maxDepth} would be exceeded.`;
33515
- }
33516
- const sessionReference = `You are a subtask worker spawned by parent session ${sessionID}.
33517
-
33518
- Your job is bounded: complete only the task below. Do not expand scope.
33519
- If needed context is missing, use read_session to inspect the parent session.
33520
- Do not spawn another subtask.`;
33521
- const files = new Set([
33522
- ...parseFileReferences(args.prompt),
33523
- ...(args.files ?? []).map(cleanFileReference)
33524
- ]);
33525
- const fileRefs = files.size > 0 ? [...files].map((f) => `@${f}`).join(" ") : "";
33526
- const fullPrompt = fileRefs ? `${sessionReference}
33527
-
33528
- TASK:
33529
- ${args.prompt}
33530
-
33531
- FILES PROVIDED:
33532
- ${fileRefs}` : `${sessionReference}
33533
-
33534
- TASK:
33535
- ${args.prompt}`;
33536
- let childSessionID;
33537
- try {
33538
- const session2 = await client.session.create({
33539
- responseStyle: "data",
33540
- throwOnError: true,
33541
- query: { directory },
33542
- body: {
33543
- parentID: sessionID === "unknown" ? undefined : sessionID,
33544
- title: `Subtask worker from ${sessionID}`
33545
- }
33546
- });
33547
- childSessionID = session2?.data?.id ?? session2?.id;
33548
- if (!childSessionID) {
33549
- throw new Error("Subtask worker session did not return an id");
33550
- }
33551
- if (sessionID !== "unknown" && depthTracker) {
33552
- const registered = depthTracker.registerChild(sessionID, childSessionID);
33553
- if (!registered) {
33554
- throw new Error("Subtask worker blocked: max subagent depth exceeded");
33555
- }
33556
- }
33557
- state.markSession(childSessionID, sessionID);
33558
- await promptWithTimeout(client, {
33559
- responseStyle: "data",
33560
- throwOnError: true,
33561
- query: { directory },
33562
- path: { id: childSessionID },
33563
- body: {
33564
- agent: "orchestrator",
33565
- parts: [
33566
- {
33567
- type: "text",
33568
- text: `${fullPrompt}
33569
-
33570
- Instructions:
33571
- 1. Understand the task and relevant file context.
33572
- 2. Make only necessary changes.
33573
- 3. Run the most relevant validation checks when practical.
33574
- 4. Stop when the requested task is done.
33575
-
33576
- Return your final response in this format:
33577
-
33578
- <subtask_summary>
33579
- Status: completed | blocked | partial
33580
-
33581
- What changed:
33582
- - ...
33583
-
33584
- Files touched:
33585
- - ...
33586
-
33587
- Validation:
33588
- - ...
33589
-
33590
- Risks / follow-up:
33591
- - ...
33592
- </subtask_summary>`
33593
- },
33594
- ...await buildSyntheticFileParts(directory, files)
33595
- ]
33596
- }
33597
- }, SUBTASK_TIMEOUT_MS, abortSignal);
33598
- const extraction = await extractSessionResult(client, childSessionID, {
33599
- directory,
33600
- includeReasoning: false
33601
- });
33602
- if (extraction.empty) {
33603
- throw new Error("Subtask worker returned no summary");
33604
- }
33605
- const summary = normalizeSubtaskSummary(extraction.text);
33606
- return [
33607
- `task_id: ${childSessionID}`,
33608
- "",
33609
- "<subtask_summary>",
33610
- summary,
33611
- "</subtask_summary>"
33612
- ].join(`
33613
- `);
33614
- } finally {
33615
- if (childSessionID) {
33616
- try {
33617
- await client.session.abort({
33618
- path: { id: childSessionID },
33619
- query: { directory }
33620
- });
33621
- state.unmarkSession(childSessionID);
33622
- } catch {}
33623
- }
33624
- }
33625
- }
33626
- });
33627
- }
33628
- function formatTranscript(messages, limit) {
33629
- const lines = [];
33630
- for (const msg of messages) {
33631
- const role = msg.info?.role;
33632
- const parts = msg.parts;
33633
- if (role === "user") {
33634
- lines.push("## User");
33635
- for (const part of parts) {
33636
- if (part.type === "text" && !part.ignored && typeof part.text === "string") {
33637
- lines.push(part.text);
33638
- }
33639
- if (part.type === "file") {
33640
- lines.push(`[Attached: ${part.filename || "file"}]`);
33641
- }
33642
- }
33643
- lines.push("");
33644
- }
33645
- if (role === "assistant") {
33646
- lines.push("## Assistant");
33647
- for (const part of parts) {
33648
- if (part.type === "text" && typeof part.text === "string") {
33649
- lines.push(part.text);
33650
- }
33651
- if (part.type === "tool" && part.state?.status === "completed" && part.tool) {
33652
- lines.push(`[Tool: ${part.tool}] ${part.state.title ?? ""}`);
33653
- }
33654
- }
33655
- lines.push("");
33656
- }
33657
- }
33658
- const output = lines.join(`
33659
- `).trim();
33660
- if (messages.length >= (limit ?? 100)) {
33661
- return output + `
33662
-
33663
- (Showing ${messages.length} most recent messages. Use a higher 'limit' to see more.)`;
33664
- }
33665
- return `${output}
33666
-
33667
- (End of session - ${messages.length} messages)`;
33668
- }
33669
- function createReadSessionTool(client, state) {
33670
- return tool5({
33671
- 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.",
33672
- args: {
33673
- sessionID: tool5.schema.string().describe("The full session ID (e.g., sess_01jxyz...)"),
33674
- limit: tool5.schema.number().optional().describe("Maximum number of messages to read (defaults to 100, max 500)")
33675
- },
33676
- async execute(args, context) {
33677
- const limit = Math.min(args.limit ?? 100, 500);
33678
- const directory = context && typeof context === "object" && "directory" in context && typeof context.directory === "string" ? context.directory : undefined;
33679
- const callerSessionID = context && typeof context === "object" && "sessionID" in context ? context.sessionID : undefined;
33680
- if (!callerSessionID || !state.isSubtaskSession(callerSessionID)) {
33681
- return "read_session is only available from subtask worker sessions.";
33682
- }
33683
- if (state.sourceFor(callerSessionID) !== args.sessionID) {
33684
- return "read_session can only read the source session for this subtask worker.";
33685
- }
33686
- try {
33687
- const response = await client.session.messages({
33688
- path: { id: args.sessionID },
33689
- query: { limit, ...directory ? { directory } : {} }
33690
- });
33691
- if (!response.data || response.data.length === 0) {
33692
- return "Session has no messages or does not exist.";
33693
- }
33694
- return formatTranscript(response.data, limit);
33695
- } catch (error) {
33696
- return `Could not read session ${args.sessionID}: ${error instanceof Error ? error.message : "Unknown error"}`;
33697
- }
33698
- }
33699
- });
33700
- }
33701
33222
  // src/utils/subagent-depth.ts
33702
33223
  class SubagentDepthTracker {
33703
33224
  depthBySession = new Map;
@@ -33810,17 +33331,16 @@ var OhMyOpenCodeLite = async (ctx) => {
33810
33331
  let jsonErrorRecoveryHook;
33811
33332
  let foregroundFallback;
33812
33333
  let todoContinuationHook;
33813
- let sessionGoalHook;
33334
+ let deepworkCommandHook;
33814
33335
  let taskSessionManagerHook;
33815
33336
  let backgroundJobBoard;
33816
33337
  let interviewManager;
33817
33338
  let presetManager;
33818
33339
  let divoomManager;
33819
33340
  let councilTools;
33341
+ let cancelTaskTools;
33820
33342
  let webfetch;
33821
33343
  let rewriteDisplayNameMentions;
33822
- let subtaskCommandManager;
33823
- let subtaskState;
33824
33344
  let toolCount = 0;
33825
33345
  try {
33826
33346
  config = loadPluginConfig(ctx.directory);
@@ -33883,7 +33403,12 @@ var OhMyOpenCodeLite = async (ctx) => {
33883
33403
  councilTools = config.council ? createCouncilTool(ctx, new CouncilManager(ctx, config, depthTracker, multiplexerEnabled)) : {};
33884
33404
  mcps = createBuiltinMcps(config.disabled_mcps, config.websearch);
33885
33405
  webfetch = createWebfetchTool(ctx);
33886
- 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);
33887
33412
  autoUpdateChecker = createAutoUpdateCheckerHook(ctx, {
33888
33413
  autoUpdate: config.autoUpdate ?? true
33889
33414
  });
@@ -33898,7 +33423,6 @@ var OhMyOpenCodeLite = async (ctx) => {
33898
33423
  applyPatchHook = createApplyPatchHook(ctx);
33899
33424
  jsonErrorRecoveryHook = createJsonErrorRecoveryHook(ctx);
33900
33425
  foregroundFallback = new ForegroundFallbackManager(ctx.client, runtimeChains, config.fallback?.enabled !== false && Object.keys(runtimeChains).length > 0);
33901
- backgroundJobBoard = new BackgroundJobBoard;
33902
33426
  todoContinuationHook = createTodoContinuationHook(ctx, {
33903
33427
  maxContinuations: config.todoContinuation?.maxContinuations ?? 5,
33904
33428
  cooldownMs: config.todoContinuation?.cooldownMs ?? 3000,
@@ -33906,22 +33430,23 @@ var OhMyOpenCodeLite = async (ctx) => {
33906
33430
  autoEnableThreshold: config.todoContinuation?.autoEnableThreshold ?? 4,
33907
33431
  backgroundJobBoard
33908
33432
  });
33909
- sessionGoalHook = createSessionGoalHook(ctx, config, {
33910
- getAgentName: (sessionID) => sessionAgentMap.get(sessionID)
33911
- });
33433
+ deepworkCommandHook = createDeepworkCommandHook();
33912
33434
  taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
33913
- maxSessionsPerAgent: config.sessionManager?.maxSessionsPerAgent ?? 2,
33914
- readContextMinLines: config.sessionManager?.readContextMinLines ?? 10,
33915
- readContextMaxFiles: config.sessionManager?.readContextMaxFiles ?? 8,
33435
+ maxSessionsPerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? 2,
33436
+ readContextMinLines: config.backgroundJobs?.readContextMinLines ?? 10,
33437
+ readContextMaxFiles: config.backgroundJobs?.readContextMaxFiles ?? 8,
33916
33438
  backgroundJobBoard,
33917
33439
  shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
33918
33440
  });
33919
33441
  interviewManager = createInterviewManager(ctx, config);
33920
33442
  presetManager = createPresetManager(ctx, config);
33921
33443
  divoomManager = createDivoomManager(config.divoom);
33922
- subtaskState = createSubtaskState();
33923
- subtaskCommandManager = createSubtaskCommandManager(ctx, subtaskState);
33924
- 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;
33925
33450
  } catch (err) {
33926
33451
  log("[plugin] FATAL: init failed", String(err));
33927
33452
  await appLog(ctx, "error", `INIT FAILED: ${String(err)}. Report at github.com/alvinunreal/oh-my-opencode-slim/issues/310`);
@@ -33963,12 +33488,11 @@ var OhMyOpenCodeLite = async (ctx) => {
33963
33488
  agent: agents,
33964
33489
  tool: {
33965
33490
  ...councilTools,
33491
+ ...cancelTaskTools,
33966
33492
  webfetch,
33967
33493
  ...todoContinuationHook.tool,
33968
33494
  ast_grep_search,
33969
- ast_grep_replace,
33970
- subtask: createSubtaskTool(ctx, subtaskState, depthTracker),
33971
- read_session: createReadSessionTool(ctx.client, subtaskState)
33495
+ ast_grep_replace
33972
33496
  },
33973
33497
  mcp: mcps,
33974
33498
  config: async (opencodeConfig) => {
@@ -34164,9 +33688,8 @@ var OhMyOpenCodeLite = async (ctx) => {
34164
33688
  };
34165
33689
  }
34166
33690
  interviewManager.registerCommand(opencodeConfig);
34167
- sessionGoalHook.registerCommand(opencodeConfig);
33691
+ deepworkCommandHook.registerCommand(opencodeConfig);
34168
33692
  presetManager.registerCommand(opencodeConfig);
34169
- subtaskCommandManager.registerCommand(opencodeConfig);
34170
33693
  },
34171
33694
  event: async (input) => {
34172
33695
  const event = input.event;
@@ -34191,11 +33714,9 @@ var OhMyOpenCodeLite = async (ctx) => {
34191
33714
  await multiplexerSessionManager.onSessionDeleted(event);
34192
33715
  await foregroundFallback.handleEvent(input.event);
34193
33716
  await todoContinuationHook.handleEvent(input);
34194
- sessionGoalHook.handleEvent(input);
34195
33717
  await autoUpdateChecker.event(input);
34196
33718
  await interviewManager.handleEvent(input);
34197
33719
  await taskSessionManagerHook.event(input);
34198
- subtaskCommandManager.handleEvent(input);
34199
33720
  if (event.type === "permission.asked" || event.type === "question.asked") {
34200
33721
  const props = event.properties;
34201
33722
  divoomManager.onUserInputRequired({
@@ -34253,7 +33774,7 @@ var OhMyOpenCodeLite = async (ctx) => {
34253
33774
  await todoContinuationHook.handleCommandExecuteBefore(input, output);
34254
33775
  await interviewManager.handleCommandExecuteBefore(input, output);
34255
33776
  await presetManager.handleCommandExecuteBefore(input, output);
34256
- await sessionGoalHook.handleCommandExecuteBefore(input, output);
33777
+ await deepworkCommandHook.handleCommandExecuteBefore(input, output);
34257
33778
  },
34258
33779
  "chat.headers": chatHeadersHook["chat.headers"],
34259
33780
  "chat.message": async (input, output) => {
@@ -34282,7 +33803,6 @@ var OhMyOpenCodeLite = async (ctx) => {
34282
33803
  ${output.system[0]}` : "");
34283
33804
  }
34284
33805
  }
34285
- sessionGoalHook.handleSystemTransform(input, output);
34286
33806
  collapseSystemInPlace(output.system);
34287
33807
  },
34288
33808
  "experimental.chat.messages.transform": async (input, output) => {