@cruxy/cli 0.23.0 → 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/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/checkpoint/service.d.ts +9 -0
- package/dist/checkpoint/service.js +20 -0
- package/dist/cli/commands/run.js +50 -16
- package/dist/cli/onboard.js +2 -2
- package/dist/cli/repl.js +39 -0
- package/dist/cli/session-factory.d.ts +23 -1
- package/dist/cli/session-factory.js +137 -47
- package/dist/config/schema.d.ts +24 -0
- package/dist/config/schema.js +9 -0
- package/dist/errors/constructors.d.ts +23 -0
- package/dist/errors/constructors.js +38 -0
- package/dist/errors/types.d.ts +8 -0
- package/dist/errors/types.js +12 -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/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/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 +67 -2
- package/dist/subagent/orchestrator.js +203 -18
- 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 +1 -1
- package/dist/tools/file/paths.d.ts +5 -6
- package/dist/tools/file/paths.js +7 -8
- package/dist/tools/shell/exec.js +36 -4
- package/dist/tools/types.d.ts +16 -5
- 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.
|
|
@@ -99,7 +99,7 @@ export function makeRunTestsTool(deps = {}) {
|
|
|
99
99
|
command: resolved.command,
|
|
100
100
|
// C.26 attribution seam (JC-β): primary root name for Steps 4/5. Tests are
|
|
101
101
|
// primary-only this release; the gate hard-attributes to the primary.
|
|
102
|
-
root: ctx.workspace
|
|
102
|
+
root: ctx.workspace.primary().name,
|
|
103
103
|
});
|
|
104
104
|
if (!decision.allow) {
|
|
105
105
|
return {
|
|
@@ -9,12 +9,11 @@ import type { ToolContext } from "../types.js";
|
|
|
9
9
|
*/
|
|
10
10
|
export { PathEscapeError };
|
|
11
11
|
/**
|
|
12
|
-
* The workspace this context acts in.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* `resolveInRoot` confined to.
|
|
12
|
+
* The workspace this context acts in. `ctx.workspace` is a required field (C.26
|
|
13
|
+
* Step 6), so this is a plain accessor — there is no single-root fallback to
|
|
14
|
+
* synthesize, because no code path can reach a path-taking tool without a
|
|
15
|
+
* workspace (the type forbids the omission; the `no-split-brain` guard forbids a
|
|
16
|
+
* mid-subsystem synthesis). Kept as the one named seam every funnel calls.
|
|
18
17
|
*/
|
|
19
18
|
export declare function contextWorkspace(ctx: ToolContext): Workspace;
|
|
20
19
|
/**
|
package/dist/tools/file/paths.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { multirootWriteDeferred, rootAmbiguous } from "../../errors/index.js";
|
|
3
|
-
import { confineToRoot, PathEscapeError, selectRoot,
|
|
3
|
+
import { confineToRoot, PathEscapeError, selectRoot, } from "../../workspace/index.js";
|
|
4
4
|
/**
|
|
5
5
|
* Path confinement for file tools (C.26). The confinement kernel itself lives in
|
|
6
6
|
* `src/workspace` ({@link confineToRoot}); this module is the tool-facing funnel
|
|
@@ -10,15 +10,14 @@ import { confineToRoot, PathEscapeError, selectRoot, singleRootWorkspace, } from
|
|
|
10
10
|
*/
|
|
11
11
|
export { PathEscapeError };
|
|
12
12
|
/**
|
|
13
|
-
* The workspace this context acts in.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* `resolveInRoot` confined to.
|
|
13
|
+
* The workspace this context acts in. `ctx.workspace` is a required field (C.26
|
|
14
|
+
* Step 6), so this is a plain accessor — there is no single-root fallback to
|
|
15
|
+
* synthesize, because no code path can reach a path-taking tool without a
|
|
16
|
+
* workspace (the type forbids the omission; the `no-split-brain` guard forbids a
|
|
17
|
+
* mid-subsystem synthesis). Kept as the one named seam every funnel calls.
|
|
19
18
|
*/
|
|
20
19
|
export function contextWorkspace(ctx) {
|
|
21
|
-
return ctx.workspace
|
|
20
|
+
return ctx.workspace;
|
|
22
21
|
}
|
|
23
22
|
/**
|
|
24
23
|
* Resolve a tool-supplied path against the workspace and prove it stays inside the
|
package/dist/tools/shell/exec.js
CHANGED
|
@@ -13,7 +13,7 @@ export async function runGatedShell(command, ctx) {
|
|
|
13
13
|
const decision = await ctx.requestApproval({
|
|
14
14
|
kind: "shell",
|
|
15
15
|
command,
|
|
16
|
-
root: ctx.workspace
|
|
16
|
+
root: ctx.workspace.primary().name,
|
|
17
17
|
});
|
|
18
18
|
if (!decision.allow) {
|
|
19
19
|
return { approved: false, rejection: decision.feedback };
|
|
@@ -74,12 +74,17 @@ function runBounded(command, ctx) {
|
|
|
74
74
|
};
|
|
75
75
|
child.stdout?.on("data", capture);
|
|
76
76
|
child.stderr?.on("data", capture);
|
|
77
|
-
// A single guard so the timeout-kill and the natural
|
|
77
|
+
// A single guard so the timeout-kill, an external abort, and the natural
|
|
78
|
+
// close can't all fire. `cleanup` deregisters the abort listener on every
|
|
79
|
+
// settle path so a resolved command never holds a dangling subscription.
|
|
78
80
|
let settled = false;
|
|
81
|
+
const abortSignal = ctx.signal;
|
|
82
|
+
const cleanup = () => abortSignal?.removeEventListener("abort", onAbort);
|
|
79
83
|
const timer = setTimeout(() => {
|
|
80
84
|
if (settled)
|
|
81
85
|
return;
|
|
82
86
|
settled = true;
|
|
87
|
+
cleanup();
|
|
83
88
|
killTree(child.pid);
|
|
84
89
|
resolve({
|
|
85
90
|
timedOut: true,
|
|
@@ -89,11 +94,37 @@ function runBounded(command, ctx) {
|
|
|
89
94
|
truncated,
|
|
90
95
|
});
|
|
91
96
|
}, timeoutMs);
|
|
97
|
+
// External cancellation (C.33): a cancelled sibling's in-flight command is
|
|
98
|
+
// kill-tree'd here instead of orphaned. Reports as a `SIGKILL` termination
|
|
99
|
+
// (not a timeout) so the caller can tell cancel apart from a real timeout.
|
|
100
|
+
function onAbort() {
|
|
101
|
+
if (settled)
|
|
102
|
+
return;
|
|
103
|
+
settled = true;
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
cleanup();
|
|
106
|
+
killTree(child.pid);
|
|
107
|
+
resolve({
|
|
108
|
+
timedOut: false,
|
|
109
|
+
exitCode: null,
|
|
110
|
+
signal: "SIGKILL",
|
|
111
|
+
output: Buffer.concat(chunks).toString("utf8"),
|
|
112
|
+
truncated,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (abortSignal?.aborted) {
|
|
116
|
+
// Already cancelled before the child got going — reap it immediately.
|
|
117
|
+
queueMicrotask(onAbort);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
121
|
+
}
|
|
92
122
|
child.on("error", (err) => {
|
|
93
123
|
if (settled)
|
|
94
124
|
return;
|
|
95
125
|
settled = true;
|
|
96
126
|
clearTimeout(timer);
|
|
127
|
+
cleanup();
|
|
97
128
|
resolve({
|
|
98
129
|
timedOut: false,
|
|
99
130
|
exitCode: null,
|
|
@@ -103,15 +134,16 @@ function runBounded(command, ctx) {
|
|
|
103
134
|
spawnError: err.message,
|
|
104
135
|
});
|
|
105
136
|
});
|
|
106
|
-
child.on("close", (code,
|
|
137
|
+
child.on("close", (code, closeSignal) => {
|
|
107
138
|
if (settled)
|
|
108
139
|
return;
|
|
109
140
|
settled = true;
|
|
110
141
|
clearTimeout(timer);
|
|
142
|
+
cleanup();
|
|
111
143
|
resolve({
|
|
112
144
|
timedOut: false,
|
|
113
145
|
exitCode: code,
|
|
114
|
-
signal:
|
|
146
|
+
signal: closeSignal ?? null,
|
|
115
147
|
output: Buffer.concat(chunks).toString("utf8"),
|
|
116
148
|
truncated,
|
|
117
149
|
});
|
package/dist/tools/types.d.ts
CHANGED
|
@@ -166,13 +166,15 @@ export interface ToolContext {
|
|
|
166
166
|
/**
|
|
167
167
|
* The declared workspace root set for this session (C.26). Path-taking tools
|
|
168
168
|
* resolve *through* this rather than through `cwd`, so a multi-root session
|
|
169
|
-
* confines each call to the one root it selects.
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
169
|
+
* confines each call to the one root it selects. **Required** (C.26 Step 6): a
|
|
170
|
+
* construction site that fails to thread it is a *compile error*, not a runtime
|
|
171
|
+
* primary-default — closing the omission hole the old optional field left. The
|
|
172
|
+
* runtime always sets it; a single-root caller builds one with `sessionWorkspace`.
|
|
173
|
+
* (The type catches *omission*; it cannot catch a *fabricated* workspace threaded
|
|
174
|
+
* through — the `no-split-brain` grep guard covers that. See its header.) `cwd`
|
|
173
175
|
* remains equal to `workspace.primary().absPath`.
|
|
174
176
|
*/
|
|
175
|
-
workspace
|
|
177
|
+
workspace: Workspace;
|
|
176
178
|
/** Fully-resolved CLI configuration. */
|
|
177
179
|
config: CruxyConfig;
|
|
178
180
|
/** Shared leveled logger (diagnostics to stderr, `print` to stdout). */
|
|
@@ -208,6 +210,15 @@ export interface ToolContext {
|
|
|
208
210
|
* populated). Absent → host execution, unchanged.
|
|
209
211
|
*/
|
|
210
212
|
sandbox?: SandboxService;
|
|
213
|
+
/**
|
|
214
|
+
* Cooperative cancellation for in-flight tool work (C.33). Set on a subagent's
|
|
215
|
+
* ctx when it runs inside a parallel fan-out: when a fatal sibling failure or
|
|
216
|
+
* Ctrl-C aborts the batch, this signal fires, and a long-running `run_command`
|
|
217
|
+
* kills its whole process tree instead of leaving it orphaned — the C.12/C.16
|
|
218
|
+
* kill-tree discipline extended to N concurrent children. Absent → no external
|
|
219
|
+
* cancellation (the tool's own timeout still applies), unchanged behaviour.
|
|
220
|
+
*/
|
|
221
|
+
signal?: AbortSignal;
|
|
211
222
|
}
|
|
212
223
|
/**
|
|
213
224
|
* The one interface every tool implements. `parameters` is a zod schema; it both
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RootSpec } from "./types.js";
|
|
2
|
+
import { Workspace } from "./workspace.js";
|
|
3
|
+
/**
|
|
4
|
+
* The interactive add-root path (C.26 step 5). Adding a workspace root is an
|
|
5
|
+
* EXPLICIT HUMAN act — it grows the trust surface (a new root can carry its own
|
|
6
|
+
* hooks / MCP / project memory), so it is:
|
|
7
|
+
*
|
|
8
|
+
* - reachable ONLY from the REPL command and the CLI, never as a model tool.
|
|
9
|
+
* The model can act only through the tool registry (`registry.get(name)`),
|
|
10
|
+
* and no tool is registered for this — the allowlist argument is unchanged.
|
|
11
|
+
* - TTY-only: refused (coded) when there is no interactive human to vouch.
|
|
12
|
+
* - held to the SAME validation as `--root`: the new root must exist, be a
|
|
13
|
+
* directory, have a unique name, and NOT nest/overlap an existing root
|
|
14
|
+
* (CRUXY_E_ROOT_OVERLAP) — reusing {@link buildWorkspace}, the single
|
|
15
|
+
* Workspace constructor from user input.
|
|
16
|
+
*
|
|
17
|
+
* Returns the NEW immutable Workspace (the root set only ever grows by
|
|
18
|
+
* constructing a new one). A freshly added root starts UNTRUSTED; its hooks and
|
|
19
|
+
* project memory stay inert until explicitly trusted.
|
|
20
|
+
*/
|
|
21
|
+
export interface AddRootOptions {
|
|
22
|
+
/** Base dir for resolving a relative `path` (the session's primary root). */
|
|
23
|
+
cwd: string;
|
|
24
|
+
/** Whether stdin is a TTY — add-root is refused without an interactive human. */
|
|
25
|
+
tty: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function addRootToWorkspace(current: Workspace, spec: RootSpec, opts: AddRootOptions): Promise<Workspace>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { usageError } from "../errors/index.js";
|
|
2
|
+
import { buildWorkspace } from "./workspace.js";
|
|
3
|
+
export async function addRootToWorkspace(current, spec, opts) {
|
|
4
|
+
if (!opts.tty) {
|
|
5
|
+
throw usageError("adding a workspace root is an interactive action and needs a TTY", ["declare it up front with `--root name=path` instead"]);
|
|
6
|
+
}
|
|
7
|
+
// Rebuild from the existing roots (as specs) + the new one, so EVERY
|
|
8
|
+
// buildWorkspace guard — existence, directory, name uniqueness, and the
|
|
9
|
+
// nested/overlap refusal — applies to the addition. Existing roots keep their
|
|
10
|
+
// names, absolute paths, and order (the primary stays first).
|
|
11
|
+
const specs = [
|
|
12
|
+
...current.roots().map((r) => ({ name: r.name, path: r.absPath })),
|
|
13
|
+
spec,
|
|
14
|
+
];
|
|
15
|
+
return buildWorkspace(specs, { cwd: opts.cwd });
|
|
16
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { DeclaredRoot, RootSpec } from "./types.js";
|
|
2
|
-
export { Workspace, buildWorkspace,
|
|
2
|
+
export { Workspace, buildWorkspace, sessionWorkspace } from "./workspace.js";
|
|
3
3
|
export { PathEscapeError, confineToRoot, isInside, resolveInWorkspace, } from "./resolve.js";
|
|
4
4
|
export { selectRoot } from "./select.js";
|
|
5
5
|
export type { RootRef, SelectedRoot } from "./select.js";
|
|
6
|
+
export { addRootToWorkspace, type AddRootOptions } from "./add-root.js";
|
package/dist/workspace/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { Workspace, buildWorkspace,
|
|
1
|
+
export { Workspace, buildWorkspace, sessionWorkspace } from "./workspace.js";
|
|
2
2
|
export { PathEscapeError, confineToRoot, isInside, resolveInWorkspace, } from "./resolve.js";
|
|
3
3
|
export { selectRoot } from "./select.js";
|
|
4
|
+
export { addRootToWorkspace } from "./add-root.js";
|
|
@@ -49,8 +49,13 @@ export declare function buildWorkspace(specs: readonly RootSpec[], opts?: {
|
|
|
49
49
|
cwd?: string;
|
|
50
50
|
}): Promise<Workspace>;
|
|
51
51
|
/**
|
|
52
|
-
* Build
|
|
53
|
-
*
|
|
54
|
-
*
|
|
52
|
+
* Build THE session's workspace from a single absolute path — the trivial
|
|
53
|
+
* single-root case. This is a *session construction* primitive, not a per-call
|
|
54
|
+
* fallback: after C.26 Step 6 it is called ONLY at the two CLI entry points that
|
|
55
|
+
* turn argv into a session (`cli/commands/run.ts`, `cli/onboard.ts`), never in the
|
|
56
|
+
* middle of a subsystem. Synthesizing one elsewhere would silently scope that
|
|
57
|
+
* subsystem to its own cwd while the fan tools see N roots (a split-brain) — the
|
|
58
|
+
* `no-split-brain` guard fails on any `sessionWorkspace(` outside those two files.
|
|
59
|
+
* The one root is primary and named by its basename.
|
|
55
60
|
*/
|
|
56
|
-
export declare function
|
|
61
|
+
export declare function sessionWorkspace(absPath: string): Workspace;
|
|
@@ -168,11 +168,16 @@ export async function buildWorkspace(specs, opts = {}) {
|
|
|
168
168
|
return new Workspace(declared);
|
|
169
169
|
}
|
|
170
170
|
/**
|
|
171
|
-
* Build
|
|
172
|
-
*
|
|
173
|
-
*
|
|
171
|
+
* Build THE session's workspace from a single absolute path — the trivial
|
|
172
|
+
* single-root case. This is a *session construction* primitive, not a per-call
|
|
173
|
+
* fallback: after C.26 Step 6 it is called ONLY at the two CLI entry points that
|
|
174
|
+
* turn argv into a session (`cli/commands/run.ts`, `cli/onboard.ts`), never in the
|
|
175
|
+
* middle of a subsystem. Synthesizing one elsewhere would silently scope that
|
|
176
|
+
* subsystem to its own cwd while the fan tools see N roots (a split-brain) — the
|
|
177
|
+
* `no-split-brain` guard fails on any `sessionWorkspace(` outside those two files.
|
|
178
|
+
* The one root is primary and named by its basename.
|
|
174
179
|
*/
|
|
175
|
-
export function
|
|
180
|
+
export function sessionWorkspace(absPath) {
|
|
176
181
|
const abs = path.resolve(absPath);
|
|
177
182
|
return new Workspace([
|
|
178
183
|
{ name: path.basename(abs) || "root", absPath: abs, primary: true },
|