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