@mu-crew/mu 4.1.1 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-B74N2EAS.js → chunk-3ZLI6YTV.js} +8 -4
- package/dist/chunk-3ZLI6YTV.js.map +1 -0
- package/dist/{chunk-DEYBQDTO.js → chunk-RVWNQLFE.js} +2 -2
- package/dist/{chunk-L7I3BLXP.js → chunk-SFV5RPZW.js} +1512 -411
- package/dist/chunk-SFV5RPZW.js.map +1 -0
- package/dist/{chunk-LBBZAG5C.js → chunk-UL2QE43S.js} +20 -20
- package/dist/chunk-UL2QE43S.js.map +1 -0
- package/dist/{cli-H2PWJAJN.js → cli-72EJ3CKW.js} +146 -31
- package/dist/cli-72EJ3CKW.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/{errors-G5IVXSMY.js → errors-LY4OZVQU.js} +8 -2
- package/dist/extension/mu-pi.js +1 -1
- package/dist/index.d.ts +2554 -2277
- package/dist/index.js +58 -4
- package/dist/index.js.map +1 -1
- package/dist/{tui-EN2CDP6G.js → tui-3ZPLSLDW.js} +13 -14
- package/dist/tui-3ZPLSLDW.js.map +1 -0
- package/docs/ARCHITECTURE.md +15 -8
- package/docs/VOCABULARY.md +14 -6
- package/docs/guide/remote.md +113 -4
- package/docs/guide/sql.md +1 -1
- package/docs/reference/env.md +1 -0
- package/package.json +1 -1
- package/skills/mu/recipes/remote-workers.md +7 -6
- package/dist/chunk-B74N2EAS.js.map +0 -1
- package/dist/chunk-L7I3BLXP.js.map +0 -1
- package/dist/chunk-LBBZAG5C.js.map +0 -1
- package/dist/cli-H2PWJAJN.js.map +0 -1
- package/dist/tui-EN2CDP6G.js.map +0 -1
- /package/dist/{chunk-DEYBQDTO.js.map → chunk-RVWNQLFE.js.map} +0 -0
- /package/dist/{errors-G5IVXSMY.js.map → errors-LY4OZVQU.js.map} +0 -0
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
logItem,
|
|
7
7
|
openHumanNeeds,
|
|
8
8
|
parseOpKey
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-RVWNQLFE.js";
|
|
10
10
|
import {
|
|
11
11
|
UNKNOWN_REASON,
|
|
12
12
|
WorkstreamNotFoundError,
|
|
@@ -16,16 +16,16 @@ import {
|
|
|
16
16
|
listLogs,
|
|
17
17
|
listTasks,
|
|
18
18
|
readLiveAgents,
|
|
19
|
+
shellWord,
|
|
19
20
|
tallyCalls,
|
|
20
21
|
tryResolveLiveWorkstreamId
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-SFV5RPZW.js";
|
|
22
23
|
|
|
23
24
|
// src/dashboard-data.ts
|
|
24
25
|
var DEFAULT_STATUS = ["OPEN", "IN_PROGRESS"];
|
|
25
26
|
var DEFAULT_LOG_LIMIT = 100;
|
|
26
27
|
var STALLED_TOOL_MS = 15 * 6e4;
|
|
27
28
|
var FINISHED_CALL = ["done", "failed", "cancelled"];
|
|
28
|
-
var FAILED_CALL_MS = 24 * 60 * 6e4;
|
|
29
29
|
var WAVE_MAX = 5;
|
|
30
30
|
var callOf = (c) => ({
|
|
31
31
|
call_id: c.callId,
|
|
@@ -146,17 +146,21 @@ var ATTENTION_ORDER = [
|
|
|
146
146
|
"sync"
|
|
147
147
|
];
|
|
148
148
|
function agentAttention(a, now) {
|
|
149
|
-
const item = (kind, text) => ({
|
|
149
|
+
const item = (kind, text, fix) => ({
|
|
150
150
|
key: `${kind}:${a.key}`,
|
|
151
151
|
workstream: a.workstream,
|
|
152
152
|
kind,
|
|
153
153
|
text: `${a.name} ${text}`,
|
|
154
|
-
target: a.key
|
|
154
|
+
target: a.key,
|
|
155
|
+
...fix !== void 0 ? { fix } : {}
|
|
155
156
|
});
|
|
156
157
|
const since = a.since == null ? Number.NaN : Date.parse(a.since);
|
|
157
158
|
const waited = Number.isFinite(since) ? ` ${formatElapsed(now - since)}` : "";
|
|
158
159
|
const out = [];
|
|
159
|
-
if (isDead(a))
|
|
160
|
+
if (isDead(a))
|
|
161
|
+
out.push(
|
|
162
|
+
item("dead", "dead", `mu agent close ${shellWord(a.name)} -w ${shellWord(a.workstream)}`)
|
|
163
|
+
);
|
|
160
164
|
if (a.state === "needs_permission") out.push(item("permission", `permission${waited}`));
|
|
161
165
|
if (a.idle === true) out.push(item("input", `input${waited}`));
|
|
162
166
|
const ran = a.tool?.elapsed_ms;
|
|
@@ -293,23 +297,18 @@ async function loadDashboard(db, opts) {
|
|
|
293
297
|
})) : [];
|
|
294
298
|
const attention = everyAgent.flatMap((a) => agentAttention(a, now));
|
|
295
299
|
const liveNames = new Set(live.map((w) => w.name));
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
updatedAfter: new Date(now - FAILED_CALL_MS).toISOString()
|
|
301
|
-
})
|
|
302
|
-
];
|
|
303
|
-
troubled.sort((a, b) => keyOf(a.workstream, a.task).localeCompare(keyOf(b.workstream, b.task)));
|
|
304
|
-
for (const c of troubled) {
|
|
300
|
+
const refused = listDelegates(db, void 0, void 0, { states: ["refused"] }).sort(
|
|
301
|
+
(a, b) => keyOf(a.workstream, a.task).localeCompare(keyOf(b.workstream, b.task))
|
|
302
|
+
);
|
|
303
|
+
for (const c of refused) {
|
|
305
304
|
if (!liveNames.has(c.workstream)) continue;
|
|
306
|
-
const how = c.state === "refused" ? " (reissue or cancel it from pi)" : "";
|
|
307
305
|
attention.push({
|
|
308
306
|
key: `delegate:${keyOf(c.workstream, c.task)}`,
|
|
309
307
|
workstream: c.workstream,
|
|
310
308
|
kind: "delegate",
|
|
311
|
-
text: `delegate
|
|
312
|
-
target: keyOf(c.workstream, c.task)
|
|
309
|
+
text: `delegate refused: ${c.request.label ?? c.task} (reissue from pi, or y copies the cancel)`,
|
|
310
|
+
target: keyOf(c.workstream, c.task),
|
|
311
|
+
fix: `mu delegates finish ${shellWord(c.callId)} --outcome cancelled --no-record --author "$USER"`
|
|
313
312
|
});
|
|
314
313
|
}
|
|
315
314
|
for (const t of db.prepare(
|
|
@@ -322,7 +321,8 @@ async function loadDashboard(db, opts) {
|
|
|
322
321
|
workstream: t.ws,
|
|
323
322
|
kind: "human",
|
|
324
323
|
text: t.title,
|
|
325
|
-
target: keyOf(t.ws, t.id)
|
|
324
|
+
target: keyOf(t.ws, t.id),
|
|
325
|
+
fix: `mu task close ${shellWord(t.id)} -w ${shellWord(t.ws)} --evidence '<what you decided>'`
|
|
326
326
|
});
|
|
327
327
|
}
|
|
328
328
|
if (next.source === "unavailable") {
|
|
@@ -372,4 +372,4 @@ export {
|
|
|
372
372
|
matches,
|
|
373
373
|
loadDashboard
|
|
374
374
|
};
|
|
375
|
-
//# sourceMappingURL=chunk-
|
|
375
|
+
//# sourceMappingURL=chunk-UL2QE43S.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/dashboard-data.ts"],"sourcesContent":["// SDK seam for the dashboard: ONE read-only projection of everything its tabs\n// show (workstreams, tasks with a delegate task's `call`, frontier, agents, log,\n// human needs, the caller's delegates, delegate `waves`, attention).\n//\n// READ-ONLY by construction: it never reconciles, reaps, ingests or writes, and\n// runs on a `readonly: true` connection. The only I/O beyond SQL is the\n// best-effort ctl probe `readLiveAgents` does (a socket read). It composes the\n// merged readers (`listTasks`, `loadOpenTaskFrontier`, `readLiveAgents`,\n// `listLogs`/`logItem`, `openHumanNeeds`, `listDelegates`, `tallyCalls`) rather than\n// re-deriving them, so a dashboard row equals the CLI's. `stateSnapshot` is NOT\n// the source of the task list: it omits OPEN/parked rows and caps closed ones.\n//\n// `next` is `computeNext` (the CLI's one rule set) with no outcome: it only reads.\n// A failed computation stays `next_source: \"unavailable\"` (never \"no authorised\n// work\"). An agent's `tool` comes from the ctl status the agent read already did:\n// an object while a tool runs, null when none, absent for an extension that\n// predates the field (unknown). Elapsed is `now - since`, calculated at read.\n//\n// Two delegate reads, on purpose: `delegates` is the caller's own unfinished calls\n// (caller-scoped like `human_needs`, empty for a human shell), while a task's\n// `call`, `waves` and the `delegate` attention items read every caller's, because\n// they describe tasks and waves anyone can see. Each delegate read is bounded in\n// SQL (shown task statuses, the newest waves, refused calls only), so a tick\n// does not grow with the delegate history in `scratch`.\n\nimport { type AgentOutput, liveAgentToOutput } from \"./agent-output.js\";\nimport type { StateReading } from \"./agent-state.js\";\nimport { UNKNOWN_REASON } from \"./agent-state.js\";\nimport { readLiveAgents } from \"./agents.js\";\nimport { type LogItem, logItem } from \"./cli/log-records.js\";\nimport type { CtlTool } from \"./ctl/protocol.js\";\nimport { loadOpenTaskFrontier } from \"./dag.js\";\nimport { type Db, tryResolveLiveWorkstreamId, WorkstreamNotFoundError } from \"./db.js\";\nimport { type DelegateRow, listDelegates, tallyCalls } from \"./delegates.js\";\nimport { openHumanNeeds } from \"./human-needed.js\";\nimport { parseOpKey } from \"./log-render.js\";\nimport { latestSeq, listLogs } from \"./logs.js\";\nimport { computeNext } from \"./next-hints.js\";\nimport type { NextPlan } from \"./output-contract.js\";\nimport { shellWord } from \"./shell-quote.js\";\nimport type { TaskStatus } from \"./tasks/status.js\";\nimport { listTasks, type TaskRow } from \"./tasks.js\";\nimport { isScratchWorkstream } from \"./workstream.js\";\n\n/** A running tool of a pi agent (from the ctl status). */\nexport interface AgentTool {\n name: string;\n /** Epoch ms the tool started. */\n since: number;\n /** Calculated at read time (`now - since`), when known. */\n elapsed_ms?: number;\n}\n\nexport interface DashboardWorkstream {\n name: string;\n scratch: boolean;\n /** Newest visible op seq in the workstream; 0 when it has none, or when `activity: false` skipped the read. */\n latest_seq: number;\n}\n\n/** One task, SQL column names, `key` = `<workstream>/<id>` (unique across workstreams). */\nexport interface DashboardTask {\n key: string;\n workstream: string;\n id: string;\n title: string;\n status: string;\n substate: string;\n impact: number;\n effort_days: number;\n /** Owner's name; only while IN_PROGRESS. */\n owner: string | null;\n /** The owner's registry id (`tasks.owner_id`; the identity: names repeat across workstreams). */\n owner_id: number | null;\n /** The owner's OWN workstream, which can differ from the task's; null when unowned. */\n owner_workstream: string | null;\n kind: string;\n wave: string | null;\n created_at: string;\n updated_at: string;\n /** `MAX(ops.seq)` over the task's own ops, its notes' and its edges' (raw log, db/sql writes included); 0 when none. */\n activity_seq: number;\n /** The call behind a `kind=delegate` task, whoever made it; absent for any other task. */\n call?: DashboardCall;\n}\n\n/** A delegate call as a task row shows it: machine-wide, never narrowed to a caller. */\nexport interface DashboardCall {\n call_id: string;\n state: string;\n /** The parsed label of a done answer; null otherwise. */\n verdict: string | null;\n /** `<workstream>/<task>` the call judges; null with no record target. */\n judges: string | null;\n wave: string | null;\n}\n\n/**\n * One wave of delegate calls in a workstream, tallied as `verdictTally` counts it\n * (one result per call), over every caller. `pending` includes `refused`.\n */\nexport interface DashboardWave {\n /** `<workstream>/<wave>`. */\n key: string;\n workstream: string;\n wave: string;\n calls: number;\n confirmed: number;\n refuted: number;\n unverified: number;\n pending: number;\n refused: number;\n /** The newest `updated_at` of its calls. */\n updated_at: string;\n}\n\n/** One line of the open DAG (`loadOpenTaskFrontier`, preorder, per workstream). */\nexport interface DashboardEdge {\n key: string;\n workstream: string;\n id: string;\n title: string;\n status: string;\n substate: string;\n depth: number;\n /** Qualified key of the entry it is listed under; null for a root. */\n parent: string | null;\n /** Already listed above (a diamond): not expanded again. */\n again: boolean;\n /** Open neighbours not listed under this entry. */\n kids: number;\n /** CLOSED neighbours collapsed into this count. */\n closed: number;\n cap_cut: number;\n}\n\n/** What the frontier cap left out of one workstream's DAG (per-entry `cap_cut` cannot say it). */\nexport interface DashboardFrontierCut {\n workstream: string;\n /** Open start tasks the node cap left out: open work absent from `edges`. */\n roots_cut: number;\n /** Recurrence (diamond) lines the cap left out; each such task is listed elsewhere. */\n again_cut: number;\n}\n\n/** A registry agent with its computed state; unknown stays unknown. */\nexport interface DashboardAgent extends AgentOutput {\n /** `<workstream>/<name>`. */\n key: string;\n /** Qualified keys of the IN_PROGRESS tasks it owns (empty when none). */\n tasks: string[];\n /** The first of `tasks`; null when none. */\n task: string | null;\n /** The running tool; null = none running; absent = unknown (old extension, no ctl): never read as \"none\". */\n tool?: AgentTool | null;\n}\n\n/** An op of the canonical log, rendered by the one formatter (`logItem`), plus its workstream. */\nexport interface DashboardLogItem extends LogItem {\n /** The workstream the op belongs to; null for a machine-wide op. */\n scope: string | null;\n}\n\n/** An OPEN `human` task of the caller. */\nexport interface DashboardHumanNeed {\n key: string;\n workstream: string;\n id: string;\n reason: string;\n}\n\n/** An unfinished delegate call of the caller. */\nexport interface DashboardDelegate {\n call_id: string;\n workstream: string;\n /** The delegate task's local id. */\n task: string;\n state: string;\n wave: string | null;\n judges: string | null;\n agent: string | null;\n verdict: string | null;\n error: string | null;\n updated_at: string;\n}\n\n/**\n * Something a human has to look at. Read over EVERY live workstream and blind\n * to the status/text filters and the workstream scope, so each surface (the\n * strip under the tabs, the Overview card) agrees.\n *\n * - `dead`: the agent's pane (or gate process) is gone.\n * - `permission`: `needs_permission`; `text` carries how long.\n * - `stalled`: one tool has run over `STALLED_TOOL_MS`.\n * - `input`: `needs_input` past the idle threshold on an agent that owns work\n * (the registry's `idle` flag; `MU_IDLE_THRESHOLD_MS`, 0 disables).\n * - `delegate`: a delegate call that is `refused` (parked until its pi session\n * reissues it), whoever made it. A failed call is not one: it is final, its\n * caller had the settle, and its task row shows it.\n * - `human`: an OPEN `kind=human` task, whoever recorded it (`human_needs`\n * is caller-scoped and empty for a human shell).\n * - `next_unavailable`: `computeNext` failed.\n * - `sync`: a warning the caller passed as `syncWarnings`.\n */\nexport type AttentionKind =\n | \"dead\"\n | \"permission\"\n | \"input\"\n | \"stalled\"\n | \"delegate\"\n | \"human\"\n | \"next_unavailable\"\n | \"sync\";\n\nexport interface DashboardAttention {\n /** Unique within a snapshot (`<kind>:<target>`). */\n key: string;\n /** The workstream it belongs to; null for a machine-wide item (`next_unavailable`, `sync`). */\n workstream: string | null;\n kind: AttentionKind;\n /** One short line, age at read (\"worker-3 permission 20m\"; a human task: its title); bare names, the UI qualifies. */\n text: string;\n /** Qualified key to drill into: the agent (dead/permission/input/stalled) or the task (delegate, human); null otherwise. */\n target: string | null;\n /** The `mu` command that clears it, for `y` to copy (absent: attach or read instead). */\n fix?: string;\n}\n\nexport interface DashboardSnapshot {\n /** The identity the caller-scoped panels were read for; null = a human shell. */\n caller: string | null;\n /** `none`: no identity, so `human_needs` and `delegates` are empty BY DESIGN (never another caller's); `call`, `waves` and attention are every caller's. */\n caller_scope: \"caller\" | \"none\";\n /** Every live workstream, scratch included; never narrowed by a filter (it is the switcher). */\n workstreams: DashboardWorkstream[];\n tasks: DashboardTask[];\n edges: DashboardEdge[];\n /** One per in-scope workstream: the frontier's root/recurrence cuts, passed through. */\n frontier: DashboardFrontierCut[];\n agents: DashboardAgent[];\n /** The newest ops, oldest first. */\n log: DashboardLogItem[];\n /** `computeNext` for the caller set and the workstream filter; null when `next_source` is `unavailable`, or computed and nothing is authorised. */\n next: NextPlan;\n /** `unavailable`: the computation failed; the UI must not read null as \"no work\". */\n next_source: \"unavailable\" | \"computed\";\n human_needs: DashboardHumanNeed[];\n delegates: DashboardDelegate[];\n /** Per read workstream (tasks scope), its `WAVE_MAX` most recently updated waves, newest first, any caller. */\n waves: DashboardWave[];\n /** Trouble across every workstream, filters ignored, most urgent first (see `AttentionKind`). */\n attention: DashboardAttention[];\n}\n\nexport interface DashboardOptions {\n /** Narrow tasks, frontier, agents, log and caller panels to one live workstream. */\n workstream?: string;\n /**\n * Read tasks, edges and frontier of this workstream ONLY (a name that is not live,\n * or null: none), leaving agents, log, caller panels and attention machine-wide:\n * the dashboard shows one workstream, and reading every other one's tasks and DAG\n * each tick is what made it costly. Absent: every live workstream. `workstream`, when\n * set, wins.\n */\n scopeWorkstream?: string | null;\n /** Statuses to list; default OPEN + IN_PROGRESS (CLOSED is hidden). */\n status?: readonly TaskStatus[];\n /** Case-insensitive substring over a task's key/title/owner/substate and an agent's key/name/state/task keys. */\n text?: string;\n /** `spawnerFromEnv()`; null = a human shell (caller-scoped panels stay empty). */\n caller: string | null;\n /** Who `next` is computed for (`currentCallers()`); default: `caller` alone, none for a human shell. */\n callers?: readonly string[];\n /** Log tail size (default 100). */\n logLimit?: number;\n /** Read the log tail of this workstream only, leaving every other panel unscoped (not checked: a name that is not live reads an empty log). */\n logWorkstream?: string;\n /**\n * Read each workstream's `latest_seq` (default true). It only ranks the starting\n * pick and costs one log scan per workstream, so the dashboard's poll passes false\n * once the set of live workstreams is known (every `latest_seq` is then 0).\n */\n activity?: boolean;\n /** Sync warnings the caller holds (`ambientIngest`); each becomes a `sync` attention item. */\n syncWarnings?: readonly string[];\n /**\n * The agent-state cache (the dashboard's slow tier, see `readLiveAgents`): with\n * `probe: false` the read is SQL only for every cached agent. Absent: probe all.\n */\n agentStates?: { cache: Map<string, StateReading>; probe: boolean };\n}\n\nconst DEFAULT_STATUS: readonly TaskStatus[] = [\"OPEN\", \"IN_PROGRESS\"];\nconst DEFAULT_LOG_LIMIT = 100;\n/** A tool running this long counts as stalled. */\nexport const STALLED_TOOL_MS = 15 * 60_000;\nconst FINISHED_CALL: readonly string[] = [\"done\", \"failed\", \"cancelled\"];\n/** Waves kept per workstream. */\nexport const WAVE_MAX = 5;\n\nconst callOf = (c: DelegateRow): DashboardCall => ({\n call_id: c.callId,\n state: c.state,\n verdict: c.verdict,\n judges: c.record ? `${c.record.workstream}/${c.record.task}` : null,\n wave: c.wave,\n});\n\n/** The `WAVE_MAX` most recently updated waves of `calls` (one workstream's), newest first. */\nfunction wavesOf(db: Db, workstream: string): DashboardWave[] {\n // The newest waves by name first (no request parse), then only their calls.\n const names = (\n db\n .prepare(\n `SELECT t.wave AS wave, MAX(d.updated_at) AS at FROM delegate_calls d\n JOIN tasks t ON t.id = d.task_id JOIN workstreams w ON w.id = t.workstream_id\n WHERE w.name = ? AND t.wave IS NOT NULL\n GROUP BY t.wave ORDER BY at DESC, t.wave LIMIT ?`,\n )\n .all(workstream, WAVE_MAX) as { wave: string }[]\n ).map((r) => r.wave);\n return names.map((wave): DashboardWave => {\n const rows = listDelegates(db, undefined, workstream, { wave });\n const t = tallyCalls(rows);\n const sum = (k: \"confirmed\" | \"refuted\" | \"unverified\" | \"pending\"): number =>\n t.findings.reduce((n, f) => n + f[k], 0);\n return {\n key: keyOf(workstream, wave),\n workstream,\n wave,\n calls: rows.length,\n confirmed: sum(\"confirmed\"),\n refuted: sum(\"refuted\"),\n unverified: sum(\"unverified\"),\n pending: sum(\"pending\"),\n refused: t.refused.length,\n updated_at: rows.reduce((m, r) => (r.updatedAt > m ? r.updatedAt : m), \"\"),\n };\n });\n}\n\n/** `MAX(ops.seq)` per task id of one workstream over its task, note and edge ops. */\nfunction taskActivity(db: Db, workstream: string): Map<string, number> {\n const prefix = `${workstream}/`;\n const rows = db\n .prepare(\n `SELECT entity, key, MAX(seq) AS seq FROM ops\n WHERE entity IN ('task', 'note', 'edge') AND key >= ? AND key < ?\n GROUP BY entity, key`,\n )\n .all(prefix, `${workstream}0`) as { entity: string; key: string; seq: number }[];\n const out = new Map<string, number>();\n const bump = (id: string | undefined, seq: number): void => {\n if (id !== undefined && seq > (out.get(id) ?? 0)) out.set(id, seq);\n };\n for (const r of rows) {\n if (r.entity === \"task\") bump(r.key.slice(prefix.length), r.seq);\n else if (r.entity === \"note\") bump(r.key.slice(prefix.length).split(\"#\")[0], r.seq);\n else {\n const parsed = parseOpKey(r.key);\n bump(parsed.local, r.seq);\n bump(parsed.to, r.seq);\n }\n }\n return out;\n}\n\nfunction nextOf(\n db: Db,\n callers: readonly string[],\n workstream: string | undefined,\n): { plan: NextPlan; source: DashboardSnapshot[\"next_source\"] } {\n try {\n const plan = computeNext(db, { callers, ...(workstream !== undefined ? { workstream } : {}) });\n return { plan, source: \"computed\" };\n } catch {\n return { plan: null, source: \"unavailable\" };\n }\n}\n\n/** The tool as the dashboard shows it, `elapsed_ms` calculated now; unknown stays absent. */\nfunction toolOf(tool: CtlTool | null | undefined, now: number): { tool?: AgentTool | null } {\n if (tool === undefined) return {};\n return { tool: tool === null ? null : { ...tool, elapsed_ms: Math.max(0, now - tool.since) } };\n}\n\nconst keyOf = (workstream: string, id: string): string => `${workstream}/${id}`;\n\ninterface AgentIdentity {\n name: string;\n workstream: string;\n}\n\nfunction dashboardTask(\n row: TaskRow,\n activity: ReadonlyMap<string, number>,\n agentsById: ReadonlyMap<number, AgentIdentity>,\n calls: ReadonlyMap<string, DelegateRow>,\n): DashboardTask {\n const call = row.kind === \"delegate\" ? calls.get(row.name) : undefined;\n const inProgress = row.status === \"IN_PROGRESS\";\n const owner = inProgress && row.ownerId !== null ? agentsById.get(row.ownerId) : undefined;\n return {\n key: keyOf(row.workstreamName, row.name),\n workstream: row.workstreamName,\n id: row.name,\n title: row.title,\n status: row.status,\n substate: row.substate,\n impact: row.impact,\n effort_days: row.effortDays,\n owner: inProgress ? row.ownerName : null,\n owner_id: inProgress ? row.ownerId : null,\n owner_workstream: owner?.workstream ?? null,\n kind: row.kind,\n wave: row.wave,\n created_at: row.createdAt,\n updated_at: row.updatedAt,\n activity_seq: activity.get(row.name) ?? 0,\n ...(call ? { call: callOf(call) } : {}),\n };\n}\n\n/** Newest activity first; no ops (capture suppressed) falls back to `updated_at`, then key. */\nfunction byActivityDesc(a: DashboardTask, b: DashboardTask): number {\n if (a.activity_seq !== b.activity_seq) return b.activity_seq - a.activity_seq;\n if (a.updated_at !== b.updated_at) return a.updated_at < b.updated_at ? 1 : -1;\n return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;\n}\n\n/** `45s`, `1m05s`, `1h05m`: the ONE duration format of the dashboard (strip and tool column). */\nexport function formatElapsed(ms: number): string {\n const s = Math.max(0, Math.floor(ms / 1000));\n if (s < 60) return `${s}s`;\n const m = Math.floor(s / 60);\n if (m < 60) return `${m}m${String(s % 60).padStart(2, \"0\")}s`;\n return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, \"0\")}m`;\n}\n\n/** The pane (or a gate's process) is gone, as opposed to a state source that merely cannot say. */\nfunction isDead(a: DashboardAgent): boolean {\n if (a.state !== \"unknown\") return false;\n if (a.reason === UNKNOWN_REASON.paneGone) return true;\n return a.gate_token !== null && /^(exited \\d+|killed by signal)/.test(a.reason ?? \"\");\n}\n\nconst ATTENTION_ORDER: readonly AttentionKind[] = [\n \"dead\",\n \"permission\",\n \"stalled\",\n \"input\",\n \"delegate\",\n \"human\",\n \"next_unavailable\",\n \"sync\",\n];\n\nfunction agentAttention(a: DashboardAgent, now: number): DashboardAttention[] {\n const item = (kind: AttentionKind, text: string, fix?: string): DashboardAttention => ({\n key: `${kind}:${a.key}`,\n workstream: a.workstream,\n kind,\n text: `${a.name} ${text}`,\n target: a.key,\n ...(fix !== undefined ? { fix } : {}),\n });\n const since = a.since == null ? Number.NaN : Date.parse(a.since);\n const waited = Number.isFinite(since) ? ` ${formatElapsed(now - since)}` : \"\";\n const out: DashboardAttention[] = [];\n // `agent close` previews first; its `--if` is the human's second step.\n if (isDead(a))\n out.push(\n item(\"dead\", \"dead\", `mu agent close ${shellWord(a.name)} -w ${shellWord(a.workstream)}`),\n );\n if (a.state === \"needs_permission\") out.push(item(\"permission\", `permission${waited}`));\n if (a.idle === true) out.push(item(\"input\", `input${waited}`));\n const ran = a.tool?.elapsed_ms;\n if (a.tool != null && ran !== undefined && ran >= STALLED_TOOL_MS) {\n out.push(item(\"stalled\", `${a.tool.name} ${formatElapsed(ran)}`));\n }\n return out;\n}\n\nexport function matches(\n text: string | undefined,\n ...fields: (string | null | undefined)[]\n): boolean {\n if (text === undefined || text === \"\") return true;\n const needle = text.toLowerCase();\n return fields.some((f) => f?.toLowerCase().includes(needle) === true);\n}\n\n/**\n * Load the dashboard's whole read model in one pass. Read-only (see the file\n * header); async only for the ctl probes. Throws `WorkstreamNotFoundError` for\n * a `workstream` filter that is not a live workstream.\n */\nexport async function loadDashboard(db: Db, opts: DashboardOptions): Promise<DashboardSnapshot> {\n const live = (\n db\n .prepare(\"SELECT name, id FROM workstreams WHERE torn_down_at IS NULL ORDER BY name\")\n .all() as { name: string; id: number }[]\n ).map((w) => ({ ...w, scratch: isScratchWorkstream(w.name) }));\n if (opts.workstream !== undefined && tryResolveLiveWorkstreamId(db, opts.workstream) === null) {\n throw new WorkstreamNotFoundError(opts.workstream);\n }\n const scoped = live.filter((w) => opts.workstream === undefined || w.name === opts.workstream);\n // Whose tasks, edges and frontier are read (`scopeWorkstream`; `workstream` wins).\n const readFrom =\n opts.workstream !== undefined || opts.scopeWorkstream === undefined\n ? scoped\n : live.filter((w) => w.name === opts.scopeWorkstream);\n const statuses =\n opts.status === undefined || opts.status.length === 0 ? DEFAULT_STATUS : opts.status;\n\n const agentsById = new Map<number, AgentIdentity>(\n (\n db\n .prepare(\n \"SELECT a.id, a.name, w.name AS workstream FROM agents a JOIN workstreams w ON w.id = a.workstream_id\",\n )\n .all() as { id: number; name: string; workstream: string }[]\n ).map((a) => [a.id, { name: a.name, workstream: a.workstream }]),\n );\n // IN_PROGRESS tasks by owner registry id, over EVERY live workstream: a task can be\n // claimed by an agent of another workstream, and a filtered read must still show it.\n const owned = new Map<number, string[]>();\n for (const t of db\n .prepare(\n `SELECT ws.name AS ws, t.local_id AS id, t.owner_id AS owner FROM tasks t\n JOIN workstreams ws ON ws.id = t.workstream_id AND ws.torn_down_at IS NULL\n WHERE t.status = 'IN_PROGRESS' AND t.owner_id IS NOT NULL ORDER BY ws.name, t.local_id`,\n )\n .all() as { ws: string; id: string; owner: number }[]) {\n owned.set(t.owner, [...(owned.get(t.owner) ?? []), keyOf(t.ws, t.id)]);\n }\n\n const tasks: DashboardTask[] = [];\n const edges: DashboardEdge[] = [];\n const frontier: DashboardFrontierCut[] = [];\n const agents: DashboardAgent[] = [];\n const waves: DashboardWave[] = [];\n const now = Date.now();\n for (const ws of readFrom) {\n const activity = taskActivity(db, ws.name);\n const calls = listDelegates(db, undefined, ws.name, { taskStatuses: statuses });\n const callByTask = new Map(calls.map((c) => [c.task, c]));\n waves.push(...wavesOf(db, ws.name));\n tasks.push(\n ...listTasks(db, ws.name, { status: statuses })\n .map((r) => dashboardTask(r, activity, agentsById, callByTask))\n .filter((t) =>\n matches(\n opts.text,\n t.key,\n t.title,\n t.owner,\n t.substate,\n t.call?.state,\n t.call?.verdict,\n t.call?.judges,\n ),\n ),\n );\n const front = loadOpenTaskFrontier(db, ws.name);\n frontier.push({\n workstream: ws.name,\n roots_cut: front.rootsCut,\n again_cut: front.againCut,\n });\n for (const e of front.entries) {\n edges.push({\n key: keyOf(ws.name, e.task.name),\n workstream: ws.name,\n id: e.task.name,\n title: e.task.title,\n status: e.task.status,\n substate: e.task.substate,\n depth: e.depth,\n parent: e.parent === null ? null : keyOf(ws.name, e.parent),\n again: e.again,\n kids: e.kids,\n closed: e.closed,\n cap_cut: e.capCut,\n });\n }\n }\n tasks.sort(byActivityDesc);\n\n // Every live workstream's agents, whatever the scope or text: Attention reads them all.\n const everyAgent: DashboardAgent[] = [];\n for (const ws of live) {\n for (const a of await readLiveAgents(db, ws.name, undefined, opts.agentStates)) {\n const mine = owned.get(a.incarnation) ?? [];\n everyAgent.push({\n ...liveAgentToOutput(a, ws.id),\n key: keyOf(ws.name, a.name),\n tasks: mine,\n task: mine[0] ?? null,\n ...toolOf(a.tool, now),\n });\n }\n }\n const inScope = new Set(scoped.map((w) => w.name));\n agents.push(\n ...everyAgent.filter(\n (a) => inScope.has(a.workstream) && matches(opts.text, a.key, a.state ?? null, ...a.tasks),\n ),\n );\n\n const next = nextOf(\n db,\n opts.callers ?? (opts.caller === null ? [] : [opts.caller]),\n opts.workstream,\n );\n const logWs = opts.logWorkstream ?? opts.workstream;\n const log = listLogs(db, {\n ...(logWs !== undefined ? { workstream: logWs } : {}),\n limit: opts.logLimit ?? DEFAULT_LOG_LIMIT,\n }).map((row) => ({\n ...logItem(row, true).item,\n scope: parseOpKey(row.workstreamName).workstream ?? null,\n }));\n\n const callerOwn = opts.caller !== null;\n const human_needs = callerOwn\n ? openHumanNeeds(db, opts.caller as string)\n .filter((h) => inScope.has(h.workstream))\n .map((h) => ({\n key: keyOf(h.workstream, h.id),\n workstream: h.workstream,\n id: h.id,\n reason: h.reason,\n }))\n : [];\n const delegates = callerOwn\n ? listDelegates(db, opts.caller as string)\n .filter((c) => inScope.has(c.workstream) && !FINISHED_CALL.includes(c.state))\n .map((c) => ({\n call_id: c.callId,\n workstream: c.workstream,\n task: c.task,\n state: c.state,\n wave: c.wave,\n judges: c.record ? `${c.record.workstream}/${c.record.task}` : null,\n agent: c.agent,\n verdict: c.verdict,\n error: c.error,\n updated_at: c.updatedAt,\n }))\n : [];\n\n const attention: DashboardAttention[] = everyAgent.flatMap((a) => agentAttention(a, now));\n const liveNames = new Set(live.map((w) => w.name));\n // Workstream, then task id, like the human items (listDelegates orders by task row id).\n const refused = listDelegates(db, undefined, undefined, { states: [\"refused\"] }).sort((a, b) =>\n keyOf(a.workstream, a.task).localeCompare(keyOf(b.workstream, b.task)),\n );\n for (const c of refused) {\n if (!liveNames.has(c.workstream)) continue;\n // A refused call has no expiry, and its pi session may be gone: the fix cancels it\n // from a shell (it never ran, so the judged task is left alone).\n attention.push({\n key: `delegate:${keyOf(c.workstream, c.task)}`,\n workstream: c.workstream,\n kind: \"delegate\",\n text: `delegate refused: ${c.request.label ?? c.task} (reissue from pi, or y copies the cancel)`,\n target: keyOf(c.workstream, c.task),\n fix: `mu delegates finish ${shellWord(c.callId)} --outcome cancelled --no-record --author \"$USER\"`,\n });\n }\n for (const t of db\n .prepare(\n `SELECT ws.name AS ws, t.local_id AS id, t.title AS title FROM tasks t\n JOIN workstreams ws ON ws.id = t.workstream_id AND ws.torn_down_at IS NULL\n WHERE t.status = 'OPEN' AND t.kind = 'human' ORDER BY ws.name, t.local_id`,\n )\n .all() as { ws: string; id: string; title: string }[]) {\n attention.push({\n key: `human:${keyOf(t.ws, t.id)}`,\n workstream: t.ws,\n kind: \"human\",\n text: t.title,\n target: keyOf(t.ws, t.id),\n fix: `mu task close ${shellWord(t.id)} -w ${shellWord(t.ws)} --evidence '<what you decided>'`,\n });\n }\n if (next.source === \"unavailable\") {\n attention.push({\n key: \"next_unavailable\",\n workstream: null,\n kind: \"next_unavailable\",\n text: \"next unavailable\",\n target: null,\n });\n }\n (opts.syncWarnings ?? []).forEach((w, i) => {\n attention.push({\n key: `sync:${i}`,\n workstream: null,\n kind: \"sync\",\n text: `sync: ${w}`,\n target: null,\n });\n });\n // Most urgent kind first; within a kind the order read holds (agents, delegate and\n // human items: workstream, then name).\n attention.sort((a, b) => ATTENTION_ORDER.indexOf(a.kind) - ATTENTION_ORDER.indexOf(b.kind));\n\n return {\n caller: opts.caller,\n caller_scope: callerOwn ? \"caller\" : \"none\",\n workstreams: live.map((w) => ({\n name: w.name,\n scratch: w.scratch,\n latest_seq: opts.activity === false ? 0 : latestSeq(db, w.name),\n })),\n tasks,\n edges,\n frontier,\n agents,\n log,\n next: next.plan,\n next_source: next.source,\n human_needs,\n delegates,\n waves,\n attention,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
steerStallHandler,
|
|
42
42
|
waitForEvents,
|
|
43
43
|
waitWithActions
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-3ZLI6YTV.js";
|
|
45
45
|
import {
|
|
46
46
|
FRONTIER_MAX_DEPTH,
|
|
47
47
|
HumanNeededInvalidError,
|
|
@@ -101,7 +101,7 @@ import {
|
|
|
101
101
|
taskRecordCut,
|
|
102
102
|
taskToOutput,
|
|
103
103
|
workstreamIdOf
|
|
104
|
-
} from "./chunk-
|
|
104
|
+
} from "./chunk-RVWNQLFE.js";
|
|
105
105
|
import {
|
|
106
106
|
AgentAbortNeedsCtlError,
|
|
107
107
|
AgentAbortTimeoutError,
|
|
@@ -127,6 +127,11 @@ import {
|
|
|
127
127
|
CTL_OPS,
|
|
128
128
|
CTL_SOCK_ENV,
|
|
129
129
|
CURRENT_SCHEMA_VERSION,
|
|
130
|
+
CarrierJobKeptError,
|
|
131
|
+
CarrierJobUnresolvedError,
|
|
132
|
+
CarrierJobUnstoppedError,
|
|
133
|
+
CarrierStopFailedError,
|
|
134
|
+
CarrierStopRefusedError,
|
|
130
135
|
ClaimerNotRegisteredError,
|
|
131
136
|
CloseNeedsEvidenceError,
|
|
132
137
|
CrossWorkstreamEdgeError,
|
|
@@ -149,6 +154,7 @@ import {
|
|
|
149
154
|
HostChannelBusyError,
|
|
150
155
|
HostCheckFailedError,
|
|
151
156
|
HostExistsError,
|
|
157
|
+
HostHandbackError,
|
|
152
158
|
HostInUseError,
|
|
153
159
|
HostNotFoundError,
|
|
154
160
|
HostProfileError,
|
|
@@ -260,6 +266,7 @@ import {
|
|
|
260
266
|
gitToplevel,
|
|
261
267
|
groupIdFromPrefix,
|
|
262
268
|
headSha,
|
|
269
|
+
hostMuleStarter,
|
|
263
270
|
hostStarter,
|
|
264
271
|
interruptAgent,
|
|
265
272
|
isAgentListArgv,
|
|
@@ -274,6 +281,7 @@ import {
|
|
|
274
281
|
lastClaimActor,
|
|
275
282
|
latestSeq,
|
|
276
283
|
listAgents,
|
|
284
|
+
listCarrierJobs,
|
|
277
285
|
listDelegates,
|
|
278
286
|
listEmptyWorkstreams,
|
|
279
287
|
listForgetCandidates,
|
|
@@ -350,7 +358,7 @@ import {
|
|
|
350
358
|
workstreamRevision,
|
|
351
359
|
worktreePath,
|
|
352
360
|
worktreesRoot
|
|
353
|
-
} from "./chunk-
|
|
361
|
+
} from "./chunk-SFV5RPZW.js";
|
|
354
362
|
import {
|
|
355
363
|
defaultStateDir
|
|
356
364
|
} from "./chunk-PZ5GOCWH.js";
|
|
@@ -3901,7 +3909,10 @@ function classifyError(err) {
|
|
|
3901
3909
|
if (err instanceof NameAmbiguousError || // A host profile that exists, is at its channel cap, or still holds channels.
|
|
3902
3910
|
err instanceof HostExistsError || err instanceof HostChannelBusyError || // A remote worktree close refuses to remove (dirty, uncollected HEAD, moved).
|
|
3903
3911
|
err instanceof RemoteCloseRefusedError || // A recovery record that is ambiguous, claimed, changed or pinned to another profile.
|
|
3904
|
-
err instanceof RemoteRecoveryRefusedError ||
|
|
3912
|
+
err instanceof RemoteRecoveryRefusedError || // A recorded job handle whose stop template changed, or whose profile is gone: nothing ran.
|
|
3913
|
+
err instanceof CarrierStopRefusedError || // A respawn of a name whose older incarnation's job was never stopped.
|
|
3914
|
+
err instanceof CarrierJobUnstoppedError || // A lease close with no handle it can stop, or a reservation it had to keep.
|
|
3915
|
+
err instanceof CarrierJobUnresolvedError || err instanceof CarrierJobKeptError || err instanceof HostInUseError || err instanceof AgentExistsError || err instanceof AgentNameExhaustedError || err instanceof TaskExistsError || err instanceof BackupTargetExistsError || err instanceof TaskAutoRefusedError || err instanceof TaskBriefMissingError || err instanceof DelegateCallConflictError || err instanceof DelegateStateError || err instanceof TaskAlreadyOwnedError || err instanceof TaskHasOpenBlockersError || // Substate refusals (ts_4): a park from the wrong phase, a claim on a
|
|
3905
3916
|
// parked task, a pair outside TASK_SUBSTATES.
|
|
3906
3917
|
err instanceof TaskParkStateError || err instanceof TaskParkedError || err instanceof HumanTaskError || err instanceof TaskInTriageError || err instanceof InvalidSubstateError || err instanceof TaskNotInWorkstreamError || err instanceof AgentNotInWorkstreamError || err instanceof AgentSendUnsupportedError || err instanceof CycleError || err instanceof CrossWorkstreamEdgeError || err instanceof WorktreeDirtyError || err instanceof WorktreeBranchExistsError || err instanceof WorktreeHeldError || err instanceof WorktreePathNotEmptyError || err instanceof WorktreeVcsRequiredError || err instanceof ClaimerNotRegisteredError || err instanceof SchemaTooOldError || err instanceof SchemaTooNewError || err instanceof SqliteTooOldError || err instanceof SyncSourceTooNewError || err instanceof DbInsideSyncDirError || err instanceof SyncDirForeignError || err instanceof WorkstreamExistsError || err instanceof WorkstreamRevisionMismatchError || err instanceof RevisionMismatchError || err instanceof WorkstreamHasAgentsError || err instanceof WorkstreamPanesLiveError || err instanceof WorkstreamSessionLiveError || // Rebuild refuses to overwrite an existing target or to write onto
|
|
3907
3918
|
// the source DB: both are name/state collisions, not usage errors.
|
|
@@ -3960,9 +3971,13 @@ function classifyError(err) {
|
|
|
3960
3971
|
if (err instanceof AgentAbortTimeoutError) {
|
|
3961
3972
|
return { label: "timeout", exitCode: 5 };
|
|
3962
3973
|
}
|
|
3963
|
-
if (err instanceof RemotePrepareError || err instanceof RemoteCollectError
|
|
3974
|
+
if (err instanceof RemotePrepareError || err instanceof RemoteCollectError || // The stop template ran and did not prove the job stopped: the host's answer, substrate.
|
|
3975
|
+
err instanceof CarrierStopFailedError) {
|
|
3964
3976
|
return { label: "remote host", exitCode: 5 };
|
|
3965
3977
|
}
|
|
3978
|
+
if (err instanceof HostHandbackError) {
|
|
3979
|
+
return { label: "human needed", exitCode: 5 };
|
|
3980
|
+
}
|
|
3966
3981
|
if (err instanceof HostCheckFailedError) {
|
|
3967
3982
|
return { label: "host check", exitCode: 5 };
|
|
3968
3983
|
}
|
|
@@ -4074,7 +4089,8 @@ function emitError(err) {
|
|
|
4074
4089
|
error: errClass,
|
|
4075
4090
|
message: cleanMessage,
|
|
4076
4091
|
exit: exitCode,
|
|
4077
|
-
...Object.keys(extra).length > 0 ? { extra } : {}
|
|
4092
|
+
...Object.keys(extra).length > 0 ? { extra } : {},
|
|
4093
|
+
...err instanceof HostHandbackError ? { data: { handback: true, host: err.host, template: err.field, detail: err.detail } } : {}
|
|
4078
4094
|
});
|
|
4079
4095
|
return exitCode;
|
|
4080
4096
|
}
|
|
@@ -4180,12 +4196,20 @@ function findCommandForArgv(root, argv) {
|
|
|
4180
4196
|
}
|
|
4181
4197
|
|
|
4182
4198
|
// src/cli/agent-close.ts
|
|
4199
|
+
function carrierJobWorkstreams(db, name) {
|
|
4200
|
+
return listCarrierJobs(db).filter((j) => j.name === name).map((j) => j.workstream);
|
|
4201
|
+
}
|
|
4202
|
+
var hasCarrierJob = (db, ws, name) => carrierJobWorkstreams(db, name).includes(ws);
|
|
4183
4203
|
async function closeScope(db, name, explicit) {
|
|
4184
4204
|
if (explicit !== void 0 || injectedWorkstream() !== void 0) {
|
|
4185
4205
|
return resolveWorkstream(explicit);
|
|
4186
4206
|
}
|
|
4187
4207
|
const matches = [
|
|
4188
|
-
.../* @__PURE__ */ new Set([
|
|
4208
|
+
.../* @__PURE__ */ new Set([
|
|
4209
|
+
...findEntityWorkstreams(db, "agent", name),
|
|
4210
|
+
...recoveryWorkstreams(db, name),
|
|
4211
|
+
...carrierJobWorkstreams(db, name)
|
|
4212
|
+
])
|
|
4189
4213
|
].sort();
|
|
4190
4214
|
if (matches.length >= 2) throw new NameAmbiguousError(name, matches, "agent");
|
|
4191
4215
|
const only = matches[0];
|
|
@@ -4194,19 +4218,21 @@ async function closeScope(db, name, explicit) {
|
|
|
4194
4218
|
async function cmdClose(db, rawName, opts = {}) {
|
|
4195
4219
|
const name = applyQualifiedRef(rawName, opts);
|
|
4196
4220
|
const ws = await closeScope(db, name, opts.workstream);
|
|
4197
|
-
if (opts.recovery === void 0 && recoveriesFor(db, ws, name).length === 0) {
|
|
4221
|
+
if (opts.recovery === void 0 && recoveriesFor(db, ws, name).length === 0 && !hasCarrierJob(db, ws, name)) {
|
|
4198
4222
|
assertAgentInWorkstream(db, name, opts.workstream ?? ws);
|
|
4199
4223
|
}
|
|
4200
4224
|
const discard = opts.discard === true;
|
|
4201
4225
|
const keepBranch = opts.keepBranch === true;
|
|
4202
4226
|
const recoveryId = opts.recovery;
|
|
4203
4227
|
const force = opts.force === true;
|
|
4228
|
+
const abandonJob = opts.abandonJob === true;
|
|
4204
4229
|
if (opts.if === void 0) {
|
|
4205
4230
|
const p = await previewClose(db, name, {
|
|
4206
4231
|
workstream: ws,
|
|
4207
4232
|
discard,
|
|
4208
4233
|
keepBranch,
|
|
4209
|
-
...recoveryId === void 0 ? {} : { recoveryId }
|
|
4234
|
+
...recoveryId === void 0 ? {} : { recoveryId },
|
|
4235
|
+
...abandonJob ? { abandonJob: true } : {}
|
|
4210
4236
|
});
|
|
4211
4237
|
if (p.kind !== "none" && !p.refusesDirty && p.recovery?.refusal == null) {
|
|
4212
4238
|
setNextOutcome({
|
|
@@ -4221,7 +4247,8 @@ async function cmdClose(db, rawName, opts = {}) {
|
|
|
4221
4247
|
...p.recovery === null ? [] : ["--recovery", String(p.recovery.id)],
|
|
4222
4248
|
...discard ? ["--discard"] : [],
|
|
4223
4249
|
...keepBranch ? ["--keep-branch"] : [],
|
|
4224
|
-
...force ? ["--force"] : []
|
|
4250
|
+
...force ? ["--force"] : [],
|
|
4251
|
+
...abandonJob ? ["--abandon-job"] : []
|
|
4225
4252
|
],
|
|
4226
4253
|
revision: p.revision
|
|
4227
4254
|
});
|
|
@@ -4239,19 +4266,29 @@ async function cmdClose(db, rawName, opts = {}) {
|
|
|
4239
4266
|
...discard ? { discard: true } : {},
|
|
4240
4267
|
...keepBranch ? { keepBranch: true } : {},
|
|
4241
4268
|
...force ? { force: true } : {},
|
|
4269
|
+
...abandonJob ? { abandonJob: true } : {},
|
|
4242
4270
|
...recoveryId === void 0 ? {} : { recoveryId }
|
|
4243
4271
|
});
|
|
4244
4272
|
if (opts.json) {
|
|
4245
4273
|
emitJson({ agentName: name, ...result });
|
|
4246
4274
|
return;
|
|
4247
4275
|
}
|
|
4248
|
-
|
|
4276
|
+
const jobs = result.carrierJobs ?? [];
|
|
4277
|
+
if (!result.killedPane && !result.deletedRow && result.worktree === null && jobs.length === 0) {
|
|
4249
4278
|
console.log(pc.dim(`no agent named ${name} (already closed?)`));
|
|
4250
4279
|
return;
|
|
4251
4280
|
}
|
|
4252
4281
|
console.log(
|
|
4253
|
-
result.recovery
|
|
4282
|
+
result.recovery !== void 0 ? `Closed the remote worktree ${pc.bold(name)} left (recovery ${result.recovery.id})` : !result.killedPane && result.worktree === null ? `Closed the lease job ${pc.bold(name)} left` : `Closed ${pc.bold(name)}`
|
|
4254
4283
|
);
|
|
4284
|
+
for (const j of jobs) {
|
|
4285
|
+
const what = j.job === null ? "lease reservation" : `job ${j.job}`;
|
|
4286
|
+
console.log(
|
|
4287
|
+
pc.dim(
|
|
4288
|
+
j.action === "stopped" ? ` ${what} on ${j.host} stopped` : j.action === "abandon" ? ` ${what} on ${j.host} abandoned without a stop: it may still run there` : ` ${what} on ${j.host} released (its carrier was never launched)`
|
|
4289
|
+
)
|
|
4290
|
+
);
|
|
4291
|
+
}
|
|
4255
4292
|
if (result.worktree !== null) {
|
|
4256
4293
|
const w = result.worktree;
|
|
4257
4294
|
console.log(
|
|
@@ -4262,11 +4299,28 @@ async function cmdClose(db, rawName, opts = {}) {
|
|
|
4262
4299
|
for (const warn of w.warnings ?? []) console.error(pc.yellow(`warning: ${warn}`));
|
|
4263
4300
|
}
|
|
4264
4301
|
}
|
|
4302
|
+
function printCarrierJob(p) {
|
|
4303
|
+
const j = p.carrierJob;
|
|
4304
|
+
if (j === null) return;
|
|
4305
|
+
const what = j.job !== null ? `job ${j.job}` : `a lease reservation with no job handle (launch ${j.launch}, pane ${j.paneId ?? "none"}${j.reason === null ? "" : `, ${j.reason}`})`;
|
|
4306
|
+
console.log(
|
|
4307
|
+
p.abandonJob ? pc.yellow(
|
|
4308
|
+
` ${what} on ${j.host}: --abandon-job drops it WITHOUT a stop (the job may keep running; the job file is left)`
|
|
4309
|
+
) : j.job !== null ? ` ${what} on ${j.host}: stopped (its stop template) before anything else; a failed stop keeps everything` : ` ${what} on ${j.host}: released only if its carrier was never launched; otherwise the close refuses (then --abandon-job)`
|
|
4310
|
+
);
|
|
4311
|
+
}
|
|
4265
4312
|
function printClosePreview(p) {
|
|
4266
4313
|
if (p.kind === "none") {
|
|
4267
4314
|
console.log(pc.dim(`no agent named ${p.agentName} (already closed?)`));
|
|
4268
4315
|
return;
|
|
4269
4316
|
}
|
|
4317
|
+
if (p.kind === "lease-job") {
|
|
4318
|
+
console.log(
|
|
4319
|
+
`${pc.bold(p.agentName)} has no agent row, only a lease job reservation (rev ${p.revision}): no pane is killed, no task changes`
|
|
4320
|
+
);
|
|
4321
|
+
printCarrierJob(p);
|
|
4322
|
+
return;
|
|
4323
|
+
}
|
|
4270
4324
|
if (p.kind === "recovery" && p.recovery !== null && p.remote !== null) {
|
|
4271
4325
|
const head = p.remote.collectedHead;
|
|
4272
4326
|
console.log(
|
|
@@ -4275,6 +4329,7 @@ function printClosePreview(p) {
|
|
|
4275
4329
|
console.log(
|
|
4276
4330
|
` remote worktree ${p.remote.path} on ${p.remote.host}: removed only if clean and at ${p.remote.base.slice(0, 12)}${head === null ? "" : ` or ${head.slice(0, 12)} (collected)`}; otherwise it refuses${p.keepBranch ? " (--keep-branch: unmerged commits go on a remote branch first; dirty still refuses)" : ""}`
|
|
4277
4331
|
);
|
|
4332
|
+
printCarrierJob(p);
|
|
4278
4333
|
if (p.recovery.refusal !== null)
|
|
4279
4334
|
console.log(pc.yellow(` would refuse: ${p.recovery.refusal}`));
|
|
4280
4335
|
return;
|
|
@@ -4282,6 +4337,7 @@ function printClosePreview(p) {
|
|
|
4282
4337
|
console.log(
|
|
4283
4338
|
p.kind === "agent" ? `Close ${pc.bold(p.agentName)}? pane ${p.paneId} will be killed and its row removed (rev ${p.revision})` : `${pc.bold(p.agentName)} has no agent row, only a registered worktree (rev ${p.revision})`
|
|
4284
4339
|
);
|
|
4340
|
+
printCarrierJob(p);
|
|
4285
4341
|
for (const t of p.ownedTasks) {
|
|
4286
4342
|
console.log(` owns ${t.ref} (${t.status}${t.reverts ? ": goes back to OPEN" : ""})`);
|
|
4287
4343
|
}
|
|
@@ -4316,6 +4372,9 @@ function wireCloseCommand(agent) {
|
|
|
4316
4372
|
).option(
|
|
4317
4373
|
"--force",
|
|
4318
4374
|
"close past what you vouch for: a pane mu cannot ask, or a worktree a process writes in or no scan can check (a cwd or reader there only warns); never overrides a live pane or uncommitted work"
|
|
4375
|
+
).option(
|
|
4376
|
+
"--abandon-job",
|
|
4377
|
+
"a lease job whose host is gone for good: drop its handle WITHOUT stopping it (logged with host and handle; the job may keep running). --force never skips the stop"
|
|
4319
4378
|
).option("--if <rev>", "apply the preview with this revision", parseNonNegativeInt).option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(name) {
|
|
4320
4379
|
const opts = this.opts();
|
|
4321
4380
|
return handle((db) => cmdClose(db, name, opts), this)();
|
|
@@ -4654,7 +4713,7 @@ async function parseWaitRef(db, raw, scope, legacy = {}) {
|
|
|
4654
4713
|
return { kind, workstream, id: parsed.name };
|
|
4655
4714
|
}
|
|
4656
4715
|
if (getAgent(db, parsed.name, workstream) === void 0) {
|
|
4657
|
-
const { AgentNotFoundError: AgentNotFoundError2 } = await import("./errors-
|
|
4716
|
+
const { AgentNotFoundError: AgentNotFoundError2 } = await import("./errors-LY4OZVQU.js");
|
|
4658
4717
|
throw new AgentNotFoundError2(parsed.name);
|
|
4659
4718
|
}
|
|
4660
4719
|
return {
|
|
@@ -8045,7 +8104,7 @@ function openDashboardOrHelp(program2) {
|
|
|
8045
8104
|
(syncDb) => withShutdown(syncDb, async (shutdown) => {
|
|
8046
8105
|
const key = "NODE_ENV";
|
|
8047
8106
|
process.env[key] ??= "production";
|
|
8048
|
-
const { runDashboard } = await import("./tui-
|
|
8107
|
+
const { runDashboard } = await import("./tui-3ZPLSLDW.js");
|
|
8049
8108
|
const first = program2.opts().workstream?.[0];
|
|
8050
8109
|
await runDashboard({ dbPath: defaultDbPath(), syncDb, shutdown, initialWorkstream: first });
|
|
8051
8110
|
}),
|
|
@@ -10170,6 +10229,31 @@ async function ctlSocketsDoctorCheck(agents, opts = {}) {
|
|
|
10170
10229
|
}
|
|
10171
10230
|
return { check, agents: reports };
|
|
10172
10231
|
}
|
|
10232
|
+
function leaseJobHints(db) {
|
|
10233
|
+
const agentIds = new Set(
|
|
10234
|
+
db.prepare("SELECT id FROM agents").all().map((r) => r.id)
|
|
10235
|
+
);
|
|
10236
|
+
const named = db.prepare(
|
|
10237
|
+
`SELECT 1 FROM agents a JOIN workstreams w ON w.id = a.workstream_id
|
|
10238
|
+
WHERE w.name = ? AND a.name = ?`
|
|
10239
|
+
);
|
|
10240
|
+
return listCarrierJobs(db).filter(
|
|
10241
|
+
(j) => j.agentId === null ? named.get(j.workstream, j.name) === void 0 : !agentIds.has(j.agentId)
|
|
10242
|
+
).map((j) => ({
|
|
10243
|
+
workstream: j.workstream,
|
|
10244
|
+
name: j.name,
|
|
10245
|
+
host: j.host,
|
|
10246
|
+
job: j.job,
|
|
10247
|
+
launch: j.launch,
|
|
10248
|
+
agentId: j.agentId,
|
|
10249
|
+
reason: j.reason,
|
|
10250
|
+
close: `mu agent close ${j.name} -w ${j.workstream}`
|
|
10251
|
+
}));
|
|
10252
|
+
}
|
|
10253
|
+
function leaseJobHintLine(h) {
|
|
10254
|
+
const what = h.job !== null ? `job ${h.job}` : `no job handle (launch ${h.launch}${h.reason === null ? "" : `, ${h.reason}`})`;
|
|
10255
|
+
return `${h.workstream}/${h.name} on ${h.host}: ${what}, agent ${h.agentId === null ? "never bound" : "gone"}: ${h.close}`;
|
|
10256
|
+
}
|
|
10173
10257
|
|
|
10174
10258
|
// src/dormant.ts
|
|
10175
10259
|
var FINISHED_IDLE_DAYS = 14;
|
|
@@ -10487,6 +10571,11 @@ state (workstream=${ws})`));
|
|
|
10487
10571
|
console.log("");
|
|
10488
10572
|
for (const line2 of oldDelegates.remediation) console.log(` ${line2}`);
|
|
10489
10573
|
}
|
|
10574
|
+
const leaseJobs = leaseJobHints(db);
|
|
10575
|
+
console.log(
|
|
10576
|
+
` ${"lease-job".padEnd(16)} : ${leaseJobs.length === 0 ? `${pc.green("ok")} ${pc.dim("no lease job reservation outlives its agent")}` : `${pc.yellow("hint")} ${pc.dim(`${leaseJobs.length} lease job reservation(s) whose agent is gone; each close stops its job (a host gone for good: --abandon-job)`)}`}`
|
|
10577
|
+
);
|
|
10578
|
+
for (const h of leaseJobs) console.log(pc.dim(` ${leaseJobHintLine(h)}`));
|
|
10490
10579
|
console.log(pc.bold("\ndisk"));
|
|
10491
10580
|
const disk = checkDiskRecon(db);
|
|
10492
10581
|
if (printHazards(disk)) sawHazard = true;
|
|
@@ -10699,6 +10788,7 @@ async function cmdDoctorJson(db, opts = {}) {
|
|
|
10699
10788
|
workstreams: findDormantWorkstreams(db, { currentWorkstream }),
|
|
10700
10789
|
thresholds: { finishedIdleDays: FINISHED_IDLE_DAYS, abandonedIdleDays: ABANDONED_IDLE_DAYS }
|
|
10701
10790
|
},
|
|
10791
|
+
leaseJobs: leaseJobHints(db),
|
|
10702
10792
|
oldDelegates: (() => {
|
|
10703
10793
|
const h = checkOldDelegates(db);
|
|
10704
10794
|
return {
|
|
@@ -10812,7 +10902,9 @@ function cmdHostAdd(db, name, opts) {
|
|
|
10812
10902
|
exec: opts.exec,
|
|
10813
10903
|
fetch: opts.fetch,
|
|
10814
10904
|
repo: opts.repo ?? null,
|
|
10815
|
-
max_sessions: opts.maxSessions ?? null
|
|
10905
|
+
max_sessions: opts.maxSessions ?? null,
|
|
10906
|
+
transport: opts.transport ?? "session",
|
|
10907
|
+
stop: opts.stop ?? null
|
|
10816
10908
|
});
|
|
10817
10909
|
if (isJsonMode()) {
|
|
10818
10910
|
emitJson({ host });
|
|
@@ -10832,9 +10924,15 @@ function cmdHostList(db) {
|
|
|
10832
10924
|
}
|
|
10833
10925
|
const held = /* @__PURE__ */ new Map();
|
|
10834
10926
|
for (const c of listHostChannels(db)) held.set(c.host, (held.get(c.host) ?? 0) + 1);
|
|
10835
|
-
const t = muTable({ head: ["HOST", "DEFAULT REPO", "CHANNELS"] });
|
|
10927
|
+
const t = muTable({ head: ["HOST", "TRANSPORT", "STOP", "DEFAULT REPO", "CHANNELS"] });
|
|
10836
10928
|
for (const h of hosts) {
|
|
10837
|
-
t.push([
|
|
10929
|
+
t.push([
|
|
10930
|
+
h.name,
|
|
10931
|
+
h.transport,
|
|
10932
|
+
h.stop ?? "-",
|
|
10933
|
+
h.repo ?? "-",
|
|
10934
|
+
`${held.get(h.name) ?? 0}/${h.max_sessions ?? "\u221E"}`
|
|
10935
|
+
]);
|
|
10838
10936
|
}
|
|
10839
10937
|
console.log(t.toString());
|
|
10840
10938
|
}
|
|
@@ -10846,9 +10944,10 @@ function cmdHostShow(db, name) {
|
|
|
10846
10944
|
return;
|
|
10847
10945
|
}
|
|
10848
10946
|
console.log(
|
|
10849
|
-
`${pc.bold(host.name)} default repo ${host.repo ?? "-"} max-sessions ${host.max_sessions ?? "none"}`
|
|
10947
|
+
`${pc.bold(host.name)} transport ${host.transport} default repo ${host.repo ?? "-"} max-sessions ${host.max_sessions ?? "none"}`
|
|
10850
10948
|
);
|
|
10851
10949
|
for (const f of ["shell", "exec", "fetch"]) console.log(` ${f.padEnd(6)} ${host[f]}`);
|
|
10950
|
+
if (host.stop !== null) console.log(` ${"stop".padEnd(6)} ${host.stop}`);
|
|
10852
10951
|
console.log(
|
|
10853
10952
|
channels.length === 0 ? " channels: none held" : ` channels: ${channels.map(channelLine).join("; ")}`
|
|
10854
10953
|
);
|
|
@@ -10862,8 +10961,11 @@ function cmdHostRm(db, name) {
|
|
|
10862
10961
|
console.log(removed ? `removed host ${name}` : `no host profile named ${name}`);
|
|
10863
10962
|
}
|
|
10864
10963
|
function cmdHostInit(name, opts) {
|
|
10865
|
-
if (opts.ssh
|
|
10866
|
-
throw new UsageError("host init
|
|
10964
|
+
if (opts.ssh === true && opts.mule === true) {
|
|
10965
|
+
throw new UsageError("host init takes one starter kind: --ssh or --mule");
|
|
10966
|
+
}
|
|
10967
|
+
if (opts.ssh !== true && opts.mule !== true) {
|
|
10968
|
+
throw new UsageError("host init needs a starter kind: --ssh or --mule");
|
|
10867
10969
|
}
|
|
10868
10970
|
if (!isValidHostName(name)) {
|
|
10869
10971
|
throw new UsageError(
|
|
@@ -10871,20 +10973,24 @@ function cmdHostInit(name, opts) {
|
|
|
10871
10973
|
{ showHelp: false }
|
|
10872
10974
|
);
|
|
10873
10975
|
}
|
|
10874
|
-
const
|
|
10976
|
+
const lease = opts.mule === true;
|
|
10977
|
+
const s = lease ? hostMuleStarter(name) : hostStarter(name);
|
|
10875
10978
|
const command2 = [
|
|
10876
|
-
`mu host add ${name}`,
|
|
10979
|
+
`mu host add ${name}${lease ? " --transport lease" : ""}`,
|
|
10877
10980
|
`--shell ${shellQuote(s.shell)}`,
|
|
10878
10981
|
`--exec ${shellQuote(s.exec)}`,
|
|
10982
|
+
...s.stop === void 0 ? [] : [`--stop ${shellQuote(s.stop)}`],
|
|
10879
10983
|
`--fetch ${shellQuote(s.fetch)}`,
|
|
10880
|
-
"--max-sessions 1"
|
|
10984
|
+
...lease ? [] : ["--max-sessions 1"]
|
|
10881
10985
|
].join(" \\\n ");
|
|
10882
10986
|
if (isJsonMode()) {
|
|
10883
|
-
emitJson({ host: name, ...s, command: command2 });
|
|
10987
|
+
emitJson({ host: name, ...lease ? { transport: "lease" } : {}, ...s, command: command2 });
|
|
10884
10988
|
return;
|
|
10885
10989
|
}
|
|
10886
10990
|
console.log(`# Starter for ${name}. Edit it, then run it: mu runs none of this.`);
|
|
10887
|
-
console.log(
|
|
10991
|
+
console.log(
|
|
10992
|
+
lease ? `# ${name} is mule's host; a lease worker holds no ssh session (docs/guide/remote.md).` : `# ${name} is the ssh target; ~/.ssh/config decides the rest.`
|
|
10993
|
+
);
|
|
10888
10994
|
console.log(command2);
|
|
10889
10995
|
}
|
|
10890
10996
|
async function cmdHostCheck(db, name, opts) {
|
|
@@ -10902,6 +11008,7 @@ async function cmdHostCheck(db, name, opts) {
|
|
|
10902
11008
|
const mark = s.ok === true ? pc.green("ok ") : s.ok === false ? pc.red("FAIL") : pc.dim("skip");
|
|
10903
11009
|
console.log(`${mark} ${s.step.padEnd(9)} ${s.detail}`);
|
|
10904
11010
|
}
|
|
11011
|
+
for (const w of report.warnings) console.log(`${pc.yellow("warn")} ${w}`);
|
|
10905
11012
|
if (!report.ok) throw failedError(report);
|
|
10906
11013
|
}
|
|
10907
11014
|
function failedError(report) {
|
|
@@ -10915,10 +11022,16 @@ function wireHostCommands(program2) {
|
|
|
10915
11022
|
"Host profiles: command templates for a remote host. mu never connects and knows no ssh flags; it fills your templates and runs them with /bin/sh -c"
|
|
10916
11023
|
);
|
|
10917
11024
|
host.command("add <name>").description(
|
|
10918
|
-
"Store a machine-local host profile
|
|
11025
|
+
"Store a machine-local host profile: --shell opens the pane, --exec runs one remote command, --fetch brings commits home. --transport lease adds --stop and {job_file}/{job}; grammar in docs/guide/remote.md."
|
|
10919
11026
|
).requiredOption("--shell <template>", "pane command template (opens the remote shell)").requiredOption("--exec <template>", "template that runs one remote command ({cmd})").requiredOption("--fetch <template>", "local template that fetches commits home ({cwd})").option(
|
|
10920
11027
|
"--repo <path>",
|
|
10921
11028
|
"default remote repo for `agent spawn --host` without --root (a profile names a machine, not a repo)"
|
|
11029
|
+
).option(
|
|
11030
|
+
"--transport <kind>",
|
|
11031
|
+
"session (default: the pane holds an ssh) or lease (the pane is a carrier owning a detached remote job; needs --stop and {job_file} in --shell)"
|
|
11032
|
+
).option(
|
|
11033
|
+
"--stop <template>",
|
|
11034
|
+
"lease only: ends a job given its handle ({job}, required; {cwd} is `.`). Must be idempotent: exit 0 for a job already ended"
|
|
10922
11035
|
).option(
|
|
10923
11036
|
"--max-sessions <n>",
|
|
10924
11037
|
"cap on channels mu holds to this host at once (panes, --exec, --fetch)",
|
|
@@ -10930,20 +11043,22 @@ function wireHostCommands(program2) {
|
|
|
10930
11043
|
host.command("list").description("List host profiles with channels held / cap").option(...JSON_OPT).action(function() {
|
|
10931
11044
|
return handle(async (db) => cmdHostList(db), this)();
|
|
10932
11045
|
});
|
|
10933
|
-
host.command("show <name>").description(
|
|
11046
|
+
host.command("show <name>").description(
|
|
11047
|
+
"Show a profile's transport, templates and the channels it holds, with their holders"
|
|
11048
|
+
).option(...JSON_OPT).action(function(name) {
|
|
10934
11049
|
return handle(async (db) => cmdHostShow(db, name), this)();
|
|
10935
11050
|
});
|
|
10936
11051
|
host.command("rm <name>").description("Remove a profile. Refuses while it holds a channel (close those agents first)").option(...JSON_OPT).action(function(name) {
|
|
10937
11052
|
return handle(async (db) => cmdHostRm(db, name), this)();
|
|
10938
11053
|
});
|
|
10939
11054
|
host.command("init <name>").description(
|
|
10940
|
-
"Print a starter `mu host add` for ssh (the socket forward, `$SHELL -ilc`, identity env inside the command) for
|
|
10941
|
-
).option("--ssh", "print the ssh starter").option(...JSON_OPT).action(function(name) {
|
|
11055
|
+
"Print a starter `mu host add` for you to edit: --ssh (the socket forward, `$SHELL -ilc`, identity env inside the command) or --mule (a lease profile for a session-capped host). Prints only: mu never runs it and never connects."
|
|
11056
|
+
).option("--ssh", "print the ssh starter").option("--mule", "print the lease starter for a session-capped host (a mule carrier)").option(...JSON_OPT).action(function(name) {
|
|
10942
11057
|
const opts = this.opts();
|
|
10943
11058
|
return handle(async () => cmdHostInit(name, opts), this)();
|
|
10944
11059
|
});
|
|
10945
11060
|
host.command("check <name>").description(
|
|
10946
|
-
"Run the profile's --exec under one channel: host answers,
|
|
11061
|
+
"Run the profile's --exec under one channel: host answers, repo is git, pi runs, `pi list` names mu. A lease profile's templates are checked too (stop expanded, not run). --agent probes its control socket and job. Exit 5 on failure."
|
|
10947
11062
|
).option("--agent <name>", "also probe this agent's control socket (the forward rides its pane)").option("--root <path>", "the remote repo the repo step checks (default: the profile's)").option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(name) {
|
|
10948
11063
|
const opts = this.optsWithGlobals();
|
|
10949
11064
|
return handle((db) => cmdHostCheck(db, name, opts), this)();
|
|
@@ -12472,4 +12587,4 @@ export {
|
|
|
12472
12587
|
truncate,
|
|
12473
12588
|
truncateFront
|
|
12474
12589
|
};
|
|
12475
|
-
//# sourceMappingURL=cli-
|
|
12590
|
+
//# sourceMappingURL=cli-72EJ3CKW.js.map
|