@cmgmyr/hive 1.11.5 → 1.11.7
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/claudeWake.js +52 -4
- package/dist/cli.js +69 -43
- package/dist/context.js +2 -2
- package/dist/crew.js +5 -4
- package/dist/dashboard.js +10 -12
- package/dist/db.js +1 -1
- package/dist/errors.js +3 -0
- package/dist/etime.js +37 -0
- package/dist/firstPrompt.js +6 -0
- package/dist/globalConfig.js +1 -1
- package/dist/heldLabel.js +1 -1
- package/dist/kickoff.js +2 -2
- package/dist/leadHandoff.js +12 -11
- package/dist/leadHandoffCli.js +22 -1
- package/dist/leadHandoffState.js +1 -2
- package/dist/leadLaunch.js +1 -1
- package/dist/leadMessage.js +13 -0
- package/dist/leadMessageDelivery.js +14 -74
- package/dist/leadState.js +3 -2
- package/dist/leadWatch.js +1 -1
- package/dist/leaseStore.js +54 -0
- package/dist/pads.js +70 -0
- package/dist/portfolio.js +7 -3
- package/dist/projectRemove.js +1 -1
- package/dist/projectYml.js +1 -1
- package/dist/ptys.js +2 -9
- package/dist/queenAudit.js +2 -18
- package/dist/queenAuditStore.js +20 -0
- package/dist/queenDashboard.js +2 -1
- package/dist/questions.js +1 -1
- package/dist/result.js +2 -3
- package/dist/scheduler.js +42 -166
- package/dist/spawn.js +2 -6
- package/dist/staleServers.js +4 -28
- package/dist/stateProvenance.js +1 -3
- package/dist/storeClock.js +4 -0
- package/dist/teardown.js +2 -1
- package/dist/todoScope.js +1 -0
- package/dist/todos.js +90 -0
- package/dist/tools/agents.js +4 -96
- package/dist/tools/leases.js +5 -25
- package/dist/tools/pads.js +3 -82
- package/dist/tools/todos.js +6 -89
- package/dist/tools/wakes.js +12 -88
- package/dist/wakeSql.js +135 -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.7","sha":"4861c81","dirty":false,"build_id":"2b159fa2c2519fbd7edef9cf4396ac349c3fb25ec433f76a9b91c5ad98188f43"}
|
package/dist/claudeWake.js
CHANGED
|
@@ -1,14 +1,62 @@
|
|
|
1
1
|
import { createConnection } from "node:net";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { db } from "./db.js";
|
|
4
|
+
import { loadProjectYml } from "./projectYml.js";
|
|
5
|
+
export { DELIVERY_METHOD } from "./wakeSql.js";
|
|
2
6
|
export const CROSS_SESSION_CLOSE = "</cross-session-message>";
|
|
7
|
+
export const ENVELOPE_OPEN = '<cross-session-message from="';
|
|
8
|
+
export const ENVELOPE_SENDER_CLOSE = '" from-name="hive">';
|
|
3
9
|
export const SOCKET_WAKE_FOOTER = "Automated hive wake. Do not reply to this sender.";
|
|
4
|
-
const
|
|
10
|
+
export const SOCKET_MESSAGE_FOOTER = "Automated hive worker message. Do not reply to this socket sender.";
|
|
11
|
+
export const SOCKET_WRITE_BOUND_MS = 1000;
|
|
12
|
+
export const SOCKET_CONFIRM_GRACE_SECONDS = 60;
|
|
5
13
|
export function senderAddress(socketPath) {
|
|
6
14
|
const encoded = socketPath.replace(/[^A-Za-z0-9/._-]/gu, (c) => [...Buffer.from(c)].map((b) => `%${b.toString(16).toUpperCase().padStart(2, "0")}`).join(""));
|
|
7
15
|
return `uds:${encoded}`;
|
|
8
16
|
}
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
export function socketSenderAddress(dir) {
|
|
18
|
+
return senderAddress(join(dir, "wake-sender.sock"));
|
|
19
|
+
}
|
|
20
|
+
function claudeWakeFrame(sender, text) {
|
|
21
|
+
const content = `${ENVELOPE_OPEN}${sender}${ENVELOPE_SENDER_CLOSE}\n${text}\n${CROSS_SESSION_CLOSE}`;
|
|
22
|
+
return JSON.stringify({ type: "user", from: sender, message: { role: "user", content } }) + "\n";
|
|
23
|
+
}
|
|
24
|
+
export function quietMessagingOn(projectId) {
|
|
25
|
+
const project = db.prepare("SELECT path FROM projects WHERE id = ?").get(projectId);
|
|
26
|
+
return project !== undefined && loadProjectYml(project.path).config?.quiet_messaging === true;
|
|
27
|
+
}
|
|
28
|
+
export function promptMarkerEvidenceSql(markerAt) {
|
|
29
|
+
const evPrompt = "(CASE WHEN json_valid(ev.payload) THEN COALESCE(json_extract(ev.payload, '$.prompt'), '') ELSE ev.payload END)";
|
|
30
|
+
const headerEnd = `instr(${evPrompt}, char(10))`;
|
|
31
|
+
return `(${markerAt(evPrompt, "1")}\n OR (${headerEnd} > ${ENVELOPE_OPEN.length + ENVELOPE_SENDER_CLOSE.length}
|
|
32
|
+
AND substr(${evPrompt}, 1, ${ENVELOPE_OPEN.length}) = '${ENVELOPE_OPEN}'
|
|
33
|
+
AND substr(${evPrompt}, ${headerEnd} - ${ENVELOPE_SENDER_CLOSE.length}, ${ENVELOPE_SENDER_CLOSE.length}) = '${ENVELOPE_SENDER_CLOSE}'
|
|
34
|
+
AND instr(substr(${evPrompt}, ${ENVELOPE_OPEN.length + 1}, ${headerEnd} - ${ENVELOPE_OPEN.length + ENVELOPE_SENDER_CLOSE.length + 1}), '"') = 0
|
|
35
|
+
AND ${markerAt(evPrompt, `${headerEnd} + 1`)}
|
|
36
|
+
AND instr(${evPrompt}, char(10) || '${CROSS_SESSION_CLOSE}') > ${headerEnd}))`;
|
|
37
|
+
}
|
|
38
|
+
export function hiveWorkerEnvelopeLikeSql(promptSql) {
|
|
39
|
+
return `(${promptSql} LIKE '${ENVELOPE_OPEN}%${ENVELOPE_SENDER_CLOSE}' || char(10) || '[hive:worker %] [message #%,%' || char(10) || '${CROSS_SESSION_CLOSE}%')`;
|
|
40
|
+
}
|
|
41
|
+
export function isHiveWorkerEnvelope(prompt) {
|
|
42
|
+
const p = prompt.toLowerCase();
|
|
43
|
+
const open = ENVELOPE_OPEN.toLowerCase().replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
44
|
+
const senderClose = ENVELOPE_SENDER_CLOSE.toLowerCase().replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
45
|
+
const close = CROSS_SESSION_CLOSE.toLowerCase().replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
46
|
+
return new RegExp(`^${open}[\\s\\S]*${senderClose}\\n\\[hive:worker [\\s\\S]*\\] \\[message #[\\s\\S]*,[\\s\\S]*\\n${close}`).test(p);
|
|
47
|
+
}
|
|
48
|
+
export function socketGraceDue(actorId, attemptAt, panePid, readTurn) {
|
|
49
|
+
const firstStop = db
|
|
50
|
+
.prepare("SELECT MIN(created_at) AS t FROM agent_state_log WHERE actor_id = ? AND event = 'stop' AND created_at >= ?")
|
|
51
|
+
.get(actorId, attemptAt).t;
|
|
52
|
+
let from = firstStop;
|
|
53
|
+
if (from === null) {
|
|
54
|
+
const turn = readTurn();
|
|
55
|
+
if (turn !== null && turn.state === "working" && turn.pane_pid === panePid)
|
|
56
|
+
return false;
|
|
57
|
+
from = attemptAt;
|
|
58
|
+
}
|
|
59
|
+
return db.prepare("SELECT (julianday('now') - julianday(?)) * 86400 >= ? AS due").get(from, SOCKET_CONFIRM_GRACE_SECONDS).due === 1;
|
|
12
60
|
}
|
|
13
61
|
// True only for a completed local write. The socket sends no ack, so true never means the lead read it.
|
|
14
62
|
export function postClaudeWake(options) {
|
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 { dashboardFileContained, describeStall, janitor, resolveDashboardFile, generateQueenDashboardNow, transcriptStaleness, } from "./scheduler.js";
|
|
27
|
+
import { ACTIVE_TIMER_WHERE, HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX, HELD_REASON_UNSUBMITTED_INPUT_PREFIX, heldWakeWhere, socketAwaitingWhere, } from "./wakeSql.js";
|
|
27
28
|
import { readTurnCount } from "./turnCount.js";
|
|
28
29
|
import { leadSessionEnded } from "./leadState.js";
|
|
29
30
|
import { heldReasonLabel } from "./heldLabel.js";
|
|
@@ -47,9 +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 { ACTIVE_TODO_SQL, OPEN_BLOCKERS_SQL } from "./todoScope.js";
|
|
52
|
-
import { createPad, getActivePadByName, listActivePads, overwritePadContent, } from "./
|
|
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";
|
|
53
54
|
import { answerQuestion, listQuestions } from "./questions.js";
|
|
54
55
|
function usage() {
|
|
55
56
|
console.log(`hive — run a crew of Claude Code and Codex workers from one lead session
|
|
@@ -389,14 +390,11 @@ function reportMigrationResult(result) {
|
|
|
389
390
|
console.error(`! ${warning}`);
|
|
390
391
|
}
|
|
391
392
|
async function cmdLead(argv) {
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const noDashboard = argv.includes("--no-dashboard");
|
|
398
|
-
const detach = argv.includes("--detach");
|
|
399
|
-
const path = argv.find((a) => !a.startsWith("--"));
|
|
393
|
+
const parsed = parseArgs(argv, { flags: ["--no-dashboard", "--detach"] });
|
|
394
|
+
rejectUnknownFlags("lead", parsed, "--no-dashboard and --detach");
|
|
395
|
+
const noDashboard = parsed.flags.has("--no-dashboard");
|
|
396
|
+
const detach = parsed.flags.has("--detach");
|
|
397
|
+
const path = parsed.positional[0];
|
|
400
398
|
let registrationNotice = null;
|
|
401
399
|
try {
|
|
402
400
|
const project = resolveProjectForWrite("hive lead", path, (text) => {
|
|
@@ -1162,6 +1160,8 @@ function profileSizeAdvisory(name, file) {
|
|
|
1162
1160
|
}
|
|
1163
1161
|
function cmdProfile(argv) {
|
|
1164
1162
|
const [sub, ...rest] = argv;
|
|
1163
|
+
if (sub?.startsWith("-"))
|
|
1164
|
+
rejectUnknownFlags("profile", parseArgs(argv, {}), "none");
|
|
1165
1165
|
try {
|
|
1166
1166
|
switch (sub) {
|
|
1167
1167
|
case undefined:
|
|
@@ -1339,12 +1339,9 @@ function openDashboardOnce(project, dashboardFile) {
|
|
|
1339
1339
|
}
|
|
1340
1340
|
}
|
|
1341
1341
|
function cmdAttach(argv) {
|
|
1342
|
-
const
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
process.exit(1);
|
|
1346
|
-
}
|
|
1347
|
-
const path = argv.find((a) => !a.startsWith("--"));
|
|
1342
|
+
const parsed = parseArgs(argv, {});
|
|
1343
|
+
rejectUnknownFlags("attach", parsed, "none");
|
|
1344
|
+
const path = parsed.positional[0];
|
|
1348
1345
|
const project = resolveProjectForWrite("hive attach", path);
|
|
1349
1346
|
maybeOpenDashboard(project, !!loadProjectYml(project.path).config?.dashboard);
|
|
1350
1347
|
const session = sessionName();
|
|
@@ -1495,8 +1492,7 @@ const leadPaneExitedHookArmed = () => globalHooks(LEAD_PANE_EXITED_HOOK).some((e
|
|
|
1495
1492
|
function cmdLeadPaneExited(argv) {
|
|
1496
1493
|
try {
|
|
1497
1494
|
const parsed = parseArgs(argv, {});
|
|
1498
|
-
|
|
1499
|
-
return;
|
|
1495
|
+
rejectUnknownFlags("lead-pane-exited", parsed, "none");
|
|
1500
1496
|
const pane = parsed.positional[0];
|
|
1501
1497
|
if (!pane)
|
|
1502
1498
|
return;
|
|
@@ -2314,14 +2310,13 @@ function reportEffectiveConfig(loaded) {
|
|
|
2314
2310
|
: `${autoAttach} (default; set with \`hive setup --auto-attach\`; source: built-in)`);
|
|
2315
2311
|
}
|
|
2316
2312
|
function cmdDoctor(argv) {
|
|
2317
|
-
const
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
}
|
|
2313
|
+
const parsed = parseArgs(argv, { flags: ["--strict", "--verbose"] });
|
|
2314
|
+
const unexpected = argv.find((arg) => arg !== "--strict" && arg !== "--verbose");
|
|
2315
|
+
if (unexpected !== undefined)
|
|
2316
|
+
parsed.unknown = [unexpected];
|
|
2317
|
+
rejectUnknownFlags("doctor", parsed, "--strict and --verbose");
|
|
2318
|
+
const strict = parsed.flags.has("--strict");
|
|
2319
|
+
doctorVerbose = parsed.flags.has("--verbose");
|
|
2325
2320
|
reportMigrationResult(migrateLegacyConfig());
|
|
2326
2321
|
let failures = 0;
|
|
2327
2322
|
const fail = (label, ...lines) => {
|
|
@@ -2800,13 +2795,13 @@ function cmdStatusline() {
|
|
|
2800
2795
|
const held = db
|
|
2801
2796
|
.prepare(`WITH chosen AS (
|
|
2802
2797
|
SELECT held_reason, first_held_at FROM wakes
|
|
2803
|
-
WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE} AND
|
|
2798
|
+
WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE} AND ${heldWakeWhere()}
|
|
2804
2799
|
ORDER BY (held_reason LIKE ?) DESC, (held_reason LIKE ?) ASC,
|
|
2805
2800
|
first_held_at IS NULL ASC, first_held_at ASC
|
|
2806
2801
|
LIMIT 1
|
|
2807
2802
|
)
|
|
2808
2803
|
SELECT
|
|
2809
|
-
(SELECT COUNT(*) FROM wakes WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE} AND
|
|
2804
|
+
(SELECT COUNT(*) FROM wakes WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE} AND ${heldWakeWhere()}) AS n,
|
|
2810
2805
|
(SELECT held_reason FROM chosen) AS held_reason,
|
|
2811
2806
|
(SELECT first_held_at FROM chosen) AS first_held_at`)
|
|
2812
2807
|
.get(project.id, `${HELD_REASON_UNSUBMITTED_INPUT_PREFIX}%`, `${HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX}%`, project.id);
|
|
@@ -3255,11 +3250,26 @@ function requireFlagValues(command, parsed) {
|
|
|
3255
3250
|
process.exit(1);
|
|
3256
3251
|
}
|
|
3257
3252
|
function cmdTodos(argv) {
|
|
3253
|
+
const parsed = parseArgs(argv, { flags: ["--all"], valued: ["--status", "--tag"] });
|
|
3254
|
+
for (const flag of ["--status", "--tag"]) {
|
|
3255
|
+
const index = argv.indexOf(flag);
|
|
3256
|
+
const value = index === -1 ? undefined : argv[index + 1];
|
|
3257
|
+
if (index === -1 || (value !== undefined && !value.startsWith("--")))
|
|
3258
|
+
continue;
|
|
3259
|
+
if (flag === "--status") {
|
|
3260
|
+
console.log(`Usage: hive todos --status <s> where <s> is one of: ${TODO_STATUSES.join(", ")}`);
|
|
3261
|
+
}
|
|
3262
|
+
else {
|
|
3263
|
+
console.log("Usage: hive todos --tag <t>");
|
|
3264
|
+
}
|
|
3265
|
+
process.exit(1);
|
|
3266
|
+
}
|
|
3267
|
+
rejectUnknownFlags("todos", parsed, "--all, --status <status>, --tag <tag>");
|
|
3268
|
+
requireFlagValues("todos", parsed);
|
|
3258
3269
|
const project = resolveProjectForRead("todos");
|
|
3259
|
-
const all =
|
|
3260
|
-
const
|
|
3261
|
-
|
|
3262
|
-
if (statusIdx !== -1 && (status === undefined || status.startsWith("--"))) {
|
|
3270
|
+
const all = parsed.flags.has("--all");
|
|
3271
|
+
const status = parsed.values.get("--status");
|
|
3272
|
+
if (parsed.values.has("--status") && status === undefined) {
|
|
3263
3273
|
console.log(`Usage: hive todos --status <s> where <s> is one of: ${TODO_STATUSES.join(", ")}`);
|
|
3264
3274
|
process.exit(1);
|
|
3265
3275
|
}
|
|
@@ -3267,9 +3277,8 @@ function cmdTodos(argv) {
|
|
|
3267
3277
|
console.log(`Unknown status "${status}". Valid: ${TODO_STATUSES.join(", ")}`);
|
|
3268
3278
|
process.exit(1);
|
|
3269
3279
|
}
|
|
3270
|
-
const
|
|
3271
|
-
|
|
3272
|
-
if (tagIdx !== -1 && (tag === undefined || tag.startsWith("--"))) {
|
|
3280
|
+
const tag = parsed.values.get("--tag");
|
|
3281
|
+
if (parsed.values.has("--tag") && tag === undefined) {
|
|
3273
3282
|
console.log("Usage: hive todos --tag <t>");
|
|
3274
3283
|
process.exit(1);
|
|
3275
3284
|
}
|
|
@@ -3299,8 +3308,10 @@ function cmdTodos(argv) {
|
|
|
3299
3308
|
}
|
|
3300
3309
|
}
|
|
3301
3310
|
function cmdTodo(argv) {
|
|
3311
|
+
const parsed = parseArgs(argv, {});
|
|
3312
|
+
rejectUnknownFlags("todo", parsed, "none");
|
|
3302
3313
|
const project = resolveProjectForRead("todo");
|
|
3303
|
-
const id = Number(
|
|
3314
|
+
const id = Number(parsed.positional[0]);
|
|
3304
3315
|
if (!Number.isInteger(id)) {
|
|
3305
3316
|
console.log("Usage: hive todo <id> (run inside the project)");
|
|
3306
3317
|
process.exit(1);
|
|
@@ -3504,6 +3515,8 @@ let rest = args.slice(1);
|
|
|
3504
3515
|
if (command === "--help" || command === "-h" || command === "help")
|
|
3505
3516
|
usage();
|
|
3506
3517
|
if (command === "--version" || command === "-v") {
|
|
3518
|
+
const parsed = parseArgs(rest, { flags: ["--check"] });
|
|
3519
|
+
rejectUnknownFlags("version", parsed, "--check");
|
|
3507
3520
|
const { line, drift } = versionInfo();
|
|
3508
3521
|
console.log(line);
|
|
3509
3522
|
if (drift)
|
|
@@ -3583,6 +3596,7 @@ try {
|
|
|
3583
3596
|
cmdHide(rest);
|
|
3584
3597
|
break;
|
|
3585
3598
|
case "status":
|
|
3599
|
+
rejectUnknownFlags("status", parseArgs(rest, {}), "none");
|
|
3586
3600
|
cmdStatus();
|
|
3587
3601
|
break;
|
|
3588
3602
|
case "portfolio":
|
|
@@ -3607,6 +3621,7 @@ try {
|
|
|
3607
3621
|
cmdDoctor(rest);
|
|
3608
3622
|
break;
|
|
3609
3623
|
case "pads":
|
|
3624
|
+
rejectUnknownFlags("pads", parseArgs(rest, {}), "none");
|
|
3610
3625
|
cmdPads();
|
|
3611
3626
|
break;
|
|
3612
3627
|
case "pad":
|
|
@@ -3625,6 +3640,7 @@ try {
|
|
|
3625
3640
|
cmdAnswer(rest);
|
|
3626
3641
|
break;
|
|
3627
3642
|
case "backups":
|
|
3643
|
+
rejectUnknownFlags("backups", parseArgs(rest, {}), "none");
|
|
3628
3644
|
cmdBackups();
|
|
3629
3645
|
break;
|
|
3630
3646
|
case "restore":
|
|
@@ -3633,19 +3649,29 @@ try {
|
|
|
3633
3649
|
case "project":
|
|
3634
3650
|
await cmdProject(rest);
|
|
3635
3651
|
break;
|
|
3636
|
-
case "runbook":
|
|
3637
|
-
|
|
3652
|
+
case "runbook": {
|
|
3653
|
+
const parsed = parseArgs(rest, {});
|
|
3654
|
+
rejectUnknownFlags("runbook", parsed, "none");
|
|
3655
|
+
cmdRunbook(parsed.positional[0]);
|
|
3638
3656
|
break;
|
|
3639
|
-
|
|
3640
|
-
|
|
3657
|
+
}
|
|
3658
|
+
case "posture": {
|
|
3659
|
+
const parsed = parseArgs(rest, {});
|
|
3660
|
+
rejectUnknownFlags("posture", parsed, "none");
|
|
3661
|
+
cmdPosture(parsed.positional[0]);
|
|
3641
3662
|
break;
|
|
3663
|
+
}
|
|
3642
3664
|
case "profile":
|
|
3643
3665
|
cmdProfile(rest);
|
|
3644
3666
|
break;
|
|
3645
|
-
case "kickoff":
|
|
3667
|
+
case "kickoff": {
|
|
3668
|
+
const parsed = parseArgs(rest, { flags: ["--explain", "--codex"] });
|
|
3669
|
+
rejectUnknownFlags("kickoff", parsed, "--explain, --codex");
|
|
3646
3670
|
await (await import("./kickoff.js")).runKickoff(rest);
|
|
3647
3671
|
break;
|
|
3672
|
+
}
|
|
3648
3673
|
case "statusline":
|
|
3674
|
+
rejectUnknownFlags("statusline", parseArgs(rest, {}), "none");
|
|
3649
3675
|
cmdStatusline();
|
|
3650
3676
|
break;
|
|
3651
3677
|
default:
|
package/dist/context.js
CHANGED
|
@@ -3,9 +3,9 @@ import { userInfo } from "node:os";
|
|
|
3
3
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
4
4
|
import { storeDir } from "./dataDir.js";
|
|
5
5
|
import { db } from "./db.js";
|
|
6
|
-
import { LEAD_KIND } from "./
|
|
6
|
+
import { LEAD_KIND } from "./agentKinds.js";
|
|
7
7
|
import { rowOwnership } from "./tmux.js";
|
|
8
|
-
import { recordQueenRefusal } from "./
|
|
8
|
+
import { recordQueenRefusal } from "./queenAuditStore.js";
|
|
9
9
|
import { gitOut } from "./git.js";
|
|
10
10
|
let selectedId = null;
|
|
11
11
|
let cachedActorId = null;
|
package/dist/crew.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { parseStoreTimestamp } from "./storeClock.js";
|
|
1
2
|
import { db } from "./db.js";
|
|
2
3
|
import { isLinkedWorktree } from "./context.js";
|
|
3
|
-
import {
|
|
4
|
+
import { finishedIdle } from "./firstPrompt.js";
|
|
4
5
|
import { heldReasonLabel } from "./heldLabel.js";
|
|
5
6
|
import { harnessFor, paneClassifierFor } from "./harnesses.js";
|
|
6
7
|
import { NEEDS_HUMAN_TAG } from "./portfolio.js";
|
|
@@ -8,7 +9,7 @@ import { ACTIVE_TODO_SQL } from "./todoScope.js";
|
|
|
8
9
|
import { pendingQuestionCount } from "./questions.js";
|
|
9
10
|
import { loadProjectYml } from "./projectYml.js";
|
|
10
11
|
import { parseTags } from "./result.js";
|
|
11
|
-
import { ACTIVE_TIMER_WHERE } from "./
|
|
12
|
+
import { ACTIVE_TIMER_WHERE } from "./wakeSql.js";
|
|
12
13
|
import { reportsAgentStateLog } from "./stateProvenance.js";
|
|
13
14
|
import { gitOut } from "./git.js";
|
|
14
15
|
import { liveTargets, rowOwnership } from "./tmux.js";
|
|
@@ -29,7 +30,7 @@ export function crewStoreProblem(database = db) {
|
|
|
29
30
|
function isoUtc(value) {
|
|
30
31
|
if (!value)
|
|
31
32
|
return null;
|
|
32
|
-
const date = new Date(
|
|
33
|
+
const date = new Date(parseStoreTimestamp(value));
|
|
33
34
|
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
34
35
|
}
|
|
35
36
|
const WAKE_LABEL_MAX = 60;
|
|
@@ -452,7 +453,7 @@ export function collectCrew(project, now) {
|
|
|
452
453
|
age_seconds: Math.max(0, Math.floor((now.getTime() - Date.parse(isoUtc(row.created_at) ?? "")) / 1000) || 0),
|
|
453
454
|
activity,
|
|
454
455
|
context_fill: kind ? readContextFill(kind, worker) : null,
|
|
455
|
-
your_turn:
|
|
456
|
+
your_turn: finishedIdle(row),
|
|
456
457
|
commits_ahead: commitsByCwd.get(row.cwd) ?? null,
|
|
457
458
|
},
|
|
458
459
|
});
|
package/dist/dashboard.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
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";
|
|
4
5
|
import { ACTIVE_TODO_SQL, NOT_COMPLETED_TODO_SQL, OPEN_BLOCKERS_SQL } from "./todoScope.js";
|
|
5
6
|
import { getProject } from "./context.js";
|
|
7
|
+
import { parseStoreTimestamp } from "./storeClock.js";
|
|
6
8
|
export const describeVisibility = (visibility) => visibility === null ? "running" : visibility === "window" ? "own window" : visibility;
|
|
7
9
|
export function processCounts(procs) {
|
|
8
10
|
return {
|
|
@@ -12,9 +14,8 @@ export function processCounts(procs) {
|
|
|
12
14
|
notStarted: procs.filter((p) => !p.running).length,
|
|
13
15
|
};
|
|
14
16
|
}
|
|
15
|
-
import {
|
|
17
|
+
import { finishedIdle } from "./firstPrompt.js";
|
|
16
18
|
import { SLUG_MAX_LEN, cutToUnitBudget } from "./slug.js";
|
|
17
|
-
const requireFromHere = createRequire(import.meta.url);
|
|
18
19
|
export const TODO_CAP = 100;
|
|
19
20
|
export const AGENT_CAP = 50;
|
|
20
21
|
export const WAKE_CAP = 50;
|
|
@@ -55,7 +56,7 @@ function formatDate(d) {
|
|
|
55
56
|
export function formatLocal(utc) {
|
|
56
57
|
if (!utc)
|
|
57
58
|
return "-";
|
|
58
|
-
const d = new Date(
|
|
59
|
+
const d = new Date(parseStoreTimestamp(utc));
|
|
59
60
|
if (Number.isNaN(d.getTime()))
|
|
60
61
|
return utc;
|
|
61
62
|
return formatDate(d);
|
|
@@ -70,7 +71,7 @@ function timeEl(utc) {
|
|
|
70
71
|
function shortWhenEl(utc) {
|
|
71
72
|
if (!utc)
|
|
72
73
|
return '<span class="muted">-</span>';
|
|
73
|
-
const d = new Date(
|
|
74
|
+
const d = new Date(parseStoreTimestamp(utc));
|
|
74
75
|
if (Number.isNaN(d.getTime()))
|
|
75
76
|
return `<span class="time">${escapeHtml(utc)}</span>`;
|
|
76
77
|
const local = formatDate(d);
|
|
@@ -87,7 +88,6 @@ function fetchProject(projectId) {
|
|
|
87
88
|
throw new Error(`No project ${projectId}.`);
|
|
88
89
|
return row;
|
|
89
90
|
}
|
|
90
|
-
const PENDING_WAKE_WHERE = "cancelled_at IS NULL AND (fired_at IS NULL OR repeat_every_ms IS NOT NULL)";
|
|
91
91
|
function agentStatusLevel(state) {
|
|
92
92
|
if (state === "working")
|
|
93
93
|
return "live";
|
|
@@ -98,7 +98,7 @@ function agentStatusLevel(state) {
|
|
|
98
98
|
return "warn";
|
|
99
99
|
}
|
|
100
100
|
function agentStateBadge(a) {
|
|
101
|
-
return
|
|
101
|
+
return a.agent_state === "idle" && !finishedIdle(a)
|
|
102
102
|
? statusBadge("live", "idle (no assignment yet)")
|
|
103
103
|
: statusBadge(agentStatusLevel(a.agent_state), a.agent_state);
|
|
104
104
|
}
|
|
@@ -121,7 +121,7 @@ function fetchNowAgents(projectId) {
|
|
|
121
121
|
function fetchNextWake(projectId) {
|
|
122
122
|
return db
|
|
123
123
|
.prepare(`SELECT kind, due_at, max_wait_at, deliver_actor FROM wakes
|
|
124
|
-
WHERE project_id = ? AND ${
|
|
124
|
+
WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE}
|
|
125
125
|
ORDER BY COALESCE(due_at, max_wait_at, created_at) LIMIT 1`)
|
|
126
126
|
.get(projectId);
|
|
127
127
|
}
|
|
@@ -428,7 +428,7 @@ function renderThroughputSection(index, stats) {
|
|
|
428
428
|
function renderWakesSection(index, projectId) {
|
|
429
429
|
const all = db
|
|
430
430
|
.prepare(`SELECT id, kind, body, owner, deliver_actor, due_at, max_wait_at, fire_count, repeat_every_ms, held_at
|
|
431
|
-
FROM wakes WHERE project_id = ? AND ${
|
|
431
|
+
FROM wakes WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE}
|
|
432
432
|
ORDER BY COALESCE(due_at, max_wait_at, created_at)`)
|
|
433
433
|
.all(projectId);
|
|
434
434
|
const shown = all.slice(0, WAKE_CAP);
|
|
@@ -449,7 +449,7 @@ function renderWakesSection(index, projectId) {
|
|
|
449
449
|
return section(index, "wakes", "Wakes", `${all.length} pending`, "bell", String(shown.length), body);
|
|
450
450
|
}
|
|
451
451
|
function questionAge(createdAt) {
|
|
452
|
-
const timestamp =
|
|
452
|
+
const timestamp = parseStoreTimestamp(createdAt);
|
|
453
453
|
if (!Number.isFinite(timestamp))
|
|
454
454
|
return "unknown";
|
|
455
455
|
const seconds = Math.max(0, Math.floor((Date.now() - timestamp) / 1000));
|
|
@@ -462,8 +462,6 @@ function questionAge(createdAt) {
|
|
|
462
462
|
return "now";
|
|
463
463
|
}
|
|
464
464
|
function renderQuestionsSection(index, projectId) {
|
|
465
|
-
// A static import of questions.js cycles through tools/wakes.ts and scheduler.ts.
|
|
466
|
-
const { listQuestions, pendingQuestionCount } = requireFromHere("./questions.js");
|
|
467
465
|
const questions = listQuestions(projectId);
|
|
468
466
|
if (questions.length === 0)
|
|
469
467
|
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) {
|
package/dist/errors.js
ADDED
package/dist/etime.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function parseEtimeSeconds(value) {
|
|
2
|
+
const dayForm = /^(\d+)-(\d{1,2}):(\d{2}):(\d{2})$/.exec(value);
|
|
3
|
+
const digitForm = /^(\d+)$/.exec(value);
|
|
4
|
+
const fields = dayForm ? dayForm.slice(1).map(Number) : value.split(":").map(Number);
|
|
5
|
+
if (!dayForm &&
|
|
6
|
+
!digitForm &&
|
|
7
|
+
((fields.length === 2 && !/^\d+:\d+$/.test(value)) ||
|
|
8
|
+
(fields.length === 3 && !/^\d+:\d+:\d+$/.test(value)) ||
|
|
9
|
+
(fields.length !== 2 && fields.length !== 3))) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
if (fields.some((field) => !Number.isSafeInteger(field) || field < 0))
|
|
13
|
+
return null;
|
|
14
|
+
let total;
|
|
15
|
+
if (digitForm) {
|
|
16
|
+
total = fields[0];
|
|
17
|
+
}
|
|
18
|
+
else if (dayForm) {
|
|
19
|
+
const [days, hours, minutes, seconds] = fields;
|
|
20
|
+
if (hours > 23 || minutes > 59 || seconds > 59)
|
|
21
|
+
return null;
|
|
22
|
+
total = ((days * 24 + hours) * 60 + minutes) * 60 + seconds;
|
|
23
|
+
}
|
|
24
|
+
else if (fields.length === 2) {
|
|
25
|
+
const [minutes, seconds] = fields;
|
|
26
|
+
if (seconds > 59)
|
|
27
|
+
return null;
|
|
28
|
+
total = minutes * 60 + seconds;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const [hours, minutes, seconds] = fields;
|
|
32
|
+
if (minutes > 59 || seconds > 59)
|
|
33
|
+
return null;
|
|
34
|
+
total = (hours * 60 + minutes) * 60 + seconds;
|
|
35
|
+
}
|
|
36
|
+
return Number.isSafeInteger(total) ? total : null;
|
|
37
|
+
}
|
package/dist/firstPrompt.js
CHANGED
|
@@ -4,3 +4,9 @@ export function awaitingFirstPrompt(row) {
|
|
|
4
4
|
export function awaitingFirstPromptSql(alias) {
|
|
5
5
|
return `${alias}.resumed_at != ''`;
|
|
6
6
|
}
|
|
7
|
+
export function finishedIdle(row) {
|
|
8
|
+
return row.agent_state === "idle" && !awaitingFirstPrompt(row);
|
|
9
|
+
}
|
|
10
|
+
export function finishedIdleSql(alias) {
|
|
11
|
+
return `${alias}.agent_state = 'idle' AND NOT (${awaitingFirstPromptSql(alias)})`;
|
|
12
|
+
}
|
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/heldLabel.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HELD_REASON_CONVERSATION, HELD_REASON_LEAD_PANE_DEAD, isLeadRowClosedHold, isUnclassifiablePaneHold, isUnsubmittedInputHold, wasHeldForPaneIdentity, } from "./
|
|
1
|
+
import { HELD_REASON_CONVERSATION, HELD_REASON_LEAD_PANE_DEAD, isLeadRowClosedHold, isUnclassifiablePaneHold, isUnsubmittedInputHold, wasHeldForPaneIdentity, } from "./wakeSql.js";
|
|
2
2
|
// test/docs.test.mjs fails if a label here is missing from docs/install.md. It checks
|
|
3
3
|
// presence only: docs/install.md also counts the set in prose, and nothing guards that number.
|
|
4
4
|
export const HELD_REASON_LABELS = ["typing", "talking", "needs you", "blocked"];
|