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

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