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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -18230,19 +18230,6 @@ var CUSTOM_SKILLS = [
18230
18230
  ];
18231
18231
 
18232
18232
  // src/cli/skills.ts
18233
- var RECOMMENDED_SKILLS = [
18234
- {
18235
- name: "agent-browser",
18236
- repo: "https://github.com/vercel-labs/agent-browser",
18237
- skillName: "agent-browser",
18238
- allowedAgents: ["designer"],
18239
- description: "High-performance browser automation",
18240
- postInstallCommands: [
18241
- "npm install -g agent-browser",
18242
- "agent-browser install"
18243
- ]
18244
- }
18245
- ];
18246
18233
  var PERMISSION_ONLY_SKILLS = [
18247
18234
  {
18248
18235
  name: "requesting-code-review",
@@ -18267,12 +18254,6 @@ function getSkillPermissionsForAgent(agentName, skillList) {
18267
18254
  }
18268
18255
  return permissions;
18269
18256
  }
18270
- for (const skill of RECOMMENDED_SKILLS) {
18271
- const isAllowed = skill.allowedAgents.includes("*") || skill.allowedAgents.includes(agentName);
18272
- if (isAllowed) {
18273
- permissions[skill.skillName] = "allow";
18274
- }
18275
- }
18276
18257
  for (const skill of CUSTOM_SKILLS) {
18277
18258
  const isAllowed = skill.allowedAgents.includes("*") || skill.allowedAgents.includes(agentName);
18278
18259
  if (isAllowed) {
@@ -18321,9 +18302,7 @@ var POLL_INTERVAL_BACKGROUND_MS = 2000;
18321
18302
  var DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
18322
18303
  var MAX_POLL_TIME_MS = 5 * 60 * 1000;
18323
18304
  var DEFAULT_MAX_SUBAGENT_DEPTH = 3;
18324
- var PHASE_REMINDER_TEXT = `!IMPORTANT! Recall the workflow rules:
18325
- Understand → choose the best parallelized path based on your capabilities and agents delegation rules → recall session reuse rules → execute → verify.
18326
- If delegating, launch the specialist in the same turn you mention it !END!`;
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!`;
18327
18306
  var TMUX_SPAWN_DELAY_MS = 500;
18328
18307
  var COUNCILLOR_STAGGER_MS = 250;
18329
18308
  var DEFAULT_DISABLED_AGENTS = ["observer"];
@@ -18670,7 +18649,9 @@ function loadConfigFromPath(configPath, options) {
18670
18649
  const content = fs.readFileSync(configPath, "utf-8");
18671
18650
  let rawConfig;
18672
18651
  try {
18673
- rawConfig = JSON.parse(stripJsonComments(content));
18652
+ const stripped = stripJsonComments(content);
18653
+ const interpolated = stripped.replace(/\{env:([^}]+)\}/g, (_, varName) => process.env[varName] ?? "");
18654
+ rawConfig = JSON.parse(interpolated);
18674
18655
  } catch (error) {
18675
18656
  const message = error instanceof Error ? error.message : String(error);
18676
18657
  options?.onWarning?.({
@@ -18977,6 +18958,7 @@ ${customAppendPrompt}`;
18977
18958
  }
18978
18959
  var AGENT_DESCRIPTIONS = {
18979
18960
  explorer: `@explorer
18961
+ - Lane: Codebase discovery and reconnaissance
18980
18962
  - Role: Parallel search specialist for discovering unknowns across the codebase
18981
18963
  - Permissions: Read files
18982
18964
  - Stats: 2x faster codebase search than orchestrator, 1/2 cost of orchestrator
@@ -18984,38 +18966,43 @@ var AGENT_DESCRIPTIONS = {
18984
18966
  - **Delegate when:** Need to discover what exists before planning • Parallel searches speed discovery • Need summarized map vs full contents • Broad/uncertain scope
18985
18967
  - **Don't delegate when:** Know the path and need actual content • Need full file anyway • Single specific lookup • About to edit the file`,
18986
18968
  librarian: `@librarian
18969
+ - Lane: External knowledge and library research
18987
18970
  - Role: Authoritative source for current library docs and API references
18988
18971
  - Permissions: External docs/search MCPs; no file edits
18989
18972
  - Stats: 10x better finding up-to-date library docs than orchestrator, 1/2 cost of orchestrator
18990
18973
  - Capabilities: Fetches latest official docs, examples, API signatures, version-specific behavior via grep_app MCP
18991
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
18992
18975
  - **Don't delegate when:** Standard usage you're confident • Simple stable APIs • General programming knowledge • Info already in conversation • Built-in language features
18993
- - **Rule of thumb:** "How does this library work?" → @librarian. "How does programming work?" → yourself.`,
18976
+ - **Rule of thumb:** "How does this library work?" → @librarian. "How does programming work?" → answer directly.`,
18994
18977
  oracle: `@oracle
18978
+ - Lane: Architecture, risk, debugging strategy, and review
18995
18979
  - Role: Strategic advisor for high-stakes decisions and persistent problems, code reviewer
18996
18980
  - Permissions: Read files
18997
18981
  - Stats: 5x better decision maker, problem solver, investigator than orchestrator, 0.8x speed of orchestrator, same cost.
18998
18982
  - Capabilities: Deep architectural reasoning, system-level trade-offs, complex debugging, code review, simplification, maintainability review
18999
18983
  - **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
19000
18984
  - **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
19001
- - **Rule of thumb:** Need senior architect review? → @oracle. Need code review or simplification? → @oracle. Just do it and PR? → yourself.`,
18985
+ - **Rule of thumb:** Need senior architect review? → @oracle. Need code review or simplification? → @oracle. Routine coordination or final synthesis? → handle directly.`,
19002
18986
  designer: `@designer
18987
+ - Lane: User-facing UI/UX design, polish, and review
19003
18988
  - Role: UI/UX specialist for intentional, polished experiences
19004
18989
  - Permissions: Read/write files
19005
18990
  - Stats: 10x better UI/UX than orchestrator
19006
18991
  - Capabilities: Visual relevant edits, interactions, responsive layouts, design systems with aesthetic intent, deep UI/UX knowledge.
19007
18992
  - **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
19008
18993
  - **Don't delegate when:** Backend/logic with no visual • Quick prototypes where design doesn't matter yet
19009
- - **Rule of thumb:** Users see it and polish matters? → @designer. Headless/functional? → yourself.`,
18994
+ - **Rule of thumb:** Users see it and polish matters? → @designer. Headless/functional implementation? → schedule @fixer.`,
19010
18995
  fixer: `@fixer
18996
+ - Lane: Bounded implementation and test execution
19011
18997
  - Role: Fast execution specialist for well-defined tasks, which empowers orchestrator with parallel, speedy executions
19012
18998
  - Permissions: Read/write files
19013
18999
  - Stats: 2x faster code edits, 1/2 cost of orchestrator, 0.8x quality of orchestrator
19014
19000
  - Tools/Constraints: Execution-focused—no research, no architectural decisions
19015
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.
19016
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
19017
- - **Rule of thumb:** Explaining > doing? yourself. Test file modifications and bounded implementation work usually go to @fixer. Bigger or lots of edits, splitting makes sense, parallelized by spawning @fixers per certain scope.`,
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.`,
19018
19004
  council: `@council
19005
+ - Lane: High-stakes multi-model decision support
19019
19006
  - Role: Multi-LLM consensus engine that runs several councillors, synthesizes their views, and returns a structured council report.
19020
19007
  - Permissions: Read files
19021
19008
  - Stats: 3x slower than orchestrator, 3x or more cost of orchestrator
@@ -19024,15 +19011,16 @@ var AGENT_DESCRIPTIONS = {
19024
19011
  - **Don't delegate when:** Straightforward tasks you're confident about • Speed matters more than confidence • Routine implementation/debugging • A single specialist is clearly the right tool • You only need current docs/search/code review rather than multi-model consensus.
19025
19012
  - **How to call:** Send the full question/task and relevant context. Be explicit about what decision, trade-off, or answer the council should resolve. Do not ask council to do routine code edits.
19026
19013
  - **Result handling:** Council returns a structured response that may include: synthesized Council Response, individual Councillor Details, and Council Summary/confidence. Preserve that structure when the user asked for council output. Do not pretend the council only returned a final answer. If you need to act on the council result, first briefly state the council's recommendation, then proceed.
19027
- - **Rule of thumb:** Need second/third opinions from different models? → @council. Need one expert agent or direct execution? → use the specialist or yourself.`,
19014
+ - **Rule of thumb:** Need second/third opinions from different models? → @council. Need one expert lane? → use the specialist. Need final synthesis? → handle directly.`,
19028
19015
  observer: `@observer
19016
+ - Lane: Visual/media analysis isolated from orchestrator context
19029
19017
  - Role: Visual analysis specialist for images, PDFs, and diagrams
19030
19018
  - Permissions: Read files
19031
19019
  - Stats: Saves main context tokens — Observer processes raw files, returns structured observations
19032
19020
  - Capabilities: Interprets images, screenshots, PDFs, and diagrams via native read tool; extracts UI elements, layouts, text, relationships
19033
19021
  - **Delegate when:** Need to analyze a multimedia file• Extract information
19034
19022
  - **Don't delegate when:** Plain text files that Read can handle directly • Files that need editing afterward (need literal content from Read)
19035
- - **Rule of thumb:** Even if your model supports vision, delegate visual analysis to @observer — it isolates large image/PDF bytes from your context window, returning only concise structured text. Need exact file contents for editing? → Read it yourself.
19023
+ - **Rule of thumb:** Even if your model supports vision, delegate visual analysis to @observer — it isolates large image/PDF bytes from your context window, returning only concise structured text. Need exact file contents for routing? → Read only the minimal context yourself.
19036
19024
  - **IMPORTANT:** When delegating to @observer, always include the **full file path** in the prompt so it can read the file. Example: "Analyze the screenshot at /path/to/file.png — describe the UI elements and error messages."`
19037
19025
  };
19038
19026
  var VALIDATION_ROUTING = [
@@ -19067,7 +19055,9 @@ function buildOrchestratorPrompt(disabledAgents) {
19067
19055
  }).join(`
19068
19056
  `);
19069
19057
  return `<Role>
19070
- You are an AI coding orchestrator that optimizes for quality, speed, cost, and reliability by delegating to specialists when it provides net efficiency gains.
19058
+ 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
+
19060
+ 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.
19071
19061
  </Role>
19072
19062
 
19073
19063
  <Agents>
@@ -19085,22 +19075,31 @@ Parse request: explicit requirements + implicit needs.
19085
19075
  Evaluate approach by: quality, speed, cost, reliability.
19086
19076
  Choose the path that optimizes all four.
19087
19077
 
19078
+ Classify work into lanes: discovery, external knowledge, implementation, UI/UX, review/risk, visual analysis, and final verification.
19079
+
19088
19080
  ## 3. Delegation Check
19089
19081
  **STOP. Review specialists before acting.**
19090
19082
 
19091
- !!! Review available agents and delegation rules. Decide whether to delegate or do it yourself. !!!
19083
+ !!! Review available agents and lane rules. Decide what to schedule, what depends on what, and what minimal direct coordination is needed. !!!
19092
19084
 
19093
- **Delegation efficiency:**
19085
+ **Dispatch efficiency:**
19094
19086
  - Reference paths/lines, don't paste files (\`src/app.ts:42\` not full contents)
19095
19087
  - Provide context summaries, let specialists read what they need
19096
19088
  - Brief user on delegation goal before each call
19097
- - Skip delegation if overhead doing it yourself
19089
+ - Keep direct work limited to clarification, minimal routing context, todos, synthesis, and final checks
19090
+ - For trivial conversational answers or tiny mechanical edits, direct execution is allowed when scheduling overhead would clearly dominate
19098
19091
 
19099
- ## 4. Split and Parallelize
19100
- Can tasks be split into subtasks and run in parallel?
19092
+ ## 4. Plan and Parallelize
19093
+ Build a short work graph before dispatching:
19094
+ - Independent lanes that can run now
19095
+ - Dependency-ordered lanes that must wait
19096
+ - Advisory ownership for write-capable lanes
19097
+ - Verification/review lanes that run after implementation
19098
+
19099
+ Can tasks be split into background specialist work?
19101
19100
  ${enabledParallelExamples}
19102
19101
 
19103
- Balance: respect dependencies, avoid parallelizing what must be sequential.
19102
+ Balance: respect dependencies, avoid parallelizing what must be sequential, and avoid overlapping write ownership.
19104
19103
 
19105
19104
  ### Context Isolation
19106
19105
  If no specialist delegation is needed, consider \`subtask\` before doing
@@ -19113,6 +19112,8 @@ compact outcome.
19113
19112
  Use \`subtask\` for focused investigation, bounded analysis, cleanup, or
19114
19113
  verification across files/logs/messages.
19115
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
+
19116
19117
  Do not use \`subtask\` for tiny tasks, open-ended work, interactive decisions,
19117
19118
  work better handled by a named specialist, or cases where the parent must reason
19118
19119
  over the details.
@@ -19121,21 +19122,28 @@ When calling \`subtask\`, give a self-contained prompt with objective,
19121
19122
  constraints, relevant context, deliverable, and validation. Pass only clearly
19122
19123
  relevant files. Wait for the summary, then integrate and verify it.
19123
19124
 
19124
- ### OpenCode subagent execution model
19125
- - A delegated specialist runs in a separate child session.
19126
- - Delegation is blocking for the parent at that point: send work out, then continue that line after results return.
19127
- - Parallel delegation means launching multiple independent child-session branches.
19128
- - Only parallelize branches that are truly independent; reconcile dependent steps after delegated results come back.
19129
-
19130
- ## 5. Execute
19131
- 1. Break complex tasks into todos
19132
- 2. Fire parallel research/implementation
19133
- 3. Delegate to specialists or do it yourself based on step 3
19134
- 4. Integrate results
19135
- 5. Adjust if needed
19125
+ ### OpenCode scheduler model
19126
+ - Delegated specialists should be launched as background tasks whenever work can run independently: use \`task(..., background: true)\`.
19127
+ - A dispatch returns a task/session ID immediately; it does not mean completion.
19128
+ - 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.
19131
+ - Parallel background tasks are allowed only when their write scopes do not conflict.
19132
+ - Final response requires relevant tasks to be terminal and reconciled.
19133
+
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
19136
19144
 
19137
19145
  ### Session Reuse
19138
- - Smartly reuse an available specialist session - constext reuse saves time and tokens
19146
+ - Smartly reuse an available specialist session - context reuse saves time and tokens
19139
19147
  - When too much unrelated, and really needed, start a fresh session with the specialist
19140
19148
  - If multiple remembered sessions fit, prefer the most recently used matching session.
19141
19149
  - Prefer re-uses over creating new sessions all the time
@@ -19187,7 +19195,7 @@ When user's approach seems problematic:
19187
19195
  **Bad:** "Great question! Let me think about the best approach here. I'm going to delegate to @librarian to check the latest Next.js documentation for the App Router, and then I'll implement the solution for you."
19188
19196
 
19189
19197
  **Good:** "Checking Next.js App Router docs via @librarian..."
19190
- [proceeds with implementation]
19198
+ [continues scheduling or integration]
19191
19199
 
19192
19200
  </Communication>
19193
19201
  `;
@@ -22600,6 +22608,224 @@ function createDisplayNameMentionRewriter(config) {
22600
22608
  return rewritten;
22601
22609
  };
22602
22610
  }
22611
+ // src/utils/task.ts
22612
+ function parseTaskIdFromTaskOutput(output) {
22613
+ const lines = output.split(/\r?\n/);
22614
+ for (const line of lines) {
22615
+ const trimmed = line.trim();
22616
+ const match = /^task_id:\s*([^\s()]+)(?:\s*\(.*)?$/.exec(trimmed);
22617
+ if (!match) {
22618
+ continue;
22619
+ }
22620
+ return match[1];
22621
+ }
22622
+ return;
22623
+ }
22624
+ function parseTaskLaunchOutput(output) {
22625
+ const taskID = parseTaskIdFromTaskOutput(output);
22626
+ const state = parseTaskStateFromOutput(output);
22627
+ if (!taskID || state !== "running")
22628
+ return;
22629
+ return {
22630
+ taskID,
22631
+ state,
22632
+ result: parseTaskResultFromOutput(output)
22633
+ };
22634
+ }
22635
+ function parseTaskStatusOutput(output) {
22636
+ const taskID = parseTaskIdFromTaskOutput(output);
22637
+ const state = parseTaskStateFromOutput(output);
22638
+ if (!taskID || !state)
22639
+ return;
22640
+ return {
22641
+ taskID,
22642
+ state,
22643
+ timedOut: state === "running" && /Timed out after \d+ms/i.test(output),
22644
+ result: parseTaskResultFromOutput(output)
22645
+ };
22646
+ }
22647
+ function parseTaskStateFromOutput(output) {
22648
+ for (const line of getTaskHeader(output).split(/\r?\n/)) {
22649
+ const match = /^state:\s*(running|completed|error|cancelled)\s*$/i.exec(line.trim());
22650
+ if (match)
22651
+ return match[1].toLowerCase();
22652
+ }
22653
+ return;
22654
+ }
22655
+ function parseTaskResultFromOutput(output) {
22656
+ const match = /<task_(result|error)>\s*([\s\S]*?)\s*<\/task_\1>/m.exec(output);
22657
+ const result = match?.[2]?.trim();
22658
+ return result || undefined;
22659
+ }
22660
+ function getTaskHeader(output) {
22661
+ const resultIndex = output.search(/<task_(?:result|error)>/);
22662
+ if (resultIndex === -1)
22663
+ return output;
22664
+ return output.slice(0, resultIndex);
22665
+ }
22666
+
22667
+ // src/utils/background-job-board.ts
22668
+ var TERMINAL_STATES = new Set([
22669
+ "completed",
22670
+ "error",
22671
+ "cancelled"
22672
+ ]);
22673
+ var AGENT_PREFIX = {
22674
+ council: "cou",
22675
+ designer: "des",
22676
+ explorer: "exp",
22677
+ fixer: "fix",
22678
+ librarian: "lib",
22679
+ observer: "obs",
22680
+ oracle: "ora"
22681
+ };
22682
+
22683
+ class BackgroundJobBoard {
22684
+ jobs = new Map;
22685
+ counters = new Map;
22686
+ registerLaunch(input) {
22687
+ const now = input.now ?? Date.now();
22688
+ const existing = this.jobs.get(input.taskID);
22689
+ if (existing) {
22690
+ const updated = {
22691
+ ...existing,
22692
+ agent: input.agent || existing.agent,
22693
+ description: input.description || existing.description,
22694
+ objective: input.objective ?? existing.objective,
22695
+ state: "running",
22696
+ timedOut: false,
22697
+ terminalUnreconciled: false,
22698
+ completedAt: undefined,
22699
+ resultSummary: undefined,
22700
+ updatedAt: now
22701
+ };
22702
+ this.jobs.set(input.taskID, updated);
22703
+ return updated;
22704
+ }
22705
+ const record = {
22706
+ taskID: input.taskID,
22707
+ parentSessionID: input.parentSessionID,
22708
+ agent: input.agent,
22709
+ description: input.description || `background ${input.agent} task`,
22710
+ objective: input.objective,
22711
+ state: "running",
22712
+ timedOut: false,
22713
+ terminalUnreconciled: false,
22714
+ launchedAt: now,
22715
+ updatedAt: now,
22716
+ alias: this.nextAlias(input.parentSessionID, input.agent)
22717
+ };
22718
+ this.jobs.set(input.taskID, record);
22719
+ return record;
22720
+ }
22721
+ updateStatus(input) {
22722
+ const existing = this.jobs.get(input.taskID);
22723
+ if (!existing)
22724
+ return;
22725
+ if (existing.state === "reconciled") {
22726
+ return existing;
22727
+ }
22728
+ const now = input.now ?? Date.now();
22729
+ const terminal = TERMINAL_STATES.has(input.state);
22730
+ const updated = {
22731
+ ...existing,
22732
+ state: input.state,
22733
+ timedOut: input.timedOut ?? false,
22734
+ terminalUnreconciled: terminal ? true : existing.terminalUnreconciled,
22735
+ updatedAt: now,
22736
+ completedAt: terminal ? existing.completedAt ?? now : existing.completedAt,
22737
+ resultSummary: input.resultSummary ?? existing.resultSummary
22738
+ };
22739
+ this.jobs.set(input.taskID, updated);
22740
+ return updated;
22741
+ }
22742
+ updateFromStatusOutput(output) {
22743
+ const status = parseTaskStatusOutput(output);
22744
+ if (!status)
22745
+ return;
22746
+ return this.updateStatus({
22747
+ taskID: status.taskID,
22748
+ state: status.state,
22749
+ timedOut: status.timedOut,
22750
+ resultSummary: status.result
22751
+ });
22752
+ }
22753
+ markReconciled(taskID, now = Date.now()) {
22754
+ const existing = this.jobs.get(taskID);
22755
+ if (!existing)
22756
+ return;
22757
+ if (!existing.terminalUnreconciled && !TERMINAL_STATES.has(existing.state)) {
22758
+ return;
22759
+ }
22760
+ const updated = {
22761
+ ...existing,
22762
+ state: "reconciled",
22763
+ terminalUnreconciled: false,
22764
+ updatedAt: now
22765
+ };
22766
+ this.jobs.set(taskID, updated);
22767
+ return updated;
22768
+ }
22769
+ get(taskID) {
22770
+ return this.jobs.get(taskID);
22771
+ }
22772
+ list(parentSessionID) {
22773
+ const jobs = [...this.jobs.values()];
22774
+ const filtered = parentSessionID ? jobs.filter((job) => job.parentSessionID === parentSessionID) : jobs;
22775
+ return filtered.sort((a, b) => a.launchedAt - b.launchedAt);
22776
+ }
22777
+ hasRunning(parentSessionID) {
22778
+ return this.list(parentSessionID).some((job) => job.state === "running");
22779
+ }
22780
+ hasTerminalUnreconciled(parentSessionID) {
22781
+ return this.list(parentSessionID).some((job) => job.terminalUnreconciled);
22782
+ }
22783
+ formatForPrompt(parentSessionID) {
22784
+ const jobs = this.list(parentSessionID).filter((job) => job.state === "running" || job.terminalUnreconciled);
22785
+ if (jobs.length === 0)
22786
+ return;
22787
+ return [
22788
+ "### Background Job Board",
22789
+ "Use task_status before consuming running jobs. Reconcile terminal jobs before final response.",
22790
+ "",
22791
+ ...jobs.map(formatJob)
22792
+ ].join(`
22793
+ `);
22794
+ }
22795
+ clearParent(parentSessionID) {
22796
+ for (const job of this.list(parentSessionID)) {
22797
+ this.jobs.delete(job.taskID);
22798
+ }
22799
+ }
22800
+ drop(taskID) {
22801
+ this.jobs.delete(taskID);
22802
+ }
22803
+ nextAlias(parentSessionID, agent) {
22804
+ const prefix2 = AGENT_PREFIX[agent] ?? (agent.slice(0, 3) || "job");
22805
+ const key = `${parentSessionID}:${prefix2}`;
22806
+ const next = (this.counters.get(key) ?? 0) + 1;
22807
+ this.counters.set(key, next);
22808
+ return `${prefix2}-${next}`;
22809
+ }
22810
+ }
22811
+ function formatJob(job) {
22812
+ const status = job.terminalUnreconciled ? `${job.state}, unreconciled` : job.timedOut ? `${job.state}, timed out` : job.state;
22813
+ const lines = [
22814
+ `- ${job.alias} / ${job.taskID} / ${job.agent} / ${status}`,
22815
+ ` Objective: ${job.objective || job.description}`
22816
+ ];
22817
+ if (job.resultSummary && job.terminalUnreconciled) {
22818
+ lines.push(` Result: ${singleLine(job.resultSummary)}`);
22819
+ }
22820
+ return lines.join(`
22821
+ `);
22822
+ }
22823
+ function singleLine(value) {
22824
+ const normalized = value.replace(/\s+/g, " ").trim();
22825
+ if (normalized.length <= 160)
22826
+ return normalized;
22827
+ return `${normalized.slice(0, 157)}...`;
22828
+ }
22603
22829
  // src/utils/internal-initiator.ts
22604
22830
  var SLIM_INTERNAL_INITIATOR_MARKER = "<!-- SLIM_INTERNAL_INITIATOR -->";
22605
22831
  function isRecord(value) {
@@ -22783,7 +23009,7 @@ class SessionManager {
22783
23009
  return;
22784
23010
  return [
22785
23011
  "### Resumable Sessions",
22786
- "Reuse only for clear continuation of the same thread. Otherwise start fresh.",
23012
+ "Reuse only completed/reconciled threads. Poll running jobs from Background Job Board.",
22787
23013
  "",
22788
23014
  ...lines
22789
23015
  ].join(`
@@ -22851,19 +23077,6 @@ function formatContextFiles(files, options) {
22851
23077
  const rendered = shown.map((file) => `${file.path} (${file.lineCount} lines)`);
22852
23078
  return `${rendered.join(", ")}${rest > 0 ? ` (+${rest} more)` : ""}`;
22853
23079
  }
22854
- // src/utils/task.ts
22855
- function parseTaskIdFromTaskOutput(output) {
22856
- const lines = output.split(/\r?\n/);
22857
- for (const line of lines) {
22858
- const trimmed = line.trim();
22859
- const match = /^task_id:\s*([^\s()]+)(?:\s*\(.*)?$/.exec(trimmed);
22860
- if (!match) {
22861
- continue;
22862
- }
22863
- return match[1];
22864
- }
22865
- return;
22866
- }
22867
23080
  // src/utils/zip-extractor.ts
22868
23081
  import { spawnSync } from "node:child_process";
22869
23082
  import { release } from "node:os";
@@ -23677,14 +23890,13 @@ function createPhaseReminderHook() {
23677
23890
  if (originalText.includes(SLIM_INTERNAL_INITIATOR_MARKER)) {
23678
23891
  return;
23679
23892
  }
23680
- if (originalText.includes(PHASE_REMINDER)) {
23893
+ if (lastUserMessage.parts.some((p) => p.text?.includes(PHASE_REMINDER))) {
23681
23894
  return;
23682
23895
  }
23683
- lastUserMessage.parts[textPartIndex].text = `${originalText}
23684
-
23685
- ---
23686
-
23687
- ${PHASE_REMINDER}`;
23896
+ lastUserMessage.parts.push({
23897
+ type: "text",
23898
+ text: PHASE_REMINDER
23899
+ });
23688
23900
  }
23689
23901
  };
23690
23902
  }
@@ -24066,6 +24278,34 @@ var AGENT_NAME_SET = new Set([
24066
24278
  var MAX_PENDING_TASK_CALLS = 100;
24067
24279
  var RESUMABLE_SESSIONS_START = "<resumable_sessions>";
24068
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
+ }
24069
24309
  function isAgentName(value) {
24070
24310
  return typeof value === "string" && AGENT_NAME_SET.has(value);
24071
24311
  }
@@ -24109,10 +24349,14 @@ function createTaskSessionManagerHook(_ctx, options) {
24109
24349
  readContextMinLines: options.readContextMinLines,
24110
24350
  readContextMaxFiles: options.readContextMaxFiles
24111
24351
  });
24352
+ const backgroundJobBoard = options.backgroundJobBoard ?? new BackgroundJobBoard;
24112
24353
  const pendingCalls = new Map;
24113
24354
  const pendingCallOrder = [];
24114
24355
  const contextByTask = new Map;
24115
24356
  const pendingManagedTaskIds = new Set;
24357
+ const terminalJobsInjectedByParent = new Map;
24358
+ const processedInjectedCompletions = new Set;
24359
+ const processedInjectedCompletionOrder = [];
24116
24360
  let anonymousPendingCallId = 0;
24117
24361
  function addTaskContext(taskId, files) {
24118
24362
  if (files.length === 0)
@@ -24156,6 +24400,62 @@ function createTaskSessionManagerHook(_ctx, options) {
24156
24400
  }
24157
24401
  }
24158
24402
  }
24403
+ function updateBackgroundJobFromOutput(output) {
24404
+ if (typeof output !== "string")
24405
+ return;
24406
+ const status = parseTaskStatusOutput(output);
24407
+ if (!status)
24408
+ return;
24409
+ const updated = backgroundJobBoard.updateStatus({
24410
+ taskID: status.taskID,
24411
+ state: status.state,
24412
+ timedOut: status.timedOut,
24413
+ resultSummary: status.result
24414
+ });
24415
+ if (!updated)
24416
+ return;
24417
+ if (updated.terminalUnreconciled) {
24418
+ pendingManagedTaskIds.delete(updated.taskID);
24419
+ contextByTask.delete(updated.taskID);
24420
+ pruneContext();
24421
+ }
24422
+ return updated;
24423
+ }
24424
+ function updateFromInjectedCompletion(part, message, _messageIndex, partIndex) {
24425
+ if (part.type !== "text" || typeof part.text !== "string") {
24426
+ return;
24427
+ }
24428
+ const isCompleted = BACKGROUND_COMPLETION_COMPLETED.test(part.text);
24429
+ const isFailed = BACKGROUND_COMPLETION_FAILED.test(part.text);
24430
+ if (part.synthetic !== true || !isCompleted && !isFailed) {
24431
+ return;
24432
+ }
24433
+ const status = parseTaskStatusOutput(part.text);
24434
+ if (!status)
24435
+ return;
24436
+ if (isCompleted && status.state !== "completed")
24437
+ return;
24438
+ if (isFailed && status.state !== "error")
24439
+ return;
24440
+ const occurrenceId = createOccurrenceId(part, message, partIndex);
24441
+ if (processedInjectedCompletions.has(occurrenceId))
24442
+ return;
24443
+ const updated = updateBackgroundJobFromOutput(part.text);
24444
+ if (!updated)
24445
+ return;
24446
+ rememberProcessedInjectedCompletion(occurrenceId);
24447
+ return updated;
24448
+ }
24449
+ function rememberProcessedInjectedCompletion(signature) {
24450
+ processedInjectedCompletions.add(signature);
24451
+ processedInjectedCompletionOrder.push(signature);
24452
+ while (processedInjectedCompletionOrder.length > MAX_PROCESSED_INJECTED_COMPLETIONS) {
24453
+ const evicted = processedInjectedCompletionOrder.shift();
24454
+ if (!evicted)
24455
+ break;
24456
+ processedInjectedCompletions.delete(evicted);
24457
+ }
24458
+ }
24159
24459
  function isMissingRememberedSessionError(output) {
24160
24460
  const firstLine = output.split(/\r?\n/, 1)[0]?.trim().toLowerCase() ?? "";
24161
24461
  return firstLine.startsWith("[error]") && firstLine.includes("session") && (firstLine.includes("not found") || firstLine.includes("no session"));
@@ -24195,6 +24495,25 @@ function createTaskSessionManagerHook(_ctx, options) {
24195
24495
  return;
24196
24496
  return pendingCallOrder.find((callId) => pendingCalls.get(callId)?.parentSessionId === parentSessionId);
24197
24497
  }
24498
+ function rememberInjectedTerminalJobs(parentSessionID) {
24499
+ const taskIDs = backgroundJobBoard.list(parentSessionID).filter((job) => job.terminalUnreconciled).map((job) => job.taskID);
24500
+ if (taskIDs.length === 0)
24501
+ return;
24502
+ const existing = terminalJobsInjectedByParent.get(parentSessionID) ?? new Set;
24503
+ for (const taskID of taskIDs) {
24504
+ existing.add(taskID);
24505
+ }
24506
+ terminalJobsInjectedByParent.set(parentSessionID, existing);
24507
+ }
24508
+ function reconcileInjectedTerminalJobs(parentSessionID) {
24509
+ const taskIDs = terminalJobsInjectedByParent.get(parentSessionID);
24510
+ if (!taskIDs)
24511
+ return;
24512
+ for (const taskID of taskIDs) {
24513
+ backgroundJobBoard.markReconciled(taskID);
24514
+ }
24515
+ terminalJobsInjectedByParent.delete(parentSessionID);
24516
+ }
24198
24517
  return {
24199
24518
  "tool.execute.before": async (input, output) => {
24200
24519
  if (input.tool.toLowerCase() !== "task")
@@ -24244,11 +24563,31 @@ function createTaskSessionManagerHook(_ctx, options) {
24244
24563
  }
24245
24564
  return;
24246
24565
  }
24566
+ if (input.tool.toLowerCase() === "task_status") {
24567
+ if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
24568
+ return;
24569
+ }
24570
+ updateBackgroundJobFromOutput(output.output);
24571
+ return;
24572
+ }
24247
24573
  if (input.tool.toLowerCase() !== "task")
24248
24574
  return;
24249
24575
  const pending = takePendingCall(input.callID, input.sessionID);
24250
24576
  if (!pending || typeof output.output !== "string")
24251
24577
  return;
24578
+ const launch = parseTaskLaunchOutput(output.output);
24579
+ if (launch) {
24580
+ backgroundJobBoard.registerLaunch({
24581
+ taskID: launch.taskID,
24582
+ parentSessionID: pending.parentSessionId,
24583
+ agent: pending.agentType,
24584
+ description: pending.label,
24585
+ objective: pending.label
24586
+ });
24587
+ sessionManager.drop(pending.parentSessionId, pending.agentType, pending.resumedTaskId ?? launch.taskID);
24588
+ pendingManagedTaskIds.add(launch.taskID);
24589
+ return;
24590
+ }
24252
24591
  const taskId = parseTaskIdFromTaskOutput(output.output);
24253
24592
  if (!taskId) {
24254
24593
  if (pending.resumedTaskId && isMissingRememberedSessionError(output.output)) {
@@ -24271,6 +24610,19 @@ function createTaskSessionManagerHook(_ctx, options) {
24271
24610
  pruneContext();
24272
24611
  },
24273
24612
  "experimental.chat.messages.transform": async (_input, output) => {
24613
+ for (const [messageIndex, message] of output.messages.entries()) {
24614
+ if (message.info.role !== "user")
24615
+ continue;
24616
+ if (message.info.agent && message.info.agent !== "orchestrator") {
24617
+ continue;
24618
+ }
24619
+ if (!message.info.sessionID || !options.shouldManageSession(message.info.sessionID)) {
24620
+ continue;
24621
+ }
24622
+ for (const [partIndex, part] of message.parts.entries()) {
24623
+ updateFromInjectedCompletion(part, message, messageIndex, partIndex);
24624
+ }
24625
+ }
24274
24626
  for (let i = output.messages.length - 1;i >= 0; i -= 1) {
24275
24627
  const message = output.messages[i];
24276
24628
  if (message.info.role !== "user")
@@ -24280,8 +24632,11 @@ function createTaskSessionManagerHook(_ctx, options) {
24280
24632
  if (!message.info.sessionID || !options.shouldManageSession(message.info.sessionID)) {
24281
24633
  return;
24282
24634
  }
24283
- const reminder = sessionManager.formatForPrompt(message.info.sessionID);
24284
- if (!reminder)
24635
+ const reminders = [
24636
+ backgroundJobBoard.formatForPrompt(message.info.sessionID),
24637
+ sessionManager.formatForPrompt(message.info.sessionID)
24638
+ ].filter((item) => Boolean(item));
24639
+ if (reminders.length === 0)
24285
24640
  return;
24286
24641
  const textPart = message.parts.find((part) => part.type === "text" && typeof part.text === "string");
24287
24642
  if (!textPart)
@@ -24290,11 +24645,14 @@ function createTaskSessionManagerHook(_ctx, options) {
24290
24645
  return;
24291
24646
  if (textPart.text?.includes(RESUMABLE_SESSIONS_START))
24292
24647
  return;
24648
+ rememberInjectedTerminalJobs(message.info.sessionID);
24293
24649
  textPart.text = [
24294
24650
  textPart.text ?? "",
24295
24651
  "",
24296
24652
  RESUMABLE_SESSIONS_START,
24297
- reminder,
24653
+ reminders.join(`
24654
+
24655
+ `),
24298
24656
  RESUMABLE_SESSIONS_END
24299
24657
  ].join(`
24300
24658
  `);
@@ -24309,6 +24667,20 @@ function createTaskSessionManagerHook(_ctx, options) {
24309
24667
  }
24310
24668
  return;
24311
24669
  }
24670
+ if (input.event.type === "session.idle" || input.event.type === "session.status" && input.event.properties?.status?.type === "idle") {
24671
+ const sessionId2 = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
24672
+ if (sessionId2 && options.shouldManageSession(sessionId2)) {
24673
+ reconcileInjectedTerminalJobs(sessionId2);
24674
+ }
24675
+ return;
24676
+ }
24677
+ if (input.event.type === "session.error") {
24678
+ const sessionId2 = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
24679
+ if (sessionId2 && options.shouldManageSession(sessionId2)) {
24680
+ terminalJobsInjectedByParent.delete(sessionId2);
24681
+ }
24682
+ return;
24683
+ }
24312
24684
  if (input.event.type !== "session.deleted")
24313
24685
  return;
24314
24686
  const sessionId = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
@@ -24316,6 +24688,8 @@ function createTaskSessionManagerHook(_ctx, options) {
24316
24688
  return;
24317
24689
  sessionManager.dropTask(sessionId);
24318
24690
  sessionManager.clearParent(sessionId);
24691
+ backgroundJobBoard.clearParent(sessionId);
24692
+ terminalJobsInjectedByParent.delete(sessionId);
24319
24693
  contextByTask.delete(sessionId);
24320
24694
  pendingManagedTaskIds.delete(sessionId);
24321
24695
  pruneContext();
@@ -24544,6 +24918,7 @@ function createTodoContinuationHook(ctx, config) {
24544
24918
  const cooldownMs = config?.cooldownMs ?? 3000;
24545
24919
  const autoEnable = config?.autoEnable ?? false;
24546
24920
  const autoEnableThreshold = config?.autoEnableThreshold ?? 4;
24921
+ const backgroundJobBoard = config?.backgroundJobBoard;
24547
24922
  const requestSignatureBySession = new Map;
24548
24923
  const state = {
24549
24924
  enabled: false,
@@ -24655,8 +25030,10 @@ function createTodoContinuationHook(ctx, config) {
24655
25030
  }
24656
25031
  if (requestSignatureBySession.get(lastUserMessage.sessionID) === lastUserMessage.signature) {
24657
25032
  const reminder = hygiene.getPendingReminder(lastUserMessage.sessionID);
24658
- if (reminder) {
24659
- appendTodoHygieneInstruction(lastUserMessage.message, reminder);
25033
+ const guardrail = backgroundGuardrail(lastUserMessage.sessionID);
25034
+ const combinedReminder = [reminder, guardrail].filter((item) => Boolean(item)).join(" ");
25035
+ if (combinedReminder) {
25036
+ appendTodoHygieneInstruction(lastUserMessage.message, combinedReminder);
24660
25037
  } else {
24661
25038
  stripTodoHygieneInstructionFromMessage(lastUserMessage.message);
24662
25039
  }
@@ -24694,6 +25071,28 @@ function createTodoContinuationHook(ctx, config) {
24694
25071
  function registerOrchestratorSession(sessionID) {
24695
25072
  state.orchestratorSessionIds.add(sessionID);
24696
25073
  }
25074
+ function backgroundGuardrail(sessionID) {
25075
+ if (!backgroundJobBoard)
25076
+ return;
25077
+ const hasRunning = backgroundJobBoard.hasRunning(sessionID);
25078
+ const hasTerminal = backgroundJobBoard.hasTerminalUnreconciled(sessionID);
25079
+ if (hasRunning && hasTerminal) {
25080
+ return "Background jobs are still unresolved: call task_status for running jobs and reconcile terminal Background Job Board results before dependent work or finalizing.";
25081
+ }
25082
+ if (hasTerminal) {
25083
+ return "Background jobs have terminal results: reconcile the Background Job Board results before finalizing.";
25084
+ }
25085
+ if (hasRunning) {
25086
+ return "Background jobs are still running: call task_status before dependent work or finalizing.";
25087
+ }
25088
+ return;
25089
+ }
25090
+ function continuationPrompt(sessionID) {
25091
+ const guardrail = backgroundGuardrail(sessionID);
25092
+ if (!guardrail)
25093
+ return CONTINUATION_PROMPT;
25094
+ return `${CONTINUATION_PROMPT} ${guardrail}`;
25095
+ }
24697
25096
  function handleChatMessage(input) {
24698
25097
  if (!input.agent) {
24699
25098
  return;
@@ -24885,7 +25284,9 @@ function createTodoContinuationHook(ctx, config) {
24885
25284
  await ctx.client.session.prompt({
24886
25285
  path: { id: sessionID },
24887
25286
  body: {
24888
- parts: [createInternalAgentTextPart(CONTINUATION_PROMPT)]
25287
+ parts: [
25288
+ createInternalAgentTextPart(continuationPrompt(sessionID))
25289
+ ]
24889
25290
  }
24890
25291
  });
24891
25292
  state.consecutiveContinuations++;
@@ -24996,7 +25397,7 @@ function createTodoContinuationHook(ctx, config) {
24996
25397
  });
24997
25398
  }
24998
25399
  if (hasIncompleteTodos) {
24999
- output.parts.push(createInternalAgentTextPart(`${CONTINUATION_PROMPT} [Auto-continue enabled: up to ${maxContinuations} continuations.]`));
25400
+ output.parts.push(createInternalAgentTextPart(`${continuationPrompt(input.sessionID)} [Auto-continue enabled: up to ${maxContinuations} continuations.]`));
25000
25401
  } else {
25001
25402
  output.parts.push(createInternalAgentTextPart(`[Auto-continue: enabled for up to ${maxContinuations} continuations. No incomplete todos right now.]`));
25002
25403
  }
@@ -33436,6 +33837,7 @@ var OhMyOpenCodeLite = async (ctx) => {
33436
33837
  let todoContinuationHook;
33437
33838
  let sessionGoalHook;
33438
33839
  let taskSessionManagerHook;
33840
+ let backgroundJobBoard;
33439
33841
  let interviewManager;
33440
33842
  let presetManager;
33441
33843
  let divoomManager;
@@ -33521,11 +33923,13 @@ var OhMyOpenCodeLite = async (ctx) => {
33521
33923
  applyPatchHook = createApplyPatchHook(ctx);
33522
33924
  jsonErrorRecoveryHook = createJsonErrorRecoveryHook(ctx);
33523
33925
  foregroundFallback = new ForegroundFallbackManager(ctx.client, runtimeChains, config.fallback?.enabled !== false && Object.keys(runtimeChains).length > 0);
33926
+ backgroundJobBoard = new BackgroundJobBoard;
33524
33927
  todoContinuationHook = createTodoContinuationHook(ctx, {
33525
33928
  maxContinuations: config.todoContinuation?.maxContinuations ?? 5,
33526
33929
  cooldownMs: config.todoContinuation?.cooldownMs ?? 3000,
33527
33930
  autoEnable: config.todoContinuation?.autoEnable ?? false,
33528
- autoEnableThreshold: config.todoContinuation?.autoEnableThreshold ?? 4
33931
+ autoEnableThreshold: config.todoContinuation?.autoEnableThreshold ?? 4,
33932
+ backgroundJobBoard
33529
33933
  });
33530
33934
  sessionGoalHook = createSessionGoalHook(ctx, config, {
33531
33935
  getAgentName: (sessionID) => sessionAgentMap.get(sessionID)
@@ -33534,6 +33938,7 @@ var OhMyOpenCodeLite = async (ctx) => {
33534
33938
  maxSessionsPerAgent: config.sessionManager?.maxSessionsPerAgent ?? 2,
33535
33939
  readContextMinLines: config.sessionManager?.readContextMinLines ?? 10,
33536
33940
  readContextMaxFiles: config.sessionManager?.readContextMaxFiles ?? 8,
33941
+ backgroundJobBoard,
33537
33942
  shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
33538
33943
  });
33539
33944
  interviewManager = createInterviewManager(ctx, config);