@cmgmyr/hive 1.6.0 → 1.8.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.
- package/claude-plugin/skills/cleanup/SKILL.md +9 -1
- package/claude-plugin/skills/profile/SKILL.md +20 -5
- package/claude-plugin/skills/profile/references/examples/README.md +26 -0
- package/claude-plugin/skills/profile/references/examples/skeleton.md +127 -0
- package/claude-plugin/skills/profile/references/examples/two-projects.md +105 -0
- package/claude-plugin/skills/profile/references/interview.md +25 -3
- package/claude-plugin/skills/profile/references/recipes/index.md +1 -0
- package/claude-plugin/skills/profile/references/recipes/review-decision.md +2 -0
- package/claude-plugin/skills/profile/references/recipes/shared-test-resource.md +2 -0
- package/claude-plugin/skills/profile/references/recipes/verification.md +2 -2
- package/claude-plugin/skills/profile/references/recipes/worker-context-checkpoint.md +2 -0
- package/dist/build-info.json +1 -1
- package/dist/cli.js +49 -19
- package/dist/help.js +9 -1
- package/dist/leadWatch.js +9 -7
- package/dist/processes.js +37 -15
- package/dist/scheduler.js +179 -62
- package/dist/spawn.js +60 -10
- package/dist/tmux.js +15 -1
- package/dist/tools/agents.js +90 -35
- package/dist/tools/pads.js +28 -2
- package/dist/tools/wakes.js +44 -21
- package/package.json +1 -1
- package/profiles/orchestration/posture.md +17 -26
- package/profiles/orchestration/runbook.md +103 -47
- package/profiles/orchestration/worker.md +28 -18
package/dist/cli.js
CHANGED
|
@@ -22,7 +22,7 @@ import { confirmQueenWrite, listQueenAudit } from "./queenAudit.js";
|
|
|
22
22
|
import { formatRowCounts, projectRowCounts, removeProject } from "./projectRemove.js";
|
|
23
23
|
import { ensureHooksFile } from "./hooks.js";
|
|
24
24
|
import { errorMessage, registrationNoticeText, withTrailingNewline } from "./result.js";
|
|
25
|
-
import { ACTIVE_TIMER_WHERE, dashboardFileContained, describeStall, HELD_REASON_CONVERSATION, HELD_REASON_LEAD_PANE_DEAD, HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX, isUnclassifiablePaneHold, HELD_REASON_UNSUBMITTED_INPUT_PREFIX, isUnsubmittedInputHold, janitor, resolveDashboardFile, generateQueenDashboardNow, transcriptStaleness,
|
|
25
|
+
import { ACTIVE_TIMER_WHERE, dashboardFileContained, describeStall, HELD_REASON_CONVERSATION, HELD_REASON_LEAD_PANE_DEAD, HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX, isLeadRowClosedHold, isUnclassifiablePaneHold, HELD_REASON_UNSUBMITTED_INPUT_PREFIX, isUnsubmittedInputHold, janitor, resolveDashboardFile, generateQueenDashboardNow, transcriptStaleness, wasHeldForPaneIdentity, } from "./scheduler.js";
|
|
26
26
|
import { readTurnCount } from "./turnCount.js";
|
|
27
27
|
import { collectPortfolio, leadText } from "./portfolio.js";
|
|
28
28
|
import { STALL_BOUND_SECONDS } from "./backgroundTasks.js";
|
|
@@ -30,7 +30,7 @@ import { probeSessionInterpreter, reexecTarget, sessionStartVerdict, } from "./s
|
|
|
30
30
|
import { backupHealth, backupNow, backupsDir, formatBytes, listSnapshots, previewRestore, restoreSnapshot, totalSizeBytes, } from "./backup.js";
|
|
31
31
|
import { describeTeardownWindow, readTeardowns, teardownLogPath, } from "./teardown.js";
|
|
32
32
|
import { asNameClash, buildEnvFlags, closeAgentRow, isReservedAgentName, launchAgent, LEAD_KIND, LEAD_NAME, mintLeadActorId, upsertActor, withWindowClaim, } from "./spawn.js";
|
|
33
|
-
import { adoptableWindow, applyLayout, claimInitialWindow, configureHiveWindow, controlModeFor, createWindow, DEFAULT_LAYOUT, describePaneChoice, ensureSession, findProjectWindow, foreignSocket,
|
|
33
|
+
import { adoptableWindow, applyLayout, claimInitialWindow, configureHiveWindow, controlModeFor, createWindow, DEFAULT_LAYOUT, describePaneChoice, ensureSession, findProjectWindow, foreignSocket, isViewSessionName, listOwnedWindows, liveTargets, observationFailed, observeRowOwnership, ORPHAN_MIN_AGE_MS, orphanScratchServers, orphansWorthWarningAbout, paneIsAloneInWindow, paneVisibility, panePidForRecord, PROCESSES_LAYOUT, processesPaneTitle, processesWindowName, projectWindows, RAW_ATTACH_TMUX_CONFIG, renderAttachCommand, resolveAttachTarget, resolveInTmuxTarget, rowLive, rowOwnership, SESSION_PREFIX, appendGlobalHook, globalHooks, replaceGlobalHooks, sessionName, setPaneTitle, shellQuote, shownPaneTitle, makeProcessesWindow, tmux, TMUX_DOC, tmuxSaysNothingThere, tmuxTimeoutOverride, tmuxSocketPath, untrustedTmuxServer, waitForPaneEstablished, windowLayout, } from "./tmux.js";
|
|
34
34
|
import { describeVisibility, processCounts } from "./dashboard.js";
|
|
35
35
|
import { runningCommandRow, runningCommandRows, snapshotProcesses, stopAllProcesses, stopLine, stopProcess, STOP_REASONS, } from "./processes.js";
|
|
36
36
|
import { activeProfile, agentVarKeys, BUILT_IN_PROJECT_YML, configHash, loadProjectYml, mergedProjectVars, NO_PROFILE, PROJECT_YML_KEYS, resolveCommandDir, resolveFirstMessage, } from "./projectYml.js";
|
|
@@ -322,17 +322,20 @@ function ensureLeadRow(project, command) {
|
|
|
322
322
|
previousCommand: existing.command,
|
|
323
323
|
previousCodexHome: existing.codex_home,
|
|
324
324
|
casExpected: existing.tmux_target,
|
|
325
|
+
casSocket: existing.tmux_socket,
|
|
326
|
+
casPid: existing.pane_pid,
|
|
325
327
|
};
|
|
326
328
|
}
|
|
327
329
|
const priorClosed = db
|
|
328
330
|
.prepare("SELECT actor_id, tmux_target, tmux_socket, pane_pid, command, codex_home FROM agents WHERE project_id = ? AND kind = ? AND status = 'closed' AND actor_id != '' ORDER BY id DESC LIMIT 1")
|
|
329
331
|
.get(project.id, LEAD_KIND);
|
|
332
|
+
const insertedSocket = tmuxSocketPath(process.env.TMUX, process.env.TMUX_TMPDIR);
|
|
330
333
|
let result;
|
|
331
334
|
try {
|
|
332
335
|
result = db.transaction(() => {
|
|
333
336
|
const info = db
|
|
334
337
|
.prepare("INSERT INTO agents (project_id, name, command, cwd, kind, parent_actor_id, tmux_target, tmux_socket, pane_pid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
335
|
-
.run(project.id, LEAD_NAME, command, project.path, LEAD_KIND, currentActor(), "",
|
|
338
|
+
.run(project.id, LEAD_NAME, command, project.path, LEAD_KIND, currentActor(), "", insertedSocket, "");
|
|
336
339
|
const agentId = Number(info.lastInsertRowid);
|
|
337
340
|
const actorId = priorClosed?.actor_id ?? mintLeadActorId(agentId);
|
|
338
341
|
db.prepare("UPDATE agents SET actor_id = ? WHERE id = ?").run(actorId, agentId);
|
|
@@ -352,6 +355,8 @@ function ensureLeadRow(project, command) {
|
|
|
352
355
|
previousCommand: priorClosed?.command ?? "",
|
|
353
356
|
previousCodexHome: priorClosed?.codex_home ?? "",
|
|
354
357
|
casExpected: "",
|
|
358
|
+
casSocket: insertedSocket,
|
|
359
|
+
casPid: "",
|
|
355
360
|
};
|
|
356
361
|
}
|
|
357
362
|
function reportMigrationResult(result) {
|
|
@@ -375,6 +380,8 @@ async function cmdLead(argv) {
|
|
|
375
380
|
registrationNotice = text;
|
|
376
381
|
});
|
|
377
382
|
const auditIdentity = captureQueenWriteIdentity(project.id);
|
|
383
|
+
// Only rows running before this invocation began are leftovers, trust prompts included; a later `hive start` is not.
|
|
384
|
+
const leftoverIds = new Set(runningCommandRows(project.id).map((row) => row.id));
|
|
378
385
|
const { config, warnings, sources } = loadProjectYml(project.path);
|
|
379
386
|
if (detach && (!process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
380
387
|
let untrusted;
|
|
@@ -453,7 +460,8 @@ async function cmdLead(argv) {
|
|
|
453
460
|
}
|
|
454
461
|
}
|
|
455
462
|
const windowName = project.name;
|
|
456
|
-
const { agentId: leadAgentId, actorId: leadActorId, previousTarget, previousSocket, previousPanePid, previousCommand, previousCodexHome, casExpected, } = ensureLeadRow(project, leadCommand);
|
|
463
|
+
const { agentId: leadAgentId, actorId: leadActorId, previousTarget, previousSocket, previousPanePid, previousCommand, previousCodexHome, casExpected, casSocket, casPid, } = ensureLeadRow(project, leadCommand);
|
|
464
|
+
const previousIdentity = { tmux_target: previousTarget, tmux_socket: previousSocket, pane_pid: previousPanePid };
|
|
457
465
|
// A fresh CODEX_HOME is built on every invocation, same as claude's hooks/posture files above -
|
|
458
466
|
// cheap (a few KB; the codex-side plugins/cache bootstrap only materializes once a process
|
|
459
467
|
// actually starts under it) and correct regardless of whether this invocation ends up creating
|
|
@@ -524,10 +532,8 @@ async function cmdLead(argv) {
|
|
|
524
532
|
}
|
|
525
533
|
else {
|
|
526
534
|
const foundWindow = findProjectWindow(session, project.id);
|
|
527
|
-
|
|
528
|
-
const adopted =
|
|
529
|
-
? adoptableWindow(session, project.id, previousTarget)
|
|
530
|
-
: null;
|
|
535
|
+
// Only a pane the row provably owns is adopted; unknown or reissued gets a fresh pane, never the stranger's pid.
|
|
536
|
+
const adopted = rowOwnership(previousIdentity) === "live" ? adoptableWindow(session, project.id, previousTarget) : null;
|
|
531
537
|
if (adopted) {
|
|
532
538
|
leadPane = previousTarget;
|
|
533
539
|
leadWindow = adopted;
|
|
@@ -574,12 +580,24 @@ async function cmdLead(argv) {
|
|
|
574
580
|
if (newCodexHomeKey)
|
|
575
581
|
reapCodexHome(newCodexHomeKey);
|
|
576
582
|
};
|
|
583
|
+
if (!createdPane && rowOwnership(previousIdentity) !== "live") {
|
|
584
|
+
abandonClaim();
|
|
585
|
+
throw new Error("The lead pane this `hive lead` was about to adopt no longer reads as the lead's own (its pane pid changed " +
|
|
586
|
+
"or could not be verified), so nothing was recorded. Re-run `hive lead`; it will start a fresh pane.");
|
|
587
|
+
}
|
|
588
|
+
const recordedPanePid = createdPane ? panePidForRecord(leadPane) : previousPanePid;
|
|
589
|
+
if (recordedPanePid === "") {
|
|
590
|
+
abandonClaim();
|
|
591
|
+
throw new Error(`tmux did not report a process id for the new lead pane ${leadPane} (asked twice), so it was discarded ` +
|
|
592
|
+
"rather than recorded: a lead row with no pane pid can never be verified as owning its pane. Re-run `hive lead`.");
|
|
593
|
+
}
|
|
577
594
|
let wonRace;
|
|
578
595
|
try {
|
|
579
596
|
wonRace = db.transaction(() => {
|
|
580
597
|
const updated = db
|
|
581
|
-
.prepare("UPDATE agents SET tmux_target = ?, tmux_socket = ?, pane_pid = ?, command = ?, codex_home = ?
|
|
582
|
-
|
|
598
|
+
.prepare("UPDATE agents SET tmux_target = ?, tmux_socket = ?, pane_pid = ?, command = ?, codex_home = ? " +
|
|
599
|
+
"WHERE id = ? AND tmux_target = ? AND tmux_socket = ? AND pane_pid = ? AND status = 'running'")
|
|
600
|
+
.run(leadPane, tmuxSocketPath(process.env.TMUX, process.env.TMUX_TMPDIR), recordedPanePid, recordedCommand, recordedCodexHome, leadAgentId, casExpected, casSocket, casPid).changes;
|
|
583
601
|
if (updated === 0)
|
|
584
602
|
return false;
|
|
585
603
|
db.prepare(`UPDATE wakes SET deliver_pane = ?, held_at = NULL, held_reason = NULL
|
|
@@ -610,7 +628,7 @@ async function cmdLead(argv) {
|
|
|
610
628
|
}
|
|
611
629
|
if (createdPane) {
|
|
612
630
|
armLeadPaneExitedHook();
|
|
613
|
-
clearLeftoverProcesses(project, config);
|
|
631
|
+
clearLeftoverProcesses(project, config, leftoverIds);
|
|
614
632
|
}
|
|
615
633
|
else if (!leadPaneExitedHookArmed()) {
|
|
616
634
|
console.log(`! this tmux server carries no ${LEAD_PANE_EXITED_HOOK} backstop and hive arms one only on a pane it created, ` +
|
|
@@ -732,8 +750,13 @@ const HIVE_YML_TEMPLATE = `# hive project config. Read by \`hive lead\` from the
|
|
|
732
750
|
# start_command: /jira-start
|
|
733
751
|
# check: npm run lint && npx tsc --noEmit # what a worker runs before
|
|
734
752
|
# reporting done; gates only,
|
|
735
|
-
# no suite (
|
|
753
|
+
# no suite (suite_command is that)
|
|
736
754
|
# check: ./vendor/bin/pint --test && ./vendor/bin/phpstan # a PHP stack's
|
|
755
|
+
# test_command: npm test -- test/foo.test.mjs # scoped check for one change
|
|
756
|
+
# suite_command: npm test # full suite; one run at a time, lead's slot
|
|
757
|
+
# verify_command: npm run build && node dist/cli.js --version # exercises the real outcome
|
|
758
|
+
# review_command: /review # a fresh-context reviewer, if your harness has one
|
|
759
|
+
# worker_model: sonnet # the cheaper tier workers spawn on
|
|
737
760
|
# ci: weekly # absent: CI runs on push/PR. weekly: CI runs
|
|
738
761
|
# on schedule + dispatch only, and the local
|
|
739
762
|
# full suite gates the merge
|
|
@@ -1293,7 +1316,7 @@ function openDashboardOnce(project, dashboardFile) {
|
|
|
1293
1316
|
if (claim.changes === 0)
|
|
1294
1317
|
return;
|
|
1295
1318
|
try {
|
|
1296
|
-
execFileSync("open", [pathToFileURL(dashboardFile).href], { stdio: "ignore", timeout:
|
|
1319
|
+
execFileSync("open", [pathToFileURL(dashboardFile).href], { stdio: "ignore", timeout: 10000 });
|
|
1297
1320
|
}
|
|
1298
1321
|
catch {
|
|
1299
1322
|
db.prepare("DELETE FROM kv WHERE project_id = ? AND key = ?").run(project.id, DASHBOARD_OPENED_KV_KEY);
|
|
@@ -1377,14 +1400,18 @@ function resolveNamedProcess(argv, verb) {
|
|
|
1377
1400
|
console.log(`${name}: not running (start with: hive start "${name}")`);
|
|
1378
1401
|
return null;
|
|
1379
1402
|
}
|
|
1380
|
-
const
|
|
1381
|
-
if (
|
|
1403
|
+
const ownership = observeRowOwnership(row);
|
|
1404
|
+
if (ownership === "probe-failed") {
|
|
1382
1405
|
console.log(CANNOT_TELL_WHERE(name));
|
|
1383
1406
|
return null;
|
|
1384
1407
|
}
|
|
1408
|
+
if (ownership === "unknown") {
|
|
1409
|
+
console.log(`${name}: its pane ownership is unknown (no pane pid recorded, or another tmux socket), so hive will not move that pane`);
|
|
1410
|
+
return null;
|
|
1411
|
+
}
|
|
1385
1412
|
// A pane id alone is not an identity: tmux restarts them at %0 on a fresh server, so a stale
|
|
1386
|
-
// running row names whoever holds that id now.
|
|
1387
|
-
if (
|
|
1413
|
+
// running row names whoever holds that id now.
|
|
1414
|
+
if (ownership !== "live") {
|
|
1388
1415
|
console.log(`${name}: not running (start with: hive start "${name}")`);
|
|
1389
1416
|
return null;
|
|
1390
1417
|
}
|
|
@@ -1466,8 +1493,10 @@ function cmdLeadPaneExited(argv) {
|
|
|
1466
1493
|
}
|
|
1467
1494
|
// Only a lead that CREATED its pane clears leftovers. An adopted live lead is the same lead those
|
|
1468
1495
|
// processes belong to, and stopping them there would take down a running crew's dev server.
|
|
1469
|
-
function clearLeftoverProcesses(project, config) {
|
|
1496
|
+
function clearLeftoverProcesses(project, config, leftoverIds) {
|
|
1470
1497
|
for (const row of runningCommandRows(project.id)) {
|
|
1498
|
+
if (!leftoverIds.has(row.id))
|
|
1499
|
+
continue;
|
|
1471
1500
|
const stopped = stopProcess(row, STOP_REASONS.previousLead);
|
|
1472
1501
|
// Nothing was running under it, so the auto-start line that follows is the whole story.
|
|
1473
1502
|
if (stopped.leg === "already-gone")
|
|
@@ -2793,7 +2822,8 @@ export function heldReasonLabel(heldReason) {
|
|
|
2793
2822
|
if (isUnsubmittedInputHold(heldReason))
|
|
2794
2823
|
return "typing";
|
|
2795
2824
|
if (heldReason === HELD_REASON_LEAD_PANE_DEAD ||
|
|
2796
|
-
|
|
2825
|
+
wasHeldForPaneIdentity(heldReason) ||
|
|
2826
|
+
isLeadRowClosedHold(heldReason) ||
|
|
2797
2827
|
isUnclassifiablePaneHold(heldReason)) {
|
|
2798
2828
|
return "needs you";
|
|
2799
2829
|
}
|
package/dist/help.js
CHANGED
|
@@ -179,6 +179,10 @@ runbook (fork it first), not a pad write.`,
|
|
|
179
179
|
a stale row closed with no kill, stop or typing; a human or peer lead
|
|
180
180
|
only, refused for a row that provably owns a live pane or when tmux
|
|
181
181
|
could not be probed.
|
|
182
|
+
Every kill or type above acts only on a pane whose recorded pid matches.
|
|
183
|
+
Unverifiable ownership (no recorded pid) refuses by name; a gone or
|
|
184
|
+
reissued pane is never touched, and close, park and rename then act
|
|
185
|
+
on the row alone.
|
|
182
186
|
|
|
183
187
|
Address a worker by its name, not its id: agent_send(name="impl", ...). A
|
|
184
188
|
partial name works when it matches one running worker, so name="123" finds
|
|
@@ -316,7 +320,11 @@ place.`,
|
|
|
316
320
|
pad_write(name, content, tags?) — create a pad; names are unique per project
|
|
317
321
|
pad_write(pad_id, name, content, expected_revision) — full overwrite; the
|
|
318
322
|
revision guard makes concurrent writers safe
|
|
319
|
-
pad_read(pad_id | name) — content plus revision and metadata
|
|
323
|
+
pad_read(pad_id | name, offset?, limit?) — content plus revision and metadata
|
|
324
|
+
offset and limit are nonnegative safe integers in UTF-16 code units, with
|
|
325
|
+
no default cap; a cut inside a surrogate pair moves back one unit, or
|
|
326
|
+
forward if moving back would return nothing, and the result reports its
|
|
327
|
+
effective offset
|
|
320
328
|
pad_append(pad_id, content, expected_revision?) — add to the end
|
|
321
329
|
pad_edit(pad_id, old_text, new_text, expected_revision?) — replace one
|
|
322
330
|
unique literal occurrence; include enough context to disambiguate
|
package/dist/leadWatch.js
CHANGED
|
@@ -2,7 +2,7 @@ import { db } from "./db.js";
|
|
|
2
2
|
import { paneClassifierFor } from "./harnesses.js";
|
|
3
3
|
import { readLeadTurnState } from "./leadState.js";
|
|
4
4
|
import { LEAD_KIND } from "./spawn.js";
|
|
5
|
-
import { holdsHumanInput,
|
|
5
|
+
import { foreignSocket, holdsHumanInput, observationFailed, rowOwnership } from "./tmux.js";
|
|
6
6
|
export const LEAD_TARGET_GONE = "LEAD_TARGET_GONE";
|
|
7
7
|
export const LEAD_TARGET_REISSUED = "LEAD_TARGET_REISSUED";
|
|
8
8
|
export const LEAD_TARGET_RESTARTED = "LEAD_TARGET_RESTARTED";
|
|
@@ -39,14 +39,16 @@ export function evaluateLeadWatch(sub, mode, snapshot, readers) {
|
|
|
39
39
|
return { kind: "invalid", reason: LEAD_TARGET_GONE };
|
|
40
40
|
if (lead.pane_pid !== sub.pane_pid)
|
|
41
41
|
return { kind: "invalid", reason: LEAD_TARGET_RESTARTED };
|
|
42
|
-
if (
|
|
43
|
-
return { kind: "pending", reason: "tmux did not answer" };
|
|
44
|
-
const probe = rowAliveProbe(lead.tmux_socket, lead.tmux_target, snapshot);
|
|
45
|
-
if (probe.live === null)
|
|
42
|
+
if (foreignSocket(lead.tmux_socket))
|
|
46
43
|
return { kind: "pending", reason: "its pane cannot be probed from this process" };
|
|
47
|
-
if (
|
|
44
|
+
if (observationFailed(snapshot))
|
|
45
|
+
return { kind: "pending", reason: "tmux did not answer" };
|
|
46
|
+
const ownership = rowOwnership(lead, snapshot);
|
|
47
|
+
if (ownership === "unknown")
|
|
48
|
+
return { kind: "pending", reason: "pane ownership unknown" };
|
|
49
|
+
if (ownership === "gone")
|
|
48
50
|
return { kind: "invalid", reason: LEAD_TARGET_GONE };
|
|
49
|
-
if (
|
|
51
|
+
if (ownership === "reissued")
|
|
50
52
|
return { kind: "invalid", reason: LEAD_TARGET_REISSUED };
|
|
51
53
|
const state = readLeadTurnState(lead.id);
|
|
52
54
|
if (state === null || state.pane_pid !== sub.pane_pid)
|
package/dist/processes.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getProject } from "./context.js";
|
|
2
2
|
import { db } from "./db.js";
|
|
3
3
|
import { loadProjectYml } from "./projectYml.js";
|
|
4
|
-
import { closeAgentRow, killAgentPane, relayoutAfterPaneLeft } from "./spawn.js";
|
|
5
|
-
import { cancelCopyMode,
|
|
4
|
+
import { closeAgentRow, currentOwnership, killAgentPane, paneIdentity, relayoutAfterPaneLeft } from "./spawn.js";
|
|
5
|
+
import { cancelCopyMode, observeRowOwnership, paneProcessExited, paneVisibility, paneWindow, projectWindows, rowLive, sessionName, tmux, } from "./tmux.js";
|
|
6
6
|
// One derivation for every surface that reports where a project's processes are: `hive status`,
|
|
7
7
|
// `hive doctor`, and the dashboard. Two tmux window lookups per project, then one per running
|
|
8
8
|
// process, and none at all for a project with neither a running command nor a defined one.
|
|
@@ -96,6 +96,19 @@ function waitForPaneToExit(target, timeoutMs) {
|
|
|
96
96
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.min(STOP_POLL_MS, remaining));
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
+
const UNKNOWN_OWNERSHIP_NOTE = "its pane ownership is unknown (no pane pid recorded, a legacy window target, or another tmux socket), so hive left it running";
|
|
100
|
+
const REISSUED_NOTE = "its pane id now belongs to another process, which was left alone";
|
|
101
|
+
const CHANGED_NOTE = "its row was closed or recorded a different pane during the stop, so hive touched nothing further";
|
|
102
|
+
// Unknown, or a row that changed underneath the stop, is left running: closing it would hide a
|
|
103
|
+
// process that may still be running from every surface that reports one.
|
|
104
|
+
function notOwned(row, ownership) {
|
|
105
|
+
if (ownership === "unknown")
|
|
106
|
+
return { name: row.name, leg: "unreachable", note: UNKNOWN_OWNERSHIP_NOTE };
|
|
107
|
+
if (ownership === "changed")
|
|
108
|
+
return { name: row.name, leg: "unreachable", note: CHANGED_NOTE };
|
|
109
|
+
closeAgentRow(row.id, paneIdentity(row));
|
|
110
|
+
return { name: row.name, leg: "already-gone", ...(ownership === "reissued" ? { note: REISSUED_NOTE } : {}) };
|
|
111
|
+
}
|
|
99
112
|
// The only thing that stops a hive.yml command. kill-pane alone is SIGHUP, which a dev server with
|
|
100
113
|
// detached grandchildren survives while still holding its port, so the graceful leg comes first and
|
|
101
114
|
// the receipt says which one ended it.
|
|
@@ -103,19 +116,19 @@ function waitForPaneToExit(target, timeoutMs) {
|
|
|
103
116
|
// argument so every caller has to declare its trigger from the closed set docs/projects.md is
|
|
104
117
|
// checked against, and whichever caller prints it already knows which one it passed.
|
|
105
118
|
export function stopProcess(row, reason) {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (probe.live === null)
|
|
119
|
+
const identity = paneIdentity(row);
|
|
120
|
+
const ownership = observeRowOwnership(identity);
|
|
121
|
+
if (ownership === "probe-failed")
|
|
110
122
|
return { name: row.name, leg: "unreachable" };
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (probe.live === false || paneReissued(row.pane_pid, probe)) {
|
|
114
|
-
closeAgentRow(row.id);
|
|
115
|
-
return { name: row.name, leg: "already-gone" };
|
|
116
|
-
}
|
|
123
|
+
if (ownership !== "live")
|
|
124
|
+
return notOwned(row, ownership);
|
|
117
125
|
markStopping(row.project_id, row.id);
|
|
118
126
|
const window = paneWindow(row.tmux_target);
|
|
127
|
+
const beforeSignal = currentOwnership(row.id, identity);
|
|
128
|
+
if (beforeSignal !== "live") {
|
|
129
|
+
clearStoppingMarker(row.project_id, row.id);
|
|
130
|
+
return notOwned(row, beforeSignal);
|
|
131
|
+
}
|
|
119
132
|
try {
|
|
120
133
|
// In copy mode C-c is `cancel`, not SIGINT, so the graceful leg would silently do nothing to a
|
|
121
134
|
// pane someone scrolled back in. A stop may take that scrollback position: the process is ending.
|
|
@@ -126,6 +139,11 @@ export function stopProcess(row, reason) {
|
|
|
126
139
|
}
|
|
127
140
|
let leg = "interrupted";
|
|
128
141
|
if (!waitForPaneToExit(row.tmux_target, STOP_GRACE_MS)) {
|
|
142
|
+
const beforeKill = currentOwnership(row.id, identity);
|
|
143
|
+
if (beforeKill !== "live") {
|
|
144
|
+
clearStoppingMarker(row.project_id, row.id);
|
|
145
|
+
return notOwned(row, beforeKill);
|
|
146
|
+
}
|
|
129
147
|
try {
|
|
130
148
|
killAgentPane(row.tmux_target);
|
|
131
149
|
}
|
|
@@ -137,7 +155,7 @@ export function stopProcess(row, reason) {
|
|
|
137
155
|
// RUNNING, so the process stays visible to hive status, hive stop and the janitor.
|
|
138
156
|
if (leg === "still-running")
|
|
139
157
|
return { name: row.name, leg };
|
|
140
|
-
closeAgentRow(row.id,
|
|
158
|
+
closeAgentRow(row.id, identity);
|
|
141
159
|
clearStoppingMarker(row.project_id, row.id);
|
|
142
160
|
relayoutAfterPaneLeft(window);
|
|
143
161
|
return { name: row.name, leg };
|
|
@@ -152,9 +170,13 @@ export function stopLine(stopped) {
|
|
|
152
170
|
case "killed":
|
|
153
171
|
return `${stopped.name}: stopped (killed after ${STOP_GRACE_MS / 1000}s)`;
|
|
154
172
|
case "already-gone":
|
|
155
|
-
return
|
|
173
|
+
return stopped.note
|
|
174
|
+
? `${stopped.name}: already gone (${stopped.note})`
|
|
175
|
+
: `${stopped.name}: already gone (nothing was running)`;
|
|
156
176
|
case "unreachable":
|
|
157
|
-
return
|
|
177
|
+
return stopped.note
|
|
178
|
+
? `${stopped.name}: ${stopped.note}`
|
|
179
|
+
: `${stopped.name}: tmux could not be probed, so hive left it running`;
|
|
158
180
|
case "still-running":
|
|
159
181
|
return `${stopped.name}: still running: its pane survived C-c and kill-pane, so hive left the row open`;
|
|
160
182
|
}
|