@martintrojer/mu 0.3.1 → 0.3.2
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/cli.js +747 -119
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +505 -72
- package/dist/index.js +601 -97
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +4 -4
- package/docs/ROADMAP.md +8 -6
- package/docs/USAGE_GUIDE.md +73 -9
- package/docs/VOCABULARY.md +6 -3
- package/package.json +1 -1
- package/skills/mu/SKILL.md +42 -15
package/dist/cli.js
CHANGED
|
@@ -530,6 +530,18 @@ function lastClaimActor(db, workstream, localId) {
|
|
|
530
530
|
if (!row) return null;
|
|
531
531
|
return parseClaimEventActor(row.payload);
|
|
532
532
|
}
|
|
533
|
+
function lastClaimEventAt(db, workstream, localId) {
|
|
534
|
+
const escaped = localId.replace(/[\\%_]/g, (c) => `\\${c}`);
|
|
535
|
+
const pattern = `${CLAIM_EVENT_PREFIX} ${escaped} %`;
|
|
536
|
+
const wsId = tryResolveWorkstreamId(db, workstream);
|
|
537
|
+
if (wsId === null) return null;
|
|
538
|
+
const row = db.prepare(
|
|
539
|
+
`SELECT created_at FROM agent_logs
|
|
540
|
+
WHERE workstream_id = ? AND kind = 'event' AND payload LIKE ? ESCAPE '\\'
|
|
541
|
+
ORDER BY seq DESC LIMIT 1`
|
|
542
|
+
).get(wsId, pattern);
|
|
543
|
+
return row ? row.created_at : null;
|
|
544
|
+
}
|
|
533
545
|
var EVENT_VERB_PREFIXES = [
|
|
534
546
|
// src/tasks.ts + src/tasks/*.ts
|
|
535
547
|
"task add",
|
|
@@ -552,6 +564,7 @@ var EVENT_VERB_PREFIXES = [
|
|
|
552
564
|
"agent close",
|
|
553
565
|
"agent free",
|
|
554
566
|
"agent adopt",
|
|
567
|
+
"agent kick",
|
|
555
568
|
// src/tasks/wait.ts — emitted when --stuck-after fires (alive +
|
|
556
569
|
// assigned + no recent progress; idle_assigned_agent_detection).
|
|
557
570
|
"agent stalled",
|
|
@@ -559,6 +572,7 @@ var EVENT_VERB_PREFIXES = [
|
|
|
559
572
|
"workspace create",
|
|
560
573
|
"workspace free",
|
|
561
574
|
"workspace refresh",
|
|
575
|
+
"workspace recreate",
|
|
562
576
|
// src/workstream.ts
|
|
563
577
|
"workstream init",
|
|
564
578
|
"workstream destroy",
|
|
@@ -892,6 +906,24 @@ async function enableMuPaneBorders(target) {
|
|
|
892
906
|
await tmux(["set-option", "-w", "-t", target, "pane-active-border-style", "fg=cyan,bold"]);
|
|
893
907
|
await tmux(["set-option", "-w", "-t", target, "pane-border-style", "fg=brightblack"]);
|
|
894
908
|
}
|
|
909
|
+
async function paneTTY(paneId) {
|
|
910
|
+
assertValidPaneId(paneId);
|
|
911
|
+
const result = await currentExecutor(["display-message", "-t", paneId, "-p", "#{pane_tty}"]);
|
|
912
|
+
if (result.exitCode !== 0) {
|
|
913
|
+
if (/can't find pane|pane not found/i.test(result.stderr)) {
|
|
914
|
+
throw new PaneNotFoundError(paneId);
|
|
915
|
+
}
|
|
916
|
+
throw new TmuxError(
|
|
917
|
+
["display-message", "-t", paneId, "-p", "#{pane_tty}"],
|
|
918
|
+
result.stderr,
|
|
919
|
+
result.stdout,
|
|
920
|
+
result.exitCode
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
const tty = result.stdout.trim();
|
|
924
|
+
if (tty === "") throw new PaneNotFoundError(paneId);
|
|
925
|
+
return tty;
|
|
926
|
+
}
|
|
895
927
|
async function getPaneTitle(paneId) {
|
|
896
928
|
if (!isValidPaneId(paneId)) return void 0;
|
|
897
929
|
const result = await currentExecutor(["display-message", "-t", paneId, "-p", "#{pane_title}"]);
|
|
@@ -1559,7 +1591,7 @@ var ClaimerNotRegisteredError = class extends Error {
|
|
|
1559
1591
|
* Three actionable resolutions in expected-frequency order:
|
|
1560
1592
|
* 1. --self : orchestrator pattern (working directly)
|
|
1561
1593
|
* 2. --for : dispatcher pattern (assigning to a worker)
|
|
1562
|
-
* 3. mu adopt: registration pattern (promote pane to worker)
|
|
1594
|
+
* 3. mu agent adopt: registration pattern (promote pane to worker)
|
|
1563
1595
|
*/
|
|
1564
1596
|
errorNextSteps() {
|
|
1565
1597
|
const steps = [
|
|
@@ -1567,9 +1599,9 @@ var ClaimerNotRegisteredError = class extends Error {
|
|
|
1567
1599
|
{ intent: "Dispatch to a worker", command: "mu task claim <id> --for <worker>" }
|
|
1568
1600
|
];
|
|
1569
1601
|
steps.push(
|
|
1570
|
-
this.paneId !== null ? { intent: "Register this pane", command: `mu adopt ${this.paneId}` } : {
|
|
1602
|
+
this.paneId !== null ? { intent: "Register this pane", command: `mu agent adopt ${this.paneId}` } : {
|
|
1571
1603
|
intent: "Register a pane",
|
|
1572
|
-
command: "mu adopt <pane-id> (must be in mu-<workstream> tmux session)"
|
|
1604
|
+
command: "mu agent adopt <pane-id> (must be in mu-<workstream> tmux session)"
|
|
1573
1605
|
}
|
|
1574
1606
|
);
|
|
1575
1607
|
return steps;
|
|
@@ -2170,16 +2202,6 @@ function listArchivedTasks(db, label, opts = {}) {
|
|
|
2170
2202
|
}
|
|
2171
2203
|
|
|
2172
2204
|
// src/exporting.ts
|
|
2173
|
-
var LegacyExportLayoutError = class extends Error {
|
|
2174
|
-
constructor(outDir) {
|
|
2175
|
-
super(
|
|
2176
|
-
`${outDir} was created with a pre-bucket export (mu < 0.3); the on-disk shape changed in 0.3 (top-level README/INDEX/manifest + per-source-ws subdirs). Re-export is not in-place; either pick a different --out or 'rm -rf ${outDir}' and re-run.`
|
|
2177
|
-
);
|
|
2178
|
-
this.outDir = outDir;
|
|
2179
|
-
}
|
|
2180
|
-
outDir;
|
|
2181
|
-
name = "LegacyExportLayoutError";
|
|
2182
|
-
};
|
|
2183
2205
|
function fenceForBody(body) {
|
|
2184
2206
|
const longestRun = (body.match(/`+/g) ?? []).reduce((m, s) => Math.max(m, s.length), 0);
|
|
2185
2207
|
return "`".repeat(Math.max(3, longestRun + 1));
|
|
@@ -2350,9 +2372,6 @@ function readManifest(path) {
|
|
|
2350
2372
|
if (obj.bucketVersion === 2 && typeof obj.sources === "object" && obj.sources !== null) {
|
|
2351
2373
|
return { kind: "v2", manifest: obj };
|
|
2352
2374
|
}
|
|
2353
|
-
if (typeof obj.workstream === "string" && Array.isArray(obj.tasks)) {
|
|
2354
|
-
return { kind: "legacy" };
|
|
2355
|
-
}
|
|
2356
2375
|
return { kind: "corrupt" };
|
|
2357
2376
|
}
|
|
2358
2377
|
function sha256Hex(content) {
|
|
@@ -2380,9 +2399,6 @@ function renderToBucket(input) {
|
|
|
2380
2399
|
}
|
|
2381
2400
|
const manifestPath = join3(outDir, "manifest.json");
|
|
2382
2401
|
const probe = readManifest(manifestPath);
|
|
2383
|
-
if (probe.kind === "legacy") {
|
|
2384
|
-
throw new LegacyExportLayoutError(outDir);
|
|
2385
|
-
}
|
|
2386
2402
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2387
2403
|
const muVersion = readMuVersion();
|
|
2388
2404
|
const previous = probe.kind === "v2" ? probe.manifest : void 0;
|
|
@@ -2556,7 +2572,6 @@ function exportSourcesForArchive(db, label) {
|
|
|
2556
2572
|
for (const [sourceName, taskList] of bySource) {
|
|
2557
2573
|
const tasks = taskList.map((t) => ({
|
|
2558
2574
|
name: t.originalLocalId,
|
|
2559
|
-
localId: t.originalLocalId,
|
|
2560
2575
|
workstreamName: t.sourceWorkstream,
|
|
2561
2576
|
title: t.title,
|
|
2562
2577
|
// Status as snapshotted; cast through the TaskStatus union by
|
|
@@ -2634,24 +2649,30 @@ var WorkspaceVcsRequiredError = class extends Error {
|
|
|
2634
2649
|
}
|
|
2635
2650
|
};
|
|
2636
2651
|
var WorkspaceDirtyError = class extends Error {
|
|
2637
|
-
constructor(workspacePath2, files) {
|
|
2652
|
+
constructor(workspacePath2, files, verb = "rebase") {
|
|
2638
2653
|
super(
|
|
2639
|
-
`workspace dirty (${files.length} uncommitted file(s)): ${workspacePath2}; refusing to
|
|
2654
|
+
`workspace dirty (${files.length} uncommitted file(s)): ${workspacePath2}; refusing to ${verb}`
|
|
2640
2655
|
);
|
|
2641
2656
|
this.workspacePath = workspacePath2;
|
|
2642
2657
|
this.files = files;
|
|
2658
|
+
this.verb = verb;
|
|
2643
2659
|
}
|
|
2644
2660
|
workspacePath;
|
|
2645
2661
|
files;
|
|
2646
2662
|
name = "WorkspaceDirtyError";
|
|
2663
|
+
/** The verb that refused ("rebase", "recreate", ...). Used to make
|
|
2664
|
+
* the error message + nextSteps point the operator at the right
|
|
2665
|
+
* escape hatch (e.g. recreate's `--force`). Default "rebase" for
|
|
2666
|
+
* backward compatibility with the original rebaseTo call sites. */
|
|
2667
|
+
verb;
|
|
2647
2668
|
errorNextSteps() {
|
|
2648
|
-
|
|
2669
|
+
const steps = [
|
|
2649
2670
|
{
|
|
2650
2671
|
intent: "Inspect the dirty files",
|
|
2651
2672
|
command: `(cd ${this.workspacePath} && git status -s) # or jj st / sl st`
|
|
2652
2673
|
},
|
|
2653
2674
|
{
|
|
2654
|
-
intent:
|
|
2675
|
+
intent: `Commit them first, then retry ${this.verb}`,
|
|
2655
2676
|
command: `(cd ${this.workspacePath} && git add -A && git commit -m WIP)`
|
|
2656
2677
|
},
|
|
2657
2678
|
{
|
|
@@ -2659,6 +2680,13 @@ var WorkspaceDirtyError = class extends Error {
|
|
|
2659
2680
|
command: `(cd ${this.workspacePath} && git stash)`
|
|
2660
2681
|
}
|
|
2661
2682
|
];
|
|
2683
|
+
if (this.verb === "recreate") {
|
|
2684
|
+
steps.push({
|
|
2685
|
+
intent: "Or DISCARD all uncommitted changes (the lossy escape)",
|
|
2686
|
+
command: "mu workspace recreate <agent> --force"
|
|
2687
|
+
});
|
|
2688
|
+
}
|
|
2689
|
+
return steps;
|
|
2662
2690
|
}
|
|
2663
2691
|
};
|
|
2664
2692
|
var WorkspaceConflictError = class extends Error {
|
|
@@ -2727,6 +2755,13 @@ var noneBackend = {
|
|
|
2727
2755
|
async commitsBehind(_workspacePath, _ref) {
|
|
2728
2756
|
return null;
|
|
2729
2757
|
},
|
|
2758
|
+
// none has no notion of clean — a cp -a snapshot doesn't track
|
|
2759
|
+
// committed vs uncommitted state. Returning true makes the
|
|
2760
|
+
// close-auto-free path silently free a none-workspace (consistent
|
|
2761
|
+
// with the fact that there are no commits to lose).
|
|
2762
|
+
async isClean(_workspacePath) {
|
|
2763
|
+
return true;
|
|
2764
|
+
},
|
|
2730
2765
|
// none has no upstream to rebase onto. Throw a typed error so the
|
|
2731
2766
|
// CLI's handle() maps it to exit 4 with a clean Next: hint.
|
|
2732
2767
|
async rebaseTo(workspacePath2, _fromRef) {
|
|
@@ -2736,6 +2771,11 @@ var noneBackend = {
|
|
|
2736
2771
|
// doesn't track history. Same typed error as rebaseTo.
|
|
2737
2772
|
async commitsSinceBase(workspacePath2, _baseRef) {
|
|
2738
2773
|
throw new WorkspaceVcsRequiredError("commits", workspacePath2);
|
|
2774
|
+
},
|
|
2775
|
+
// No VCS → nothing to compare against; "dirty" is unanswerable.
|
|
2776
|
+
// Caller (`recreateWorkspace`) treats [] as "clean" and proceeds.
|
|
2777
|
+
async listDirtyFiles(_workspacePath) {
|
|
2778
|
+
return [];
|
|
2739
2779
|
}
|
|
2740
2780
|
};
|
|
2741
2781
|
var gitBackend = {
|
|
@@ -2756,6 +2796,20 @@ var gitBackend = {
|
|
|
2756
2796
|
const sha = await run("git", ["rev-parse", "HEAD"], opts.workspacePath);
|
|
2757
2797
|
return { parentRef: sha };
|
|
2758
2798
|
},
|
|
2799
|
+
// Working-copy clean check: empty `git status --porcelain` output
|
|
2800
|
+
// means no working-tree, staged, or untracked-not-ignored changes.
|
|
2801
|
+
// Returns false on any failure (workspace path missing, git
|
|
2802
|
+
// explodes) — be conservative; auto-free should never "silently
|
|
2803
|
+
// succeed" because we couldn't check.
|
|
2804
|
+
async isClean(workspacePath2) {
|
|
2805
|
+
if (!existsSync3(workspacePath2)) return false;
|
|
2806
|
+
try {
|
|
2807
|
+
const files = await listGitDirtyFiles(workspacePath2);
|
|
2808
|
+
return files.length === 0;
|
|
2809
|
+
} catch {
|
|
2810
|
+
return false;
|
|
2811
|
+
}
|
|
2812
|
+
},
|
|
2759
2813
|
// Compute commits-behind as: count of commits reachable from main
|
|
2760
2814
|
// but not from `ref`. Resolves "main" via origin/HEAD (the symbolic
|
|
2761
2815
|
// ref the remote advertises), falling back to origin/main and then
|
|
@@ -2898,6 +2952,10 @@ var gitBackend = {
|
|
|
2898
2952
|
const result = { removed: true };
|
|
2899
2953
|
if (committedRef !== void 0) result.committedRef = committedRef;
|
|
2900
2954
|
return result;
|
|
2955
|
+
},
|
|
2956
|
+
async listDirtyFiles(workspacePath2) {
|
|
2957
|
+
if (!existsSync3(workspacePath2)) return [];
|
|
2958
|
+
return listGitDirtyFiles(workspacePath2);
|
|
2901
2959
|
}
|
|
2902
2960
|
};
|
|
2903
2961
|
async function resolveGitMainRef(workspacePath2) {
|
|
@@ -3006,6 +3064,23 @@ var jjBackend = {
|
|
|
3006
3064
|
if (committedRef !== void 0) result.committedRef = committedRef;
|
|
3007
3065
|
return result;
|
|
3008
3066
|
},
|
|
3067
|
+
// jj working-copy clean: @ has no diff from its parent.
|
|
3068
|
+
// `jj diff -r @ --summary` prints one line per changed file; empty
|
|
3069
|
+
// stdout = clean. jj's auto-snapshotting means there's no separate
|
|
3070
|
+
// "untracked" bucket — every working-tree change is already in @.
|
|
3071
|
+
async isClean(workspacePath2) {
|
|
3072
|
+
if (!existsSync3(workspacePath2)) return false;
|
|
3073
|
+
try {
|
|
3074
|
+
const out = await run(
|
|
3075
|
+
"jj",
|
|
3076
|
+
["diff", "-r", "@", "--summary", "--no-pager", "--color", "never"],
|
|
3077
|
+
workspacePath2
|
|
3078
|
+
);
|
|
3079
|
+
return out.length === 0;
|
|
3080
|
+
} catch {
|
|
3081
|
+
return false;
|
|
3082
|
+
}
|
|
3083
|
+
},
|
|
3009
3084
|
// Compute commits-behind via jj's `trunk()` revset, which resolves
|
|
3010
3085
|
// to the project's configured trunk (default-branch heuristic).
|
|
3011
3086
|
// Returns null when trunk() is unresolvable (e.g. fresh repo with
|
|
@@ -3126,6 +3201,13 @@ var jjBackend = {
|
|
|
3126
3201
|
workspacePath2
|
|
3127
3202
|
);
|
|
3128
3203
|
return parseNulRecords(out);
|
|
3204
|
+
},
|
|
3205
|
+
// jj is always-snapshotted: there is no "uncommitted" state. The
|
|
3206
|
+
// working copy is itself a commit; the next snapshot folds any
|
|
3207
|
+
// edits in. Surface that by returning [] so `recreateWorkspace`
|
|
3208
|
+
// never refuses a jj workspace as "dirty".
|
|
3209
|
+
async listDirtyFiles(_workspacePath) {
|
|
3210
|
+
return [];
|
|
3129
3211
|
}
|
|
3130
3212
|
};
|
|
3131
3213
|
function parseNulRecords(raw) {
|
|
@@ -3197,6 +3279,18 @@ var slBackend = {
|
|
|
3197
3279
|
if (committedRef !== void 0) result.committedRef = committedRef;
|
|
3198
3280
|
return result;
|
|
3199
3281
|
},
|
|
3282
|
+
// sl working-copy clean: empty `sl status` output. Same shape as
|
|
3283
|
+
// listSlDirtyFiles below but inlined to keep the failure-mode
|
|
3284
|
+
// boundary tight (any throw → not clean).
|
|
3285
|
+
async isClean(workspacePath2) {
|
|
3286
|
+
if (!existsSync3(workspacePath2)) return false;
|
|
3287
|
+
try {
|
|
3288
|
+
const out = await run("sl", ["status"], workspacePath2);
|
|
3289
|
+
return out.length === 0;
|
|
3290
|
+
} catch {
|
|
3291
|
+
return false;
|
|
3292
|
+
}
|
|
3293
|
+
},
|
|
3200
3294
|
// Same shape as the jj impl: count commits in trunk() not reachable
|
|
3201
3295
|
// from ref. Sapling's revset language is close enough to jj's that
|
|
3202
3296
|
// the same idiom works. Returns null when trunk() is unresolvable
|
|
@@ -3273,6 +3367,10 @@ var slBackend = {
|
|
|
3273
3367
|
workspacePath2
|
|
3274
3368
|
);
|
|
3275
3369
|
return parseNulRecords(out).reverse();
|
|
3370
|
+
},
|
|
3371
|
+
async listDirtyFiles(workspacePath2) {
|
|
3372
|
+
if (!existsSync3(workspacePath2)) return [];
|
|
3373
|
+
return listSlDirtyFiles(workspacePath2);
|
|
3276
3374
|
}
|
|
3277
3375
|
};
|
|
3278
3376
|
async function listSlDirtyFiles(workspacePath2) {
|
|
@@ -3315,6 +3413,10 @@ import { existsSync as existsSync4, readdirSync, rmSync as rmSync2 } from "fs";
|
|
|
3315
3413
|
import { homedir as homedir2 } from "os";
|
|
3316
3414
|
import { join as join5, resolve as resolve2 } from "path";
|
|
3317
3415
|
|
|
3416
|
+
// src/agents/spawn.ts
|
|
3417
|
+
import { execFile as execFile2 } from "child_process";
|
|
3418
|
+
import { promisify as promisify2 } from "util";
|
|
3419
|
+
|
|
3318
3420
|
// src/output.ts
|
|
3319
3421
|
import Table from "cli-table3";
|
|
3320
3422
|
import picocolors from "picocolors";
|
|
@@ -3397,10 +3499,49 @@ function maybeWarnNonConventionalAgentName(name) {
|
|
|
3397
3499
|
);
|
|
3398
3500
|
}
|
|
3399
3501
|
function resolveCliCommand(cli) {
|
|
3400
|
-
const envName =
|
|
3502
|
+
const envName = envVarNameForCli(cli);
|
|
3401
3503
|
const override = process.env[envName];
|
|
3402
3504
|
return override && override.trim() !== "" ? override : cli;
|
|
3403
3505
|
}
|
|
3506
|
+
function envVarNameForCli(cli) {
|
|
3507
|
+
return `MU_${cli.toUpperCase().replace(/-/g, "_")}_COMMAND`;
|
|
3508
|
+
}
|
|
3509
|
+
function resolveCliCommandWithSource(cli) {
|
|
3510
|
+
const envVar = envVarNameForCli(cli);
|
|
3511
|
+
const override = process.env[envVar];
|
|
3512
|
+
if (override !== void 0 && override.trim() !== "") {
|
|
3513
|
+
return { command: override, envVar, resolvedFromEnv: true };
|
|
3514
|
+
}
|
|
3515
|
+
return { command: cli, envVar, resolvedFromEnv: false };
|
|
3516
|
+
}
|
|
3517
|
+
var execFileP = promisify2(execFile2);
|
|
3518
|
+
async function defaultCommandResolver(command) {
|
|
3519
|
+
const binary = parseFirstToken(command);
|
|
3520
|
+
if (binary === "") return { ok: false, binary };
|
|
3521
|
+
try {
|
|
3522
|
+
const { stdout } = await execFileP("/bin/sh", ["-c", `command -v -- ${shellQuote(binary)}`], {
|
|
3523
|
+
env: process.env
|
|
3524
|
+
});
|
|
3525
|
+
const resolvedPath = stdout.trim();
|
|
3526
|
+
if (resolvedPath === "") return { ok: false, binary };
|
|
3527
|
+
return { ok: true, binary, resolvedPath };
|
|
3528
|
+
} catch {
|
|
3529
|
+
return { ok: false, binary };
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
function shellQuote(s) {
|
|
3533
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
3534
|
+
}
|
|
3535
|
+
function parseFirstToken(command) {
|
|
3536
|
+
const trimmed = command.trim();
|
|
3537
|
+
if (trimmed === "") return "";
|
|
3538
|
+
const match = trimmed.match(/^\S+/);
|
|
3539
|
+
return match ? match[0] : "";
|
|
3540
|
+
}
|
|
3541
|
+
var activeCommandResolver = defaultCommandResolver;
|
|
3542
|
+
async function checkCommandResolvable(command) {
|
|
3543
|
+
return activeCommandResolver(command);
|
|
3544
|
+
}
|
|
3404
3545
|
async function spawnAgent(db, opts) {
|
|
3405
3546
|
if (!isValidAgentName(opts.name)) {
|
|
3406
3547
|
throw new TypeError(
|
|
@@ -3414,33 +3555,36 @@ async function spawnAgent(db, opts) {
|
|
|
3414
3555
|
const windowName = opts.tab ?? opts.name;
|
|
3415
3556
|
const cli = opts.cli ?? "pi";
|
|
3416
3557
|
const command = opts.command ?? resolveCliCommand(cli);
|
|
3558
|
+
if (opts.command === void 0) {
|
|
3559
|
+
const check = await checkCommandResolvable(command);
|
|
3560
|
+
if (!check.ok) {
|
|
3561
|
+
throw new AgentSpawnCliNotFoundError(cli, check.binary, envVarNameForCli(cli));
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3417
3564
|
const workspacePathStr = opts.workspace ? await prestageWorkspace(db, opts, cli) : void 0;
|
|
3418
3565
|
const paneEnv = {
|
|
3419
3566
|
MU_MANAGED_AGENT: "1",
|
|
3420
3567
|
MU_AGENT_NAME: opts.name,
|
|
3421
3568
|
MU_WORKSTREAM: opts.workstream
|
|
3422
3569
|
};
|
|
3423
|
-
const paneId = await createOrReusePane({
|
|
3424
|
-
session,
|
|
3425
|
-
windowName,
|
|
3426
|
-
command,
|
|
3427
|
-
cwd: workspacePathStr ?? opts.cwd,
|
|
3428
|
-
env: paneEnv
|
|
3429
|
-
});
|
|
3430
3570
|
const hasWorkspace = workspacePathStr !== void 0;
|
|
3571
|
+
let paneId;
|
|
3431
3572
|
let agent;
|
|
3432
3573
|
try {
|
|
3574
|
+
paneId = await createOrReusePane({
|
|
3575
|
+
session,
|
|
3576
|
+
windowName,
|
|
3577
|
+
command,
|
|
3578
|
+
cwd: workspacePathStr ?? opts.cwd,
|
|
3579
|
+
env: paneEnv
|
|
3580
|
+
});
|
|
3433
3581
|
await setPaneTitle(paneId, opts.name);
|
|
3434
3582
|
await enableMuPaneBordersForPane(paneId);
|
|
3435
3583
|
agent = finalizeAgentRow(db, { opts, cli, paneId, hasWorkspace });
|
|
3436
|
-
} catch (err) {
|
|
3437
|
-
await rollbackSpawn(db, opts.name, paneId, hasWorkspace, opts.workstream);
|
|
3438
|
-
throw err;
|
|
3439
|
-
}
|
|
3440
|
-
try {
|
|
3441
3584
|
await awaitSpawnLiveness(paneId, opts.name);
|
|
3442
3585
|
} catch (err) {
|
|
3443
3586
|
await rollbackSpawn(db, opts.name, paneId, hasWorkspace, opts.workstream);
|
|
3587
|
+
if (hasWorkspace) attachOrphanCleanupHint(err, opts.name, opts.workstream);
|
|
3444
3588
|
throw err;
|
|
3445
3589
|
}
|
|
3446
3590
|
emitEvent(
|
|
@@ -3500,7 +3644,7 @@ function finalizeAgentRow(db, args) {
|
|
|
3500
3644
|
return row;
|
|
3501
3645
|
}
|
|
3502
3646
|
async function rollbackSpawn(db, name, paneId, hasWorkspace, workstream) {
|
|
3503
|
-
await killPane(paneId).catch(() => {
|
|
3647
|
+
if (paneId !== void 0) await killPane(paneId).catch(() => {
|
|
3504
3648
|
});
|
|
3505
3649
|
if (hasWorkspace) {
|
|
3506
3650
|
await freeWorkspace(db, name, { workstream }).catch(() => {
|
|
@@ -3508,6 +3652,22 @@ async function rollbackSpawn(db, name, paneId, hasWorkspace, workstream) {
|
|
|
3508
3652
|
}
|
|
3509
3653
|
deleteAgent(db, name, workstream);
|
|
3510
3654
|
}
|
|
3655
|
+
function attachOrphanCleanupHint(err, agent, workstream) {
|
|
3656
|
+
if (typeof err !== "object" || err === null) return;
|
|
3657
|
+
const target = err;
|
|
3658
|
+
const existing = typeof target.errorNextSteps === "function" ? target.errorNextSteps.bind(target) : null;
|
|
3659
|
+
const orphanHints = [
|
|
3660
|
+
{
|
|
3661
|
+
intent: "Check for an orphan workspace dir (rollback is best-effort; may have failed)",
|
|
3662
|
+
command: `mu workspace orphans -w ${workstream}`
|
|
3663
|
+
},
|
|
3664
|
+
{
|
|
3665
|
+
intent: "Free the workspace if it survived the rollback (idempotent on missing)",
|
|
3666
|
+
command: `mu workspace free ${agent} -w ${workstream}`
|
|
3667
|
+
}
|
|
3668
|
+
];
|
|
3669
|
+
target.errorNextSteps = () => [...existing ? existing() : [], ...orphanHints];
|
|
3670
|
+
}
|
|
3511
3671
|
function defaultSpawnLivenessMs() {
|
|
3512
3672
|
const raw = process.env.MU_SPAWN_LIVENESS_MS;
|
|
3513
3673
|
if (raw === void 0) return 1500;
|
|
@@ -3515,13 +3675,51 @@ function defaultSpawnLivenessMs() {
|
|
|
3515
3675
|
if (Number.isNaN(parsed) || parsed < 0) return 1500;
|
|
3516
3676
|
return parsed;
|
|
3517
3677
|
}
|
|
3678
|
+
var STARTUP_ERROR_PATTERNS = [
|
|
3679
|
+
/No API key found for [\w-]+/i,
|
|
3680
|
+
/Error: invalid API key/i,
|
|
3681
|
+
/Authentication failed/i,
|
|
3682
|
+
/401 Unauthorized/i,
|
|
3683
|
+
/Could not authenticate/i,
|
|
3684
|
+
// fb_agent_spawn_no_validation part B: post-spawn detection of the
|
|
3685
|
+
// "binary not found at exec time" failure mode. The pre-flight check
|
|
3686
|
+
// above catches the common typo BEFORE any side effect, but a few
|
|
3687
|
+
// edge cases still slip through and only surface in the pane:
|
|
3688
|
+
// - `--command "..."` skips the pre-flight (operator opt-out).
|
|
3689
|
+
// - PATH inside the spawned shell differs from PATH in mu's
|
|
3690
|
+
// process (login shell rc files, /etc/paths.d, etc.).
|
|
3691
|
+
// - Race: binary on PATH at spawn time, gone 1.5s later.
|
|
3692
|
+
// Scoped to the FIRST 30 lines of scrollback (see
|
|
3693
|
+
// STARTUP_ERROR_TAIL_LINES) so a user's later `cat /no/such/file`
|
|
3694
|
+
// can't false-positive long after spawn.
|
|
3695
|
+
/command not found/i,
|
|
3696
|
+
/No such file or directory/i
|
|
3697
|
+
];
|
|
3698
|
+
var STARTUP_ERROR_TAIL_LINES = 30;
|
|
3699
|
+
function detectSpawnStartupError(scrollback) {
|
|
3700
|
+
const lines = scrollback.split(/\r?\n/);
|
|
3701
|
+
const tail = lines.slice(Math.max(0, lines.length - STARTUP_ERROR_TAIL_LINES));
|
|
3702
|
+
for (const line of tail) {
|
|
3703
|
+
for (const pattern of STARTUP_ERROR_PATTERNS) {
|
|
3704
|
+
if (pattern.test(line)) return line;
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
return void 0;
|
|
3708
|
+
}
|
|
3518
3709
|
async function awaitSpawnLiveness(paneId, agentName) {
|
|
3519
3710
|
const ms = defaultSpawnLivenessMs();
|
|
3520
3711
|
if (ms === 0) return;
|
|
3521
3712
|
await sleep(ms);
|
|
3522
3713
|
const scrollback = await capturePane(paneId, { lines: 50 }).catch(() => void 0);
|
|
3523
|
-
if (await paneExists(paneId))
|
|
3524
|
-
|
|
3714
|
+
if (!await paneExists(paneId)) {
|
|
3715
|
+
throw new AgentDiedOnSpawnError(agentName, paneId, scrollback);
|
|
3716
|
+
}
|
|
3717
|
+
if (scrollback !== void 0) {
|
|
3718
|
+
const matchedLine = detectSpawnStartupError(scrollback);
|
|
3719
|
+
if (matchedLine !== void 0) {
|
|
3720
|
+
throw new AgentSpawnStartupError(agentName, paneId, matchedLine, scrollback);
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3525
3723
|
}
|
|
3526
3724
|
async function createOrReusePane(opts) {
|
|
3527
3725
|
if (!await sessionExists(opts.session)) {
|
|
@@ -3552,6 +3750,36 @@ async function createOrReusePane(opts) {
|
|
|
3552
3750
|
}
|
|
3553
3751
|
|
|
3554
3752
|
// src/agents/errors.ts
|
|
3753
|
+
var AgentSpawnCliNotFoundError = class extends Error {
|
|
3754
|
+
constructor(cli, binary, envVarChecked) {
|
|
3755
|
+
super(
|
|
3756
|
+
`--cli ${cli} resolved to binary "${binary}" which is not on PATH (and not an executable absolute/relative path). Refusing to spawn \u2014 would create a pane that dies immediately on "command not found".`
|
|
3757
|
+
);
|
|
3758
|
+
this.cli = cli;
|
|
3759
|
+
this.binary = binary;
|
|
3760
|
+
this.envVarChecked = envVarChecked;
|
|
3761
|
+
}
|
|
3762
|
+
cli;
|
|
3763
|
+
binary;
|
|
3764
|
+
envVarChecked;
|
|
3765
|
+
name = "AgentSpawnCliNotFoundError";
|
|
3766
|
+
errorNextSteps() {
|
|
3767
|
+
return [
|
|
3768
|
+
{
|
|
3769
|
+
intent: "Try the default CLI (the one mu's substrate ships against)",
|
|
3770
|
+
command: "mu agent spawn <name> --cli pi"
|
|
3771
|
+
},
|
|
3772
|
+
{
|
|
3773
|
+
intent: "If you meant a custom alias, set the env var to its real path",
|
|
3774
|
+
command: `export ${this.envVarChecked}="<absolute-path-to-binary> [args...]"`
|
|
3775
|
+
},
|
|
3776
|
+
{
|
|
3777
|
+
intent: "List installed CLIs typically supported by mu",
|
|
3778
|
+
command: "which pi pi-meta claude codex"
|
|
3779
|
+
}
|
|
3780
|
+
];
|
|
3781
|
+
}
|
|
3782
|
+
};
|
|
3555
3783
|
var AgentExistsError = class extends Error {
|
|
3556
3784
|
constructor(agentName) {
|
|
3557
3785
|
super(`agent already exists in this workstream: ${agentName}`);
|
|
@@ -3672,6 +3900,51 @@ ${tail}
|
|
|
3672
3900
|
];
|
|
3673
3901
|
}
|
|
3674
3902
|
};
|
|
3903
|
+
var AgentSpawnStartupError = class extends Error {
|
|
3904
|
+
constructor(agentName, paneId, matchedLine, scrollback) {
|
|
3905
|
+
super(
|
|
3906
|
+
`agent ${agentName} reported a startup error within ${defaultSpawnLivenessMs()}ms of spawn (pane ${paneId}). The pane is alive but the spawned CLI parked at an error prompt instead of becoming a working agent.
|
|
3907
|
+
|
|
3908
|
+
Matched line: ${matchedLine.trim()}
|
|
3909
|
+
|
|
3910
|
+
--- pane scrollback ---
|
|
3911
|
+
${scrollback.trim()}
|
|
3912
|
+
--- end scrollback ---`
|
|
3913
|
+
);
|
|
3914
|
+
this.agentName = agentName;
|
|
3915
|
+
this.paneId = paneId;
|
|
3916
|
+
this.matchedLine = matchedLine;
|
|
3917
|
+
this.scrollback = scrollback;
|
|
3918
|
+
}
|
|
3919
|
+
agentName;
|
|
3920
|
+
paneId;
|
|
3921
|
+
matchedLine;
|
|
3922
|
+
scrollback;
|
|
3923
|
+
name = "AgentSpawnStartupError";
|
|
3924
|
+
errorNextSteps() {
|
|
3925
|
+
return [
|
|
3926
|
+
{
|
|
3927
|
+
intent: "Inspect the parked pane's scrollback for the full error",
|
|
3928
|
+
command: `mu agent read ${this.agentName} -n 100`
|
|
3929
|
+
},
|
|
3930
|
+
{
|
|
3931
|
+
// Most common today: the operator picked a model whose
|
|
3932
|
+
// provider has no credentials in this env. Default Anthropic
|
|
3933
|
+
// is the safe fallback for pi-meta.
|
|
3934
|
+
intent: "Re-spawn with a CLI command whose provider credentials are present",
|
|
3935
|
+
command: `mu agent spawn ${this.agentName} --command "pi-meta --no-solo" # default Anthropic`
|
|
3936
|
+
},
|
|
3937
|
+
{
|
|
3938
|
+
intent: "Or set the missing API key env var for the provider you wanted, then re-spawn",
|
|
3939
|
+
command: "export ANTHROPIC_API_KEY=... # or AWS_BEARER_TOKEN_BEDROCK, OPENAI_API_KEY, ..."
|
|
3940
|
+
},
|
|
3941
|
+
{
|
|
3942
|
+
intent: "Disable the startup-error scan if you actually wanted that prompt (CI / scripted recovery)",
|
|
3943
|
+
command: "export MU_SPAWN_LIVENESS_MS=0"
|
|
3944
|
+
}
|
|
3945
|
+
];
|
|
3946
|
+
}
|
|
3947
|
+
};
|
|
3675
3948
|
var WorkspacePreservedError = class extends Error {
|
|
3676
3949
|
constructor(agentName, workspacePath2) {
|
|
3677
3950
|
super(
|
|
@@ -3928,11 +4201,13 @@ async function createWorkspace(db, opts) {
|
|
|
3928
4201
|
});
|
|
3929
4202
|
throw err;
|
|
3930
4203
|
}
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
4204
|
+
if (opts._suppressEvent !== true) {
|
|
4205
|
+
emitEvent(
|
|
4206
|
+
db,
|
|
4207
|
+
opts.workstream,
|
|
4208
|
+
`workspace create ${opts.agent} (backend=${backend.name}, path=${path}${created.parentRef ? `, parent=${created.parentRef.slice(0, 12)}` : ""})`
|
|
4209
|
+
);
|
|
4210
|
+
}
|
|
3936
4211
|
return {
|
|
3937
4212
|
agentName: opts.agent,
|
|
3938
4213
|
workstreamName: opts.workstream,
|
|
@@ -4024,10 +4299,30 @@ async function listCommitsForWorkspace(db, agent, opts) {
|
|
|
4024
4299
|
const commits = await backend.commitsSinceBase(row.path, baseRef);
|
|
4025
4300
|
return { vcs: row.backend, baseRef, commits, workspacePath: row.path };
|
|
4026
4301
|
}
|
|
4302
|
+
async function isWorkspaceClean(row) {
|
|
4303
|
+
const backend = backendByName(row.backend);
|
|
4304
|
+
let clean;
|
|
4305
|
+
try {
|
|
4306
|
+
clean = await backend.isClean(row.path);
|
|
4307
|
+
} catch {
|
|
4308
|
+
return false;
|
|
4309
|
+
}
|
|
4310
|
+
if (!clean) return false;
|
|
4311
|
+
if (row.backend === "none") return true;
|
|
4312
|
+
if (row.parentRef === null || row.parentRef.length === 0) return false;
|
|
4313
|
+
try {
|
|
4314
|
+
const commits = await backend.commitsSinceBase(row.path, row.parentRef);
|
|
4315
|
+
return commits.length === 0;
|
|
4316
|
+
} catch {
|
|
4317
|
+
return false;
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4027
4320
|
async function freeWorkspace(db, agent, opts) {
|
|
4028
4321
|
const row = getWorkspaceForAgent(db, agent, opts.workstream);
|
|
4029
4322
|
if (!row) return { removed: false, rowDeleted: false };
|
|
4030
|
-
|
|
4323
|
+
if (opts._suppressEvent !== true) {
|
|
4324
|
+
captureSnapshot(db, `workspace free ${agent}`, row.workstreamName);
|
|
4325
|
+
}
|
|
4031
4326
|
const backend = backendByName(row.backend);
|
|
4032
4327
|
const result = await backend.freeWorkspace({
|
|
4033
4328
|
workspacePath: row.path,
|
|
@@ -4039,17 +4334,55 @@ async function freeWorkspace(db, agent, opts) {
|
|
|
4039
4334
|
WHERE agent_id = (SELECT id FROM agents WHERE name = ? AND workstream_id = ?)
|
|
4040
4335
|
AND workstream_id = ?`
|
|
4041
4336
|
).run(agent, wsIdForDel, wsIdForDel);
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4337
|
+
if (opts._suppressEvent !== true) {
|
|
4338
|
+
emitEvent(
|
|
4339
|
+
db,
|
|
4340
|
+
row.workstreamName,
|
|
4341
|
+
`workspace free ${agent} (backend=${row.backend}, path=${row.path}${result.committedRef ? `, committed=${result.committedRef.slice(0, 12)}` : ""})`
|
|
4342
|
+
);
|
|
4343
|
+
}
|
|
4047
4344
|
return {
|
|
4048
4345
|
removed: result.removed,
|
|
4049
4346
|
rowDeleted: del.changes > 0,
|
|
4050
4347
|
...result.committedRef !== void 0 ? { committedRef: result.committedRef } : {}
|
|
4051
4348
|
};
|
|
4052
4349
|
}
|
|
4350
|
+
async function recreateWorkspace(db, agent, opts) {
|
|
4351
|
+
const row = getWorkspaceForAgent(db, agent, opts.workstream);
|
|
4352
|
+
if (!row) throw new WorkspaceNotFoundError(agent);
|
|
4353
|
+
if (opts.force !== true) {
|
|
4354
|
+
const oldBackend = backendByName(row.backend);
|
|
4355
|
+
const dirty = await oldBackend.listDirtyFiles(row.path);
|
|
4356
|
+
if (dirty.length > 0) {
|
|
4357
|
+
throw new WorkspaceDirtyError(row.path, dirty, "recreate");
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
captureSnapshot(db, `workspace recreate ${agent}`, row.workstreamName);
|
|
4361
|
+
await freeWorkspace(db, agent, {
|
|
4362
|
+
workstream: opts.workstream,
|
|
4363
|
+
commit: false,
|
|
4364
|
+
_suppressEvent: true
|
|
4365
|
+
});
|
|
4366
|
+
const createOpts = {
|
|
4367
|
+
agent,
|
|
4368
|
+
workstream: opts.workstream,
|
|
4369
|
+
_suppressEvent: true
|
|
4370
|
+
};
|
|
4371
|
+
if (opts.projectRoot !== void 0) createOpts.projectRoot = opts.projectRoot;
|
|
4372
|
+
if (opts.backend !== void 0) {
|
|
4373
|
+
createOpts.backend = opts.backend;
|
|
4374
|
+
} else {
|
|
4375
|
+
createOpts.backend = row.backend;
|
|
4376
|
+
}
|
|
4377
|
+
if (opts.parentRef !== void 0) createOpts.parentRef = opts.parentRef;
|
|
4378
|
+
const fresh = await createWorkspace(db, createOpts);
|
|
4379
|
+
emitEvent(
|
|
4380
|
+
db,
|
|
4381
|
+
opts.workstream,
|
|
4382
|
+
`workspace recreate ${agent} (backend=${fresh.backend}, path=${fresh.path}, old_parent=${row.parentRef ? row.parentRef.slice(0, 12) : "\u2014"}, new_parent=${fresh.parentRef ? fresh.parentRef.slice(0, 12) : "\u2014"})`
|
|
4383
|
+
);
|
|
4384
|
+
return { workspace: fresh, previousParentRef: row.parentRef };
|
|
4385
|
+
}
|
|
4053
4386
|
|
|
4054
4387
|
// src/workstream.ts
|
|
4055
4388
|
var WORKSTREAM_NAME_RE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
@@ -4331,7 +4664,6 @@ async function waitForTasks(db, input, opts) {
|
|
|
4331
4664
|
const stuckAfterMs = opts.stuckAfterMs ?? 3e5;
|
|
4332
4665
|
const onStall = opts.onStall ?? "warn";
|
|
4333
4666
|
const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : Number.POSITIVE_INFINITY;
|
|
4334
|
-
const startedAt = Date.now();
|
|
4335
4667
|
for (const ref of refs) {
|
|
4336
4668
|
if (getTask(db, ref.name, ref.workstreamName) === void 0)
|
|
4337
4669
|
throw new TaskNotFoundError(`${ref.workstreamName}/${ref.name}`);
|
|
@@ -4352,7 +4684,7 @@ async function waitForTasks(db, input, opts) {
|
|
|
4352
4684
|
return ageMs >= stuckAfterMs;
|
|
4353
4685
|
};
|
|
4354
4686
|
const snapshot = () => {
|
|
4355
|
-
const
|
|
4687
|
+
const refStates = refs.map((ref) => {
|
|
4356
4688
|
const row = getTask(db, ref.name, ref.workstreamName);
|
|
4357
4689
|
const status = row?.status ?? "OPEN";
|
|
4358
4690
|
const owner = row?.ownerName ?? null;
|
|
@@ -4384,16 +4716,15 @@ async function waitForTasks(db, input, opts) {
|
|
|
4384
4716
|
stuck
|
|
4385
4717
|
};
|
|
4386
4718
|
});
|
|
4387
|
-
const reachedCount = tasks.filter((t) => t.reachedTarget).length;
|
|
4388
4719
|
return {
|
|
4389
|
-
|
|
4390
|
-
allReached: reachedCount === tasks.length,
|
|
4391
|
-
anyReached: reachedCount > 0,
|
|
4392
|
-
elapsedMs: Date.now() - startedAt,
|
|
4720
|
+
refs: refStates,
|
|
4393
4721
|
timedOut: false
|
|
4394
4722
|
};
|
|
4395
4723
|
};
|
|
4396
|
-
const isDone = (snap2) =>
|
|
4724
|
+
const isDone = (snap2) => {
|
|
4725
|
+
const reached = snap2.refs.filter((r) => r.reachedTarget).length;
|
|
4726
|
+
return wantAny ? reached > 0 : reached === snap2.refs.length;
|
|
4727
|
+
};
|
|
4397
4728
|
if (opts.beforePoll) await opts.beforePoll();
|
|
4398
4729
|
let snap = snapshot();
|
|
4399
4730
|
if (isDone(snap)) return snap;
|
|
@@ -4454,7 +4785,15 @@ function closeTask(db, localId, opts) {
|
|
|
4454
4785
|
if (before && before.status !== "CLOSED") {
|
|
4455
4786
|
captureSnapshot(db, `task close ${localId}`, before.workstreamName);
|
|
4456
4787
|
}
|
|
4457
|
-
|
|
4788
|
+
const r = setTaskStatus(db, localId, "CLOSED", opts);
|
|
4789
|
+
if (r.changed && before && opts.evidence !== void 0 && opts.evidence !== "") {
|
|
4790
|
+
const noteOpts = {
|
|
4791
|
+
workstream: before.workstreamName
|
|
4792
|
+
};
|
|
4793
|
+
if (opts.author !== void 0 && opts.author !== "") noteOpts.author = opts.author;
|
|
4794
|
+
addNote(db, localId, `CLOSE: ${opts.evidence}`, noteOpts);
|
|
4795
|
+
}
|
|
4796
|
+
return r;
|
|
4458
4797
|
}
|
|
4459
4798
|
function openTask(db, localId, opts) {
|
|
4460
4799
|
return setTaskStatus(db, localId, "OPEN", opts);
|
|
@@ -4707,7 +5046,6 @@ var SELECT_NOTE_COLS = `
|
|
|
4707
5046
|
function rowFromDb4(row) {
|
|
4708
5047
|
return {
|
|
4709
5048
|
name: row.local_id,
|
|
4710
|
-
localId: row.local_id,
|
|
4711
5049
|
workstreamName: row.workstream,
|
|
4712
5050
|
title: row.title,
|
|
4713
5051
|
status: row.status,
|
|
@@ -4762,19 +5100,23 @@ function slugifyTitleVerbose(title) {
|
|
|
4762
5100
|
const lastSep = window.lastIndexOf("_");
|
|
4763
5101
|
trimmed = lastSep > 0 ? window.slice(0, lastSep) : window;
|
|
4764
5102
|
}
|
|
4765
|
-
const
|
|
5103
|
+
const applyPrefix = (s) => /^[a-z]/.test(s) ? s.slice(0, SLUG_HARD_CAP) : `t_${s}`.slice(0, SLUG_HARD_CAP);
|
|
5104
|
+
const slug = applyPrefix(trimmed);
|
|
5105
|
+
const originalSlug = applyPrefix(stripped);
|
|
4766
5106
|
return {
|
|
4767
5107
|
slug,
|
|
4768
5108
|
strippedLength: stripped.length,
|
|
5109
|
+
originalSlug,
|
|
4769
5110
|
truncated: trimmed.length < stripped.length
|
|
4770
5111
|
};
|
|
4771
5112
|
}
|
|
4772
5113
|
function idFromTitleVerbose(db, workstream, title) {
|
|
4773
|
-
const { slug: base, truncated } = slugifyTitleVerbose(title);
|
|
4774
|
-
if (getTask(db, base, workstream) === void 0) return { id: base, truncated };
|
|
5114
|
+
const { slug: base, truncated, originalSlug } = slugifyTitleVerbose(title);
|
|
5115
|
+
if (getTask(db, base, workstream) === void 0) return { id: base, truncated, originalSlug };
|
|
4775
5116
|
for (let i = 2; i < 1e3; i++) {
|
|
4776
5117
|
const candidate = `${base}_${i}`.slice(0, SLUG_HARD_CAP);
|
|
4777
|
-
if (getTask(db, candidate, workstream) === void 0)
|
|
5118
|
+
if (getTask(db, candidate, workstream) === void 0)
|
|
5119
|
+
return { id: candidate, truncated, originalSlug };
|
|
4778
5120
|
}
|
|
4779
5121
|
throw new Error(`could not derive a unique id from title in workstream ${workstream}: ${title}`);
|
|
4780
5122
|
}
|
|
@@ -4872,14 +5214,26 @@ function listRecentClosed(db, workstream, limit = 5) {
|
|
|
4872
5214
|
).all(wsId, limit);
|
|
4873
5215
|
return rows.map(rowFromDb4);
|
|
4874
5216
|
}
|
|
4875
|
-
function listNotes(db, taskLocalId, workstream) {
|
|
5217
|
+
function listNotes(db, taskLocalId, workstream, opts = {}) {
|
|
4876
5218
|
const taskId = taskIdFor(db, taskLocalId, workstream);
|
|
4877
5219
|
if (taskId === null) return [];
|
|
4878
|
-
|
|
5220
|
+
let cutoff = opts.since;
|
|
5221
|
+
if (cutoff === void 0 && opts.sinceClaim === true) {
|
|
5222
|
+
const at = lastClaimEventAt(db, workstream, taskLocalId);
|
|
5223
|
+
if (at !== null) cutoff = at;
|
|
5224
|
+
}
|
|
5225
|
+
const rows = cutoff !== void 0 ? db.prepare(
|
|
5226
|
+
`SELECT ${SELECT_NOTE_COLS} FROM task_notes n JOIN tasks t ON t.id = n.task_id
|
|
5227
|
+
WHERE n.task_id = ? AND n.created_at > ? ORDER BY n.id`
|
|
5228
|
+
).all(taskId, cutoff) : db.prepare(
|
|
4879
5229
|
`SELECT ${SELECT_NOTE_COLS} FROM task_notes n JOIN tasks t ON t.id = n.task_id
|
|
4880
|
-
|
|
5230
|
+
WHERE n.task_id = ? ORDER BY n.id`
|
|
4881
5231
|
).all(taskId);
|
|
4882
|
-
|
|
5232
|
+
const mapped = rows.map(noteFromDb);
|
|
5233
|
+
if (opts.tail !== void 0 && opts.tail >= 0) {
|
|
5234
|
+
return opts.tail === 0 ? [] : mapped.slice(-opts.tail);
|
|
5235
|
+
}
|
|
5236
|
+
return mapped;
|
|
4883
5237
|
}
|
|
4884
5238
|
function listTasksByOwner(db, workstream, owner, opts = {}) {
|
|
4885
5239
|
const filter = opts.includeClosed ? "" : "AND t.status NOT IN ('CLOSED', 'REJECTED', 'DEFERRED')";
|
|
@@ -5287,6 +5641,140 @@ async function adoptAgent(db, opts) {
|
|
|
5287
5641
|
};
|
|
5288
5642
|
}
|
|
5289
5643
|
|
|
5644
|
+
// src/agents/kick.ts
|
|
5645
|
+
var ALLOWED_SIGNALS = ["SIGINT", "SIGTERM", "SIGKILL"];
|
|
5646
|
+
function isKickSignal(s) {
|
|
5647
|
+
return ALLOWED_SIGNALS.includes(s);
|
|
5648
|
+
}
|
|
5649
|
+
var NoForegroundProcessError = class extends Error {
|
|
5650
|
+
constructor(agentName, tty, reason) {
|
|
5651
|
+
const detail = reason === "no-foreground" ? `no foreground process group on tty ${tty} (pane is idle)` : `the only foreground process on tty ${tty} is the agent's wrapping CLI itself; refusing to signal it (use \`mu agent close ${agentName}\` to close the agent)`;
|
|
5652
|
+
super(`agent ${agentName}: ${detail}`);
|
|
5653
|
+
this.agentName = agentName;
|
|
5654
|
+
this.tty = tty;
|
|
5655
|
+
this.reason = reason;
|
|
5656
|
+
}
|
|
5657
|
+
agentName;
|
|
5658
|
+
tty;
|
|
5659
|
+
reason;
|
|
5660
|
+
name = "NoForegroundProcessError";
|
|
5661
|
+
errorNextSteps() {
|
|
5662
|
+
return [
|
|
5663
|
+
{
|
|
5664
|
+
intent: "Inspect what's running in the pane",
|
|
5665
|
+
command: `mu agent show ${this.agentName} -n 50`
|
|
5666
|
+
},
|
|
5667
|
+
{
|
|
5668
|
+
intent: "Close the agent (kills the wrapping CLI + pane)",
|
|
5669
|
+
command: `mu agent close ${this.agentName}`
|
|
5670
|
+
}
|
|
5671
|
+
];
|
|
5672
|
+
}
|
|
5673
|
+
};
|
|
5674
|
+
var realExecutor2 = async (cmd, args) => {
|
|
5675
|
+
const { execa: execa2 } = await import("execa");
|
|
5676
|
+
const result = await execa2(cmd, [...args], { reject: false });
|
|
5677
|
+
return {
|
|
5678
|
+
stdout: result.stdout ?? "",
|
|
5679
|
+
stderr: result.stderr ?? "",
|
|
5680
|
+
exitCode: result.exitCode ?? null
|
|
5681
|
+
};
|
|
5682
|
+
};
|
|
5683
|
+
var currentExecutor2 = realExecutor2;
|
|
5684
|
+
function parsePsTtyOutput(output) {
|
|
5685
|
+
const rows = [];
|
|
5686
|
+
for (const raw of output.split("\n")) {
|
|
5687
|
+
const line = raw.trim();
|
|
5688
|
+
if (line === "") continue;
|
|
5689
|
+
const parts = line.split(/\s+/);
|
|
5690
|
+
if (parts.length < 4) continue;
|
|
5691
|
+
const [pidStr, pgidStr, stat2, ...commParts] = parts;
|
|
5692
|
+
if (pidStr === void 0 || pgidStr === void 0 || stat2 === void 0) continue;
|
|
5693
|
+
const pid = Number.parseInt(pidStr, 10);
|
|
5694
|
+
const pgid = Number.parseInt(pgidStr, 10);
|
|
5695
|
+
if (!Number.isFinite(pid) || !Number.isFinite(pgid)) continue;
|
|
5696
|
+
rows.push({ pid, pgid, stat: stat2, comm: commParts.join(" ") });
|
|
5697
|
+
}
|
|
5698
|
+
return rows;
|
|
5699
|
+
}
|
|
5700
|
+
var WRAPPER_COMM_PREFIXES = [
|
|
5701
|
+
"pi",
|
|
5702
|
+
"claude",
|
|
5703
|
+
"codex",
|
|
5704
|
+
"bash",
|
|
5705
|
+
"zsh",
|
|
5706
|
+
"sh",
|
|
5707
|
+
"fish",
|
|
5708
|
+
"dash"
|
|
5709
|
+
];
|
|
5710
|
+
function isWrapperComm(comm) {
|
|
5711
|
+
const cleaned = comm.replace(/^-/, "").trim();
|
|
5712
|
+
if (cleaned === "") return false;
|
|
5713
|
+
for (const prefix of WRAPPER_COMM_PREFIXES) {
|
|
5714
|
+
if (cleaned === prefix) return true;
|
|
5715
|
+
if (cleaned.startsWith(`${prefix}-`)) return true;
|
|
5716
|
+
}
|
|
5717
|
+
return false;
|
|
5718
|
+
}
|
|
5719
|
+
async function foregroundPgid(tty) {
|
|
5720
|
+
const ttyShort = tty.startsWith("/dev/") ? tty.slice("/dev/".length) : tty;
|
|
5721
|
+
const result = await currentExecutor2("ps", ["-t", ttyShort, "-o", "pid=,pgid=,stat=,comm="]);
|
|
5722
|
+
if (result.exitCode !== 0 && result.stdout.trim() === "") {
|
|
5723
|
+
return { kind: "no-foreground", rows: [] };
|
|
5724
|
+
}
|
|
5725
|
+
const rows = parsePsTtyOutput(result.stdout);
|
|
5726
|
+
if (rows.length === 0) return { kind: "no-foreground", rows };
|
|
5727
|
+
const fg = rows.find((r) => r.stat.includes("+"));
|
|
5728
|
+
if (!fg) {
|
|
5729
|
+
return { kind: "no-foreground", rows };
|
|
5730
|
+
}
|
|
5731
|
+
if (isWrapperComm(fg.comm)) {
|
|
5732
|
+
return { kind: "shell-only", pgid: fg.pgid, fgRow: fg, rows };
|
|
5733
|
+
}
|
|
5734
|
+
return { kind: "ok", pgid: fg.pgid, fgRow: fg, rows };
|
|
5735
|
+
}
|
|
5736
|
+
async function killPgrp(pgid, signal) {
|
|
5737
|
+
const result = await currentExecutor2("kill", [`-${signal}`, `-${pgid}`]);
|
|
5738
|
+
if (result.exitCode !== 0) {
|
|
5739
|
+
if (/no such process/i.test(result.stderr)) return;
|
|
5740
|
+
throw new Error(
|
|
5741
|
+
`kill -${signal} -${pgid} failed (exit ${result.exitCode}): ${result.stderr.trim() || "no stderr"}`
|
|
5742
|
+
);
|
|
5743
|
+
}
|
|
5744
|
+
}
|
|
5745
|
+
async function kickAgent(db, name, opts) {
|
|
5746
|
+
const signal = opts.signal ?? "SIGINT";
|
|
5747
|
+
const agent = getAgent(db, name, opts.workstream);
|
|
5748
|
+
if (!agent) throw new AgentNotFoundError(name, opts.workstream);
|
|
5749
|
+
const tty = await paneTTY(agent.paneId);
|
|
5750
|
+
const lookup = await foregroundPgid(tty);
|
|
5751
|
+
if (lookup.kind === "no-foreground") {
|
|
5752
|
+
throw new NoForegroundProcessError(name, tty, "no-foreground");
|
|
5753
|
+
}
|
|
5754
|
+
if (lookup.kind === "shell-only") {
|
|
5755
|
+
throw new NoForegroundProcessError(name, tty, "shell-only");
|
|
5756
|
+
}
|
|
5757
|
+
const pgid = lookup.pgid;
|
|
5758
|
+
const fgRow = lookup.fgRow;
|
|
5759
|
+
if (pgid === void 0 || fgRow === void 0) {
|
|
5760
|
+
throw new NoForegroundProcessError(name, tty, "no-foreground");
|
|
5761
|
+
}
|
|
5762
|
+
await killPgrp(pgid, signal);
|
|
5763
|
+
emitEvent(
|
|
5764
|
+
db,
|
|
5765
|
+
agent.workstreamName,
|
|
5766
|
+
`agent kick ${name} (signal=${signal}, pgid=${pgid}, comm=${fgRow.comm})`
|
|
5767
|
+
);
|
|
5768
|
+
return {
|
|
5769
|
+
agentName: name,
|
|
5770
|
+
paneId: agent.paneId,
|
|
5771
|
+
tty,
|
|
5772
|
+
signaledPgid: pgid,
|
|
5773
|
+
signal,
|
|
5774
|
+
foregroundComm: fgRow.comm
|
|
5775
|
+
};
|
|
5776
|
+
}
|
|
5777
|
+
|
|
5290
5778
|
// src/agents.ts
|
|
5291
5779
|
var DEFAULT_IDLE_THRESHOLD_MS = 3e5;
|
|
5292
5780
|
function idleThresholdMs() {
|
|
@@ -5511,15 +5999,24 @@ function freeAgent(db, name, workstream) {
|
|
|
5511
5999
|
async function closeAgent(db, name, opts) {
|
|
5512
6000
|
const agent = getAgent(db, name, opts.workstream);
|
|
5513
6001
|
if (!agent) {
|
|
5514
|
-
return {
|
|
6002
|
+
return {
|
|
6003
|
+
killedPane: false,
|
|
6004
|
+
deletedRow: false,
|
|
6005
|
+
workspaceFreed: false,
|
|
6006
|
+
workspaceAutoFreedClean: false
|
|
6007
|
+
};
|
|
5515
6008
|
}
|
|
5516
6009
|
const ws = getWorkspaceForAgent(db, name, agent.workstreamName);
|
|
6010
|
+
let autoFreeClean = false;
|
|
5517
6011
|
if (ws !== void 0 && opts.discardWorkspace !== true) {
|
|
5518
|
-
|
|
6012
|
+
autoFreeClean = await isWorkspaceClean(ws);
|
|
6013
|
+
if (!autoFreeClean) {
|
|
6014
|
+
throw new WorkspacePreservedError(name, ws.path);
|
|
6015
|
+
}
|
|
5519
6016
|
}
|
|
5520
6017
|
captureSnapshot(db, `agent close ${name}`, agent.workstreamName);
|
|
5521
6018
|
let workspaceFreed = false;
|
|
5522
|
-
if (ws !== void 0 && opts.discardWorkspace === true) {
|
|
6019
|
+
if (ws !== void 0 && (opts.discardWorkspace === true || autoFreeClean)) {
|
|
5523
6020
|
await freeWorkspace(db, name, { commit: false, workstream: agent.workstreamName });
|
|
5524
6021
|
workspaceFreed = true;
|
|
5525
6022
|
}
|
|
@@ -5529,12 +6026,13 @@ async function closeAgent(db, name, opts) {
|
|
|
5529
6026
|
emitEvent(
|
|
5530
6027
|
db,
|
|
5531
6028
|
agent.workstreamName,
|
|
5532
|
-
`agent close ${name} (pane=${agent.paneId}${workspaceFreed ? ", workspace discarded" : ""})`
|
|
6029
|
+
`agent close ${name} (pane=${agent.paneId}${workspaceFreed ? autoFreeClean ? ", workspace auto-freed (clean)" : ", workspace discarded" : ""})`
|
|
5533
6030
|
);
|
|
5534
6031
|
return {
|
|
5535
6032
|
killedPane: true,
|
|
5536
6033
|
deletedRow,
|
|
5537
|
-
workspaceFreed
|
|
6034
|
+
workspaceFreed,
|
|
6035
|
+
workspaceAutoFreedClean: workspaceFreed && autoFreeClean
|
|
5538
6036
|
};
|
|
5539
6037
|
}
|
|
5540
6038
|
async function listLiveAgents(db, opts) {
|
|
@@ -5682,9 +6180,10 @@ function formatEdgeList(edges, dim) {
|
|
|
5682
6180
|
}
|
|
5683
6181
|
async function cmdTaskAdd(db, localId, opts) {
|
|
5684
6182
|
const workstream = await resolveWorkstream(opts.workstream);
|
|
6183
|
+
const autoDerived = localId === void 0;
|
|
5685
6184
|
let derivation;
|
|
5686
6185
|
if (localId !== void 0) {
|
|
5687
|
-
derivation = { id: localId, truncated: false };
|
|
6186
|
+
derivation = { id: localId, truncated: false, originalSlug: localId };
|
|
5688
6187
|
} else {
|
|
5689
6188
|
derivation = idFromTitleVerbose(db, workstream, opts.title);
|
|
5690
6189
|
}
|
|
@@ -5718,7 +6217,13 @@ async function cmdTaskAdd(db, localId, opts) {
|
|
|
5718
6217
|
}
|
|
5719
6218
|
];
|
|
5720
6219
|
if (opts.json) {
|
|
5721
|
-
|
|
6220
|
+
const truncationFields = autoDerived && derivation.truncated ? { truncated: true, originalSlug: derivation.originalSlug } : {};
|
|
6221
|
+
emitJson({
|
|
6222
|
+
task: withRoiAll([task])[0],
|
|
6223
|
+
blockers: blockedBy ?? [],
|
|
6224
|
+
nextSteps,
|
|
6225
|
+
...truncationFields
|
|
6226
|
+
});
|
|
5722
6227
|
return;
|
|
5723
6228
|
}
|
|
5724
6229
|
if (derivation.truncated) {
|
|
@@ -5807,12 +6312,32 @@ async function cmdTaskShow(db, rawId, opts = {}) {
|
|
|
5807
6312
|
}
|
|
5808
6313
|
}
|
|
5809
6314
|
async function cmdTaskNotes(db, rawId, opts = {}) {
|
|
6315
|
+
if (opts.since !== void 0 && opts.sinceClaim === true) {
|
|
6316
|
+
throw new UsageError(
|
|
6317
|
+
"--since and --since-claim are mutually exclusive (both define a cutoff); pick one"
|
|
6318
|
+
);
|
|
6319
|
+
}
|
|
6320
|
+
if (opts.tail !== void 0 && (!Number.isFinite(opts.tail) || opts.tail <= 0 || !Number.isInteger(opts.tail))) {
|
|
6321
|
+
throw new UsageError(`--tail must be a positive integer (got ${JSON.stringify(opts.tail)})`);
|
|
6322
|
+
}
|
|
6323
|
+
if (opts.since !== void 0) {
|
|
6324
|
+
const parsed = Date.parse(opts.since);
|
|
6325
|
+
if (Number.isNaN(parsed)) {
|
|
6326
|
+
throw new UsageError(
|
|
6327
|
+
`--since must be an ISO 8601 timestamp (got ${JSON.stringify(opts.since)})`
|
|
6328
|
+
);
|
|
6329
|
+
}
|
|
6330
|
+
}
|
|
5810
6331
|
const { name: localId } = await resolveEntityRef(db, rawId, opts, "task");
|
|
5811
6332
|
assertTaskInWorkstream(db, localId, opts.workstream);
|
|
5812
6333
|
const ws = await resolveWorkstream(opts.workstream);
|
|
5813
6334
|
const task = getTask(db, localId, ws);
|
|
5814
6335
|
if (!task) throw new TaskNotFoundError(localId);
|
|
5815
|
-
const
|
|
6336
|
+
const filterOpts = {};
|
|
6337
|
+
if (opts.tail !== void 0) filterOpts.tail = opts.tail;
|
|
6338
|
+
if (opts.since !== void 0) filterOpts.since = opts.since;
|
|
6339
|
+
if (opts.sinceClaim === true) filterOpts.sinceClaim = true;
|
|
6340
|
+
const notes = listNotes(db, localId, task.workstreamName, filterOpts);
|
|
5816
6341
|
if (opts.json) {
|
|
5817
6342
|
emitJsonCollection(notes);
|
|
5818
6343
|
return;
|
|
@@ -6015,10 +6540,12 @@ async function cmdTaskWait(db, ids, opts) {
|
|
|
6015
6540
|
priorState.set(key, { status, owner });
|
|
6016
6541
|
}
|
|
6017
6542
|
};
|
|
6543
|
+
const startedAt = Date.now();
|
|
6018
6544
|
const result = await waitForTasks(db, refs, sdkOpts);
|
|
6019
|
-
const
|
|
6020
|
-
const
|
|
6021
|
-
const
|
|
6545
|
+
const elapsedMs = Date.now() - startedAt;
|
|
6546
|
+
const firingRef = wantFirstShape && !result.timedOut ? result.refs.find((t) => t.reachedTarget) ?? null : null;
|
|
6547
|
+
const reachedRefs = result.refs.filter((t) => t.reachedTarget);
|
|
6548
|
+
const unmetRefs = result.refs.filter((t) => !t.reachedTarget);
|
|
6022
6549
|
const nextSteps = [];
|
|
6023
6550
|
if (!result.timedOut && firingRef !== null) {
|
|
6024
6551
|
const owner = firingRef.owner;
|
|
@@ -6030,7 +6557,7 @@ async function cmdTaskWait(db, ids, opts) {
|
|
|
6030
6557
|
}
|
|
6031
6558
|
nextSteps.push({
|
|
6032
6559
|
intent: "Verify the cherry-pick",
|
|
6033
|
-
command: "
|
|
6560
|
+
command: "<your project verify command \u2014 e.g. npm run test, cargo test, uv run pytest>"
|
|
6034
6561
|
});
|
|
6035
6562
|
if (owner !== null) {
|
|
6036
6563
|
nextSteps.push({
|
|
@@ -6041,7 +6568,7 @@ async function cmdTaskWait(db, ids, opts) {
|
|
|
6041
6568
|
} else if (!result.timedOut && wantFirstShape === false) {
|
|
6042
6569
|
nextSteps.push({
|
|
6043
6570
|
intent: "Verify the merged work",
|
|
6044
|
-
command: "
|
|
6571
|
+
command: "<your project verify command \u2014 e.g. npm run test, cargo test, uv run pytest>"
|
|
6045
6572
|
});
|
|
6046
6573
|
}
|
|
6047
6574
|
for (const t of unmetRefs) {
|
|
@@ -6068,7 +6595,6 @@ async function cmdTaskWait(db, ids, opts) {
|
|
|
6068
6595
|
};
|
|
6069
6596
|
const timedOutArray = result.timedOut ? unmetRefs.map((t) => ({ ...t, qualifiedId: qualifiedId(t) })) : [];
|
|
6070
6597
|
emitJson({
|
|
6071
|
-
...result,
|
|
6072
6598
|
firing: firingJson,
|
|
6073
6599
|
all: reachedRefs.map((t) => ({
|
|
6074
6600
|
...t,
|
|
@@ -6085,11 +6611,11 @@ async function cmdTaskWait(db, ids, opts) {
|
|
|
6085
6611
|
if (firingRef !== null) {
|
|
6086
6612
|
console.log(qualifiedId(firingRef));
|
|
6087
6613
|
}
|
|
6088
|
-
const summary = result.timedOut ? pc.yellow(`Timed out after ${
|
|
6089
|
-
`${wantAny ? "any-of" : "all-of"} ${refs.length} reached ${targetStatus} in ${
|
|
6614
|
+
const summary = result.timedOut ? pc.yellow(`Timed out after ${elapsedMs}ms`) : pc.green(
|
|
6615
|
+
`${wantAny ? "any-of" : "all-of"} ${refs.length} reached ${targetStatus} in ${elapsedMs}ms`
|
|
6090
6616
|
);
|
|
6091
6617
|
console.log(summary);
|
|
6092
|
-
for (const t of result.
|
|
6618
|
+
for (const t of result.refs) {
|
|
6093
6619
|
const marker = t.reachedTarget ? pc.green("\u2713") : pc.dim("\u2022");
|
|
6094
6620
|
const label = workstreamSet.size > 1 ? qualifiedId(t) : t.name;
|
|
6095
6621
|
console.log(` ${marker} ${pc.bold(label)} ${pc.dim(`(${t.status})`)}`);
|
|
@@ -6232,6 +6758,9 @@ async function cmdTaskClose(db, rawId, opts = {}) {
|
|
|
6232
6758
|
const sdkOpts = { workstream: ws };
|
|
6233
6759
|
if (opts.evidence !== void 0) sdkOpts.evidence = opts.evidence;
|
|
6234
6760
|
if (opts.ifReady) sdkOpts.ifReady = true;
|
|
6761
|
+
if (opts.evidence !== void 0 && opts.evidence !== "") {
|
|
6762
|
+
sdkOpts.author = await resolveActorIdentity();
|
|
6763
|
+
}
|
|
6235
6764
|
const taskRow = getTask(db, localId, ws);
|
|
6236
6765
|
const r = closeTask(db, localId, sdkOpts);
|
|
6237
6766
|
if ("skipped" in r) {
|
|
@@ -6537,9 +7066,21 @@ function wireTaskCommands(program) {
|
|
|
6537
7066
|
const opts = this.opts();
|
|
6538
7067
|
return handle((db) => cmdTaskTree(db, id, opts), this)();
|
|
6539
7068
|
});
|
|
6540
|
-
task.command("notes <id>").description(
|
|
7069
|
+
task.command("notes <id>").description(
|
|
7070
|
+
"List the notes attached to a task (oldest first). Filters: --tail N (last N), --since <iso> (after timestamp), --since-claim (since most recent claim event)."
|
|
7071
|
+
).option(...WORKSTREAM_OPT).option(...JSON_OPT).option("--tail <n>", "print only the last N notes (alias --last)", parsePositiveNumber).option("--last <n>", "alias for --tail", parsePositiveNumber).option("--since <iso>", "print only notes created after this ISO 8601 timestamp").option(
|
|
7072
|
+
"--since-claim",
|
|
7073
|
+
"print only notes since the most recent 'task claim' event (auto-resolved)"
|
|
7074
|
+
).action(function(id) {
|
|
6541
7075
|
const opts = this.opts();
|
|
6542
|
-
|
|
7076
|
+
const tail = opts.tail ?? opts.last;
|
|
7077
|
+
const merged = {};
|
|
7078
|
+
if (opts.json !== void 0) merged.json = opts.json;
|
|
7079
|
+
if (opts.workstream !== void 0) merged.workstream = opts.workstream;
|
|
7080
|
+
if (tail !== void 0) merged.tail = tail;
|
|
7081
|
+
if (opts.since !== void 0) merged.since = opts.since;
|
|
7082
|
+
if (opts.sinceClaim !== void 0) merged.sinceClaim = opts.sinceClaim;
|
|
7083
|
+
return handle((db) => cmdTaskNotes(db, id, merged), this)();
|
|
6543
7084
|
});
|
|
6544
7085
|
const EVIDENCE_OPT = [
|
|
6545
7086
|
"--evidence <text>",
|
|
@@ -6683,6 +7224,7 @@ async function cmdSpawn(db, name, opts) {
|
|
|
6683
7224
|
const workspace = opts.workspace ? getWorkspaceForAgent(db, name, workstream) : void 0;
|
|
6684
7225
|
const resolvedCommand = opts.command ?? resolveCliCommand(agent.cli);
|
|
6685
7226
|
const commandOverridden = resolvedCommand !== agent.cli;
|
|
7227
|
+
const envSourced = opts.command === void 0 ? resolveCliCommandWithSource(agent.cli) : void 0;
|
|
6686
7228
|
const nextSteps = [
|
|
6687
7229
|
{ intent: "Send work", command: `mu agent send ${name} "..." -w ${workstream}` },
|
|
6688
7230
|
{ intent: "Read pane", command: `mu agent read ${name} -w ${workstream}` },
|
|
@@ -6698,12 +7240,16 @@ async function cmdSpawn(db, name, opts) {
|
|
|
6698
7240
|
workspace: workspace ?? null,
|
|
6699
7241
|
resolvedCommand,
|
|
6700
7242
|
commandOverridden,
|
|
7243
|
+
// env-var attribution for machine consumers: present iff the
|
|
7244
|
+
// resolution came from $MU_<UPPER_CLI>_COMMAND. Mirrors the
|
|
7245
|
+
// human `(via $MU_PI_META_COMMAND)` suffix below.
|
|
7246
|
+
...envSourced?.resolvedFromEnv ? { resolvedFromEnvVar: envSourced.envVar } : {},
|
|
6701
7247
|
nextSteps
|
|
6702
7248
|
});
|
|
6703
7249
|
return;
|
|
6704
7250
|
}
|
|
6705
7251
|
const wsBit = opts.workspace ? pc.dim(" with auto-workspace") : "";
|
|
6706
|
-
const cliDisplay =
|
|
7252
|
+
const cliDisplay = opts.command !== void 0 ? `${agent.cli} ${pc.dim(`(cmd: ${resolvedCommand})`)}` : envSourced?.resolvedFromEnv ? `${agent.cli} ${pc.dim(`(via $${envSourced.envVar})`)}` : agent.cli;
|
|
6707
7253
|
console.log(
|
|
6708
7254
|
`Spawned ${pc.bold(agent.name)} (${cliDisplay}) in window ${pc.bold(agent.tab ?? agent.name)} of ${pc.bold(`mu-${workstream}`)}, pane ${pc.dim(agent.paneId)}${wsBit}`
|
|
6709
7255
|
);
|
|
@@ -6761,7 +7307,7 @@ async function cmdList(db, opts) {
|
|
|
6761
7307
|
console.log(pc.dim(" Panes that look like agents but aren't in the registry."));
|
|
6762
7308
|
console.log(
|
|
6763
7309
|
pc.dim(
|
|
6764
|
-
" Run `mu adopt <pane-id>` to register one as a managed agent (e.g. `mu adopt %15`)."
|
|
7310
|
+
" Run `mu agent adopt <pane-id>` to register one as a managed agent (e.g. `mu agent adopt %15`)."
|
|
6765
7311
|
)
|
|
6766
7312
|
);
|
|
6767
7313
|
for (const orphan of view.orphans) {
|
|
@@ -6848,7 +7394,7 @@ async function cmdClose(db, rawName, opts = {}) {
|
|
|
6848
7394
|
const next = [];
|
|
6849
7395
|
if (result.workspaceFreed) {
|
|
6850
7396
|
next.push({
|
|
6851
|
-
intent: "Workspace was freed alongside the agent (--discard-workspace)",
|
|
7397
|
+
intent: result.workspaceAutoFreedClean ? "Workspace was clean (no uncommitted changes, no commits since fork) so it was auto-freed alongside the agent" : "Workspace was freed alongside the agent (--discard-workspace)",
|
|
6852
7398
|
command: "cd / # the workspace dir is gone"
|
|
6853
7399
|
});
|
|
6854
7400
|
}
|
|
@@ -6865,7 +7411,7 @@ async function cmdClose(db, rawName, opts = {}) {
|
|
|
6865
7411
|
printNextSteps(next);
|
|
6866
7412
|
return;
|
|
6867
7413
|
}
|
|
6868
|
-
const wsBit = result.workspaceFreed ? pc.dim(" (workspace discarded)") : "";
|
|
7414
|
+
const wsBit = result.workspaceFreed ? pc.dim(result.workspaceAutoFreedClean ? " (workspace auto-freed)" : " (workspace discarded)") : "";
|
|
6869
7415
|
console.log(`Closed ${pc.bold(name)}${wsBit}`);
|
|
6870
7416
|
printNextSteps(next);
|
|
6871
7417
|
}
|
|
@@ -6924,6 +7470,41 @@ async function cmdAdopt(db, paneOrTitle, opts) {
|
|
|
6924
7470
|
}
|
|
6925
7471
|
printNextSteps(nextSteps);
|
|
6926
7472
|
}
|
|
7473
|
+
async function cmdKick(db, rawName, opts = {}) {
|
|
7474
|
+
const { name } = await resolveEntityRef(db, rawName, opts, "agent");
|
|
7475
|
+
assertAgentInWorkstream(db, name, opts.workstream);
|
|
7476
|
+
const ws = await resolveWorkstream(opts.workstream);
|
|
7477
|
+
const sigRaw = opts.signal ?? "SIGINT";
|
|
7478
|
+
if (!isKickSignal(sigRaw)) {
|
|
7479
|
+
throw new UsageError(
|
|
7480
|
+
`--signal must be one of SIGINT, SIGTERM, SIGKILL (got ${JSON.stringify(sigRaw)})`
|
|
7481
|
+
);
|
|
7482
|
+
}
|
|
7483
|
+
const signal = sigRaw;
|
|
7484
|
+
const result = await kickAgent(db, name, { workstream: ws, signal });
|
|
7485
|
+
const nextSteps = [
|
|
7486
|
+
{
|
|
7487
|
+
intent: "Read the pane to confirm the tool aborted",
|
|
7488
|
+
command: `mu agent read ${name} -n 30 -w ${ws}`
|
|
7489
|
+
},
|
|
7490
|
+
{
|
|
7491
|
+
intent: "Send follow-up steering once the prompt returns",
|
|
7492
|
+
command: `mu agent send ${name} '...' -w ${ws}`
|
|
7493
|
+
},
|
|
7494
|
+
{
|
|
7495
|
+
intent: "Escalate (graceful \u2192 polite \u2192 hammer)",
|
|
7496
|
+
command: `mu agent kick ${name} --signal SIGTERM -w ${ws}`
|
|
7497
|
+
}
|
|
7498
|
+
];
|
|
7499
|
+
if (opts.json) {
|
|
7500
|
+
emitJson({ ...result, nextSteps });
|
|
7501
|
+
return;
|
|
7502
|
+
}
|
|
7503
|
+
console.log(
|
|
7504
|
+
`Kicked ${pc.bold(name)} ${pc.dim(`(signal=${result.signal}, pgid=${result.signaledPgid}, comm=${result.foregroundComm}, tty=${result.tty})`)}`
|
|
7505
|
+
);
|
|
7506
|
+
printNextSteps(nextSteps);
|
|
7507
|
+
}
|
|
6927
7508
|
async function cmdFree(db, rawName, opts = {}) {
|
|
6928
7509
|
const { name } = await resolveEntityRef(db, rawName, opts, "agent");
|
|
6929
7510
|
assertAgentInWorkstream(db, name, opts.workstream);
|
|
@@ -7007,7 +7588,7 @@ function wireAgentCommands(program) {
|
|
|
7007
7588
|
return handle((db) => cmdAgentShow(db, name, opts), this)();
|
|
7008
7589
|
});
|
|
7009
7590
|
agent.command("close <name>").description(
|
|
7010
|
-
"Kill an agent's pane and remove its registry row. If the agent has a workspace
|
|
7591
|
+
"Kill an agent's pane and remove its registry row. If the agent has a clean workspace (no uncommitted changes AND no commits since fork) it is auto-freed alongside the close; otherwise close refuses (would orphan or lose work) \u2014 pass --discard-workspace to free both anyway (lossy), or run `mu workspace free <agent>` first."
|
|
7011
7592
|
).option(
|
|
7012
7593
|
"--discard-workspace",
|
|
7013
7594
|
"free the agent's workspace alongside close (lossy: pending changes are gone)"
|
|
@@ -7015,6 +7596,16 @@ function wireAgentCommands(program) {
|
|
|
7015
7596
|
const opts = this.opts();
|
|
7016
7597
|
return handle((db) => cmdClose(db, name, opts), this)();
|
|
7017
7598
|
});
|
|
7599
|
+
agent.command("kick <name>").description(
|
|
7600
|
+
"Signal the foreground process group of an agent's pane TTY (escape hatch for a worker wedged on an unbounded `find` / busy-wait loop). Default --signal SIGINT (graceful, matches Ctrl-C). Refuses when the foreground is the wrapping CLI itself \u2014 use `mu agent close` to close the agent."
|
|
7601
|
+
).option(
|
|
7602
|
+
"--signal <sig>",
|
|
7603
|
+
"signal to send: SIGINT (default; graceful), SIGTERM (polite), SIGKILL (hammer)",
|
|
7604
|
+
"SIGINT"
|
|
7605
|
+
).option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(name) {
|
|
7606
|
+
const opts = this.opts();
|
|
7607
|
+
return handle((db) => cmdKick(db, name, opts), this)();
|
|
7608
|
+
});
|
|
7018
7609
|
agent.command("free <name>").description(
|
|
7019
7610
|
"Mark an agent's status as 'free' (idempotent). Pane untouched; reconcile flips back to busy on real activity."
|
|
7020
7611
|
).option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(name) {
|
|
@@ -7025,7 +7616,7 @@ function wireAgentCommands(program) {
|
|
|
7025
7616
|
const opts = this.opts();
|
|
7026
7617
|
return handle((db) => cmdAttach(db, name, opts), this)();
|
|
7027
7618
|
});
|
|
7028
|
-
|
|
7619
|
+
agent.command("adopt <pane-or-title>").description(
|
|
7029
7620
|
"Register an existing tmux pane as a managed mu agent (the inverse of `mu agent list`'s 'orphan' state). Pane id form '%15' or pane title form 'worker-2'."
|
|
7030
7621
|
).option("--name <name>", "agent name (defaults to the pane's current title)").option("--cli <cli>", "agent CLI key (default: pi)").option("--role <role>", "full-access | read-only", "full-access").option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(paneOrTitle) {
|
|
7031
7622
|
const opts = this.optsWithGlobals();
|
|
@@ -7737,24 +8328,6 @@ var ImportSourceNotInBucketError = class extends Error {
|
|
|
7737
8328
|
];
|
|
7738
8329
|
}
|
|
7739
8330
|
};
|
|
7740
|
-
var ImportLegacyLayoutError = class extends Error {
|
|
7741
|
-
constructor(bucketDir) {
|
|
7742
|
-
super(
|
|
7743
|
-
`${bucketDir} is a pre-0.3 (single-workstream) export; mu workstream import requires bucketVersion 2. Re-export with mu \u2265 0.3, or run mu workstream import on a freshly-rendered bucket.`
|
|
7744
|
-
);
|
|
7745
|
-
this.bucketDir = bucketDir;
|
|
7746
|
-
}
|
|
7747
|
-
bucketDir;
|
|
7748
|
-
name = "ImportLegacyLayoutError";
|
|
7749
|
-
errorNextSteps() {
|
|
7750
|
-
return [
|
|
7751
|
-
{
|
|
7752
|
-
intent: "Re-export the source workstream into a new bucket",
|
|
7753
|
-
command: "mu workstream export -w <ws> --out <new-bucket-dir>"
|
|
7754
|
-
}
|
|
7755
|
-
];
|
|
7756
|
-
}
|
|
7757
|
-
};
|
|
7758
8331
|
var WorkstreamAlreadyExistsError = class extends Error {
|
|
7759
8332
|
constructor(workstream) {
|
|
7760
8333
|
super(
|
|
@@ -8038,9 +8611,6 @@ function walkBucket(bucketDir) {
|
|
|
8038
8611
|
if (probe.kind === "corrupt") {
|
|
8039
8612
|
throw new ImportBucketInvalidError(bucketDir, "manifest.json is unreadable / malformed");
|
|
8040
8613
|
}
|
|
8041
|
-
if (probe.kind === "legacy") {
|
|
8042
|
-
throw new ImportLegacyLayoutError(bucketDir);
|
|
8043
|
-
}
|
|
8044
8614
|
const tasksDir = join7(bucketDir, "tasks");
|
|
8045
8615
|
const looksLikeSourceWs = existsSync6(join7(bucketDir, "README.md")) && existsSync6(join7(bucketDir, "INDEX.md")) && existsSync6(tasksDir) && statSync3(tasksDir).isDirectory();
|
|
8046
8616
|
if (!looksLikeSourceWs) {
|
|
@@ -8295,7 +8865,7 @@ var NameAmbiguousError = class extends Error {
|
|
|
8295
8865
|
}
|
|
8296
8866
|
};
|
|
8297
8867
|
function classifyError(err) {
|
|
8298
|
-
if (err instanceof UsageError || err instanceof WorkstreamNameInvalidError || err instanceof ArchiveLabelInvalidError || err instanceof
|
|
8868
|
+
if (err instanceof UsageError || err instanceof WorkstreamNameInvalidError || err instanceof ArchiveLabelInvalidError || err instanceof ImportBucketInvalidError || err instanceof ImportFrontmatterParseError || err instanceof ImportEdgeRefMissingError || err instanceof PruneOptionsInvalidError) {
|
|
8299
8869
|
return { label: "error", exitCode: 2 };
|
|
8300
8870
|
}
|
|
8301
8871
|
if (err instanceof AgentNotFoundError || err instanceof TaskNotFoundError || err instanceof WorkstreamNotFoundError || err instanceof WorkspaceNotFoundError || err instanceof SnapshotNotFoundError || err instanceof ArchiveNotFoundError) {
|
|
@@ -8304,9 +8874,15 @@ function classifyError(err) {
|
|
|
8304
8874
|
if (err instanceof NameAmbiguousError || err instanceof AgentExistsError || err instanceof TaskExistsError || err instanceof TaskAlreadyOwnedError || err instanceof TaskNotInWorkstreamError || err instanceof AgentNotInWorkstreamError || err instanceof CycleError || err instanceof TaskHasOpenDependentsError || err instanceof CrossWorkstreamEdgeError || err instanceof WorkspaceExistsError || err instanceof WorkspacePathNotEmptyError || err instanceof WorkspacePreservedError || err instanceof HomeDirAsProjectRootError || err instanceof WorkspaceVcsRequiredError || err instanceof WorkspaceDirtyError || err instanceof ClaimerNotRegisteredError || err instanceof SnapshotVersionMismatchError || err instanceof SchemaTooOldError || err instanceof TaskIdInvalidError || err instanceof ArchiveAlreadyExistsError || err instanceof ImportSourceNotInBucketError || err instanceof WorkstreamAlreadyExistsError) {
|
|
8305
8875
|
return { label: "conflict", exitCode: 4 };
|
|
8306
8876
|
}
|
|
8877
|
+
if (err instanceof AgentSpawnCliNotFoundError) {
|
|
8878
|
+
return { label: "spawn cli not found", exitCode: 1 };
|
|
8879
|
+
}
|
|
8307
8880
|
if (err instanceof AgentDiedOnSpawnError) {
|
|
8308
8881
|
return { label: "spawn failed", exitCode: 1 };
|
|
8309
8882
|
}
|
|
8883
|
+
if (err instanceof AgentSpawnStartupError) {
|
|
8884
|
+
return { label: "spawn startup error", exitCode: 1 };
|
|
8885
|
+
}
|
|
8310
8886
|
if (err instanceof TmuxError || err instanceof PaneNotFoundError) {
|
|
8311
8887
|
return { label: "tmux", exitCode: 5 };
|
|
8312
8888
|
}
|
|
@@ -8954,7 +9530,7 @@ async function cmdSnapshotShow(db, id, opts = {}) {
|
|
|
8954
9530
|
}
|
|
8955
9531
|
function wireSnapshotCommands(program) {
|
|
8956
9532
|
program.command("undo").description(
|
|
8957
|
-
"Restore the most recent snapshot (or one selected via --to). Pass --yes to actually restore; otherwise prints a dry-run summary. tmux state is NOT rolled back \u2014 the post-restore reconcile prunes ghost agents and surfaces orphan panes; re-spawn or `mu adopt` as needed."
|
|
9533
|
+
"Restore the most recent snapshot (or one selected via --to). Pass --yes to actually restore; otherwise prints a dry-run summary. tmux state is NOT rolled back \u2014 the post-restore reconcile prunes ghost agents and surfaces orphan panes; re-spawn or `mu agent adopt` as needed."
|
|
8958
9534
|
).option("--to <id>", "snapshot id to restore (default: most recent)", parseLines).option("-y, --yes", "actually restore (without this flag, prints a dry-run summary)").option(...JSON_OPT).action(function() {
|
|
8959
9535
|
const opts = this.opts();
|
|
8960
9536
|
return handle((db) => cmdUndo(db, opts), this)();
|
|
@@ -9912,6 +10488,38 @@ async function cmdWorkspaceList(db, opts) {
|
|
|
9912
10488
|
}
|
|
9913
10489
|
console.log(formatWorkspacesTable(decorated));
|
|
9914
10490
|
}
|
|
10491
|
+
async function cmdWorkspaceRecreate(db, rawAgent, opts) {
|
|
10492
|
+
const { name: agent } = await resolveEntityRef(db, rawAgent, opts, "workspace");
|
|
10493
|
+
assertAgentInWorkstream(db, agent, opts.workstream);
|
|
10494
|
+
const workstream = await resolveWorkstream(opts.workstream);
|
|
10495
|
+
const recreateOpts = { workstream };
|
|
10496
|
+
if (opts.backend !== void 0) recreateOpts.backend = opts.backend;
|
|
10497
|
+
if (opts.from !== void 0) recreateOpts.parentRef = opts.from;
|
|
10498
|
+
if (opts.projectRoot !== void 0) recreateOpts.projectRoot = opts.projectRoot;
|
|
10499
|
+
if (opts.force === true) recreateOpts.force = true;
|
|
10500
|
+
const r = await recreateWorkspace(db, agent, recreateOpts);
|
|
10501
|
+
const nextSteps = [
|
|
10502
|
+
{
|
|
10503
|
+
intent: "Send work to the agent (workspace is ready)",
|
|
10504
|
+
command: `mu agent send ${agent} -w ${workstream} "<prompt>"`
|
|
10505
|
+
},
|
|
10506
|
+
{
|
|
10507
|
+
intent: "cd into the freshly recreated workspace",
|
|
10508
|
+
command: `cd $(mu workspace path ${agent} -w ${workstream})`
|
|
10509
|
+
},
|
|
10510
|
+
{ intent: "List workspaces in this workstream", command: `mu workspace list -w ${workstream}` }
|
|
10511
|
+
];
|
|
10512
|
+
if (opts.json) {
|
|
10513
|
+
emitJson({ workspace: r.workspace, previousParentRef: r.previousParentRef, nextSteps });
|
|
10514
|
+
return;
|
|
10515
|
+
}
|
|
10516
|
+
const oldRef = r.previousParentRef ? r.previousParentRef.slice(0, 12) : "\u2014";
|
|
10517
|
+
const newRef = r.workspace.parentRef ? r.workspace.parentRef.slice(0, 12) : "\u2014";
|
|
10518
|
+
console.log(
|
|
10519
|
+
`Recreated workspace ${pc.bold(agent)} ${pc.dim(`(backend=${r.workspace.backend}, ${oldRef} \u2192 ${newRef})`)}`
|
|
10520
|
+
);
|
|
10521
|
+
printNextSteps(nextSteps);
|
|
10522
|
+
}
|
|
9915
10523
|
async function cmdWorkspaceFree(db, rawAgent, opts) {
|
|
9916
10524
|
const { name: agent } = await resolveEntityRef(db, rawAgent, opts, "workspace");
|
|
9917
10525
|
assertAgentInWorkstream(db, agent, opts.workstream);
|
|
@@ -10082,6 +10690,15 @@ function wireWorkspaceCommands(program) {
|
|
|
10082
10690
|
const opts = this.opts();
|
|
10083
10691
|
return handle((db) => cmdWorkspaceFree(db, agent, opts), this)();
|
|
10084
10692
|
});
|
|
10693
|
+
workspace.command("recreate <agent>").description(
|
|
10694
|
+
"Free + create an agent's workspace in one shot \u2014 the canonical between-wave \"prep this worker for the next dispatch\" verb. Atomic guarantees match the underlying free + create pair (one pre-mutation snapshot, one `workspace recreate` event in the audit trail). Reuses the previous backend unless --backend overrides; bases on the project's current main unless --from <ref> overrides. Refuses on a dirty workspace (uncommitted changes) the same way `free` does \u2014 pass --force to discard the dirty changes (the lossy escape hatch)."
|
|
10695
|
+
).option(
|
|
10696
|
+
"--backend <name>",
|
|
10697
|
+
"force a backend instead of reusing the previous one (jj | sl | git | none)"
|
|
10698
|
+
).option("--from <ref>", "base the new workspace on a specific commit / branch / changeset").option("--project-root <path>", "override the project root to branch from (default: cwd)").option("--force", "discard uncommitted changes in the existing workspace (the lossy escape)").option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(agent) {
|
|
10699
|
+
const opts = this.opts();
|
|
10700
|
+
return handle((db) => cmdWorkspaceRecreate(db, agent, opts), this)();
|
|
10701
|
+
});
|
|
10085
10702
|
workspace.command("commits <agent>").description(
|
|
10086
10703
|
"Print commits the agent's workspace has on top of its recorded parent_ref (the fork point), oldest-first. Default text output is `<sha> <subject>` per line; --json emits the full array `[{sha, subject, body, authorDate}]` for piping. --since <ref> overrides the base. The `none` backend errors (no fork point to compare against)."
|
|
10087
10704
|
).option("--since <ref>", "override the base ref (default: workspace's recorded parent_ref)").option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(agent) {
|
|
@@ -11188,12 +11805,22 @@ function applyExitOverride(cmd) {
|
|
|
11188
11805
|
applyExitOverride(sub);
|
|
11189
11806
|
}
|
|
11190
11807
|
}
|
|
11808
|
+
function injectBareNamespaceHelp(program, argv) {
|
|
11809
|
+
if (argv.length !== 3) return argv;
|
|
11810
|
+
const token = argv[2];
|
|
11811
|
+
if (token === void 0 || token.startsWith("-")) return argv;
|
|
11812
|
+
const sub = program.commands.find((c) => c.name() === token || c.aliases().includes(token));
|
|
11813
|
+
if (!sub) return argv;
|
|
11814
|
+
if (sub.commands.length === 0) return argv;
|
|
11815
|
+
return [...argv, "--help"];
|
|
11816
|
+
}
|
|
11191
11817
|
if (isMainEntrypoint()) {
|
|
11192
11818
|
const program = buildProgram();
|
|
11819
|
+
const argv = injectBareNamespaceHelp(program, process.argv);
|
|
11193
11820
|
try {
|
|
11194
|
-
await program.parseAsync(
|
|
11821
|
+
await program.parseAsync(argv);
|
|
11195
11822
|
} catch (err) {
|
|
11196
|
-
const failingCmd = findCommandForArgv(program,
|
|
11823
|
+
const failingCmd = findCommandForArgv(program, argv.slice(2));
|
|
11197
11824
|
const exitCode = emitParseError(err, failingCmd);
|
|
11198
11825
|
process.exit(exitCode);
|
|
11199
11826
|
}
|
|
@@ -11233,6 +11860,7 @@ export {
|
|
|
11233
11860
|
formatWorkspacesTable,
|
|
11234
11861
|
formatWorkstreamsTable,
|
|
11235
11862
|
handle,
|
|
11863
|
+
injectBareNamespaceHelp,
|
|
11236
11864
|
isTaskSortKey,
|
|
11237
11865
|
parseCsvFlag,
|
|
11238
11866
|
parseImpact,
|