@cruxy/cli 0.22.1 → 0.24.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.d.ts +21 -2
- package/dist/agent/loop.js +21 -5
- package/dist/approval/classify.js +18 -0
- package/dist/approval/index.d.ts +1 -0
- package/dist/approval/index.js +1 -0
- package/dist/approval/mutex.d.ts +45 -0
- package/dist/approval/mutex.js +57 -0
- package/dist/approval/prompt.js +11 -0
- package/dist/checkpoint/gate.d.ts +65 -0
- package/dist/checkpoint/gate.js +86 -0
- package/dist/checkpoint/index.d.ts +2 -0
- package/dist/checkpoint/index.js +2 -0
- package/dist/checkpoint/service.d.ts +9 -0
- package/dist/checkpoint/service.js +20 -0
- package/dist/checkpoint/set-rollback.d.ts +51 -0
- package/dist/checkpoint/set-rollback.js +74 -0
- package/dist/cli/commands/rollback.d.ts +11 -6
- package/dist/cli/commands/rollback.js +93 -33
- package/dist/cli/commands/run.js +104 -21
- package/dist/cli/onboard.js +4 -1
- package/dist/cli/repl.d.ts +2 -2
- package/dist/cli/repl.js +39 -0
- package/dist/cli/session-factory.d.ts +27 -4
- package/dist/cli/session-factory.js +234 -58
- package/dist/config/schema.d.ts +24 -0
- package/dist/config/schema.js +9 -0
- package/dist/errors/constructors.d.ts +51 -0
- package/dist/errors/constructors.js +97 -0
- package/dist/errors/types.d.ts +28 -0
- package/dist/errors/types.js +38 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/router.d.ts +58 -0
- package/dist/hooks/router.js +136 -0
- package/dist/hooks/runner.d.ts +12 -0
- package/dist/hooks/runner.js +23 -1
- package/dist/indexing/retriever.d.ts +29 -0
- package/dist/indexing/retriever.js +26 -0
- package/dist/indexing/service.js +3 -1
- package/dist/indexing/types.d.ts +7 -0
- package/dist/lsp/tools/common.d.ts +34 -7
- package/dist/lsp/tools/common.js +33 -11
- package/dist/lsp/tools/find-definition.js +2 -2
- package/dist/lsp/tools/find-references.js +10 -4
- package/dist/lsp/tools/get-diagnostics.js +6 -4
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/sibling-banner.d.ts +25 -0
- package/dist/mcp/sibling-banner.js +34 -0
- package/dist/memory/recall.d.ts +24 -0
- package/dist/memory/recall.js +54 -0
- package/dist/memory/remember-tool.d.ts +3 -0
- package/dist/memory/remember-tool.js +11 -1
- package/dist/render/diff.js +42 -5
- package/dist/sandbox/policy.js +14 -5
- package/dist/sandbox/service.d.ts +8 -1
- package/dist/sandbox/service.js +4 -1
- package/dist/subagent/index.d.ts +1 -0
- package/dist/subagent/index.js +1 -0
- package/dist/subagent/orchestrator.d.ts +81 -1
- package/dist/subagent/orchestrator.js +204 -17
- package/dist/subagent/registry-scope.d.ts +13 -0
- package/dist/subagent/registry-scope.js +28 -2
- package/dist/subagent/semaphore.d.ts +27 -0
- package/dist/subagent/semaphore.js +56 -0
- package/dist/subagent/spawn-tool.d.ts +57 -0
- package/dist/subagent/spawn-tool.js +104 -9
- package/dist/subagent/types.d.ts +17 -2
- package/dist/testing/run-tests-tool.js +3 -0
- package/dist/tools/create-pull-request.d.ts +3 -0
- package/dist/tools/create-pull-request.js +50 -4
- package/dist/tools/file/apply-patch.js +2 -2
- package/dist/tools/file/edit-file.js +2 -2
- package/dist/tools/file/glob.d.ts +9 -2
- package/dist/tools/file/glob.js +73 -19
- package/dist/tools/file/grep-files.d.ts +12 -2
- package/dist/tools/file/grep-files.js +113 -38
- package/dist/tools/file/paths.d.ts +121 -9
- package/dist/tools/file/paths.js +164 -10
- package/dist/tools/file/read-file.js +2 -2
- package/dist/tools/file/write-file.js +2 -2
- package/dist/tools/git-status.d.ts +8 -1
- package/dist/tools/git-status.js +43 -11
- package/dist/tools/list-files.d.ts +9 -3
- package/dist/tools/list-files.js +48 -13
- package/dist/tools/search-codebase.d.ts +10 -0
- package/dist/tools/search-codebase.js +117 -14
- package/dist/tools/shell/exec.js +43 -4
- package/dist/tools/types.d.ts +74 -1
- package/dist/vcs/git.d.ts +8 -0
- package/dist/vcs/git.js +14 -0
- package/dist/vcs/github.d.ts +7 -1
- package/dist/vcs/github.js +10 -1
- package/dist/vcs/service.d.ts +8 -0
- package/dist/vcs/service.js +33 -1
- package/dist/vcs/types.d.ts +18 -2
- package/dist/workspace/add-root.d.ts +27 -0
- package/dist/workspace/add-root.js +16 -0
- package/dist/workspace/index.d.ts +2 -1
- package/dist/workspace/index.js +2 -1
- package/dist/workspace/workspace.d.ts +9 -4
- package/dist/workspace/workspace.js +9 -4
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { CruxyError, ErrorCode } from "../errors/index.js";
|
|
3
|
-
import { SPAWN_SUBAGENT_TOOL_NAME } from "./registry-scope.js";
|
|
3
|
+
import { SPAWN_SUBAGENT_TOOL_NAME, SPAWN_SUBAGENTS_TOOL_NAME, } from "./registry-scope.js";
|
|
4
4
|
/**
|
|
5
5
|
* The `spawn_subagent` tool (C.14): the parent-facing seam for delegation. A
|
|
6
6
|
* normal tool on the same loop as everything else — no hidden control flow.
|
|
@@ -34,9 +34,9 @@ const parameters = z.object({
|
|
|
34
34
|
.optional()
|
|
35
35
|
.describe("Cap on the subagent's total tokens (clamped to the configured ceiling)."),
|
|
36
36
|
});
|
|
37
|
-
/** The compact
|
|
38
|
-
function
|
|
39
|
-
return
|
|
37
|
+
/** The compact, transcript-free object fed back to the parent model. */
|
|
38
|
+
function resultPayload(result) {
|
|
39
|
+
return {
|
|
40
40
|
status: result.status,
|
|
41
41
|
summary: result.summary,
|
|
42
42
|
...(result.artifacts ? { artifacts: result.artifacts } : {}),
|
|
@@ -46,7 +46,18 @@ function renderResult(result) {
|
|
|
46
46
|
input: result.usage.input_tokens,
|
|
47
47
|
output: result.usage.output_tokens,
|
|
48
48
|
},
|
|
49
|
-
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** The compact wire shape fed back to the parent model. */
|
|
52
|
+
function renderResult(result) {
|
|
53
|
+
return JSON.stringify(resultPayload(result));
|
|
54
|
+
}
|
|
55
|
+
/** A coded, actionable tool-error string for an error the MODEL should correct
|
|
56
|
+
* (scope overlap, depth exceeded, tool scoping) — never the raw stack. */
|
|
57
|
+
function toolErrorMessage(err) {
|
|
58
|
+
return CruxyError.is(err)
|
|
59
|
+
? `${err.code}: ${err.title}${err.cause ? ` — ${err.cause}` : ""}`
|
|
60
|
+
: err.message;
|
|
50
61
|
}
|
|
51
62
|
/** Build a `spawn_subagent` tool bound to `orchestrator` at `depth`. */
|
|
52
63
|
export function makeSpawnSubagentTool(orchestrator, depth) {
|
|
@@ -79,10 +90,7 @@ export function makeSpawnSubagentTool(orchestrator, depth) {
|
|
|
79
90
|
}
|
|
80
91
|
// Depth-exceed and scope violations are the model's to correct: feed
|
|
81
92
|
// the coded, actionable message back as a tool error.
|
|
82
|
-
|
|
83
|
-
? `${err.code}: ${err.title}${err.cause ? ` — ${err.cause}` : ""}`
|
|
84
|
-
: err.message;
|
|
85
|
-
return { ok: false, error: message };
|
|
93
|
+
return { ok: false, error: toolErrorMessage(err) };
|
|
86
94
|
}
|
|
87
95
|
// A failed child is an is_error result (strong signal), still structured;
|
|
88
96
|
// budget-exceeded is informational — a partial result, not an error.
|
|
@@ -92,3 +100,90 @@ export function makeSpawnSubagentTool(orchestrator, depth) {
|
|
|
92
100
|
},
|
|
93
101
|
};
|
|
94
102
|
}
|
|
103
|
+
// ── parallel fan-out (C.33) ───────────────────────────────────────────────────
|
|
104
|
+
/** One child's spec inside a `spawn_subagents` batch. Mirrors the singular
|
|
105
|
+
* tool's shape plus `root` — the C.33 disjoint-scope unit. */
|
|
106
|
+
const batchChild = z.object({
|
|
107
|
+
task: z
|
|
108
|
+
.string()
|
|
109
|
+
.min(1)
|
|
110
|
+
.describe("The complete, self-contained subtask. The subagent starts with NO context " +
|
|
111
|
+
"beyond this text — include every path, constraint, and expected output."),
|
|
112
|
+
tools: z
|
|
113
|
+
.array(z.string().min(1))
|
|
114
|
+
.nonempty()
|
|
115
|
+
.optional()
|
|
116
|
+
.describe("Tool names to grant this child — a subset of your own. Omit for read-only. " +
|
|
117
|
+
"Two children that both hold write/shell tools MUST target different `root`s."),
|
|
118
|
+
root: z
|
|
119
|
+
.string()
|
|
120
|
+
.min(1)
|
|
121
|
+
.optional()
|
|
122
|
+
.describe("Workspace root name (from your Environment) to scope this child to: its " +
|
|
123
|
+
"writes are confined there. Required to make two writing children disjoint; " +
|
|
124
|
+
"omit for read-only children or a single-root workspace."),
|
|
125
|
+
maxIterations: z.number().int().positive().optional(),
|
|
126
|
+
maxTokens: z.number().int().positive().optional(),
|
|
127
|
+
});
|
|
128
|
+
const batchParameters = z.object({
|
|
129
|
+
tasks: z
|
|
130
|
+
.array(batchChild)
|
|
131
|
+
.nonempty()
|
|
132
|
+
.describe("The independent subtasks to run CONCURRENTLY. Results come back in this " +
|
|
133
|
+
"same order. Bounded by subagent.maxConcurrency; excess children queue."),
|
|
134
|
+
});
|
|
135
|
+
/** Map a validated batch child to a {@link SubagentSpec}. */
|
|
136
|
+
function toSpec(child) {
|
|
137
|
+
const budget = {
|
|
138
|
+
...(child.maxIterations !== undefined
|
|
139
|
+
? { maxIterations: child.maxIterations }
|
|
140
|
+
: {}),
|
|
141
|
+
...(child.maxTokens !== undefined ? { maxTokens: child.maxTokens } : {}),
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
task: child.task,
|
|
145
|
+
...(child.tools ? { tools: child.tools } : {}),
|
|
146
|
+
...(child.root !== undefined ? { root: child.root } : {}),
|
|
147
|
+
budget,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Build the `spawn_subagents` tool (C.33) — the PARALLEL fan-out seam, bound to
|
|
152
|
+
* `depth`. One tool call dispatches N independent, internally-sequential children
|
|
153
|
+
* concurrently (JC-A) under the shared concurrency semaphore, and returns their
|
|
154
|
+
* results IN REQUEST ORDER. A DEPTH-0 capability: it is never granted to a child,
|
|
155
|
+
* so fan-out never nests (which keeps the semaphore deadlock-free).
|
|
156
|
+
*/
|
|
157
|
+
export function makeSpawnSubagentsTool(orchestrator, depth) {
|
|
158
|
+
return {
|
|
159
|
+
name: SPAWN_SUBAGENTS_TOOL_NAME,
|
|
160
|
+
description: "Delegate SEVERAL independent subtasks to scoped subagents that run in PARALLEL, " +
|
|
161
|
+
"each with its own fresh context, restricted toolset, and hard budget. Returns a " +
|
|
162
|
+
"structured result per child, in the same order as `tasks` — transcripts discarded. " +
|
|
163
|
+
"Use when subtasks don't depend on each other (e.g. investigate N areas at once). " +
|
|
164
|
+
"For children that WRITE, give each a distinct `root`; overlapping write scope is " +
|
|
165
|
+
"refused. For a single subtask, use spawn_subagent instead.",
|
|
166
|
+
parameters: batchParameters,
|
|
167
|
+
async execute(input) {
|
|
168
|
+
let results;
|
|
169
|
+
try {
|
|
170
|
+
results = await orchestrator.spawnMany(input.tasks.map(toSpec), depth);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
// Non-interactive default-deny propagates to the boundary (U.3).
|
|
174
|
+
if (CruxyError.is(err) && err.code === ErrorCode.ApprovalRequired) {
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
// Scope overlap / depth-exceed are the model's to correct — coded error.
|
|
178
|
+
return { ok: false, error: toolErrorMessage(err) };
|
|
179
|
+
}
|
|
180
|
+
// Partial results are honest, not an error: the array carries each child's
|
|
181
|
+
// own status (done / failed / budget-exceeded / cancelled). The batch call
|
|
182
|
+
// succeeds as long as it was dispatched — the parent reasons over the mix.
|
|
183
|
+
return {
|
|
184
|
+
ok: true,
|
|
185
|
+
output: JSON.stringify(results.map(resultPayload)),
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
package/dist/subagent/types.d.ts
CHANGED
|
@@ -7,8 +7,13 @@ import type { TaskClass } from "../routing/index.js";
|
|
|
7
7
|
* result and discards its transcript. The parent reasons over the result only;
|
|
8
8
|
* context isolation is the whole point.
|
|
9
9
|
*/
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Why a subagent run ended. Every path returns a result — never a hang.
|
|
12
|
+
* `cancelled` (C.33) is a parallel-fan-out outcome: a fatal sibling failure or
|
|
13
|
+
* Ctrl-C aborted this child before it finished — recorded honestly, never
|
|
14
|
+
* dressed up as `done`.
|
|
15
|
+
*/
|
|
16
|
+
export type SubagentStatus = "done" | "budget-exceeded" | "failed" | "cancelled";
|
|
12
17
|
/**
|
|
13
18
|
* Hard caps a subagent runs under. `maxIterations` and `maxTokens` are always
|
|
14
19
|
* finite — a subagent is bounded by construction; `timeoutMs` is an optional
|
|
@@ -41,6 +46,16 @@ export interface SubagentSpec {
|
|
|
41
46
|
* declaration at the spawn call site — not something the router guesses.
|
|
42
47
|
*/
|
|
43
48
|
taskClass?: TaskClass;
|
|
49
|
+
/**
|
|
50
|
+
* The workspace root (by exact name, C.26) this child is scoped to (C.33).
|
|
51
|
+
* When set, the child's cwd and confinement narrow to that ONE root, so its
|
|
52
|
+
* writes land there and nowhere else — the unit of the parallel fan-out's
|
|
53
|
+
* disjoint-scope guarantee: two writing children must name distinct roots or
|
|
54
|
+
* the batch is refused pre-dispatch. Omitted → the child inherits the full
|
|
55
|
+
* session workspace (read-only fan-out, or a single-root session), unchanged
|
|
56
|
+
* from C.14.
|
|
57
|
+
*/
|
|
58
|
+
root?: string;
|
|
44
59
|
}
|
|
45
60
|
/**
|
|
46
61
|
* What the parent gets back — compact structured data, never the transcript.
|
|
@@ -97,6 +97,9 @@ export function makeRunTestsTool(deps = {}) {
|
|
|
97
97
|
const decision = await ctx.requestApproval({
|
|
98
98
|
kind: "test",
|
|
99
99
|
command: resolved.command,
|
|
100
|
+
// C.26 attribution seam (JC-β): primary root name for Steps 4/5. Tests are
|
|
101
|
+
// primary-only this release; the gate hard-attributes to the primary.
|
|
102
|
+
root: ctx.workspace.primary().name,
|
|
100
103
|
});
|
|
101
104
|
if (!decision.allow) {
|
|
102
105
|
return {
|
|
@@ -8,14 +8,17 @@ import type { Tool } from "./types.js";
|
|
|
8
8
|
* never prompted for or stored.
|
|
9
9
|
*/
|
|
10
10
|
declare const parameters: z.ZodObject<{
|
|
11
|
+
root: z.ZodOptional<z.ZodString>;
|
|
11
12
|
title: z.ZodOptional<z.ZodString>;
|
|
12
13
|
body: z.ZodOptional<z.ZodString>;
|
|
13
14
|
base: z.ZodOptional<z.ZodString>;
|
|
14
15
|
}, "strip", z.ZodTypeAny, {
|
|
16
|
+
root?: string | undefined;
|
|
15
17
|
body?: string | undefined;
|
|
16
18
|
base?: string | undefined;
|
|
17
19
|
title?: string | undefined;
|
|
18
20
|
}, {
|
|
21
|
+
root?: string | undefined;
|
|
19
22
|
body?: string | undefined;
|
|
20
23
|
base?: string | undefined;
|
|
21
24
|
title?: string | undefined;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { z } from "zod";
|
|
2
|
-
import { CruxyError, ErrorCode } from "../errors/index.js";
|
|
3
|
-
import { createForgeProvider, createPrService, fillContent, loadCommitGuidance, resolveForgeToken, } from "../vcs/index.js";
|
|
3
|
+
import { CruxyError, ErrorCode, vcsCrossRoot } from "../errors/index.js";
|
|
4
|
+
import { createForgeProvider, createPrService, fillContent, gitToplevel, loadCommitGuidance, resolveForgeToken, } from "../vcs/index.js";
|
|
5
|
+
import { contextWorkspace, resolveMutationRoot } from "./file/paths.js";
|
|
4
6
|
/**
|
|
5
7
|
* Open a pull request from the agent's work (C.15): branch → conventional commit
|
|
6
8
|
* → push → open PR, all behind the one U.3 approval. The model authors `title`
|
|
@@ -9,6 +11,12 @@ import { createForgeProvider, createPrService, fillContent, loadCommitGuidance,
|
|
|
9
11
|
* never prompted for or stored.
|
|
10
12
|
*/
|
|
11
13
|
const parameters = z.object({
|
|
14
|
+
root: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Workspace root to open the pull request for, by its declared name. Required " +
|
|
18
|
+
"when the session declares more than one root (a PR targets exactly one " +
|
|
19
|
+
"repository, so it must not be guessed); optional in a single-root session."),
|
|
12
20
|
title: z
|
|
13
21
|
.string()
|
|
14
22
|
.optional()
|
|
@@ -31,11 +39,18 @@ export const createPullRequestTool = {
|
|
|
31
39
|
parameters,
|
|
32
40
|
async execute(input, ctx) {
|
|
33
41
|
try {
|
|
42
|
+
// ⚖︎#11 / JC-1: commit to exactly one root first. Multi-root with no `root`
|
|
43
|
+
// refuses (ROOT_AMBIGUOUS) BEFORE any git runs; single-root defaults to the
|
|
44
|
+
// primary (byte-identical to pre-C.26). A cross-root PR (a sibling root in the
|
|
45
|
+
// same repo) is refused too — a PR is a single-repo artifact (JC-3).
|
|
46
|
+
const selected = resolveMutationRoot(ctx, { root: input.root }, "a pull request");
|
|
47
|
+
assertSingleRepoRoot(contextWorkspace(ctx), selected);
|
|
34
48
|
const token = resolveForgeToken();
|
|
35
49
|
const forge = createForgeProvider(token);
|
|
36
|
-
const guidance = await loadCommitGuidance(
|
|
50
|
+
const guidance = await loadCommitGuidance(selected.absPath);
|
|
37
51
|
const service = createPrService({
|
|
38
|
-
cwd:
|
|
52
|
+
cwd: selected.absPath,
|
|
53
|
+
rootName: selected.name,
|
|
39
54
|
config: ctx.config,
|
|
40
55
|
forge,
|
|
41
56
|
requestApproval: ctx.requestApproval,
|
|
@@ -75,6 +90,37 @@ export const createPullRequestTool = {
|
|
|
75
90
|
}
|
|
76
91
|
},
|
|
77
92
|
};
|
|
93
|
+
/**
|
|
94
|
+
* Refuse a PR whose selected root shares one git working tree with a SIBLING
|
|
95
|
+
* declared root (JC-3). Filesystem-overlap refusal (ROOT_OVERLAP) only catches
|
|
96
|
+
* nesting; two non-overlapping roots — `packages/a` and `packages/b` under one
|
|
97
|
+
* `.git` — pass that check yet still share a repo. Committing in one would
|
|
98
|
+
* `git add -A` the other's changes, so the PR would span both. We detect it by
|
|
99
|
+
* comparing git top-levels: same top-level ⇒ same repo ⇒ refuse (naming both).
|
|
100
|
+
*
|
|
101
|
+
* If the selected root is not in a git repo, there is nothing to compare — the PR
|
|
102
|
+
* flow fails loudly later at `getRepoInfo`/`currentBranch`. Read-only `rev-parse`
|
|
103
|
+
* only; this runs after selection, so a no-root multi-root call still spawns zero
|
|
104
|
+
* git before its ROOT_AMBIGUOUS refusal.
|
|
105
|
+
*/
|
|
106
|
+
function assertSingleRepoRoot(ws, selected) {
|
|
107
|
+
if (!ws.isMultiRoot)
|
|
108
|
+
return;
|
|
109
|
+
const topSelected = gitToplevel(selected.absPath);
|
|
110
|
+
if (topSelected === null)
|
|
111
|
+
return;
|
|
112
|
+
const canonical = path.resolve(topSelected);
|
|
113
|
+
for (const sibling of ws.roots()) {
|
|
114
|
+
if (sibling.name === selected.name)
|
|
115
|
+
continue;
|
|
116
|
+
const topSibling = gitToplevel(sibling.absPath);
|
|
117
|
+
if (topSibling === null)
|
|
118
|
+
continue;
|
|
119
|
+
if (path.resolve(topSibling) === canonical) {
|
|
120
|
+
throw vcsCrossRoot(selected.name, sibling.name, canonical);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
78
124
|
/** One-line, model-readable rendering of a coded error + its next steps. */
|
|
79
125
|
function formatCoded(err) {
|
|
80
126
|
const head = err.cause ? `${err.title} — ${err.cause}` : err.title;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveToolPath } from "./paths.js";
|
|
5
5
|
import { countOccurrences } from "./edit-file.js";
|
|
6
6
|
/** How many leading lines of a created file the approval preview shows. */
|
|
7
7
|
const PREVIEW_LINES = 20;
|
|
@@ -62,7 +62,7 @@ export const applyPatchTool = {
|
|
|
62
62
|
const op = input.operations[i];
|
|
63
63
|
let abs;
|
|
64
64
|
try {
|
|
65
|
-
abs = await
|
|
65
|
+
({ abs } = await resolveToolPath(ctx, { path: op.path }, { deferNonPrimaryWrite: true }));
|
|
66
66
|
}
|
|
67
67
|
catch (err) {
|
|
68
68
|
return { ok: false, error: opError(i, op, err.message) };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveToolPath } from "./paths.js";
|
|
4
4
|
/** Count non-overlapping exact occurrences of `needle` in `haystack`. */
|
|
5
5
|
export function countOccurrences(haystack, needle) {
|
|
6
6
|
let count = 0;
|
|
@@ -32,7 +32,7 @@ export const editFileTool = {
|
|
|
32
32
|
async execute(input, ctx) {
|
|
33
33
|
let abs;
|
|
34
34
|
try {
|
|
35
|
-
abs = await
|
|
35
|
+
({ abs } = await resolveToolPath(ctx, { path: input.path }, { deferNonPrimaryWrite: true }));
|
|
36
36
|
}
|
|
37
37
|
catch (err) {
|
|
38
38
|
return { ok: false, error: err.message };
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { Tool } from "../types.js";
|
|
3
3
|
/**
|
|
4
|
-
* Find files by glob pattern within the
|
|
5
|
-
* The pattern is constrained to
|
|
4
|
+
* Find files by glob pattern within the workspace. Read-only — no approval.
|
|
5
|
+
* The pattern is constrained to a root (no absolute or `..` patterns) so glob
|
|
6
6
|
* can't reach outside, consistent with the other file tools.
|
|
7
|
+
*
|
|
8
|
+
* Multi-repo (C.26, Funnel B): with more than one declared root, glob fans every
|
|
9
|
+
* root — an independent walk rooted at each root's own `absPath` — and labels each
|
|
10
|
+
* match `‹root› ▸ path`. There is no path to confine here; the boundary is which
|
|
11
|
+
* root's `absPath` each walk starts from, so a match can only ever come from the
|
|
12
|
+
* root its label names. A `root` argument scopes the walk to that one root.
|
|
7
13
|
*/
|
|
8
14
|
export declare const globTool: Tool<z.ZodObject<{
|
|
9
15
|
pattern: z.ZodString;
|
|
16
|
+
root: z.ZodOptional<z.ZodString>;
|
|
10
17
|
}>>;
|
package/dist/tools/file/glob.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import { glob } from "tinyglobby";
|
|
3
2
|
import { z } from "zod";
|
|
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
|
const DEFAULT_IGNORE = ["**/node_modules/**", "**/.git/**"];
|
|
7
7
|
/**
|
|
8
|
-
* Find files by glob pattern within the
|
|
9
|
-
* The pattern is constrained to
|
|
8
|
+
* Find files by glob pattern within the workspace. Read-only — no approval.
|
|
9
|
+
* The pattern is constrained to a root (no absolute or `..` patterns) so glob
|
|
10
10
|
* can't reach outside, consistent with the other file tools.
|
|
11
|
+
*
|
|
12
|
+
* Multi-repo (C.26, Funnel B): with more than one declared root, glob fans every
|
|
13
|
+
* root — an independent walk rooted at each root's own `absPath` — and labels each
|
|
14
|
+
* match `‹root› ▸ path`. There is no path to confine here; the boundary is which
|
|
15
|
+
* root's `absPath` each walk starts from, so a match can only ever come from the
|
|
16
|
+
* root its label names. A `root` argument scopes the walk to that one root.
|
|
11
17
|
*/
|
|
12
18
|
export const globTool = {
|
|
13
19
|
name: "glob",
|
|
@@ -16,37 +22,85 @@ export const globTool = {
|
|
|
16
22
|
pattern: z
|
|
17
23
|
.string()
|
|
18
24
|
.describe("Glob pattern, relative to the project root (e.g. 'src/**/*.ts')."),
|
|
25
|
+
root: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("In a multi-repo session, restrict the search to a single declared root by name. Omit to search every root and label each match with its root."),
|
|
19
29
|
}),
|
|
20
30
|
async execute(input, ctx) {
|
|
21
31
|
const pattern = input.pattern;
|
|
22
|
-
|
|
32
|
+
// Walk-rooted tool: the pattern is not a resolvable path, so confinement is
|
|
33
|
+
// (a) rejecting a pattern that could escape (shared with grep_files) and
|
|
34
|
+
// (b) rooting each walk at a declared root's absPath, never a bare ctx.cwd.
|
|
35
|
+
if (isEscapingPattern(pattern)) {
|
|
23
36
|
return {
|
|
24
37
|
ok: false,
|
|
25
38
|
error: "pattern must be relative to the project root (no '..' or absolute paths)",
|
|
26
39
|
};
|
|
27
40
|
}
|
|
41
|
+
const ws = contextWorkspace(ctx);
|
|
42
|
+
let roots;
|
|
43
|
+
try {
|
|
44
|
+
roots = resolveReadRoots(ctx, { root: input.root });
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
return { ok: false, error: err.message };
|
|
48
|
+
}
|
|
28
49
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
50
|
+
// Fan: one independent walk per root; each match is attributed to (and, in
|
|
51
|
+
// multi-root, labelled with) the root whose absPath produced it.
|
|
52
|
+
const rows = [];
|
|
53
|
+
const totalByRoot = new Map();
|
|
54
|
+
for (const root of roots) {
|
|
55
|
+
const matches = await glob(pattern, {
|
|
56
|
+
cwd: root.absPath,
|
|
57
|
+
ignore: DEFAULT_IGNORE,
|
|
58
|
+
onlyFiles: true,
|
|
59
|
+
dot: false,
|
|
60
|
+
});
|
|
61
|
+
matches.sort();
|
|
62
|
+
totalByRoot.set(root.name, matches.length);
|
|
63
|
+
for (const m of matches) {
|
|
64
|
+
rows.push({
|
|
65
|
+
root: root.name,
|
|
66
|
+
line: labelPath(root, m, ws.isMultiRoot),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (rows.length === 0) {
|
|
37
71
|
return { ok: true, output: "(no matches)" };
|
|
38
72
|
}
|
|
39
|
-
if (
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
73
|
+
if (rows.length > MAX_RESULTS) {
|
|
74
|
+
const shownRows = rows.slice(0, MAX_RESULTS);
|
|
75
|
+
const shown = shownRows.map((r) => r.line).join("\n");
|
|
76
|
+
const footer = ws.isMultiRoot
|
|
77
|
+
? truncationFooter(rows.length, shownRows, totalByRoot)
|
|
78
|
+
: `[truncated: showing ${MAX_RESULTS} of ${rows.length} matches]`;
|
|
79
|
+
return { ok: true, output: `${shown}\n\n${footer}` };
|
|
45
80
|
}
|
|
46
|
-
return { ok: true, output:
|
|
81
|
+
return { ok: true, output: rows.map((r) => r.line).join("\n") };
|
|
47
82
|
}
|
|
48
83
|
catch (err) {
|
|
49
84
|
return { ok: false, error: err.message };
|
|
50
85
|
}
|
|
51
86
|
},
|
|
52
87
|
};
|
|
88
|
+
/**
|
|
89
|
+
* Truncation footer for a fanned walk: the overall count plus a per-root note for
|
|
90
|
+
* every root whose matches the cap dropped — so a root cut off by the cap is
|
|
91
|
+
* named, never silently read as having no files.
|
|
92
|
+
*/
|
|
93
|
+
function truncationFooter(total, shownRows, totalByRoot) {
|
|
94
|
+
const shownByRoot = new Map();
|
|
95
|
+
for (const r of shownRows) {
|
|
96
|
+
shownByRoot.set(r.root, (shownByRoot.get(r.root) ?? 0) + 1);
|
|
97
|
+
}
|
|
98
|
+
const lines = [`[truncated: showing ${MAX_RESULTS} of ${total} matches]`];
|
|
99
|
+
for (const [name, count] of totalByRoot) {
|
|
100
|
+
const dropped = count - (shownByRoot.get(name) ?? 0);
|
|
101
|
+
if (dropped > 0) {
|
|
102
|
+
lines.push(`— ${name}: ${dropped} more not shown`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
@@ -6,27 +6,37 @@ declare const parameters: z.ZodObject<{
|
|
|
6
6
|
glob: z.ZodOptional<z.ZodString>;
|
|
7
7
|
ignoreCase: z.ZodOptional<z.ZodBoolean>;
|
|
8
8
|
maxResults: z.ZodOptional<z.ZodNumber>;
|
|
9
|
+
root: z.ZodOptional<z.ZodString>;
|
|
9
10
|
}, "strip", z.ZodTypeAny, {
|
|
10
11
|
pattern: string;
|
|
11
12
|
path?: string | undefined;
|
|
13
|
+
root?: string | undefined;
|
|
12
14
|
maxResults?: number | undefined;
|
|
13
15
|
glob?: string | undefined;
|
|
14
16
|
ignoreCase?: boolean | undefined;
|
|
15
17
|
}, {
|
|
16
18
|
pattern: string;
|
|
17
19
|
path?: string | undefined;
|
|
20
|
+
root?: string | undefined;
|
|
18
21
|
maxResults?: number | undefined;
|
|
19
22
|
glob?: string | undefined;
|
|
20
23
|
ignoreCase?: boolean | undefined;
|
|
21
24
|
}>;
|
|
22
25
|
/**
|
|
23
|
-
* Search file *contents* for a regex within the
|
|
26
|
+
* Search file *contents* for a regex within the workspace. Read-only — no
|
|
24
27
|
* approval — so the model should prefer this over shelling out to grep/rg via
|
|
25
28
|
* run_command (which is platform-dependent and routes through the approval gate).
|
|
26
29
|
*
|
|
27
30
|
* Files are enumerated with the same glob mechanism as the `glob` tool (so
|
|
28
31
|
* node_modules and .git are always ignored), binary files are skipped, and the
|
|
29
|
-
* search is bounded by the
|
|
32
|
+
* search is bounded by the same root boundary as every file tool.
|
|
33
|
+
*
|
|
34
|
+
* Multi-repo (C.26, Funnel B): with more than one declared root and no root-
|
|
35
|
+
* selecting argument, the search FANS every root — a bare relative `path` is
|
|
36
|
+
* applied under each root (⚖︎JC-6) — and labels each match `‹root› ▸ path`. A
|
|
37
|
+
* `root` argument, or an absolute / `‹root›/…`-prefixed `path`, scopes it to that
|
|
38
|
+
* one root. Each walk starts at exactly one confined root directory, so a match
|
|
39
|
+
* can only come from the root its label names.
|
|
30
40
|
*/
|
|
31
41
|
export declare const grepFilesTool: Tool<typeof parameters>;
|
|
32
42
|
export {};
|