opencode-hive 1.3.3 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -108,6 +108,32 @@ When using Dynamic Context Pruning (DCP), use a Hive-safe config in `~/.config/o
108
108
 
109
109
  For local plugin testing, keep OpenCode plugin entry as `"opencode-hive"` (not `"opencode-hive@latest"`).
110
110
 
111
+ #### Compaction recovery and session re-anchoring
112
+
113
+ OpenCode can compact long sessions. When that happens mid-orchestration or mid-task, Hive needs the session to recover its role and task boundaries without re-reading the whole repository.
114
+
115
+ The plugin now persists durable session metadata and uses it during `experimental.session.compacting` to rebuild a compact re-anchor prompt.
116
+
117
+ Where:
118
+
119
+ - Global session state is written to `.hive/sessions.json`.
120
+ - Feature-local mirrors are written to `.hive/features/<feature>/sessions.json`.
121
+ - Session classification distinguishes `primary`, `subagent`, `task-worker`, and `unknown`.
122
+ - Primary and subagent recovery can replay the stored user directive once after compaction.
123
+ - For task workers, the re-anchor context can include `.hive/features/<feature>/tasks/<task>/worker-prompt.md`.
124
+
125
+ Task-worker recovery is intentionally strict:
126
+
127
+ - keep the same role
128
+ - do not delegate
129
+ - do not re-read the full codebase
130
+ - re-read `worker-prompt.md`
131
+ - continue from the last known point
132
+
133
+ This split is deliberate: post-compaction replay is for primary/subagent intent, while task-worker recovery comes from durable worktree context plus `worker-prompt.md` so implementation sessions stay attached to the exact task contract.
134
+
135
+ This matters most for `forager-worker` and forager-derived custom agents, because they are the sessions most likely to be compacted mid-implementation.
136
+
111
137
  ## Prompt Budgeting & Observability
112
138
 
113
139
  Hive automatically bounds worker prompt sizes to prevent context overflow and tool output truncation.
@@ -136,6 +162,8 @@ When limits are exceeded, content is truncated with `...[truncated]` markers and
136
162
 
137
163
  Large prompts are written to `.hive/features/<feature>/tasks/<task>/worker-prompt.md` and passed by file reference (`workerPromptPath`) rather than inlined in tool output. This prevents truncation of large prompts.
138
164
 
165
+ That same `worker-prompt.md` path is also reused during compaction recovery so task workers can re-anchor to the exact task assignment after a compacted session resumes.
166
+
139
167
  ## Plan Format
140
168
 
141
169
  ```markdown
@@ -359,6 +387,13 @@ ID guardrails:
359
387
  - plugin-reserved aliases are blocked (`hive`, `architect`, `swarm`, `scout`, `forager`, `hygienic`, `receiver`)
360
388
  - operational IDs are blocked (`build`, `plan`, `code`)
361
389
 
390
+ Compaction classification follows the base agent:
391
+
392
+ - `forager-worker` derivatives are treated as `task-worker`
393
+ - `hygienic-reviewer` derivatives are treated as `subagent`
394
+
395
+ This ensures custom workers recover with the same execution constraints as their base role.
396
+
362
397
  ### Custom Models
363
398
 
364
399
  Override models for specific agents:
@@ -1,10 +1,17 @@
1
- import { SessionInfo } from '../types.js';
1
+ import type { SessionInfo } from '../types.js';
2
2
  export declare class SessionService {
3
3
  private projectRoot;
4
4
  constructor(projectRoot: string);
5
+ private applySessionPatch;
5
6
  private getSessionsPath;
6
7
  private getSessions;
7
8
  private saveSessions;
9
+ private getGlobalSessions;
10
+ private saveGlobalSessions;
11
+ private updateGlobalSessions;
12
+ trackGlobal(sessionId: string, patch?: Partial<SessionInfo>): SessionInfo;
13
+ bindFeature(sessionId: string, featureName: string, patch?: Partial<SessionInfo>): SessionInfo;
14
+ getGlobal(sessionId: string): SessionInfo | undefined;
8
15
  track(featureName: string, sessionId: string, taskFolder?: string): SessionInfo;
9
16
  setMaster(featureName: string, sessionId: string): void;
10
17
  getMaster(featureName: string): string | undefined;
@@ -120,9 +120,17 @@ export interface ContextFile {
120
120
  content: string;
121
121
  updatedAt: string;
122
122
  }
123
+ export type SessionKind = 'primary' | 'subagent' | 'task-worker' | 'unknown';
123
124
  export interface SessionInfo {
124
125
  sessionId: string;
126
+ featureName?: string;
125
127
  taskFolder?: string;
128
+ agent?: string;
129
+ baseAgent?: string;
130
+ sessionKind?: SessionKind;
131
+ workerPromptPath?: string;
132
+ directivePrompt?: string;
133
+ replayDirectivePending?: boolean;
126
134
  startedAt: string;
127
135
  lastActiveAt: string;
128
136
  messageCount?: number;
@@ -3,6 +3,7 @@ export declare function normalizePath(filePath: string): string;
3
3
  export declare function getHivePath(projectRoot: string): string;
4
4
  export declare function getFeaturesPath(projectRoot: string): string;
5
5
  export declare function getActiveFeaturePath(projectRoot: string): string;
6
+ export declare function getGlobalSessionsPath(projectRoot: string): string;
6
7
  export declare function listFeatureDirectories(projectRoot: string): FeatureDirectoryInfo[];
7
8
  export declare function resolveFeatureDirectoryName(projectRoot: string, featureName: string): string;
8
9
  export declare function getNextIndexedFeatureDirectoryName(projectRoot: string, featureName: string): string;
package/dist/index.js CHANGED
@@ -871,8 +871,8 @@ var require_dist2 = __commonJS((exports) => {
871
871
  });
872
872
 
873
873
  // src/index.ts
874
- import * as path10 from "path";
875
- import * as fs12 from "fs";
874
+ import * as path11 from "path";
875
+ import * as fs13 from "fs";
876
876
  import * as os from "os";
877
877
 
878
878
  // ../../node_modules/zod/v4/classic/external.js
@@ -16738,6 +16738,9 @@ function getFeaturesPath(projectRoot) {
16738
16738
  function getActiveFeaturePath(projectRoot) {
16739
16739
  return path2.join(getHivePath(projectRoot), ACTIVE_FEATURE_FILE);
16740
16740
  }
16741
+ function getGlobalSessionsPath(projectRoot) {
16742
+ return path2.join(getHivePath(projectRoot), "sessions.json");
16743
+ }
16741
16744
  function parseIndexedFeatureDirectoryName(directoryName) {
16742
16745
  const match = directoryName.match(/^(\d+)[_-](.+)$/);
16743
16746
  if (!match) {
@@ -22431,17 +22434,217 @@ ${f.content}`);
22431
22434
  return `${normalized}.md`;
22432
22435
  }
22433
22436
  }
22434
- // ../hive-core/src/services/configService.ts
22437
+ // ../hive-core/src/services/sessionService.ts
22435
22438
  import * as fs9 from "fs";
22436
22439
  import * as path7 from "path";
22440
+ class SessionService {
22441
+ projectRoot;
22442
+ constructor(projectRoot) {
22443
+ this.projectRoot = projectRoot;
22444
+ }
22445
+ applySessionPatch(target, patch) {
22446
+ if (!patch) {
22447
+ return;
22448
+ }
22449
+ const { sessionId: _sessionId, ...rest } = patch;
22450
+ Object.assign(target, rest);
22451
+ }
22452
+ getSessionsPath(featureName) {
22453
+ return path7.join(getFeaturePath(this.projectRoot, featureName), "sessions.json");
22454
+ }
22455
+ getSessions(featureName) {
22456
+ const sessionsPath = this.getSessionsPath(featureName);
22457
+ return readJson(sessionsPath) || { sessions: [] };
22458
+ }
22459
+ saveSessions(featureName, data) {
22460
+ const sessionsPath = this.getSessionsPath(featureName);
22461
+ ensureDir(path7.dirname(sessionsPath));
22462
+ writeJson(sessionsPath, data);
22463
+ }
22464
+ getGlobalSessions() {
22465
+ const globalPath = getGlobalSessionsPath(this.projectRoot);
22466
+ return readJson(globalPath) || { sessions: [] };
22467
+ }
22468
+ saveGlobalSessions(data) {
22469
+ const globalPath = getGlobalSessionsPath(this.projectRoot);
22470
+ ensureDir(path7.dirname(globalPath));
22471
+ writeJson(globalPath, data);
22472
+ }
22473
+ updateGlobalSessions(mutator) {
22474
+ const globalPath = getGlobalSessionsPath(this.projectRoot);
22475
+ ensureDir(path7.dirname(globalPath));
22476
+ const release = acquireLockSync(globalPath);
22477
+ try {
22478
+ const data = readJson(globalPath) || { sessions: [] };
22479
+ const session = mutator(data);
22480
+ writeJsonAtomic(globalPath, data);
22481
+ return session;
22482
+ } finally {
22483
+ release();
22484
+ }
22485
+ }
22486
+ trackGlobal(sessionId, patch) {
22487
+ return this.updateGlobalSessions((data) => {
22488
+ const now = new Date().toISOString();
22489
+ let session = data.sessions.find((s) => s.sessionId === sessionId);
22490
+ if (session) {
22491
+ session.lastActiveAt = now;
22492
+ this.applySessionPatch(session, patch);
22493
+ } else {
22494
+ session = {
22495
+ sessionId,
22496
+ startedAt: now,
22497
+ lastActiveAt: now
22498
+ };
22499
+ this.applySessionPatch(session, patch);
22500
+ data.sessions.push(session);
22501
+ }
22502
+ return session;
22503
+ });
22504
+ }
22505
+ bindFeature(sessionId, featureName, patch) {
22506
+ const session = this.updateGlobalSessions((data) => {
22507
+ let current = data.sessions.find((s) => s.sessionId === sessionId);
22508
+ const now = new Date().toISOString();
22509
+ if (!current) {
22510
+ current = {
22511
+ sessionId,
22512
+ startedAt: now,
22513
+ lastActiveAt: now
22514
+ };
22515
+ data.sessions.push(current);
22516
+ }
22517
+ current.featureName = featureName;
22518
+ current.lastActiveAt = now;
22519
+ this.applySessionPatch(current, patch);
22520
+ return current;
22521
+ });
22522
+ const featureData = this.getSessions(featureName);
22523
+ let featureSession = featureData.sessions.find((s) => s.sessionId === sessionId);
22524
+ if (featureSession) {
22525
+ Object.assign(featureSession, session);
22526
+ } else {
22527
+ featureData.sessions.push({ ...session });
22528
+ }
22529
+ this.saveSessions(featureName, featureData);
22530
+ return session;
22531
+ }
22532
+ getGlobal(sessionId) {
22533
+ const data = this.getGlobalSessions();
22534
+ return data.sessions.find((s) => s.sessionId === sessionId);
22535
+ }
22536
+ track(featureName, sessionId, taskFolder) {
22537
+ const data = this.getSessions(featureName);
22538
+ const now = new Date().toISOString();
22539
+ let session = data.sessions.find((s) => s.sessionId === sessionId);
22540
+ if (session) {
22541
+ session.lastActiveAt = now;
22542
+ if (taskFolder)
22543
+ session.taskFolder = taskFolder;
22544
+ } else {
22545
+ session = {
22546
+ sessionId,
22547
+ taskFolder,
22548
+ startedAt: now,
22549
+ lastActiveAt: now
22550
+ };
22551
+ data.sessions.push(session);
22552
+ }
22553
+ if (!data.master) {
22554
+ data.master = sessionId;
22555
+ }
22556
+ this.saveSessions(featureName, data);
22557
+ return session;
22558
+ }
22559
+ setMaster(featureName, sessionId) {
22560
+ const data = this.getSessions(featureName);
22561
+ data.master = sessionId;
22562
+ this.saveSessions(featureName, data);
22563
+ }
22564
+ getMaster(featureName) {
22565
+ return this.getSessions(featureName).master;
22566
+ }
22567
+ list(featureName) {
22568
+ return this.getSessions(featureName).sessions;
22569
+ }
22570
+ get(featureName, sessionId) {
22571
+ return this.getSessions(featureName).sessions.find((s) => s.sessionId === sessionId);
22572
+ }
22573
+ getByTask(featureName, taskFolder) {
22574
+ return this.getSessions(featureName).sessions.find((s) => s.taskFolder === taskFolder);
22575
+ }
22576
+ remove(featureName, sessionId) {
22577
+ const data = this.getSessions(featureName);
22578
+ const index = data.sessions.findIndex((s) => s.sessionId === sessionId);
22579
+ if (index === -1)
22580
+ return false;
22581
+ data.sessions.splice(index, 1);
22582
+ if (data.master === sessionId) {
22583
+ data.master = data.sessions[0]?.sessionId;
22584
+ }
22585
+ this.saveSessions(featureName, data);
22586
+ return true;
22587
+ }
22588
+ findFeatureBySession(sessionId) {
22589
+ const globalSession = this.getGlobal(sessionId);
22590
+ if (globalSession?.featureName) {
22591
+ return globalSession.featureName;
22592
+ }
22593
+ const featuresPath = path7.join(this.projectRoot, ".hive", "features");
22594
+ if (!fs9.existsSync(featuresPath))
22595
+ return null;
22596
+ const features = fs9.readdirSync(featuresPath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
22597
+ for (const feature of features) {
22598
+ const sessions = this.getSessions(feature);
22599
+ if (sessions.sessions.some((s) => s.sessionId === sessionId)) {
22600
+ return feature;
22601
+ }
22602
+ if (sessions.master === sessionId) {
22603
+ return feature;
22604
+ }
22605
+ }
22606
+ return null;
22607
+ }
22608
+ fork(featureName, fromSessionId) {
22609
+ const data = this.getSessions(featureName);
22610
+ const now = new Date().toISOString();
22611
+ const sourceSession = fromSessionId ? data.sessions.find((s) => s.sessionId === fromSessionId) : data.sessions.find((s) => s.sessionId === data.master);
22612
+ const newSessionId = `ses_fork_${Date.now()}`;
22613
+ const newSession = {
22614
+ sessionId: newSessionId,
22615
+ taskFolder: sourceSession?.taskFolder,
22616
+ startedAt: now,
22617
+ lastActiveAt: now
22618
+ };
22619
+ data.sessions.push(newSession);
22620
+ this.saveSessions(featureName, data);
22621
+ return newSession;
22622
+ }
22623
+ fresh(featureName, title) {
22624
+ const data = this.getSessions(featureName);
22625
+ const now = new Date().toISOString();
22626
+ const newSessionId = `ses_${title ? title.replace(/\s+/g, "_").toLowerCase() : Date.now()}`;
22627
+ const newSession = {
22628
+ sessionId: newSessionId,
22629
+ startedAt: now,
22630
+ lastActiveAt: now
22631
+ };
22632
+ data.sessions.push(newSession);
22633
+ this.saveSessions(featureName, data);
22634
+ return newSession;
22635
+ }
22636
+ }
22637
+ // ../hive-core/src/services/configService.ts
22638
+ import * as fs10 from "fs";
22639
+ import * as path8 from "path";
22437
22640
  class ConfigService {
22438
22641
  configPath;
22439
22642
  cachedConfig = null;
22440
22643
  cachedCustomAgentConfigs = null;
22441
22644
  constructor() {
22442
22645
  const homeDir = process.env.HOME || process.env.USERPROFILE || "";
22443
- const configDir = path7.join(homeDir, ".config", "opencode");
22444
- this.configPath = path7.join(configDir, "agent_hive.json");
22646
+ const configDir = path8.join(homeDir, ".config", "opencode");
22647
+ this.configPath = path8.join(configDir, "agent_hive.json");
22445
22648
  }
22446
22649
  getPath() {
22447
22650
  return this.configPath;
@@ -22451,12 +22654,12 @@ class ConfigService {
22451
22654
  return this.cachedConfig;
22452
22655
  }
22453
22656
  try {
22454
- if (!fs9.existsSync(this.configPath)) {
22657
+ if (!fs10.existsSync(this.configPath)) {
22455
22658
  this.cachedConfig = { ...DEFAULT_HIVE_CONFIG };
22456
22659
  this.cachedCustomAgentConfigs = null;
22457
22660
  return this.cachedConfig;
22458
22661
  }
22459
- const raw = fs9.readFileSync(this.configPath, "utf-8");
22662
+ const raw = fs10.readFileSync(this.configPath, "utf-8");
22460
22663
  const stored = JSON.parse(raw);
22461
22664
  const storedCustomAgents = this.isObjectRecord(stored.customAgents) ? stored.customAgents : {};
22462
22665
  const mergedBuiltInAgents = BUILT_IN_AGENT_NAMES.reduce((acc, agentName) => {
@@ -22504,17 +22707,17 @@ class ConfigService {
22504
22707
  ...updates.customAgents
22505
22708
  } : current.customAgents
22506
22709
  };
22507
- const configDir = path7.dirname(this.configPath);
22508
- if (!fs9.existsSync(configDir)) {
22509
- fs9.mkdirSync(configDir, { recursive: true });
22710
+ const configDir = path8.dirname(this.configPath);
22711
+ if (!fs10.existsSync(configDir)) {
22712
+ fs10.mkdirSync(configDir, { recursive: true });
22510
22713
  }
22511
- fs9.writeFileSync(this.configPath, JSON.stringify(merged, null, 2));
22714
+ fs10.writeFileSync(this.configPath, JSON.stringify(merged, null, 2));
22512
22715
  this.cachedConfig = merged;
22513
22716
  this.cachedCustomAgentConfigs = null;
22514
22717
  return merged;
22515
22718
  }
22516
22719
  exists() {
22517
- return fs9.existsSync(this.configPath);
22720
+ return fs10.existsSync(this.configPath);
22518
22721
  }
22519
22722
  init() {
22520
22723
  if (!this.exists()) {
@@ -22650,8 +22853,8 @@ class ConfigService {
22650
22853
  }
22651
22854
  }
22652
22855
  // ../hive-core/src/services/agentsMdService.ts
22653
- import * as fs10 from "fs";
22654
- import * as path8 from "path";
22856
+ import * as fs11 from "fs";
22857
+ import * as path9 from "path";
22655
22858
  class AgentsMdService {
22656
22859
  rootDir;
22657
22860
  contextService;
@@ -22660,7 +22863,7 @@ class AgentsMdService {
22660
22863
  this.contextService = contextService;
22661
22864
  }
22662
22865
  async init() {
22663
- const agentsMdPath = path8.join(this.rootDir, "AGENTS.md");
22866
+ const agentsMdPath = path9.join(this.rootDir, "AGENTS.md");
22664
22867
  const existed = fileExists(agentsMdPath);
22665
22868
  if (existed) {
22666
22869
  const existing = readText(agentsMdPath);
@@ -22671,14 +22874,14 @@ class AgentsMdService {
22671
22874
  }
22672
22875
  async sync(featureName) {
22673
22876
  const contexts = this.contextService.list(featureName);
22674
- const agentsMdPath = path8.join(this.rootDir, "AGENTS.md");
22675
- const current = await fs10.promises.readFile(agentsMdPath, "utf-8").catch(() => "");
22877
+ const agentsMdPath = path9.join(this.rootDir, "AGENTS.md");
22878
+ const current = await fs11.promises.readFile(agentsMdPath, "utf-8").catch(() => "");
22676
22879
  const findings = this.extractFindings(contexts);
22677
22880
  const proposals = this.generateProposals(findings, current);
22678
22881
  return { proposals, diff: this.formatDiff(current, proposals) };
22679
22882
  }
22680
22883
  apply(content) {
22681
- const agentsMdPath = path8.join(this.rootDir, "AGENTS.md");
22884
+ const agentsMdPath = path9.join(this.rootDir, "AGENTS.md");
22682
22885
  const isNew = !fileExists(agentsMdPath);
22683
22886
  writeText(agentsMdPath, content);
22684
22887
  return { path: agentsMdPath, chars: content.length, isNew };
@@ -22738,7 +22941,7 @@ class AgentsMdService {
22738
22941
  return this.generateTemplate(detections);
22739
22942
  }
22740
22943
  async detectProjectInfo() {
22741
- const packageJsonPath = path8.join(this.rootDir, "package.json");
22944
+ const packageJsonPath = path9.join(this.rootDir, "package.json");
22742
22945
  let packageJson = null;
22743
22946
  if (fileExists(packageJsonPath)) {
22744
22947
  try {
@@ -22758,26 +22961,26 @@ class AgentsMdService {
22758
22961
  return info;
22759
22962
  }
22760
22963
  detectPackageManager() {
22761
- if (fileExists(path8.join(this.rootDir, "bun.lockb")))
22964
+ if (fileExists(path9.join(this.rootDir, "bun.lockb")))
22762
22965
  return "bun";
22763
- if (fileExists(path8.join(this.rootDir, "pnpm-lock.yaml")))
22966
+ if (fileExists(path9.join(this.rootDir, "pnpm-lock.yaml")))
22764
22967
  return "pnpm";
22765
- if (fileExists(path8.join(this.rootDir, "yarn.lock")))
22968
+ if (fileExists(path9.join(this.rootDir, "yarn.lock")))
22766
22969
  return "yarn";
22767
- if (fileExists(path8.join(this.rootDir, "package-lock.json")))
22970
+ if (fileExists(path9.join(this.rootDir, "package-lock.json")))
22768
22971
  return "npm";
22769
22972
  return "npm";
22770
22973
  }
22771
22974
  detectLanguage() {
22772
- if (fileExists(path8.join(this.rootDir, "tsconfig.json")))
22975
+ if (fileExists(path9.join(this.rootDir, "tsconfig.json")))
22773
22976
  return "TypeScript";
22774
- if (fileExists(path8.join(this.rootDir, "package.json")))
22977
+ if (fileExists(path9.join(this.rootDir, "package.json")))
22775
22978
  return "JavaScript";
22776
- if (fileExists(path8.join(this.rootDir, "requirements.txt")))
22979
+ if (fileExists(path9.join(this.rootDir, "requirements.txt")))
22777
22980
  return "Python";
22778
- if (fileExists(path8.join(this.rootDir, "go.mod")))
22981
+ if (fileExists(path9.join(this.rootDir, "go.mod")))
22779
22982
  return "Go";
22780
- if (fileExists(path8.join(this.rootDir, "Cargo.toml")))
22983
+ if (fileExists(path9.join(this.rootDir, "Cargo.toml")))
22781
22984
  return "Rust";
22782
22985
  return "Unknown";
22783
22986
  }
@@ -22856,25 +23059,25 @@ class AgentsMdService {
22856
23059
  }
22857
23060
  }
22858
23061
  // ../hive-core/src/services/dockerSandboxService.ts
22859
- import { existsSync as existsSync4 } from "fs";
22860
- import { join as join9, sep } from "path";
23062
+ import { existsSync as existsSync5 } from "fs";
23063
+ import { join as join10, sep } from "path";
22861
23064
  import { execSync } from "child_process";
22862
23065
 
22863
23066
  class DockerSandboxService {
22864
23067
  static detectImage(worktreePath) {
22865
- if (existsSync4(join9(worktreePath, "Dockerfile"))) {
23068
+ if (existsSync5(join10(worktreePath, "Dockerfile"))) {
22866
23069
  return null;
22867
23070
  }
22868
- if (existsSync4(join9(worktreePath, "package.json"))) {
23071
+ if (existsSync5(join10(worktreePath, "package.json"))) {
22869
23072
  return "node:22-slim";
22870
23073
  }
22871
- if (existsSync4(join9(worktreePath, "requirements.txt")) || existsSync4(join9(worktreePath, "pyproject.toml"))) {
23074
+ if (existsSync5(join10(worktreePath, "requirements.txt")) || existsSync5(join10(worktreePath, "pyproject.toml"))) {
22872
23075
  return "python:3.12-slim";
22873
23076
  }
22874
- if (existsSync4(join9(worktreePath, "go.mod"))) {
23077
+ if (existsSync5(join10(worktreePath, "go.mod"))) {
22875
23078
  return "golang:1.22-slim";
22876
23079
  }
22877
- if (existsSync4(join9(worktreePath, "Cargo.toml"))) {
23080
+ if (existsSync5(join10(worktreePath, "Cargo.toml"))) {
22878
23081
  return "rust:1.77-slim";
22879
23082
  }
22880
23083
  return "ubuntu:24.04";
@@ -23444,17 +23647,17 @@ function applyContextBudget(files, config2 = {}) {
23444
23647
  }
23445
23648
 
23446
23649
  // src/utils/prompt-file.ts
23447
- import * as fs11 from "fs";
23448
- import * as path9 from "path";
23650
+ import * as fs12 from "fs";
23651
+ import * as path10 from "path";
23449
23652
  function writeWorkerPromptFile(feature, task, prompt, hiveDir) {
23450
- const projectRoot = path9.dirname(hiveDir);
23653
+ const projectRoot = path10.dirname(hiveDir);
23451
23654
  const featureDir = resolveFeatureDirectoryName(projectRoot, feature);
23452
- const promptDir = path9.join(hiveDir, "features", featureDir, "tasks", task);
23453
- const promptPath = path9.join(promptDir, "worker-prompt.md");
23454
- if (!fs11.existsSync(promptDir)) {
23455
- fs11.mkdirSync(promptDir, { recursive: true });
23655
+ const promptDir = path10.join(hiveDir, "features", featureDir, "tasks", task);
23656
+ const promptPath = path10.join(promptDir, "worker-prompt.md");
23657
+ if (!fs12.existsSync(promptDir)) {
23658
+ fs12.mkdirSync(promptDir, { recursive: true });
23456
23659
  }
23457
- fs11.writeFileSync(promptPath, prompt, "utf-8");
23660
+ fs12.writeFileSync(promptPath, prompt, "utf-8");
23458
23661
  return promptPath;
23459
23662
  }
23460
23663
 
@@ -23465,9 +23668,48 @@ function normalizeVariant(variant) {
23465
23668
  const trimmed2 = variant.trim();
23466
23669
  return trimmed2.length > 0 ? trimmed2 : undefined;
23467
23670
  }
23468
- function createVariantHook(configService) {
23671
+ var BUILT_IN_AGENTS = {
23672
+ "hive-master": { sessionKind: "primary", baseAgent: "hive-master" },
23673
+ "architect-planner": { sessionKind: "primary", baseAgent: "architect-planner" },
23674
+ "swarm-orchestrator": { sessionKind: "primary", baseAgent: "swarm-orchestrator" },
23675
+ "forager-worker": { sessionKind: "task-worker", baseAgent: "forager-worker" },
23676
+ "scout-researcher": { sessionKind: "subagent", baseAgent: "scout-researcher" },
23677
+ "hygienic-reviewer": { sessionKind: "subagent", baseAgent: "hygienic-reviewer" }
23678
+ };
23679
+ var BASE_AGENT_KIND = {
23680
+ "forager-worker": "task-worker",
23681
+ "hygienic-reviewer": "subagent"
23682
+ };
23683
+ function classifySession(agent, customAgents = {}) {
23684
+ const builtIn = BUILT_IN_AGENTS[agent];
23685
+ if (builtIn) {
23686
+ return { sessionKind: builtIn.sessionKind, baseAgent: builtIn.baseAgent };
23687
+ }
23688
+ const custom2 = customAgents[agent];
23689
+ if (custom2) {
23690
+ const kind = BASE_AGENT_KIND[custom2.baseAgent];
23691
+ if (kind) {
23692
+ return { sessionKind: kind, baseAgent: custom2.baseAgent };
23693
+ }
23694
+ }
23695
+ return { sessionKind: "unknown", baseAgent: undefined };
23696
+ }
23697
+ function createVariantHook(configService, sessionService, customAgents, taskWorkerRecovery) {
23469
23698
  return async (input, output) => {
23470
23699
  const { agent } = input;
23700
+ if (agent && sessionService) {
23701
+ const { sessionKind, baseAgent } = classifySession(agent, customAgents);
23702
+ const patch = { agent, sessionKind };
23703
+ if (baseAgent) {
23704
+ patch.baseAgent = baseAgent;
23705
+ }
23706
+ if (sessionKind === "task-worker" && taskWorkerRecovery) {
23707
+ patch.featureName = taskWorkerRecovery.featureName;
23708
+ patch.taskFolder = taskWorkerRecovery.taskFolder;
23709
+ patch.workerPromptPath = taskWorkerRecovery.workerPromptPath;
23710
+ }
23711
+ sessionService.trackGlobal(input.sessionID, patch);
23712
+ }
23471
23713
  if (!agent)
23472
23714
  return;
23473
23715
  if (!configService.hasConfiguredAgent(agent))
@@ -23501,10 +23743,69 @@ var HIVE_SYSTEM_PROMPT = `
23501
23743
  Use hive_merge to integrate changes into the current branch.
23502
23744
  `;
23503
23745
 
23504
- // src/utils/compaction-prompt.ts
23505
- var COMPACTION_RESUME_PROMPT = "You were compacted mid-task. " + "Resume by reading your worker-prompt.md (in the task worktree root) to recall your assignment. " + "Do not call status tools or re-read the full codebase. " + "Locate your last commit message or notes, then continue from where you left off.";
23506
- function buildCompactionPrompt() {
23507
- return COMPACTION_RESUME_PROMPT;
23746
+ // src/utils/compaction-anchor.ts
23747
+ var AGENT_ROLE_MAP = {
23748
+ "hive-master": "Hive",
23749
+ "architect-planner": "Architect",
23750
+ "swarm-orchestrator": "Swarm",
23751
+ "forager-worker": "Forager",
23752
+ "scout-researcher": "Scout",
23753
+ "hygienic-reviewer": "Hygienic"
23754
+ };
23755
+ var BASE_AGENT_ROLE_MAP = {
23756
+ "forager-worker": "Forager",
23757
+ "hygienic-reviewer": "Hygienic",
23758
+ "scout-researcher": "Scout"
23759
+ };
23760
+ function resolveRole(ctx) {
23761
+ if (ctx.agent && AGENT_ROLE_MAP[ctx.agent]) {
23762
+ return AGENT_ROLE_MAP[ctx.agent];
23763
+ }
23764
+ if (ctx.baseAgent && BASE_AGENT_ROLE_MAP[ctx.baseAgent]) {
23765
+ return BASE_AGENT_ROLE_MAP[ctx.baseAgent];
23766
+ }
23767
+ return;
23768
+ }
23769
+ function resolveWorkerPromptPath(ctx) {
23770
+ if (ctx.workerPromptPath) {
23771
+ return ctx.workerPromptPath;
23772
+ }
23773
+ if (ctx.featureName && ctx.taskFolder) {
23774
+ return `.hive/features/${ctx.featureName}/tasks/${ctx.taskFolder}/worker-prompt.md`;
23775
+ }
23776
+ return;
23777
+ }
23778
+ function buildCompactionReanchor(ctx) {
23779
+ const role = resolveRole(ctx);
23780
+ const kind = ctx.sessionKind ?? "unknown";
23781
+ const workerPromptPath = resolveWorkerPromptPath(ctx);
23782
+ const lines = [];
23783
+ const context = [];
23784
+ lines.push("Compaction recovery — you were compacted mid-session.");
23785
+ if (role) {
23786
+ lines.push(`Role: ${role}`);
23787
+ }
23788
+ lines.push("Do not switch roles.");
23789
+ lines.push("Do not call status tools to rediscover state.");
23790
+ lines.push("Do not re-read the full codebase.");
23791
+ if (kind === "task-worker") {
23792
+ lines.push("Do not delegate.");
23793
+ if (workerPromptPath) {
23794
+ lines.push("Re-read worker-prompt.md now to recall your assignment.");
23795
+ context.push(workerPromptPath);
23796
+ } else {
23797
+ lines.push("Re-read worker-prompt.md from the Hive task metadata to recall your assignment.");
23798
+ }
23799
+ }
23800
+ if ((kind === "primary" || kind === "subagent") && ctx.directivePrompt) {
23801
+ lines.push("Original directive survives via post-compaction replay.");
23802
+ }
23803
+ lines.push("Next action: resume from where you left off.");
23804
+ return {
23805
+ prompt: lines.join(`
23806
+ `),
23807
+ context
23808
+ };
23508
23809
  }
23509
23810
 
23510
23811
  // src/index.ts
@@ -23619,13 +23920,14 @@ No Hive skills available.` : base + formatSkillsXml(filteredSkills);
23619
23920
  });
23620
23921
  }
23621
23922
  var plugin = async (ctx) => {
23622
- const { directory, client } = ctx;
23923
+ const { directory, client, worktree } = ctx;
23623
23924
  const featureService = new FeatureService(directory);
23624
23925
  const planService = new PlanService(directory);
23625
23926
  const taskService = new TaskService(directory);
23626
23927
  const contextService = new ContextService(directory);
23627
23928
  const agentsMdService = new AgentsMdService(directory, contextService);
23628
23929
  const configService = new ConfigService;
23930
+ const sessionService = new SessionService(directory);
23629
23931
  const disabledMcps = configService.getDisabledMcps();
23630
23932
  const disabledSkills = configService.getDisabledSkills();
23631
23933
  const builtinMcps = createBuiltinMcps(disabledMcps);
@@ -23633,8 +23935,15 @@ var plugin = async (ctx) => {
23633
23935
  const effectiveAutoLoadSkills = configService.getAgentConfig("hive-master").autoLoadSkills ?? [];
23634
23936
  const worktreeService = new WorktreeService({
23635
23937
  baseDir: directory,
23636
- hiveDir: path10.join(directory, ".hive")
23938
+ hiveDir: path11.join(directory, ".hive")
23637
23939
  });
23940
+ const customAgentConfigsForClassification = getCustomAgentConfigsCompat(configService);
23941
+ const runtimeContext = detectContext(worktree || directory);
23942
+ const taskWorkerRecovery = runtimeContext.isWorktree && runtimeContext.feature && runtimeContext.task ? {
23943
+ featureName: runtimeContext.feature,
23944
+ taskFolder: runtimeContext.task,
23945
+ workerPromptPath: path11.posix.join(".hive", "features", resolveFeatureDirectoryName(directory, runtimeContext.feature), "tasks", runtimeContext.task, "worker-prompt.md")
23946
+ } : undefined;
23638
23947
  const isOmoSlimEnabled = () => {
23639
23948
  return configService.isOmoSlimEnabled();
23640
23949
  };
@@ -23655,12 +23964,50 @@ var plugin = async (ctx) => {
23655
23964
  }
23656
23965
  }
23657
23966
  };
23967
+ const bindFeatureSession = (feature, toolContext, patch) => {
23968
+ const ctx2 = toolContext;
23969
+ if (!ctx2?.sessionID)
23970
+ return;
23971
+ sessionService.bindFeature(ctx2.sessionID, feature, patch);
23972
+ };
23973
+ const extractTextParts = (parts) => {
23974
+ if (!Array.isArray(parts))
23975
+ return [];
23976
+ return parts.filter((part) => {
23977
+ return !!part && typeof part === "object" && part.type === "text" && typeof part.text === "string";
23978
+ }).map((part) => part.text.trim()).filter(Boolean);
23979
+ };
23980
+ const shouldCaptureDirective = (info, parts) => {
23981
+ if (info.role !== "user")
23982
+ return false;
23983
+ const textParts = parts.filter((part) => {
23984
+ return !!part && typeof part === "object" && part.type === "text";
23985
+ });
23986
+ if (textParts.length === 0)
23987
+ return false;
23988
+ return !textParts.every((part) => part.synthetic === true);
23989
+ };
23990
+ const buildDirectiveReplayText = (session) => {
23991
+ if (!session.directivePrompt)
23992
+ return null;
23993
+ const role = session.agent === "scout-researcher" || session.baseAgent === "scout-researcher" ? "Scout" : session.agent === "hygienic-reviewer" || session.baseAgent === "hygienic-reviewer" ? "Hygienic" : session.agent === "architect-planner" || session.baseAgent === "architect-planner" ? "Architect" : session.agent === "swarm-orchestrator" || session.baseAgent === "swarm-orchestrator" ? "Swarm" : session.agent === "hive-master" || session.baseAgent === "hive-master" ? "Hive" : "current role";
23994
+ return [
23995
+ `Post-compaction recovery: You are still ${role}.`,
23996
+ "Resume the original assignment below. Do not replace it with a new goal.",
23997
+ "",
23998
+ session.directivePrompt
23999
+ ].join(`
24000
+ `);
24001
+ };
24002
+ const shouldUseDirectiveReplay = (session) => {
24003
+ return session?.sessionKind === "primary" || session?.sessionKind === "subagent";
24004
+ };
23658
24005
  const checkBlocked = (feature) => {
23659
- const fs13 = __require("fs");
24006
+ const fs14 = __require("fs");
23660
24007
  const featureDir = resolveFeatureDirectoryName(directory, feature);
23661
- const blockedPath = path10.join(directory, ".hive", "features", featureDir, "BLOCKED");
23662
- if (fs13.existsSync(blockedPath)) {
23663
- const reason = fs13.readFileSync(blockedPath, "utf-8").trim();
24008
+ const blockedPath = path11.join(directory, ".hive", "features", featureDir, "BLOCKED");
24009
+ if (fs14.existsSync(blockedPath)) {
24010
+ const reason = fs14.readFileSync(blockedPath, "utf-8").trim();
23664
24011
  return `⛔ BLOCKED by Beekeeper
23665
24012
 
23666
24013
  ${reason || "(No reason provided)"}
@@ -23713,13 +24060,13 @@ To unblock: Remove .hive/features/${featureDir}/BLOCKED`;
23713
24060
  feature,
23714
24061
  task,
23715
24062
  taskInfo,
23716
- worktree,
24063
+ worktree: worktree2,
23717
24064
  continueFrom,
23718
24065
  decision
23719
24066
  }) => {
23720
24067
  taskService.update(feature, task, {
23721
24068
  status: "in_progress",
23722
- baseCommit: worktree.commit
24069
+ baseCommit: worktree2.commit
23723
24070
  });
23724
24071
  const planResult = planService.read(feature);
23725
24072
  const allTasks = taskService.list(feature);
@@ -23770,8 +24117,8 @@ To unblock: Remove .hive/features/${featureDir}/BLOCKED`;
23770
24117
  feature,
23771
24118
  task,
23772
24119
  taskOrder,
23773
- worktreePath: worktree.path,
23774
- branch: worktree.branch,
24120
+ worktreePath: worktree2.path,
24121
+ branch: worktree2.branch,
23775
24122
  plan: planResult?.content || "No plan available",
23776
24123
  contextFiles,
23777
24124
  spec: specContent,
@@ -23813,9 +24160,9 @@ To unblock: Remove .hive/features/${featureDir}/BLOCKED`;
23813
24160
  spec: specContent,
23814
24161
  workerPrompt
23815
24162
  });
23816
- const hiveDir = path10.join(directory, ".hive");
24163
+ const hiveDir = path11.join(directory, ".hive");
23817
24164
  const workerPromptPath = writeWorkerPromptFile(feature, task, workerPrompt, hiveDir);
23818
- const relativePromptPath = normalizePath(path10.relative(directory, workerPromptPath));
24165
+ const relativePromptPath = normalizePath(path11.relative(directory, workerPromptPath));
23819
24166
  const PREVIEW_MAX_LENGTH = 200;
23820
24167
  const workerPromptPreview = workerPrompt.length > PREVIEW_MAX_LENGTH ? workerPrompt.slice(0, PREVIEW_MAX_LENGTH) + "..." : workerPrompt;
23821
24168
  const taskToolPrompt = `Follow instructions in @${relativePromptPath}`;
@@ -23844,8 +24191,8 @@ Use the \`@path\` attachment syntax in the prompt to reference the file. Do not
23844
24191
  const responseBase = {
23845
24192
  success: true,
23846
24193
  terminal: false,
23847
- worktreePath: worktree.path,
23848
- branch: worktree.branch,
24194
+ worktreePath: worktree2.path,
24195
+ branch: worktree2.branch,
23849
24196
  mode: "delegate",
23850
24197
  agent,
23851
24198
  defaultAgent,
@@ -23981,8 +24328,8 @@ Use the \`@path\` attachment syntax in the prompt to reference the file. Do not
23981
24328
  ]
23982
24329
  });
23983
24330
  }
23984
- const worktree = await worktreeService.create(feature, task);
23985
- return buildWorktreeLaunchResponse({ feature, task, taskInfo, worktree });
24331
+ const worktree2 = await worktreeService.create(feature, task);
24332
+ return buildWorktreeLaunchResponse({ feature, task, taskInfo, worktree: worktree2 });
23986
24333
  };
23987
24334
  const executeBlockedResume = async ({
23988
24335
  task,
@@ -24073,8 +24420,8 @@ Use the \`@path\` attachment syntax in the prompt to reference the file. Do not
24073
24420
  ]
24074
24421
  });
24075
24422
  }
24076
- const worktree = await worktreeService.get(feature, task);
24077
- if (!worktree) {
24423
+ const worktree2 = await worktreeService.get(feature, task);
24424
+ if (!worktree2) {
24078
24425
  return respond({
24079
24426
  success: false,
24080
24427
  terminal: true,
@@ -24090,12 +24437,23 @@ Use the \`@path\` attachment syntax in the prompt to reference the file. Do not
24090
24437
  feature,
24091
24438
  task,
24092
24439
  taskInfo,
24093
- worktree,
24440
+ worktree: worktree2,
24094
24441
  continueFrom,
24095
24442
  decision
24096
24443
  });
24097
24444
  };
24098
24445
  return {
24446
+ event: async (input) => {
24447
+ if (input.event.type !== "session.compacted") {
24448
+ return;
24449
+ }
24450
+ const sessionID = input.event.properties.sessionID;
24451
+ const existing = sessionService.getGlobal(sessionID);
24452
+ if (!existing?.directivePrompt || !shouldUseDirectiveReplay(existing)) {
24453
+ return;
24454
+ }
24455
+ sessionService.trackGlobal(sessionID, { replayDirectivePending: true });
24456
+ },
24099
24457
  "experimental.chat.system.transform": async (input, output) => {
24100
24458
  if (!shouldExecuteHook("experimental.chat.system.transform", configService, turnCounters)) {
24101
24459
  return;
@@ -24129,9 +24487,81 @@ Use the \`@path\` attachment syntax in the prompt to reference the file. Do not
24129
24487
  }
24130
24488
  },
24131
24489
  "experimental.session.compacting": async (_input, output) => {
24132
- output.context.push(buildCompactionPrompt());
24490
+ const session = sessionService.getGlobal(_input.sessionID);
24491
+ if (session) {
24492
+ const ctx2 = {
24493
+ agent: session.agent,
24494
+ baseAgent: session.baseAgent,
24495
+ sessionKind: session.sessionKind,
24496
+ featureName: session.featureName,
24497
+ taskFolder: session.taskFolder,
24498
+ workerPromptPath: session.workerPromptPath,
24499
+ directivePrompt: session.directivePrompt
24500
+ };
24501
+ const reanchor = buildCompactionReanchor(ctx2);
24502
+ output.prompt = reanchor.prompt;
24503
+ output.context.push(...reanchor.context);
24504
+ } else {
24505
+ const reanchor = buildCompactionReanchor({});
24506
+ output.prompt = reanchor.prompt;
24507
+ output.context.push(...reanchor.context);
24508
+ }
24509
+ },
24510
+ "chat.message": createVariantHook(configService, sessionService, customAgentConfigsForClassification, taskWorkerRecovery),
24511
+ "experimental.chat.messages.transform": async (_input, output) => {
24512
+ if (!Array.isArray(output.messages) || output.messages.length === 0) {
24513
+ return;
24514
+ }
24515
+ const firstMessage = output.messages[0];
24516
+ const sessionID = firstMessage?.info?.sessionID;
24517
+ if (!sessionID) {
24518
+ return;
24519
+ }
24520
+ const session = sessionService.getGlobal(sessionID);
24521
+ const captureCandidates = output.messages.filter(({ info, parts }) => info.sessionID === sessionID && shouldCaptureDirective(info, parts));
24522
+ const latestDirective = captureCandidates.at(-1);
24523
+ if (latestDirective) {
24524
+ const directiveText = extractTextParts(latestDirective.parts).join(`
24525
+
24526
+ `);
24527
+ const existingDirective = session?.directivePrompt;
24528
+ if (directiveText && directiveText !== existingDirective && shouldUseDirectiveReplay(session ?? { sessionKind: "subagent" })) {
24529
+ sessionService.trackGlobal(sessionID, { directivePrompt: directiveText });
24530
+ }
24531
+ }
24532
+ const refreshed = sessionService.getGlobal(sessionID);
24533
+ if (!refreshed?.replayDirectivePending || !shouldUseDirectiveReplay(refreshed)) {
24534
+ if (refreshed?.replayDirectivePending && !shouldUseDirectiveReplay(refreshed)) {
24535
+ sessionService.trackGlobal(sessionID, { replayDirectivePending: false });
24536
+ }
24537
+ return;
24538
+ }
24539
+ const replayText = buildDirectiveReplayText(refreshed);
24540
+ if (!replayText) {
24541
+ sessionService.trackGlobal(sessionID, { replayDirectivePending: false });
24542
+ return;
24543
+ }
24544
+ const now = Date.now();
24545
+ output.messages.push({
24546
+ info: {
24547
+ id: `msg_replay_${sessionID}`,
24548
+ sessionID,
24549
+ role: "user",
24550
+ time: { created: now }
24551
+ },
24552
+ parts: [
24553
+ {
24554
+ id: `prt_replay_${sessionID}`,
24555
+ sessionID,
24556
+ messageID: `msg_replay_${sessionID}`,
24557
+ type: "text",
24558
+ text: replayText,
24559
+ synthetic: true
24560
+ }
24561
+ ]
24562
+ });
24563
+ sessionService.trackGlobal(sessionID, { replayDirectivePending: false });
24133
24564
  },
24134
- "chat.message": createVariantHook(configService),
24135
24565
  "tool.execute.before": async (input, output) => {
24136
24566
  if (!shouldExecuteHook("tool.execute.before", configService, turnCounters, { safetyCritical: true })) {
24137
24567
  return;
@@ -24153,7 +24583,7 @@ Use the \`@path\` attachment syntax in the prompt to reference the file. Do not
24153
24583
  const workdir = output.args?.workdir;
24154
24584
  if (!workdir)
24155
24585
  return;
24156
- const hiveWorktreeBase = path10.join(directory, ".hive", ".worktrees");
24586
+ const hiveWorktreeBase = path11.join(directory, ".hive", ".worktrees");
24157
24587
  if (!workdir.startsWith(hiveWorktreeBase))
24158
24588
  return;
24159
24589
  const wrapped = DockerSandboxService.wrapCommand(workdir, command, sandboxConfig);
@@ -24267,6 +24697,7 @@ Expand your Discovery section and try again.`;
24267
24697
  if (!feature)
24268
24698
  return "Error: No feature specified. Create a feature or provide feature param.";
24269
24699
  captureSession(feature, toolContext);
24700
+ bindFeatureSession(feature, toolContext);
24270
24701
  const result = planService.read(feature);
24271
24702
  if (!result)
24272
24703
  return "Error: No plan.md found";
@@ -24390,7 +24821,7 @@ Reminder: start work with hive_worktree_start to use its worktree, and ensure an
24390
24821
  }).optional().describe("Blocker info when status is blocked"),
24391
24822
  feature: tool.schema.string().optional().describe("Feature name (defaults to detection or single feature)")
24392
24823
  },
24393
- async execute({ task, summary, message, status = "completed", blocker, feature: explicitFeature }) {
24824
+ async execute({ task, summary, message, status = "completed", blocker, feature: explicitFeature }, toolContext) {
24394
24825
  const respond2 = (payload) => JSON.stringify(payload, null, 2);
24395
24826
  const feature = resolveFeature(explicitFeature);
24396
24827
  if (!feature) {
@@ -24432,6 +24863,9 @@ Reminder: start work with hive_worktree_start to use its worktree, and ensure an
24432
24863
  nextAction: "Only in_progress or blocked tasks can be committed. Start/resume the task first."
24433
24864
  });
24434
24865
  }
24866
+ const featureDir = resolveFeatureDirectoryName(directory, feature);
24867
+ const workerPromptPath = path11.posix.join(".hive", "features", featureDir, "tasks", task, "worker-prompt.md");
24868
+ bindFeatureSession(feature, toolContext, { taskFolder: task, workerPromptPath });
24435
24869
  let verificationNote;
24436
24870
  if (status === "completed") {
24437
24871
  const verificationKeywords = ["test", "build", "lint", "vitest", "jest", "npm run", "pnpm", "cargo", "pytest", "verified", "passes", "succeeds", "ast-grep", "scan"];
@@ -24447,7 +24881,7 @@ Reminder: start work with hive_worktree_start to use its worktree, and ensure an
24447
24881
  summary,
24448
24882
  blocker
24449
24883
  });
24450
- const worktree2 = await worktreeService.get(feature, task);
24884
+ const worktree3 = await worktreeService.get(feature, task);
24451
24885
  return respond2({
24452
24886
  ok: true,
24453
24887
  terminal: true,
@@ -24458,8 +24892,8 @@ Reminder: start work with hive_worktree_start to use its worktree, and ensure an
24458
24892
  taskState: "blocked",
24459
24893
  summary,
24460
24894
  blocker,
24461
- worktreePath: worktree2?.path,
24462
- branch: worktree2?.branch,
24895
+ worktreePath: worktree3?.path,
24896
+ branch: worktree3?.branch,
24463
24897
  message: 'Task blocked. Hive Master will ask user and resume with hive_worktree_create(continueFrom: "blocked", decision: answer)',
24464
24898
  nextAction: 'Wait for orchestrator to collect user decision and resume with continueFrom: "blocked".'
24465
24899
  });
@@ -24518,7 +24952,7 @@ Reminder: start work with hive_worktree_start to use its worktree, and ensure an
24518
24952
  `));
24519
24953
  const finalStatus = status === "completed" ? "done" : status;
24520
24954
  taskService.update(feature, task, { status: finalStatus, summary });
24521
- const worktree = await worktreeService.get(feature, task);
24955
+ const worktree2 = await worktreeService.get(feature, task);
24522
24956
  return respond2({
24523
24957
  ok: true,
24524
24958
  terminal: true,
@@ -24533,8 +24967,8 @@ Reminder: start work with hive_worktree_start to use its worktree, and ensure an
24533
24967
  sha: commitResult.sha,
24534
24968
  message: commitResult.message
24535
24969
  },
24536
- worktreePath: worktree?.path,
24537
- branch: worktree?.branch,
24970
+ worktreePath: worktree2?.path,
24971
+ branch: worktree2?.branch,
24538
24972
  reportPath,
24539
24973
  message: `Task "${task}" ${status}.`,
24540
24974
  nextAction: "Use hive_merge to integrate changes. Worktree is preserved for review."
@@ -24596,10 +25030,11 @@ Files changed: ${result.filesChanged?.length || 0}`;
24596
25030
  content: tool.schema.string().describe("Markdown content to write"),
24597
25031
  feature: tool.schema.string().optional().describe("Feature name (defaults to active)")
24598
25032
  },
24599
- async execute({ name, content, feature: explicitFeature }) {
25033
+ async execute({ name, content, feature: explicitFeature }, toolContext) {
24600
25034
  const feature = resolveFeature(explicitFeature);
24601
25035
  if (!feature)
24602
25036
  return "Error: No feature specified. Create a feature or provide feature param.";
25037
+ bindFeatureSession(feature, toolContext);
24603
25038
  const filePath = contextService.write(feature, name, content);
24604
25039
  return `Context file written: ${filePath}`;
24605
25040
  }
@@ -24649,36 +25084,36 @@ Files changed: ${result.filesChanged?.length || 0}`;
24649
25084
  const contextFiles = contextService.list(feature);
24650
25085
  const overview = contextFiles.find((file2) => file2.name === "overview") ?? null;
24651
25086
  const readThreads = (filePath) => {
24652
- if (!fs12.existsSync(filePath)) {
25087
+ if (!fs13.existsSync(filePath)) {
24653
25088
  return null;
24654
25089
  }
24655
25090
  try {
24656
- const data = JSON.parse(fs12.readFileSync(filePath, "utf-8"));
25091
+ const data = JSON.parse(fs13.readFileSync(filePath, "utf-8"));
24657
25092
  return data.threads ?? [];
24658
25093
  } catch {
24659
25094
  return [];
24660
25095
  }
24661
25096
  };
24662
- const featurePath = path10.join(directory, ".hive", "features", resolveFeatureDirectoryName(directory, feature));
24663
- const reviewDir = path10.join(featurePath, "comments");
24664
- const planThreads = readThreads(path10.join(reviewDir, "plan.json")) ?? readThreads(path10.join(featurePath, "comments.json"));
24665
- const overviewThreads = readThreads(path10.join(reviewDir, "overview.json"));
25097
+ const featurePath = path11.join(directory, ".hive", "features", resolveFeatureDirectoryName(directory, feature));
25098
+ const reviewDir = path11.join(featurePath, "comments");
25099
+ const planThreads = readThreads(path11.join(reviewDir, "plan.json")) ?? readThreads(path11.join(featurePath, "comments.json"));
25100
+ const overviewThreads = readThreads(path11.join(reviewDir, "overview.json"));
24666
25101
  const reviewCounts = {
24667
25102
  plan: planThreads?.length ?? 0,
24668
25103
  overview: overviewThreads?.length ?? 0
24669
25104
  };
24670
25105
  const tasksSummary = await Promise.all(tasks.map(async (t) => {
24671
25106
  const rawStatus = taskService.getRawStatus(feature, t.folder);
24672
- const worktree = await worktreeService.get(feature, t.folder);
24673
- const hasChanges = worktree ? await worktreeService.hasUncommittedChanges(worktree.feature, worktree.step) : null;
25107
+ const worktree2 = await worktreeService.get(feature, t.folder);
25108
+ const hasChanges = worktree2 ? await worktreeService.hasUncommittedChanges(worktree2.feature, worktree2.step) : null;
24674
25109
  return {
24675
25110
  folder: t.folder,
24676
25111
  name: t.name,
24677
25112
  status: t.status,
24678
25113
  origin: t.origin || "plan",
24679
25114
  dependsOn: rawStatus?.dependsOn ?? null,
24680
- worktree: worktree ? {
24681
- branch: worktree.branch,
25115
+ worktree: worktree2 ? {
25116
+ branch: worktree2.branch,
24682
25117
  hasChanges
24683
25118
  } : null
24684
25119
  };
@@ -1,6 +1,18 @@
1
- import type { ConfigService } from 'hive-core';
1
+ import type { ConfigService, SessionService, SessionKind } from 'hive-core';
2
2
  export declare function normalizeVariant(variant: string | undefined): string | undefined;
3
- export declare function createVariantHook(configService: ConfigService): (input: {
3
+ export declare function classifySession(agent: string, customAgents?: Record<string, {
4
+ baseAgent: string;
5
+ }>): {
6
+ sessionKind: SessionKind;
7
+ baseAgent?: string;
8
+ };
9
+ export declare function createVariantHook(configService: ConfigService, sessionService?: SessionService, customAgents?: Record<string, {
10
+ baseAgent: string;
11
+ }>, taskWorkerRecovery?: {
12
+ featureName: string;
13
+ taskFolder: string;
14
+ workerPromptPath: string;
15
+ }): (input: {
4
16
  sessionID: string;
5
17
  agent?: string;
6
18
  model?: {
@@ -0,0 +1,14 @@
1
+ export interface CompactionSessionContext {
2
+ agent?: string;
3
+ baseAgent?: string;
4
+ sessionKind?: 'primary' | 'subagent' | 'task-worker' | 'unknown';
5
+ featureName?: string;
6
+ taskFolder?: string;
7
+ workerPromptPath?: string;
8
+ directivePrompt?: string;
9
+ }
10
+ export interface CompactionReanchor {
11
+ prompt: string;
12
+ context: string[];
13
+ }
14
+ export declare function buildCompactionReanchor(ctx: CompactionSessionContext): CompactionReanchor;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-hive",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Agent Hive - from vibe coding to hive coding",
6
6
  "license": "MIT WITH Commons-Clause",
@@ -39,17 +39,17 @@
39
39
  "simple-git": "^3.27.0"
40
40
  },
41
41
  "optionalDependencies": {
42
- "grep-mcp": "^1.1.0",
43
42
  "@notprolands/ast-grep-mcp": "^1.1.1",
44
43
  "@upstash/context7-mcp": "^2.1.0",
45
- "exa-mcp-server": "^3.1.5"
44
+ "exa-mcp-server": "^3.1.5",
45
+ "grep-mcp": "^1.1.0"
46
46
  },
47
47
  "devDependencies": {
48
- "hive-core": "workspace:*",
49
48
  "@opencode-ai/plugin": "^1.0.143",
50
49
  "@opencode-ai/sdk": "^0.13.0",
51
50
  "@types/bun": "^1.2.0",
52
51
  "@types/node": "^20.0.0",
52
+ "hive-core": "workspace:*",
53
53
  "typescript": "^5.0.0"
54
54
  },
55
55
  "files": [