@cruxy/cli 0.28.1 → 0.29.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/agent/loop.js +3 -3
- package/dist/agent/prompts.d.ts +0 -1
- package/dist/agent/prompts.js +7 -2
- package/dist/cli/commands/run.js +21 -2
- package/dist/config/schema.d.ts +34 -2
- package/dist/config/schema.js +14 -1
- package/dist/errors/constructors.js +1 -1
- package/dist/tools/file/glob.js +17 -2
- package/dist/tools/file/grep-files.d.ts +2 -1
- package/dist/tools/file/grep-files.js +15 -3
- package/package.json +1 -1
package/dist/agent/loop.js
CHANGED
|
@@ -51,9 +51,9 @@ async function driveLoop(args, renderer, routed) {
|
|
|
51
51
|
platform: process.platform,
|
|
52
52
|
date: new Date().toISOString().slice(0, 10),
|
|
53
53
|
model: `${config.model.provider}/${config.model.model}`,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
// Names only: each tool's description/parameters reach the model as its wire
|
|
55
|
+
// schema, so the prompt's Tools section lists just the roster (see prompts.ts).
|
|
56
|
+
tools: registry.list().map((tool) => ({ name: tool.name })),
|
|
57
57
|
git: args.git ?? null,
|
|
58
58
|
projectInstructions: args.projectInstructions ?? null,
|
|
59
59
|
recalledMemory: args.recalledMemory ?? null,
|
package/dist/agent/prompts.d.ts
CHANGED
package/dist/agent/prompts.js
CHANGED
|
@@ -74,8 +74,13 @@ function renderTools(tools) {
|
|
|
74
74
|
if (tools.length === 0) {
|
|
75
75
|
return "## Tools\nNo tools are available this session; respond in text only.";
|
|
76
76
|
}
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
// Names only. Each tool's description and parameter schema already reach the
|
|
78
|
+
// model as its wire spec (ToolRegistry.specs → the provider's tools field), so
|
|
79
|
+
// repeating the descriptions here just duplicated them on every first turn (and
|
|
80
|
+
// every turn on uncached providers). The roster and the "use them" nudge below
|
|
81
|
+
// are the parts the wire schema does NOT carry.
|
|
82
|
+
const names = tools.map((t) => t.name).join(", ");
|
|
83
|
+
return `## Tools\nYou have these tools available (each one's parameters and description are in its schema). Use them — don't ask the user to run things you can run yourself:\n${names}`;
|
|
79
84
|
}
|
|
80
85
|
/** Assemble the full system prompt for a session. */
|
|
81
86
|
export function buildSystemPrompt(ctx) {
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -187,7 +187,24 @@ export function runCommand() {
|
|
|
187
187
|
logger.info(t.muted(`mcp: server "${d.server}" declared in ${d.root} not loaded (primary-root MCP only this release)`));
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
|
-
|
|
190
|
+
// One-shot vs REPL turn ceiling. A one-shot run caps at
|
|
191
|
+
// `agent.maxIterationsOneShot` (default 40): a headless run can't be
|
|
192
|
+
// resumed by a human, so hitting the cap abandons the whole task — it
|
|
193
|
+
// needs headroom above the interactive cap to clear legit long work,
|
|
194
|
+
// while still bounding a runaway (and it now exits non-zero, see below).
|
|
195
|
+
// The REPL keeps `agent.maxIterations` (a soft checkpoint). Keyed on the
|
|
196
|
+
// one-shot MODE, never on TTY, so `cruxy run "task"` behaves the same
|
|
197
|
+
// piped into CI or run in a terminal.
|
|
198
|
+
const sessionConfig = interactive
|
|
199
|
+
? config
|
|
200
|
+
: {
|
|
201
|
+
...config,
|
|
202
|
+
agent: {
|
|
203
|
+
...config.agent,
|
|
204
|
+
maxIterations: config.agent.maxIterationsOneShot,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
const session = buildAgentSession(sessionConfig, apiKey, workspace, Boolean(process.stdin.isTTY), planMode, renderer, checkpoints, sandbox, hooksRunner, mcp.tools);
|
|
191
208
|
if (interactive) {
|
|
192
209
|
try {
|
|
193
210
|
await runInteractive(session, undefined, renderer, checkpoints, hookCommands);
|
|
@@ -253,7 +270,9 @@ export function runCommand() {
|
|
|
253
270
|
stop: result.stop,
|
|
254
271
|
iterations: result.iterations,
|
|
255
272
|
reason: result.stopReason,
|
|
256
|
-
|
|
273
|
+
// The effective one-shot cap (agent.maxIterationsOneShot), so the
|
|
274
|
+
// message names the limit the run actually hit.
|
|
275
|
+
maxIterations: sessionConfig.agent.maxIterations,
|
|
257
276
|
});
|
|
258
277
|
}
|
|
259
278
|
});
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -26,18 +26,33 @@ export declare const CruxyBackendConfigSchema: z.ZodObject<{
|
|
|
26
26
|
gatewayUrl?: string | undefined;
|
|
27
27
|
}>;
|
|
28
28
|
export declare const AgentConfigSchema: z.ZodObject<{
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Hard ceiling on agent loop turns per user turn — the interactive default.
|
|
31
|
+
* In the REPL this is a soft checkpoint: hitting it ends the turn and the
|
|
32
|
+
* human continues (or Ctrl-C's a runaway), so it can be generous.
|
|
33
|
+
*/
|
|
30
34
|
maxIterations: z.ZodDefault<z.ZodNumber>;
|
|
35
|
+
/**
|
|
36
|
+
* Turn ceiling for a ONE-SHOT `cruxy run "task"` (non-interactive). Set higher
|
|
37
|
+
* than {@link maxIterations} because a headless run has no human to resume it:
|
|
38
|
+
* hitting the cap abandons the whole task (fail-loud, non-zero exit), so it
|
|
39
|
+
* must clear the legit long tail (real "fix the failing tests" work runs
|
|
40
|
+
* ~10-25 turns) while still bounding a runaway. Not clamped to
|
|
41
|
+
* `maxIterations` — a user may deliberately set it lower or higher.
|
|
42
|
+
*/
|
|
43
|
+
maxIterationsOneShot: z.ZodDefault<z.ZodNumber>;
|
|
31
44
|
/** Skip per-action confirmation prompts. */
|
|
32
45
|
autoApprove: z.ZodDefault<z.ZodBoolean>;
|
|
33
46
|
/** Plan mode: propose a plan for approval before executing (C.31, opt-in). */
|
|
34
47
|
planMode: z.ZodDefault<z.ZodBoolean>;
|
|
35
48
|
}, "strict", z.ZodTypeAny, {
|
|
36
49
|
maxIterations: number;
|
|
50
|
+
maxIterationsOneShot: number;
|
|
37
51
|
autoApprove: boolean;
|
|
38
52
|
planMode: boolean;
|
|
39
53
|
}, {
|
|
40
54
|
maxIterations?: number | undefined;
|
|
55
|
+
maxIterationsOneShot?: number | undefined;
|
|
41
56
|
autoApprove?: boolean | undefined;
|
|
42
57
|
planMode?: boolean | undefined;
|
|
43
58
|
}>;
|
|
@@ -979,18 +994,33 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
979
994
|
gatewayUrl?: string | undefined;
|
|
980
995
|
}>>;
|
|
981
996
|
agent: z.ZodDefault<z.ZodObject<{
|
|
982
|
-
/**
|
|
997
|
+
/**
|
|
998
|
+
* Hard ceiling on agent loop turns per user turn — the interactive default.
|
|
999
|
+
* In the REPL this is a soft checkpoint: hitting it ends the turn and the
|
|
1000
|
+
* human continues (or Ctrl-C's a runaway), so it can be generous.
|
|
1001
|
+
*/
|
|
983
1002
|
maxIterations: z.ZodDefault<z.ZodNumber>;
|
|
1003
|
+
/**
|
|
1004
|
+
* Turn ceiling for a ONE-SHOT `cruxy run "task"` (non-interactive). Set higher
|
|
1005
|
+
* than {@link maxIterations} because a headless run has no human to resume it:
|
|
1006
|
+
* hitting the cap abandons the whole task (fail-loud, non-zero exit), so it
|
|
1007
|
+
* must clear the legit long tail (real "fix the failing tests" work runs
|
|
1008
|
+
* ~10-25 turns) while still bounding a runaway. Not clamped to
|
|
1009
|
+
* `maxIterations` — a user may deliberately set it lower or higher.
|
|
1010
|
+
*/
|
|
1011
|
+
maxIterationsOneShot: z.ZodDefault<z.ZodNumber>;
|
|
984
1012
|
/** Skip per-action confirmation prompts. */
|
|
985
1013
|
autoApprove: z.ZodDefault<z.ZodBoolean>;
|
|
986
1014
|
/** Plan mode: propose a plan for approval before executing (C.31, opt-in). */
|
|
987
1015
|
planMode: z.ZodDefault<z.ZodBoolean>;
|
|
988
1016
|
}, "strict", z.ZodTypeAny, {
|
|
989
1017
|
maxIterations: number;
|
|
1018
|
+
maxIterationsOneShot: number;
|
|
990
1019
|
autoApprove: boolean;
|
|
991
1020
|
planMode: boolean;
|
|
992
1021
|
}, {
|
|
993
1022
|
maxIterations?: number | undefined;
|
|
1023
|
+
maxIterationsOneShot?: number | undefined;
|
|
994
1024
|
autoApprove?: boolean | undefined;
|
|
995
1025
|
planMode?: boolean | undefined;
|
|
996
1026
|
}>>;
|
|
@@ -1737,6 +1767,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1737
1767
|
};
|
|
1738
1768
|
agent: {
|
|
1739
1769
|
maxIterations: number;
|
|
1770
|
+
maxIterationsOneShot: number;
|
|
1740
1771
|
autoApprove: boolean;
|
|
1741
1772
|
planMode: boolean;
|
|
1742
1773
|
};
|
|
@@ -1889,6 +1920,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1889
1920
|
} | undefined;
|
|
1890
1921
|
agent?: {
|
|
1891
1922
|
maxIterations?: number | undefined;
|
|
1923
|
+
maxIterationsOneShot?: number | undefined;
|
|
1892
1924
|
autoApprove?: boolean | undefined;
|
|
1893
1925
|
planMode?: boolean | undefined;
|
|
1894
1926
|
} | undefined;
|
package/dist/config/schema.js
CHANGED
|
@@ -26,8 +26,21 @@ export const CruxyBackendConfigSchema = z
|
|
|
26
26
|
.strict();
|
|
27
27
|
export const AgentConfigSchema = z
|
|
28
28
|
.object({
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Hard ceiling on agent loop turns per user turn — the interactive default.
|
|
31
|
+
* In the REPL this is a soft checkpoint: hitting it ends the turn and the
|
|
32
|
+
* human continues (or Ctrl-C's a runaway), so it can be generous.
|
|
33
|
+
*/
|
|
30
34
|
maxIterations: z.number().int().positive().default(25),
|
|
35
|
+
/**
|
|
36
|
+
* Turn ceiling for a ONE-SHOT `cruxy run "task"` (non-interactive). Set higher
|
|
37
|
+
* than {@link maxIterations} because a headless run has no human to resume it:
|
|
38
|
+
* hitting the cap abandons the whole task (fail-loud, non-zero exit), so it
|
|
39
|
+
* must clear the legit long tail (real "fix the failing tests" work runs
|
|
40
|
+
* ~10-25 turns) while still bounding a runaway. Not clamped to
|
|
41
|
+
* `maxIterations` — a user may deliberately set it lower or higher.
|
|
42
|
+
*/
|
|
43
|
+
maxIterationsOneShot: z.number().int().positive().default(40),
|
|
31
44
|
/** Skip per-action confirmation prompts. */
|
|
32
45
|
autoApprove: z.boolean().default(false),
|
|
33
46
|
/** Plan mode: propose a plan for approval before executing (C.31, opt-in). */
|
|
@@ -1235,7 +1235,7 @@ export function agentIncomplete(info) {
|
|
|
1235
1235
|
cause: `reached the turn limit${cap} after ${turns} without completing. The work so far is shown above.`,
|
|
1236
1236
|
nextSteps: [
|
|
1237
1237
|
"review the partial output above, then re-run with a narrower prompt",
|
|
1238
|
-
"raise `agent.maxIterations` if the task legitimately needs more turns",
|
|
1238
|
+
"raise `agent.maxIterationsOneShot` (one-shot) or `agent.maxIterations` if the task legitimately needs more turns",
|
|
1239
1239
|
],
|
|
1240
1240
|
meta,
|
|
1241
1241
|
});
|
package/dist/tools/file/glob.js
CHANGED
|
@@ -3,7 +3,22 @@ import { z } from "zod";
|
|
|
3
3
|
import { contextWorkspace, isEscapingPattern, labelPath, resolveReadRoots, } from "./paths.js";
|
|
4
4
|
/** Cap on returned paths — beyond this we truncate with a notice. */
|
|
5
5
|
const MAX_RESULTS = 200;
|
|
6
|
-
|
|
6
|
+
// Dependencies, VCS internals, and conventional compiled-output dirs. The last
|
|
7
|
+
// group keeps gitignored build artifacts (e.g. dist/mcp.js and its .d.ts) from
|
|
8
|
+
// crowding out the real source file a pattern like `**/*mcp*` is looking for.
|
|
9
|
+
// These are glob patterns, not gitignore rules: tinyglobby's `ignore` matches
|
|
10
|
+
// globs, so honoring an actual .gitignore would mean reusing the indexer's
|
|
11
|
+
// gitignore matcher per root — out of proportion for these ad-hoc tools, and the
|
|
12
|
+
// indexed search path already honors .gitignore.
|
|
13
|
+
const DEFAULT_IGNORE = [
|
|
14
|
+
"**/node_modules/**",
|
|
15
|
+
"**/.git/**",
|
|
16
|
+
"**/dist/**",
|
|
17
|
+
"**/build/**",
|
|
18
|
+
"**/coverage/**",
|
|
19
|
+
"**/.next/**",
|
|
20
|
+
"**/out/**",
|
|
21
|
+
];
|
|
7
22
|
/**
|
|
8
23
|
* Find files by glob pattern within the workspace. Read-only — no approval.
|
|
9
24
|
* The pattern is constrained to a root (no absolute or `..` patterns) so glob
|
|
@@ -17,7 +32,7 @@ const DEFAULT_IGNORE = ["**/node_modules/**", "**/.git/**"];
|
|
|
17
32
|
*/
|
|
18
33
|
export const globTool = {
|
|
19
34
|
name: "glob",
|
|
20
|
-
description: "Find files by glob pattern (e.g. 'src/**/*.ts') within the project, ignoring node_modules and .
|
|
35
|
+
description: "Find files by glob pattern (e.g. 'src/**/*.ts') within the project, ignoring node_modules, .git, and common build-output dirs (dist, build, coverage, .next, out). Returns paths relative to the project root.",
|
|
21
36
|
parameters: z.object({
|
|
22
37
|
pattern: z
|
|
23
38
|
.string()
|
|
@@ -28,7 +28,8 @@ declare const parameters: z.ZodObject<{
|
|
|
28
28
|
* run_command (which is platform-dependent and routes through the approval gate).
|
|
29
29
|
*
|
|
30
30
|
* Files are enumerated with the same glob mechanism as the `glob` tool (so
|
|
31
|
-
* node_modules
|
|
31
|
+
* node_modules, .git, and build-output dirs are always ignored), binary files
|
|
32
|
+
* are skipped, and the
|
|
32
33
|
* search is bounded by the same root boundary as every file tool.
|
|
33
34
|
*
|
|
34
35
|
* Multi-repo (C.26, Funnel B): with more than one declared root and no root-
|
|
@@ -10,7 +10,18 @@ const DEFAULT_MAX_RESULTS = 100;
|
|
|
10
10
|
const MAX_LINE_LENGTH = 200;
|
|
11
11
|
/** Bytes sniffed for a NUL to decide a file is binary and skip it. */
|
|
12
12
|
const BINARY_SNIFF_BYTES = 8 * 1024;
|
|
13
|
-
|
|
13
|
+
// Dependencies, VCS internals, and conventional compiled-output dirs — kept in
|
|
14
|
+
// sync with glob's DEFAULT_IGNORE so both file tools skip build artifacts (see
|
|
15
|
+
// glob.ts for why these are static globs rather than a parsed .gitignore).
|
|
16
|
+
const DEFAULT_IGNORE = [
|
|
17
|
+
"**/node_modules/**",
|
|
18
|
+
"**/.git/**",
|
|
19
|
+
"**/dist/**",
|
|
20
|
+
"**/build/**",
|
|
21
|
+
"**/coverage/**",
|
|
22
|
+
"**/.next/**",
|
|
23
|
+
"**/out/**",
|
|
24
|
+
];
|
|
14
25
|
const parameters = z.object({
|
|
15
26
|
pattern: z
|
|
16
27
|
.string()
|
|
@@ -49,7 +60,8 @@ function firstSegmentIsRoot(ws, p) {
|
|
|
49
60
|
* run_command (which is platform-dependent and routes through the approval gate).
|
|
50
61
|
*
|
|
51
62
|
* Files are enumerated with the same glob mechanism as the `glob` tool (so
|
|
52
|
-
* node_modules
|
|
63
|
+
* node_modules, .git, and build-output dirs are always ignored), binary files
|
|
64
|
+
* are skipped, and the
|
|
53
65
|
* search is bounded by the same root boundary as every file tool.
|
|
54
66
|
*
|
|
55
67
|
* Multi-repo (C.26, Funnel B): with more than one declared root and no root-
|
|
@@ -61,7 +73,7 @@ function firstSegmentIsRoot(ws, p) {
|
|
|
61
73
|
*/
|
|
62
74
|
export const grepFilesTool = {
|
|
63
75
|
name: "grep_files",
|
|
64
|
-
description: "Search file CONTENTS for a regular expression within the project (vs `glob`, which matches file NAMES). Returns matches as 'path:line: text', ignoring node_modules and .
|
|
76
|
+
description: "Search file CONTENTS for a regular expression within the project (vs `glob`, which matches file NAMES). Returns matches as 'path:line: text', ignoring node_modules, .git, and common build-output dirs (dist, build, coverage, .next, out). Read-only and requires no approval — prefer it over shelling out to grep/rg/find via run_command.",
|
|
65
77
|
parameters,
|
|
66
78
|
async execute(input, ctx) {
|
|
67
79
|
// Compile the regex up front; an invalid pattern is a clean failure, not a throw.
|