opencode-hive 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -12931,9 +12931,9 @@ Each agent gets:
12931
12931
 
12932
12932
  \`\`\`typescript
12933
12933
  // Using Hive tools for parallel execution
12934
- hive_worktree_create({ task: "01-fix-abort-tests" })
12935
- hive_worktree_create({ task: "02-fix-batch-tests" })
12936
- hive_worktree_create({ task: "03-fix-race-condition-tests" })
12934
+ hive_worktree_start({ task: "01-fix-abort-tests" })
12935
+ hive_worktree_start({ task: "02-fix-batch-tests" })
12936
+ hive_worktree_start({ task: "03-fix-race-condition-tests" })
12937
12937
  // All three run concurrently in isolated worktrees
12938
12938
  \`\`\`
12939
12939
 
@@ -13433,7 +13433,7 @@ Only \`done\` satisfies dependencies (not \`blocked\`, \`failed\`, \`partial\`,
13433
13433
  ### Step 3: Execute Batch
13434
13434
 
13435
13435
  For each task in the batch:
13436
- 1. Mark as in_progress via \`hive_worktree_create()\`
13436
+ 1. Mark as in_progress via \`hive_worktree_start()\`
13437
13437
  2. Follow each step exactly (plan has bite-sized steps)
13438
13438
  3. Run verifications as specified
13439
13439
  4. Mark as completed
@@ -13501,7 +13501,7 @@ When you need to answer "where/how does X work?" across multiple domains (codeba
13501
13501
 
13502
13502
  **Safe in Planning mode:** This is read-only exploration. It is OK to use during exploratory research even when there is no feature, no plan, and no approved tasks.
13503
13503
 
13504
- **This skill is for read-only research.** For parallel implementation work, use \`hive_skill("dispatching-parallel-agents")\` with \`hive_worktree_create\`.
13504
+ **This skill is for read-only research.** For parallel implementation work, use \`hive_skill("dispatching-parallel-agents")\` with \`hive_worktree_start\`.
13505
13505
 
13506
13506
  ## When to Use
13507
13507
 
@@ -13512,7 +13512,7 @@ When you need to answer "where/how does X work?" across multiple domains (codeba
13512
13512
  - Questions are independent (answer to A doesn't affect B)
13513
13513
  - User asks **3+ independent questions** (often as a numbered list or separate bullets)
13514
13514
  - No edits needed (read-only exploration)
13515
- - User asks for an explorationthat likely spans multiple files/packages
13515
+ - User asks for an exploration that likely spans multiple files/packages
13516
13516
  - The work is read-only and the questions can be investigated independently
13517
13517
 
13518
13518
  **Only skip this skill when:**
@@ -14852,7 +14852,7 @@ Intent Verbalization — verbalize before acting:
14852
14852
  ### Delegation
14853
14853
  - Single-scout research → \`task({ subagent_type: "scout-researcher", prompt: "..." })\`
14854
14854
  - Parallel exploration → Load \`hive_skill("parallel-exploration")\` and follow the task mode delegation guidance.
14855
- - Implementation → \`hive_worktree_create({ task: "01-task-name" })\` (creates worktree + Forager)
14855
+ - Implementation → \`hive_worktree_start({ task: "01-task-name" })\` (creates worktree + Forager)
14856
14856
 
14857
14857
  During Planning, use \`task({ subagent_type: "scout-researcher", ... })\` for exploration (BLOCKING — returns when done). For parallel exploration, issue multiple \`task()\` calls in the same message.
14858
14858
 
@@ -14945,7 +14945,7 @@ Each task declares dependencies with **Depends on**:
14945
14945
  ### After Plan Written
14946
14946
  Ask user via \`question()\`: "Plan complete. Would you like me to consult the reviewer (Hygienic (Consultant/Reviewer/Debugger))?"
14947
14947
 
14948
- If yes → \`task({ subagent_type: "hygienic", prompt: "Review plan..." })\`
14948
+ If yes → default to built-in \`hygienic-reviewer\`; choose a configured hygienic-derived reviewer only when its description in \`Configured Custom Subagents\` is a better match. Then run \`task({ subagent_type: "<chosen-reviewer>", prompt: "Review plan..." })\`.
14949
14949
 
14950
14950
  After review decision, offer execution choice (subagent-driven vs parallel session) consistent with writing-plans.
14951
14951
 
@@ -14978,15 +14978,17 @@ Use \`hive_status()\` to see **runnable** tasks (dependencies satisfied) and **b
14978
14978
 
14979
14979
  ### Worker Spawning
14980
14980
  \`\`\`
14981
- hive_worktree_create({ task: "01-task-name" }) // Creates worktree + Forager
14981
+ hive_worktree_start({ task: "01-task-name" }) // Creates worktree + Forager
14982
14982
  \`\`\`
14983
14983
 
14984
14984
  ### After Delegation
14985
14985
  1. \`task()\` is blocking — when it returns, the worker is done
14986
- 2. Immediately call \`hive_status()\` to check the new task state and find next runnable tasks
14987
- 3. The delegated task MUST transition out of \`in_progress\`; if still \`in_progress\`, resume worker with explicit instruction to resolve commit response and retry
14988
- 4. If task status is blocked: read blocker info \`question()\` user decision resume with \`continueFrom: "blocked"\`
14989
- 5. Skip polling the result is available when \`task()\` returns
14986
+ 2. After \`task()\` returns, immediately call \`hive_status()\` to check the new task state and find next runnable tasks before any resume attempt
14987
+ 3. Use \`continueFrom: "blocked"\` only when status is exactly \`blocked\`
14988
+ 4. If status is not \`blocked\`, do not use \`continueFrom: "blocked"\`; use \`hive_worktree_start({ feature, task })\` only for normal starts (\`pending\` / \`in_progress\`)
14989
+ 5. Never loop \`continueFrom: "blocked"\` on non-blocked statuses
14990
+ 6. If task status is blocked: read blocker info → \`question()\` → user decision → resume with \`continueFrom: "blocked"\`
14991
+ 7. Skip polling — the result is available when \`task()\` returns
14990
14992
 
14991
14993
  ### Batch Merge + Verify Workflow
14992
14994
  When multiple tasks are in flight, prefer **batch completion** over per-task verification:
@@ -15008,8 +15010,9 @@ When multiple tasks are in flight, prefer **batch completion** over per-task ver
15008
15010
  ### Post-Batch Review (Hygienic)
15009
15011
  After completing and merging a batch:
15010
15012
  1. Ask the user via \`question()\` if they want a Hygienic code review for the batch.
15011
- 2. If yes, run \`task({ subagent_type: "hygienic", prompt: "Review implementation changes from the latest batch." })\`.
15012
- 3. Apply feedback before starting the next batch.
15013
+ 2. If yes default to built-in \`hygienic-reviewer\`; choose a configured hygienic-derived reviewer only when its description in \`Configured Custom Subagents\` is a better match.
15014
+ 3. Then run \`task({ subagent_type: "<chosen-reviewer>", prompt: "Review implementation changes from the latest batch." })\`.
15015
+ 4. Apply feedback before starting the next batch.
15013
15016
 
15014
15017
  ### AGENTS.md Maintenance
15015
15018
  After feature completion (all tasks merged):
@@ -15219,7 +15222,7 @@ Standard checks: specialized agent? can I do it myself for sure? external system
15219
15222
  ## Worker Spawning
15220
15223
 
15221
15224
  \`\`\`
15222
- hive_worktree_create({ task: "01-task-name" })
15225
+ hive_worktree_start({ task: "01-task-name" })
15223
15226
  // If external system data is needed (parallel exploration):
15224
15227
  // Load hive_skill("parallel-exploration") for the full playbook, then:
15225
15228
  // In task mode, use task() for research fan-out.
@@ -15227,8 +15230,10 @@ hive_worktree_create({ task: "01-task-name" })
15227
15230
 
15228
15231
  Delegation guidance:
15229
15232
  - \`task()\` is BLOCKING — returns when the worker is done
15230
- - Call \`hive_status()\` immediately after to check new state and find next runnable tasks
15231
- - Invariant: delegated task must not remain \`in_progress\`; if it does, treat as non-terminal completion and resume/retry worker with explicit commit-result handling
15233
+ - After \`task()\` returns, call \`hive_status()\` immediately to check new state and find next runnable tasks before any resume attempt
15234
+ - Use \`continueFrom: "blocked"\` only when status is exactly \`blocked\`
15235
+ - If status is not \`blocked\`, do not use \`continueFrom: "blocked"\`; use \`hive_worktree_start({ feature, task })\` only for normal starts (\`pending\` / \`in_progress\`)
15236
+ - Never loop \`continueFrom: "blocked"\` on non-blocked statuses
15232
15237
  - For parallel fan-out, issue multiple \`task()\` calls in the same message
15233
15238
 
15234
15239
  ## After Delegation - VERIFY
@@ -15256,7 +15261,7 @@ After completing and merging a batch, run full verification on the main branch:
15256
15261
 
15257
15262
  ## Blocker Handling
15258
15263
 
15259
- When worker reports blocked: \`hive_status()\` → read blocker info; \`question()\` → ask user (no plain text); \`hive_worktree_create({ task, continueFrom: "blocked", decision })\`.
15264
+ When worker reports blocked: \`hive_status()\` → confirm status is exactly \`blocked\` → read blocker info; \`question()\` → ask user (no plain text); \`hive_worktree_create({ task, continueFrom: "blocked", decision })\`. If status is not \`blocked\`, do not use blocked resume; only use \`hive_worktree_start({ feature, task })\` for normal starts (\`pending\` / \`in_progress\`).
15260
15265
 
15261
15266
  ## Failure Recovery (After 3 Consecutive Failures)
15262
15267
 
@@ -15275,7 +15280,9 @@ Merge after batch completes, then verify the merged result.
15275
15280
 
15276
15281
  ### Post-Batch Review (Hygienic)
15277
15282
 
15278
- After completing and merging a batch: ask via \`question()\` if they want a Hygienic review. If yes, run \`task({ subagent_type: "hygienic", prompt: "Review implementation changes from the latest batch." })\` and apply feedback before the next batch.
15283
+ After completing and merging a batch: ask via \`question()\` if they want a Hygienic review.
15284
+ If yes, default to built-in \`hygienic-reviewer\`; choose a configured hygienic-derived reviewer only when its description in \`Configured Custom Subagents\` is a better match.
15285
+ Then run \`task({ subagent_type: "<chosen-reviewer>", prompt: "Review implementation changes from the latest batch." })\` and apply feedback before the next batch.
15279
15286
 
15280
15287
  ### AGENTS.md Maintenance
15281
15288
 
@@ -15289,7 +15296,7 @@ For projects without AGENTS.md:
15289
15296
 
15290
15297
  ## Turn Termination
15291
15298
 
15292
- Valid endings: worker delegation (hive_worktree_create), status check (hive_status), user question (question()), merge (hive_merge).
15299
+ Valid endings: worker delegation (hive_worktree_start/hive_worktree_create), status check (hive_status), user question (question()), merge (hive_merge).
15293
15300
  Avoid ending with: "Let me know when you're ready", "When you're ready...", summary without next action, or waiting for something unspecified.
15294
15301
 
15295
15302
  ## Guardrails
@@ -15653,6 +15660,33 @@ Before verdict, mentally execute 2-3 tasks:
15653
15660
  - Focus on worker success, not perfection
15654
15661
  `;
15655
15662
 
15663
+ // src/agents/custom-agents.ts
15664
+ function buildCustomSubagents({
15665
+ customAgents,
15666
+ baseAgents,
15667
+ autoLoadedSkills = {}
15668
+ }) {
15669
+ const derived = {};
15670
+ for (const [agentName, customConfig] of Object.entries(customAgents)) {
15671
+ const baseAgent = baseAgents[customConfig.baseAgent];
15672
+ if (!baseAgent) {
15673
+ continue;
15674
+ }
15675
+ const autoLoadedSkillsContent = autoLoadedSkills[agentName] ?? "";
15676
+ derived[agentName] = {
15677
+ model: customConfig.model ?? baseAgent.model,
15678
+ variant: customConfig.variant ?? baseAgent.variant,
15679
+ temperature: customConfig.temperature ?? baseAgent.temperature,
15680
+ mode: "subagent",
15681
+ description: customConfig.description,
15682
+ prompt: baseAgent.prompt + autoLoadedSkillsContent,
15683
+ tools: baseAgent.tools,
15684
+ permission: baseAgent.permission
15685
+ };
15686
+ }
15687
+ return derived;
15688
+ }
15689
+
15656
15690
  // src/mcp/websearch.ts
15657
15691
  var websearchMcp = {
15658
15692
  type: "remote",
@@ -16554,6 +16588,28 @@ var require_dist2 = __commonJS((exports) => {
16554
16588
  exports.createDeferred = deferred;
16555
16589
  exports.default = deferred;
16556
16590
  });
16591
+ var BUILT_IN_AGENT_NAMES = [
16592
+ "hive-master",
16593
+ "architect-planner",
16594
+ "swarm-orchestrator",
16595
+ "scout-researcher",
16596
+ "forager-worker",
16597
+ "hygienic-reviewer"
16598
+ ];
16599
+ var CUSTOM_AGENT_BASES = ["forager-worker", "hygienic-reviewer"];
16600
+ var CUSTOM_AGENT_RESERVED_NAMES = [
16601
+ ...BUILT_IN_AGENT_NAMES,
16602
+ "hive",
16603
+ "architect",
16604
+ "swarm",
16605
+ "scout",
16606
+ "forager",
16607
+ "hygienic",
16608
+ "receiver",
16609
+ "build",
16610
+ "plan",
16611
+ "code"
16612
+ ];
16557
16613
  var DEFAULT_AGENT_MODELS = {
16558
16614
  "hive-master": "github-copilot/claude-opus-4.5",
16559
16615
  "architect-planner": "github-copilot/gpt-5.2-codex",
@@ -16569,6 +16625,21 @@ var DEFAULT_HIVE_CONFIG = {
16569
16625
  disableMcps: [],
16570
16626
  agentMode: "unified",
16571
16627
  sandbox: "none",
16628
+ customAgents: {
16629
+ "forager-example-template": {
16630
+ baseAgent: "forager-worker",
16631
+ description: "Example template only: rename or delete this entry before use. Do not expect planners/orchestrators to select this placeholder agent as configured.",
16632
+ model: "anthropic/claude-sonnet-4-20250514",
16633
+ temperature: 0.2,
16634
+ variant: "high",
16635
+ autoLoadSkills: ["test-driven-development"]
16636
+ },
16637
+ "hygienic-example-template": {
16638
+ baseAgent: "hygienic-reviewer",
16639
+ description: "Example template only: rename or delete this entry before use. Do not expect planners/orchestrators to select this placeholder agent as configured.",
16640
+ autoLoadSkills: ["code-reviewer"]
16641
+ }
16642
+ },
16572
16643
  agents: {
16573
16644
  "hive-master": {
16574
16645
  model: DEFAULT_AGENT_MODELS["hive-master"],
@@ -22143,6 +22214,7 @@ ${f.content}`);
22143
22214
  class ConfigService {
22144
22215
  configPath;
22145
22216
  cachedConfig = null;
22217
+ cachedCustomAgentConfigs = null;
22146
22218
  constructor() {
22147
22219
  const homeDir = process.env.HOME || process.env.USERPROFILE || "";
22148
22220
  const configDir = path6.join(homeDir, ".config", "opencode");
@@ -22158,51 +22230,44 @@ class ConfigService {
22158
22230
  try {
22159
22231
  if (!fs10.existsSync(this.configPath)) {
22160
22232
  this.cachedConfig = { ...DEFAULT_HIVE_CONFIG };
22233
+ this.cachedCustomAgentConfigs = null;
22161
22234
  return this.cachedConfig;
22162
22235
  }
22163
22236
  const raw = fs10.readFileSync(this.configPath, "utf-8");
22164
22237
  const stored = JSON.parse(raw);
22238
+ const storedCustomAgents = this.isObjectRecord(stored.customAgents) ? stored.customAgents : {};
22239
+ const mergedBuiltInAgents = BUILT_IN_AGENT_NAMES.reduce((acc, agentName) => {
22240
+ acc[agentName] = {
22241
+ ...DEFAULT_HIVE_CONFIG.agents?.[agentName],
22242
+ ...stored.agents?.[agentName]
22243
+ };
22244
+ return acc;
22245
+ }, {});
22165
22246
  const merged = {
22166
22247
  ...DEFAULT_HIVE_CONFIG,
22167
22248
  ...stored,
22168
22249
  agents: {
22169
22250
  ...DEFAULT_HIVE_CONFIG.agents,
22170
22251
  ...stored.agents,
22171
- "hive-master": {
22172
- ...DEFAULT_HIVE_CONFIG.agents?.["hive-master"],
22173
- ...stored.agents?.["hive-master"]
22174
- },
22175
- "architect-planner": {
22176
- ...DEFAULT_HIVE_CONFIG.agents?.["architect-planner"],
22177
- ...stored.agents?.["architect-planner"]
22178
- },
22179
- "swarm-orchestrator": {
22180
- ...DEFAULT_HIVE_CONFIG.agents?.["swarm-orchestrator"],
22181
- ...stored.agents?.["swarm-orchestrator"]
22182
- },
22183
- "scout-researcher": {
22184
- ...DEFAULT_HIVE_CONFIG.agents?.["scout-researcher"],
22185
- ...stored.agents?.["scout-researcher"]
22186
- },
22187
- "forager-worker": {
22188
- ...DEFAULT_HIVE_CONFIG.agents?.["forager-worker"],
22189
- ...stored.agents?.["forager-worker"]
22190
- },
22191
- "hygienic-reviewer": {
22192
- ...DEFAULT_HIVE_CONFIG.agents?.["hygienic-reviewer"],
22193
- ...stored.agents?.["hygienic-reviewer"]
22194
- }
22252
+ ...mergedBuiltInAgents
22253
+ },
22254
+ customAgents: {
22255
+ ...DEFAULT_HIVE_CONFIG.customAgents,
22256
+ ...storedCustomAgents
22195
22257
  }
22196
22258
  };
22197
22259
  this.cachedConfig = merged;
22260
+ this.cachedCustomAgentConfigs = null;
22198
22261
  return this.cachedConfig;
22199
22262
  } catch {
22200
22263
  this.cachedConfig = { ...DEFAULT_HIVE_CONFIG };
22264
+ this.cachedCustomAgentConfigs = null;
22201
22265
  return this.cachedConfig;
22202
22266
  }
22203
22267
  }
22204
22268
  set(updates) {
22205
22269
  this.cachedConfig = null;
22270
+ this.cachedCustomAgentConfigs = null;
22206
22271
  const current = this.get();
22207
22272
  const merged = {
22208
22273
  ...current,
@@ -22210,7 +22275,11 @@ class ConfigService {
22210
22275
  agents: updates.agents ? {
22211
22276
  ...current.agents,
22212
22277
  ...updates.agents
22213
- } : current.agents
22278
+ } : current.agents,
22279
+ customAgents: updates.customAgents ? {
22280
+ ...current.customAgents,
22281
+ ...updates.customAgents
22282
+ } : current.customAgents
22214
22283
  };
22215
22284
  const configDir = path6.dirname(this.configPath);
22216
22285
  if (!fs10.existsSync(configDir)) {
@@ -22218,6 +22287,7 @@ class ConfigService {
22218
22287
  }
22219
22288
  fs10.writeFileSync(this.configPath, JSON.stringify(merged, null, 2));
22220
22289
  this.cachedConfig = merged;
22290
+ this.cachedCustomAgentConfigs = null;
22221
22291
  return merged;
22222
22292
  }
22223
22293
  exists() {
@@ -22231,20 +22301,94 @@ class ConfigService {
22231
22301
  }
22232
22302
  getAgentConfig(agent) {
22233
22303
  const config2 = this.get();
22234
- const agentConfig = config2.agents?.[agent] ?? {};
22235
- const defaultAutoLoadSkills = DEFAULT_HIVE_CONFIG.agents?.[agent]?.autoLoadSkills ?? [];
22236
- const userAutoLoadSkills = agentConfig.autoLoadSkills ?? [];
22237
- const isPlannerAgent = agent === "hive-master" || agent === "architect-planner";
22238
- const effectiveUserAutoLoadSkills = isPlannerAgent ? userAutoLoadSkills : userAutoLoadSkills.filter((skill) => skill !== "onboarding");
22239
- const effectiveDefaultAutoLoadSkills = isPlannerAgent ? defaultAutoLoadSkills : defaultAutoLoadSkills.filter((skill) => skill !== "onboarding");
22240
- const combinedAutoLoadSkills = [...effectiveDefaultAutoLoadSkills, ...effectiveUserAutoLoadSkills];
22304
+ if (this.isBuiltInAgent(agent)) {
22305
+ const agentConfig = config2.agents?.[agent] ?? {};
22306
+ const defaultAutoLoadSkills = DEFAULT_HIVE_CONFIG.agents?.[agent]?.autoLoadSkills ?? [];
22307
+ const effectiveAutoLoadSkills = this.resolveAutoLoadSkills(defaultAutoLoadSkills, agentConfig.autoLoadSkills ?? [], this.isPlannerAgent(agent));
22308
+ return {
22309
+ ...agentConfig,
22310
+ autoLoadSkills: effectiveAutoLoadSkills
22311
+ };
22312
+ }
22313
+ const customAgents = this.getCustomAgentConfigs();
22314
+ return customAgents[agent] ?? {};
22315
+ }
22316
+ getCustomAgentConfigs() {
22317
+ if (this.cachedCustomAgentConfigs !== null) {
22318
+ return this.cachedCustomAgentConfigs;
22319
+ }
22320
+ const config2 = this.get();
22321
+ const customAgents = this.isObjectRecord(config2.customAgents) ? config2.customAgents : {};
22322
+ const resolved = {};
22323
+ for (const [agentName, declaration] of Object.entries(customAgents)) {
22324
+ if (this.isReservedCustomAgentName(agentName)) {
22325
+ console.warn(`[hive:config] Skipping custom agent "${agentName}": reserved name`);
22326
+ continue;
22327
+ }
22328
+ if (!this.isObjectRecord(declaration)) {
22329
+ console.warn(`[hive:config] Skipping custom agent "${agentName}": invalid declaration (expected object)`);
22330
+ continue;
22331
+ }
22332
+ const baseAgent = declaration["baseAgent"];
22333
+ if (typeof baseAgent !== "string" || !this.isSupportedCustomAgentBase(baseAgent)) {
22334
+ console.warn(`[hive:config] Skipping custom agent "${agentName}": unsupported baseAgent "${String(baseAgent)}"`);
22335
+ continue;
22336
+ }
22337
+ const autoLoadSkillsValue = declaration["autoLoadSkills"];
22338
+ const additionalAutoLoadSkills = Array.isArray(autoLoadSkillsValue) ? autoLoadSkillsValue.filter((skill) => typeof skill === "string") : [];
22339
+ const baseAgentConfig = this.getAgentConfig(baseAgent);
22340
+ const effectiveAutoLoadSkills = this.resolveAutoLoadSkills(baseAgentConfig.autoLoadSkills ?? [], additionalAutoLoadSkills, this.isPlannerAgent(baseAgent));
22341
+ const descriptionValue = declaration["description"];
22342
+ const description = typeof descriptionValue === "string" ? descriptionValue.trim() : "";
22343
+ if (!description) {
22344
+ console.warn(`[hive:config] Skipping custom agent "${agentName}": description must be a non-empty string`);
22345
+ continue;
22346
+ }
22347
+ const modelValue = declaration["model"];
22348
+ const temperatureValue = declaration["temperature"];
22349
+ const variantValue = declaration["variant"];
22350
+ const model = typeof modelValue === "string" ? modelValue.trim() || baseAgentConfig.model : baseAgentConfig.model;
22351
+ const variant = typeof variantValue === "string" ? variantValue.trim() || baseAgentConfig.variant : baseAgentConfig.variant;
22352
+ resolved[agentName] = {
22353
+ baseAgent,
22354
+ description,
22355
+ model,
22356
+ temperature: typeof temperatureValue === "number" ? temperatureValue : baseAgentConfig.temperature,
22357
+ variant,
22358
+ autoLoadSkills: effectiveAutoLoadSkills
22359
+ };
22360
+ }
22361
+ this.cachedCustomAgentConfigs = resolved;
22362
+ return this.cachedCustomAgentConfigs;
22363
+ }
22364
+ hasConfiguredAgent(agent) {
22365
+ if (this.isBuiltInAgent(agent)) {
22366
+ return true;
22367
+ }
22368
+ const customAgents = this.getCustomAgentConfigs();
22369
+ return customAgents[agent] !== undefined;
22370
+ }
22371
+ isBuiltInAgent(agent) {
22372
+ return BUILT_IN_AGENT_NAMES.includes(agent);
22373
+ }
22374
+ isReservedCustomAgentName(agent) {
22375
+ return CUSTOM_AGENT_RESERVED_NAMES.includes(agent);
22376
+ }
22377
+ isSupportedCustomAgentBase(baseAgent) {
22378
+ return CUSTOM_AGENT_BASES.includes(baseAgent);
22379
+ }
22380
+ isPlannerAgent(agent) {
22381
+ return agent === "hive-master" || agent === "architect-planner";
22382
+ }
22383
+ isObjectRecord(value) {
22384
+ return value !== null && typeof value === "object" && !Array.isArray(value);
22385
+ }
22386
+ resolveAutoLoadSkills(baseAutoLoadSkills, additionalAutoLoadSkills, isPlannerAgent) {
22387
+ const effectiveAdditionalSkills = isPlannerAgent ? additionalAutoLoadSkills : additionalAutoLoadSkills.filter((skill) => skill !== "onboarding");
22388
+ const combinedAutoLoadSkills = [...baseAutoLoadSkills, ...effectiveAdditionalSkills];
22241
22389
  const uniqueAutoLoadSkills = Array.from(new Set(combinedAutoLoadSkills));
22242
- const disabledSkills = config2.disableSkills ?? [];
22243
- const effectiveAutoLoadSkills = uniqueAutoLoadSkills.filter((skill) => !disabledSkills.includes(skill));
22244
- return {
22245
- ...agentConfig,
22246
- autoLoadSkills: effectiveAutoLoadSkills
22247
- };
22390
+ const disabledSkills = this.getDisabledSkills();
22391
+ return uniqueAutoLoadSkills.filter((skill) => !disabledSkills.includes(skill));
22248
22392
  }
22249
22393
  isOmoSlimEnabled() {
22250
22394
  const config2 = this.get();
@@ -23075,23 +23219,28 @@ function writeWorkerPromptFile(feature, task, prompt, hiveDir) {
23075
23219
  }
23076
23220
 
23077
23221
  // src/hooks/variant-hook.ts
23078
- var HIVE_AGENT_NAMES = [
23079
- "hive-master",
23080
- "architect-planner",
23081
- "swarm-orchestrator",
23082
- "scout-researcher",
23083
- "forager-worker",
23084
- "hygienic-reviewer"
23085
- ];
23086
- function isHiveAgent(agent) {
23087
- return agent !== undefined && HIVE_AGENT_NAMES.includes(agent);
23088
- }
23089
23222
  function normalizeVariant(variant) {
23090
23223
  if (variant === undefined)
23091
23224
  return;
23092
23225
  const trimmed2 = variant.trim();
23093
23226
  return trimmed2.length > 0 ? trimmed2 : undefined;
23094
23227
  }
23228
+ function createVariantHook(configService) {
23229
+ return async (input, output) => {
23230
+ const { agent } = input;
23231
+ if (!agent)
23232
+ return;
23233
+ if (!configService.hasConfiguredAgent(agent))
23234
+ return;
23235
+ if (output.message.variant !== undefined)
23236
+ return;
23237
+ const agentConfig = configService.getAgentConfig(agent);
23238
+ const configuredVariant = normalizeVariant(agentConfig.variant);
23239
+ if (configuredVariant !== undefined) {
23240
+ output.message.variant = configuredVariant;
23241
+ }
23242
+ };
23243
+ }
23095
23244
 
23096
23245
  // src/hooks/system-hook.ts
23097
23246
  var fallbackTurnCounters = {};
@@ -23138,9 +23287,8 @@ function formatSkillsXml(skills) {
23138
23287
  ${skillsXml}
23139
23288
  </available_skills>`;
23140
23289
  }
23141
- async function buildAutoLoadedSkillsContent(agentName, configService, projectRoot) {
23142
- const agentConfig = configService.getAgentConfig(agentName);
23143
- const autoLoadSkills = agentConfig.autoLoadSkills ?? [];
23290
+ async function buildAutoLoadedSkillsContent(agentName, configService, projectRoot, autoLoadSkillsOverride) {
23291
+ const autoLoadSkills = autoLoadSkillsOverride ?? (configService.getAgentConfig(agentName).autoLoadSkills ?? []);
23144
23292
  if (autoLoadSkills.length === 0) {
23145
23293
  return "";
23146
23294
  }
@@ -23295,6 +23443,392 @@ To unblock: Remove .hive/features/${feature}/BLOCKED`;
23295
23443
  }
23296
23444
  return { allowed: true };
23297
23445
  };
23446
+ const respond = (payload) => JSON.stringify(payload, null, 2);
23447
+ const buildWorktreeLaunchResponse = async ({
23448
+ feature,
23449
+ task,
23450
+ taskInfo,
23451
+ worktree,
23452
+ continueFrom,
23453
+ decision
23454
+ }) => {
23455
+ taskService.update(feature, task, {
23456
+ status: "in_progress",
23457
+ baseCommit: worktree.commit
23458
+ });
23459
+ const planResult = planService.read(feature);
23460
+ const allTasks = taskService.list(feature);
23461
+ const rawContextFiles = contextService.list(feature).map((f) => ({
23462
+ name: f.name,
23463
+ content: f.content
23464
+ }));
23465
+ const rawPreviousTasks = allTasks.filter((t) => t.status === "done" && t.summary).map((t) => ({ name: t.folder, summary: t.summary }));
23466
+ const taskBudgetResult = applyTaskBudget(rawPreviousTasks, { ...DEFAULT_BUDGET, feature });
23467
+ const contextBudgetResult = applyContextBudget(rawContextFiles, { ...DEFAULT_BUDGET, feature });
23468
+ const contextFiles = contextBudgetResult.files.map((f) => ({
23469
+ name: f.name,
23470
+ content: f.content
23471
+ }));
23472
+ const previousTasks = taskBudgetResult.tasks.map((t) => ({
23473
+ name: t.name,
23474
+ summary: t.summary
23475
+ }));
23476
+ const truncationEvents = [
23477
+ ...taskBudgetResult.truncationEvents,
23478
+ ...contextBudgetResult.truncationEvents
23479
+ ];
23480
+ const droppedTasksHint = taskBudgetResult.droppedTasksHint;
23481
+ const taskOrder = parseInt(taskInfo.folder.match(/^(\d+)/)?.[1] || "0", 10);
23482
+ const status = taskService.getRawStatus(feature, task);
23483
+ const dependsOn = status?.dependsOn ?? [];
23484
+ const specContent = taskService.buildSpecContent({
23485
+ featureName: feature,
23486
+ task: {
23487
+ folder: task,
23488
+ name: taskInfo.planTitle ?? taskInfo.name,
23489
+ order: taskOrder,
23490
+ description: undefined
23491
+ },
23492
+ dependsOn,
23493
+ allTasks: allTasks.map((t) => ({
23494
+ folder: t.folder,
23495
+ name: t.name,
23496
+ order: parseInt(t.folder.match(/^(\d+)/)?.[1] || "0", 10)
23497
+ })),
23498
+ planContent: planResult?.content ?? null,
23499
+ contextFiles,
23500
+ completedTasks: previousTasks
23501
+ });
23502
+ taskService.writeSpec(feature, task, specContent);
23503
+ const workerPrompt = buildWorkerPrompt({
23504
+ feature,
23505
+ task,
23506
+ taskOrder,
23507
+ worktreePath: worktree.path,
23508
+ branch: worktree.branch,
23509
+ plan: planResult?.content || "No plan available",
23510
+ contextFiles,
23511
+ spec: specContent,
23512
+ previousTasks,
23513
+ continueFrom: continueFrom === "blocked" ? {
23514
+ status: "blocked",
23515
+ previousSummary: taskInfo.summary || "No previous summary",
23516
+ decision: decision || "No decision provided"
23517
+ } : undefined
23518
+ });
23519
+ const customAgentConfigs = configService.getCustomAgentConfigs();
23520
+ const defaultAgent = "forager-worker";
23521
+ const eligibleAgents = [
23522
+ {
23523
+ name: defaultAgent,
23524
+ baseAgent: defaultAgent,
23525
+ description: "Default implementation worker"
23526
+ },
23527
+ ...Object.entries(customAgentConfigs).filter(([, config2]) => config2.baseAgent === "forager-worker").sort(([left], [right]) => left.localeCompare(right)).map(([name, config2]) => ({
23528
+ name,
23529
+ baseAgent: config2.baseAgent,
23530
+ description: config2.description
23531
+ }))
23532
+ ];
23533
+ const agent = defaultAgent;
23534
+ const rawStatus = taskService.getRawStatus(feature, task);
23535
+ const attempt = (rawStatus?.workerSession?.attempt || 0) + 1;
23536
+ const idempotencyKey = `hive-${feature}-${task}-${attempt}`;
23537
+ taskService.patchBackgroundFields(feature, task, { idempotencyKey });
23538
+ const contextContent = contextFiles.map((f) => f.content).join(`
23539
+
23540
+ `);
23541
+ const previousTasksContent = previousTasks.map((t) => `- **${t.name}**: ${t.summary}`).join(`
23542
+ `);
23543
+ const promptMeta = calculatePromptMeta({
23544
+ plan: planResult?.content || "",
23545
+ context: contextContent,
23546
+ previousTasks: previousTasksContent,
23547
+ spec: specContent,
23548
+ workerPrompt
23549
+ });
23550
+ const hiveDir = path8.join(directory, ".hive");
23551
+ const workerPromptPath = writeWorkerPromptFile(feature, task, workerPrompt, hiveDir);
23552
+ const relativePromptPath = normalizePath(path8.relative(directory, workerPromptPath));
23553
+ const PREVIEW_MAX_LENGTH = 200;
23554
+ const workerPromptPreview = workerPrompt.length > PREVIEW_MAX_LENGTH ? workerPrompt.slice(0, PREVIEW_MAX_LENGTH) + "..." : workerPrompt;
23555
+ const taskToolPrompt = `Follow instructions in @${relativePromptPath}`;
23556
+ const taskToolInstructions = `## Delegation Required
23557
+
23558
+ Choose one of the eligible forager-derived agents below.
23559
+ Default to \`${defaultAgent}\` if no specialist is a better match.
23560
+
23561
+ ${eligibleAgents.map((candidate) => `- \`${candidate.name}\` — ${candidate.description}`).join(`
23562
+ `)}
23563
+
23564
+ Use OpenCode's built-in \`task\` tool with the chosen \`subagent_type\` and the provided \`taskToolCall.prompt\` value.
23565
+ \`taskToolCall.subagent_type\` is prefilled with the default for convenience; override it when a specialist in \`eligibleAgents\` is a better match.
23566
+
23567
+ \`\`\`
23568
+ task({
23569
+ subagent_type: "<chosen-agent>",
23570
+ description: "Hive: ${task}",
23571
+ prompt: "${taskToolPrompt}"
23572
+ })
23573
+ \`\`\`
23574
+
23575
+ Use the \`@path\` attachment syntax in the prompt to reference the file. Do not inline the file contents.
23576
+
23577
+ `;
23578
+ const responseBase = {
23579
+ success: true,
23580
+ terminal: false,
23581
+ worktreePath: worktree.path,
23582
+ branch: worktree.branch,
23583
+ mode: "delegate",
23584
+ agent,
23585
+ defaultAgent,
23586
+ eligibleAgents,
23587
+ delegationRequired: true,
23588
+ workerPromptPath: relativePromptPath,
23589
+ workerPromptPreview,
23590
+ taskPromptMode: "opencode-at-file",
23591
+ taskToolCall: {
23592
+ subagent_type: agent,
23593
+ description: `Hive: ${task}`,
23594
+ prompt: taskToolPrompt
23595
+ },
23596
+ instructions: taskToolInstructions
23597
+ };
23598
+ const jsonPayload = JSON.stringify(responseBase, null, 2);
23599
+ const payloadMeta = calculatePayloadMeta({
23600
+ jsonPayload,
23601
+ promptInlined: false,
23602
+ promptReferencedByFile: true
23603
+ });
23604
+ const sizeWarnings = checkWarnings(promptMeta, payloadMeta);
23605
+ const budgetWarnings = truncationEvents.map((event) => ({
23606
+ type: event.type,
23607
+ severity: "info",
23608
+ message: event.message,
23609
+ affected: event.affected,
23610
+ count: event.count
23611
+ }));
23612
+ const allWarnings = [...sizeWarnings, ...budgetWarnings];
23613
+ return respond({
23614
+ ...responseBase,
23615
+ promptMeta,
23616
+ payloadMeta,
23617
+ budgetApplied: {
23618
+ maxTasks: DEFAULT_BUDGET.maxTasks,
23619
+ maxSummaryChars: DEFAULT_BUDGET.maxSummaryChars,
23620
+ maxContextChars: DEFAULT_BUDGET.maxContextChars,
23621
+ maxTotalContextChars: DEFAULT_BUDGET.maxTotalContextChars,
23622
+ tasksIncluded: previousTasks.length,
23623
+ tasksDropped: rawPreviousTasks.length - previousTasks.length,
23624
+ droppedTasksHint
23625
+ },
23626
+ warnings: allWarnings.length > 0 ? allWarnings : undefined
23627
+ });
23628
+ };
23629
+ const executeWorktreeStart = async ({
23630
+ task,
23631
+ feature: explicitFeature
23632
+ }) => {
23633
+ const feature = resolveFeature(explicitFeature);
23634
+ if (!feature) {
23635
+ return respond({
23636
+ success: false,
23637
+ terminal: true,
23638
+ error: "No feature specified. Create a feature or provide feature param.",
23639
+ reason: "feature_required",
23640
+ task,
23641
+ hints: [
23642
+ "Create/select a feature first or pass the feature parameter explicitly.",
23643
+ "Use hive_status to inspect the active feature state before retrying."
23644
+ ]
23645
+ });
23646
+ }
23647
+ const blockedMessage = checkBlocked(feature);
23648
+ if (blockedMessage) {
23649
+ return respond({
23650
+ success: false,
23651
+ terminal: true,
23652
+ error: blockedMessage,
23653
+ reason: "feature_blocked",
23654
+ feature,
23655
+ task,
23656
+ hints: [
23657
+ "Wait for the human to unblock the feature before retrying.",
23658
+ `If approved, remove .hive/features/${feature}/BLOCKED and retry hive_worktree_start.`
23659
+ ]
23660
+ });
23661
+ }
23662
+ const taskInfo = taskService.get(feature, task);
23663
+ if (!taskInfo) {
23664
+ return respond({
23665
+ success: false,
23666
+ terminal: true,
23667
+ error: `Task "${task}" not found`,
23668
+ reason: "task_not_found",
23669
+ feature,
23670
+ task,
23671
+ hints: [
23672
+ "Check the task folder name in tasks.json or hive_status output.",
23673
+ "Run hive_tasks_sync if the approved plan has changed and tasks need regeneration."
23674
+ ]
23675
+ });
23676
+ }
23677
+ if (taskInfo.status === "done") {
23678
+ return respond({
23679
+ success: false,
23680
+ terminal: true,
23681
+ error: `Task "${task}" is already completed (status: done). It cannot be restarted.`,
23682
+ currentStatus: "done",
23683
+ hints: [
23684
+ "Use hive_merge to integrate the completed task branch if not already merged.",
23685
+ "Use hive_status to see all task states and find the next runnable task."
23686
+ ]
23687
+ });
23688
+ }
23689
+ if (taskInfo.status === "blocked") {
23690
+ return respond({
23691
+ success: false,
23692
+ terminal: true,
23693
+ error: `Task "${task}" is blocked and must be resumed with hive_worktree_create using continueFrom: 'blocked'.`,
23694
+ currentStatus: "blocked",
23695
+ feature,
23696
+ task,
23697
+ hints: [
23698
+ 'Ask the user the blocker question, then call hive_worktree_create({ task, continueFrom: "blocked", decision }).',
23699
+ "Use hive_status to inspect blocker details before retrying."
23700
+ ]
23701
+ });
23702
+ }
23703
+ const depCheck = checkDependencies(feature, task);
23704
+ if (!depCheck.allowed) {
23705
+ return respond({
23706
+ success: false,
23707
+ terminal: true,
23708
+ reason: "dependencies_not_done",
23709
+ feature,
23710
+ task,
23711
+ error: depCheck.error,
23712
+ hints: [
23713
+ "Complete the required dependencies before starting this task.",
23714
+ "Use hive_status to see current task states."
23715
+ ]
23716
+ });
23717
+ }
23718
+ const worktree = await worktreeService.create(feature, task);
23719
+ return buildWorktreeLaunchResponse({ feature, task, taskInfo, worktree });
23720
+ };
23721
+ const executeBlockedResume = async ({
23722
+ task,
23723
+ feature: explicitFeature,
23724
+ continueFrom,
23725
+ decision
23726
+ }) => {
23727
+ const feature = resolveFeature(explicitFeature);
23728
+ if (!feature) {
23729
+ return respond({
23730
+ success: false,
23731
+ terminal: true,
23732
+ error: "No feature specified. Create a feature or provide feature param.",
23733
+ reason: "feature_required",
23734
+ task,
23735
+ hints: [
23736
+ "Create/select a feature first or pass the feature parameter explicitly.",
23737
+ "Use hive_status to inspect the active feature state before retrying."
23738
+ ]
23739
+ });
23740
+ }
23741
+ const blockedMessage = checkBlocked(feature);
23742
+ if (blockedMessage) {
23743
+ return respond({
23744
+ success: false,
23745
+ terminal: true,
23746
+ error: blockedMessage,
23747
+ reason: "feature_blocked",
23748
+ feature,
23749
+ task,
23750
+ hints: [
23751
+ "Wait for the human to unblock the feature before retrying.",
23752
+ `If approved, remove .hive/features/${feature}/BLOCKED and retry hive_worktree_create.`
23753
+ ]
23754
+ });
23755
+ }
23756
+ const taskInfo = taskService.get(feature, task);
23757
+ if (!taskInfo) {
23758
+ return respond({
23759
+ success: false,
23760
+ terminal: true,
23761
+ error: `Task "${task}" not found`,
23762
+ reason: "task_not_found",
23763
+ feature,
23764
+ task,
23765
+ hints: [
23766
+ "Check the task folder name in tasks.json or hive_status output.",
23767
+ "Run hive_tasks_sync if the approved plan has changed and tasks need regeneration."
23768
+ ]
23769
+ });
23770
+ }
23771
+ if (taskInfo.status === "done") {
23772
+ return respond({
23773
+ success: false,
23774
+ terminal: true,
23775
+ error: `Task "${task}" is already completed (status: done). It cannot be restarted.`,
23776
+ currentStatus: "done",
23777
+ hints: [
23778
+ "Use hive_merge to integrate the completed task branch if not already merged.",
23779
+ "Use hive_status to see all task states and find the next runnable task."
23780
+ ]
23781
+ });
23782
+ }
23783
+ if (continueFrom !== "blocked") {
23784
+ return respond({
23785
+ success: false,
23786
+ terminal: true,
23787
+ error: "hive_worktree_create is only for resuming blocked tasks.",
23788
+ reason: "blocked_resume_required",
23789
+ currentStatus: taskInfo.status,
23790
+ feature,
23791
+ task,
23792
+ hints: [
23793
+ "Use hive_worktree_start({ feature, task }) to start a pending or in-progress task normally.",
23794
+ 'Use hive_worktree_create({ task, continueFrom: "blocked", decision }) only after hive_status confirms the task is blocked.'
23795
+ ]
23796
+ });
23797
+ }
23798
+ if (taskInfo.status !== "blocked") {
23799
+ return respond({
23800
+ success: false,
23801
+ terminal: true,
23802
+ error: `continueFrom: 'blocked' was specified but task "${task}" is not in blocked state (current status: ${taskInfo.status}).`,
23803
+ currentStatus: taskInfo.status,
23804
+ hints: [
23805
+ "Use hive_worktree_start({ feature, task }) for normal starts or re-dispatch.",
23806
+ "Use hive_status to verify the current task status before retrying."
23807
+ ]
23808
+ });
23809
+ }
23810
+ const worktree = await worktreeService.get(feature, task);
23811
+ if (!worktree) {
23812
+ return respond({
23813
+ success: false,
23814
+ terminal: true,
23815
+ error: `Cannot resume blocked task "${task}": no existing worktree record found.`,
23816
+ currentStatus: taskInfo.status,
23817
+ hints: [
23818
+ "The worktree may have been removed manually. Use hive_worktree_discard to reset the task to pending, then restart it with hive_worktree_start.",
23819
+ "Use hive_status to inspect the current state of the task and its worktree."
23820
+ ]
23821
+ });
23822
+ }
23823
+ return buildWorktreeLaunchResponse({
23824
+ feature,
23825
+ task,
23826
+ taskInfo,
23827
+ worktree,
23828
+ continueFrom,
23829
+ decision
23830
+ });
23831
+ };
23298
23832
  return {
23299
23833
  "experimental.chat.system.transform": async (input, output) => {
23300
23834
  if (!shouldExecuteHook("experimental.chat.system.transform", configService, turnCounters)) {
@@ -23323,20 +23857,7 @@ To unblock: Remove .hive/features/${feature}/BLOCKED`;
23323
23857
  "experimental.session.compacting": async (_input, output) => {
23324
23858
  output.context.push(buildCompactionPrompt());
23325
23859
  },
23326
- "chat.message": async (input, output) => {
23327
- const { agent } = input;
23328
- if (!agent)
23329
- return;
23330
- if (!isHiveAgent(agent))
23331
- return;
23332
- if (output.message.variant !== undefined)
23333
- return;
23334
- const agentConfig = configService.getAgentConfig(agent);
23335
- const configuredVariant = normalizeVariant(agentConfig.variant);
23336
- if (configuredVariant !== undefined) {
23337
- output.message.variant = configuredVariant;
23338
- }
23339
- },
23860
+ "chat.message": createVariantHook(configService),
23340
23861
  "tool.execute.before": async (input, output) => {
23341
23862
  if (!shouldExecuteHook("tool.execute.before", configService, turnCounters, { safetyCritical: true })) {
23342
23863
  return;
@@ -23529,7 +24050,7 @@ Expand your Discovery section and try again.`;
23529
24050
  return "Error: No feature specified. Create a feature or provide feature param.";
23530
24051
  const folder = taskService.create(feature, name, order);
23531
24052
  return `Manual task created: ${folder}
23532
- Reminder: start work with hive_worktree_create to use its worktree, and ensure any subagents work in that worktree too.`;
24053
+ Reminder: start work with hive_worktree_start to use its worktree, and ensure any subagents work in that worktree too.`;
23533
24054
  }
23534
24055
  }),
23535
24056
  hive_task_update: tool({
@@ -23551,198 +24072,26 @@ Reminder: start work with hive_worktree_create to use its worktree, and ensure a
23551
24072
  return `Task "${task}" updated: status=${updated.status}`;
23552
24073
  }
23553
24074
  }),
24075
+ hive_worktree_start: tool({
24076
+ description: "Create worktree and begin work on pending/in-progress task. Spawns Forager worker automatically.",
24077
+ args: {
24078
+ task: tool.schema.string().describe("Task folder name"),
24079
+ feature: tool.schema.string().optional().describe("Feature name (defaults to detection or single feature)")
24080
+ },
24081
+ async execute({ task, feature: explicitFeature }) {
24082
+ return executeWorktreeStart({ task, feature: explicitFeature });
24083
+ }
24084
+ }),
23554
24085
  hive_worktree_create: tool({
23555
- description: "Create worktree and begin work on task. Spawns Forager worker automatically.",
24086
+ description: "Resume a blocked task in its existing worktree. Spawns Forager worker automatically.",
23556
24087
  args: {
23557
24088
  task: tool.schema.string().describe("Task folder name"),
23558
24089
  feature: tool.schema.string().optional().describe("Feature name (defaults to detection or single feature)"),
23559
24090
  continueFrom: tool.schema.enum(["blocked"]).optional().describe("Resume a blocked task"),
23560
24091
  decision: tool.schema.string().optional().describe("Answer to blocker question when continuing")
23561
24092
  },
23562
- async execute({ task, feature: explicitFeature, continueFrom, decision }, toolContext) {
23563
- const feature = resolveFeature(explicitFeature);
23564
- if (!feature)
23565
- return "Error: No feature specified. Create a feature or provide feature param.";
23566
- const blockedMessage = checkBlocked(feature);
23567
- if (blockedMessage)
23568
- return blockedMessage;
23569
- const taskInfo = taskService.get(feature, task);
23570
- if (!taskInfo)
23571
- return `Error: Task "${task}" not found`;
23572
- if (taskInfo.status === "done")
23573
- return "Error: Task already completed";
23574
- if (continueFrom === "blocked" && taskInfo.status !== "blocked") {
23575
- return "Error: Task is not in blocked state. Use without continueFrom.";
23576
- }
23577
- if (continueFrom !== "blocked") {
23578
- const depCheck = checkDependencies(feature, task);
23579
- if (!depCheck.allowed) {
23580
- return JSON.stringify({
23581
- success: false,
23582
- error: depCheck.error,
23583
- hints: [
23584
- "Complete the required dependencies before starting this task.",
23585
- "Use hive_status to see current task states."
23586
- ]
23587
- });
23588
- }
23589
- }
23590
- let worktree;
23591
- if (continueFrom === "blocked") {
23592
- worktree = await worktreeService.get(feature, task);
23593
- if (!worktree)
23594
- return "Error: No worktree found for blocked task";
23595
- } else {
23596
- worktree = await worktreeService.create(feature, task);
23597
- }
23598
- taskService.update(feature, task, {
23599
- status: "in_progress",
23600
- baseCommit: worktree.commit
23601
- });
23602
- const planResult = planService.read(feature);
23603
- const allTasks = taskService.list(feature);
23604
- const rawContextFiles = contextService.list(feature).map((f) => ({
23605
- name: f.name,
23606
- content: f.content
23607
- }));
23608
- const rawPreviousTasks = allTasks.filter((t) => t.status === "done" && t.summary).map((t) => ({ name: t.folder, summary: t.summary }));
23609
- const taskBudgetResult = applyTaskBudget(rawPreviousTasks, { ...DEFAULT_BUDGET, feature });
23610
- const contextBudgetResult = applyContextBudget(rawContextFiles, { ...DEFAULT_BUDGET, feature });
23611
- const contextFiles = contextBudgetResult.files.map((f) => ({
23612
- name: f.name,
23613
- content: f.content
23614
- }));
23615
- const previousTasks = taskBudgetResult.tasks.map((t) => ({
23616
- name: t.name,
23617
- summary: t.summary
23618
- }));
23619
- const truncationEvents = [
23620
- ...taskBudgetResult.truncationEvents,
23621
- ...contextBudgetResult.truncationEvents
23622
- ];
23623
- const droppedTasksHint = taskBudgetResult.droppedTasksHint;
23624
- const taskOrder = parseInt(taskInfo.folder.match(/^(\d+)/)?.[1] || "0", 10);
23625
- const status = taskService.getRawStatus(feature, task);
23626
- const dependsOn = status?.dependsOn ?? [];
23627
- const specContent = taskService.buildSpecContent({
23628
- featureName: feature,
23629
- task: {
23630
- folder: task,
23631
- name: taskInfo.planTitle ?? taskInfo.name,
23632
- order: taskOrder,
23633
- description: undefined
23634
- },
23635
- dependsOn,
23636
- allTasks: allTasks.map((t) => ({
23637
- folder: t.folder,
23638
- name: t.name,
23639
- order: parseInt(t.folder.match(/^(\d+)/)?.[1] || "0", 10)
23640
- })),
23641
- planContent: planResult?.content ?? null,
23642
- contextFiles,
23643
- completedTasks: previousTasks
23644
- });
23645
- taskService.writeSpec(feature, task, specContent);
23646
- const workerPrompt = buildWorkerPrompt({
23647
- feature,
23648
- task,
23649
- taskOrder: parseInt(taskInfo.folder.match(/^(\d+)/)?.[1] || "0", 10),
23650
- worktreePath: worktree.path,
23651
- branch: worktree.branch,
23652
- plan: planResult?.content || "No plan available",
23653
- contextFiles,
23654
- spec: specContent,
23655
- previousTasks,
23656
- continueFrom: continueFrom === "blocked" ? {
23657
- status: "blocked",
23658
- previousSummary: taskInfo.summary || "No previous summary",
23659
- decision: decision || "No decision provided"
23660
- } : undefined
23661
- });
23662
- const agent = "forager-worker";
23663
- const rawStatus = taskService.getRawStatus(feature, task);
23664
- const attempt = (rawStatus?.workerSession?.attempt || 0) + 1;
23665
- const idempotencyKey = `hive-${feature}-${task}-${attempt}`;
23666
- taskService.patchBackgroundFields(feature, task, { idempotencyKey });
23667
- const contextContent = contextFiles.map((f) => f.content).join(`
23668
-
23669
- `);
23670
- const previousTasksContent = previousTasks.map((t) => `- **${t.name}**: ${t.summary}`).join(`
23671
- `);
23672
- const promptMeta = calculatePromptMeta({
23673
- plan: planResult?.content || "",
23674
- context: contextContent,
23675
- previousTasks: previousTasksContent,
23676
- spec: specContent,
23677
- workerPrompt
23678
- });
23679
- const hiveDir = path8.join(directory, ".hive");
23680
- const workerPromptPath = writeWorkerPromptFile(feature, task, workerPrompt, hiveDir);
23681
- const relativePromptPath = normalizePath(path8.relative(directory, workerPromptPath));
23682
- const PREVIEW_MAX_LENGTH = 200;
23683
- const workerPromptPreview = workerPrompt.length > PREVIEW_MAX_LENGTH ? workerPrompt.slice(0, PREVIEW_MAX_LENGTH) + "..." : workerPrompt;
23684
- const taskToolPrompt = `Follow instructions in @${relativePromptPath}`;
23685
- const taskToolInstructions = `## Delegation Required
23686
-
23687
- Use OpenCode's built-in \`task\` tool to spawn a Forager (Worker/Coder) worker.
23688
-
23689
- \`\`\`
23690
- task({
23691
- subagent_type: "${agent}",
23692
- description: "Hive: ${task}",
23693
- prompt: "${taskToolPrompt}"
23694
- })
23695
- \`\`\`
23696
-
23697
- Use the \`@path\` attachment syntax in the prompt to reference the file. Do not inline the file contents.
23698
-
23699
- `;
23700
- const responseBase = {
23701
- worktreePath: worktree.path,
23702
- branch: worktree.branch,
23703
- mode: "delegate",
23704
- agent,
23705
- delegationRequired: true,
23706
- workerPromptPath: relativePromptPath,
23707
- workerPromptPreview,
23708
- taskPromptMode: "opencode-at-file",
23709
- taskToolCall: {
23710
- subagent_type: agent,
23711
- description: `Hive: ${task}`,
23712
- prompt: taskToolPrompt
23713
- },
23714
- instructions: taskToolInstructions
23715
- };
23716
- const jsonPayload = JSON.stringify(responseBase, null, 2);
23717
- const payloadMeta = calculatePayloadMeta({
23718
- jsonPayload,
23719
- promptInlined: false,
23720
- promptReferencedByFile: true
23721
- });
23722
- const sizeWarnings = checkWarnings(promptMeta, payloadMeta);
23723
- const budgetWarnings = truncationEvents.map((event) => ({
23724
- type: event.type,
23725
- severity: "info",
23726
- message: event.message,
23727
- affected: event.affected,
23728
- count: event.count
23729
- }));
23730
- const allWarnings = [...sizeWarnings, ...budgetWarnings];
23731
- return JSON.stringify({
23732
- ...responseBase,
23733
- promptMeta,
23734
- payloadMeta,
23735
- budgetApplied: {
23736
- maxTasks: DEFAULT_BUDGET.maxTasks,
23737
- maxSummaryChars: DEFAULT_BUDGET.maxSummaryChars,
23738
- maxContextChars: DEFAULT_BUDGET.maxContextChars,
23739
- maxTotalContextChars: DEFAULT_BUDGET.maxTotalContextChars,
23740
- tasksIncluded: previousTasks.length,
23741
- tasksDropped: rawPreviousTasks.length - previousTasks.length,
23742
- droppedTasksHint
23743
- },
23744
- warnings: allWarnings.length > 0 ? allWarnings : undefined
23745
- }, null, 2);
24093
+ async execute({ task, feature: explicitFeature, continueFrom, decision }) {
24094
+ return executeBlockedResume({ task, feature: explicitFeature, continueFrom, decision });
23746
24095
  }
23747
24096
  }),
23748
24097
  hive_worktree_commit: tool({
@@ -23760,10 +24109,10 @@ Use the \`@path\` attachment syntax in the prompt to reference the file. Do not
23760
24109
  feature: tool.schema.string().optional().describe("Feature name (defaults to detection or single feature)")
23761
24110
  },
23762
24111
  async execute({ task, summary, status = "completed", blocker, feature: explicitFeature }) {
23763
- const respond = (payload) => JSON.stringify(payload, null, 2);
24112
+ const respond2 = (payload) => JSON.stringify(payload, null, 2);
23764
24113
  const feature = resolveFeature(explicitFeature);
23765
24114
  if (!feature) {
23766
- return respond({
24115
+ return respond2({
23767
24116
  ok: false,
23768
24117
  terminal: false,
23769
24118
  status: "error",
@@ -23776,7 +24125,7 @@ Use the \`@path\` attachment syntax in the prompt to reference the file. Do not
23776
24125
  }
23777
24126
  const taskInfo = taskService.get(feature, task);
23778
24127
  if (!taskInfo) {
23779
- return respond({
24128
+ return respond2({
23780
24129
  ok: false,
23781
24130
  terminal: false,
23782
24131
  status: "error",
@@ -23789,7 +24138,7 @@ Use the \`@path\` attachment syntax in the prompt to reference the file. Do not
23789
24138
  });
23790
24139
  }
23791
24140
  if (taskInfo.status !== "in_progress" && taskInfo.status !== "blocked") {
23792
- return respond({
24141
+ return respond2({
23793
24142
  ok: false,
23794
24143
  terminal: false,
23795
24144
  status: "error",
@@ -23817,7 +24166,7 @@ Use the \`@path\` attachment syntax in the prompt to reference the file. Do not
23817
24166
  blocker
23818
24167
  });
23819
24168
  const worktree2 = await worktreeService.get(feature, task);
23820
- return respond({
24169
+ return respond2({
23821
24170
  ok: true,
23822
24171
  terminal: true,
23823
24172
  status: "blocked",
@@ -23835,7 +24184,7 @@ Use the \`@path\` attachment syntax in the prompt to reference the file. Do not
23835
24184
  }
23836
24185
  const commitResult = await worktreeService.commitChanges(feature, task, `hive(${task}): ${summary.slice(0, 50)}`);
23837
24186
  if (status === "completed" && !commitResult.committed && commitResult.message !== "No changes to commit") {
23838
- return respond({
24187
+ return respond2({
23839
24188
  ok: false,
23840
24189
  terminal: false,
23841
24190
  status: "rejected",
@@ -23887,7 +24236,7 @@ Use the \`@path\` attachment syntax in the prompt to reference the file. Do not
23887
24236
  const finalStatus = status === "completed" ? "done" : status;
23888
24237
  taskService.update(feature, task, { status: finalStatus, summary });
23889
24238
  const worktree = await worktreeService.get(feature, task);
23890
- return respond({
24239
+ return respond2({
23891
24240
  ok: true,
23892
24241
  terminal: true,
23893
24242
  status,
@@ -23977,23 +24326,40 @@ Files changed: ${result.filesChanged?.length || 0}`;
23977
24326
  feature: tool.schema.string().optional().describe("Feature name (defaults to active)")
23978
24327
  },
23979
24328
  async execute({ feature: explicitFeature }) {
24329
+ const respond2 = (payload) => JSON.stringify(payload, null, 2);
23980
24330
  const feature = resolveFeature(explicitFeature);
23981
24331
  if (!feature) {
23982
- return JSON.stringify({
24332
+ return respond2({
24333
+ success: false,
24334
+ terminal: true,
24335
+ reason: "feature_required",
23983
24336
  error: "No feature specified and no active feature found",
23984
24337
  hint: "Use hive_feature_create to create a new feature"
23985
24338
  });
23986
24339
  }
23987
24340
  const featureData = featureService.get(feature);
23988
24341
  if (!featureData) {
23989
- return JSON.stringify({
24342
+ return respond2({
24343
+ success: false,
24344
+ terminal: true,
24345
+ reason: "feature_not_found",
23990
24346
  error: `Feature '${feature}' not found`,
23991
24347
  availableFeatures: featureService.list()
23992
24348
  });
23993
24349
  }
23994
24350
  const blocked = checkBlocked(feature);
23995
- if (blocked)
23996
- return blocked;
24351
+ if (blocked) {
24352
+ return respond2({
24353
+ success: false,
24354
+ terminal: true,
24355
+ blocked: true,
24356
+ error: blocked,
24357
+ hints: [
24358
+ "Read the blocker details and resolve them before retrying hive_status.",
24359
+ `Remove .hive/features/${feature}/BLOCKED once the blocker is resolved.`
24360
+ ]
24361
+ });
24362
+ }
23997
24363
  const plan = planService.read(feature);
23998
24364
  const tasks = taskService.list(feature);
23999
24365
  const contextFiles = contextService.list(feature);
@@ -24050,7 +24416,7 @@ Files changed: ${result.filesChanged?.length || 0}`;
24050
24416
  return `${runnableTasks.length} tasks are ready to start in parallel: ${runnableTasks.join(", ")}`;
24051
24417
  }
24052
24418
  if (runnableTasks.length === 1) {
24053
- return `Start next task with hive_worktree_create: ${runnableTasks[0]}`;
24419
+ return `Start next task with hive_worktree_start: ${runnableTasks[0]}`;
24054
24420
  }
24055
24421
  const pending = tasks2.find((t) => t.status === "pending");
24056
24422
  if (pending) {
@@ -24059,7 +24425,7 @@ Files changed: ${result.filesChanged?.length || 0}`;
24059
24425
  return "All tasks complete. Review and merge or complete feature.";
24060
24426
  };
24061
24427
  const planStatus = featureData.status === "planning" ? "draft" : featureData.status === "approved" ? "approved" : featureData.status === "executing" ? "locked" : "none";
24062
- return JSON.stringify({
24428
+ return respond2({
24063
24429
  feature: {
24064
24430
  name: feature,
24065
24431
  status: featureData.status,
@@ -24152,6 +24518,7 @@ ${result.diff}
24152
24518
  "hive_tasks_sync",
24153
24519
  "hive_task_create",
24154
24520
  "hive_task_update",
24521
+ "hive_worktree_start",
24155
24522
  "hive_worktree_create",
24156
24523
  "hive_worktree_commit",
24157
24524
  "hive_worktree_discard",
@@ -24170,6 +24537,14 @@ ${result.diff}
24170
24537
  return result;
24171
24538
  }
24172
24539
  configService.init();
24540
+ const hiveConfigData = configService.get();
24541
+ const agentMode = hiveConfigData.agentMode ?? "unified";
24542
+ const customAgentConfigs = configService.getCustomAgentConfigs();
24543
+ const customSubagentAppendix = Object.keys(customAgentConfigs).length === 0 ? "" : `
24544
+
24545
+ ## Configured Custom Subagents
24546
+ ${Object.entries(customAgentConfigs).sort(([left], [right]) => left.localeCompare(right)).map(([name, config2]) => `- \`${name}\` — derived from \`${config2.baseAgent}\`; ${config2.description}`).join(`
24547
+ `)}`;
24173
24548
  const hiveUserConfig = configService.getAgentConfig("hive-master");
24174
24549
  const hiveAutoLoadedSkills = await buildAutoLoadedSkillsContent("hive-master", configService, directory);
24175
24550
  const hiveConfig = {
@@ -24177,7 +24552,7 @@ ${result.diff}
24177
24552
  variant: hiveUserConfig.variant,
24178
24553
  temperature: hiveUserConfig.temperature ?? 0.5,
24179
24554
  description: "Hive (Hybrid) - Plans + orchestrates. Detects phase, loads skills on-demand.",
24180
- prompt: QUEEN_BEE_PROMPT + hiveAutoLoadedSkills,
24555
+ prompt: QUEEN_BEE_PROMPT + hiveAutoLoadedSkills + (agentMode === "unified" ? customSubagentAppendix : ""),
24181
24556
  permission: {
24182
24557
  question: "allow",
24183
24558
  skill: "allow",
@@ -24192,7 +24567,7 @@ ${result.diff}
24192
24567
  variant: architectUserConfig.variant,
24193
24568
  temperature: architectUserConfig.temperature ?? 0.7,
24194
24569
  description: "Architect (Planner) - Plans features, interviews, writes plans. NEVER executes.",
24195
- prompt: ARCHITECT_BEE_PROMPT + architectAutoLoadedSkills,
24570
+ prompt: ARCHITECT_BEE_PROMPT + architectAutoLoadedSkills + (agentMode === "dedicated" ? customSubagentAppendix : ""),
24196
24571
  tools: agentTools(["hive_feature_create", "hive_plan_write", "hive_plan_read", "hive_context_write", "hive_status", "hive_skill"]),
24197
24572
  permission: {
24198
24573
  edit: "deny",
@@ -24211,7 +24586,7 @@ ${result.diff}
24211
24586
  variant: swarmUserConfig.variant,
24212
24587
  temperature: swarmUserConfig.temperature ?? 0.5,
24213
24588
  description: "Swarm (Orchestrator) - Orchestrates execution. Delegates, spawns workers, verifies, merges.",
24214
- prompt: SWARM_BEE_PROMPT + swarmAutoLoadedSkills,
24589
+ prompt: SWARM_BEE_PROMPT + swarmAutoLoadedSkills + (agentMode === "dedicated" ? customSubagentAppendix : ""),
24215
24590
  tools: agentTools([
24216
24591
  "hive_feature_create",
24217
24592
  "hive_feature_complete",
@@ -24220,6 +24595,7 @@ ${result.diff}
24220
24595
  "hive_tasks_sync",
24221
24596
  "hive_task_create",
24222
24597
  "hive_task_update",
24598
+ "hive_worktree_start",
24223
24599
  "hive_worktree_create",
24224
24600
  "hive_worktree_discard",
24225
24601
  "hive_merge",
@@ -24286,21 +24662,44 @@ ${result.diff}
24286
24662
  skill: "allow"
24287
24663
  }
24288
24664
  };
24289
- const hiveConfigData = configService.get();
24290
- const agentMode = hiveConfigData.agentMode ?? "unified";
24665
+ const builtInAgentConfigs = {
24666
+ "hive-master": hiveConfig,
24667
+ "architect-planner": architectConfig,
24668
+ "swarm-orchestrator": swarmConfig,
24669
+ "scout-researcher": scoutConfig,
24670
+ "forager-worker": foragerConfig,
24671
+ "hygienic-reviewer": hygienicConfig
24672
+ };
24673
+ const customAutoLoadedSkills = Object.fromEntries(await Promise.all(Object.entries(customAgentConfigs).map(async ([customAgentName, customAgentConfig]) => {
24674
+ const inheritedBaseSkills = customAgentConfig.baseAgent === "forager-worker" ? foragerUserConfig.autoLoadSkills ?? [] : hygienicUserConfig.autoLoadSkills ?? [];
24675
+ const deltaAutoLoadSkills = (customAgentConfig.autoLoadSkills ?? []).filter((skill) => !inheritedBaseSkills.includes(skill));
24676
+ return [
24677
+ customAgentName,
24678
+ await buildAutoLoadedSkillsContent(customAgentName, configService, directory, deltaAutoLoadSkills)
24679
+ ];
24680
+ })));
24681
+ const customSubagents = buildCustomSubagents({
24682
+ customAgents: customAgentConfigs,
24683
+ baseAgents: {
24684
+ "forager-worker": foragerConfig,
24685
+ "hygienic-reviewer": hygienicConfig
24686
+ },
24687
+ autoLoadedSkills: customAutoLoadedSkills
24688
+ });
24291
24689
  const allAgents = {};
24292
24690
  if (agentMode === "unified") {
24293
- allAgents["hive-master"] = hiveConfig;
24294
- allAgents["scout-researcher"] = scoutConfig;
24295
- allAgents["forager-worker"] = foragerConfig;
24296
- allAgents["hygienic-reviewer"] = hygienicConfig;
24691
+ allAgents["hive-master"] = builtInAgentConfigs["hive-master"];
24692
+ allAgents["scout-researcher"] = builtInAgentConfigs["scout-researcher"];
24693
+ allAgents["forager-worker"] = builtInAgentConfigs["forager-worker"];
24694
+ allAgents["hygienic-reviewer"] = builtInAgentConfigs["hygienic-reviewer"];
24297
24695
  } else {
24298
- allAgents["architect-planner"] = architectConfig;
24299
- allAgents["swarm-orchestrator"] = swarmConfig;
24300
- allAgents["scout-researcher"] = scoutConfig;
24301
- allAgents["forager-worker"] = foragerConfig;
24302
- allAgents["hygienic-reviewer"] = hygienicConfig;
24696
+ allAgents["architect-planner"] = builtInAgentConfigs["architect-planner"];
24697
+ allAgents["swarm-orchestrator"] = builtInAgentConfigs["swarm-orchestrator"];
24698
+ allAgents["scout-researcher"] = builtInAgentConfigs["scout-researcher"];
24699
+ allAgents["forager-worker"] = builtInAgentConfigs["forager-worker"];
24700
+ allAgents["hygienic-reviewer"] = builtInAgentConfigs["hygienic-reviewer"];
24303
24701
  }
24702
+ Object.assign(allAgents, customSubagents);
24304
24703
  const configAgent = opencodeConfig.agent;
24305
24704
  if (!configAgent) {
24306
24705
  opencodeConfig.agent = allAgents;