@cmgmyr/hive 1.11.3 → 1.11.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/dist/abi.js +9 -7
- package/dist/build-info.json +1 -1
- package/dist/cli.js +53 -30
- package/dist/codexHome.js +0 -1
- package/dist/context.js +13 -24
- package/dist/crew.js +6 -23
- package/dist/dashboard.js +11 -14
- package/dist/db.js +36 -0
- package/dist/dispatcher.js +4 -1
- package/dist/git.js +16 -0
- package/dist/harnesses.js +0 -3
- package/dist/help.js +20 -14
- package/dist/kickoff.js +4 -13
- package/dist/kvValue.js +8 -0
- package/dist/leadHandoffState.js +1 -1
- package/dist/portfolio.js +2 -2
- package/dist/processes.js +12 -0
- package/dist/scheduler.js +77 -37
- package/dist/spawn.js +10 -29
- package/dist/todoScope.js +5 -0
- package/dist/tools/agents.js +20 -18
- package/dist/tools/kv.js +6 -5
- package/dist/tools/todos.js +3 -8
- package/dist/version.js +4 -17
- package/package.json +1 -1
package/dist/abi.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import { cliPath, dispatcherPath, readDispatcher } from "./dispatcher.js";
|
|
4
|
+
import { cliPath, dispatcherPath, readDispatcher, repinCommand } from "./dispatcher.js";
|
|
5
5
|
const requireFromHere = createRequire(import.meta.url);
|
|
6
6
|
export function classifyAddonLoadError(error) {
|
|
7
7
|
return /NODE_MODULE_VERSION \d+|Module did not self-register/i.test(error) ? "mismatch" : "load";
|
|
@@ -129,7 +129,7 @@ export function abiFixLines(status, pinned) {
|
|
|
129
129
|
if (status.failure === "napi") {
|
|
130
130
|
const need = status.nodeApiRequired;
|
|
131
131
|
const range = need === null ? null : nodeRangeForNodeApi(need);
|
|
132
|
-
const good = pinned
|
|
132
|
+
const good = pinned ?? `<a Node matching ${range ?? "that Node-API level"}>`;
|
|
133
133
|
return [
|
|
134
134
|
`better-sqlite3's addon is built against Node-API ${need}${range ? `, which only Node ${range} provides` : ""}.`,
|
|
135
135
|
`This interpreter provides Node-API ${describeNodeApi(status.nodeApi)}, so the addon cannot load here.`,
|
|
@@ -141,7 +141,7 @@ export function abiFixLines(status, pinned) {
|
|
|
141
141
|
"",
|
|
142
142
|
`Run hive under a Node matching ${range ?? "that Node-API level"}, and re-pin the \`hive\` command`,
|
|
143
143
|
"to it (setup pins whatever Node runs it, so it has to be that one):",
|
|
144
|
-
` ${good
|
|
144
|
+
` ${repinCommand(good, cliPath())}`,
|
|
145
145
|
];
|
|
146
146
|
}
|
|
147
147
|
if (status.failure === "missing") {
|
|
@@ -164,7 +164,7 @@ export function abiFixLines(status, pinned) {
|
|
|
164
164
|
if (status.failure !== "mismatch") {
|
|
165
165
|
return ["Build it: npm install && npm run build"];
|
|
166
166
|
}
|
|
167
|
-
const good = pinned
|
|
167
|
+
const good = pinned ?? "<the Node that built it>";
|
|
168
168
|
return [
|
|
169
169
|
"better-sqlite3's addon is ABI-locked to the Node that compiled it, and a Node",
|
|
170
170
|
"version manager resolves `node` from the working directory, so a `cd` is enough",
|
|
@@ -172,10 +172,12 @@ export function abiFixLines(status, pinned) {
|
|
|
172
172
|
"",
|
|
173
173
|
"Run hive under the Node it was built for, and re-pin the `hive` command to it",
|
|
174
174
|
"(setup pins whatever Node runs it, so it has to be that one):",
|
|
175
|
-
` ${good
|
|
175
|
+
` ${repinCommand(good, cliPath())}`,
|
|
176
176
|
"",
|
|
177
|
-
"
|
|
178
|
-
"
|
|
177
|
+
"Rebuilding is not the fix for the supported addon. Update hive, then re-pin this CLI:",
|
|
178
|
+
" Global install: npm install -g @cmgmyr/hive",
|
|
179
|
+
" Checkout: npm install && npm run build",
|
|
180
|
+
` ${repinCommand(good, cliPath())}`,
|
|
179
181
|
];
|
|
180
182
|
}
|
|
181
183
|
function workingInterpreterFromDispatcher() {
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.11.
|
|
1
|
+
{"version":"1.11.5","sha":"486161d","dirty":false,"build_id":"3a33da919c1ac52f423bfdb530cfeda01207921e9c0fb8b262462fae1c266b12"}
|
package/dist/cli.js
CHANGED
|
@@ -3,14 +3,14 @@ import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { chmodSync, existsSync, linkSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
5
5
|
import { homedir, tmpdir } from "node:os";
|
|
6
|
-
import { join, resolve, sep } from "node:path";
|
|
6
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
7
7
|
import { createInterface } from "node:readline/promises";
|
|
8
8
|
import { pathToFileURL } from "node:url";
|
|
9
9
|
import { checkAbi, describeAbi, describeInterpreter, nodeRangeForNodeApi, requiredNodeApi } from "./abi.js";
|
|
10
10
|
import { versionInfo } from "./version.js";
|
|
11
11
|
import { claudeConfigDir } from "./claudeDir.js";
|
|
12
12
|
import { ATTACH_MODES, AUTO_ATTACH_MODES, attachMode, isAttachMode, isAutoAttach, migrateLegacyConfig, resolvedAutoAttach, setAttachMode, setAutoAttach, } from "./config.js";
|
|
13
|
-
import { cliPath, dispatcherDir, dispatcherPath, dispatcherScript, durabilityLines, firstHiveOnPath, linkedWorktreePin, pathAdvice, readDispatcher, } from "./dispatcher.js";
|
|
13
|
+
import { cliPath, dispatcherDir, dispatcherPath, dispatcherScript, durabilityLines, firstHiveOnPath, linkedWorktreePin, pathAdvice, repinCommand, readDispatcher, } from "./dispatcher.js";
|
|
14
14
|
import { codexConfigPath, codexHiveRegistrations, codexRegistrationProblem, hiveRegistrations, registrationOffer, registrationProblem, } from "./mcpConfig.js";
|
|
15
15
|
import { DEFAULT_DATA_DIR, storeDir } from "./dataDir.js";
|
|
16
16
|
import { dataDir, db, migrate, storeSchemaAhead } from "./db.js";
|
|
@@ -36,7 +36,7 @@ import { asNameClash, buildEnvFlags, closeAgentRow, isReservedAgentName, launchA
|
|
|
36
36
|
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";
|
|
37
37
|
import { collectCrew, crewStoreProblem } from "./crew.js";
|
|
38
38
|
import { describeVisibility, formatLocal, processCounts } from "./dashboard.js";
|
|
39
|
-
import { runningCommandRow, runningCommandRows, snapshotProcesses, stopAllProcesses, stopLine, stopProcess, STOP_REASONS, } from "./processes.js";
|
|
39
|
+
import { runningCommandRow, runningCommandRows, snapshotProcesses, stopAllProcesses, stopLine, stopProcess, renderStart, STOP_REASONS, } from "./processes.js";
|
|
40
40
|
import { activeProfile, agentVarKeys, BUILT_IN_PROJECT_YML, configHash, loadProjectYml, mergedProjectVars, NO_PROFILE, PROJECT_YML_KEYS, resolveCommandDir, resolveFirstMessage, } from "./projectYml.js";
|
|
41
41
|
import { appendClaudeLeadArgs, crewPluginDir, isTrusted, leadIdentityEnv, renderLeadPosture } from "./leadLaunch.js";
|
|
42
42
|
import { handoffStatusSegment } from "./leadHandoff.js";
|
|
@@ -47,7 +47,8 @@ import { FIRST_MESSAGE_SHA_ENV, firstMessageDigest } from "./firstMessage.js";
|
|
|
47
47
|
import { ageSecondsSince, deriveProvenance, describeForHuman, describeLastLogEvent, humanizeAge, lastLogEvent, lastPermissionMode, reportsAgentStateLog, } from "./stateProvenance.js";
|
|
48
48
|
import { awaitingFirstPromptSql } from "./firstPrompt.js";
|
|
49
49
|
import { checkoutRoot, createProfile, forkProfile, PROFILE_FILES, ProfileError, profileExists, profileFileNames, profileNames, profileStatus, readProfileFile, referencedPads, referencedPaths, renderProfileFile, resolveProfileFile, REWRITE_THRESHOLD, templateVars, userProfilesDir, } from "./profiles.js";
|
|
50
|
-
import { getTodoDetail, listTodoSummaries,
|
|
50
|
+
import { getTodoDetail, listTodoSummaries, TODO_STATUSES, } from "./tools/todos.js";
|
|
51
|
+
import { ACTIVE_TODO_SQL, OPEN_BLOCKERS_SQL } from "./todoScope.js";
|
|
51
52
|
import { createPad, getActivePadByName, listActivePads, overwritePadContent, } from "./tools/pads.js";
|
|
52
53
|
import { answerQuestion, listQuestions } from "./questions.js";
|
|
53
54
|
function usage() {
|
|
@@ -213,9 +214,15 @@ async function ensureTrusted(projectId, name, command, dir, env) {
|
|
|
213
214
|
db.prepare("INSERT OR IGNORE INTO command_trust (project_id, name, config_hash) VALUES (?, ?, ?)").run(projectId, name, hash);
|
|
214
215
|
return true;
|
|
215
216
|
}
|
|
216
|
-
function startYmlCommand(project, name, proc, config) {
|
|
217
|
+
function startYmlCommand(project, name, proc, config, trusted) {
|
|
218
|
+
if (!trusted)
|
|
219
|
+
return { status: "refused", reason: "untrusted" };
|
|
217
220
|
if (isReservedAgentName(name)) {
|
|
218
|
-
return
|
|
221
|
+
return {
|
|
222
|
+
status: "refused",
|
|
223
|
+
reason: "reserved-name",
|
|
224
|
+
message: `skipped: "lead" is reserved for this project's lead session and cannot be used as a process name`,
|
|
225
|
+
};
|
|
219
226
|
}
|
|
220
227
|
const existing = db
|
|
221
228
|
.prepare("SELECT id, tmux_target, tmux_socket FROM agents WHERE project_id = ? AND name = ? AND status = 'running'")
|
|
@@ -225,10 +232,15 @@ function startYmlCommand(project, name, proc, config) {
|
|
|
225
232
|
if (live) {
|
|
226
233
|
const windows = projectWindows(sessionName(), project.id);
|
|
227
234
|
const where = windows === null ? null : paneVisibility(existing.tmux_target, windows);
|
|
228
|
-
return where === "shown" || where === "hidden" ?
|
|
235
|
+
return { status: "already-running", location: where === "shown" || where === "hidden" ? where : null };
|
|
236
|
+
}
|
|
237
|
+
if (live === null) {
|
|
238
|
+
return {
|
|
239
|
+
status: "refused",
|
|
240
|
+
reason: "unknown-liveness",
|
|
241
|
+
message: "skipped: tmux could not be probed, so hive cannot tell whether it is already running",
|
|
242
|
+
};
|
|
229
243
|
}
|
|
230
|
-
if (live === null)
|
|
231
|
-
return "skipped: tmux could not be probed, so hive cannot tell whether it is already running";
|
|
232
244
|
closeAgentRow(existing.id);
|
|
233
245
|
}
|
|
234
246
|
let dir;
|
|
@@ -236,7 +248,7 @@ function startYmlCommand(project, name, proc, config) {
|
|
|
236
248
|
dir = resolveCommandDir(project.path, proc.dir);
|
|
237
249
|
}
|
|
238
250
|
catch (e) {
|
|
239
|
-
return `skipped: ${errorMessage(e)}
|
|
251
|
+
return { status: "refused", reason: "escaped-dir", message: `skipped: ${errorMessage(e)}` };
|
|
240
252
|
}
|
|
241
253
|
const placement = proc.visible
|
|
242
254
|
? (config.placement ?? (process.env.HIVE_SPAWN_PLACEMENT === "window" ? "window" : "split"))
|
|
@@ -260,17 +272,20 @@ function startYmlCommand(project, name, proc, config) {
|
|
|
260
272
|
setPaneTitle(target, shownPaneTitle(project.name, name));
|
|
261
273
|
if (landedInProjectId != null) {
|
|
262
274
|
const landedName = getProject(landedInProjectId)?.name ?? `project ${landedInProjectId}`;
|
|
263
|
-
return `started, landed in project "${landedName}"'s window
|
|
275
|
+
return { status: "started", message: `started, landed in project "${landedName}"'s window` };
|
|
264
276
|
}
|
|
265
|
-
return "started";
|
|
277
|
+
return { status: "started", message: "started" };
|
|
266
278
|
}
|
|
267
|
-
return
|
|
268
|
-
|
|
269
|
-
:
|
|
270
|
-
|
|
279
|
+
return {
|
|
280
|
+
status: "started",
|
|
281
|
+
message: inProcessesWindow
|
|
282
|
+
? "started (hidden)"
|
|
283
|
+
: "started in its own window: it opened this project's tmux session, and a session's first window " +
|
|
284
|
+
`cannot be a tile. Move it with: hive hide "${name}"`,
|
|
285
|
+
};
|
|
271
286
|
}
|
|
272
287
|
catch (e) {
|
|
273
|
-
return
|
|
288
|
+
return { status: "failed", message: errorMessage(e) };
|
|
274
289
|
}
|
|
275
290
|
}
|
|
276
291
|
function attach(session, project, window) {
|
|
@@ -636,9 +651,10 @@ async function cmdLead(argv) {
|
|
|
636
651
|
console.log(`- ${name}: defined, auto_start off (start with: hive start "${name}")`);
|
|
637
652
|
continue;
|
|
638
653
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
654
|
+
const trusted = await ensureTrusted(project.id, name, proc.command, proc.dir, proc.env);
|
|
655
|
+
const result = startYmlCommand(project, name, proc, config, trusted);
|
|
656
|
+
if (trusted)
|
|
657
|
+
console.log(`- ${name}: ${renderStart(result)}`);
|
|
642
658
|
}
|
|
643
659
|
}
|
|
644
660
|
if (registrationNotice) {
|
|
@@ -1366,9 +1382,12 @@ async function cmdStart(argv) {
|
|
|
1366
1382
|
console.log(processNotDefined(config, name));
|
|
1367
1383
|
process.exit(1);
|
|
1368
1384
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1385
|
+
const trusted = await ensureTrusted(project.id, name, proc.command, proc.dir, proc.env);
|
|
1386
|
+
const result = startYmlCommand(project, name, proc, config, trusted);
|
|
1387
|
+
if (trusted)
|
|
1388
|
+
console.log(`${name}: ${renderStart(result)}`);
|
|
1389
|
+
if (!trusted || result.status === "failed" || result.status === "refused")
|
|
1390
|
+
process.exitCode = 1;
|
|
1372
1391
|
}
|
|
1373
1392
|
function processNotDefined(config, name) {
|
|
1374
1393
|
const known = Object.keys(config?.processes ?? {});
|
|
@@ -1631,7 +1650,7 @@ function cmdStatus() {
|
|
|
1631
1650
|
.prepare("SELECT * FROM agents WHERE project_id = ? AND status = 'running' ORDER BY kind DESC, id")
|
|
1632
1651
|
.all(project.id);
|
|
1633
1652
|
const todos = db
|
|
1634
|
-
.prepare(
|
|
1653
|
+
.prepare(`SELECT COUNT(*) AS n FROM todos t WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}`)
|
|
1635
1654
|
.get(project.id).n;
|
|
1636
1655
|
const { wakes, heldWakes } = db
|
|
1637
1656
|
.prepare(`SELECT COUNT(*) AS wakes, COUNT(held_at) AS heldWakes FROM wakes WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE}`)
|
|
@@ -1889,7 +1908,7 @@ function reportDispatcher() {
|
|
|
1889
1908
|
}
|
|
1890
1909
|
info("dispatcher", `${dispatcher.file} -> ${dispatcher.node ?? "an exec line hive cannot parse"}`);
|
|
1891
1910
|
if (!dispatcher.node || !dispatcher.cli) {
|
|
1892
|
-
gatingWarn("dispatcher", "written by another version of hive; re-
|
|
1911
|
+
gatingWarn("dispatcher", "written by another version of hive; use this re-pin command to refresh it:", ` ${repinCommand(process.execPath, cliPath())}`);
|
|
1893
1912
|
}
|
|
1894
1913
|
else if (!existsSync(dispatcher.node)) {
|
|
1895
1914
|
const need = requiredNodeApi();
|
|
@@ -1907,7 +1926,7 @@ function reportDispatcher() {
|
|
|
1907
1926
|
gatingWarn("dispatcher", `pinned to a linked git worktree, which this repo tears down: ${dispatcher.cli}`, "A worktree teardown leaves `hive` failing with MODULE_NOT_FOUND, and running `hive setup`", "to repair it just re-pins the same worktree, since that shim is what runs the command.", `Re-pin from the durable checkout instead: "${dispatcher.node}" "${repairCli}" setup`);
|
|
1908
1927
|
}
|
|
1909
1928
|
else if (dispatcher.node !== process.execPath || dispatcher.cli !== cliPath()) {
|
|
1910
|
-
gatingWarn("dispatcher", "pinned to a different build than this CLI is running.", `this run: ${process.execPath} ${cliPath()}`,
|
|
1929
|
+
gatingWarn("dispatcher", "pinned to a different build than this CLI is running.", `this run: ${process.execPath} ${cliPath()}`, `Re-pin after a rebuild: ${repinCommand(process.execPath, cliPath())}`);
|
|
1911
1930
|
}
|
|
1912
1931
|
if (onPath !== dispatcher.file)
|
|
1913
1932
|
gatingWarn("dispatcher", ...pathAdvice(dispatcherDir(), dispatcher.file));
|
|
@@ -2005,8 +2024,12 @@ function reportPtyHeadroom(orphans) {
|
|
|
2005
2024
|
const scratchHeld = orphans ? orphans.live + orphans.wedged : 0;
|
|
2006
2025
|
const livePanes = db.prepare("SELECT COUNT(*) AS n FROM agents WHERE status = 'running'").get()
|
|
2007
2026
|
.n;
|
|
2008
|
-
warn("ptys", `${headroom.allocated} of ${headroom.max} in use (${free} free), below the safety margin${overrideNote}`, shellsLine, `${scratchHeld} orphaned scratch tmux server(s) still holding a socket (detail in the scratch tmux servers check)`, `${livePanes} pane(s) recorded 'running' in hive's own store (leads and workers, this machine)`, "
|
|
2009
|
-
|
|
2027
|
+
warn("ptys", `${headroom.allocated} of ${headroom.max} in use (${free} free), below the safety margin${overrideNote}`, shellsLine, `${scratchHeld} orphaned scratch tmux server(s) still holding a socket (detail in the scratch tmux servers check)`, `${livePanes} pane(s) recorded 'running' in hive's own store (leads and workers, this machine)`, ...(existsSync(join(dirname(cliPath()), "..", "scripts", "sweep-scratch.mjs"))
|
|
2028
|
+
? [
|
|
2029
|
+
"reap the orphaned shells and scratch servers with `node scripts/sweep-scratch.mjs` (dry run by default, " +
|
|
2030
|
+
"`--kill` to act)",
|
|
2031
|
+
]
|
|
2032
|
+
: []));
|
|
2010
2033
|
}
|
|
2011
2034
|
function reportOrphanTmuxServers(orphans) {
|
|
2012
2035
|
if (!orphans)
|
|
@@ -2765,9 +2788,9 @@ function cmdStatusline() {
|
|
|
2765
2788
|
const count = (sql) => db.prepare(sql).get(project.id).n;
|
|
2766
2789
|
const agents = count("SELECT COUNT(*) AS n FROM agents WHERE project_id = ? AND status = 'running' AND kind = 'agent'");
|
|
2767
2790
|
const commands = count("SELECT COUNT(*) AS n FROM agents WHERE project_id = ? AND status = 'running' AND kind = 'command'");
|
|
2768
|
-
const todos = count(
|
|
2791
|
+
const todos = count(`SELECT COUNT(*) AS n FROM todos t WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}`);
|
|
2769
2792
|
const ready = count(`SELECT COUNT(*) AS n FROM todos t
|
|
2770
|
-
WHERE t.project_id = ? AND
|
|
2793
|
+
WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}
|
|
2771
2794
|
AND NOT EXISTS (${OPEN_BLOCKERS_SQL})`);
|
|
2772
2795
|
const pads = count("SELECT COUNT(*) AS n FROM pads WHERE project_id = ? AND archived = 0");
|
|
2773
2796
|
const wakes = count(`SELECT COUNT(*) AS n FROM wakes WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE}`);
|
package/dist/codexHome.js
CHANGED
package/dist/context.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
1
|
import { lstatSync, realpathSync } from "node:fs";
|
|
3
2
|
import { userInfo } from "node:os";
|
|
4
3
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
@@ -7,6 +6,7 @@ import { db } from "./db.js";
|
|
|
7
6
|
import { LEAD_KIND } from "./spawn.js";
|
|
8
7
|
import { rowOwnership } from "./tmux.js";
|
|
9
8
|
import { recordQueenRefusal } from "./queenAudit.js";
|
|
9
|
+
import { gitOut } from "./git.js";
|
|
10
10
|
let selectedId = null;
|
|
11
11
|
let cachedActorId = null;
|
|
12
12
|
let lastTouchMs = 0;
|
|
@@ -92,18 +92,13 @@ export function registeredAncestor(dir) {
|
|
|
92
92
|
return bestPrefixMatch(projects, dir);
|
|
93
93
|
}
|
|
94
94
|
export function gitPrimaryRoot(dir) {
|
|
95
|
+
const commonDir = gitOut(dir, ["rev-parse", "--git-common-dir"]);
|
|
96
|
+
if (commonDir === null)
|
|
97
|
+
return null;
|
|
98
|
+
const resolvedCommonDir = resolve(dir, commonDir);
|
|
99
|
+
if (basename(resolvedCommonDir) !== ".git")
|
|
100
|
+
return null;
|
|
95
101
|
try {
|
|
96
|
-
const { GIT_DIR: _gitDir, GIT_COMMON_DIR: _gitCommonDir, GIT_WORK_TREE: _gitWorkTree, ...env } = process.env;
|
|
97
|
-
const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
98
|
-
cwd: dir,
|
|
99
|
-
encoding: "utf8",
|
|
100
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
101
|
-
timeout: 2000,
|
|
102
|
-
env,
|
|
103
|
-
}).trim();
|
|
104
|
-
const resolvedCommonDir = resolve(dir, commonDir);
|
|
105
|
-
if (basename(resolvedCommonDir) !== ".git")
|
|
106
|
-
return null;
|
|
107
102
|
return realpathSync(dirname(resolvedCommonDir));
|
|
108
103
|
}
|
|
109
104
|
catch {
|
|
@@ -111,19 +106,13 @@ export function gitPrimaryRoot(dir) {
|
|
|
111
106
|
}
|
|
112
107
|
}
|
|
113
108
|
function gitDirs(dir) {
|
|
109
|
+
const result = gitOut(dir, ["rev-parse", "--git-dir", "--git-common-dir"]);
|
|
110
|
+
if (result === null)
|
|
111
|
+
return null;
|
|
112
|
+
const out = result.split("\n");
|
|
113
|
+
if (out.length !== 2)
|
|
114
|
+
return null;
|
|
114
115
|
try {
|
|
115
|
-
const { GIT_DIR: _gitDir, GIT_COMMON_DIR: _gitCommonDir, GIT_WORK_TREE: _gitWorkTree, ...env } = process.env;
|
|
116
|
-
const out = execFileSync("git", ["rev-parse", "--git-dir", "--git-common-dir"], {
|
|
117
|
-
cwd: dir,
|
|
118
|
-
encoding: "utf8",
|
|
119
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
120
|
-
timeout: 2000,
|
|
121
|
-
env,
|
|
122
|
-
})
|
|
123
|
-
.trim()
|
|
124
|
-
.split("\n");
|
|
125
|
-
if (out.length !== 2)
|
|
126
|
-
return null;
|
|
127
116
|
return {
|
|
128
117
|
gitDir: realpathSync(resolve(dir, out[0])),
|
|
129
118
|
commonDir: realpathSync(resolve(dir, out[1])),
|
package/dist/crew.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
1
|
import { db } from "./db.js";
|
|
3
2
|
import { isLinkedWorktree } from "./context.js";
|
|
4
3
|
import { awaitingFirstPrompt } from "./firstPrompt.js";
|
|
5
4
|
import { heldReasonLabel } from "./heldLabel.js";
|
|
6
5
|
import { harnessFor, paneClassifierFor } from "./harnesses.js";
|
|
7
6
|
import { NEEDS_HUMAN_TAG } from "./portfolio.js";
|
|
7
|
+
import { ACTIVE_TODO_SQL } from "./todoScope.js";
|
|
8
8
|
import { pendingQuestionCount } from "./questions.js";
|
|
9
9
|
import { loadProjectYml } from "./projectYml.js";
|
|
10
10
|
import { parseTags } from "./result.js";
|
|
11
11
|
import { ACTIVE_TIMER_WHERE } from "./scheduler.js";
|
|
12
12
|
import { reportsAgentStateLog } from "./stateProvenance.js";
|
|
13
|
+
import { gitOut } from "./git.js";
|
|
13
14
|
import { liveTargets, rowOwnership } from "./tmux.js";
|
|
14
15
|
import { SKILL_TOOL, lastComponent, readContextFill, readRecentToolCalls, } from "./transcript.js";
|
|
15
16
|
export function crewStoreProblem(database = db) {
|
|
@@ -50,36 +51,18 @@ function oneShotWatched(pending) {
|
|
|
50
51
|
}
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
|
-
function stripped(env) {
|
|
54
|
-
const { GIT_DIR: _gitDir, GIT_COMMON_DIR: _gitCommonDir, GIT_WORK_TREE: _gitWorkTree, ...rest } = env;
|
|
55
|
-
return rest;
|
|
56
|
-
}
|
|
57
|
-
function git(cwd, args) {
|
|
58
|
-
try {
|
|
59
|
-
return execFileSync("git", args, {
|
|
60
|
-
cwd,
|
|
61
|
-
encoding: "utf8",
|
|
62
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
63
|
-
timeout: 2000,
|
|
64
|
-
env: stripped(process.env),
|
|
65
|
-
}).trim();
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
54
|
export function readCommitsAhead(cwd, projectRoot) {
|
|
72
55
|
if (!cwd || cwd === projectRoot || !isLinkedWorktree(cwd))
|
|
73
56
|
return null;
|
|
74
57
|
const candidates = [];
|
|
75
|
-
const remoteHead =
|
|
58
|
+
const remoteHead = gitOut(cwd, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"]);
|
|
76
59
|
if (remoteHead)
|
|
77
60
|
candidates.push(remoteHead);
|
|
78
61
|
candidates.push("refs/heads/main", "refs/heads/master");
|
|
79
62
|
for (const ref of candidates) {
|
|
80
|
-
if (
|
|
63
|
+
if (gitOut(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]) === null)
|
|
81
64
|
continue;
|
|
82
|
-
const count =
|
|
65
|
+
const count = gitOut(cwd, ["rev-list", "--count", `${ref}..HEAD`]);
|
|
83
66
|
return count !== null && /^\d+$/.test(count) ? Number(count) : null;
|
|
84
67
|
}
|
|
85
68
|
return null;
|
|
@@ -481,7 +464,7 @@ export function collectCrew(project, now) {
|
|
|
481
464
|
if (!staffed.has(todo.id))
|
|
482
465
|
lanes.push({ todo, worker: null });
|
|
483
466
|
const needsYou = db
|
|
484
|
-
.prepare(
|
|
467
|
+
.prepare(`SELECT t.id, t.slug, t.tags FROM todos t WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL} ORDER BY t.id`)
|
|
485
468
|
.all(project.id)
|
|
486
469
|
.filter((t) => parseTags(t.tags).includes(NEEDS_HUMAN_TAG))
|
|
487
470
|
.map((t) => ({ id: t.id, slug: t.slug }));
|
package/dist/dashboard.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { db } from "./db.js";
|
|
4
|
+
import { ACTIVE_TODO_SQL, NOT_COMPLETED_TODO_SQL, OPEN_BLOCKERS_SQL } from "./todoScope.js";
|
|
4
5
|
import { getProject } from "./context.js";
|
|
5
6
|
export const describeVisibility = (visibility) => visibility === null ? "running" : visibility === "window" ? "own window" : visibility;
|
|
6
7
|
export function processCounts(procs) {
|
|
@@ -127,12 +128,9 @@ function fetchNextWake(projectId) {
|
|
|
127
128
|
function fetchTodoCounts(projectId) {
|
|
128
129
|
return db
|
|
129
130
|
.prepare(`SELECT
|
|
130
|
-
(SELECT COUNT(*) FROM todos WHERE project_id = ? AND
|
|
131
|
-
(SELECT COUNT(*) FROM todos t WHERE t.project_id = ? AND
|
|
132
|
-
AND EXISTS (
|
|
133
|
-
SELECT 1 FROM todo_blockers b JOIN todos bt ON bt.id = b.blocker_id
|
|
134
|
-
WHERE b.todo_id = t.id AND bt.status != 'completed'
|
|
135
|
-
)) AS blocked,
|
|
131
|
+
(SELECT COUNT(*) FROM todos t WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}) AS open,
|
|
132
|
+
(SELECT COUNT(*) FROM todos t WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}
|
|
133
|
+
AND EXISTS (${OPEN_BLOCKERS_SQL})) AS blocked,
|
|
136
134
|
(SELECT COUNT(*) FROM todos
|
|
137
135
|
WHERE project_id = ? AND completed_at IS NOT NULL
|
|
138
136
|
AND date(completed_at, 'localtime') >= date('now', 'localtime', '-6 days')) AS completed7d`)
|
|
@@ -262,10 +260,9 @@ function renderBoardSection(index, projectId) {
|
|
|
262
260
|
}
|
|
263
261
|
const OPEN_TODOS_SQL = `
|
|
264
262
|
SELECT t.id, t.title, t.body, t.slug, t.status, t.priority,
|
|
265
|
-
|
|
266
|
-
WHERE b.todo_id = t.id AND bt.status != 'completed') AS open_blockers
|
|
263
|
+
EXISTS (${OPEN_BLOCKERS_SQL}) AS blocked
|
|
267
264
|
FROM todos t
|
|
268
|
-
WHERE t.project_id = ? AND
|
|
265
|
+
WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}
|
|
269
266
|
ORDER BY CASE t.priority WHEN 'high' THEN 0 WHEN 'medium' THEN 1 ELSE 2 END, t.id`;
|
|
270
267
|
function fetchBlockersFor(todoIds) {
|
|
271
268
|
const byTodo = new Map();
|
|
@@ -274,7 +271,7 @@ function fetchBlockersFor(todoIds) {
|
|
|
274
271
|
const placeholders = todoIds.map(() => "?").join(",");
|
|
275
272
|
const rows = db
|
|
276
273
|
.prepare(`SELECT b.todo_id, t.id, t.title FROM todo_blockers b JOIN todos t ON t.id = b.blocker_id
|
|
277
|
-
WHERE b.todo_id IN (${placeholders}) AND t
|
|
274
|
+
WHERE b.todo_id IN (${placeholders}) AND t.${NOT_COMPLETED_TODO_SQL}`)
|
|
278
275
|
.all(...todoIds);
|
|
279
276
|
for (const row of rows) {
|
|
280
277
|
const list = byTodo.get(row.todo_id) ?? [];
|
|
@@ -291,11 +288,11 @@ function todoBadgeLevel(status, blocked) {
|
|
|
291
288
|
function renderTodosSection(index, projectId) {
|
|
292
289
|
const all = db.prepare(OPEN_TODOS_SQL).all(projectId);
|
|
293
290
|
const shown = all.slice(0, TODO_CAP);
|
|
294
|
-
const blockersByTodo = fetchBlockersFor(shown.filter((t) => t.
|
|
295
|
-
const blockedCount = all.filter((t) => t.
|
|
291
|
+
const blockersByTodo = fetchBlockersFor(shown.filter((t) => t.blocked).map((t) => t.id));
|
|
292
|
+
const blockedCount = all.filter((t) => t.blocked).length;
|
|
296
293
|
const rows = shown
|
|
297
294
|
.map((t) => {
|
|
298
|
-
const blocked = t.
|
|
295
|
+
const blocked = Boolean(t.blocked);
|
|
299
296
|
const blockers = blocked
|
|
300
297
|
? (blockersByTodo.get(t.id) ?? []).map((b) => `${mono(`#${b.id}`)} ${prose(b.title)}`).join(", ")
|
|
301
298
|
: "";
|
|
@@ -324,7 +321,7 @@ function renderTodosSection(index, projectId) {
|
|
|
324
321
|
`<p class="filter-none" id="todo-filter-none" hidden>No open todo matches that filter.</p>`
|
|
325
322
|
: "";
|
|
326
323
|
const body = filter +
|
|
327
|
-
renderCappedList(shown.length, all.length, "open todos", "todos", rows, emptyState("No open todos.", "
|
|
324
|
+
renderCappedList(shown.length, all.length, "open todos", "todos", rows, emptyState("No open todos.", "No active todos are listed. New open or in-progress todos land here. Backlog todos stay off this list."), "todo-list");
|
|
328
325
|
return section(index, "todos", "Todos", `${all.length} open, ${blockedCount} blocked`, "checklist", String(shown.length), body);
|
|
329
326
|
}
|
|
330
327
|
export const CHART_DAYS = 7;
|
package/dist/db.js
CHANGED
|
@@ -576,6 +576,42 @@ ALTER TABLE queen_audit ADD COLUMN problem TEXT NOT NULL DEFAULT '';
|
|
|
576
576
|
`,
|
|
577
577
|
`
|
|
578
578
|
ALTER TABLE agents ADD COLUMN pane_started_at TEXT;
|
|
579
|
+
`,
|
|
580
|
+
`
|
|
581
|
+
DROP TRIGGER fence_handoff_held_claim;
|
|
582
|
+
CREATE TRIGGER fence_handoff_held_claim
|
|
583
|
+
BEFORE UPDATE OF fire_count, delivery_method ON wakes
|
|
584
|
+
FOR EACH ROW
|
|
585
|
+
WHEN (NEW.fire_count > OLD.fire_count
|
|
586
|
+
OR (NEW.delivery_method IS 'pty-after-socket-timeout'
|
|
587
|
+
AND OLD.delivery_method IS NOT 'pty-after-socket-timeout'
|
|
588
|
+
AND NEW.typed_at IS NULL))
|
|
589
|
+
AND EXISTS (
|
|
590
|
+
SELECT 1 FROM lead_handoffs h
|
|
591
|
+
WHERE h.lead_agent_id = (SELECT a.id FROM agents a
|
|
592
|
+
WHERE a.actor_id = NEW.deliver_actor
|
|
593
|
+
ORDER BY (a.status = 'running') DESC,
|
|
594
|
+
a.id DESC
|
|
595
|
+
LIMIT 1)
|
|
596
|
+
AND h.state NOT IN ('completed', 'failed')
|
|
597
|
+
AND h.request_wake_id IS NOT NEW.id
|
|
598
|
+
AND (
|
|
599
|
+
(h.state = 'grace'
|
|
600
|
+
AND h.hold_released_at IS NULL
|
|
601
|
+
AND h.grace_started_at IS NOT NULL
|
|
602
|
+
AND (julianday('now') - julianday(h.grace_started_at)) * 86400 < 900)
|
|
603
|
+
OR ((h.state IN ('grace', 'started')
|
|
604
|
+
OR (h.hold_released_at IS NULL AND (
|
|
605
|
+
h.state IN ('respawning', 'ambiguous', 'wind_down')
|
|
606
|
+
OR (h.state = 'requested'
|
|
607
|
+
AND (h.reason = 'stop' OR h.pass >= 2))
|
|
608
|
+
OR (h.state = 'postponed' AND h.reason = 'stop'))))
|
|
609
|
+
AND h.state <> 'grace'
|
|
610
|
+
AND (julianday('now') - julianday(h.updated_at)) * 86400 < 3600)
|
|
611
|
+
))
|
|
612
|
+
BEGIN
|
|
613
|
+
SELECT RAISE(IGNORE);
|
|
614
|
+
END;
|
|
579
615
|
`,
|
|
580
616
|
];
|
|
581
617
|
export function storeSchemaAhead(database) {
|
package/dist/dispatcher.js
CHANGED
|
@@ -10,6 +10,9 @@ export const cliPath = () => fileURLToPath(new URL("./cli.js", import.meta.url))
|
|
|
10
10
|
function shQuote(s) {
|
|
11
11
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
12
12
|
}
|
|
13
|
+
export function repinCommand(node, cli) {
|
|
14
|
+
return `${shQuote(node)} ${shQuote(cli)} setup`;
|
|
15
|
+
}
|
|
13
16
|
export function dispatcherScript(node, cli) {
|
|
14
17
|
return [
|
|
15
18
|
"#!/bin/sh",
|
|
@@ -17,7 +20,7 @@ export function dispatcherScript(node, cli) {
|
|
|
17
20
|
"# Written by `hive setup`. It runs hive under one fixed interpreter, so a",
|
|
18
21
|
"# version manager resolving `node` per directory cannot land hive on a Node",
|
|
19
22
|
"# better-sqlite3's addon refuses to load under.",
|
|
20
|
-
|
|
23
|
+
`# Regenerate after every update: ${repinCommand(node, cli)}`,
|
|
21
24
|
`exec ${shQuote(node)} ${shQuote(cli)} "$@"`,
|
|
22
25
|
"",
|
|
23
26
|
].join("\n");
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
export function gitOut(cwd, args) {
|
|
3
|
+
const { GIT_DIR: _gitDir, GIT_COMMON_DIR: _gitCommonDir, GIT_WORK_TREE: _gitWorkTree, ...env } = process.env;
|
|
4
|
+
try {
|
|
5
|
+
return execFileSync("git", args, {
|
|
6
|
+
cwd,
|
|
7
|
+
encoding: "utf8",
|
|
8
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
9
|
+
timeout: 2000,
|
|
10
|
+
env,
|
|
11
|
+
}).trim();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
package/dist/harnesses.js
CHANGED
|
@@ -53,7 +53,6 @@ const claudeHarness = {
|
|
|
53
53
|
supportsRename: true,
|
|
54
54
|
classifiesPaneScreen: true,
|
|
55
55
|
paneClassifier: { choiceCheck: paneChoiceCheck, inputBoxState, inputBoxSnapshot, hasInputBox: paneHasInputBox, trailerView: claudeTrailerView },
|
|
56
|
-
hasScopes: true,
|
|
57
56
|
needsHome: false,
|
|
58
57
|
// "--" so a message that starts with a dash is never read as a flag.
|
|
59
58
|
initialPromptArgs: (message) => ["--", message],
|
|
@@ -82,7 +81,6 @@ export const codexHarness = {
|
|
|
82
81
|
hasInputBox: codexPaneHasInputBox,
|
|
83
82
|
trailerView: codexTrailerView,
|
|
84
83
|
},
|
|
85
|
-
hasScopes: false,
|
|
86
84
|
needsHome: true,
|
|
87
85
|
// Auto-submits with no Enter; verified live on v0.149.0.
|
|
88
86
|
initialPromptArgs: (message) => ["--", message],
|
|
@@ -106,7 +104,6 @@ const unknownHarness = {
|
|
|
106
104
|
supportsRename: false,
|
|
107
105
|
classifiesPaneScreen: false,
|
|
108
106
|
paneClassifier: null,
|
|
109
|
-
hasScopes: false,
|
|
110
107
|
needsHome: false,
|
|
111
108
|
initialPromptArgs: null,
|
|
112
109
|
// An unrecognized command's vocabulary is unknown, so it stops nothing rather than guessing.
|
package/dist/help.js
CHANGED
|
@@ -7,10 +7,10 @@ one project-scoped state store: pads, todos, kv, leases.
|
|
|
7
7
|
Call help(topic="<name>") for details. Topics: ${Object.keys(HELP_TOPICS).join(", ")}.
|
|
8
8
|
|
|
9
9
|
Scope resolution for project-scoped tools:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
resolution order is project_id override, session selection, worker pin,
|
|
11
|
+
then working directory; a git worktree or subdirectory resolves to its
|
|
12
|
+
primary checkout, and an unmatched directory becomes a new project
|
|
13
|
+
instead of falling back to an unrelated one
|
|
14
14
|
|
|
15
15
|
Quick start:
|
|
16
16
|
1. whoami — see your actor id and effective project
|
|
@@ -46,6 +46,9 @@ QUESTIONS — open questions for the human, recorded in the store
|
|
|
46
46
|
PROJECTS — scope management
|
|
47
47
|
project_list, project_add, project_select, project_prune
|
|
48
48
|
|
|
49
|
+
QUEEN AUDIT — confirmed queen writes and refused cross-project guard calls
|
|
50
|
+
queen_audit_list(project_id?, limit?)
|
|
51
|
+
|
|
49
52
|
ACTORS — who is writing
|
|
50
53
|
actor_prune`;
|
|
51
54
|
}
|
|
@@ -131,7 +134,7 @@ If the human asks to change how work runs here, that is an edit to the
|
|
|
131
134
|
runbook (fork it first), not a pad write.`,
|
|
132
135
|
agents: `AGENTS — spawn and drive worker sessions in tmux
|
|
133
136
|
|
|
134
|
-
agent_spawn(name?, model?, command?, extra_args?, read_only?, cwd?, placement?, layout?) —
|
|
137
|
+
agent_spawn(name?, model?, command?, extra_args?, harness?, read_only?, cwd?, placement?, layout?, todo_id?) —
|
|
135
138
|
start a worker (default command: claude) in session hive-main.
|
|
136
139
|
placement="split" (default) tiles the worker as a pane in the lead's
|
|
137
140
|
window so the whole crew shares one screen; placement="window" gives it
|
|
@@ -169,7 +172,7 @@ runbook (fork it first), not a pad write.`,
|
|
|
169
172
|
stays agent:N, so older pad writes and todo comments still point here.
|
|
170
173
|
A live claude worker is told to retitle its own session, which arrives as
|
|
171
174
|
a user turn: rename between assignments, not mid-task.
|
|
172
|
-
agent_park(name|agent_id) — END OF DAY. Kill the pane, mark the row PARKED
|
|
175
|
+
agent_park(name|agent_id, confirm_self?) — END OF DAY. Kill the pane, mark the row PARKED
|
|
173
176
|
rather than plain closed, record the branch, and hand back a board line
|
|
174
177
|
plus the one call that brings the lane back. Use this instead of
|
|
175
178
|
agent_close whenever the lane is PAUSED rather than finished: "closed"
|
|
@@ -179,7 +182,7 @@ runbook (fork it first), not a pad write.`,
|
|
|
179
182
|
with the same actor_id and its full prior context. It does not send
|
|
180
183
|
the assignment; agent_send it afterwards. Read the pane before
|
|
181
184
|
believing any wake about a worker you just resumed.
|
|
182
|
-
agent_close(name|agent_id) — kill the worker's pane and mark closed. Capture
|
|
185
|
+
agent_close(name|agent_id, confirm_self?, row_only?) — kill the worker's pane and mark closed. Capture
|
|
183
186
|
handoffs first; output is not retained. Self-close needs confirm_self.
|
|
184
187
|
On a PARKED row (by agent_id) it releases the park instead, which is how
|
|
185
188
|
you abandon a lane you have decided not to resume. row_only=true marks
|
|
@@ -239,8 +242,9 @@ its turn), waiting (needs permission or input). Non-claude commands show
|
|
|
239
242
|
interval used for firings after this one. Only body can be edited on an
|
|
240
243
|
idle wake (from wake_when_idle); delay_seconds/repeat_every_seconds
|
|
241
244
|
apply to a delay wake (from wake_set) only.
|
|
242
|
-
wake_cancel(wake_id) — cancel a pending wake-up you own
|
|
243
|
-
|
|
245
|
+
wake_cancel(wake_id) — cancel a pending wake-up you own, or any pending
|
|
246
|
+
wake-up in this project when you are a running lead
|
|
247
|
+
wake_list() — pending wake-ups and recently delivered one-shot wakes
|
|
244
248
|
|
|
245
249
|
Delivery contract:
|
|
246
250
|
When a wake-up fires, its body is typed into the target session's terminal
|
|
@@ -338,7 +342,7 @@ place.`,
|
|
|
338
342
|
pad_append(pad_id, content, expected_revision?) — add to the end
|
|
339
343
|
pad_edit(pad_id, old_text, new_text, expected_revision?) — replace one
|
|
340
344
|
unique literal occurrence; include enough context to disambiguate
|
|
341
|
-
pad_list(query?, tags?, include_archived?) — summaries without content;
|
|
345
|
+
pad_list(query?, tags?, include_archived?, limit?, offset?) — summaries without content;
|
|
342
346
|
query matches names and content and returns a snippet
|
|
343
347
|
pad_archive(pad_id, archived?) — archive frees the name for a new active
|
|
344
348
|
pad; content stays readable by pad_id. archived=false unarchives.
|
|
@@ -401,10 +405,12 @@ a phase flag. Use pads for anything longer than a line or two.`,
|
|
|
401
405
|
to a pending or defaulted question
|
|
402
406
|
|
|
403
407
|
hive stores the question; it never puts it to the human. Render it in your
|
|
404
|
-
own turn. Answering queues a wake to the asker carrying the question,
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
+
own turn. Answering queues a wake to the asker carrying the question, answer
|
|
409
|
+
and notes, or reroutes it to the project's running lead when the asker has
|
|
410
|
+
no running row. It skips wake delivery when you are the asker, no eligible
|
|
411
|
+
target is running, or the target cannot be resolved. The answer stays
|
|
412
|
+
readable either way. Nothing expires or defaults on its own: only the asker
|
|
413
|
+
or the human moves a question out of pending.`,
|
|
408
414
|
leases: `LEASES — advisory expiring claims on shared state
|
|
409
415
|
|
|
410
416
|
lease_acquire(key, ttl_seconds) — non-blocking; returns the holder if
|
package/dist/kickoff.js
CHANGED
|
@@ -1,22 +1,13 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
1
|
import { realpathSync } from "node:fs";
|
|
3
2
|
import { fileURLToPath } from "node:url";
|
|
4
3
|
import { cutToUnitBudget } from "./slug.js";
|
|
5
4
|
import { FIRST_MESSAGE_SHA_ENV } from "./firstMessage.js";
|
|
5
|
+
import { gitOut } from "./git.js";
|
|
6
6
|
export const OUTPUT_BUDGET = 10_000;
|
|
7
7
|
const CONTEXT_BUDGET = 6_000;
|
|
8
8
|
const BOARD_BUDGET = 1_800;
|
|
9
9
|
function currentBranch(dir) {
|
|
10
|
-
|
|
11
|
-
return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
12
|
-
cwd: dir,
|
|
13
|
-
encoding: "utf8",
|
|
14
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
15
|
-
}).trim();
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
10
|
+
return gitOut(dir, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
20
11
|
}
|
|
21
12
|
export function truncate(text, limit) {
|
|
22
13
|
if (text.length <= limit)
|
|
@@ -28,7 +19,7 @@ async function digest(projectPath, profile, warnings) {
|
|
|
28
19
|
const { findProjectForCwd } = await import("./context.js");
|
|
29
20
|
const { ACTIVE_TIMER_WHERE } = await import("./scheduler.js");
|
|
30
21
|
const { getActivePadByName } = await import("./tools/pads.js");
|
|
31
|
-
const { OPEN_BLOCKERS_SQL } = await import("./
|
|
22
|
+
const { ACTIVE_TODO_SQL, OPEN_BLOCKERS_SQL } = await import("./todoScope.js");
|
|
32
23
|
migrate();
|
|
33
24
|
const project = findProjectForCwd();
|
|
34
25
|
if (!project || project.path !== projectPath)
|
|
@@ -53,7 +44,7 @@ async function digest(projectPath, profile, warnings) {
|
|
|
53
44
|
.all(project.id);
|
|
54
45
|
const blocked = db
|
|
55
46
|
.prepare(`SELECT COUNT(*) AS n FROM todos t
|
|
56
|
-
WHERE t.project_id = ? AND
|
|
47
|
+
WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}
|
|
57
48
|
AND EXISTS (${OPEN_BLOCKERS_SQL})`)
|
|
58
49
|
.get(project.id).n;
|
|
59
50
|
if (inFlight.length > 0) {
|
package/dist/kvValue.js
ADDED
package/dist/leadHandoffState.js
CHANGED
|
@@ -30,7 +30,7 @@ export function readActiveHandoff(leadAgentId) {
|
|
|
30
30
|
export function handoffHoldsAutomation(row) {
|
|
31
31
|
switch (row.state) {
|
|
32
32
|
case "grace":
|
|
33
|
-
return
|
|
33
|
+
return row.hold_released_at === null && !olderThan(row.grace_started_at, STOP_HOLD_CAP_SECONDS);
|
|
34
34
|
case "started":
|
|
35
35
|
return !olderThan(row.updated_at, HOLD_CEILING_SECONDS);
|
|
36
36
|
case "respawning":
|
package/dist/portfolio.js
CHANGED
|
@@ -2,7 +2,7 @@ import { db } from "./db.js";
|
|
|
2
2
|
import { listProjects } from "./context.js";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { parseTags } from "./result.js";
|
|
5
|
-
import { OPEN_BLOCKERS_SQL } from "./
|
|
5
|
+
import { ACTIVE_TODO_SQL, OPEN_BLOCKERS_SQL } from "./todoScope.js";
|
|
6
6
|
import { pendingQuestionCount } from "./questions.js";
|
|
7
7
|
import { leadSessionEnded, readLeadTurnState } from "./leadState.js";
|
|
8
8
|
import { liveTargets, ownershipLiveness, rowOwnership } from "./tmux.js";
|
|
@@ -63,7 +63,7 @@ function projectRow(project, now, snapshot) {
|
|
|
63
63
|
.prepare(`SELECT t.id, t.title, t.slug, t.priority, t.status, t.tags, t.updated_at,
|
|
64
64
|
EXISTS (${OPEN_BLOCKERS_SQL}) AS blocked
|
|
65
65
|
FROM todos t
|
|
66
|
-
WHERE t.project_id = ? AND
|
|
66
|
+
WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}`)
|
|
67
67
|
.all(project.id);
|
|
68
68
|
const todos = { open: 0, in_progress: 0, blocked: 0, blocked_in_progress: 0, high: 0 };
|
|
69
69
|
const tagged = [];
|
package/dist/processes.js
CHANGED
|
@@ -38,6 +38,18 @@ export const STOP_REASONS = {
|
|
|
38
38
|
leadPaneExited: "lead pane exited",
|
|
39
39
|
previousLead: "left running by a previous lead",
|
|
40
40
|
};
|
|
41
|
+
export function renderStart(result) {
|
|
42
|
+
switch (result.status) {
|
|
43
|
+
case "started":
|
|
44
|
+
return result.message;
|
|
45
|
+
case "already-running":
|
|
46
|
+
return `already running${result.location ? ` (${result.location})` : ""}`;
|
|
47
|
+
case "refused":
|
|
48
|
+
return result.message ?? "";
|
|
49
|
+
case "failed":
|
|
50
|
+
return `failed: ${result.message}`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
41
53
|
const STOPPABLE_COLUMNS = "id, project_id, name, tmux_target, tmux_socket, pane_pid, created_at";
|
|
42
54
|
export function runningCommandRows(projectId) {
|
|
43
55
|
return db
|
package/dist/scheduler.js
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync, lstatSync, mkdirSync, realpathSync, renameSync, statSync, u
|
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { dirname, join, sep } from "node:path";
|
|
6
6
|
import { dataDir, db, storeReplaced } from "./db.js";
|
|
7
|
+
import { decodeKvValue } from "./kvValue.js";
|
|
7
8
|
import { maybeBackupHourly } from "./backup.js";
|
|
8
9
|
import { renderDashboardForWrite } from "./dashboard.js";
|
|
9
10
|
import { snapshotProcesses } from "./processes.js";
|
|
@@ -183,11 +184,11 @@ function reportDeadProcess(agent, snapshot) {
|
|
|
183
184
|
// interrupted between the two is not reported as a crash.
|
|
184
185
|
if (stoppingMarkerLive(agent.project_id, agent.id))
|
|
185
186
|
return;
|
|
186
|
-
const lead = stmt(`SELECT actor_id, tmux_target, tmux_socket, pane_pid FROM agents
|
|
187
|
+
const lead = stmt(`SELECT actor_id, tmux_target, tmux_socket, pane_pid, command FROM agents
|
|
187
188
|
WHERE project_id = ? AND kind = ? AND status = 'running' ORDER BY id DESC LIMIT 1`).get(agent.project_id, LEAD_KIND);
|
|
188
189
|
// Never mint a notice hive could not deliver: with no live lead pane there is nobody this could
|
|
189
190
|
// ever reach, and the row would sit in wakes forever.
|
|
190
|
-
if (!lead?.tmux_target || rowOwnership(lead, snapshot) !== "live")
|
|
191
|
+
if (!lead?.tmux_target || !screenClassifiable(lead.command) || rowOwnership(lead, snapshot) !== "live")
|
|
191
192
|
return;
|
|
192
193
|
insertNotice({ project_id: agent.project_id, owner: lead.actor_id }, lead.actor_id, lead.tmux_target, deadProcessBody(agent.name), null);
|
|
193
194
|
}
|
|
@@ -410,19 +411,22 @@ function renderQueenPage(queenId) {
|
|
|
410
411
|
const QUEEN_PAGE_CLAIM_KEY = "hive:queen-page-claim";
|
|
411
412
|
const QUEEN_PAGE_MARK_KEY = "hive:queen-page-mark";
|
|
412
413
|
function claimQueenPageAttempt(queenId) {
|
|
413
|
-
bestEffortRun("INSERT OR IGNORE INTO kv (project_id, key, value, updated_at) VALUES (?, ?,
|
|
414
|
+
bestEffortRun("INSERT OR IGNORE INTO kv (project_id, key, value, updated_at) VALUES (?, ?, ?, '1970-01-01 00:00:00')", queenId, QUEEN_PAGE_CLAIM_KEY, JSON.stringify(""));
|
|
414
415
|
return (stmt(`UPDATE kv SET updated_at = datetime('now')
|
|
415
416
|
WHERE project_id = ? AND key = ?
|
|
416
417
|
AND updated_at <= datetime('now', '-${DASHBOARD_MIN_INTERVAL_SECONDS} seconds')`).run(queenId, QUEEN_PAGE_CLAIM_KEY).changes === 1);
|
|
417
418
|
}
|
|
418
419
|
function readQueenPageMark(queenId) {
|
|
419
420
|
const row = stmt("SELECT value FROM kv WHERE project_id = ? AND key = ?").get(queenId, QUEEN_PAGE_MARK_KEY);
|
|
420
|
-
|
|
421
|
+
if (!row)
|
|
422
|
+
return null;
|
|
423
|
+
const value = decodeKvValue(row.value);
|
|
424
|
+
return typeof value === "string" ? value : row.value;
|
|
421
425
|
}
|
|
422
426
|
function publishQueenPage(queenId, target, page) {
|
|
423
427
|
writeDashboardAtomically(target, page.html);
|
|
424
428
|
bestEffortRun(`INSERT INTO kv (project_id, key, value) VALUES (?, ?, ?)
|
|
425
|
-
ON CONFLICT(project_id, key) DO UPDATE SET value = excluded.value, updated_at = datetime('now')`, queenId, QUEEN_PAGE_MARK_KEY, page.hash);
|
|
429
|
+
ON CONFLICT(project_id, key) DO UPDATE SET value = excluded.value, updated_at = datetime('now')`, queenId, QUEEN_PAGE_MARK_KEY, JSON.stringify(page.hash));
|
|
426
430
|
}
|
|
427
431
|
export function generateQueenDashboardNow() {
|
|
428
432
|
try {
|
|
@@ -494,6 +498,24 @@ function driveOwnLeadHandoff() {
|
|
|
494
498
|
catch {
|
|
495
499
|
}
|
|
496
500
|
}
|
|
501
|
+
function isolated(label, task) {
|
|
502
|
+
const report = (err) => {
|
|
503
|
+
try {
|
|
504
|
+
console.error(`[scheduler] ${label} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
try {
|
|
510
|
+
const result = task();
|
|
511
|
+
if (result !== null && typeof result === "object" && "then" in result) {
|
|
512
|
+
return Promise.resolve(result).then(() => { }, report);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
catch (err) {
|
|
516
|
+
report(err);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
497
519
|
export async function tick(snapshot) {
|
|
498
520
|
if (ticking)
|
|
499
521
|
return;
|
|
@@ -506,14 +528,15 @@ export async function tick(snapshot) {
|
|
|
506
528
|
}
|
|
507
529
|
if (snapshot === undefined)
|
|
508
530
|
snapshot = liveTargets();
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
531
|
+
const tickSnapshot = snapshot;
|
|
532
|
+
isolated("janitor", () => janitor(tickSnapshot));
|
|
533
|
+
if (tickSnapshot)
|
|
534
|
+
isolated("running-build check", () => reportRunningBuildChange(tickSnapshot));
|
|
535
|
+
isolated("lead handoff", driveOwnLeadHandoff);
|
|
536
|
+
isolated("wake confirmations", checkConfirmations);
|
|
537
|
+
isolated("state-log pruning", pruneStateLog);
|
|
538
|
+
isolated("hourly backup", () => maybeBackupHourly(db, dataDir));
|
|
539
|
+
isolated("dashboard generation", maybeGenerateDashboards);
|
|
517
540
|
const now = stmt("SELECT datetime('now') AS now").get().now;
|
|
518
541
|
const candidates = stmt(`SELECT wakes.*, ${DELIVER_ROW_COLUMNS}
|
|
519
542
|
FROM wakes ${DELIVER_SOCKET_JOIN}
|
|
@@ -524,18 +547,20 @@ export async function tick(snapshot) {
|
|
|
524
547
|
)`).all();
|
|
525
548
|
const choices = new Map();
|
|
526
549
|
for (const timer of candidates) {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
550
|
+
await isolated(`timer ${timer.id}`, async () => {
|
|
551
|
+
if (timer.kind === "delay") {
|
|
552
|
+
await fireDelay(timer, tickSnapshot, choices);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const leadWatch = leadSubscription(timer.id);
|
|
556
|
+
if (leadWatch !== null)
|
|
557
|
+
await maybeFireLeadWatch(timer, leadWatch, tickSnapshot, now, choices);
|
|
558
|
+
else
|
|
559
|
+
await maybeFireIdle(timer, tickSnapshot, now, choices);
|
|
560
|
+
});
|
|
536
561
|
}
|
|
537
|
-
await retryUnconfirmedSocketWakes(
|
|
538
|
-
await retryQuietLeadMessages(
|
|
562
|
+
await isolated("unconfirmed socket wake retry", () => retryUnconfirmedSocketWakes(tickSnapshot, choices));
|
|
563
|
+
await isolated("quiet lead message retry", () => retryQuietLeadMessages(tickSnapshot));
|
|
539
564
|
}
|
|
540
565
|
catch {
|
|
541
566
|
}
|
|
@@ -972,6 +997,8 @@ function alreadyToldAbout(timerId, agentId, blockedSince) {
|
|
|
972
997
|
const CONDITION_IDLE = "idle";
|
|
973
998
|
const CONDITION_GONE = "gone";
|
|
974
999
|
const NOTICE_RETRY_AFTER = "-60 seconds";
|
|
1000
|
+
const spentUntypedNotice = (alias) => `${alias}.fired_at IS NOT NULL AND COALESCE(${alias}.typed_at, ${alias}.socket_attempt_at) IS NULL ` +
|
|
1001
|
+
`AND ${alias}.cancelled_at IS NULL AND ${alias}.fired_at < datetime('now', '${NOTICE_RETRY_AFTER}')`;
|
|
975
1002
|
const NOTICE_MAX_AGE = "-1 hours";
|
|
976
1003
|
const ROSTER_STILL_GOING = 8;
|
|
977
1004
|
const FINISHED_SHOWN_CAP = ROSTER_STILL_GOING;
|
|
@@ -979,8 +1006,7 @@ const STANDING_EXPIRED_NOTE = "this standing watch has expired and nothing is wa
|
|
|
979
1006
|
const unreported = (condition, episode) => `NOT EXISTS (
|
|
980
1007
|
SELECT 1 FROM wake_idle_notices n LEFT JOIN wakes nt ON nt.id = n.notice_wake_id
|
|
981
1008
|
WHERE n.wake_id = ? AND n.agent_id = a.id AND n.condition = '${condition}' AND n.episode = ${episode}
|
|
982
|
-
AND NOT (
|
|
983
|
-
AND nt.fired_at < datetime('now', '${NOTICE_RETRY_AFTER}')))`;
|
|
1009
|
+
AND NOT (${spentUntypedNotice("nt")}))`;
|
|
984
1010
|
const CREW_COLUMNS = `a.id, a.name, a.actor_id, a.tmux_target, a.tmux_socket, a.pane_pid, a.agent_state,
|
|
985
1011
|
a.state_changed_at, a.status, a.command, a.kind, a.resumed_at, a.closed_at,
|
|
986
1012
|
a.cwd, a.session_id, a.transcript_path`;
|
|
@@ -1218,12 +1244,11 @@ function standingNoticeBody(timer, finished, totalFinished, carriedTotal, span)
|
|
|
1218
1244
|
return lines.join("\n");
|
|
1219
1245
|
}
|
|
1220
1246
|
function rearmSpentEpisode(timerId, agentId, condition, episode) {
|
|
1221
|
-
stmt(`DELETE FROM wake_idle_notices
|
|
1247
|
+
return stmt(`DELETE FROM wake_idle_notices
|
|
1222
1248
|
WHERE wake_id = ? AND agent_id = ? AND condition = ? AND episode = ?
|
|
1223
1249
|
AND notice_wake_id IS NOT NULL
|
|
1224
1250
|
AND EXISTS (SELECT 1 FROM wakes t WHERE t.id = wake_idle_notices.notice_wake_id
|
|
1225
|
-
AND t.
|
|
1226
|
-
AND t.fired_at < datetime('now', '${NOTICE_RETRY_AFTER}'))`).run(timerId, agentId, condition, episode);
|
|
1251
|
+
AND ${spentUntypedNotice("t")})`).run(timerId, agentId, condition, episode).changes > 0;
|
|
1227
1252
|
}
|
|
1228
1253
|
function claimEpisode(timerId, agentId, condition, episode) {
|
|
1229
1254
|
return (stmt(`INSERT OR IGNORE INTO wake_idle_notices (wake_id, agent_id, condition, episode)
|
|
@@ -1366,11 +1391,15 @@ const claimStallBatch = db.transaction((timer, tell, candidates, observedAt) =>
|
|
|
1366
1391
|
return false;
|
|
1367
1392
|
const won = [];
|
|
1368
1393
|
for (const c of candidates) {
|
|
1369
|
-
|
|
1394
|
+
const rearmed = rearmSpentEpisode(timer.id, c.row.id, CONDITION_STALL, c.row.episode);
|
|
1395
|
+
if (!claimEpisode(timer.id, c.row.id, CONDITION_STALL, c.row.episode))
|
|
1370
1396
|
continue;
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1397
|
+
if (c.row.agent_state === "waiting" && !rearmed && !claimBlockNotice(timer.id, c.row.id, c.row.episode)) {
|
|
1398
|
+
stmt(`DELETE FROM wake_idle_notices
|
|
1399
|
+
WHERE wake_id = ? AND agent_id = ? AND condition = ? AND episode = ? AND notice_wake_id IS NULL`).run(timer.id, c.row.id, CONDITION_STALL, c.row.episode);
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
won.push(c);
|
|
1374
1403
|
}
|
|
1375
1404
|
if (won.length === 0)
|
|
1376
1405
|
return false;
|
|
@@ -2164,10 +2193,21 @@ async function deliver(timer, note, choices, typedSeen, firstHeldAt, owner, fall
|
|
|
2164
2193
|
return;
|
|
2165
2194
|
if (firing !== null && !pasted)
|
|
2166
2195
|
dropRepeatingFiring(timer, firing, err);
|
|
2167
|
-
if (
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2196
|
+
if (err instanceof PaneOwnershipLost) {
|
|
2197
|
+
if (firing === null || pasted) {
|
|
2198
|
+
recordLostAfterClaim(timer, pasted, err instanceof Error ? err.message : String(err), firing);
|
|
2199
|
+
}
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
if (pasted) {
|
|
2203
|
+
const repairableNotice = stmt(`SELECT 1 FROM wake_idle_notices n JOIN wakes w ON w.id = n.notice_wake_id
|
|
2204
|
+
WHERE n.notice_wake_id = ? AND w.fired_at IS NOT NULL
|
|
2205
|
+
AND COALESCE(w.typed_at, w.socket_attempt_at) IS NULL AND w.cancelled_at IS NULL
|
|
2206
|
+
LIMIT 1`).get(timer.id);
|
|
2207
|
+
if (pasted && repairableNotice === undefined) {
|
|
2208
|
+
recordLostAfterClaim(timer, pasted, err instanceof Error ? err.message : String(err), firing);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2171
2211
|
return;
|
|
2172
2212
|
}
|
|
2173
2213
|
finally {
|
package/dist/spawn.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
1
|
import { existsSync } from "node:fs";
|
|
3
2
|
import { preservedRolloutPath, reapCodexHome } from "./codexHome.js";
|
|
4
3
|
import { getProject } from "./context.js";
|
|
4
|
+
import { gitOut } from "./git.js";
|
|
5
5
|
import { dataDir, db } from "./db.js";
|
|
6
6
|
import { loadProjectYml } from "./projectYml.js";
|
|
7
7
|
import { applyLayout, claimInitialWindow, createWindow, DEFAULT_LAYOUT, ensureSession, crossServerRefusal, findProcessesWindow, findProjectWindow, isPaneTarget, paneProcessExited, paneWindow, PROCESSES_LAYOUT, processesPaneTitle, processesWindowName, retainOnExitArgs, rowLive, sessionName, setPaneTitle, setRemainOnExit, makeProcessesWindow, tmux, tmuxSocketPath, untrustedTmuxServer, waitForPaneEstablished, windowLayout, windowOwner, windowTitle, rowOwnership, } from "./tmux.js";
|
|
@@ -347,34 +347,19 @@ export function renameAgent(agent, newName, projectName) {
|
|
|
347
347
|
}
|
|
348
348
|
}
|
|
349
349
|
export function branchAt(cwd) {
|
|
350
|
-
const
|
|
351
|
-
try {
|
|
352
|
-
return execFileSync("git", args, {
|
|
353
|
-
cwd,
|
|
354
|
-
encoding: "utf8",
|
|
355
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
356
|
-
timeout: 2000,
|
|
357
|
-
}).trim();
|
|
358
|
-
}
|
|
359
|
-
catch {
|
|
360
|
-
return null;
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
350
|
+
const branch = gitOut(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
364
351
|
if (branch === null)
|
|
365
352
|
return "";
|
|
366
353
|
if (branch !== "HEAD")
|
|
367
354
|
return branch;
|
|
368
|
-
return
|
|
355
|
+
return gitOut(cwd, ["rev-parse", "--short", "HEAD"]) ?? "";
|
|
369
356
|
}
|
|
370
357
|
export function parkAgentRow(agentId, expected, branch) {
|
|
371
358
|
const park = "UPDATE agents SET status = 'closed', closed_at = datetime('now'), parked_at = datetime('now'), " +
|
|
372
359
|
"parked_branch = ? WHERE id = ? AND status = 'running' AND tmux_target = ?";
|
|
373
|
-
const row =
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
.prepare(`${park} AND tmux_socket = ? AND pane_pid = ? RETURNING parked_at`)
|
|
377
|
-
.get(branch, agentId, expected.tmux_target, expected.tmux_socket, expected.pane_pid);
|
|
360
|
+
const row = db
|
|
361
|
+
.prepare(`${park} AND tmux_socket = ? AND pane_pid = ? RETURNING parked_at`)
|
|
362
|
+
.get(branch, agentId, expected.tmux_target, expected.tmux_socket, expected.pane_pid);
|
|
378
363
|
return row?.parked_at;
|
|
379
364
|
}
|
|
380
365
|
export const paneIdentity = (row) => ({
|
|
@@ -455,13 +440,9 @@ export function closeAgentRow(agentId, expected) {
|
|
|
455
440
|
? db
|
|
456
441
|
.prepare("UPDATE agents SET status = 'closed', closed_at = datetime('now') WHERE id = ? AND status = 'running'")
|
|
457
442
|
.run(agentId)
|
|
458
|
-
:
|
|
459
|
-
?
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
: db
|
|
463
|
-
.prepare("UPDATE agents SET status = 'closed', closed_at = datetime('now') WHERE id = ? AND status = 'running' " +
|
|
464
|
-
"AND tmux_target = ? AND tmux_socket = ? AND pane_pid = ?")
|
|
465
|
-
.run(agentId, expected.tmux_target, expected.tmux_socket, expected.pane_pid);
|
|
443
|
+
: db
|
|
444
|
+
.prepare("UPDATE agents SET status = 'closed', closed_at = datetime('now') WHERE id = ? AND status = 'running' " +
|
|
445
|
+
"AND tmux_target = ? AND tmux_socket = ? AND pane_pid = ?")
|
|
446
|
+
.run(agentId, expected.tmux_target, expected.tmux_socket, expected.pane_pid);
|
|
466
447
|
return info.changes > 0;
|
|
467
448
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const ACTIVE_TODO_SQL = "t.status IN ('open', 'in_progress') AND t.archived_at IS NULL";
|
|
2
|
+
export const NOT_COMPLETED_TODO_SQL = "status != 'completed'";
|
|
3
|
+
export const OPEN_BLOCKERS_FROM_WHERE = `FROM todo_blockers b JOIN todos bt ON bt.id = b.blocker_id
|
|
4
|
+
WHERE b.todo_id = t.id AND bt.${NOT_COMPLETED_TODO_SQL}`;
|
|
5
|
+
export const OPEN_BLOCKERS_SQL = `SELECT 1 ${OPEN_BLOCKERS_FROM_WHERE}`;
|
package/dist/tools/agents.js
CHANGED
|
@@ -695,19 +695,15 @@ export function registerAgents(server) {
|
|
|
695
695
|
note: "The pane never became ready. The brief is loaded regardless; check agent_output before sending the worker its assignment - typing into it now risks losing the text silently.",
|
|
696
696
|
}),
|
|
697
697
|
}
|
|
698
|
-
:
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
brief_path
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
"supported way to send them. The brief is written to brief_path - open that pane " +
|
|
708
|
-
"(tmux attach) and paste it in yourself, or drive the worker with agent_send(keys: [...]). " +
|
|
709
|
-
"Wakes aimed at this worker are refused for the same reason.",
|
|
710
|
-
}),
|
|
698
|
+
: {
|
|
699
|
+
brief_path: writeAgentBrief(agentId, workerBrief(briefFor(actorId))),
|
|
700
|
+
note: `This worker runs ${JSON.stringify(commandHead(baseCommand))}, which hive cannot brief and ` +
|
|
701
|
+
"cannot type into: the guards that make typing safe read claude's chrome, so agent_send's " +
|
|
702
|
+
"text path refuses this pane. There are no instructions to prepend, because there is no " +
|
|
703
|
+
"supported way to send them. The brief is written to brief_path - open that pane " +
|
|
704
|
+
"(tmux attach) and paste it in yourself, or drive the worker with agent_send(keys: [...]). " +
|
|
705
|
+
"Wakes aimed at this worker are refused for the same reason.",
|
|
706
|
+
}),
|
|
711
707
|
};
|
|
712
708
|
if (omittedTodoWarning) {
|
|
713
709
|
const existingNote = typeof receipt.note === "string" ? receipt.note : "";
|
|
@@ -996,6 +992,7 @@ export function registerAgents(server) {
|
|
|
996
992
|
const classifier = renameHarness.paneClassifier;
|
|
997
993
|
const { awaitingChoice, tail } = classifier.choiceCheck(agent.tmux_target);
|
|
998
994
|
const box = awaitingChoice === false ? classifier.inputBoxState(agent.tmux_target) : null;
|
|
995
|
+
const inCopyMode = paneInCopyMode(agent.tmux_target);
|
|
999
996
|
if (awaitingChoice === true) {
|
|
1000
997
|
heldNote =
|
|
1001
998
|
"Not retitled: the pane is waiting on a choice, so typing /rename would answer it instead of setting the title. Clear the prompt first (agent_send with keys), then retry.";
|
|
@@ -1017,6 +1014,11 @@ export function registerAgents(server) {
|
|
|
1017
1014
|
`Not retitled: the pane's input box holds unsubmitted text, so /rename would be pasted onto the end of it and submitted as prose rather than run as a command. The row IS renamed - it is "${newName}" now - and only the pane's own title was left alone. Clear the line with agent_send(keys: ["C-a", "C-k"]) once you can attribute the text, then call agent_rename(name: "${newName}", new_name: "${newName}") to retitle the pane.`;
|
|
1018
1015
|
heldTail = tail;
|
|
1019
1016
|
}
|
|
1017
|
+
else if (inCopyMode === true) {
|
|
1018
|
+
heldNote =
|
|
1019
|
+
`Not retitled: the pane is in tmux copy mode, so /rename would not reach its prompt. The row IS renamed - it is "${newName}" now - and only the pane's own title was left alone. Leave copy mode (press q or Escape there), then call agent_rename(name: "${newName}", new_name: "${newName}") to retitle the pane.`;
|
|
1020
|
+
heldTail = tail;
|
|
1021
|
+
}
|
|
1020
1022
|
else {
|
|
1021
1023
|
let pasted = false;
|
|
1022
1024
|
try {
|
|
@@ -1191,6 +1193,7 @@ export function registerAgents(server) {
|
|
|
1191
1193
|
const agent = resolveQueenLeadTarget("agent_send", project.id, args.agent_id ?? args.name, () => findAgent(project.id, args));
|
|
1192
1194
|
requireLive(agent);
|
|
1193
1195
|
const target = agent.tmux_target;
|
|
1196
|
+
const identity = paneIdentity(agent);
|
|
1194
1197
|
const auditIdentity = captureQueenWriteIdentity(project.id);
|
|
1195
1198
|
let outgoing = args.text ?? "";
|
|
1196
1199
|
const tag = senderTag(project.id, currentActor());
|
|
@@ -1241,10 +1244,9 @@ export function registerAgents(server) {
|
|
|
1241
1244
|
}
|
|
1242
1245
|
if (args.wait_ms == null)
|
|
1243
1246
|
return quiet;
|
|
1244
|
-
await sleep(
|
|
1247
|
+
await sleep(args.wait_ms);
|
|
1245
1248
|
try {
|
|
1246
|
-
|
|
1247
|
-
throw new Error("not live");
|
|
1249
|
+
requireStillOwned(agent.id, identity, "agent_send");
|
|
1248
1250
|
return { ...quiet, tail: capturePane(target, 15) };
|
|
1249
1251
|
}
|
|
1250
1252
|
catch {
|
|
@@ -1342,7 +1344,6 @@ export function registerAgents(server) {
|
|
|
1342
1344
|
outgoing = tag + args.text;
|
|
1343
1345
|
let pasted = false;
|
|
1344
1346
|
let buffered = false;
|
|
1345
|
-
const identity = paneIdentity(agent);
|
|
1346
1347
|
try {
|
|
1347
1348
|
await sendText(target, outgoing, submitting, () => {
|
|
1348
1349
|
pasted = true;
|
|
@@ -1376,9 +1377,10 @@ export function registerAgents(server) {
|
|
|
1376
1377
|
throw new Error("Pass text or keys.");
|
|
1377
1378
|
}
|
|
1378
1379
|
if (args.wait_ms != null) {
|
|
1379
|
-
await sleep(
|
|
1380
|
+
await sleep(args.wait_ms);
|
|
1380
1381
|
let tailField;
|
|
1381
1382
|
try {
|
|
1383
|
+
requireStillOwned(agent.id, identity, "agent_send");
|
|
1382
1384
|
tailField = { tail: capturePane(target, 15) };
|
|
1383
1385
|
}
|
|
1384
1386
|
catch {
|
package/dist/tools/kv.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { db } from "../db.js";
|
|
3
3
|
import { currentActor, effectiveProjectId } from "../context.js";
|
|
4
4
|
import { run } from "../result.js";
|
|
5
|
+
import { decodeKvValue } from "../kvValue.js";
|
|
5
6
|
import { idParam, projectIdParam } from "./params.js";
|
|
6
7
|
function purgeExpired(projectId) {
|
|
7
8
|
db.prepare("DELETE FROM kv WHERE project_id = ? AND expires_at IS NOT NULL AND expires_at < datetime('now')").run(projectId);
|
|
@@ -56,7 +57,7 @@ export function registerKv(server) {
|
|
|
56
57
|
project_id: projectId,
|
|
57
58
|
key: row.key,
|
|
58
59
|
found: true,
|
|
59
|
-
value:
|
|
60
|
+
value: decodeKvValue(row.value),
|
|
60
61
|
updated_by: row.updated_by,
|
|
61
62
|
updated_at: row.updated_at,
|
|
62
63
|
expires_at: row.expires_at,
|
|
@@ -76,9 +77,9 @@ export function registerKv(server) {
|
|
|
76
77
|
purgeExpired(projectId);
|
|
77
78
|
let sql = "SELECT * FROM kv WHERE project_id = ?";
|
|
78
79
|
const params = [projectId];
|
|
79
|
-
if (args.prefix) {
|
|
80
|
-
sql += " AND key
|
|
81
|
-
params.push(
|
|
80
|
+
if (args.prefix !== undefined) {
|
|
81
|
+
sql += " AND substr(key, 1, length(?)) = ?";
|
|
82
|
+
params.push(args.prefix, args.prefix);
|
|
82
83
|
}
|
|
83
84
|
sql += " ORDER BY key";
|
|
84
85
|
const rows = db.prepare(sql).all(...params);
|
|
@@ -86,7 +87,7 @@ export function registerKv(server) {
|
|
|
86
87
|
project_id: projectId,
|
|
87
88
|
entries: rows.map((r) => ({
|
|
88
89
|
key: r.key,
|
|
89
|
-
value:
|
|
90
|
+
value: decodeKvValue(r.value),
|
|
90
91
|
updated_by: r.updated_by,
|
|
91
92
|
updated_at: r.updated_at,
|
|
92
93
|
expires_at: r.expires_at,
|
package/dist/tools/todos.js
CHANGED
|
@@ -6,6 +6,7 @@ import { matchesAnyTag, parseTags, run } from "../result.js";
|
|
|
6
6
|
import { findUnsafeControlChar } from "../tmux.js";
|
|
7
7
|
import { SLUG_MAX_LEN, fallbackSlug } from "../slug.js";
|
|
8
8
|
import { idParam, limitParam, offsetParam, projectIdParam } from "./params.js";
|
|
9
|
+
import { NOT_COMPLETED_TODO_SQL, OPEN_BLOCKERS_FROM_WHERE, OPEN_BLOCKERS_SQL } from "../todoScope.js";
|
|
9
10
|
const priorityParam = z.enum(["high", "medium", "low"]);
|
|
10
11
|
const slugParam = z
|
|
11
12
|
.string()
|
|
@@ -19,11 +20,8 @@ const slugParam = z
|
|
|
19
20
|
export { SLUG_MAX_LEN, fallbackSlug };
|
|
20
21
|
export const TODO_STATUSES = ["open", "in_progress", "backlog", "completed"];
|
|
21
22
|
const statusParam = z.enum(TODO_STATUSES);
|
|
22
|
-
const OPEN_BLOCKERS_FROM_WHERE = `FROM todo_blockers b JOIN todos bt ON bt.id = b.blocker_id
|
|
23
|
-
WHERE b.todo_id = t.id AND bt.status != 'completed'`;
|
|
24
|
-
export const OPEN_BLOCKERS_SQL = `SELECT 1 ${OPEN_BLOCKERS_FROM_WHERE}`;
|
|
25
23
|
const LIVE_DEPENDENTS_SQL = `SELECT t.id AS todo_id FROM todo_blockers b JOIN todos t ON t.id = b.todo_id
|
|
26
|
-
WHERE b.blocker_id = ? AND t
|
|
24
|
+
WHERE b.blocker_id = ? AND t.${NOT_COMPLETED_TODO_SQL} AND t.archived_at IS NULL`;
|
|
27
25
|
export const COMMENT_COUNT_SQL = `(SELECT COUNT(*) FROM todo_comments c WHERE c.todo_id = t.id)`;
|
|
28
26
|
const SUMMARY_SQL = `
|
|
29
27
|
SELECT t.*,
|
|
@@ -191,10 +189,7 @@ const completeTodo = db.transaction((projectId, todoId, completed) => {
|
|
|
191
189
|
if (completed) {
|
|
192
190
|
newlyUnblocked = db
|
|
193
191
|
.prepare(`${LIVE_DEPENDENTS_SQL}
|
|
194
|
-
AND NOT EXISTS (
|
|
195
|
-
SELECT 1 FROM todo_blockers b2 JOIN todos bt ON bt.id = b2.blocker_id
|
|
196
|
-
WHERE b2.todo_id = t.id AND bt.status != 'completed'
|
|
197
|
-
)`)
|
|
192
|
+
AND NOT EXISTS (${OPEN_BLOCKERS_SQL})`)
|
|
198
193
|
.all(todo.id).map((r) => r.todo_id);
|
|
199
194
|
}
|
|
200
195
|
return { todo_id: todo.id, completed, newly_unblocked: newlyUnblocked };
|
package/dist/version.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
1
|
import { readFileSync, statSync } from "node:fs";
|
|
3
2
|
import { dirname, join } from "node:path";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { gitOut } from "./git.js";
|
|
5
5
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
6
6
|
const checkoutRoot = join(moduleDir, "..");
|
|
7
7
|
function readBuildInfo() {
|
|
@@ -60,22 +60,9 @@ function packageVersion() {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
function currentGit() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
encoding: "utf8",
|
|
67
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
68
|
-
}).trim();
|
|
69
|
-
const status = execFileSync("git", ["status", "--porcelain"], {
|
|
70
|
-
cwd: checkoutRoot,
|
|
71
|
-
encoding: "utf8",
|
|
72
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
73
|
-
});
|
|
74
|
-
return { sha, dirty: status.trim().length > 0 };
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
63
|
+
const sha = gitOut(checkoutRoot, ["rev-parse", "--short", "HEAD"]);
|
|
64
|
+
const status = gitOut(checkoutRoot, ["status", "--porcelain"]);
|
|
65
|
+
return sha === null || status === null ? null : { sha, dirty: status.length > 0 };
|
|
79
66
|
}
|
|
80
67
|
const describe = (sha, dirty) => `${sha}${dirty ? "-dirty" : ""}`;
|
|
81
68
|
export function versionInfo() {
|
package/package.json
CHANGED