muonroi-cli 1.6.5 → 1.7.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/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/orchestrator/message-processor.js +1 -1
- package/dist/src/orchestrator/prompts.js +16 -2
- package/dist/src/orchestrator/stream-runner.js +50 -3
- package/dist/src/orchestrator/subagent-compactor.d.ts +1 -1
- package/dist/src/orchestrator/subagent-compactor.js +1 -1
- package/dist/src/pil/__tests__/layer4-gsd.test.js +40 -23
- package/dist/src/pil/__tests__/llm-classify.test.js +40 -3
- package/dist/src/pil/layer1-intent.js +10 -1
- package/dist/src/pil/layer1-intent.test.js +18 -0
- package/dist/src/pil/layer4-gsd.js +43 -19
- package/dist/src/pil/llm-classify.d.ts +36 -0
- package/dist/src/pil/llm-classify.js +84 -18
- package/dist/src/pil/types.d.ts +27 -2
- package/dist/src/{gsd → playbook}/__tests__/directives.test.js +34 -58
- package/dist/src/playbook/complexity.d.ts +17 -0
- package/dist/src/playbook/complexity.js +18 -0
- package/dist/src/{gsd → playbook}/directives.d.ts +20 -13
- package/dist/src/playbook/directives.js +149 -0
- package/dist/src/providers/__tests__/reasoning-roundtrip.test.js +70 -1
- package/dist/src/providers/strategies/deepseek.strategy.js +5 -22
- package/dist/src/providers/strategies/siliconflow.strategy.js +5 -0
- package/dist/src/providers/strategies/thinking-mode.d.ts +35 -0
- package/dist/src/providers/strategies/thinking-mode.js +73 -0
- package/dist/src/tools/registry.js +47 -47
- package/dist/src/ui/app.js +91 -24
- package/dist/src/ui/hooks/use-session-picker.d.ts +14 -0
- package/dist/src/ui/hooks/use-session-picker.js +20 -0
- package/dist/src/ui/modals/session-picker-modal.d.ts +14 -0
- package/dist/src/ui/modals/session-picker-modal.js +39 -0
- package/dist/src/ui/utils/relaunch.d.ts +41 -0
- package/dist/src/ui/utils/relaunch.js +71 -0
- package/dist/src/ui/utils/relaunch.test.js +83 -0
- package/package.json +1 -1
- package/dist/src/gsd/__tests__/complexity.test.js +0 -0
- package/dist/src/gsd/complexity.d.ts +0 -28
- package/dist/src/gsd/complexity.js +0 -103
- package/dist/src/gsd/directives.js +0 -154
- /package/dist/src/{gsd → playbook}/__tests__/directives.test.d.ts +0 -0
- /package/dist/src/{gsd/__tests__/complexity.test.d.ts → ui/utils/relaunch.test.d.ts} +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SessionInfo } from "../../types/index.js";
|
|
2
|
+
import type { Theme } from "../theme.js";
|
|
3
|
+
/**
|
|
4
|
+
* Recent-sessions picker. Opened by `/sessions` or `/session`. Selecting a
|
|
5
|
+
* row relaunches the CLI with `--session <id>` (see ui/utils/relaunch.ts) so
|
|
6
|
+
* the user does not need to remember the id or restart by hand.
|
|
7
|
+
*/
|
|
8
|
+
export declare function SessionPickerModal({ t, sessions, focusIndex, width, height, }: {
|
|
9
|
+
t: Theme;
|
|
10
|
+
sessions: SessionInfo[];
|
|
11
|
+
focusIndex: number;
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
}): import("react").ReactNode;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { bottomAlignedModalTop } from "../utils/modal.js";
|
|
3
|
+
/**
|
|
4
|
+
* Recent-sessions picker. Opened by `/sessions` or `/session`. Selecting a
|
|
5
|
+
* row relaunches the CLI with `--session <id>` (see ui/utils/relaunch.ts) so
|
|
6
|
+
* the user does not need to remember the id or restart by hand.
|
|
7
|
+
*/
|
|
8
|
+
export function SessionPickerModal({ t, sessions, focusIndex, width, height, }) {
|
|
9
|
+
const panelWidth = Math.min(80, width - 6);
|
|
10
|
+
const rowCount = Math.max(sessions.length, 1);
|
|
11
|
+
// 4 chrome lines (title row + spacer + footer + paddings) + the rows
|
|
12
|
+
const contentHeight = rowCount + 4;
|
|
13
|
+
const maxH = Math.floor(height * 0.7);
|
|
14
|
+
const panelHeight = Math.min(contentHeight, maxH);
|
|
15
|
+
const top = bottomAlignedModalTop(height, panelHeight);
|
|
16
|
+
const overlayBg = "#000000cc";
|
|
17
|
+
return (_jsx("box", { position: "absolute", left: 0, top: 0, width: width, height: height, alignItems: "center", paddingTop: top, backgroundColor: overlayBg, children: _jsxs("box", { width: panelWidth, height: panelHeight, backgroundColor: t.backgroundPanel, paddingTop: 1, paddingBottom: 1, flexDirection: "column", children: [_jsxs("box", { flexShrink: 0, flexDirection: "row", justifyContent: "space-between", paddingLeft: 2, paddingRight: 2, children: [_jsx("text", { fg: t.primary, children: _jsx("b", { children: "Resume session" }) }), _jsx("text", { fg: t.textMuted, children: "esc" })] }), _jsx("scrollbox", { flexGrow: 1, minHeight: 0, children: sessions.length === 0 ? (_jsx("box", { paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("text", { fg: t.textMuted, children: "No prior sessions in this workspace." }) })) : (sessions.map((s, idx) => {
|
|
18
|
+
const focused = idx === focusIndex;
|
|
19
|
+
const ts = formatTimestamp(s.updatedAt);
|
|
20
|
+
const titleRaw = s.title?.trim() || "(untitled)";
|
|
21
|
+
const titleMax = Math.max(8, panelWidth - 38);
|
|
22
|
+
const title = titleRaw.length > titleMax ? `${titleRaw.slice(0, titleMax - 1)}…` : titleRaw;
|
|
23
|
+
const idShort = s.id.slice(-8);
|
|
24
|
+
return (_jsxs("box", { backgroundColor: focused ? t.selectedBg : undefined, paddingLeft: 2, paddingRight: 2, width: "100%", flexDirection: "row", justifyContent: "space-between", children: [_jsx("text", { fg: focused ? t.selected : t.text, children: `${ts} ${title}` }), _jsx("text", { fg: focused ? t.primary : t.textMuted, children: `${s.model} ${idShort}` })] }, s.id));
|
|
25
|
+
})) }), _jsx("box", { flexShrink: 0, paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("text", { fg: t.textMuted, children: "↑↓ navigate · enter resume (restarts CLI) · esc cancel" }) })] }) }));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Compact MM-DD HH:MM timestamp for the picker rows. Trades the year for
|
|
29
|
+
* space — the picker is workspace-scoped + lists the latest 20 sessions, so
|
|
30
|
+
* a year boundary is rare and an obvious context.
|
|
31
|
+
*/
|
|
32
|
+
function formatTimestamp(d) {
|
|
33
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
34
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
35
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
36
|
+
const min = String(d.getMinutes()).padStart(2, "0");
|
|
37
|
+
return `${mm}-${dd} ${hh}:${min}`;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=session-picker-modal.js.map
|
|
@@ -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,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
|
package/package.json
CHANGED
|
Binary file
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* src/gsd/complexity.ts
|
|
3
|
-
*
|
|
4
|
-
* Heuristic complexity scorer for incoming prompts.
|
|
5
|
-
* Maps a raw user prompt to one of three tiers that drive the GSD directive
|
|
6
|
-
* injected by layer4:
|
|
7
|
-
*
|
|
8
|
-
* - "heavy" → multi-file / multi-repo / architectural / "do everything"
|
|
9
|
-
* Triggers the full discuss → research → verify → plan → impl → verify flow.
|
|
10
|
-
* - "standard" → ordinary feature/bugfix work. GSD-quick mindset.
|
|
11
|
-
* - "quick" → trivial single-shot tasks (typo, rename, read-and-explain).
|
|
12
|
-
*
|
|
13
|
-
* The scorer is intentionally cheap: regex + length checks. It runs inside the
|
|
14
|
-
* 200ms PIL budget and must never throw.
|
|
15
|
-
*/
|
|
16
|
-
export type ComplexityTier = "quick" | "standard" | "heavy";
|
|
17
|
-
export interface ComplexitySignal {
|
|
18
|
-
/** Short tag identifying which heuristic fired (e.g. "multi-repo", "wholesale"). */
|
|
19
|
-
tag: string;
|
|
20
|
-
/** Weight contributed to the score. Positive = heavier, negative = lighter. */
|
|
21
|
-
weight: number;
|
|
22
|
-
}
|
|
23
|
-
export interface ComplexityResult {
|
|
24
|
-
tier: ComplexityTier;
|
|
25
|
-
score: number;
|
|
26
|
-
signals: ComplexitySignal[];
|
|
27
|
-
}
|
|
28
|
-
export declare function scoreComplexity(prompt: string): ComplexityResult;
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* src/gsd/complexity.ts
|
|
3
|
-
*
|
|
4
|
-
* Heuristic complexity scorer for incoming prompts.
|
|
5
|
-
* Maps a raw user prompt to one of three tiers that drive the GSD directive
|
|
6
|
-
* injected by layer4:
|
|
7
|
-
*
|
|
8
|
-
* - "heavy" → multi-file / multi-repo / architectural / "do everything"
|
|
9
|
-
* Triggers the full discuss → research → verify → plan → impl → verify flow.
|
|
10
|
-
* - "standard" → ordinary feature/bugfix work. GSD-quick mindset.
|
|
11
|
-
* - "quick" → trivial single-shot tasks (typo, rename, read-and-explain).
|
|
12
|
-
*
|
|
13
|
-
* The scorer is intentionally cheap: regex + length checks. It runs inside the
|
|
14
|
-
* 200ms PIL budget and must never throw.
|
|
15
|
-
*/
|
|
16
|
-
/** Patterns that strongly suggest a large, multi-step undertaking. */
|
|
17
|
-
const HEAVY_PATTERNS = [
|
|
18
|
-
{ tag: "wholesale", pattern: /\b(toàn bộ|all of|entire|whole|everything|tất cả)\b/i, weight: 3 },
|
|
19
|
-
{ tag: "deep-map", pattern: /\b(deep[-\s]?map|repo[-\s]?map|map (the )?(codebase|project|repo))\b/i, weight: 3 },
|
|
20
|
-
{ tag: "redo", pattern: /\b(redo|rewrite|rebuild|migrate (the )?entire|port (the )?(whole|entire))\b/i, weight: 3 },
|
|
21
|
-
{
|
|
22
|
-
tag: "from-scratch",
|
|
23
|
-
pattern: /\b(from[-\s]scratch|greenfield|new project|khởi tạo (project|dự án))\b/i,
|
|
24
|
-
weight: 2,
|
|
25
|
-
},
|
|
26
|
-
{ tag: "architecture", pattern: /\b(architect(?:ure)?|system design|design contract|domain model)\b/i, weight: 2 },
|
|
27
|
-
{ tag: "milestone", pattern: /\b(milestone|roadmap|epic|phase \d+|sprint \d+)\b/i, weight: 2 },
|
|
28
|
-
{ tag: "multi-repo", pattern: /\b(multi[-\s]repo|across repos|every repo|all repos|cross[-\s]repo)\b/i, weight: 3 },
|
|
29
|
-
{ tag: "refactor-wide", pattern: /\b(refactor (the )?(entire|whole|all))\b/i, weight: 3 },
|
|
30
|
-
{ tag: "i18n", pattern: /\b(i18n|internationali[sz]e|localization|translation pipeline)\b/i, weight: 2 },
|
|
31
|
-
{ tag: "auth-system", pattern: /\b(auth(entication)? system|sso|oauth flow|rbac)\b/i, weight: 2 },
|
|
32
|
-
{ tag: "many-files", pattern: /\b(\d{2,})\s+(files?|modules?|services?)\b/i, weight: 2 },
|
|
33
|
-
];
|
|
34
|
-
/** Patterns that suggest a tiny, one-shot task. */
|
|
35
|
-
const QUICK_PATTERNS = [
|
|
36
|
-
{ tag: "typo", pattern: /\b(typo|misspell|spelling)\b/i, weight: -3 },
|
|
37
|
-
{ tag: "rename", pattern: /\b(rename (this|the|a) (var|variable|function|file)|đổi tên)\b/i, weight: -2 },
|
|
38
|
-
{
|
|
39
|
-
tag: "read-explain",
|
|
40
|
-
pattern: /^(what (does|is)|how (does|do|is)|explain|giải thích|là gì|nghĩa là)\b/i,
|
|
41
|
-
weight: -2,
|
|
42
|
-
},
|
|
43
|
-
{ tag: "single-line", pattern: /\b(one[-\s]liner|single line|một dòng)\b/i, weight: -2 },
|
|
44
|
-
{ tag: "lookup", pattern: /\b(where is|find the|locate|tìm)\b/i, weight: -1 },
|
|
45
|
-
];
|
|
46
|
-
/** Words that, when stacked, indicate orchestration vs single task. */
|
|
47
|
-
const COORDINATION_MARKERS = [
|
|
48
|
-
/\b(?:and then|sau đó|tiếp theo|after that|followed by)\b/gi,
|
|
49
|
-
/\b(?:multiple|several|many|nhiều)\b/gi,
|
|
50
|
-
];
|
|
51
|
-
const HEAVY_THRESHOLD = 4;
|
|
52
|
-
const QUICK_THRESHOLD = -2;
|
|
53
|
-
const LONG_PROMPT_CHARS = 500;
|
|
54
|
-
const SHORT_PROMPT_CHARS = 60;
|
|
55
|
-
export function scoreComplexity(prompt) {
|
|
56
|
-
const signals = [];
|
|
57
|
-
let score = 0;
|
|
58
|
-
if (!prompt || prompt.trim().length === 0) {
|
|
59
|
-
return { tier: "quick", score: 0, signals: [{ tag: "empty", weight: 0 }] };
|
|
60
|
-
}
|
|
61
|
-
for (const { tag, pattern, weight } of HEAVY_PATTERNS) {
|
|
62
|
-
if (pattern.test(prompt)) {
|
|
63
|
-
signals.push({ tag, weight });
|
|
64
|
-
score += weight;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
for (const { tag, pattern, weight } of QUICK_PATTERNS) {
|
|
68
|
-
if (pattern.test(prompt)) {
|
|
69
|
-
signals.push({ tag, weight });
|
|
70
|
-
score += weight;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
// Coordination words: each match adds 1 point (capped at +3).
|
|
74
|
-
let coordinationHits = 0;
|
|
75
|
-
for (const re of COORDINATION_MARKERS) {
|
|
76
|
-
const matches = prompt.match(re);
|
|
77
|
-
if (matches)
|
|
78
|
-
coordinationHits += matches.length;
|
|
79
|
-
}
|
|
80
|
-
if (coordinationHits > 0) {
|
|
81
|
-
const weight = Math.min(coordinationHits, 3);
|
|
82
|
-
signals.push({ tag: "coordination", weight });
|
|
83
|
-
score += weight;
|
|
84
|
-
}
|
|
85
|
-
// Length heuristics.
|
|
86
|
-
if (prompt.length >= LONG_PROMPT_CHARS) {
|
|
87
|
-
signals.push({ tag: "long-prompt", weight: 1 });
|
|
88
|
-
score += 1;
|
|
89
|
-
}
|
|
90
|
-
else if (prompt.length <= SHORT_PROMPT_CHARS) {
|
|
91
|
-
signals.push({ tag: "short-prompt", weight: -1 });
|
|
92
|
-
score -= 1;
|
|
93
|
-
}
|
|
94
|
-
let tier;
|
|
95
|
-
if (score >= HEAVY_THRESHOLD)
|
|
96
|
-
tier = "heavy";
|
|
97
|
-
else if (score <= QUICK_THRESHOLD)
|
|
98
|
-
tier = "quick";
|
|
99
|
-
else
|
|
100
|
-
tier = "standard";
|
|
101
|
-
return { tier, score, signals };
|
|
102
|
-
}
|
|
103
|
-
//# sourceMappingURL=complexity.js.map
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* src/gsd/directives.ts
|
|
3
|
-
*
|
|
4
|
-
* Builds the system-prompt directive block injected by layer4-gsd. The directive
|
|
5
|
-
* is what actually changes the agent's behaviour: it lists the GSD-style steps
|
|
6
|
-
* the agent must take before touching code.
|
|
7
|
-
*
|
|
8
|
-
* Three tiers:
|
|
9
|
-
* - heavy: full discuss → research → verify → plan → impl → verify flow,
|
|
10
|
-
* with mandatory AskUserQuestion + parallel Agent dispatch.
|
|
11
|
-
* - standard: GSD-quick mindset — short plan, then implement, then verify.
|
|
12
|
-
* - quick: minimal hint, run inline.
|
|
13
|
-
*
|
|
14
|
-
* All directive text is English. The agent is responsible for translating
|
|
15
|
-
* user-facing prompts into the user's language at render time.
|
|
16
|
-
*/
|
|
17
|
-
const HEADER = "[gsd-native]";
|
|
18
|
-
/**
|
|
19
|
-
* High-precision predicate: is this turn about the Muonroi ECOSYSTEM (where the
|
|
20
|
-
* muonroi-docs MCP is the right source), as opposed to muonroi-cli internals?
|
|
21
|
-
* Deliberately TIGHTER than smart-filter's hasEcosystemSignal — that one keeps
|
|
22
|
-
* the server (over-keeping costs only tokens), but a behavioural "call docs
|
|
23
|
-
* FIRST" nudge must not fire on every "muonroi" mention or it misdirects
|
|
24
|
-
* CLI-internals questions toward .NET package docs. EN + VI.
|
|
25
|
-
*/
|
|
26
|
-
const ECOSYSTEM_SCOPE_RE = /\becosystem\b|hệ\s*sinh\s*thái|he\s*sinh\s*thai|building[-\s]?block|open[-\s]?core|rule\s*engine|decision\s*table|\bnuget\b/i;
|
|
27
|
-
export function mentionsEcosystemScope(message) {
|
|
28
|
-
return ECOSYSTEM_SCOPE_RE.test(message);
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Appended to any directive when the turn is ecosystem-scoped. Phrased
|
|
32
|
-
* conditionally ("if … available") so it is harmless when muonroi-docs is not
|
|
33
|
-
* configured — the model simply finds no such tool and falls back to local files.
|
|
34
|
-
*/
|
|
35
|
-
export const ECOSYSTEM_DOCS_NUDGE = [
|
|
36
|
-
`${HEADER} ECOSYSTEM SCOPE — this turn concerns the Muonroi ecosystem (platform overview, BB/.NET packages, building-block, open-core boundary, setup).`,
|
|
37
|
-
"If the muonroi-docs MCP is available, it is the AUTHORITATIVE source — call it FIRST (docs_search / setup_guide / bb_recipe_list / bb_package_describe), THEN ground with local files. Do NOT characterize the ecosystem from local repo files alone.",
|
|
38
|
-
].join("\n");
|
|
39
|
-
/**
|
|
40
|
-
* Appended to any directive when the user's reply language is non-English.
|
|
41
|
-
* The base system prompt's "reply in user's language" rule normally suffices,
|
|
42
|
-
* but `concise` / `FIX-FIRST` / GSD-debug directive bodies stack on top of it
|
|
43
|
-
* with strong "be terse / code over prose" language that crowds the rule out
|
|
44
|
-
* — observed live (storyflow_ui 22661c8de9f2). This NUDGE re-anchors the rule
|
|
45
|
-
* inside the directive itself so brevity preferences cannot override it.
|
|
46
|
-
*/
|
|
47
|
-
export function buildLanguageNudge(lang) {
|
|
48
|
-
return [
|
|
49
|
-
`${HEADER} LANGUAGE — the user wrote in ${lang}. Reply in ${lang}.`,
|
|
50
|
-
"This rule OVERRIDES any brevity / concise / code-over-prose directive: terseness is fine, but the response language stays the user's.",
|
|
51
|
-
].join("\n");
|
|
52
|
-
}
|
|
53
|
-
function renderGrayAreas(qs) {
|
|
54
|
-
if (qs.length === 0)
|
|
55
|
-
return " (no gray areas detected — confirm the request is fully specified before proceeding)";
|
|
56
|
-
return qs
|
|
57
|
-
.map((q, idx) => {
|
|
58
|
-
const opts = q.options.map((o, i) => `${i === 0 ? "[recommended]" : "[alt]"} ${o}`).join(" / ");
|
|
59
|
-
return ` ${idx + 1}. (${q.dimension}) ${q.question}\n options: ${opts}`;
|
|
60
|
-
})
|
|
61
|
-
.join("\n");
|
|
62
|
-
}
|
|
63
|
-
function buildHeavy(input) {
|
|
64
|
-
const grayBlock = renderGrayAreas(input.grayAreas);
|
|
65
|
-
return [
|
|
66
|
-
`${HEADER} HEAVY task detected (score=${input.complexity.score}, signals=${input.complexity.signals.map((s) => s.tag).join(",") || "none"}).`,
|
|
67
|
-
"MANDATORY flow before any implementation:",
|
|
68
|
-
" 1. DISCUSS — call AskUserQuestion with the gray areas below. Each question MUST include the recommended default first so the user can accept by selecting it.",
|
|
69
|
-
" Localize question text into the user's language (the language of their last message). Keep options labels English unless they refer to natural-language content.",
|
|
70
|
-
" 2. RESEARCH + VERIFY — once the user answers, dispatch two Agent calls IN PARALLEL in a single message:",
|
|
71
|
-
" (a) a research agent to gather codebase / external facts needed for the task,",
|
|
72
|
-
" (b) a verify agent to enumerate acceptance criteria and risks.",
|
|
73
|
-
" Wait for both before planning.",
|
|
74
|
-
" 3. PLAN — produce a short numbered plan grounded in the research + verify outputs. Confirm the plan with the user only if it diverges from their stated intent.",
|
|
75
|
-
" 4. IMPLEMENT — execute the plan in atomic steps. Prefer parallel Agent dispatch when steps are independent.",
|
|
76
|
-
" 5. VERIFY — run tests / lints / type checks. Report evidence (command + exit code) before claiming done.",
|
|
77
|
-
"Gray areas to clarify in step 1:",
|
|
78
|
-
grayBlock,
|
|
79
|
-
"Do NOT skip steps. Do NOT begin implementation until step 1 is answered.",
|
|
80
|
-
].join("\n");
|
|
81
|
-
}
|
|
82
|
-
function buildStandard(input) {
|
|
83
|
-
const phaseHint = input.phase ? ` (detected phase: ${input.phase})` : "";
|
|
84
|
-
// Debug-phase variant: tighten exploration budget. Session 7d56a049e1e3
|
|
85
|
-
// ran 109 tool calls (58 bash + 33 read_file + 16 grep + 2 mcp) over 6
|
|
86
|
-
// minutes WITHOUT a single edit_file / write_file — agent over-researched
|
|
87
|
-
// the CI failure instead of attempting a fix. Add an explicit exploration
|
|
88
|
-
// cap and require committing to a hypothesis early.
|
|
89
|
-
if (input.phase === "debug") {
|
|
90
|
-
return [
|
|
91
|
-
`${HEADER} DEBUG task${phaseHint} — apply GSD-quick mindset, FIX-FIRST.`,
|
|
92
|
-
"Flow:",
|
|
93
|
-
" 1. State a 2-3 line hypothesis (what is failing, your best guess of why) BEFORE reading more than 3 files.",
|
|
94
|
-
" 2. Apply the smallest plausible fix with edit_file / write_file. Do NOT keep exploring — commit to a hypothesis, ship the diff, iterate on failure.",
|
|
95
|
-
" 3. Verify the fix (rerun the failing command / test) and report evidence.",
|
|
96
|
-
"Hard limits — exceed only if a tool result genuinely contradicts your hypothesis:",
|
|
97
|
-
" - ≤ 8 read_file calls before first edit_file",
|
|
98
|
-
" - ≤ 5 grep calls before first edit_file",
|
|
99
|
-
" - ≤ 10 bash log-fetching calls (gh run view, cat log, etc.) before first edit_file",
|
|
100
|
-
"If hard limits are blown and you still have no fix, STOP and report what you tried + why you're stuck. Do NOT keep searching.",
|
|
101
|
-
].join("\n");
|
|
102
|
-
}
|
|
103
|
-
return [
|
|
104
|
-
`${HEADER} STANDARD task${phaseHint} — apply GSD-quick mindset.`,
|
|
105
|
-
"Flow:",
|
|
106
|
-
" 1. State a 2-3 line plan in your reply.",
|
|
107
|
-
" 2. Implement directly with the appropriate tools.",
|
|
108
|
-
" 3. Verify (tests / type-check / quick smoke) and report evidence.",
|
|
109
|
-
"Skip the discuss/research subagent dance unless a real ambiguity blocks step 1.",
|
|
110
|
-
].join("\n");
|
|
111
|
-
}
|
|
112
|
-
function buildQuestion() {
|
|
113
|
-
// Informational / question / meta-analysis turns. The deliverable is the
|
|
114
|
-
// answer itself — there is no code to implement or test. Keep the agent's
|
|
115
|
-
// process OUT of the reply: a human asked, a human reads the result.
|
|
116
|
-
return [
|
|
117
|
-
`${HEADER} QUESTION / explanatory request — no code change is being asked for.`,
|
|
118
|
-
"Answer it directly and completely, written for the HUMAN who asked:",
|
|
119
|
-
" 1. Investigate only as needed — read/grep the specific files that ground your answer this turn.",
|
|
120
|
-
" 2. Lead with the answer. Use clear prose + structure (headings, bullets). Where a claim rests on the code, cite a concise file:line inline.",
|
|
121
|
-
" 3. Do NOT output an implementation plan, do NOT narrate your own process or restate these instructions, and do NOT name internal layers / contract rules / tools as if the reader were the agent.",
|
|
122
|
-
"There is no implement/verify step — the answer is the deliverable.",
|
|
123
|
-
].join("\n");
|
|
124
|
-
}
|
|
125
|
-
function buildQuick(input) {
|
|
126
|
-
const phaseHint = input.phase ? ` phase=${input.phase}` : "";
|
|
127
|
-
return `${HEADER} QUICK task${phaseHint} — handle inline. No plan, no subagents. Make the smallest correct change and report.`;
|
|
128
|
-
}
|
|
129
|
-
export function buildDirective(input) {
|
|
130
|
-
// Informational/meta prompts answer a human — never apply the
|
|
131
|
-
// implement/verify scaffold (it agent-ifies the reply), regardless of tier.
|
|
132
|
-
const base = input.informational
|
|
133
|
-
? { text: buildQuestion(), tier: input.complexity.tier, blocking: false }
|
|
134
|
-
: input.complexity.tier === "heavy"
|
|
135
|
-
? { text: buildHeavy(input), tier: "heavy", blocking: true }
|
|
136
|
-
: input.complexity.tier === "standard"
|
|
137
|
-
? { text: buildStandard(input), tier: "standard", blocking: false }
|
|
138
|
-
: { text: buildQuick(input), tier: "quick", blocking: false };
|
|
139
|
-
// Ecosystem-scoped turns get a docs-first nudge regardless of tier (question
|
|
140
|
-
// OR task): muonroi-docs is the authoritative source and must not be skipped
|
|
141
|
-
// in favour of guessing from local files (session 41ccfeb2ceee turn 1).
|
|
142
|
-
let text = base.text;
|
|
143
|
-
if (input.ecosystem) {
|
|
144
|
-
text = `${text}\n${ECOSYSTEM_DOCS_NUDGE}`;
|
|
145
|
-
}
|
|
146
|
-
// Language nudge: re-anchor the "reply in user's language" rule INSIDE the
|
|
147
|
-
// directive when the user wrote in a non-English language, so layered
|
|
148
|
-
// brevity/concise directives can't drown it (storyflow_ui 22661c8de9f2).
|
|
149
|
-
if (input.replyLanguage) {
|
|
150
|
-
text = `${text}\n${buildLanguageNudge(input.replyLanguage)}`;
|
|
151
|
-
}
|
|
152
|
-
return { ...base, text };
|
|
153
|
-
}
|
|
154
|
-
//# sourceMappingURL=directives.js.map
|
|
File without changes
|
|
File without changes
|