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