@cruxy/cli 0.10.0 → 0.12.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.
Files changed (70) hide show
  1. package/dist/approval/classify.js +21 -0
  2. package/dist/approval/policy.js +6 -0
  3. package/dist/approval/prompt.js +21 -17
  4. package/dist/approval/types.d.ts +5 -0
  5. package/dist/cli/commands/checkpoint.js +6 -4
  6. package/dist/cli/commands/config.js +10 -7
  7. package/dist/cli/commands/index.js +16 -15
  8. package/dist/cli/commands/init.js +5 -3
  9. package/dist/cli/commands/login.js +5 -3
  10. package/dist/cli/commands/pr.js +8 -7
  11. package/dist/cli/commands/rollback.js +7 -6
  12. package/dist/cli/commands/run.js +7 -6
  13. package/dist/cli/commands/skills.js +12 -10
  14. package/dist/cli/commands/test.d.ts +9 -0
  15. package/dist/cli/commands/test.js +47 -0
  16. package/dist/cli/program.js +9 -6
  17. package/dist/cli/repl.js +11 -9
  18. package/dist/cli/session-factory.js +6 -2
  19. package/dist/components/frame.js +3 -1
  20. package/dist/components/fuzzy.d.ts +4 -4
  21. package/dist/components/fuzzy.js +14 -13
  22. package/dist/components/select.js +8 -7
  23. package/dist/config/schema.d.ts +47 -0
  24. package/dist/config/schema.js +20 -0
  25. package/dist/errors/constructors.d.ts +5 -0
  26. package/dist/errors/constructors.js +16 -0
  27. package/dist/errors/format.js +8 -8
  28. package/dist/errors/types.d.ts +3 -0
  29. package/dist/errors/types.js +8 -0
  30. package/dist/onboarding/flow.js +6 -6
  31. package/dist/onboarding/steps.js +11 -11
  32. package/dist/plan/approve.js +6 -6
  33. package/dist/plan/render.js +26 -18
  34. package/dist/render/capabilities.js +4 -0
  35. package/dist/render/diff.d.ts +6 -7
  36. package/dist/render/diff.js +33 -22
  37. package/dist/render/highlight.d.ts +3 -3
  38. package/dist/render/highlight.js +15 -15
  39. package/dist/render/index.d.ts +1 -1
  40. package/dist/render/plain-renderer.d.ts +2 -1
  41. package/dist/render/plain-renderer.js +7 -6
  42. package/dist/render/state.d.ts +7 -2
  43. package/dist/render/state.js +16 -10
  44. package/dist/render/tty-renderer.d.ts +2 -1
  45. package/dist/render/tty-renderer.js +20 -17
  46. package/dist/render/types.d.ts +7 -0
  47. package/dist/subagent/orchestrator.js +21 -6
  48. package/dist/testing/detect.d.ts +3 -0
  49. package/dist/testing/detect.js +44 -0
  50. package/dist/testing/index.d.ts +5 -0
  51. package/dist/testing/index.js +5 -0
  52. package/dist/testing/parse.d.ts +33 -0
  53. package/dist/testing/parse.js +137 -0
  54. package/dist/testing/run-tests-tool.d.ts +42 -0
  55. package/dist/testing/run-tests-tool.js +128 -0
  56. package/dist/testing/runner.d.ts +26 -0
  57. package/dist/testing/runner.js +124 -0
  58. package/dist/testing/types.d.ts +61 -0
  59. package/dist/testing/types.js +7 -0
  60. package/dist/theme/index.d.ts +2 -0
  61. package/dist/theme/index.js +2 -0
  62. package/dist/theme/resolve.d.ts +32 -0
  63. package/dist/theme/resolve.js +73 -0
  64. package/dist/theme/tokens.d.ts +104 -0
  65. package/dist/theme/tokens.js +52 -0
  66. package/dist/tools/registry.js +3 -0
  67. package/dist/tools/types.d.ts +2 -2
  68. package/dist/utils/logger.d.ts +2 -0
  69. package/dist/utils/logger.js +7 -4
  70. package/package.json +1 -1
@@ -0,0 +1,124 @@
1
+ import { spawn } from "node:child_process";
2
+ import { parseFailures } from "./parse.js";
3
+ /**
4
+ * The shipped {@link TestRunner}: spawn the command via the system shell (the
5
+ * same detached-group + kill-tree discipline as `run_command`), capture a
6
+ * TAIL-biased, byte-capped transcript (failures live at the end of test
7
+ * output), and derive `passed` from the exit code — the only source of truth.
8
+ * A timeout, a signal kill, or a spawn error is a *failed result*, never a
9
+ * thrown exception and never a fabricated success.
10
+ */
11
+ export class CommandTestRunner {
12
+ run(command, opts) {
13
+ const startedAt = Date.now();
14
+ return new Promise((resolve) => {
15
+ const capture = new TailCapture(opts.captureBytes);
16
+ let child;
17
+ try {
18
+ child = spawn(command, {
19
+ shell: true,
20
+ cwd: opts.cwd,
21
+ detached: true,
22
+ });
23
+ }
24
+ catch (err) {
25
+ resolve(failed(null, err.message, startedAt));
26
+ return;
27
+ }
28
+ child.stdout?.on("data", (buf) => capture.push(buf));
29
+ child.stderr?.on("data", (buf) => capture.push(buf));
30
+ let settled = false;
31
+ const timer = setTimeout(() => {
32
+ if (settled)
33
+ return;
34
+ settled = true;
35
+ killTree(child.pid);
36
+ resolve(failed(null, capture.text() +
37
+ `\n… [test run timed out after ${opts.timeoutMs}ms and was killed]`, startedAt, capture.truncated));
38
+ }, opts.timeoutMs);
39
+ child.on("error", (err) => {
40
+ if (settled)
41
+ return;
42
+ settled = true;
43
+ clearTimeout(timer);
44
+ resolve(failed(null, err.message, startedAt));
45
+ });
46
+ child.on("close", (code) => {
47
+ if (settled)
48
+ return;
49
+ settled = true;
50
+ clearTimeout(timer);
51
+ const output = capture.text();
52
+ // THE honest-green rule: exit code 0 and nothing else means passed.
53
+ // `code` is null on a signal kill — a failure, whatever the output says.
54
+ const passed = code === 0;
55
+ const parsed = passed ? { failures: [] } : parseFailures(output);
56
+ resolve({
57
+ passed,
58
+ exitCode: code,
59
+ durationMs: Date.now() - startedAt,
60
+ ...(parsed.total !== undefined ? { total: parsed.total } : {}),
61
+ failures: parsed.failures,
62
+ output,
63
+ outputTruncated: capture.truncated,
64
+ });
65
+ });
66
+ });
67
+ }
68
+ }
69
+ /** Shape a non-execution failure (spawn error, timeout) as a failed result. */
70
+ function failed(exitCode, output, startedAt, truncated = false) {
71
+ return {
72
+ passed: false,
73
+ exitCode,
74
+ durationMs: Date.now() - startedAt,
75
+ failures: [],
76
+ output,
77
+ outputTruncated: truncated,
78
+ };
79
+ }
80
+ /**
81
+ * A rolling, tail-biased capture: whole chunks are dropped from the FRONT
82
+ * once the byte cap is exceeded, so the end of the output — where test
83
+ * runners print their failure summaries — always survives.
84
+ */
85
+ export class TailCapture {
86
+ cap;
87
+ chunks = [];
88
+ bytes = 0;
89
+ truncated = false;
90
+ constructor(cap) {
91
+ this.cap = cap;
92
+ }
93
+ push(buf) {
94
+ this.chunks.push(buf);
95
+ this.bytes += buf.length;
96
+ // Drop head chunks while the REMAINDER still meets the cap.
97
+ while (this.chunks.length > 1 &&
98
+ this.bytes - this.chunks[0].length >= this.cap) {
99
+ this.bytes -= this.chunks[0].length;
100
+ this.chunks.shift();
101
+ this.truncated = true;
102
+ }
103
+ }
104
+ text() {
105
+ let all = Buffer.concat(this.chunks);
106
+ if (all.length > this.cap) {
107
+ all = all.subarray(all.length - this.cap);
108
+ this.truncated = true;
109
+ }
110
+ const body = all.toString("utf8");
111
+ return this.truncated ? `… [earlier output truncated]\n${body}` : body;
112
+ }
113
+ }
114
+ /** Kill the whole process group (POSIX; matches run_command's behavior). */
115
+ function killTree(pid) {
116
+ if (pid === undefined)
117
+ return;
118
+ try {
119
+ process.kill(-pid, "SIGKILL");
120
+ }
121
+ catch {
122
+ // Already exited, or no group — nothing to kill.
123
+ }
124
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Types for the test-execution loop (C.13): run the project's test suite,
3
+ * parse what's honestly parseable, and let the agent iterate on failures
4
+ * under a hard cap. The cardinal rule lives in `runner.ts`: **passed is
5
+ * derived from the exit code and nothing else.**
6
+ */
7
+ /** A resolved test command and where it came from — shown, never guessed. */
8
+ export interface TestCommand {
9
+ command: string;
10
+ source: "config" | "package-json";
11
+ }
12
+ /** One extracted failure. Every field beyond `name`/`message` is best-effort. */
13
+ export interface TestFailure {
14
+ name: string;
15
+ message: string;
16
+ file?: string;
17
+ line?: number;
18
+ }
19
+ /** The structured outcome of one test-suite execution. */
20
+ export interface TestRunResult {
21
+ /** `exitCode === 0`, full stop — never inferred from output text. */
22
+ passed: boolean;
23
+ /** The command's exit code; `null` when killed by signal or never spawned. */
24
+ exitCode: number | null;
25
+ /** Measured wall time of the execution. */
26
+ durationMs: number;
27
+ /** Total tests, only when a parser confidently extracted it. */
28
+ total?: number;
29
+ /** Best-effort extracted failures; may be empty even when `passed` is false. */
30
+ failures: TestFailure[];
31
+ /** Tail-biased captured stdout+stderr, capped at `test.captureBytes`. */
32
+ output: string;
33
+ /** Whether the head of the output was dropped to honor the byte cap. */
34
+ outputTruncated: boolean;
35
+ }
36
+ /** Execution bounds handed to a runner per run. */
37
+ export interface TestRunOptions {
38
+ cwd: string;
39
+ /** Kill the run (and its process tree) after this many ms. */
40
+ timeoutMs: number;
41
+ /** Cap on captured output bytes (tail-biased). */
42
+ captureBytes: number;
43
+ }
44
+ /**
45
+ * The swappable execution seam (same discipline as VectorStore/ForgeProvider):
46
+ * the shipped {@link CommandTestRunner} spawns the command via the system
47
+ * shell; tests inject fakes, and a future framework-native runner (e.g. a
48
+ * vitest API runner) slots in without touching the tool.
49
+ */
50
+ export interface TestRunner {
51
+ run(command: string, opts: TestRunOptions): Promise<TestRunResult>;
52
+ }
53
+ /**
54
+ * A pluggable best-effort failure extractor: given the captured output,
55
+ * return whatever structure it can positively recognize — and nothing it
56
+ * can't. Parsers never touch `passed` and never fabricate counts.
57
+ */
58
+ export type FailureParser = (output: string) => {
59
+ failures: TestFailure[];
60
+ total?: number;
61
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Types for the test-execution loop (C.13): run the project's test suite,
3
+ * parse what's honestly parseable, and let the agent iterate on failures
4
+ * under a hard cap. The cardinal rule lives in `runner.ts`: **passed is
5
+ * derived from the exit code and nothing else.**
6
+ */
7
+ export {};
@@ -0,0 +1,2 @@
1
+ export * from "./tokens.js";
2
+ export * from "./resolve.js";
@@ -0,0 +1,2 @@
1
+ export * from "./tokens.js";
2
+ export * from "./resolve.js";
@@ -0,0 +1,32 @@
1
+ import { type Theme, type ThemeCapabilities } from "./tokens.js";
2
+ /**
3
+ * Theme resolution (U.1) — the ONE place picocolors is used. The two axes are
4
+ * fully independent by construction:
5
+ *
6
+ * - `color` drives the stylers only. `pc.createColors(false)` is the identity,
7
+ * so NO_COLOR yields zero ANSI bytes (dim/bold vanish too) — asserted in the
8
+ * tests.
9
+ * - `unicode` drives the glyph table only. It never touches color: a NO_COLOR
10
+ * unicode terminal still prints ✓/✗ (glyphs aren't ANSI), and a colored
11
+ * CRUXY_ASCII terminal prints a colored `[ok]`.
12
+ */
13
+ export declare function resolveTheme(caps: ThemeCapabilities): Theme;
14
+ /**
15
+ * Whether the terminal can render unicode glyphs. `TERM=dumb` and an explicit
16
+ * `CRUXY_ASCII` opt-in fall back to the ASCII table; everything else (real
17
+ * terminals AND pipes) keeps unicode — piping ✓/✗ to a file is fine, and this
18
+ * preserves pre-U.1 behavior for non-TTY output.
19
+ *
20
+ * The single source of the rule: `render/capabilities.ts` calls this to fill
21
+ * `RenderCapabilities.unicode`, and the boolean-seam surfaces (errors,
22
+ * approval, plan, onboarding, CLI commands) call it to build a theme without a
23
+ * signature change.
24
+ */
25
+ export declare function detectUnicode(env?: NodeJS.ProcessEnv): boolean;
26
+ /**
27
+ * Build a theme for a surface that only knows a `color` boolean (the U.3
28
+ * PromptIO, error formatting, plan/onboarding/CLI copy). Unicode is detected
29
+ * from the environment so these surfaces still degrade on dumb terminals,
30
+ * with no change to their public boolean signatures.
31
+ */
32
+ export declare function themeForColor(color: boolean, env?: NodeJS.ProcessEnv): Theme;
@@ -0,0 +1,73 @@
1
+ import pc from "picocolors";
2
+ import { ASCII_GLYPHS, UNICODE_GLYPHS, } from "./tokens.js";
3
+ /**
4
+ * Theme resolution (U.1) — the ONE place picocolors is used. The two axes are
5
+ * fully independent by construction:
6
+ *
7
+ * - `color` drives the stylers only. `pc.createColors(false)` is the identity,
8
+ * so NO_COLOR yields zero ANSI bytes (dim/bold vanish too) — asserted in the
9
+ * tests.
10
+ * - `unicode` drives the glyph table only. It never touches color: a NO_COLOR
11
+ * unicode terminal still prints ✓/✗ (glyphs aren't ANSI), and a colored
12
+ * CRUXY_ASCII terminal prints a colored `[ok]`.
13
+ */
14
+ export function resolveTheme(caps) {
15
+ const c = pc.createColors(caps.color);
16
+ const glyph = caps.unicode ? UNICODE_GLYPHS : ASCII_GLYPHS;
17
+ const strong = c.bold;
18
+ const indent = (text, level = 1) => {
19
+ const pad = " ".repeat(Math.max(0, level));
20
+ return text
21
+ .split("\n")
22
+ .map((line) => (line === "" ? line : pad + line))
23
+ .join("\n");
24
+ };
25
+ return {
26
+ danger: c.red,
27
+ warning: c.yellow,
28
+ success: c.green,
29
+ accent: c.cyan,
30
+ muted: c.dim,
31
+ strong,
32
+ syntax: {
33
+ keyword: c.magenta,
34
+ string: c.green,
35
+ number: c.yellow,
36
+ comment: c.dim,
37
+ },
38
+ glyph,
39
+ heading: strong,
40
+ indent,
41
+ kv: (key, value, keyWidth) => `${strong(keyWidth ? key.padEnd(keyWidth) : key)} ${value}`,
42
+ sep: ` ${glyph.sep} `,
43
+ color: caps.color,
44
+ unicode: caps.unicode,
45
+ };
46
+ }
47
+ /**
48
+ * Whether the terminal can render unicode glyphs. `TERM=dumb` and an explicit
49
+ * `CRUXY_ASCII` opt-in fall back to the ASCII table; everything else (real
50
+ * terminals AND pipes) keeps unicode — piping ✓/✗ to a file is fine, and this
51
+ * preserves pre-U.1 behavior for non-TTY output.
52
+ *
53
+ * The single source of the rule: `render/capabilities.ts` calls this to fill
54
+ * `RenderCapabilities.unicode`, and the boolean-seam surfaces (errors,
55
+ * approval, plan, onboarding, CLI commands) call it to build a theme without a
56
+ * signature change.
57
+ */
58
+ export function detectUnicode(env = process.env) {
59
+ if (env.CRUXY_ASCII !== undefined && env.CRUXY_ASCII !== "")
60
+ return false;
61
+ if (env.TERM === "dumb")
62
+ return false;
63
+ return true;
64
+ }
65
+ /**
66
+ * Build a theme for a surface that only knows a `color` boolean (the U.3
67
+ * PromptIO, error formatting, plan/onboarding/CLI copy). Unicode is detected
68
+ * from the environment so these surfaces still degrade on dumb terminals,
69
+ * with no change to their public boolean signatures.
70
+ */
71
+ export function themeForColor(color, env = process.env) {
72
+ return resolveTheme({ color, unicode: detectUnicode(env) });
73
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The terminal design system (U.1): one typed {@link Theme} of semantic tokens
3
+ * — color *roles* (never raw hues at call sites), a glyph set with ASCII
4
+ * fallbacks, and structure primitives. Every rendered surface resolves ONE
5
+ * theme (from {@link RenderCapabilities}) and references roles like
6
+ * `theme.danger(x)` / `theme.glyph.success`, so the CLI reads as one product
7
+ * and NO_COLOR / dumb-terminal degradation is handled in exactly one place.
8
+ *
9
+ * Picocolors is imported only by `resolve.ts`; this file is pure types + the
10
+ * two glyph tables.
11
+ */
12
+ /** A text styler: wraps a string in ANSI, or the identity under NO_COLOR. */
13
+ export type Styler = (text: string) => string;
14
+ /**
15
+ * Language-token colors for the streaming syntax highlighter. A distinct
16
+ * sub-palette (not status semantics), centralized here so every color choice
17
+ * lives in the theme.
18
+ */
19
+ export interface ThemeSyntax {
20
+ keyword: Styler;
21
+ string: Styler;
22
+ number: Styler;
23
+ comment: Styler;
24
+ }
25
+ /**
26
+ * The glyph vocabulary. Concrete strings (resolved to the unicode or ASCII
27
+ * table), so a call site writes `theme.glyph.success`, never a literal "✓".
28
+ */
29
+ export interface ThemeGlyphs {
30
+ /** Success / done. */
31
+ success: string;
32
+ /** Failure / error. */
33
+ failure: string;
34
+ /** Not-yet-started (plan step). */
35
+ pending: string;
36
+ /** In-progress marker (static, non-animated). */
37
+ running: string;
38
+ /** Selected-row pointer in pickers. */
39
+ pointer: string;
40
+ /** Input caret (REPL prompt, fuzzy query). */
41
+ caret: string;
42
+ /** Next-step / relation arrow. */
43
+ arrow: string;
44
+ caretUp: string;
45
+ caretDown: string;
46
+ /** Text-cursor bar in the fuzzy query line. */
47
+ cursorBar: string;
48
+ bullet: string;
49
+ /** Inline separator (the ` · ` joiner uses this). */
50
+ sep: string;
51
+ /** Truncation marker. */
52
+ ellipsis: string;
53
+ /** Subagent activity marker. */
54
+ play: string;
55
+ /** Animated spinner frames (used only where cursor control exists). */
56
+ spinnerFrames: readonly string[];
57
+ /** Non-animated spinner glyph (no-cursor terminals). */
58
+ spinnerStatic: string;
59
+ }
60
+ /**
61
+ * The resolved design system handed to a surface. Color roles are stylers
62
+ * (identity under NO_COLOR); `glyph`/`sep` are concrete strings; the structure
63
+ * helpers give consistent indentation, headings, and key/value alignment.
64
+ */
65
+ export interface Theme {
66
+ /** Errors, destructive risk, failure. (red) */
67
+ danger: Styler;
68
+ /** Reversible-mutation risk, warnings. (yellow) */
69
+ warning: Styler;
70
+ /** Done / ok. (green) */
71
+ success: Styler;
72
+ /** Brand / interactive / links / pointers. (cyan) */
73
+ accent: Styler;
74
+ /** Secondary text: hints, causes, codes, rationale. (dim) */
75
+ muted: Styler;
76
+ /** Emphasis / headings — a weight, hue-independent; composes with a color. (bold) */
77
+ strong: Styler;
78
+ /** Syntax-highlighting sub-palette. */
79
+ syntax: ThemeSyntax;
80
+ /** Glyph vocabulary (unicode or ASCII per capabilities). */
81
+ glyph: ThemeGlyphs;
82
+ /** Bold heading (alias of {@link strong}, named for intent). */
83
+ heading: Styler;
84
+ /** Prefix every line with `2 * level` spaces. */
85
+ indent(text: string, level?: number): string;
86
+ /** Aligned `key value` with a bold key (optionally padded to `keyWidth`). */
87
+ kv(key: string, value: string, keyWidth?: number): string;
88
+ /** The inline joiner, e.g. ` · ` (unicode) / ` - ` (ascii). */
89
+ sep: string;
90
+ readonly color: boolean;
91
+ readonly unicode: boolean;
92
+ }
93
+ /** The two axes a theme is resolved from — a structural subset of RenderCapabilities. */
94
+ export interface ThemeCapabilities {
95
+ color: boolean;
96
+ unicode: boolean;
97
+ }
98
+ /** The unicode glyph table (real terminals). */
99
+ export declare const UNICODE_GLYPHS: ThemeGlyphs;
100
+ /**
101
+ * The ASCII glyph table (TERM=dumb / CRUXY_ASCII). Readable, single-byte, no
102
+ * mojibake — the intentional U.1 degradation for unicode-unsafe terminals.
103
+ */
104
+ export declare const ASCII_GLYPHS: ThemeGlyphs;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The terminal design system (U.1): one typed {@link Theme} of semantic tokens
3
+ * — color *roles* (never raw hues at call sites), a glyph set with ASCII
4
+ * fallbacks, and structure primitives. Every rendered surface resolves ONE
5
+ * theme (from {@link RenderCapabilities}) and references roles like
6
+ * `theme.danger(x)` / `theme.glyph.success`, so the CLI reads as one product
7
+ * and NO_COLOR / dumb-terminal degradation is handled in exactly one place.
8
+ *
9
+ * Picocolors is imported only by `resolve.ts`; this file is pure types + the
10
+ * two glyph tables.
11
+ */
12
+ /** The unicode glyph table (real terminals). */
13
+ export const UNICODE_GLYPHS = {
14
+ success: "✓",
15
+ failure: "✗",
16
+ pending: "○",
17
+ running: "◐",
18
+ pointer: "❯",
19
+ caret: "›",
20
+ arrow: "→",
21
+ caretUp: "↑",
22
+ caretDown: "↓",
23
+ cursorBar: "▏",
24
+ bullet: "•",
25
+ sep: "·",
26
+ ellipsis: "…",
27
+ play: "⏵",
28
+ spinnerFrames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
29
+ spinnerStatic: "◐",
30
+ };
31
+ /**
32
+ * The ASCII glyph table (TERM=dumb / CRUXY_ASCII). Readable, single-byte, no
33
+ * mojibake — the intentional U.1 degradation for unicode-unsafe terminals.
34
+ */
35
+ export const ASCII_GLYPHS = {
36
+ success: "[ok]",
37
+ failure: "[x]",
38
+ pending: "[ ]",
39
+ running: "~",
40
+ pointer: ">",
41
+ caret: ">",
42
+ arrow: "->",
43
+ caretUp: "^",
44
+ caretDown: "v",
45
+ cursorBar: "|",
46
+ bullet: "*",
47
+ sep: "-",
48
+ ellipsis: "...",
49
+ play: ">",
50
+ spinnerFrames: ["-", "\\", "|", "/"],
51
+ spinnerStatic: "~",
52
+ };
@@ -3,6 +3,7 @@ import { listFilesTool } from "./list-files.js";
3
3
  import { gitStatusTool } from "./git-status.js";
4
4
  import { readFileTool, writeFileTool, editFileTool, applyPatchTool, globTool, grepFilesTool, } from "./file/index.js";
5
5
  import { runCommandTool } from "./shell/index.js";
6
+ import { makeRunTestsTool } from "../testing/run-tests-tool.js";
6
7
  import { searchCodebaseTool } from "./search-codebase.js";
7
8
  import { listSkillsTool } from "./list-skills.js";
8
9
  import { loadSkillTool } from "./load-skill.js";
@@ -63,6 +64,8 @@ export function buildDefaultRegistry() {
63
64
  registry.register(grepFilesTool);
64
65
  registry.register(gitStatusTool);
65
66
  registry.register(runCommandTool);
67
+ // A fresh tool per registry — its iteration budget (C.13) is session-scoped.
68
+ registry.register(makeRunTestsTool());
66
69
  registry.register(searchCodebaseTool);
67
70
  registry.register(listSkillsTool);
68
71
  registry.register(loadSkillTool);
@@ -101,10 +101,10 @@ export type ActionPreview =
101
101
  */
102
102
  export interface ApproveAction {
103
103
  /** The category of side effect being requested. */
104
- kind: "write" | "edit" | "shell" | "patch" | "vcs" | "rollback";
104
+ kind: "write" | "edit" | "shell" | "patch" | "vcs" | "rollback" | "test";
105
105
  /** Absolute resolved path the action targets (write/edit). */
106
106
  path?: string;
107
- /** The command to run (shell). */
107
+ /** The command to run (shell / test). */
108
108
  command?: string;
109
109
  /** Exact-change preview rendered above the prompt (write/edit/patch/vcs/rollback). */
110
110
  preview?: ActionPreview;
@@ -2,6 +2,8 @@ export declare const LOG_LEVELS: readonly ["debug", "info", "warn", "error", "si
2
2
  export type LogLevel = (typeof LOG_LEVELS)[number];
3
3
  declare class Logger {
4
4
  private level;
5
+ /** Diagnostics go to stderr, so the theme resolves against stderr's color. */
6
+ private readonly theme;
5
7
  setLevel(level: LogLevel): void;
6
8
  getLevel(): LogLevel;
7
9
  private enabled;
@@ -1,4 +1,5 @@
1
- import pc from "picocolors";
1
+ import { shouldUseColor } from "../errors/format.js";
2
+ import { themeForColor } from "../theme/index.js";
2
3
  export const LOG_LEVELS = ["debug", "info", "warn", "error", "silent"];
3
4
  const WEIGHT = {
4
5
  debug: 10,
@@ -9,6 +10,8 @@ const WEIGHT = {
9
10
  };
10
11
  class Logger {
11
12
  level = "info";
13
+ /** Diagnostics go to stderr, so the theme resolves against stderr's color. */
14
+ theme = themeForColor(shouldUseColor(process.stderr));
12
15
  setLevel(level) {
13
16
  this.level = level;
14
17
  }
@@ -20,7 +23,7 @@ class Logger {
20
23
  }
21
24
  debug(...args) {
22
25
  if (this.enabled("debug"))
23
- console.error(pc.dim("debug"), ...args);
26
+ console.error(this.theme.muted("debug"), ...args);
24
27
  }
25
28
  info(...args) {
26
29
  if (this.enabled("info"))
@@ -28,11 +31,11 @@ class Logger {
28
31
  }
29
32
  warn(...args) {
30
33
  if (this.enabled("warn"))
31
- console.error(pc.yellow("warn"), ...args);
34
+ console.error(this.theme.warning("warn"), ...args);
32
35
  }
33
36
  error(...args) {
34
37
  if (this.enabled("error"))
35
- console.error(pc.red("error"), ...args);
38
+ console.error(this.theme.danger("error"), ...args);
36
39
  }
37
40
  /** Primary user-facing output — always written to stdout. */
38
41
  print(...args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cruxy/cli",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "an agentic coding CLI",
5
5
  "type": "module",
6
6
  "bin": {