@shanesaravia/hive 0.2.1 → 0.3.0

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +2 -0
  3. package/node_modules/@hive/shared/dist/directStudio.d.ts +6 -0
  4. package/node_modules/@hive/shared/dist/directStudio.js +12 -0
  5. package/node_modules/@hive/shared/dist/index.d.ts +2 -0
  6. package/node_modules/@hive/shared/dist/index.js +2 -0
  7. package/node_modules/@hive/shared/dist/reviewHall.d.ts +15 -0
  8. package/node_modules/@hive/shared/dist/reviewHall.js +49 -0
  9. package/node_modules/@hive/shared/dist/types.d.ts +29 -0
  10. package/node_modules/@hive/shared/dist/workers.d.ts +14 -0
  11. package/node_modules/@hive/shared/dist/workers.js +22 -0
  12. package/package.json +1 -1
  13. package/packages/server/dist/api/rest.js +65 -10
  14. package/packages/server/dist/api/ws.js +25 -8
  15. package/packages/server/dist/control/launcher.js +50 -11
  16. package/packages/server/dist/control/messaging.js +3 -2
  17. package/packages/server/dist/health/deriveAlerts.js +1 -2
  18. package/packages/server/dist/hooks/hookIngest.js +69 -10
  19. package/packages/server/dist/index.js +20 -4
  20. package/packages/server/dist/messages/messagesStore.js +25 -12
  21. package/packages/server/dist/missions/missionsStore.js +9 -0
  22. package/packages/server/dist/missions/reopenOnWork.js +20 -0
  23. package/packages/server/dist/plans/planReconcile.js +114 -0
  24. package/packages/server/dist/plans/plansStore.js +46 -3
  25. package/packages/server/dist/roster/missionReplay.js +82 -0
  26. package/packages/server/dist/roster/rosterBuilder.js +37 -142
  27. package/packages/server/dist/roster/workerIdentity.js +886 -0
  28. package/packages/server/dist/watch/jobsWatcher.js +55 -24
  29. package/packages/web/dist/assets/index-BpEYVjCF.css +2 -0
  30. package/packages/web/dist/assets/index-rIAIJyuF.js +12 -0
  31. package/packages/web/dist/index.html +2 -2
  32. package/templates/agents/hive-orchestrator.md +1 -0
  33. package/packages/web/dist/assets/index-Bzle5Xla.css +0 -2
  34. package/packages/web/dist/assets/index-C6AY0vYC.js +0 -11
@@ -1,7 +1,9 @@
1
1
  import { deriveStatus, } from "@hive/shared";
2
2
  import { allPlanTasks } from "../plans/plansStore.js";
3
+ import { deriveChangedFiles, reconcilePlanWorkers } from "../plans/planReconcile.js";
3
4
  import { config } from "../config.js";
4
5
  import { deriveAlerts } from "../health/deriveAlerts.js";
6
+ import { resolveMissionWorkers } from "./workerIdentity.js";
5
7
  const ORCHESTRATOR_AGENT_NAME = "hive-orchestrator";
6
8
  function dateMs(value) {
7
9
  const parsed = value ? Date.parse(value) : NaN;
@@ -18,7 +20,7 @@ function resultText(job) {
18
20
  return undefined;
19
21
  }
20
22
  /** Joins Claude's short-lived jobs into durable, Hive-managed missions. */
21
- export function buildFleetSnapshot(sessions, jobs, events, missions, messageStore, planStore, now = Date.now()) {
23
+ export function buildFleetSnapshot(sessions, jobs, events, missions, messageStore, planStore, now = Date.now(), workerIdentity) {
22
24
  const workerCorrelationGraceMs = 8_000;
23
25
  const orchestrators = [];
24
26
  const other = [];
@@ -124,7 +126,7 @@ export function buildFleetSnapshot(sessions, jobs, events, missions, messageStor
124
126
  .map((event) => [`${event.sessionId}:${event.jobId ?? ""}:${event.ts}:${event.phase}:${event.decisionId ?? ""}:${event.targetWorker ?? ""}:${event.detail}`, event])).values()]
125
127
  .sort((a, b) => a.ts - b.ts);
126
128
  let recentEvents = allEvents.slice(-100);
127
- let workers = group.flatMap(({ jobId, job }) => (job.fan ?? []).filter((worker) => worker.kind === "agent" || worker.kind === "subagent").map((worker) => ({
129
+ const fanWorkers = group.flatMap(({ jobId, job }) => (job.fan ?? []).filter((worker) => worker.kind === "agent" || worker.kind === "subagent").map((worker) => ({
128
130
  ...worker,
129
131
  jobId,
130
132
  jobState: job.state,
@@ -134,150 +136,41 @@ export function buildFleetSnapshot(sessions, jobs, events, missions, messageStor
134
136
  const linkedSession = sessionsByJob.get(jobId);
135
137
  return [...(job.sessionId ? [[job.sessionId, jobId]] : []), ...(linkedSession ? [[linkedSession.sessionId, jobId]] : [])];
136
138
  }));
137
- const jobStateById = new Map(group.map(({ jobId, job }) => [jobId, job.state]));
138
- const parentTurnFinished = (jobId) => ["done", "completed", "failed", "error", "cancelled", "canceled", "stopped"].includes((jobStateById.get(jobId) ?? "").toLowerCase());
139
139
  const eventJobId = (event) => event.jobId ?? jobIdBySession.get(event.sessionId);
140
- // A job's fan array is only a current view and can drop an earlier worker
141
- // as soon as a later worker starts. Rebuild the durable native roster from
142
- // hook history, keyed by Claude's real agent id, and merge fan metadata.
143
- const nativeEvidence = new Map();
144
- for (const event of allEvents) {
145
- const resolvedJobId = eventJobId(event);
146
- const raw = event.rawPayload && typeof event.rawPayload === "object" ? event.rawPayload : {};
147
- const response = raw.tool_response && typeof raw.tool_response === "object" ? raw.tool_response : {};
148
- const input = raw.tool_input && typeof raw.tool_input === "object" ? raw.tool_input : {};
149
- const launchId = typeof response.agentId === "string" ? response.agentId : typeof response.agent_id === "string" ? response.agent_id : undefined;
150
- const nativeId = launchId ?? event.targetWorker;
151
- if (!resolvedJobId || !nativeId || nativeId === "hive-orchestrator")
152
- continue;
153
- const isStart = event.hookEventName === "SubagentStart" || (event.hookEventName === "PostToolUse" && event.toolName === "Agent" && Boolean(launchId));
154
- const isStop = event.hookEventName === "SubagentStop";
155
- if (!isStart && !isStop)
156
- continue;
157
- const label = typeof response.description === "string" ? response.description : typeof input.description === "string" ? input.description : nativeId;
158
- const key = `${resolvedJobId}:${nativeId}`;
159
- const prior = nativeEvidence.get(key);
160
- // A stop without any observed launch is often a nested/background task
161
- // notification. It is evidence, but not enough to invent a person.
162
- if (isStop && !prior)
163
- continue;
164
- nativeEvidence.set(key, {
165
- id: nativeId,
166
- jobId: resolvedJobId,
167
- label: prior?.label !== prior?.id ? prior.label : label,
168
- startedAt: Math.min(prior?.startedAt ?? event.ts, event.ts),
169
- doneAt: isStop ? event.ts : isStart ? undefined : prior?.doneAt,
170
- updatedAt: event.ts,
171
- running: isStart,
172
- });
173
- }
174
- const workerByNativeKey = new Map(workers.map((worker) => [`${worker.jobId}:${worker.id}`, worker]));
175
- for (const native of nativeEvidence.values()) {
176
- const key = `${native.jobId}:${native.id}`;
177
- const fanWorker = workerByNativeKey.get(key);
178
- const turnFinished = parentTurnFinished(native.jobId);
179
- // SubagentStop is the worker's own terminal lifecycle boundary. The
180
- // manager may remain idle/open for follow-up, but that must not keep a
181
- // finished worker seated indefinitely. A later SubagentStart for the
182
- // same native id clears native.doneAt above and resumes that person.
183
- const workerFinished = Boolean(native.doneAt) || turnFinished;
184
- const merged = fanWorker ? {
185
- ...fanWorker,
186
- label: fanWorker.label || native.label,
187
- startedAt: Math.min(fanWorker.startedAt, native.startedAt),
188
- doneAt: native.doneAt ?? (turnFinished ? fanWorker.doneAt : undefined),
189
- updatedAt: Math.max(fanWorker.updatedAt ?? 0, native.updatedAt),
190
- jobState: workerFinished ? "done" : native.running ? "working" : "idle",
191
- } : {
192
- id: native.id,
193
- kind: "agent",
194
- label: native.label,
195
- startedAt: native.startedAt,
196
- doneAt: native.doneAt,
197
- updatedAt: native.updatedAt,
198
- jobId: native.jobId,
199
- jobState: workerFinished ? "done" : native.running ? "working" : "idle",
200
- };
201
- workerByNativeKey.set(key, merged);
140
+ const missionSummary = missions?.summaryFor(missionId, oldest.job.intent ?? "");
141
+ // One canonical person per delegated job. The resolver owns every identity
142
+ // decision native ids, delegation labels, plan tasks, nested agents
143
+ // and persists them so a later poll cannot re-decide them differently.
144
+ const resolved = resolveMissionWorkers({
145
+ missionId,
146
+ fanWorkers,
147
+ events: allEvents,
148
+ eventJobId,
149
+ planTasks: planStore?.get(missionId)?.phases.flatMap((phase) => allPlanTasks(phase.tasks)),
150
+ missionActive: (missionSummary?.lifecycleStatus ?? "active") === "active",
151
+ seeded: workerIdentity?.list(missionId),
152
+ seededNested: workerIdentity?.listNested(missionId),
153
+ now,
154
+ correlationGraceMs: workerCorrelationGraceMs,
155
+ });
156
+ let workers = resolved.workers;
157
+ if (workerIdentity && resolved.dirty) {
158
+ workerIdentity.save(missionId, resolved.records);
159
+ workerIdentity.saveNested(missionId, resolved.nested);
202
160
  }
203
- workers = [...workerByNativeKey.values()];
204
- // A resumed/follow-up job may address the same provider-native agent id.
205
- // The mission roster is person-oriented, so expose that id once using its
206
- // latest lifecycle state instead of creating duplicate React/desk actors
207
- // for each short-lived parent job.
208
- const workersByIdentity = new Map();
209
- for (const worker of workers.sort((a, b) => a.startedAt - b.startedAt || a.id.localeCompare(b.id))) {
210
- const prior = workersByIdentity.get(worker.id);
211
- if (!prior) {
212
- workersByIdentity.set(worker.id, worker);
213
- continue;
161
+ // Keep the board honest without waiting for the orchestrator to republish
162
+ // it: a worker that is provably running advances its task to working.
163
+ if (planStore) {
164
+ // `doneAt` is the only lifecycle signal; `jobState` is decoration.
165
+ const present = workers.filter((worker) => !worker.doneAt);
166
+ for (const binding of reconcilePlanWorkers(planStore.get(missionId), present)) {
167
+ planStore.attachWorker(missionId, binding);
168
+ if (binding.inferred)
169
+ workerIdentity?.addTaskBinding(missionId, binding.canonicalWorker, binding.taskId);
214
170
  }
215
- const priorFreshness = prior.updatedAt ?? prior.doneAt ?? prior.startedAt;
216
- const workerFreshness = worker.updatedAt ?? worker.doneAt ?? worker.startedAt;
217
- const latest = workerFreshness >= priorFreshness ? worker : prior;
218
- workersByIdentity.set(worker.id, {
219
- ...latest,
220
- label: latest.label || prior.label,
221
- startedAt: Math.min(prior.startedAt, worker.startedAt),
222
- });
223
171
  }
224
- workers = [...workersByIdentity.values()];
225
- // Friendly/custom worker names are presentation aliases only. Native
226
- // Claude ids remain canonical so close or repeated delegations can never
227
- // swap React identities or manufacture an extra avatar.
228
- const workerAliases = new Map();
229
- const claimedNativeWorkers = new Set();
230
- for (const event of allEvents.filter((candidate) => candidate.source === "custom" && candidate.phase === "delegating" && candidate.targetWorker)) {
231
- const resolvedJobId = eventJobId(event);
232
- if (!resolvedJobId)
233
- continue;
234
- const aliasKey = `${resolvedJobId}:${event.targetWorker}`;
235
- const candidate = workers
236
- .filter((worker) => worker.jobId === resolvedJobId && !claimedNativeWorkers.has(worker.id) && Math.abs(worker.startedAt - event.ts) <= 30_000)
237
- .sort((a, b) => Math.abs(a.startedAt - event.ts) - Math.abs(b.startedAt - event.ts))[0];
238
- workerAliases.set(aliasKey, candidate?.id ?? aliasKey);
239
- if (!candidate)
240
- continue;
241
- claimedNativeWorkers.add(candidate.id);
242
- }
243
- if (workerAliases.size)
244
- recentEvents = recentEvents.map((event) => {
245
- if (!event.targetWorker)
246
- return event;
247
- const resolvedJobId = eventJobId(event);
248
- if (!resolvedJobId)
249
- return event;
250
- const key = `${resolvedJobId}:${event.targetWorker}`;
251
- const targetWorker = workerAliases.get(key);
252
- return targetWorker ? { ...event, targetWorker } : event;
253
- });
254
- // Claude's fan file can arrive just before its delegation hook. Publishing
255
- // that unmatched native id for a single poll makes the office animate a
256
- // second person. Hold it briefly for correlation; canonical/event-backed
257
- // workers remain immediate, and hookless workers appear after the grace.
258
- const correlatedWorkerIds = new Set(recentEvents.filter((event) => event.source === "custom" && event.phase === "delegating").map((event) => event.targetWorker).filter((id) => Boolean(id)));
259
- workers = workers.filter((worker) => nativeEvidence.has(`${worker.jobId}:${worker.id}`) || correlatedWorkerIds.has(worker.id) || now - worker.startedAt >= workerCorrelationGraceMs);
172
+ recentEvents = recentEvents.map(resolved.rewrite);
260
173
  const derived = deriveStatus(session, latest.job, recentEvents, now);
261
- const workerIds = new Set(workers.map((worker) => worker.id));
262
- for (const event of recentEvents) {
263
- if (!event.targetWorker || workerIds.has(event.targetWorker))
264
- continue;
265
- const related = recentEvents.filter((candidate) => candidate.targetWorker === event.targetWorker);
266
- const start = related.find((candidate) => candidate.phase === "delegating" || candidate.phase?.toLowerCase().includes("subagentstart") || candidate.hookEventName?.toLowerCase() === "subagentstart");
267
- if (!start)
268
- continue;
269
- // A friendly delegation target is an alias, not a person. Hold every
270
- // unmatched identity through the correlation window; a native Agent id
271
- // normally arrives within milliseconds and becomes the sole actor.
272
- if (now - start.ts < workerCorrelationGraceMs)
273
- continue;
274
- const done = [...related].reverse().find((candidate) => candidate.phase === "worker_reported" || candidate.phase?.toLowerCase().includes("subagentstop") || candidate.hookEventName?.toLowerCase() === "subagentstop");
275
- const resolvedJobId = eventJobId(event);
276
- if (!resolvedJobId)
277
- continue;
278
- workers.push({ id: event.targetWorker, label: event.targetWorker, kind: "subagent", startedAt: start.ts, doneAt: done?.ts, updatedAt: related.at(-1)?.ts, jobId: resolvedJobId, jobState: done ? "done" : latest.job.state });
279
- workerIds.add(event.targetWorker);
280
- }
281
174
  const derivedMessages = group.flatMap(({ jobId, job }) => {
282
175
  const createdAt = dateMs(job.createdAt);
283
176
  const updatedAt = dateMs(job.updatedAt) || createdAt;
@@ -319,7 +212,9 @@ export function buildFleetSnapshot(sessions, jobs, events, missions, messageStor
319
212
  lifecycleStatus: "active",
320
213
  mode: "orchestrated",
321
214
  };
322
- const missionPlan = planStore?.get(missionId);
215
+ // Derived at read time rather than written back: the events are the source
216
+ // of truth, so this always reflects the latest ones without extra writes.
217
+ const missionPlan = deriveChangedFiles(planStore?.get(missionId), workers, recentEvents);
323
218
  const inactiveForMs = Math.max(0, now - updatedAt);
324
219
  let activityStatus = derived.status;
325
220
  if (mission.lifecycleStatus === "completed" || mission.lifecycleStatus === "archived") {