@cmgmyr/hive 1.5.0 → 1.7.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 +30 -3
- package/dist/build-info.json +1 -1
- package/dist/cli.js +123 -40
- package/dist/contextCheckpoint.js +2 -2
- package/dist/help.js +16 -4
- package/dist/hook.js +141 -11
- package/dist/leadWatch.js +9 -7
- package/dist/portfolio.js +4 -5
- package/dist/processes.js +37 -15
- package/dist/projectRemove.js +46 -10
- package/dist/scheduler.js +179 -62
- package/dist/spawn.js +70 -15
- package/dist/tmux.js +40 -1
- package/dist/tools/agents.js +147 -39
- package/dist/tools/meta.js +1 -1
- package/dist/tools/pads.js +28 -2
- package/dist/tools/wakes.js +44 -21
- package/package.json +1 -1
|
@@ -47,13 +47,21 @@ Archive it. A pad still describing in-flight state for work that finished is wor
|
|
|
47
47
|
|
|
48
48
|
A watch armed for a crew that is gone is noise. A dated reminder someone set on purpose is not - cancelling one silently loses a commitment nobody will notice missing until the date passes. If you cannot tell which it is, leave it and say so.
|
|
49
49
|
|
|
50
|
+
## Project copies of the shared profile: report, do not rewrite
|
|
51
|
+
|
|
52
|
+
At wrap-up, run `hive profile list` and use the `*` to identify the active profile and every file it carries, including extra Markdown files. Compare its rendered output from `hive posture`, `hive runbook`, and `hive profile read <file>` for every listed file with five kinds of project artifacts: pad headers, lines in the board pad, active todo bodies, the lead's project memory, and `CLAUDE.md`, `AGENTS.md`, or files under `docs/`. Read pad headers rather than whole lessons pads, and inspect todos without their comments. Read profile sources only when this pass runs: redirect `hive posture`, `hive runbook`, and each `hive profile read <file>` to files, then inspect them in bounded sections. For example, use `hive runbook > <file>` and read it by range. For every match, give the artifact and location, the exact profile source and line, its class, and a proposed pointer such as `see <profile source>, <section or line>`.
|
|
53
|
+
|
|
54
|
+
List contradictions first, then restatements, then likely deliberate copies. A contradiction gives an outdated project-specific value as current guidance when the rendered profile now says something different or no longer says it. For example, a project memory that says commits use `--no-gpg-sign` contradicts a rendered rule requiring signed commits. A restatement repeats a profile rule without adding a project-specific choice; if replacing it with a pointer loses no useful detail, propose the pointer. A deliberate override has a project-specific delta, such as a path, a measured value, or a dated human decision; keep that detail and report it as an override. When a copy says it was kept because readers could not find the on-demand source, report it as a likely deliberate copy and ask whether it is still needed. Never infer that an outdated value is an override just because it is project-specific.
|
|
55
|
+
|
|
56
|
+
This pass only reports and proposes. The lead may apply pointer changes to pads and board lines. Any memory change or deletion needs the human's OK. Do not edit, delete, archive, or send anything as part of this pass. Run it at wrap-up, not at every lane end.
|
|
57
|
+
|
|
50
58
|
## Residue you report and do not remove
|
|
51
59
|
|
|
52
60
|
Worktrees, branches, running agents. Removing a worktree can destroy uncommitted work, and a running agent may be mid-turn. Name what you found and let the human or the lead decide.
|
|
53
61
|
|
|
54
62
|
## Finish by saying what you left, not just what you changed
|
|
55
63
|
|
|
56
|
-
A cleanup that reports only its edits cannot be audited, and the interesting part is usually what was deliberately not touched. Leave the board in the shape the next session needs: where the code is now, whatever verification number this project quotes, what is genuinely in flight (often nothing),
|
|
64
|
+
A cleanup that reports only its edits cannot be audited, and the interesting part is usually what was deliberately not touched. Leave the board in the shape the next session needs: where the code is now, whatever verification number this project quotes, what is genuinely in flight (often nothing), what is waiting on a human, and the profile-drift report with its proposed pointers.
|
|
57
65
|
|
|
58
66
|
## Where to route evidence in this project
|
|
59
67
|
|
|
@@ -9,7 +9,27 @@ You are interviewing someone to produce, or change, a hive profile: the standing
|
|
|
9
9
|
|
|
10
10
|
## Read before you ask anything
|
|
11
11
|
|
|
12
|
-
Run `hive profile list` and `hive doctor` (or read this project's `hive.yml` directly) before the first question. If this is an edit, also read the named profile's current, rendered behavior
|
|
12
|
+
Run `hive profile list` and `hive doctor` (or read this project's `hive.yml` directly) before the first question. If this is an edit, also read the named profile's current, rendered behavior for each of `posture.md`, `runbook.md`, `worker.md`, and any other `.md` it carries, using "Read a rendered file in bounded sections" below, so you understand what this profile actually does today. Do not ask the user to repeat anything already answered by what you just read. This rendered read is for understanding only - `references/interview.md` covers the raw template (via `hive profile path`) you must read from, and write onto, when you actually apply an edit; a rendered copy drops any conditional section whose var is unset, so it is never a safe edit source.
|
|
13
|
+
|
|
14
|
+
## Read a rendered file in bounded sections
|
|
15
|
+
|
|
16
|
+
A harness may persist tool output over about 30KB to a file and show only a short preview, so a large rendered file read straight into a tool result is read mostly unseen. Redirect first, then look. `profile_name` is a name from `hive profile list`, `profile_file` is a filename it lists, and `profile_rendered` is a disposable file:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
profile_rendered=$(mktemp)
|
|
20
|
+
hive profile read "$profile_file" --profile "$profile_name" > "$profile_rendered" && wc -c < "$profile_rendered"
|
|
21
|
+
printf '%s\n' "$profile_rendered"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Shell variables do not survive between tool calls. Note the path the last line printed and write that literal path into every later command and every file-reader call; do not rely on `$profile_rendered` after this call.
|
|
25
|
+
|
|
26
|
+
Use the saved content only if the read command succeeded. `wc -c` is the rendered size in bytes, the same number `hive profile list` and `hive doctor` show. If it is 25600 or less, read the whole file with your file reader. Above that, never read the whole file in one tool call; read it in sections of at most 16000 bytes each:
|
|
27
|
+
|
|
28
|
+
1. Check for long lines first: `awk 'length > 2000 {print NR; exit}' PATH`. A file reader truncates any line over 2000 characters, so if this prints a line number, read the whole file by bytes with step 3 instead of with line ranges.
|
|
29
|
+
2. Otherwise pick a line range with your file reader's offset and limit, and measure it first: `sed -n '1,80p' PATH | wc -c`. Shrink the range until the count is 16000 or less, then read it. Continue from the next line until you have read the last line. A scan of the headers is not a read.
|
|
30
|
+
3. To read by bytes: `dd if=PATH bs=1 skip=0 count=16000 2>/dev/null`. Advance `skip` by 15996 each time, which overlaps 4 bytes so text split at a section edge shows whole in the next section. Stop when a section reaches the byte count from `wc -c`.
|
|
31
|
+
|
|
32
|
+
Delete the temporary file when you are done. It is for understanding and checking only, never a source for an edit (see below).
|
|
13
33
|
|
|
14
34
|
## Discovery first, not a checklist
|
|
15
35
|
|
|
@@ -70,9 +90,16 @@ A command, a repository name, a ticket prefix: these belong in `hive.yml` `vars`
|
|
|
70
90
|
|
|
71
91
|
## Validate before you tell them it's done
|
|
72
92
|
|
|
73
|
-
After writing, run `hive doctor` and
|
|
93
|
+
After writing, run `hive doctor` and read each file you touched with "Read a rendered file in bounded sections" above, from the project the profile is meant to serve. Read it to the last line: a change near the end of a large file is easy to miss. A profile with no readable `runbook.md` fails `hive doctor` outright; `posture.md` and `worker.md` do not, but check them anyway. Tell the user to restart any running hive session so it picks up the change, and invite one small follow-up edit once they've tried it - that is the expected way a profile keeps improving.
|
|
94
|
+
|
|
95
|
+
## After a profile edit lands, ask once about notifying leads
|
|
96
|
+
|
|
97
|
+
Do this once after the full profile edit has been reviewed and committed, not after each commit or review round. Find the changed files and the commit range for that edit, and record when both review and commit are complete; that is when the edit landed. Use `project_list` to enumerate registered projects and their paths. In each project, run `hive profile list`; its `*` marks the active profile. Keep projects whose active profile matches the one you edited.
|
|
98
|
+
|
|
99
|
+
For each matching project, call `agent_list` with `{project_id: <project id>}`. A recipient must have `kind: "lead"`, `status: "running"`, and `alive: true`, which means the row owns a live pane. Inspect its `command` for `--append-system-prompt-file <path>`. When the named file exists, compare its modification time with the edit's landed time: notify if the file is older, and exclude it if it is equal or newer. `hive lead` rewrites that file when it launches, so its modification time reflects the current session's posture render. If the command has no such flag or the file is missing, include the lead and state the reason, such as `no posture file flag` or `posture file missing`. Do not include rows with `alive: false` or `alive: null`. Ask once and show the project names, lead names, and any reason a posture age could not be checked: `N running leads use this profile: <projects and reasons>. Notify them?` If no leads qualify, report that and stop.
|
|
100
|
+
|
|
101
|
+
On no, send nothing. On yes, call `wake_set` once per qualifying lead with `delay_seconds: 1`, `project_id: <that project's integer id>`, and `deliver_to: <lead's integer agent_id>`. Do not pass an actor id such as `lead:663` as `deliver_to`. Each wake body names the changed files and commit range, then tells the lead to re-read with `hive posture`, `hive runbook`, and `hive profile read <file>`, or restart through `hive lead` to reload the posture. Do not send a wake before the human says yes.
|
|
74
102
|
|
|
75
103
|
## What this skill does not do
|
|
76
104
|
|
|
77
105
|
It does not dispatch project work, change which agent application or model a project uses beyond what was explicitly asked, install a third-party tool, or commit or publish anything. It edits `hive.yml`'s `profile`, `agents`, `vars`, `context_checkpoint_percent`, and `lead_turn_budget` keys only on an explicit ask, and only to values the user gave or confirmed.
|
|
78
|
-
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.
|
|
1
|
+
{"version":"1.7.0","sha":"2000149","dirty":false,"build_id":"c539d81be530294a451590d781bd6922cd7155087a70b8fc64dedc35cf94f3e2"}
|
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, ` +
|
|
@@ -1111,6 +1129,16 @@ function profileDriftText(f) {
|
|
|
1111
1129
|
text: `hive's default ${f.file} changed since you forked it${pct != null ? ` (${pct}% diverged)` : ""}`,
|
|
1112
1130
|
};
|
|
1113
1131
|
}
|
|
1132
|
+
const PROFILE_SIZE_WARNING_BYTES = 25600;
|
|
1133
|
+
function profileRenderedBytes(name, file, vars) {
|
|
1134
|
+
const rendered = renderProfileFile(name, file, vars);
|
|
1135
|
+
return rendered == null ? null : Buffer.byteLength(withTrailingNewline(rendered), "utf8");
|
|
1136
|
+
}
|
|
1137
|
+
function profileSizeAdvisory(name, file) {
|
|
1138
|
+
return (`${name}/${file}: rendered size exceeds ${PROFILE_SIZE_WARNING_BYTES} bytes. A harness may persist tool output ` +
|
|
1139
|
+
"over about 30KB and show only a short preview. Split into smaller profile files read with hive profile read, " +
|
|
1140
|
+
"or redirect hive profile read to a file and read by section.");
|
|
1141
|
+
}
|
|
1114
1142
|
function cmdProfile(argv) {
|
|
1115
1143
|
const [sub, ...rest] = argv;
|
|
1116
1144
|
try {
|
|
@@ -1125,7 +1153,9 @@ function cmdProfile(argv) {
|
|
|
1125
1153
|
return;
|
|
1126
1154
|
}
|
|
1127
1155
|
const here = findProjectForCwd();
|
|
1128
|
-
const
|
|
1156
|
+
const hereConfig = here ? loadProjectYml(here.path).config : null;
|
|
1157
|
+
const current = here ? activeProfile(hereConfig) : null;
|
|
1158
|
+
const listVars = mergedProjectVars(hereConfig);
|
|
1129
1159
|
const statuses = names.map((profile) => profileStatus(profile));
|
|
1130
1160
|
const fileWidth = Math.max(11, ...statuses.flatMap((s) => s.files.map((f) => f.file.length)));
|
|
1131
1161
|
for (const [i, profile] of names.entries()) {
|
|
@@ -1133,7 +1163,11 @@ function cmdProfile(argv) {
|
|
|
1133
1163
|
console.log(`${profile === current ? "*" : " "} ${profile}`);
|
|
1134
1164
|
for (const f of status.files) {
|
|
1135
1165
|
const drift = profileDriftText(f);
|
|
1136
|
-
|
|
1166
|
+
const bytes = profileRenderedBytes(profile, f.file, listVars);
|
|
1167
|
+
const size = bytes == null ? "[rendered size unavailable]" : `[${bytes} rendered bytes]`;
|
|
1168
|
+
console.log(` ${f.file.padEnd(fileWidth)} ${f.source.padEnd(7)} ${f.path}${drift ? ` (${drift.text})` : ""} ${size}`);
|
|
1169
|
+
if (bytes != null && bytes > PROFILE_SIZE_WARNING_BYTES)
|
|
1170
|
+
console.log(` ${profileSizeAdvisory(profile, f.file)}`);
|
|
1137
1171
|
}
|
|
1138
1172
|
}
|
|
1139
1173
|
if (current)
|
|
@@ -1277,7 +1311,7 @@ function openDashboardOnce(project, dashboardFile) {
|
|
|
1277
1311
|
if (claim.changes === 0)
|
|
1278
1312
|
return;
|
|
1279
1313
|
try {
|
|
1280
|
-
execFileSync("open", [pathToFileURL(dashboardFile).href], { stdio: "ignore", timeout:
|
|
1314
|
+
execFileSync("open", [pathToFileURL(dashboardFile).href], { stdio: "ignore", timeout: 10000 });
|
|
1281
1315
|
}
|
|
1282
1316
|
catch {
|
|
1283
1317
|
db.prepare("DELETE FROM kv WHERE project_id = ? AND key = ?").run(project.id, DASHBOARD_OPENED_KV_KEY);
|
|
@@ -1361,14 +1395,18 @@ function resolveNamedProcess(argv, verb) {
|
|
|
1361
1395
|
console.log(`${name}: not running (start with: hive start "${name}")`);
|
|
1362
1396
|
return null;
|
|
1363
1397
|
}
|
|
1364
|
-
const
|
|
1365
|
-
if (
|
|
1398
|
+
const ownership = observeRowOwnership(row);
|
|
1399
|
+
if (ownership === "probe-failed") {
|
|
1366
1400
|
console.log(CANNOT_TELL_WHERE(name));
|
|
1367
1401
|
return null;
|
|
1368
1402
|
}
|
|
1403
|
+
if (ownership === "unknown") {
|
|
1404
|
+
console.log(`${name}: its pane ownership is unknown (no pane pid recorded, or another tmux socket), so hive will not move that pane`);
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1369
1407
|
// A pane id alone is not an identity: tmux restarts them at %0 on a fresh server, so a stale
|
|
1370
|
-
// running row names whoever holds that id now.
|
|
1371
|
-
if (
|
|
1408
|
+
// running row names whoever holds that id now.
|
|
1409
|
+
if (ownership !== "live") {
|
|
1372
1410
|
console.log(`${name}: not running (start with: hive start "${name}")`);
|
|
1373
1411
|
return null;
|
|
1374
1412
|
}
|
|
@@ -1450,8 +1488,10 @@ function cmdLeadPaneExited(argv) {
|
|
|
1450
1488
|
}
|
|
1451
1489
|
// Only a lead that CREATED its pane clears leftovers. An adopted live lead is the same lead those
|
|
1452
1490
|
// processes belong to, and stopping them there would take down a running crew's dev server.
|
|
1453
|
-
function clearLeftoverProcesses(project, config) {
|
|
1491
|
+
function clearLeftoverProcesses(project, config, leftoverIds) {
|
|
1454
1492
|
for (const row of runningCommandRows(project.id)) {
|
|
1493
|
+
if (!leftoverIds.has(row.id))
|
|
1494
|
+
continue;
|
|
1455
1495
|
const stopped = stopProcess(row, STOP_REASONS.previousLead);
|
|
1456
1496
|
// Nothing was running under it, so the auto-start line that follows is the whole story.
|
|
1457
1497
|
if (stopped.leg === "already-gone")
|
|
@@ -1550,9 +1590,17 @@ function cmdHide(argv) {
|
|
|
1550
1590
|
return `${name}: hidden`;
|
|
1551
1591
|
}));
|
|
1552
1592
|
}
|
|
1593
|
+
const LEAD_OWNERSHIP_LABEL = {
|
|
1594
|
+
live: "running",
|
|
1595
|
+
gone: "no live pane",
|
|
1596
|
+
reissued: "no live pane",
|
|
1597
|
+
unknown: "pane identity unknown",
|
|
1598
|
+
};
|
|
1553
1599
|
function cmdStatus() {
|
|
1554
1600
|
janitor();
|
|
1555
1601
|
let anyOutput = false;
|
|
1602
|
+
let snapshot;
|
|
1603
|
+
const statusSnapshot = () => (snapshot === undefined ? (snapshot = liveTargets()) : snapshot);
|
|
1556
1604
|
let windows;
|
|
1557
1605
|
let windowsError;
|
|
1558
1606
|
const ownedWindows = () => {
|
|
@@ -1598,7 +1646,9 @@ function cmdStatus() {
|
|
|
1598
1646
|
for (const a of agents) {
|
|
1599
1647
|
const state = a.kind === "agent"
|
|
1600
1648
|
? describeForHuman(deriveProvenance(a, null))
|
|
1601
|
-
:
|
|
1649
|
+
: a.kind === LEAD_KIND
|
|
1650
|
+
? LEAD_OWNERSHIP_LABEL[rowOwnership(a, statusSnapshot())]
|
|
1651
|
+
: (whereIs.get(a.name) ?? "running");
|
|
1602
1652
|
const label = a.kind === "command" ? "cmd " : a.kind === LEAD_KIND ? "lead " : "agent";
|
|
1603
1653
|
console.log(` ${label} ${a.name.padEnd(20)} ${state}`);
|
|
1604
1654
|
if (reportsAgentStateLog(a)) {
|
|
@@ -2153,7 +2203,7 @@ function reportCrewTeardown(projectId) {
|
|
|
2153
2203
|
}
|
|
2154
2204
|
function reportStalledWorkers(projectId) {
|
|
2155
2205
|
const latched = db
|
|
2156
|
-
.prepare(`SELECT name, command, kind, cwd, session_id, transcript_path, agent_state, state_changed_at, tmux_target, tmux_socket
|
|
2206
|
+
.prepare(`SELECT name, command, kind, cwd, session_id, transcript_path, agent_state, state_changed_at, tmux_target, tmux_socket, pane_pid
|
|
2157
2207
|
FROM agents
|
|
2158
2208
|
WHERE project_id = ? AND status = 'running' AND kind = 'agent'
|
|
2159
2209
|
AND agent_state IN ('working', 'waiting') AND state_changed_at IS NOT NULL
|
|
@@ -2164,6 +2214,7 @@ function reportStalledWorkers(projectId) {
|
|
|
2164
2214
|
// never this latch"), and a harness with no transcript to check would have nothing to read.
|
|
2165
2215
|
.filter((row) => reportsAgentStateLog(row) && hasTranscriptSignal(row) && row.session_id !== "");
|
|
2166
2216
|
const stalled = [];
|
|
2217
|
+
let snapshot;
|
|
2167
2218
|
for (const row of latched) {
|
|
2168
2219
|
const latchedFor = ageSecondsSince(row.state_changed_at);
|
|
2169
2220
|
if (latchedFor < STALL_BOUND_SECONDS)
|
|
@@ -2180,7 +2231,9 @@ function reportStalledWorkers(projectId) {
|
|
|
2180
2231
|
if (stale !== "never" && stale.seconds < STALL_BOUND_SECONDS)
|
|
2181
2232
|
continue;
|
|
2182
2233
|
if (row.agent_state === "waiting") {
|
|
2183
|
-
if (
|
|
2234
|
+
if (snapshot === undefined)
|
|
2235
|
+
snapshot = liveTargets();
|
|
2236
|
+
if (rowOwnership(row, snapshot) !== "live")
|
|
2184
2237
|
continue;
|
|
2185
2238
|
const choiceCheck = paneClassifierFor(row.command)?.choiceCheck;
|
|
2186
2239
|
if (!choiceCheck || choiceCheck(row.tmux_target).awaitingChoice !== false)
|
|
@@ -2359,6 +2412,12 @@ function cmdDoctor(argv) {
|
|
|
2359
2412
|
warn("profile vars", `hive.yml vars ${collisions.join(", ")} are derived from agents: and are ignored.`);
|
|
2360
2413
|
}
|
|
2361
2414
|
const vars = mergedProjectVars(cfg);
|
|
2415
|
+
for (const f of resolved) {
|
|
2416
|
+
const bytes = profileRenderedBytes(name, f.file, vars);
|
|
2417
|
+
info("profile size", bytes == null ? `${name}/${f.file}: rendered size unavailable` : `${name}/${f.file}: ${bytes} rendered bytes`);
|
|
2418
|
+
if (bytes != null && bytes > PROFILE_SIZE_WARNING_BYTES)
|
|
2419
|
+
info("profile size warning", profileSizeAdvisory(name, f.file));
|
|
2420
|
+
}
|
|
2362
2421
|
const renderedText = profileFileNames(name)
|
|
2363
2422
|
.map((file) => renderProfileFile(name, file, vars))
|
|
2364
2423
|
.filter((t) => t != null)
|
|
@@ -2408,21 +2467,36 @@ function cmdDoctor(argv) {
|
|
|
2408
2467
|
});
|
|
2409
2468
|
if (here) {
|
|
2410
2469
|
const lead = db
|
|
2411
|
-
.prepare("SELECT tmux_target, tmux_socket FROM agents WHERE project_id = ? AND kind = ? AND status = 'running' ORDER BY id")
|
|
2470
|
+
.prepare("SELECT id, tmux_target, tmux_socket, pane_pid FROM agents WHERE project_id = ? AND kind = ? AND status = 'running' ORDER BY id")
|
|
2412
2471
|
.get(here.id, LEAD_KIND);
|
|
2413
2472
|
if (lead) {
|
|
2414
|
-
const
|
|
2415
|
-
|
|
2416
|
-
|
|
2473
|
+
const snapshot = liveTargets();
|
|
2474
|
+
const ownership = rowOwnership(lead, snapshot);
|
|
2475
|
+
const unobserved = !foreignSocket(lead.tmux_socket) && observationFailed(snapshot);
|
|
2476
|
+
if (ownership === "gone" || ownership === "reissued") {
|
|
2477
|
+
warn("lead", (ownership === "reissued"
|
|
2478
|
+
? "the lead's row is running but its pane id now belongs to a different process. "
|
|
2479
|
+
: "the lead's row is running but its pane is not live. ") +
|
|
2480
|
+
"The janitor leaves lead rows alone on " +
|
|
2417
2481
|
"purpose, so nothing will fix this by itself. Run `hive lead` to record a fresh pane and reuse " +
|
|
2418
2482
|
"this identity, or ask a claude session connected to this project's hive MCP server to call " +
|
|
2419
|
-
|
|
2483
|
+
(ownership === "reissued"
|
|
2484
|
+
? `agent_close({agent_id: ${lead.id}, row_only: true}) on it (plain agent_close refuses a pane id ` +
|
|
2485
|
+
"that still exists)"
|
|
2486
|
+
: "the agent_close tool on it") +
|
|
2487
|
+
" to retire the row for good - required before `hive restore`, which " +
|
|
2420
2488
|
"otherwise refuses while any lead row reads running, and end that session too, since it also " +
|
|
2421
2489
|
"holds this store open.");
|
|
2422
2490
|
}
|
|
2423
|
-
else if (
|
|
2491
|
+
else if (ownership === "unknown" && unobserved) {
|
|
2424
2492
|
warn("lead", "the lead's pane liveness could not be probed (tmux did not answer).");
|
|
2425
2493
|
}
|
|
2494
|
+
else if (ownership === "unknown") {
|
|
2495
|
+
warn("lead", `pane identity unknown: the lead's row (agents.id ${lead.id}) cannot be verified as owning pane ` +
|
|
2496
|
+
`${lead.tmux_target || "(none)"}, because it is on another tmux socket or no pane pid was recorded ` +
|
|
2497
|
+
"for it. Its screen was not read. If this lead is gone, a human or peer lead (never a worker) can " +
|
|
2498
|
+
`retire the row with the MCP tool agent_close({agent_id: ${lead.id}, row_only: true}).`);
|
|
2499
|
+
}
|
|
2426
2500
|
}
|
|
2427
2501
|
const stuck = db
|
|
2428
2502
|
.prepare("SELECT name, kind, tmux_socket FROM agents WHERE project_id = ? AND status = 'running' AND kind != ?")
|
|
@@ -2441,19 +2515,21 @@ function cmdDoctor(argv) {
|
|
|
2441
2515
|
}
|
|
2442
2516
|
if (here) {
|
|
2443
2517
|
const workers = db
|
|
2444
|
-
.prepare("SELECT name, actor_id, command, kind, tmux_target, tmux_socket FROM agents WHERE project_id = ? AND status = 'running' AND kind = 'agent' ORDER BY id")
|
|
2518
|
+
.prepare("SELECT name, actor_id, command, kind, tmux_target, tmux_socket, pane_pid FROM agents WHERE project_id = ? AND status = 'running' AND kind = 'agent' ORDER BY id")
|
|
2445
2519
|
.all(here.id);
|
|
2520
|
+
const paneSnapshot = workers.length > 0 ? liveTargets() : null;
|
|
2446
2521
|
let inputBoxClean = 0;
|
|
2447
2522
|
let inputBoxDrifted = 0;
|
|
2448
2523
|
let inputBoxUnclassified = 0;
|
|
2449
2524
|
let leadsProbed = 0;
|
|
2450
2525
|
let workersProbed = 0;
|
|
2451
2526
|
const leadRows = db
|
|
2452
|
-
.prepare("SELECT name, command, tmux_target, tmux_socket FROM agents WHERE project_id = ? AND kind = ? AND status = 'running' ORDER BY id")
|
|
2527
|
+
.prepare("SELECT name, command, tmux_target, tmux_socket, pane_pid FROM agents WHERE project_id = ? AND kind = ? AND status = 'running' ORDER BY id")
|
|
2453
2528
|
.all(here.id, LEAD_KIND);
|
|
2529
|
+
const leadSnapshot = leadRows.length > 0 ? (paneSnapshot ?? liveTargets()) : null;
|
|
2454
2530
|
for (const leadBox of leadRows) {
|
|
2455
2531
|
const leadHarness = harnessFor(leadBox.command);
|
|
2456
|
-
if (!leadHarness.classifiesPaneScreen ||
|
|
2532
|
+
if (!leadHarness.classifiesPaneScreen || rowOwnership(leadBox, leadSnapshot) !== "live")
|
|
2457
2533
|
continue;
|
|
2458
2534
|
leadsProbed += 1;
|
|
2459
2535
|
const box = leadHarness.paneClassifier.inputBoxState(leadBox.tmux_target);
|
|
@@ -2478,20 +2554,26 @@ function cmdDoctor(argv) {
|
|
|
2478
2554
|
workersReported += 1;
|
|
2479
2555
|
// Dispatched through w's OWN harness, never the claude-only import - reading a codex pane
|
|
2480
2556
|
// with claude's dialog regexes fails silently in either direction (.claude/rules/tmux-and-panes.md).
|
|
2557
|
+
const ownership = rowOwnership(w, paneSnapshot);
|
|
2481
2558
|
const foreign = foreignSocket(w.tmux_socket);
|
|
2559
|
+
const unowned = ownership !== "live";
|
|
2482
2560
|
const workerChoiceCheck = paneClassifierFor(w.command)?.choiceCheck;
|
|
2483
|
-
const { awaitingChoice, tail } =
|
|
2561
|
+
const { awaitingChoice, tail } = unowned || !workerChoiceCheck ? { awaitingChoice: null, tail: "" } : workerChoiceCheck(w.tmux_target);
|
|
2484
2562
|
if (awaitingChoice === true)
|
|
2485
2563
|
dialogCount += 1;
|
|
2486
2564
|
verboseInfo("worker live state", `worker ${w.name}`, `last log event: ${describeLastLogEvent(lastLogEvent(w.actor_id))}`, `permission mode: ${lastPermissionMode(w.actor_id) ?? "unknown (no record)"}`, foreign
|
|
2487
2565
|
? `pane: recorded on a different tmux socket (${w.tmux_socket}); this process cannot read it`
|
|
2488
|
-
:
|
|
2566
|
+
: unowned
|
|
2567
|
+
? `pane: not read - pane ownership ${ownership}`
|
|
2568
|
+
: `pane: ${describePaneChoice(awaitingChoice)}`, ...(foreign
|
|
2489
2569
|
? ["tail: (not read - foreign socket)"]
|
|
2490
|
-
:
|
|
2491
|
-
? [
|
|
2492
|
-
:
|
|
2493
|
-
? ["tail: (pane
|
|
2494
|
-
:
|
|
2570
|
+
: unowned
|
|
2571
|
+
? [`tail: (not read - pane ownership ${ownership})`]
|
|
2572
|
+
: awaitingChoice === null
|
|
2573
|
+
? ["tail: (pane could not be read)"]
|
|
2574
|
+
: tail === ""
|
|
2575
|
+
? ["tail: (pane rendered nothing)"]
|
|
2576
|
+
: ["tail:", ...tail.split("\n").map((line) => `| ${line}`)]));
|
|
2495
2577
|
}
|
|
2496
2578
|
// Gated on classifiesPaneScreen, not reportsAgentStateLog: this drift check only needs the pane,
|
|
2497
2579
|
// and a codex worker's pane has been readable since todo 524, independent of whether its
|
|
@@ -2499,7 +2581,7 @@ function cmdDoctor(argv) {
|
|
|
2499
2581
|
// own harness, never the claude-only import above, or a codex worker's screen gets read with
|
|
2500
2582
|
// claude's regexes (see hive-internals).
|
|
2501
2583
|
const harness = harnessFor(w.command);
|
|
2502
|
-
if (!harness.classifiesPaneScreen ||
|
|
2584
|
+
if (!harness.classifiesPaneScreen || rowOwnership(w, paneSnapshot) !== "live")
|
|
2503
2585
|
continue;
|
|
2504
2586
|
workersProbed += 1;
|
|
2505
2587
|
const box = harness.paneClassifier.inputBoxState(w.tmux_target);
|
|
@@ -2735,7 +2817,8 @@ export function heldReasonLabel(heldReason) {
|
|
|
2735
2817
|
if (isUnsubmittedInputHold(heldReason))
|
|
2736
2818
|
return "typing";
|
|
2737
2819
|
if (heldReason === HELD_REASON_LEAD_PANE_DEAD ||
|
|
2738
|
-
|
|
2820
|
+
wasHeldForPaneIdentity(heldReason) ||
|
|
2821
|
+
isLeadRowClosedHold(heldReason) ||
|
|
2739
2822
|
isUnclassifiablePaneHold(heldReason)) {
|
|
2740
2823
|
return "needs you";
|
|
2741
2824
|
}
|
|
@@ -24,7 +24,7 @@ export function contextCheckpointAdditionalContext(actorId, thresholdPercent, fi
|
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
export function runContextCheckpointHook(kind) {
|
|
27
|
+
export function runContextCheckpointHook(kind, suppliedPayload) {
|
|
28
28
|
const thresholdText = process.env.HIVE_CONTEXT_CHECKPOINT_PERCENT;
|
|
29
29
|
if (!thresholdText || !/^\d+$/.test(thresholdText))
|
|
30
30
|
return;
|
|
@@ -35,7 +35,7 @@ export function runContextCheckpointHook(kind) {
|
|
|
35
35
|
if (!actorId.startsWith("agent:") || (kind !== "claude" && kind !== "codex"))
|
|
36
36
|
return;
|
|
37
37
|
try {
|
|
38
|
-
const payload = JSON.parse(readFileSync(0, "utf8"));
|
|
38
|
+
const payload = suppliedPayload === undefined ? JSON.parse(readFileSync(0, "utf8")) : suppliedPayload;
|
|
39
39
|
if (payload === null || typeof payload !== "object" || Array.isArray(payload))
|
|
40
40
|
return;
|
|
41
41
|
const fill = readContextFill(kind, {
|
package/dist/help.js
CHANGED
|
@@ -175,7 +175,14 @@ runbook (fork it first), not a pad write.`,
|
|
|
175
175
|
agent_close(name|agent_id) — kill the worker's pane and mark closed. Capture
|
|
176
176
|
handoffs first; output is not retained. Self-close needs confirm_self.
|
|
177
177
|
On a PARKED row (by agent_id) it releases the park instead, which is how
|
|
178
|
-
you abandon a lane you have decided not to resume.
|
|
178
|
+
you abandon a lane you have decided not to resume. row_only=true marks
|
|
179
|
+
a stale row closed with no kill, stop or typing; a human or peer lead
|
|
180
|
+
only, refused for a row that provably owns a live pane or when tmux
|
|
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.
|
|
179
186
|
|
|
180
187
|
Address a worker by its name, not its id: agent_send(name="impl", ...). A
|
|
181
188
|
partial name works when it matches one running worker, so name="123" finds
|
|
@@ -257,8 +264,9 @@ Every pad, todo, kv entry, and lease belongs to one project (a directory).
|
|
|
257
264
|
under HIVE_PROJECT_LOCK=1 since it sweeps every project, not just yours.
|
|
258
265
|
With project_id, removes only that project, and only if it owns no rows.
|
|
259
266
|
With project_id and confirm_name (its exact name), removes it with every
|
|
260
|
-
row it owns after a snapshot; refuses while
|
|
261
|
-
|
|
267
|
+
row it owns after a snapshot; refuses while a running row owns a live
|
|
268
|
+
pane or cannot be verified (agent_close row_only retires the latter).
|
|
269
|
+
From a terminal: hive project rm <id|path> [--yes].
|
|
262
270
|
|
|
263
271
|
Resolution order: explicit project_id argument, then session selection, then
|
|
264
272
|
working-directory auto-detection. When the working directory matches no
|
|
@@ -312,7 +320,11 @@ place.`,
|
|
|
312
320
|
pad_write(name, content, tags?) — create a pad; names are unique per project
|
|
313
321
|
pad_write(pad_id, name, content, expected_revision) — full overwrite; the
|
|
314
322
|
revision guard makes concurrent writers safe
|
|
315
|
-
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
|
|
316
328
|
pad_append(pad_id, content, expected_revision?) — add to the end
|
|
317
329
|
pad_edit(pad_id, old_text, new_text, expected_revision?) — replace one
|
|
318
330
|
unique literal occurrence; include enough context to disambiguate
|