muonroi-cli 1.6.4 → 1.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/packages/agent-harness-core/src/event-filter.js +1 -0
  2. package/dist/packages/agent-harness-core/src/event-redact.js +7 -2
  3. package/dist/packages/agent-harness-core/src/protocol.d.ts +8 -0
  4. package/dist/src/generated/version.d.ts +1 -1
  5. package/dist/src/generated/version.js +1 -1
  6. package/dist/src/gsd/__tests__/directives.test.js +37 -0
  7. package/dist/src/gsd/directives.d.ts +18 -0
  8. package/dist/src/gsd/directives.js +23 -2
  9. package/dist/src/orchestrator/message-processor.d.ts +8 -0
  10. package/dist/src/orchestrator/message-processor.js +56 -7
  11. package/dist/src/orchestrator/orchestrator.d.ts +10 -0
  12. package/dist/src/orchestrator/orchestrator.js +11 -0
  13. package/dist/src/orchestrator/stall-rescue.d.ts +1 -0
  14. package/dist/src/orchestrator/stall-rescue.js +20 -1
  15. package/dist/src/orchestrator/stall-rescue.test.js +30 -1
  16. package/dist/src/orchestrator/steer-inbox.d.ts +32 -0
  17. package/dist/src/orchestrator/steer-inbox.js +20 -0
  18. package/dist/src/orchestrator/steer-inbox.test.d.ts +1 -0
  19. package/dist/src/orchestrator/steer-inbox.test.js +33 -0
  20. package/dist/src/orchestrator/tool-loop-askcard.d.ts +59 -0
  21. package/dist/src/orchestrator/tool-loop-askcard.js +86 -0
  22. package/dist/src/orchestrator/tool-loop-askcard.test.d.ts +1 -0
  23. package/dist/src/orchestrator/tool-loop-askcard.test.js +71 -0
  24. package/dist/src/pil/layer4-gsd.js +5 -1
  25. package/dist/src/ui/app.js +142 -59
  26. package/dist/src/ui/hooks/use-session-picker.d.ts +14 -0
  27. package/dist/src/ui/hooks/use-session-picker.js +20 -0
  28. package/dist/src/ui/modals/session-picker-modal.d.ts +14 -0
  29. package/dist/src/ui/modals/session-picker-modal.js +39 -0
  30. package/dist/src/ui/utils/relaunch.d.ts +41 -0
  31. package/dist/src/ui/utils/relaunch.js +71 -0
  32. package/dist/src/ui/utils/relaunch.test.d.ts +1 -0
  33. package/dist/src/ui/utils/relaunch.test.js +83 -0
  34. package/dist/src/utils/settings.d.ts +10 -0
  35. package/dist/src/utils/settings.js +12 -0
  36. package/dist/src/utils/settings.test.js +21 -0
  37. package/package.json +1 -1
@@ -0,0 +1,41 @@
1
+ /**
2
+ * src/ui/utils/relaunch.ts
3
+ *
4
+ * Helpers for "relaunch the CLI with a different session" — used by the
5
+ * /sessions picker so the user does not have to remember the id + restart
6
+ * the binary manually (the whole motivation of the picker).
7
+ *
8
+ * The argv mangling is a PURE function so it is unit-testable in isolation
9
+ * from the spawn side effects. `relaunchWithSession` glues argv mangling +
10
+ * child_process spawn + parent exit; it returns nothing (process replaces).
11
+ */
12
+ import { spawn } from "node:child_process";
13
+ /**
14
+ * Strip any existing `-s <id>` / `--session <id>` / `--session=<id>` from
15
+ * argv (kept indices intact otherwise) and append a fresh `--session <id>`.
16
+ * Pure — input arrays are not mutated.
17
+ *
18
+ * `argv` is in the shape Node provides: `[exec, scriptOrFirstArg, ...rest]`.
19
+ * The caller passes `process.argv.slice(1)` (the args part) and re-prepends
20
+ * `process.argv[0]` itself. We sanitize the WHOLE args portion in one pass.
21
+ */
22
+ export declare function sanitizeArgvForResume(args: ReadonlyArray<string>, sessionId: string): string[];
23
+ export interface RelaunchOptions {
24
+ /** Override process.argv (tests). Defaults to live process.argv. */
25
+ argv?: ReadonlyArray<string>;
26
+ /** Override the exit hook (tests). Defaults to process.exit. */
27
+ onExit?: (code: number) => void;
28
+ /** Injected spawn for tests. Defaults to the real node:child_process spawn. */
29
+ spawnFn?: typeof spawn;
30
+ }
31
+ /**
32
+ * Spawn a fresh CLI process bound to {sessionId} and exit the current one.
33
+ * Cross-platform: uses `stdio: "inherit"` so the child takes over the TTY,
34
+ * and `detached: false` so killing the parent's terminal kills the child
35
+ * (the user expects "close window = kill" semantics).
36
+ *
37
+ * NOTE: the caller should disconnect/teardown the current TUI before invoking
38
+ * this — the spawn happens immediately and the parent exit is on next tick,
39
+ * so any open file handles / MCP transports must be released first.
40
+ */
41
+ export declare function relaunchWithSession(sessionId: string, opts?: RelaunchOptions): void;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * src/ui/utils/relaunch.ts
3
+ *
4
+ * Helpers for "relaunch the CLI with a different session" — used by the
5
+ * /sessions picker so the user does not have to remember the id + restart
6
+ * the binary manually (the whole motivation of the picker).
7
+ *
8
+ * The argv mangling is a PURE function so it is unit-testable in isolation
9
+ * from the spawn side effects. `relaunchWithSession` glues argv mangling +
10
+ * child_process spawn + parent exit; it returns nothing (process replaces).
11
+ */
12
+ import { spawn } from "node:child_process";
13
+ /**
14
+ * Strip any existing `-s <id>` / `--session <id>` / `--session=<id>` from
15
+ * argv (kept indices intact otherwise) and append a fresh `--session <id>`.
16
+ * Pure — input arrays are not mutated.
17
+ *
18
+ * `argv` is in the shape Node provides: `[exec, scriptOrFirstArg, ...rest]`.
19
+ * The caller passes `process.argv.slice(1)` (the args part) and re-prepends
20
+ * `process.argv[0]` itself. We sanitize the WHOLE args portion in one pass.
21
+ */
22
+ export function sanitizeArgvForResume(args, sessionId) {
23
+ if (!sessionId || !sessionId.trim()) {
24
+ throw new Error("sanitizeArgvForResume: sessionId is required");
25
+ }
26
+ const out = [];
27
+ for (let i = 0; i < args.length; i++) {
28
+ const a = args[i];
29
+ if (a === "-s" || a === "--session") {
30
+ // skip the flag AND its value (if present and not another flag)
31
+ const next = args[i + 1];
32
+ if (next !== undefined && !next.startsWith("-"))
33
+ i++;
34
+ continue;
35
+ }
36
+ if (a.startsWith("--session=")) {
37
+ continue; // skip the combined form
38
+ }
39
+ out.push(a);
40
+ }
41
+ out.push("--session", sessionId);
42
+ return out;
43
+ }
44
+ /**
45
+ * Spawn a fresh CLI process bound to {sessionId} and exit the current one.
46
+ * Cross-platform: uses `stdio: "inherit"` so the child takes over the TTY,
47
+ * and `detached: false` so killing the parent's terminal kills the child
48
+ * (the user expects "close window = kill" semantics).
49
+ *
50
+ * NOTE: the caller should disconnect/teardown the current TUI before invoking
51
+ * this — the spawn happens immediately and the parent exit is on next tick,
52
+ * so any open file handles / MCP transports must be released first.
53
+ */
54
+ export function relaunchWithSession(sessionId, opts = {}) {
55
+ const argv = opts.argv ?? process.argv;
56
+ const exec = argv[0];
57
+ if (!exec) {
58
+ throw new Error("relaunchWithSession: process.argv[0] is empty — cannot relaunch");
59
+ }
60
+ const exit = opts.onExit ?? ((code) => process.exit(code));
61
+ const spawnImpl = opts.spawnFn ?? spawn;
62
+ const args = sanitizeArgvForResume(argv.slice(1), sessionId);
63
+ const child = spawnImpl(exec, args, { stdio: "inherit", detached: false });
64
+ child.once("error", (err) => {
65
+ console.error(`[relaunch] spawn failed: ${err?.message ?? err}`);
66
+ exit(1);
67
+ });
68
+ // Hand the TTY to the child and exit cleanly. The child takes over rendering.
69
+ child.once("spawn", () => exit(0));
70
+ }
71
+ //# sourceMappingURL=relaunch.js.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,83 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { relaunchWithSession, sanitizeArgvForResume } from "./relaunch.js";
4
+ describe("sanitizeArgvForResume", () => {
5
+ it("appends --session when no prior session flag exists", () => {
6
+ expect(sanitizeArgvForResume(["-m", "grok-build-0.1"], "abc-123")).toEqual([
7
+ "-m",
8
+ "grok-build-0.1",
9
+ "--session",
10
+ "abc-123",
11
+ ]);
12
+ });
13
+ it("strips an existing `-s <id>` and replaces it", () => {
14
+ expect(sanitizeArgvForResume(["-s", "old-id", "-m", "grok-build-0.1"], "new-id")).toEqual([
15
+ "-m",
16
+ "grok-build-0.1",
17
+ "--session",
18
+ "new-id",
19
+ ]);
20
+ });
21
+ it("strips an existing `--session <id>` (long form)", () => {
22
+ expect(sanitizeArgvForResume(["--session", "old-id", "-y"], "new-id")).toEqual(["-y", "--session", "new-id"]);
23
+ });
24
+ it("strips the combined `--session=<id>` form", () => {
25
+ expect(sanitizeArgvForResume(["--session=old-id", "-y"], "new-id")).toEqual(["-y", "--session", "new-id"]);
26
+ });
27
+ it("strips `--session` even when its value looks like another flag (treats value as missing)", () => {
28
+ // edge: user typed `--session --batch-api` — we don't eat the next flag
29
+ expect(sanitizeArgvForResume(["--session", "--batch-api", "-y"], "new-id")).toEqual([
30
+ "--batch-api",
31
+ "-y",
32
+ "--session",
33
+ "new-id",
34
+ ]);
35
+ });
36
+ it("removes multiple stray session flags (defensive — last wins)", () => {
37
+ expect(sanitizeArgvForResume(["-s", "a", "--session", "b", "--session=c"], "z")).toEqual(["--session", "z"]);
38
+ });
39
+ it("throws when sessionId is empty or whitespace", () => {
40
+ expect(() => sanitizeArgvForResume([], "")).toThrow(/sessionId is required/);
41
+ expect(() => sanitizeArgvForResume([], " ")).toThrow(/sessionId is required/);
42
+ });
43
+ });
44
+ describe("relaunchWithSession", () => {
45
+ it("spawns the same executable with the sanitized argv + session id, then exits 0", () => {
46
+ const exitMock = vi.fn();
47
+ const child = new EventEmitter();
48
+ const spawnMock = vi.fn(() => child);
49
+ relaunchWithSession("sess-xyz", {
50
+ argv: ["/usr/local/bin/muonroi-cli", "-m", "grok-build-0.1"],
51
+ onExit: exitMock,
52
+ spawnFn: spawnMock,
53
+ });
54
+ expect(spawnMock).toHaveBeenCalledTimes(1);
55
+ expect(spawnMock).toHaveBeenCalledWith("/usr/local/bin/muonroi-cli", ["-m", "grok-build-0.1", "--session", "sess-xyz"], { stdio: "inherit", detached: false });
56
+ // exit fires on the child's "spawn" event
57
+ child.emit("spawn");
58
+ expect(exitMock).toHaveBeenCalledWith(0);
59
+ });
60
+ it("exits 1 if the child spawn errors before starting", () => {
61
+ const exitMock = vi.fn();
62
+ const errMock = vi.spyOn(console, "error").mockImplementation(() => { });
63
+ const child = new EventEmitter();
64
+ const spawnMock = vi.fn(() => child);
65
+ relaunchWithSession("sess-xyz", {
66
+ argv: ["/bin/muonroi", "-y"],
67
+ onExit: exitMock,
68
+ spawnFn: spawnMock,
69
+ });
70
+ child.emit("error", new Error("ENOENT"));
71
+ expect(exitMock).toHaveBeenCalledWith(1);
72
+ expect(errMock).toHaveBeenCalled();
73
+ errMock.mockRestore();
74
+ });
75
+ it("throws when argv[0] is missing (cannot relaunch without an executable)", () => {
76
+ expect(() => relaunchWithSession("sess", {
77
+ argv: [],
78
+ onExit: () => { },
79
+ spawnFn: (() => new EventEmitter()),
80
+ })).toThrow(/process\.argv\[0\] is empty/);
81
+ });
82
+ });
83
+ //# sourceMappingURL=relaunch.test.js.map
@@ -330,6 +330,16 @@ export declare function getProviderStallTimeoutMs(): number;
330
330
  * Default 1. Env override: MUONROI_PROVIDER_STALL_RETRIES.
331
331
  */
332
332
  export declare function getProviderStallRetries(): number;
333
+ /**
334
+ * Live-queue steering: when true, a message typed while a turn is streaming is
335
+ * injected into the running turn at the next prepareStep boundary (as a `user`
336
+ * interjection) instead of waiting for the turn to finish and running as a new
337
+ * turn. When false, the legacy deferred-queue behaviour is preserved (the
338
+ * message runs only after the current turn completes). House convention for a
339
+ * default-true boolean knob: only an explicit "0" disables; unset/blank/any
340
+ * other value = enabled. Env override: MUONROI_STEER_INJECTION.
341
+ */
342
+ export declare function getSteerInjectionEnabled(): boolean;
333
343
  /**
334
344
  * Phase B3 — threshold (in chars of cumulative message content) above which
335
345
  * the sub-agent `prepareStep` compactor rewrites older tool_result parts
@@ -714,6 +714,18 @@ export function getProviderStallRetries() {
714
714
  }
715
715
  return 1;
716
716
  }
717
+ /**
718
+ * Live-queue steering: when true, a message typed while a turn is streaming is
719
+ * injected into the running turn at the next prepareStep boundary (as a `user`
720
+ * interjection) instead of waiting for the turn to finish and running as a new
721
+ * turn. When false, the legacy deferred-queue behaviour is preserved (the
722
+ * message runs only after the current turn completes). House convention for a
723
+ * default-true boolean knob: only an explicit "0" disables; unset/blank/any
724
+ * other value = enabled. Env override: MUONROI_STEER_INJECTION.
725
+ */
726
+ export function getSteerInjectionEnabled() {
727
+ return process.env.MUONROI_STEER_INJECTION !== "0";
728
+ }
717
729
  /**
718
730
  * Phase B3 — threshold (in chars of cumulative message content) above which
719
731
  * the sub-agent `prepareStep` compactor rewrites older tool_result parts
@@ -188,4 +188,25 @@ describe("getProviderStallRetries", () => {
188
188
  }
189
189
  });
190
190
  });
191
+ describe("getSteerInjectionEnabled", () => {
192
+ it("defaults to true when the env var is unset or blank", async () => {
193
+ vi.unstubAllEnvs();
194
+ const { getSteerInjectionEnabled } = await import("./settings.js");
195
+ expect(getSteerInjectionEnabled()).toBe(true);
196
+ vi.stubEnv("MUONROI_STEER_INJECTION", "");
197
+ expect(getSteerInjectionEnabled()).toBe(true);
198
+ });
199
+ it("returns false only for an explicit '0'", async () => {
200
+ const { getSteerInjectionEnabled } = await import("./settings.js");
201
+ vi.stubEnv("MUONROI_STEER_INJECTION", "0");
202
+ expect(getSteerInjectionEnabled()).toBe(false);
203
+ });
204
+ it("returns true for '1' and any other non-'0' value", async () => {
205
+ const { getSteerInjectionEnabled } = await import("./settings.js");
206
+ for (const v of ["1", "true", "yes", "on", "xyz"]) {
207
+ vi.stubEnv("MUONROI_STEER_INJECTION", v);
208
+ expect(getSteerInjectionEnabled()).toBe(true);
209
+ }
210
+ });
211
+ });
191
212
  //# sourceMappingURL=settings.test.js.map
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "1.6.4",
6
+ "version": "1.6.6",
7
7
  "description": "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.",
8
8
  "repository": {
9
9
  "type": "git",