@cmgmyr/hive 1.11.4 → 1.11.6
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/agentKinds.js +6 -0
- package/dist/agentRecords.js +98 -0
- package/dist/backup.js +1 -1
- package/dist/build-info.json +1 -1
- package/dist/cli.js +46 -26
- package/dist/codexHome.js +0 -1
- package/dist/context.js +15 -26
- package/dist/crew.js +7 -24
- package/dist/dashboard.js +15 -21
- package/dist/db.js +37 -1
- package/dist/errors.js +3 -0
- package/dist/git.js +16 -0
- package/dist/globalConfig.js +1 -1
- package/dist/harnesses.js +0 -3
- package/dist/kickoff.js +6 -15
- package/dist/leadHandoffState.js +1 -1
- package/dist/leadLaunch.js +1 -1
- package/dist/leadWatch.js +1 -1
- package/dist/pads.js +70 -0
- package/dist/portfolio.js +4 -3
- package/dist/processes.js +12 -0
- package/dist/projectRemove.js +1 -1
- package/dist/projectYml.js +1 -1
- package/dist/queenAudit.js +2 -18
- package/dist/queenAuditStore.js +20 -0
- package/dist/questions.js +1 -1
- package/dist/result.js +2 -3
- package/dist/scheduler.js +4 -2
- package/dist/spawn.js +12 -35
- package/dist/todoScope.js +6 -0
- package/dist/todos.js +90 -0
- package/dist/tools/agents.js +13 -109
- package/dist/tools/pads.js +3 -82
- package/dist/tools/todos.js +8 -96
- package/dist/tools/wakes.js +7 -75
- package/dist/version.js +4 -17
- package/dist/wakeSql.js +1 -0
- package/dist/wakeTarget.js +76 -0
- package/package.json +1 -1
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const LEAD_KIND = "lead";
|
|
2
|
+
export const LEAD_ACTOR_PREFIX = `${LEAD_KIND}:`;
|
|
3
|
+
export const isLeadActorId = (actorId) => actorId.startsWith(LEAD_ACTOR_PREFIX);
|
|
4
|
+
export const mintLeadActorId = (agentId) => `${LEAD_ACTOR_PREFIX}${agentId}`;
|
|
5
|
+
export const LEAD_NAME = LEAD_KIND;
|
|
6
|
+
export const isReservedAgentName = (name) => name.toLowerCase() === LEAD_NAME;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { db } from "./db.js";
|
|
2
|
+
import { LEAD_KIND } from "./agentKinds.js";
|
|
3
|
+
import { observationFailed, observeRowOwnership, ownershipLiveness, rowOwnership } from "./tmux.js";
|
|
4
|
+
export const CLOSED_ROW_ORDER = "(parked_at != '') DESC, closed_at DESC, id DESC";
|
|
5
|
+
export function getAgentRow(projectId, id, notFoundHint) {
|
|
6
|
+
const row = db.prepare("SELECT * FROM agents WHERE project_id = ? AND id = ?").get(projectId, id);
|
|
7
|
+
if (!row)
|
|
8
|
+
throw new Error(`No agent ${id} in project ${projectId}. ${notFoundHint}`);
|
|
9
|
+
return row;
|
|
10
|
+
}
|
|
11
|
+
function closedAgentNamed(projectId, needle) {
|
|
12
|
+
return db
|
|
13
|
+
.prepare(`SELECT id, name, kind, parked_at FROM agents WHERE project_id = ? AND status != 'running' ORDER BY ${CLOSED_ROW_ORDER}`)
|
|
14
|
+
.all(projectId).find((r) => r.name.toLowerCase() === needle);
|
|
15
|
+
}
|
|
16
|
+
export function findAgent(projectId, ref) {
|
|
17
|
+
if (ref.agent_id != null) {
|
|
18
|
+
return getAgentRow(projectId, ref.agent_id, "Call agent_list.");
|
|
19
|
+
}
|
|
20
|
+
if (ref.name) {
|
|
21
|
+
const rows = db
|
|
22
|
+
.prepare("SELECT * FROM agents WHERE project_id = ? AND status = 'running' ORDER BY id")
|
|
23
|
+
.all(projectId);
|
|
24
|
+
const exact = rows.filter((r) => r.name === ref.name);
|
|
25
|
+
if (exact.length === 1)
|
|
26
|
+
return exact[0];
|
|
27
|
+
if (exact.length > 1) {
|
|
28
|
+
throw new Error(`Multiple running agents named "${ref.name}". Target by agent_id instead.`);
|
|
29
|
+
}
|
|
30
|
+
const needle = ref.name.toLowerCase();
|
|
31
|
+
const sameName = rows.filter((r) => r.name.toLowerCase() === needle);
|
|
32
|
+
if (sameName.length === 1)
|
|
33
|
+
return sameName[0];
|
|
34
|
+
const closed = closedAgentNamed(projectId, needle);
|
|
35
|
+
if (closed) {
|
|
36
|
+
if (closed.parked_at) {
|
|
37
|
+
throw new Error(`Agent ${closed.id} ("${closed.name}") is PARKED, not finished - it was paused on ` +
|
|
38
|
+
`${closed.parked_at} and its session is waiting. Bring it back with agent_resume(agent_id: ` +
|
|
39
|
+
`${closed.id}), or abandon the park with agent_close(agent_id: ${closed.id}).`);
|
|
40
|
+
}
|
|
41
|
+
const remedy = closed.kind === LEAD_KIND ? "Run `hive lead` to start a new one" : "Spawn a new worker";
|
|
42
|
+
throw new Error(`Agent ${closed.id} ("${closed.name}") is closed. ${remedy}, or target a running one by name or agent_id.`);
|
|
43
|
+
}
|
|
44
|
+
const partial = rows.filter((r) => r.name.toLowerCase().includes(needle));
|
|
45
|
+
if (partial.length === 1)
|
|
46
|
+
return partial[0];
|
|
47
|
+
if (partial.length > 1) {
|
|
48
|
+
const candidates = partial.map((r) => `${r.name} (agent_id ${r.id})`).join(", ");
|
|
49
|
+
throw new Error(`"${ref.name}" matches ${partial.length} running agents: ${candidates}. Use the full name or agent_id.`);
|
|
50
|
+
}
|
|
51
|
+
if (/^\d+$/.test(ref.name)) {
|
|
52
|
+
const id = Number(ref.name);
|
|
53
|
+
if (Number.isSafeInteger(id)) {
|
|
54
|
+
const byId = db
|
|
55
|
+
.prepare("SELECT * FROM agents WHERE project_id = ? AND id = ? AND status = 'running'")
|
|
56
|
+
.get(projectId, id);
|
|
57
|
+
if (byId)
|
|
58
|
+
return byId;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else if (/^[a-z]+:\d+$/i.test(ref.name)) {
|
|
62
|
+
const byActorId = db
|
|
63
|
+
.prepare("SELECT * FROM agents WHERE project_id = ? AND actor_id = ? AND status = 'running'")
|
|
64
|
+
.get(projectId, ref.name);
|
|
65
|
+
if (byActorId)
|
|
66
|
+
return byActorId;
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`No running agent matching "${ref.name}" in project ${projectId}. Call agent_list.`);
|
|
69
|
+
}
|
|
70
|
+
throw new Error("Pass agent_id or name.");
|
|
71
|
+
}
|
|
72
|
+
export const PROBE_FAILED_NOTE = "tmux could not be probed, so liveness is unknown. Nothing was changed. Retry in a few seconds.";
|
|
73
|
+
const unknownOwnershipNote = (snapshot) => observationFailed(snapshot) ? PROBE_FAILED_NOTE : UNVERIFIED_OWNERSHIP_NOTE;
|
|
74
|
+
export const UNVERIFIED_OWNERSHIP_NOTE = "This row's pane cannot be verified as its own: it is on another tmux socket, or no pane pid was recorded for it, so liveness is unknown and no screen was read. Nothing was changed. If the row is stale, a human or peer lead can retire it with agent_close(row_only=true).";
|
|
75
|
+
export const probeFailed = (agent) => new Error(`Agent ${agent.id} ("${agent.name}"): ${PROBE_FAILED_NOTE}`);
|
|
76
|
+
// A failed probe throws rather than answering unknown, so callers keep the retry wording.
|
|
77
|
+
export function observeOwnership(agent) {
|
|
78
|
+
const ownership = observeRowOwnership(agent);
|
|
79
|
+
if (ownership === "probe-failed")
|
|
80
|
+
throw probeFailed(agent);
|
|
81
|
+
return ownership;
|
|
82
|
+
}
|
|
83
|
+
export const REISSUED_PANE_NOTE = "Its recorded pane id now belongs to a different process (the recorded pane pid no longer matches), so its own process is gone and the pane is someone else's. Nothing was typed, read or killed there.";
|
|
84
|
+
export function notOwnedError(agent, ownership) {
|
|
85
|
+
const who = `Agent ${agent.id} ("${agent.name}")`;
|
|
86
|
+
if (ownership === "gone") {
|
|
87
|
+
return new Error(`${who} has no live tmux window (its process exited or the window was killed). Close it with agent_close and spawn a new one.`);
|
|
88
|
+
}
|
|
89
|
+
if (ownership === "reissued") {
|
|
90
|
+
return new Error(`${who}: pane ownership reads reissued. ${REISSUED_PANE_NOTE} Close the row with agent_close, which retires it without touching that pane.`);
|
|
91
|
+
}
|
|
92
|
+
return new Error(`${who}: pane ownership reads unknown. ${UNVERIFIED_OWNERSHIP_NOTE}`);
|
|
93
|
+
}
|
|
94
|
+
export function summaryLiveness(row, snapshot) {
|
|
95
|
+
if (row.status !== "running")
|
|
96
|
+
return false;
|
|
97
|
+
return ownershipLiveness(rowOwnership(row, snapshot));
|
|
98
|
+
}
|
package/dist/backup.js
CHANGED
|
@@ -2,7 +2,7 @@ import SqliteDb from "better-sqlite3";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { errorMessage } from "./
|
|
5
|
+
import { errorMessage } from "./errors.js";
|
|
6
6
|
export const BACKUP_DIRNAME = "backups";
|
|
7
7
|
export function backupsDir(dataDir) {
|
|
8
8
|
return join(dataDir, BACKUP_DIRNAME);
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.11.
|
|
1
|
+
{"version":"1.11.6","sha":"b4e9fb0","dirty":false,"build_id":"fb0debbc0e5e030650660c10505b25c26e60160403f384ce900920146e639961"}
|
package/dist/cli.js
CHANGED
|
@@ -23,7 +23,8 @@ import { confirmQueenWrite, listQueenAudit } from "./queenAudit.js";
|
|
|
23
23
|
import { formatRowCounts, projectRowCounts, removeProject } from "./projectRemove.js";
|
|
24
24
|
import { ensureHooksFile, ensureLeadHooksFile } from "./hooks.js";
|
|
25
25
|
import { errorMessage, registrationNoticeText, withTrailingNewline } from "./result.js";
|
|
26
|
-
import {
|
|
26
|
+
import { socketAwaitingWhere, dashboardFileContained, describeStall, HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX, HELD_REASON_UNSUBMITTED_INPUT_PREFIX, janitor, resolveDashboardFile, generateQueenDashboardNow, transcriptStaleness, } from "./scheduler.js";
|
|
27
|
+
import { ACTIVE_TIMER_WHERE } from "./wakeSql.js";
|
|
27
28
|
import { readTurnCount } from "./turnCount.js";
|
|
28
29
|
import { leadSessionEnded } from "./leadState.js";
|
|
29
30
|
import { heldReasonLabel } from "./heldLabel.js";
|
|
@@ -36,7 +37,7 @@ import { asNameClash, buildEnvFlags, closeAgentRow, isReservedAgentName, launchA
|
|
|
36
37
|
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
38
|
import { collectCrew, crewStoreProblem } from "./crew.js";
|
|
38
39
|
import { describeVisibility, formatLocal, processCounts } from "./dashboard.js";
|
|
39
|
-
import { runningCommandRow, runningCommandRows, snapshotProcesses, stopAllProcesses, stopLine, stopProcess, STOP_REASONS, } from "./processes.js";
|
|
40
|
+
import { runningCommandRow, runningCommandRows, snapshotProcesses, stopAllProcesses, stopLine, stopProcess, renderStart, STOP_REASONS, } from "./processes.js";
|
|
40
41
|
import { activeProfile, agentVarKeys, BUILT_IN_PROJECT_YML, configHash, loadProjectYml, mergedProjectVars, NO_PROFILE, PROJECT_YML_KEYS, resolveCommandDir, resolveFirstMessage, } from "./projectYml.js";
|
|
41
42
|
import { appendClaudeLeadArgs, crewPluginDir, isTrusted, leadIdentityEnv, renderLeadPosture } from "./leadLaunch.js";
|
|
42
43
|
import { handoffStatusSegment } from "./leadHandoff.js";
|
|
@@ -47,8 +48,9 @@ import { FIRST_MESSAGE_SHA_ENV, firstMessageDigest } from "./firstMessage.js";
|
|
|
47
48
|
import { ageSecondsSince, deriveProvenance, describeForHuman, describeLastLogEvent, humanizeAge, lastLogEvent, lastPermissionMode, reportsAgentStateLog, } from "./stateProvenance.js";
|
|
48
49
|
import { awaitingFirstPromptSql } from "./firstPrompt.js";
|
|
49
50
|
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,
|
|
51
|
-
import {
|
|
51
|
+
import { getTodoDetail, listTodoSummaries, } from "./todos.js";
|
|
52
|
+
import { ACTIVE_TODO_SQL, OPEN_BLOCKERS_SQL, TODO_STATUSES } from "./todoScope.js";
|
|
53
|
+
import { createPad, getActivePadByName, listActivePads, overwritePadContent, } from "./pads.js";
|
|
52
54
|
import { answerQuestion, listQuestions } from "./questions.js";
|
|
53
55
|
function usage() {
|
|
54
56
|
console.log(`hive — run a crew of Claude Code and Codex workers from one lead session
|
|
@@ -213,9 +215,15 @@ async function ensureTrusted(projectId, name, command, dir, env) {
|
|
|
213
215
|
db.prepare("INSERT OR IGNORE INTO command_trust (project_id, name, config_hash) VALUES (?, ?, ?)").run(projectId, name, hash);
|
|
214
216
|
return true;
|
|
215
217
|
}
|
|
216
|
-
function startYmlCommand(project, name, proc, config) {
|
|
218
|
+
function startYmlCommand(project, name, proc, config, trusted) {
|
|
219
|
+
if (!trusted)
|
|
220
|
+
return { status: "refused", reason: "untrusted" };
|
|
217
221
|
if (isReservedAgentName(name)) {
|
|
218
|
-
return
|
|
222
|
+
return {
|
|
223
|
+
status: "refused",
|
|
224
|
+
reason: "reserved-name",
|
|
225
|
+
message: `skipped: "lead" is reserved for this project's lead session and cannot be used as a process name`,
|
|
226
|
+
};
|
|
219
227
|
}
|
|
220
228
|
const existing = db
|
|
221
229
|
.prepare("SELECT id, tmux_target, tmux_socket FROM agents WHERE project_id = ? AND name = ? AND status = 'running'")
|
|
@@ -225,10 +233,15 @@ function startYmlCommand(project, name, proc, config) {
|
|
|
225
233
|
if (live) {
|
|
226
234
|
const windows = projectWindows(sessionName(), project.id);
|
|
227
235
|
const where = windows === null ? null : paneVisibility(existing.tmux_target, windows);
|
|
228
|
-
return where === "shown" || where === "hidden" ?
|
|
236
|
+
return { status: "already-running", location: where === "shown" || where === "hidden" ? where : null };
|
|
237
|
+
}
|
|
238
|
+
if (live === null) {
|
|
239
|
+
return {
|
|
240
|
+
status: "refused",
|
|
241
|
+
reason: "unknown-liveness",
|
|
242
|
+
message: "skipped: tmux could not be probed, so hive cannot tell whether it is already running",
|
|
243
|
+
};
|
|
229
244
|
}
|
|
230
|
-
if (live === null)
|
|
231
|
-
return "skipped: tmux could not be probed, so hive cannot tell whether it is already running";
|
|
232
245
|
closeAgentRow(existing.id);
|
|
233
246
|
}
|
|
234
247
|
let dir;
|
|
@@ -236,7 +249,7 @@ function startYmlCommand(project, name, proc, config) {
|
|
|
236
249
|
dir = resolveCommandDir(project.path, proc.dir);
|
|
237
250
|
}
|
|
238
251
|
catch (e) {
|
|
239
|
-
return `skipped: ${errorMessage(e)}
|
|
252
|
+
return { status: "refused", reason: "escaped-dir", message: `skipped: ${errorMessage(e)}` };
|
|
240
253
|
}
|
|
241
254
|
const placement = proc.visible
|
|
242
255
|
? (config.placement ?? (process.env.HIVE_SPAWN_PLACEMENT === "window" ? "window" : "split"))
|
|
@@ -260,17 +273,20 @@ function startYmlCommand(project, name, proc, config) {
|
|
|
260
273
|
setPaneTitle(target, shownPaneTitle(project.name, name));
|
|
261
274
|
if (landedInProjectId != null) {
|
|
262
275
|
const landedName = getProject(landedInProjectId)?.name ?? `project ${landedInProjectId}`;
|
|
263
|
-
return `started, landed in project "${landedName}"'s window
|
|
276
|
+
return { status: "started", message: `started, landed in project "${landedName}"'s window` };
|
|
264
277
|
}
|
|
265
|
-
return "started";
|
|
278
|
+
return { status: "started", message: "started" };
|
|
266
279
|
}
|
|
267
|
-
return
|
|
268
|
-
|
|
269
|
-
:
|
|
270
|
-
|
|
280
|
+
return {
|
|
281
|
+
status: "started",
|
|
282
|
+
message: inProcessesWindow
|
|
283
|
+
? "started (hidden)"
|
|
284
|
+
: "started in its own window: it opened this project's tmux session, and a session's first window " +
|
|
285
|
+
`cannot be a tile. Move it with: hive hide "${name}"`,
|
|
286
|
+
};
|
|
271
287
|
}
|
|
272
288
|
catch (e) {
|
|
273
|
-
return
|
|
289
|
+
return { status: "failed", message: errorMessage(e) };
|
|
274
290
|
}
|
|
275
291
|
}
|
|
276
292
|
function attach(session, project, window) {
|
|
@@ -636,9 +652,10 @@ async function cmdLead(argv) {
|
|
|
636
652
|
console.log(`- ${name}: defined, auto_start off (start with: hive start "${name}")`);
|
|
637
653
|
continue;
|
|
638
654
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
655
|
+
const trusted = await ensureTrusted(project.id, name, proc.command, proc.dir, proc.env);
|
|
656
|
+
const result = startYmlCommand(project, name, proc, config, trusted);
|
|
657
|
+
if (trusted)
|
|
658
|
+
console.log(`- ${name}: ${renderStart(result)}`);
|
|
642
659
|
}
|
|
643
660
|
}
|
|
644
661
|
if (registrationNotice) {
|
|
@@ -1366,9 +1383,12 @@ async function cmdStart(argv) {
|
|
|
1366
1383
|
console.log(processNotDefined(config, name));
|
|
1367
1384
|
process.exit(1);
|
|
1368
1385
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1386
|
+
const trusted = await ensureTrusted(project.id, name, proc.command, proc.dir, proc.env);
|
|
1387
|
+
const result = startYmlCommand(project, name, proc, config, trusted);
|
|
1388
|
+
if (trusted)
|
|
1389
|
+
console.log(`${name}: ${renderStart(result)}`);
|
|
1390
|
+
if (!trusted || result.status === "failed" || result.status === "refused")
|
|
1391
|
+
process.exitCode = 1;
|
|
1372
1392
|
}
|
|
1373
1393
|
function processNotDefined(config, name) {
|
|
1374
1394
|
const known = Object.keys(config?.processes ?? {});
|
|
@@ -1631,7 +1651,7 @@ function cmdStatus() {
|
|
|
1631
1651
|
.prepare("SELECT * FROM agents WHERE project_id = ? AND status = 'running' ORDER BY kind DESC, id")
|
|
1632
1652
|
.all(project.id);
|
|
1633
1653
|
const todos = db
|
|
1634
|
-
.prepare(
|
|
1654
|
+
.prepare(`SELECT COUNT(*) AS n FROM todos t WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}`)
|
|
1635
1655
|
.get(project.id).n;
|
|
1636
1656
|
const { wakes, heldWakes } = db
|
|
1637
1657
|
.prepare(`SELECT COUNT(*) AS wakes, COUNT(held_at) AS heldWakes FROM wakes WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE}`)
|
|
@@ -2769,9 +2789,9 @@ function cmdStatusline() {
|
|
|
2769
2789
|
const count = (sql) => db.prepare(sql).get(project.id).n;
|
|
2770
2790
|
const agents = count("SELECT COUNT(*) AS n FROM agents WHERE project_id = ? AND status = 'running' AND kind = 'agent'");
|
|
2771
2791
|
const commands = count("SELECT COUNT(*) AS n FROM agents WHERE project_id = ? AND status = 'running' AND kind = 'command'");
|
|
2772
|
-
const todos = count(
|
|
2792
|
+
const todos = count(`SELECT COUNT(*) AS n FROM todos t WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}`);
|
|
2773
2793
|
const ready = count(`SELECT COUNT(*) AS n FROM todos t
|
|
2774
|
-
WHERE t.project_id = ? AND
|
|
2794
|
+
WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}
|
|
2775
2795
|
AND NOT EXISTS (${OPEN_BLOCKERS_SQL})`);
|
|
2776
2796
|
const pads = count("SELECT COUNT(*) AS n FROM pads WHERE project_id = ? AND archived = 0");
|
|
2777
2797
|
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,12 +1,12 @@
|
|
|
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";
|
|
5
4
|
import { storeDir } from "./dataDir.js";
|
|
6
5
|
import { db } from "./db.js";
|
|
7
|
-
import { LEAD_KIND } from "./
|
|
6
|
+
import { LEAD_KIND } from "./agentKinds.js";
|
|
8
7
|
import { rowOwnership } from "./tmux.js";
|
|
9
|
-
import { recordQueenRefusal } from "./
|
|
8
|
+
import { recordQueenRefusal } from "./queenAuditStore.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
|
-
import { ACTIVE_TIMER_WHERE } from "./
|
|
11
|
+
import { ACTIVE_TIMER_WHERE } from "./wakeSql.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,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { listQuestions, pendingQuestionCount } from "./questions.js";
|
|
3
|
+
import { ACTIVE_TIMER_WHERE } from "./wakeSql.js";
|
|
3
4
|
import { db } from "./db.js";
|
|
5
|
+
import { ACTIVE_TODO_SQL, NOT_COMPLETED_TODO_SQL, OPEN_BLOCKERS_SQL } from "./todoScope.js";
|
|
4
6
|
import { getProject } from "./context.js";
|
|
5
7
|
export const describeVisibility = (visibility) => visibility === null ? "running" : visibility === "window" ? "own window" : visibility;
|
|
6
8
|
export function processCounts(procs) {
|
|
@@ -13,7 +15,6 @@ export function processCounts(procs) {
|
|
|
13
15
|
}
|
|
14
16
|
import { awaitingFirstPrompt } from "./firstPrompt.js";
|
|
15
17
|
import { SLUG_MAX_LEN, cutToUnitBudget } from "./slug.js";
|
|
16
|
-
const requireFromHere = createRequire(import.meta.url);
|
|
17
18
|
export const TODO_CAP = 100;
|
|
18
19
|
export const AGENT_CAP = 50;
|
|
19
20
|
export const WAKE_CAP = 50;
|
|
@@ -86,7 +87,6 @@ function fetchProject(projectId) {
|
|
|
86
87
|
throw new Error(`No project ${projectId}.`);
|
|
87
88
|
return row;
|
|
88
89
|
}
|
|
89
|
-
const PENDING_WAKE_WHERE = "cancelled_at IS NULL AND (fired_at IS NULL OR repeat_every_ms IS NOT NULL)";
|
|
90
90
|
function agentStatusLevel(state) {
|
|
91
91
|
if (state === "working")
|
|
92
92
|
return "live";
|
|
@@ -120,19 +120,16 @@ function fetchNowAgents(projectId) {
|
|
|
120
120
|
function fetchNextWake(projectId) {
|
|
121
121
|
return db
|
|
122
122
|
.prepare(`SELECT kind, due_at, max_wait_at, deliver_actor FROM wakes
|
|
123
|
-
WHERE project_id = ? AND ${
|
|
123
|
+
WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE}
|
|
124
124
|
ORDER BY COALESCE(due_at, max_wait_at, created_at) LIMIT 1`)
|
|
125
125
|
.get(projectId);
|
|
126
126
|
}
|
|
127
127
|
function fetchTodoCounts(projectId) {
|
|
128
128
|
return db
|
|
129
129
|
.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,
|
|
130
|
+
(SELECT COUNT(*) FROM todos t WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}) AS open,
|
|
131
|
+
(SELECT COUNT(*) FROM todos t WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}
|
|
132
|
+
AND EXISTS (${OPEN_BLOCKERS_SQL})) AS blocked,
|
|
136
133
|
(SELECT COUNT(*) FROM todos
|
|
137
134
|
WHERE project_id = ? AND completed_at IS NOT NULL
|
|
138
135
|
AND date(completed_at, 'localtime') >= date('now', 'localtime', '-6 days')) AS completed7d`)
|
|
@@ -262,10 +259,9 @@ function renderBoardSection(index, projectId) {
|
|
|
262
259
|
}
|
|
263
260
|
const OPEN_TODOS_SQL = `
|
|
264
261
|
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
|
|
262
|
+
EXISTS (${OPEN_BLOCKERS_SQL}) AS blocked
|
|
267
263
|
FROM todos t
|
|
268
|
-
WHERE t.project_id = ? AND
|
|
264
|
+
WHERE t.project_id = ? AND ${ACTIVE_TODO_SQL}
|
|
269
265
|
ORDER BY CASE t.priority WHEN 'high' THEN 0 WHEN 'medium' THEN 1 ELSE 2 END, t.id`;
|
|
270
266
|
function fetchBlockersFor(todoIds) {
|
|
271
267
|
const byTodo = new Map();
|
|
@@ -274,7 +270,7 @@ function fetchBlockersFor(todoIds) {
|
|
|
274
270
|
const placeholders = todoIds.map(() => "?").join(",");
|
|
275
271
|
const rows = db
|
|
276
272
|
.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
|
|
273
|
+
WHERE b.todo_id IN (${placeholders}) AND t.${NOT_COMPLETED_TODO_SQL}`)
|
|
278
274
|
.all(...todoIds);
|
|
279
275
|
for (const row of rows) {
|
|
280
276
|
const list = byTodo.get(row.todo_id) ?? [];
|
|
@@ -291,11 +287,11 @@ function todoBadgeLevel(status, blocked) {
|
|
|
291
287
|
function renderTodosSection(index, projectId) {
|
|
292
288
|
const all = db.prepare(OPEN_TODOS_SQL).all(projectId);
|
|
293
289
|
const shown = all.slice(0, TODO_CAP);
|
|
294
|
-
const blockersByTodo = fetchBlockersFor(shown.filter((t) => t.
|
|
295
|
-
const blockedCount = all.filter((t) => t.
|
|
290
|
+
const blockersByTodo = fetchBlockersFor(shown.filter((t) => t.blocked).map((t) => t.id));
|
|
291
|
+
const blockedCount = all.filter((t) => t.blocked).length;
|
|
296
292
|
const rows = shown
|
|
297
293
|
.map((t) => {
|
|
298
|
-
const blocked = t.
|
|
294
|
+
const blocked = Boolean(t.blocked);
|
|
299
295
|
const blockers = blocked
|
|
300
296
|
? (blockersByTodo.get(t.id) ?? []).map((b) => `${mono(`#${b.id}`)} ${prose(b.title)}`).join(", ")
|
|
301
297
|
: "";
|
|
@@ -324,7 +320,7 @@ function renderTodosSection(index, projectId) {
|
|
|
324
320
|
`<p class="filter-none" id="todo-filter-none" hidden>No open todo matches that filter.</p>`
|
|
325
321
|
: "";
|
|
326
322
|
const body = filter +
|
|
327
|
-
renderCappedList(shown.length, all.length, "open todos", "todos", rows, emptyState("No open todos.", "
|
|
323
|
+
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
324
|
return section(index, "todos", "Todos", `${all.length} open, ${blockedCount} blocked`, "checklist", String(shown.length), body);
|
|
329
325
|
}
|
|
330
326
|
export const CHART_DAYS = 7;
|
|
@@ -431,7 +427,7 @@ function renderThroughputSection(index, stats) {
|
|
|
431
427
|
function renderWakesSection(index, projectId) {
|
|
432
428
|
const all = db
|
|
433
429
|
.prepare(`SELECT id, kind, body, owner, deliver_actor, due_at, max_wait_at, fire_count, repeat_every_ms, held_at
|
|
434
|
-
FROM wakes WHERE project_id = ? AND ${
|
|
430
|
+
FROM wakes WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE}
|
|
435
431
|
ORDER BY COALESCE(due_at, max_wait_at, created_at)`)
|
|
436
432
|
.all(projectId);
|
|
437
433
|
const shown = all.slice(0, WAKE_CAP);
|
|
@@ -465,8 +461,6 @@ function questionAge(createdAt) {
|
|
|
465
461
|
return "now";
|
|
466
462
|
}
|
|
467
463
|
function renderQuestionsSection(index, projectId) {
|
|
468
|
-
// A static import of questions.js cycles through tools/wakes.ts and scheduler.ts.
|
|
469
|
-
const { listQuestions, pendingQuestionCount } = requireFromHere("./questions.js");
|
|
470
464
|
const questions = listQuestions(projectId);
|
|
471
465
|
if (questions.length === 0)
|
|
472
466
|
return "";
|
package/dist/db.js
CHANGED
|
@@ -4,7 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
import { guardAbi } from "./abi.js";
|
|
5
5
|
import { guardStoreDir } from "./dataDir.js";
|
|
6
6
|
import { maybeBackupBeforeMigrations } from "./backup.js";
|
|
7
|
-
import { errorMessage } from "./
|
|
7
|
+
import { errorMessage } from "./errors.js";
|
|
8
8
|
export const dataDir = guardStoreDir();
|
|
9
9
|
mkdirSync(dataDir, { recursive: true });
|
|
10
10
|
function sleepSync(ms) {
|
|
@@ -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/errors.js
ADDED
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/globalConfig.js
CHANGED
|
@@ -3,7 +3,7 @@ import { chmodSync, closeSync, lstatSync, mkdirSync, openSync, readFileSync, ren
|
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { isMap, parse, parseDocument } from "yaml";
|
|
5
5
|
import { storeDir } from "./dataDir.js";
|
|
6
|
-
import { errorMessage } from "./
|
|
6
|
+
import { errorMessage } from "./errors.js";
|
|
7
7
|
export const ATTACH_MODES = ["auto", "raw", "control"];
|
|
8
8
|
export const AUTO_ATTACH_MODES = ["auto", "on", "off"];
|
|
9
9
|
export function isAttachMode(value) {
|
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.
|