opencode-hive 1.0.5 → 1.0.6

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
@@ -12608,6 +12608,19 @@ When you have multiple unrelated failures (different test files, different subsy
12608
12608
 
12609
12609
  **Core principle:** Dispatch one agent per independent problem domain. Let them work concurrently.
12610
12610
 
12611
+ ## Prerequisite: Check Runnable Tasks
12612
+
12613
+ Before dispatching, use \`hive_status()\` to get the **runnable** list — tasks whose dependencies are all satisfied.
12614
+
12615
+ **Only dispatch tasks that are runnable.** Never start tasks with unmet dependencies.
12616
+
12617
+ Only \`done\` satisfies dependencies (not \`blocked\`, \`failed\`, \`partial\`, \`cancelled\`).
12618
+
12619
+ **Ask the operator first:**
12620
+ - Use \`question()\`: "These tasks are runnable and independent: [list]. Execute in parallel?"
12621
+ - Record the decision with \`hive_context_write({ name: "execution-decisions", content: "..." })\`
12622
+ - Proceed only after operator approval
12623
+
12611
12624
  ## When to Use
12612
12625
 
12613
12626
  \`\`\`dot
@@ -12668,6 +12681,13 @@ hive_exec_start({ task: "03-fix-race-condition-tests" })
12668
12681
  // All three run concurrently in isolated worktrees
12669
12682
  \`\`\`
12670
12683
 
12684
+ Parallelize by issuing multiple task() calls in the same assistant message.
12685
+
12686
+ \`\`\`typescript
12687
+ task({ subagent_type: 'scout-researcher', prompt: 'Investigate failure A' })
12688
+ task({ subagent_type: 'scout-researcher', prompt: 'Investigate failure B' })
12689
+ \`\`\`
12690
+
12671
12691
  ### 4. Review and Integrate
12672
12692
 
12673
12693
  When agents return:
@@ -12797,28 +12817,39 @@ Load plan, review critically, execute tasks in batches, report for review betwee
12797
12817
  3. If concerns: Raise them with your human partner before starting
12798
12818
  4. If no concerns: Create TodoWrite and proceed
12799
12819
 
12800
- ### Step 2: Execute Batch
12801
- **Default: First 3 tasks**
12820
+ ### Step 2: Identify Runnable Tasks
12821
+
12822
+ Use \`hive_status()\` to get the **runnable** list — tasks with all dependencies satisfied.
12823
+
12824
+ Only \`done\` satisfies dependencies (not \`blocked\`, \`failed\`, \`partial\`, \`cancelled\`).
12825
+
12826
+ **When 2+ tasks are runnable:**
12827
+ - **Ask the operator** via \`question()\`: "Multiple tasks are runnable: [list]. Run in parallel, sequential, or a specific subset?"
12828
+ - Record the decision with \`hive_context_write({ name: "execution-decisions", content: "..." })\` for future reference
12829
+
12830
+ **When 1 task is runnable:** Proceed directly.
12831
+
12832
+ ### Step 3: Execute Batch
12802
12833
 
12803
- For each task:
12804
- 1. Mark as in_progress
12834
+ For each task in the batch:
12835
+ 1. Mark as in_progress via \`hive_exec_start()\`
12805
12836
  2. Follow each step exactly (plan has bite-sized steps)
12806
12837
  3. Run verifications as specified
12807
12838
  4. Mark as completed
12808
12839
 
12809
- ### Step 3: Report
12840
+ ### Step 4: Report
12810
12841
  When batch complete:
12811
12842
  - Show what was implemented
12812
12843
  - Show verification output
12813
12844
  - Say: "Ready for feedback."
12814
12845
 
12815
- ### Step 4: Continue
12846
+ ### Step 5: Continue
12816
12847
  Based on feedback:
12817
12848
  - Apply changes if needed
12818
12849
  - Execute next batch
12819
12850
  - Repeat until complete
12820
12851
 
12821
- ### Step 5: Complete Development
12852
+ ### Step 6: Complete Development
12822
12853
 
12823
12854
  After all tasks complete and verified:
12824
12855
  - Announce: "I'm using the finishing-a-development-branch skill to complete this work."
@@ -12913,19 +12944,23 @@ Ask one at a time, with the provided options. Store the answers in \`.hive/conte
12913
12944
  },
12914
12945
  {
12915
12946
  name: "parallel-exploration",
12916
- description: "Use when you need parallel, read-only exploration via hive_hive_background_task (Scout fan-out)",
12917
- template: `# Parallel Exploration (Background Scout Fan-Out)
12947
+ description: "Use when you need parallel, read-only exploration with hive_background_* or task() (Scout fan-out)",
12948
+ template: `# Parallel Exploration (Scout Fan-Out)
12918
12949
 
12919
12950
  ## Overview
12920
12951
 
12921
12952
  When you need to answer "where/how does X work?" across multiple domains (codebase, tests, docs, OSS), investigating sequentially wastes time. Each investigation is independent and can happen in parallel.
12922
12953
 
12923
- **Core principle:** Decompose into independent sub-questions, spawn one \`hive_hive_background_task\` per sub-question, collect results asynchronously.
12954
+ **Core principle:** Decompose into independent sub-questions, spawn one task per sub-question, collect results asynchronously.
12924
12955
 
12925
12956
  **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.
12926
12957
 
12927
12958
  **This skill is for read-only research.** For parallel implementation work, use \`hive_skill("dispatching-parallel-agents")\` with \`hive_exec_start\`.
12928
12959
 
12960
+ **Two valid execution paths:**
12961
+ - **Path A (Hive background tools):** Use \`hive_background_task\`, \`hive_background_output\`, \`hive_background_cancel\` when available.
12962
+ - **Path B (Task mode):** Use native \`task()\` for delegation when background tools are not registered.
12963
+
12929
12964
  ## When to Use
12930
12965
 
12931
12966
  **Default to this skill when:**
@@ -12968,6 +13003,7 @@ Split your investigation into 2-4 independent sub-questions. Good decomposition:
12968
13003
  Launch all tasks before waiting for any results:
12969
13004
 
12970
13005
  \`\`\`typescript
13006
+ // Path A: Hive background tools (when available)
12971
13007
  // Fan-out: spawn all tasks first
12972
13008
  hive_background_task({
12973
13009
  agent: "scout-researcher",
@@ -13000,6 +13036,37 @@ hive_background_task({
13000
13036
  })
13001
13037
  \`\`\`
13002
13038
 
13039
+ \`\`\`typescript
13040
+ // Path B: Task mode (native task tool)
13041
+ // Parallelize by issuing multiple task() calls in the same assistant message.
13042
+ task({
13043
+ subagent_type: 'scout-researcher',
13044
+ description: 'Find hive_background_task implementation',
13045
+ prompt: \`Where is hive_background_task implemented and registered?
13046
+ - Find the tool definition
13047
+ - Find the plugin registration
13048
+ - Return file paths with line numbers\`,
13049
+ });
13050
+
13051
+ task({
13052
+ subagent_type: 'scout-researcher',
13053
+ description: 'Analyze background task concurrency',
13054
+ prompt: \`How does background task concurrency/queueing work?
13055
+ - Find the manager/scheduler code
13056
+ - Document the concurrency model
13057
+ - Return file paths with evidence\`,
13058
+ });
13059
+
13060
+ task({
13061
+ subagent_type: 'scout-researcher',
13062
+ description: 'Find parent notification mechanism',
13063
+ prompt: \`How does parent notification work for background tasks?
13064
+ - Where is the notification built?
13065
+ - How is it sent to the parent session?
13066
+ - Return file paths with evidence\`,
13067
+ });
13068
+ \`\`\`
13069
+
13003
13070
  **Key points:**
13004
13071
  - Use \`agent: "scout-researcher"\` for read-only exploration
13005
13072
  - Use \`sync: false\` to return immediately (non-blocking)
@@ -13020,6 +13087,7 @@ You'll receive a \`<system-reminder>\` notification when each task completes.
13020
13087
  When notified of completion, retrieve results:
13021
13088
 
13022
13089
  \`\`\`typescript
13090
+ // Path A: Hive background tools
13023
13091
  // Get output from completed task
13024
13092
  hive_background_output({
13025
13093
  task_id: "task-abc123",
@@ -13058,6 +13126,7 @@ Combine results from all tasks:
13058
13126
  Cancel tasks that are no longer needed:
13059
13127
 
13060
13128
  \`\`\`typescript
13129
+ // Path A: Hive background tools
13061
13130
  // Cancel specific task
13062
13131
  hive_background_cancel({ task_id: "task-abc123" })
13063
13132
 
@@ -13120,6 +13189,7 @@ Return:
13120
13189
 
13121
13190
  **Fan-out:**
13122
13191
  \`\`\`typescript
13192
+ // Path A: Hive background tools
13123
13193
  // Task 1: Implementation
13124
13194
  hive_background_task({
13125
13195
  agent: "scout-researcher",
@@ -13145,6 +13215,28 @@ hive_background_task({
13145
13215
  })
13146
13216
  \`\`\`
13147
13217
 
13218
+ \`\`\`typescript
13219
+ // Path B: Task mode (native task tool)
13220
+ // Parallelize by issuing multiple task() calls in the same assistant message.
13221
+ task({
13222
+ subagent_type: 'scout-researcher',
13223
+ description: 'Find hive_background_task implementation',
13224
+ prompt: 'Where is hive_background_task implemented? Find tool definition and registration.',
13225
+ });
13226
+
13227
+ task({
13228
+ subagent_type: 'scout-researcher',
13229
+ description: 'Analyze concurrency model',
13230
+ prompt: 'How does background task concurrency work? Find the manager/scheduler.',
13231
+ });
13232
+
13233
+ task({
13234
+ subagent_type: 'scout-researcher',
13235
+ description: 'Find notification mechanism',
13236
+ prompt: 'How are parent sessions notified of task completion?',
13237
+ });
13238
+ \`\`\`
13239
+
13148
13240
  **Results:**
13149
13241
  - Task 1: Found \`background-tools.ts\` (tool definition), \`index.ts\` (registration)
13150
13242
  - Task 2: Found \`manager.ts\` with concurrency=3 default, queue-based scheduling
@@ -13169,6 +13261,13 @@ hive_background_task({ ..., sync: false }) // Returns immediately
13169
13261
  // ... later, collect results with hive_background_output
13170
13262
  \`\`\`
13171
13263
 
13264
+ \`\`\`typescript
13265
+ // GOOD (task mode): Spawn all in the same assistant message
13266
+ task({ ... });
13267
+ task({ ... });
13268
+ task({ ... });
13269
+ \`\`\`
13270
+
13172
13271
  **Too many tasks (diminishing returns):**
13173
13272
  - 2-4 tasks: Good parallelization
13174
13273
  - 5+ tasks: Overhead exceeds benefit, harder to synthesize
@@ -13192,8 +13291,9 @@ hive_background_task({ ..., sync: false }) // Returns immediately
13192
13291
 
13193
13292
  After using this pattern, verify:
13194
13293
  - [ ] All tasks spawned before collecting any results (true fan-out)
13195
- - [ ] Received notifications for completed tasks
13196
- - [ ] Successfully retrieved output with \`hive_background_output\`
13294
+ - [ ] Received notifications for completed tasks (Path A)
13295
+ - [ ] Successfully retrieved output with \`hive_background_output\` (Path A)
13296
+ - [ ] Verified \`task()\` fan-out pattern used when in task mode (Path B)
13197
13297
  - [ ] Synthesized findings into coherent answer`
13198
13298
  },
13199
13299
  {
@@ -14281,9 +14381,11 @@ Run \`hive_status()\` or \`hive_feature_list()\` to detect phase:
14281
14381
  ### Delegation
14282
14382
 
14283
14383
  - Single-scout research → \`hive_background_task(agent: "scout-researcher", sync: true, ...)\` (blocks until complete, simpler flow)
14284
- - Parallel exploration → Load \`hive_skill("parallel-exploration")\` and use \`hive_background_task(agent: "scout-researcher", sync: false, ...)\`
14384
+ - Parallel exploration → Load \`hive_skill("parallel-exploration")\` and follow the task vs hive mode delegation guidance.
14285
14385
  - Implementation → \`hive_exec_start(task)\` (spawns Forager)
14286
14386
 
14387
+ In task mode, use task() for research fan-out; in hive mode, use hive_background_task.
14388
+
14287
14389
  During Planning, default to synchronous exploration (\`sync: true\`). If async/parallel exploration would help, ask the user via \`question()\`.
14288
14390
 
14289
14391
  ### Context Persistence
@@ -14306,7 +14408,7 @@ Load when detailed guidance needed:
14306
14408
  - \`hive_skill("brainstorming")\` - exploring ideas and requirements
14307
14409
  - \`hive_skill("writing-plans")\` - structuring implementation plans
14308
14410
  - \`hive_skill("dispatching-parallel-agents")\` - parallel task delegation
14309
- - \`hive_skill("parallel-exploration")\` - parallel read-only research via hive_background_task (Scout fan-out)
14411
+ - \`hive_skill("parallel-exploration")\` - parallel read-only research via task() or hive_background_task (Scout fan-out)
14310
14412
  - \`hive_skill("executing-plans")\` - step-by-step plan execution
14311
14413
 
14312
14414
  Load ONE skill at a time. Only when you need guidance beyond this prompt.
@@ -14365,6 +14467,13 @@ If yes → \`task({ subagent_type: "hygienic", prompt: "Review plan..." })\`
14365
14467
 
14366
14468
  *Active when: plan approved, tasks exist*
14367
14469
 
14470
+ ### Task Dependencies (Always Check)
14471
+
14472
+ Use \`hive_status()\` to see **runnable** tasks (dependencies satisfied) and **blockedBy** info.
14473
+ - Only start tasks from the runnable list
14474
+ - When 2+ tasks are runnable: ask operator via \`question()\` before parallelizing
14475
+ - Record execution decisions with \`hive_context_write({ name: "execution-decisions", ... })\`
14476
+
14368
14477
  ### When to Load Skills
14369
14478
 
14370
14479
  - Multiple independent tasks → \`hive_skill("dispatching-parallel-agents")\`
@@ -14500,7 +14609,9 @@ Plan MUST include:
14500
14609
  **Never:**
14501
14610
  - Execute code (you plan, not implement)
14502
14611
  - Spawn implementation/coding workers (Swarm (Orchestrator) does this); read-only research delegation to Scout is allowed
14503
- - Use the task tool
14612
+ - You may use task() to delegate read-only research to Scout and plan review to Hygienic.
14613
+ - Never use task() to delegate implementation or coding work.
14614
+ - Tool availability depends on delegateMode.
14504
14615
  - Skip discovery for complex tasks
14505
14616
  - Assume when uncertain - ASK
14506
14617
 
@@ -14530,16 +14641,26 @@ Delegate by default. Work yourself only when trivial.
14530
14641
  |------|--------|--------|
14531
14642
  | Trivial | Single file, known location | Direct tools only |
14532
14643
  | Explicit | Specific file/line, clear command | Execute directly |
14533
- | Exploratory | "How does X work?" | Delegate to Scout via hive_background_task(agent: "scout-researcher", sync: false, …). |
14644
+ | Exploratory | "How does X work?" | Delegate to Scout via the parallel-exploration playbook. |
14534
14645
  | Open-ended | "Improve", "Refactor" | Assess first, then delegate |
14535
14646
  | Ambiguous | Unclear scope | Ask ONE clarifying question |
14536
14647
 
14537
14648
  ## Delegation Check (Before Acting)
14538
14649
 
14650
+ ### Task Dependencies (Always Check)
14651
+
14652
+ Use \`hive_status()\` to see **runnable** tasks (dependencies satisfied) and **blockedBy** info.
14653
+ - Only start tasks from the runnable list
14654
+ - When 2+ tasks are runnable: ask operator via \`question()\` before parallelizing
14655
+ - Record execution decisions with \`hive_context_write({ name: "execution-decisions", ... })\`
14656
+
14657
+ ### Standard Checks
14658
+
14539
14659
  1. Is there a specialized agent that matches?
14540
14660
  2. Can I do it myself FOR SURE? REALLY?
14541
14661
  3. Does this require external system data (DBs/APIs/3rd-party tools)?
14542
14662
  → If external data needed: Load \`hive_skill("parallel-exploration")\` for parallel Scout fan-out
14663
+ In task mode, use task() for research fan-out; in hive mode, use hive_background_task.
14543
14664
  During Planning, default to synchronous exploration. If async exploration would help, ask the user via \`question()\` and follow the onboarding preferences.
14544
14665
  → Default: DELEGATE
14545
14666
 
@@ -14562,7 +14683,7 @@ hive_exec_start({ task: "01-task-name" })
14562
14683
  hive_background_task({ agent: "forager-worker", prompt: "...", sync: false })
14563
14684
  // If external system data is needed (parallel exploration):
14564
14685
  // Load hive_skill("parallel-exploration") for the full playbook, then:
14565
- hive_background_task({ agent: "scout-researcher", prompt: "...", sync: false })
14686
+ // In task mode, use task() for research fan-out; in hive mode, use hive_background_task.
14566
14687
  \`\`\`
14567
14688
 
14568
14689
  **Sync Mode Guidance:**
@@ -15860,7 +15981,7 @@ var DEFAULT_HIVE_CONFIG = {
15860
15981
  model: DEFAULT_AGENT_MODELS["scout-researcher"],
15861
15982
  temperature: 0.5,
15862
15983
  skills: [],
15863
- autoLoadSkills: ["parallel-exploration"]
15984
+ autoLoadSkills: []
15864
15985
  },
15865
15986
  "forager-worker": {
15866
15987
  model: DEFAULT_AGENT_MODELS["forager-worker"],
@@ -16356,6 +16477,7 @@ class TaskService {
16356
16477
  throw new Error(`No plan.md found for feature '${featureName}'`);
16357
16478
  }
16358
16479
  const planTasks = this.parseTasksFromPlan(planContent);
16480
+ this.validateDependencyGraph(planTasks, featureName);
16359
16481
  const existingTasks = this.list(featureName);
16360
16482
  const result = {
16361
16483
  created: [],
@@ -16388,7 +16510,7 @@ class TaskService {
16388
16510
  }
16389
16511
  for (const planTask of planTasks) {
16390
16512
  if (!existingByName.has(planTask.folder)) {
16391
- this.createFromPlan(featureName, planTask, planTasks);
16513
+ this.createFromPlan(featureName, planTask, planTasks, planContent);
16392
16514
  result.created.push(planTask.folder);
16393
16515
  }
16394
16516
  }
@@ -16409,49 +16531,156 @@ class TaskService {
16409
16531
  writeJson(getTaskStatusPath(this.projectRoot, featureName, folder), status);
16410
16532
  return folder;
16411
16533
  }
16412
- createFromPlan(featureName, task, allTasks) {
16534
+ createFromPlan(featureName, task, allTasks, planContent) {
16413
16535
  const taskPath = getTaskPath(this.projectRoot, featureName, task.folder);
16414
16536
  ensureDir(taskPath);
16537
+ const dependsOn = this.resolveDependencies(task, allTasks);
16415
16538
  const status = {
16416
16539
  status: "pending",
16417
16540
  origin: "plan",
16418
- planTitle: task.name
16541
+ planTitle: task.name,
16542
+ dependsOn
16419
16543
  };
16420
16544
  writeJson(getTaskStatusPath(this.projectRoot, featureName, task.folder), status);
16545
+ const specContent = this.buildSpecContent({
16546
+ featureName,
16547
+ task,
16548
+ dependsOn,
16549
+ allTasks,
16550
+ planContent
16551
+ });
16552
+ writeText(getTaskSpecPath(this.projectRoot, featureName, task.folder), specContent);
16553
+ }
16554
+ buildSpecContent(params) {
16555
+ const { featureName, task, dependsOn, allTasks, planContent, contextFiles = [], completedTasks = [] } = params;
16421
16556
  const specLines = [
16422
- `# Task ${task.order}: ${task.name}`,
16557
+ `# Task: ${task.folder}`,
16423
16558
  "",
16424
- `**Feature:** ${featureName}`,
16425
- `**Folder:** ${task.folder}`,
16426
- `**Status:** pending`,
16559
+ `## Feature: ${featureName}`,
16427
16560
  "",
16428
- "---",
16429
- "",
16430
- "## Description",
16431
- "",
16432
- task.description || "_No description provided in plan_",
16561
+ "## Dependencies",
16433
16562
  ""
16434
16563
  ];
16435
- if (task.order > 1) {
16436
- const priorTasks = allTasks.filter((t) => t.order < task.order);
16437
- if (priorTasks.length > 0) {
16438
- specLines.push("---", "", "## Prior Tasks", "");
16439
- for (const prior of priorTasks) {
16440
- specLines.push(`- **${prior.order}. ${prior.name}** (${prior.folder})`);
16564
+ if (dependsOn.length > 0) {
16565
+ for (const dep of dependsOn) {
16566
+ const depTask = allTasks.find((t) => t.folder === dep);
16567
+ if (depTask) {
16568
+ specLines.push(`- **${depTask.order}. ${depTask.name}** (${dep})`);
16569
+ } else {
16570
+ specLines.push(`- ${dep}`);
16441
16571
  }
16442
- specLines.push("");
16443
16572
  }
16573
+ } else {
16574
+ specLines.push("_None_");
16575
+ }
16576
+ specLines.push("", "## Plan Section", "");
16577
+ const planSection = this.extractPlanSection(planContent ?? null, task);
16578
+ if (planSection) {
16579
+ specLines.push(planSection.trim());
16580
+ } else {
16581
+ specLines.push("_No plan section available._");
16582
+ }
16583
+ specLines.push("");
16584
+ if (contextFiles.length > 0) {
16585
+ const contextCompiled = contextFiles.map((f) => `## ${f.name}
16586
+
16587
+ ${f.content}`).join(`
16588
+
16589
+ ---
16590
+
16591
+ `);
16592
+ specLines.push("## Context", "", contextCompiled, "");
16593
+ }
16594
+ if (completedTasks.length > 0) {
16595
+ const completedLines = completedTasks.map((t) => `- ${t.name}: ${t.summary}`);
16596
+ specLines.push("## Completed Tasks", "", ...completedLines, "");
16597
+ }
16598
+ return specLines.join(`
16599
+ `);
16600
+ }
16601
+ extractPlanSection(planContent, task) {
16602
+ if (!planContent)
16603
+ return null;
16604
+ const escapedTitle = task.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16605
+ const titleRegex = new RegExp(`###\\s*\\d+\\.\\s*${escapedTitle}[\\s\\S]*?(?=###|$)`, "i");
16606
+ let taskMatch = planContent.match(titleRegex);
16607
+ if (!taskMatch && task.order > 0) {
16608
+ const orderRegex = new RegExp(`###\\s*${task.order}\\.\\s*[^\\n]+[\\s\\S]*?(?=###|$)`, "i");
16609
+ taskMatch = planContent.match(orderRegex);
16610
+ }
16611
+ return taskMatch ? taskMatch[0].trim() : null;
16612
+ }
16613
+ resolveDependencies(task, allTasks) {
16614
+ if (task.dependsOnNumbers !== null && task.dependsOnNumbers.length === 0) {
16615
+ return [];
16444
16616
  }
16445
- const nextTasks = allTasks.filter((t) => t.order > task.order);
16446
- if (nextTasks.length > 0) {
16447
- specLines.push("---", "", "## Upcoming Tasks", "");
16448
- for (const next of nextTasks) {
16449
- specLines.push(`- **${next.order}. ${next.name}** (${next.folder})`);
16617
+ if (task.dependsOnNumbers !== null) {
16618
+ return task.dependsOnNumbers.map((num) => allTasks.find((t) => t.order === num)?.folder).filter((folder) => folder !== undefined);
16619
+ }
16620
+ if (task.order === 1) {
16621
+ return [];
16622
+ }
16623
+ const previousTask = allTasks.find((t) => t.order === task.order - 1);
16624
+ return previousTask ? [previousTask.folder] : [];
16625
+ }
16626
+ validateDependencyGraph(tasks, featureName) {
16627
+ const taskNumbers = new Set(tasks.map((t) => t.order));
16628
+ for (const task of tasks) {
16629
+ if (task.dependsOnNumbers === null) {
16630
+ continue;
16631
+ }
16632
+ for (const depNum of task.dependsOnNumbers) {
16633
+ if (depNum === task.order) {
16634
+ throw new Error(`Invalid dependency graph in plan.md: Self-dependency detected for task ${task.order} ("${task.name}"). ` + `A task cannot depend on itself. Please fix the "Depends on:" line in plan.md.`);
16635
+ }
16636
+ if (!taskNumbers.has(depNum)) {
16637
+ throw new Error(`Invalid dependency graph in plan.md: Unknown task number ${depNum} referenced in dependencies for task ${task.order} ("${task.name}"). ` + `Available task numbers are: ${Array.from(taskNumbers).sort((a, b) => a - b).join(", ")}. ` + `Please fix the "Depends on:" line in plan.md.`);
16638
+ }
16639
+ }
16640
+ }
16641
+ this.detectCycles(tasks);
16642
+ }
16643
+ detectCycles(tasks) {
16644
+ const taskByOrder = new Map(tasks.map((t) => [t.order, t]));
16645
+ const getDependencies = (task) => {
16646
+ if (task.dependsOnNumbers !== null) {
16647
+ return task.dependsOnNumbers;
16648
+ }
16649
+ if (task.order === 1) {
16650
+ return [];
16651
+ }
16652
+ return [task.order - 1];
16653
+ };
16654
+ const visited = new Map;
16655
+ const path32 = [];
16656
+ const dfs = (taskOrder) => {
16657
+ const state = visited.get(taskOrder);
16658
+ if (state === 2) {
16659
+ return;
16660
+ }
16661
+ if (state === 1) {
16662
+ const cycleStart = path32.indexOf(taskOrder);
16663
+ const cyclePath = [...path32.slice(cycleStart), taskOrder];
16664
+ const cycleDesc = cyclePath.join(" -> ");
16665
+ throw new Error(`Invalid dependency graph in plan.md: Cycle detected in task dependencies: ${cycleDesc}. ` + `Tasks cannot have circular dependencies. Please fix the "Depends on:" lines in plan.md.`);
16666
+ }
16667
+ visited.set(taskOrder, 1);
16668
+ path32.push(taskOrder);
16669
+ const task = taskByOrder.get(taskOrder);
16670
+ if (task) {
16671
+ const deps = getDependencies(task);
16672
+ for (const depOrder of deps) {
16673
+ dfs(depOrder);
16674
+ }
16675
+ }
16676
+ path32.pop();
16677
+ visited.set(taskOrder, 2);
16678
+ };
16679
+ for (const task of tasks) {
16680
+ if (!visited.has(task.order)) {
16681
+ dfs(task.order);
16450
16682
  }
16451
- specLines.push("");
16452
16683
  }
16453
- writeText(getTaskSpecPath(this.projectRoot, featureName, task.folder), specLines.join(`
16454
- `));
16455
16684
  }
16456
16685
  writeSpec(featureName, taskFolder, content) {
16457
16686
  const specPath = getTaskSpecPath(this.projectRoot, featureName, taskFolder);
@@ -16542,6 +16771,7 @@ class TaskService {
16542
16771
  `);
16543
16772
  let currentTask = null;
16544
16773
  let descriptionLines = [];
16774
+ const dependsOnRegex = /^\s*\*{0,2}Depends\s+on\*{0,2}\s*:\s*(.+)$/i;
16545
16775
  for (const line of lines) {
16546
16776
  const taskMatch = line.match(/^###\s+(\d+)\.\s+(.+)$/);
16547
16777
  if (taskMatch) {
@@ -16558,7 +16788,8 @@ class TaskService {
16558
16788
  folder,
16559
16789
  order,
16560
16790
  name: rawName,
16561
- description: ""
16791
+ description: "",
16792
+ dependsOnNumbers: null
16562
16793
  };
16563
16794
  descriptionLines = [];
16564
16795
  } else if (currentTask) {
@@ -16569,6 +16800,16 @@ class TaskService {
16569
16800
  currentTask = null;
16570
16801
  descriptionLines = [];
16571
16802
  } else {
16803
+ const dependsMatch = line.match(dependsOnRegex);
16804
+ if (dependsMatch) {
16805
+ const value = dependsMatch[1].trim().toLowerCase();
16806
+ if (value === "none") {
16807
+ currentTask.dependsOnNumbers = [];
16808
+ } else {
16809
+ const numbers = value.split(/[,\s]+/).map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
16810
+ currentTask.dependsOnNumbers = numbers;
16811
+ }
16812
+ }
16572
16813
  descriptionLines.push(line);
16573
16814
  }
16574
16815
  }
@@ -21356,6 +21597,62 @@ class ConfigService {
21356
21597
  return this.getDelegateMode() === "hive";
21357
21598
  }
21358
21599
  }
21600
+ function computeRunnableAndBlocked(tasks) {
21601
+ const statusByFolder = new Map;
21602
+ for (const task of tasks) {
21603
+ statusByFolder.set(task.folder, task.status);
21604
+ }
21605
+ const runnable = [];
21606
+ const blocked = {};
21607
+ const effectiveDepsByFolder = buildEffectiveDependencies(tasks);
21608
+ for (const task of tasks) {
21609
+ if (task.status !== "pending") {
21610
+ continue;
21611
+ }
21612
+ const deps = effectiveDepsByFolder.get(task.folder) ?? [];
21613
+ const unmetDeps = deps.filter((dep) => {
21614
+ const depStatus = statusByFolder.get(dep);
21615
+ return depStatus !== "done";
21616
+ });
21617
+ if (unmetDeps.length === 0) {
21618
+ runnable.push(task.folder);
21619
+ } else {
21620
+ blocked[task.folder] = unmetDeps;
21621
+ }
21622
+ }
21623
+ return { runnable, blocked };
21624
+ }
21625
+ function buildEffectiveDependencies(tasks) {
21626
+ const orderByFolder = new Map;
21627
+ const folderByOrder = new Map;
21628
+ for (const task of tasks) {
21629
+ const match = task.folder.match(/^(\d+)-/);
21630
+ if (!match) {
21631
+ orderByFolder.set(task.folder, null);
21632
+ continue;
21633
+ }
21634
+ const order = parseInt(match[1], 10);
21635
+ orderByFolder.set(task.folder, order);
21636
+ if (!folderByOrder.has(order)) {
21637
+ folderByOrder.set(order, task.folder);
21638
+ }
21639
+ }
21640
+ const effectiveDeps = new Map;
21641
+ for (const task of tasks) {
21642
+ if (task.dependsOn !== undefined) {
21643
+ effectiveDeps.set(task.folder, task.dependsOn);
21644
+ continue;
21645
+ }
21646
+ const order = orderByFolder.get(task.folder);
21647
+ if (!order || order <= 1) {
21648
+ effectiveDeps.set(task.folder, []);
21649
+ continue;
21650
+ }
21651
+ const previousFolder = folderByOrder.get(order - 1);
21652
+ effectiveDeps.set(task.folder, previousFolder ? [previousFolder] : []);
21653
+ }
21654
+ return effectiveDeps;
21655
+ }
21359
21656
 
21360
21657
  // src/utils/worker-prompt.ts
21361
21658
  function buildWorkerPrompt(params) {
@@ -22634,7 +22931,11 @@ class BackgroundManager {
22634
22931
  parts: [{ type: "text", text: options.prompt }],
22635
22932
  tools: {
22636
22933
  background_task: false,
22637
- delegate: false
22934
+ delegate: false,
22935
+ hive_background_task: false,
22936
+ hive_background_output: false,
22937
+ hive_background_cancel: false,
22938
+ task: false
22638
22939
  },
22639
22940
  ...normalizedVariant !== undefined && { variant: normalizedVariant }
22640
22941
  }
@@ -22649,6 +22950,36 @@ class BackgroundManager {
22649
22950
  };
22650
22951
  }
22651
22952
  checkHiveTaskOrdering(feature, taskFolder) {
22953
+ const taskStatus = this.taskService.getRawStatus(feature, taskFolder);
22954
+ if (taskStatus?.dependsOn !== undefined) {
22955
+ return this.checkDependencies(feature, taskFolder, taskStatus.dependsOn);
22956
+ }
22957
+ return this.checkNumericOrdering(feature, taskFolder);
22958
+ }
22959
+ checkDependencies(feature, taskFolder, dependsOn) {
22960
+ if (dependsOn.length === 0) {
22961
+ return { allowed: true };
22962
+ }
22963
+ const unmetDeps = [];
22964
+ for (const depFolder of dependsOn) {
22965
+ const depStatus = this.taskService.getRawStatus(feature, depFolder);
22966
+ if (!depStatus || depStatus.status !== "done") {
22967
+ unmetDeps.push({
22968
+ folder: depFolder,
22969
+ status: depStatus?.status ?? "unknown"
22970
+ });
22971
+ }
22972
+ }
22973
+ if (unmetDeps.length > 0) {
22974
+ const depList = unmetDeps.map((d) => `"${d.folder}" (${d.status})`).join(", ");
22975
+ return {
22976
+ allowed: false,
22977
+ error: `Dependency constraint: Task "${taskFolder}" cannot start - dependencies not done: ${depList}. ` + `Only tasks with status 'done' satisfy dependencies.`
22978
+ };
22979
+ }
22980
+ return { allowed: true };
22981
+ }
22982
+ checkNumericOrdering(feature, taskFolder) {
22652
22983
  const orderMatch = taskFolder.match(/^(\d+)-/);
22653
22984
  if (!orderMatch) {
22654
22985
  return { allowed: true };
@@ -22716,7 +23047,7 @@ class BackgroundManager {
22716
23047
  **Description:** ${task.description}
22717
23048
  **Agent:** ${task.agent}${errorLine}
22718
23049
 
22719
- Use \`background_output({ task_id: "${task.taskId}" })\` to retrieve the result.
23050
+ Use \`hive_background_output({ task_id: "${task.taskId}" })\` to retrieve the result.
22720
23051
  </system-reminder>`;
22721
23052
  try {
22722
23053
  await this.client.session.prompt({
@@ -22913,6 +23244,22 @@ function createBackgroundTools(manager, client, configService) {
22913
23244
  attempt
22914
23245
  }, toolContext) {
22915
23246
  const ctx = toolContext;
23247
+ const ALLOWED_CALLERS = new Set([
23248
+ "hive-master",
23249
+ "architect-planner",
23250
+ "swarm-orchestrator"
23251
+ ]);
23252
+ const callerAgent = ctx?.agent;
23253
+ if (!callerAgent || !ALLOWED_CALLERS.has(callerAgent)) {
23254
+ const output = {
23255
+ provider: "hive",
23256
+ task_id: "",
23257
+ session_id: "",
23258
+ status: "error",
23259
+ error: `Agent "${callerAgent ?? "unknown"}" is not allowed to spawn background tasks. Only orchestrator agents (${[...ALLOWED_CALLERS].join(", ")}) can delegate.`
23260
+ };
23261
+ return JSON.stringify(output, null, 2);
23262
+ }
22916
23263
  let resolvedPrompt = prompt;
22917
23264
  if (promptFile) {
22918
23265
  const baseDir = workdir || process.cwd();
@@ -23424,6 +23771,43 @@ To unblock: Remove .hive/features/${feature}/BLOCKED`;
23424
23771
  }
23425
23772
  return null;
23426
23773
  };
23774
+ const checkDependencies = (feature, taskFolder) => {
23775
+ const taskStatus = taskService.getRawStatus(feature, taskFolder);
23776
+ if (!taskStatus) {
23777
+ return { allowed: true };
23778
+ }
23779
+ const tasks = taskService.list(feature).map((task) => {
23780
+ const status = taskService.getRawStatus(feature, task.folder);
23781
+ return {
23782
+ folder: task.folder,
23783
+ status: task.status,
23784
+ dependsOn: status?.dependsOn
23785
+ };
23786
+ });
23787
+ const effectiveDeps = buildEffectiveDependencies(tasks);
23788
+ const deps = effectiveDeps.get(taskFolder) ?? [];
23789
+ if (deps.length === 0) {
23790
+ return { allowed: true };
23791
+ }
23792
+ const unmetDeps = [];
23793
+ for (const depFolder of deps) {
23794
+ const depStatus = taskService.getRawStatus(feature, depFolder);
23795
+ if (!depStatus || depStatus.status !== "done") {
23796
+ unmetDeps.push({
23797
+ folder: depFolder,
23798
+ status: depStatus?.status ?? "unknown"
23799
+ });
23800
+ }
23801
+ }
23802
+ if (unmetDeps.length > 0) {
23803
+ const depList = unmetDeps.map((d) => `"${d.folder}" (${d.status})`).join(", ");
23804
+ return {
23805
+ allowed: false,
23806
+ error: `Dependency constraint: Task "${taskFolder}" cannot start - dependencies not done: ${depList}. ` + `Only tasks with status 'done' satisfy dependencies.`
23807
+ };
23808
+ }
23809
+ return { allowed: true };
23810
+ };
23427
23811
  return {
23428
23812
  "experimental.chat.system.transform": async (input, output) => {
23429
23813
  output.system.push(HIVE_SYSTEM_PROMPT);
@@ -23694,9 +24078,9 @@ Reminder: start work with hive_exec_start to use its worktree, and ensure any su
23694
24078
  const feature = resolveFeature(explicitFeature);
23695
24079
  if (!feature)
23696
24080
  return "Error: No feature specified. Create a feature or provide feature param.";
23697
- const blocked = checkBlocked(feature);
23698
- if (blocked)
23699
- return blocked;
24081
+ const blockedMessage = checkBlocked(feature);
24082
+ if (blockedMessage)
24083
+ return blockedMessage;
23700
24084
  const taskInfo = taskService.get(feature, task);
23701
24085
  if (!taskInfo)
23702
24086
  return `Error: Task "${task}" not found`;
@@ -23705,6 +24089,19 @@ Reminder: start work with hive_exec_start to use its worktree, and ensure any su
23705
24089
  if (continueFrom === "blocked" && taskInfo.status !== "blocked") {
23706
24090
  return "Error: Task is not in blocked state. Use without continueFrom.";
23707
24091
  }
24092
+ if (continueFrom !== "blocked") {
24093
+ const depCheck = checkDependencies(feature, task);
24094
+ if (!depCheck.allowed) {
24095
+ return JSON.stringify({
24096
+ success: false,
24097
+ error: depCheck.error,
24098
+ hints: [
24099
+ "Complete the required dependencies before starting this task.",
24100
+ "Use hive_status to see current task states."
24101
+ ]
24102
+ });
24103
+ }
24104
+ }
23708
24105
  let worktree;
23709
24106
  if (continueFrom === "blocked") {
23710
24107
  worktree = await worktreeService.get(feature, task);
@@ -23738,56 +24135,28 @@ Reminder: start work with hive_exec_start to use its worktree, and ensure any su
23738
24135
  ...taskBudgetResult.truncationEvents,
23739
24136
  ...contextBudgetResult.truncationEvents
23740
24137
  ];
23741
- const priorTasksFormatted = previousTasks.map((t) => `- ${t.name}: ${t.summary}`).join(`
23742
- `);
23743
24138
  const droppedTasksHint = taskBudgetResult.droppedTasksHint;
23744
- let specContent = `# Task: ${task}
23745
-
23746
- `;
23747
- specContent += `## Feature: ${feature}
23748
-
23749
- `;
23750
- if (planResult) {
23751
- const planTitle = taskInfo.planTitle ?? taskInfo.name;
23752
- const escapedTitle = planTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23753
- const titleRegex = new RegExp(`###\\s*\\d+\\.\\s*${escapedTitle}[\\s\\S]*?(?=###|$)`, "i");
23754
- let taskMatch = planResult.content.match(titleRegex);
23755
- if (!taskMatch) {
23756
- const orderMatch = taskInfo.folder.match(/^(\d+)-/);
23757
- if (orderMatch) {
23758
- const orderRegex = new RegExp(`###\\s*${orderMatch[1]}\\.\\s*[^\\n]+[\\s\\S]*?(?=###|$)`, "i");
23759
- taskMatch = planResult.content.match(orderRegex);
23760
- }
23761
- }
23762
- if (taskMatch) {
23763
- specContent += `## Plan Section
23764
-
23765
- ${taskMatch[0].trim()}
23766
-
23767
- `;
23768
- }
23769
- }
23770
- if (contextFiles.length > 0) {
23771
- const contextCompiled = contextFiles.map((f) => `## ${f.name}
23772
-
23773
- ${f.content}`).join(`
23774
-
23775
- ---
23776
-
23777
- `);
23778
- specContent += `## Context
23779
-
23780
- ${contextCompiled}
23781
-
23782
- `;
23783
- }
23784
- if (priorTasksFormatted) {
23785
- specContent += `## Completed Tasks
23786
-
23787
- ${priorTasksFormatted}
23788
-
23789
- `;
23790
- }
24139
+ const taskOrder = parseInt(taskInfo.folder.match(/^(\d+)/)?.[1] || "0", 10);
24140
+ const status = taskService.getRawStatus(feature, task);
24141
+ const dependsOn = status?.dependsOn ?? [];
24142
+ const specContent = taskService.buildSpecContent({
24143
+ featureName: feature,
24144
+ task: {
24145
+ folder: task,
24146
+ name: taskInfo.planTitle ?? taskInfo.name,
24147
+ order: taskOrder,
24148
+ description: undefined
24149
+ },
24150
+ dependsOn,
24151
+ allTasks: allTasks.map((t) => ({
24152
+ folder: t.folder,
24153
+ name: t.name,
24154
+ order: parseInt(t.folder.match(/^(\d+)/)?.[1] || "0", 10)
24155
+ })),
24156
+ planContent: planResult?.content ?? null,
24157
+ contextFiles,
24158
+ completedTasks: previousTasks
24159
+ });
23791
24160
  taskService.writeSpec(feature, task, specContent);
23792
24161
  const workerPrompt = buildWorkerPrompt({
23793
24162
  feature,
@@ -24214,12 +24583,16 @@ Files changed: ${result.filesChanged?.length || 0}`;
24214
24583
  const plan = planService.read(feature);
24215
24584
  const tasks = taskService.list(feature);
24216
24585
  const contextFiles = contextService.list(feature);
24217
- const tasksSummary = tasks.map((t) => ({
24218
- folder: t.folder,
24219
- name: t.name,
24220
- status: t.status,
24221
- origin: t.origin || "plan"
24222
- }));
24586
+ const tasksSummary = tasks.map((t) => {
24587
+ const rawStatus = taskService.getRawStatus(feature, t.folder);
24588
+ return {
24589
+ folder: t.folder,
24590
+ name: t.name,
24591
+ status: t.status,
24592
+ origin: t.origin || "plan",
24593
+ dependsOn: rawStatus?.dependsOn ?? null
24594
+ };
24595
+ });
24223
24596
  const contextSummary = contextFiles.map((c) => ({
24224
24597
  name: c.name,
24225
24598
  chars: c.content.length,
@@ -24228,7 +24601,18 @@ Files changed: ${result.filesChanged?.length || 0}`;
24228
24601
  const pendingTasks = tasksSummary.filter((t) => t.status === "pending");
24229
24602
  const inProgressTasks = tasksSummary.filter((t) => t.status === "in_progress");
24230
24603
  const doneTasks = tasksSummary.filter((t) => t.status === "done");
24231
- const getNextAction = (planStatus2, tasks2) => {
24604
+ const tasksWithDeps = tasksSummary.map((t) => ({
24605
+ folder: t.folder,
24606
+ status: t.status,
24607
+ dependsOn: t.dependsOn ?? undefined
24608
+ }));
24609
+ const effectiveDeps = buildEffectiveDependencies(tasksWithDeps);
24610
+ const normalizedTasks = tasksWithDeps.map((task) => ({
24611
+ ...task,
24612
+ dependsOn: effectiveDeps.get(task.folder)
24613
+ }));
24614
+ const { runnable, blocked: blockedBy } = computeRunnableAndBlocked(normalizedTasks);
24615
+ const getNextAction = (planStatus2, tasks2, runnableTasks) => {
24232
24616
  if (!planStatus2 || planStatus2 === "draft") {
24233
24617
  return "Write or revise plan with hive_plan_write, then get approval";
24234
24618
  }
@@ -24242,9 +24626,15 @@ Files changed: ${result.filesChanged?.length || 0}`;
24242
24626
  if (inProgress) {
24243
24627
  return `Continue work on task: ${inProgress.folder}`;
24244
24628
  }
24629
+ if (runnableTasks.length > 1) {
24630
+ return `${runnableTasks.length} tasks are ready to start in parallel: ${runnableTasks.join(", ")}`;
24631
+ }
24632
+ if (runnableTasks.length === 1) {
24633
+ return `Start next task with hive_exec_start: ${runnableTasks[0]}`;
24634
+ }
24245
24635
  const pending = tasks2.find((t) => t.status === "pending");
24246
24636
  if (pending) {
24247
- return `Start next task with hive_exec_start: ${pending.folder}`;
24637
+ return `Pending tasks exist but are blocked by dependencies. Check blockedBy for details.`;
24248
24638
  }
24249
24639
  return "All tasks complete. Review and merge or complete feature.";
24250
24640
  };
@@ -24266,13 +24656,15 @@ Files changed: ${result.filesChanged?.length || 0}`;
24266
24656
  pending: pendingTasks.length,
24267
24657
  inProgress: inProgressTasks.length,
24268
24658
  done: doneTasks.length,
24269
- list: tasksSummary
24659
+ list: tasksSummary,
24660
+ runnable,
24661
+ blockedBy
24270
24662
  },
24271
24663
  context: {
24272
24664
  fileCount: contextFiles.length,
24273
24665
  files: contextSummary
24274
24666
  },
24275
- nextAction: getNextAction(planStatus, tasksSummary)
24667
+ nextAction: getNextAction(planStatus, tasksSummary, runnable)
24276
24668
  });
24277
24669
  }
24278
24670
  }),
@@ -24406,7 +24798,7 @@ Make the requested changes, then call hive_request_review again.`;
24406
24798
  prompt: ARCHITECT_BEE_PROMPT + architectAutoLoadedSkills,
24407
24799
  permission: {
24408
24800
  edit: "deny",
24409
- task: "deny",
24801
+ task: "allow",
24410
24802
  question: "allow",
24411
24803
  skill: "allow",
24412
24804
  todowrite: "allow",
@@ -24444,6 +24836,11 @@ Make the requested changes, then call hive_request_review again.`;
24444
24836
  prompt: SCOUT_BEE_PROMPT + scoutAutoLoadedSkills,
24445
24837
  permission: {
24446
24838
  edit: "deny",
24839
+ hive_background_task: "deny",
24840
+ hive_background_output: "deny",
24841
+ hive_background_cancel: "deny",
24842
+ task: "deny",
24843
+ delegate: "deny",
24447
24844
  skill: "allow",
24448
24845
  webfetch: "allow"
24449
24846
  }
@@ -24457,6 +24854,11 @@ Make the requested changes, then call hive_request_review again.`;
24457
24854
  description: "Forager (Worker/Coder) - Executes tasks directly in isolated worktrees. Never delegates.",
24458
24855
  prompt: FORAGER_BEE_PROMPT + foragerAutoLoadedSkills,
24459
24856
  permission: {
24857
+ hive_background_task: "deny",
24858
+ hive_background_output: "deny",
24859
+ hive_background_cancel: "deny",
24860
+ task: "deny",
24861
+ delegate: "deny",
24460
24862
  skill: "allow"
24461
24863
  }
24462
24864
  };
@@ -24470,6 +24872,11 @@ Make the requested changes, then call hive_request_review again.`;
24470
24872
  prompt: HYGIENIC_BEE_PROMPT + hygienicAutoLoadedSkills,
24471
24873
  permission: {
24472
24874
  edit: "deny",
24875
+ hive_background_task: "deny",
24876
+ hive_background_output: "deny",
24877
+ hive_background_cancel: "deny",
24878
+ task: "deny",
24879
+ delegate: "deny",
24473
24880
  skill: "allow"
24474
24881
  }
24475
24882
  };