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