@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,23 +1,50 @@
|
|
|
1
1
|
import type { ToolContext, ToolResult } from "../../tools/types.js";
|
|
2
|
+
import type { DeclaredRoot } from "../../workspace/index.js";
|
|
2
3
|
import { type LspService } from "../service.js";
|
|
3
4
|
import type { LspLocation } from "../types.js";
|
|
4
5
|
/**
|
|
5
|
-
* The
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
* The root a query ran against, threaded to the tool so it can label locations by
|
|
7
|
+
* their source root and — for references — name the per-root limitation (C.26).
|
|
8
|
+
*/
|
|
9
|
+
export interface LspRootContext {
|
|
10
|
+
/** The declared root whose language-server pool answered the query. */
|
|
11
|
+
root: DeclaredRoot;
|
|
12
|
+
/** Whether this is a genuine multi-root session (labels render only then). */
|
|
13
|
+
isMultiRoot: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The shared spine of every LSP tool (C.12): enforce the master switch, resolve
|
|
17
|
+
* the target file to the ONE declared root that contains it, prove it exists, get
|
|
18
|
+
* THAT root's language-server pool, and run `query`. Read-only throughout — no
|
|
19
|
+
* `ctx.requestApproval`, so these bypass the U.3 gate exactly like `search_codebase`
|
|
20
|
+
* and `grep_files`.
|
|
21
|
+
*
|
|
22
|
+
* Multi-repo (C.26, Funnel A→pool): the file is resolved through the shared
|
|
23
|
+
* `resolveToolPath`, so it commits to exactly one root, and the pool is keyed by
|
|
24
|
+
* THAT root's `absPath` (`getLspService` caches per resolved cwd → a per-root map
|
|
25
|
+
* for free). A file in root B is answered by B's server, never A's — the
|
|
26
|
+
* `(file → root)` selection and the `(root → pool)` key are the same value.
|
|
9
27
|
*
|
|
10
28
|
* Errors are surfaced as `{ ok:false }` text the agent can act on: a coded LSP
|
|
11
29
|
* failure (no server / timeout / crash) is rendered WITH its next step, so the
|
|
12
30
|
* agent can reroute (e.g. to grep) or the user can install the server. A genuine
|
|
13
31
|
* empty answer never reaches here as an error — `query` returns it as `ok:true`.
|
|
14
32
|
*/
|
|
15
|
-
export declare function runLspTool(ctx: ToolContext, file: string, query: (service: LspService, absFile: string) => Promise<ToolResult>): Promise<ToolResult>;
|
|
33
|
+
export declare function runLspTool(ctx: ToolContext, file: string, query: (service: LspService, absFile: string, rc: LspRootContext) => Promise<ToolResult>): Promise<ToolResult>;
|
|
34
|
+
/**
|
|
35
|
+
* A note stating that a per-root language server only sees its own root, so
|
|
36
|
+
* references/definitions in sibling roots are NOT searched (⚖︎JC-7) — returned only
|
|
37
|
+
* in a multi-root session, so a references list is never read as complete when it
|
|
38
|
+
* silently could not span roots. Empty string in single-root (byte-identical).
|
|
39
|
+
*/
|
|
40
|
+
export declare function crossRootNote(rc: LspRootContext): string;
|
|
16
41
|
/**
|
|
17
42
|
* Render up to `max` locations as `path:line:col-endLine:endCol`, one per line,
|
|
18
43
|
* with a trailing "N more" note when capped — the same bounded-honest pattern as
|
|
19
|
-
* grep_files and search_codebase (never silently drop the overflow).
|
|
44
|
+
* grep_files and search_codebase (never silently drop the overflow). In a
|
|
45
|
+
* multi-root session each path is labelled `‹root› ▸ path` (the query ran against
|
|
46
|
+
* one root, so every location carries that root's label).
|
|
20
47
|
*/
|
|
21
|
-
export declare function formatLocations(locations: LspLocation[], max: number): string;
|
|
48
|
+
export declare function formatLocations(locations: LspLocation[], max: number, rc: LspRootContext): string;
|
|
22
49
|
/** Relative label for a validated absolute path, for tool messages. */
|
|
23
50
|
export declare function relLabel(cwd: string, absFile: string): string;
|
package/dist/lsp/tools/common.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { promises as fsp } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { CruxyError } from "../../errors/index.js";
|
|
4
|
-
import {
|
|
4
|
+
import { contextWorkspace, labelPath, resolveToolPath, } from "../../tools/file/paths.js";
|
|
5
5
|
import { getLspService } from "../service.js";
|
|
6
6
|
/**
|
|
7
|
-
* The shared spine of every LSP tool (C.12): enforce the master switch,
|
|
8
|
-
* the target
|
|
9
|
-
*
|
|
10
|
-
* these bypass the U.3 gate exactly like `search_codebase`
|
|
7
|
+
* The shared spine of every LSP tool (C.12): enforce the master switch, resolve
|
|
8
|
+
* the target file to the ONE declared root that contains it, prove it exists, get
|
|
9
|
+
* THAT root's language-server pool, and run `query`. Read-only throughout — no
|
|
10
|
+
* `ctx.requestApproval`, so these bypass the U.3 gate exactly like `search_codebase`
|
|
11
|
+
* and `grep_files`.
|
|
12
|
+
*
|
|
13
|
+
* Multi-repo (C.26, Funnel A→pool): the file is resolved through the shared
|
|
14
|
+
* `resolveToolPath`, so it commits to exactly one root, and the pool is keyed by
|
|
15
|
+
* THAT root's `absPath` (`getLspService` caches per resolved cwd → a per-root map
|
|
16
|
+
* for free). A file in root B is answered by B's server, never A's — the
|
|
17
|
+
* `(file → root)` selection and the `(root → pool)` key are the same value.
|
|
11
18
|
*
|
|
12
19
|
* Errors are surfaced as `{ ok:false }` text the agent can act on: a coded LSP
|
|
13
20
|
* failure (no server / timeout / crash) is rendered WITH its next step, so the
|
|
@@ -21,9 +28,11 @@ export async function runLspTool(ctx, file, query) {
|
|
|
21
28
|
error: "LSP tools are disabled (set lsp.enabled = true to use language-server features)",
|
|
22
29
|
};
|
|
23
30
|
}
|
|
31
|
+
const ws = contextWorkspace(ctx);
|
|
32
|
+
let root;
|
|
24
33
|
let absFile;
|
|
25
34
|
try {
|
|
26
|
-
absFile = await
|
|
35
|
+
({ root, abs: absFile } = await resolveToolPath(ctx, { path: file }));
|
|
27
36
|
}
|
|
28
37
|
catch (err) {
|
|
29
38
|
return { ok: false, error: err.message };
|
|
@@ -38,8 +47,8 @@ export async function runLspTool(ctx, file, query) {
|
|
|
38
47
|
return { ok: false, error: `file not found: ${file}` };
|
|
39
48
|
}
|
|
40
49
|
try {
|
|
41
|
-
const service = getLspService(
|
|
42
|
-
return await query(service, absFile);
|
|
50
|
+
const service = getLspService(root.absPath, ctx.config, ctx.logger);
|
|
51
|
+
return await query(service, absFile, { root, isMultiRoot: ws.isMultiRoot });
|
|
43
52
|
}
|
|
44
53
|
catch (err) {
|
|
45
54
|
return { ok: false, error: describeError(err) };
|
|
@@ -54,14 +63,27 @@ function describeError(err) {
|
|
|
54
63
|
}
|
|
55
64
|
return err.message;
|
|
56
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* A note stating that a per-root language server only sees its own root, so
|
|
68
|
+
* references/definitions in sibling roots are NOT searched (⚖︎JC-7) — returned only
|
|
69
|
+
* in a multi-root session, so a references list is never read as complete when it
|
|
70
|
+
* silently could not span roots. Empty string in single-root (byte-identical).
|
|
71
|
+
*/
|
|
72
|
+
export function crossRootNote(rc) {
|
|
73
|
+
return rc.isMultiRoot
|
|
74
|
+
? `\n(note: only root ‹${rc.root.name}› was searched — a per-root language server does not resolve symbols across roots)`
|
|
75
|
+
: "";
|
|
76
|
+
}
|
|
57
77
|
/**
|
|
58
78
|
* Render up to `max` locations as `path:line:col-endLine:endCol`, one per line,
|
|
59
79
|
* with a trailing "N more" note when capped — the same bounded-honest pattern as
|
|
60
|
-
* grep_files and search_codebase (never silently drop the overflow).
|
|
80
|
+
* grep_files and search_codebase (never silently drop the overflow). In a
|
|
81
|
+
* multi-root session each path is labelled `‹root› ▸ path` (the query ran against
|
|
82
|
+
* one root, so every location carries that root's label).
|
|
61
83
|
*/
|
|
62
|
-
export function formatLocations(locations, max) {
|
|
84
|
+
export function formatLocations(locations, max, rc) {
|
|
63
85
|
const shown = locations.slice(0, max);
|
|
64
|
-
const lines = shown.map((l) => `${l.path}:${l.startLine}:${l.startCol}-${l.endLine}:${l.endCol}`);
|
|
86
|
+
const lines = shown.map((l) => `${labelPath(rc.root, l.path, rc.isMultiRoot)}:${l.startLine}:${l.startCol}-${l.endLine}:${l.endCol}`);
|
|
65
87
|
const omitted = locations.length - shown.length;
|
|
66
88
|
if (omitted > 0) {
|
|
67
89
|
lines.push(`… [${omitted} more location(s) omitted]`);
|
|
@@ -27,14 +27,14 @@ export const findDefinitionTool = {
|
|
|
27
27
|
description: "Resolve where a symbol is defined using the project's language server (go-to-definition). Give the file and the 1-based line/column of the identifier. Returns definition locations as 'path:line:col-endLine:endCol'. Read-only, no approval. Precise where grep is textual — prefer this to jump to a symbol's definition.",
|
|
28
28
|
parameters,
|
|
29
29
|
execute(input, ctx) {
|
|
30
|
-
return runLspTool(ctx, input.file, async (service, absFile) => {
|
|
30
|
+
return runLspTool(ctx, input.file, async (service, absFile, rc) => {
|
|
31
31
|
const locations = await service.definition(absFile, input.line, input.column);
|
|
32
32
|
if (locations.length === 0) {
|
|
33
33
|
return { ok: true, output: "(no definition found)" };
|
|
34
34
|
}
|
|
35
35
|
return {
|
|
36
36
|
ok: true,
|
|
37
|
-
output: formatLocations(locations, ctx.config.lsp.maxResults),
|
|
37
|
+
output: formatLocations(locations, ctx.config.lsp.maxResults, rc),
|
|
38
38
|
};
|
|
39
39
|
});
|
|
40
40
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { formatLocations, runLspTool } from "./common.js";
|
|
2
|
+
import { crossRootNote, formatLocations, runLspTool } from "./common.js";
|
|
3
3
|
const parameters = z.object({
|
|
4
4
|
file: z
|
|
5
5
|
.string()
|
|
@@ -27,14 +27,20 @@ export const findReferencesTool = {
|
|
|
27
27
|
description: "Find all references to a symbol using the project's language server. Give the file and the 1-based line/column of the identifier. Returns use sites as 'path:line:col-endLine:endCol' (capped, with an 'N more' note). Read-only, no approval. Precise where grep is textual — prefer this to see every caller/user of a symbol.",
|
|
28
28
|
parameters,
|
|
29
29
|
execute(input, ctx) {
|
|
30
|
-
return runLspTool(ctx, input.file, async (service, absFile) => {
|
|
30
|
+
return runLspTool(ctx, input.file, async (service, absFile, rc) => {
|
|
31
31
|
const locations = await service.references(absFile, input.line, input.column);
|
|
32
|
+
// The cross-root note is appended even to an empty result: "(no references)"
|
|
33
|
+
// from a per-root server must not read as "no references anywhere" (⚖︎JC-7).
|
|
32
34
|
if (locations.length === 0) {
|
|
33
|
-
return {
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
output: `(no references found)${crossRootNote(rc)}`,
|
|
38
|
+
};
|
|
34
39
|
}
|
|
35
40
|
return {
|
|
36
41
|
ok: true,
|
|
37
|
-
output: formatLocations(locations, ctx.config.lsp.maxResults)
|
|
42
|
+
output: formatLocations(locations, ctx.config.lsp.maxResults, rc) +
|
|
43
|
+
crossRootNote(rc),
|
|
38
44
|
};
|
|
39
45
|
});
|
|
40
46
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { labelPath } from "../../tools/file/paths.js";
|
|
2
3
|
import { runLspTool } from "./common.js";
|
|
3
4
|
const parameters = z.object({
|
|
4
5
|
file: z
|
|
@@ -17,23 +18,24 @@ export const getDiagnosticsTool = {
|
|
|
17
18
|
description: "Get the language server's diagnostics (errors, warnings) for a file. Give the project-relative path. Returns diagnostics as 'severity path:line:col message'. Read-only, no approval. Use this after an edit to see type errors the compiler/linter reports, without running a build.",
|
|
18
19
|
parameters,
|
|
19
20
|
execute(input, ctx) {
|
|
20
|
-
return runLspTool(ctx, input.file, async (service, absFile) => {
|
|
21
|
+
return runLspTool(ctx, input.file, async (service, absFile, rc) => {
|
|
21
22
|
const diagnostics = await service.diagnostics(absFile);
|
|
22
23
|
if (diagnostics.length === 0) {
|
|
23
24
|
return { ok: true, output: "(no diagnostics)" };
|
|
24
25
|
}
|
|
25
26
|
return {
|
|
26
27
|
ok: true,
|
|
27
|
-
output: formatDiagnostics(diagnostics, ctx.config.lsp.maxResults),
|
|
28
|
+
output: formatDiagnostics(diagnostics, ctx.config.lsp.maxResults, rc),
|
|
28
29
|
};
|
|
29
30
|
});
|
|
30
31
|
},
|
|
31
32
|
};
|
|
32
|
-
function formatDiagnostics(diagnostics, max) {
|
|
33
|
+
function formatDiagnostics(diagnostics, max, rc) {
|
|
33
34
|
const shown = diagnostics.slice(0, max);
|
|
34
35
|
const lines = shown.map((d) => {
|
|
35
36
|
const src = d.source ? ` [${d.source}]` : "";
|
|
36
|
-
|
|
37
|
+
const loc = labelPath(rc.root, d.path, rc.isMultiRoot);
|
|
38
|
+
return `${d.severity} ${loc}:${d.range.startLine}:${d.range.startCol} ${d.message}${src}`;
|
|
37
39
|
});
|
|
38
40
|
const omitted = diagnostics.length - shown.length;
|
|
39
41
|
if (omitted > 0) {
|
package/dist/mcp/index.d.ts
CHANGED
|
@@ -6,4 +6,5 @@ export { boundToolList, type McpBounds, type BoundedTool, type BoundedToolList,
|
|
|
6
6
|
export { McpStdioTransport, type McpSpawnSpec } from "./transport.js";
|
|
7
7
|
export { McpClient, type McpClientTimeouts } from "./client.js";
|
|
8
8
|
export { mcpToolsFrom, type McpToolSource } from "./adapter.js";
|
|
9
|
+
export { deferredSiblingServers, type DeferredSiblingServer, } from "./sibling-banner.js";
|
|
9
10
|
export { connectMcpTools, resetMcpServices, liveMcpConnectionCount, type ConnectMcpToolsParams, type ConnectMcpToolsResult, type McpServiceDeps, } from "./service.js";
|
package/dist/mcp/index.js
CHANGED
|
@@ -5,4 +5,5 @@ export { boundToolList, } from "./bounds.js";
|
|
|
5
5
|
export { McpStdioTransport } from "./transport.js";
|
|
6
6
|
export { McpClient } from "./client.js";
|
|
7
7
|
export { mcpToolsFrom } from "./adapter.js";
|
|
8
|
+
export { deferredSiblingServers, } from "./sibling-banner.js";
|
|
8
9
|
export { connectMcpTools, resetMcpServices, liveMcpConnectionCount, } from "./service.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Workspace } from "../workspace/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* MCP is PRIMARY-ROOT ONLY this release (JC-D). Two roots each declaring a
|
|
4
|
+
* `github` server would collide on the wire-name `mcp__github__<tool>` (the
|
|
5
|
+
* adapter derives it from the server id with no root component), so sibling-root
|
|
6
|
+
* servers are NOT registered. They are also NOT silently dropped: each one is
|
|
7
|
+
* named individually in the startup banner (server + which root declared it), so
|
|
8
|
+
* the deferral is explicit. Root-qualified wire-names are a separate follow-up.
|
|
9
|
+
*/
|
|
10
|
+
export interface DeferredSiblingServer {
|
|
11
|
+
/** The declared server id (the would-be `mcp__<server>__*` prefix). */
|
|
12
|
+
server: string;
|
|
13
|
+
/** The declaring non-primary root's name. */
|
|
14
|
+
root: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Enumerate the MCP servers declared by each NON-primary root's own project
|
|
18
|
+
* config — the set deferred this release. Pure over an injectable reader so the
|
|
19
|
+
* naming is unit-testable without touching disk. Reads each sibling's OWN
|
|
20
|
+
* declaration (not the merged/global set), so a sibling's `github` is named even
|
|
21
|
+
* when the primary also declares one (the exact collision we're deferring).
|
|
22
|
+
*/
|
|
23
|
+
export declare function deferredSiblingServers(workspace: Workspace, opts?: {
|
|
24
|
+
declaredServersFor?: (absPath: string) => string[];
|
|
25
|
+
}): DeferredSiblingServer[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { findProjectConfig } from "../config/paths.js";
|
|
3
|
+
/**
|
|
4
|
+
* Enumerate the MCP servers declared by each NON-primary root's own project
|
|
5
|
+
* config — the set deferred this release. Pure over an injectable reader so the
|
|
6
|
+
* naming is unit-testable without touching disk. Reads each sibling's OWN
|
|
7
|
+
* declaration (not the merged/global set), so a sibling's `github` is named even
|
|
8
|
+
* when the primary also declares one (the exact collision we're deferring).
|
|
9
|
+
*/
|
|
10
|
+
export function deferredSiblingServers(workspace, opts = {}) {
|
|
11
|
+
const declaredServersFor = opts.declaredServersFor ?? readProjectServers;
|
|
12
|
+
const out = [];
|
|
13
|
+
for (const root of workspace.roots()) {
|
|
14
|
+
if (root.primary)
|
|
15
|
+
continue;
|
|
16
|
+
for (const server of declaredServersFor(root.absPath)) {
|
|
17
|
+
out.push({ server, root: root.name });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
/** Read `mcp.servers` keys from a root's own project config file. */
|
|
23
|
+
function readProjectServers(absPath) {
|
|
24
|
+
const p = findProjectConfig(absPath);
|
|
25
|
+
if (!p || !existsSync(p))
|
|
26
|
+
return [];
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(readFileSync(p, "utf8"));
|
|
29
|
+
return Object.keys(parsed.mcp?.servers ?? {});
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/memory/recall.d.ts
CHANGED
|
@@ -30,3 +30,27 @@ export interface RecallInput {
|
|
|
30
30
|
* truncated. Only entries actually passed in are rendered — nothing is invented.
|
|
31
31
|
*/
|
|
32
32
|
export declare function buildRecallBlock(input: RecallInput): string | null;
|
|
33
|
+
/** One trusted root's project entries, tagged with the root's declared name.
|
|
34
|
+
* The name is STRUCTURAL — the caller pairs it with the store the entries were
|
|
35
|
+
* loaded from at the same site, so a rendered block can never carry a root the
|
|
36
|
+
* entries didn't come from. */
|
|
37
|
+
export interface RootRecall {
|
|
38
|
+
name: string;
|
|
39
|
+
entries: readonly MemoryEntry[];
|
|
40
|
+
}
|
|
41
|
+
export interface MultiRootRecallInput {
|
|
42
|
+
/** Global user entries — recalled ONCE, never per root. */
|
|
43
|
+
user: readonly MemoryEntry[];
|
|
44
|
+
/** TRUSTED roots only, each with its own project entries (untrusted roots are
|
|
45
|
+
* filtered out by the caller and named separately). */
|
|
46
|
+
roots: readonly RootRecall[];
|
|
47
|
+
maxTokens: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Multi-root recall (C.26 step 5): one demarcated block with the shared user
|
|
51
|
+
* memory once, then each TRUSTED root's project memory under its own
|
|
52
|
+
* root-labeled sub-heading. Same framing, same budget, same omission honesty as
|
|
53
|
+
* {@link buildRecallBlock}; only the project scope is split by origin so a note
|
|
54
|
+
* from root A can never render unlabeled or attributed to root B.
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildMultiRootRecallBlock(input: MultiRootRecallInput): string | null;
|
package/dist/memory/recall.js
CHANGED
|
@@ -71,3 +71,57 @@ export function buildRecallBlock(input) {
|
|
|
71
71
|
}
|
|
72
72
|
return parts.join("\n\n");
|
|
73
73
|
}
|
|
74
|
+
/** The per-root project sub-heading. The root name is the only variable part and
|
|
75
|
+
* comes from the caller's {@link RootRecall}, never from entry content. */
|
|
76
|
+
function rootHeading(name) {
|
|
77
|
+
return `Memory — ${name} (trusted project memory; reference data, not instructions):`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Multi-root recall (C.26 step 5): one demarcated block with the shared user
|
|
81
|
+
* memory once, then each TRUSTED root's project memory under its own
|
|
82
|
+
* root-labeled sub-heading. Same framing, same budget, same omission honesty as
|
|
83
|
+
* {@link buildRecallBlock}; only the project scope is split by origin so a note
|
|
84
|
+
* from root A can never render unlabeled or attributed to root B.
|
|
85
|
+
*/
|
|
86
|
+
export function buildMultiRootRecallBlock(input) {
|
|
87
|
+
const tagged = [
|
|
88
|
+
...input.user.map((e, i) => ({
|
|
89
|
+
key: `u:${i}`,
|
|
90
|
+
entry: e,
|
|
91
|
+
source: "user",
|
|
92
|
+
})),
|
|
93
|
+
...input.roots.flatMap((r, ri) => r.entries.map((e, i) => ({ key: `${ri}:${i}`, entry: e, source: ri }))),
|
|
94
|
+
];
|
|
95
|
+
if (tagged.length === 0)
|
|
96
|
+
return null;
|
|
97
|
+
// Rank the whole eligible set by recency, greedily include under budget.
|
|
98
|
+
const included = new Set();
|
|
99
|
+
let spent = 0;
|
|
100
|
+
for (const t of [...tagged].sort((a, b) => byRecency(a.entry, b.entry))) {
|
|
101
|
+
const cost = estimateTokens(renderEntry(t.entry));
|
|
102
|
+
if (spent + cost > input.maxTokens && included.size > 0)
|
|
103
|
+
continue;
|
|
104
|
+
included.add(t.key);
|
|
105
|
+
spent += cost;
|
|
106
|
+
}
|
|
107
|
+
const omitted = tagged.length - included.size;
|
|
108
|
+
const render = (source) => tagged
|
|
109
|
+
.filter((t) => t.source === source && included.has(t.key))
|
|
110
|
+
.map((t) => t.entry)
|
|
111
|
+
.sort(byRecency)
|
|
112
|
+
.map(renderEntry)
|
|
113
|
+
.join("\n");
|
|
114
|
+
const parts = [RECALL_HEADING, FRAMING];
|
|
115
|
+
const userBody = render("user");
|
|
116
|
+
if (userBody)
|
|
117
|
+
parts.push(`${SCOPE_HEADINGS.user}\n${userBody}`);
|
|
118
|
+
input.roots.forEach((r, ri) => {
|
|
119
|
+
const body = render(ri);
|
|
120
|
+
if (body)
|
|
121
|
+
parts.push(`${rootHeading(r.name)}\n${body}`);
|
|
122
|
+
});
|
|
123
|
+
if (omitted > 0) {
|
|
124
|
+
parts.push(`[${omitted} older note${omitted === 1 ? "" : "s"} omitted to stay within the memory context budget.]`);
|
|
125
|
+
}
|
|
126
|
+
return parts.join("\n\n");
|
|
127
|
+
}
|
|
@@ -12,13 +12,16 @@ declare const RememberSchema: z.ZodObject<{
|
|
|
12
12
|
kind: z.ZodEnum<["fact", "decision", "preference"]>;
|
|
13
13
|
content: z.ZodString;
|
|
14
14
|
scope: z.ZodOptional<z.ZodEnum<["user", "project"]>>;
|
|
15
|
+
root: z.ZodOptional<z.ZodString>;
|
|
15
16
|
}, "strip", z.ZodTypeAny, {
|
|
16
17
|
kind: "fact" | "decision" | "preference";
|
|
17
18
|
content: string;
|
|
19
|
+
root?: string | undefined;
|
|
18
20
|
scope?: "project" | "user" | undefined;
|
|
19
21
|
}, {
|
|
20
22
|
kind: "fact" | "decision" | "preference";
|
|
21
23
|
content: string;
|
|
24
|
+
root?: string | undefined;
|
|
22
25
|
scope?: "project" | "user" | undefined;
|
|
23
26
|
}>;
|
|
24
27
|
export declare const rememberTool: Tool<typeof RememberSchema>;
|
|
@@ -23,6 +23,10 @@ const RememberSchema = z.object({
|
|
|
23
23
|
.enum(["user", "project"])
|
|
24
24
|
.optional()
|
|
25
25
|
.describe("'user' (default) saves to your cross-project memory; 'project' saves to this repo's memory (shared with the repo)."),
|
|
26
|
+
root: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Which declared workspace root a 'project' note belongs to, by name. Defaults to the primary root; ignored for 'user' scope. In a single-root session, omit."),
|
|
26
30
|
});
|
|
27
31
|
export const rememberTool = {
|
|
28
32
|
name: "remember",
|
|
@@ -30,8 +34,14 @@ export const rememberTool = {
|
|
|
30
34
|
parameters: RememberSchema,
|
|
31
35
|
async execute(input, ctx) {
|
|
32
36
|
try {
|
|
37
|
+
// A named `root` targets that root's project memory (default: primary). A
|
|
38
|
+
// bad name throws CRUXY_E_ROOT_UNKNOWN, surfaced to the model below. User
|
|
39
|
+
// memory is global, so `root` never changes where it lands.
|
|
40
|
+
const cwd = input.root && ctx.workspace
|
|
41
|
+
? ctx.workspace.rootByName(input.root).absPath
|
|
42
|
+
: ctx.cwd;
|
|
33
43
|
const service = new MemoryService({
|
|
34
|
-
cwd
|
|
44
|
+
cwd,
|
|
35
45
|
config: ctx.config.memory,
|
|
36
46
|
});
|
|
37
47
|
const entry = service.remember({
|
package/dist/render/diff.js
CHANGED
|
@@ -36,6 +36,10 @@ function renderPatchFiles(files, c) {
|
|
|
36
36
|
/** Render a `vcs` pull-request publish plan: branch, commit, and PR body. */
|
|
37
37
|
function renderPrPreview(preview, c) {
|
|
38
38
|
const out = [];
|
|
39
|
+
// The resolved forge destination (⚖︎JC-4): the human sees the real owner/repo the
|
|
40
|
+
// PR will open against — not just a branch — and it is exactly the target the
|
|
41
|
+
// wrong-repo guard re-checks before the API call.
|
|
42
|
+
out.push(`${c.strong("target")} ${c.accent(`${preview.target.host}/${preview.target.owner}/${preview.target.repo}`)}`);
|
|
39
43
|
out.push(`${c.strong("branch")} ${c.success(preview.branch)} ${c.glyph.arrow} ${preview.base}`);
|
|
40
44
|
out.push("");
|
|
41
45
|
out.push(c.strong("commit"));
|
|
@@ -60,16 +64,46 @@ function renderRollbackPreview(preview, c) {
|
|
|
60
64
|
if (preview.runSummary)
|
|
61
65
|
out.push(c.muted(`run: ${preview.runSummary}`));
|
|
62
66
|
out.push(c.muted("working-tree files only — commits, pushes, and PRs made during the run are not undone"));
|
|
63
|
-
|
|
67
|
+
out.push(...rollbackWarnings(preview.externalPaths, preview.attributionUnknown, c));
|
|
68
|
+
out.push("");
|
|
69
|
+
out.push(...renderPatchFiles(preview.files, c));
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The blast-radius warnings for one checkpoint: paths changed outside the tracked
|
|
74
|
+
* run (which rollback will overwrite), and the shell-attribution caveat. Emitted
|
|
75
|
+
* BEFORE the file diffs so the global collapse cap can never hide them.
|
|
76
|
+
*/
|
|
77
|
+
function rollbackWarnings(externalPaths, attributionUnknown, c) {
|
|
78
|
+
const out = [];
|
|
79
|
+
if (externalPaths.length > 0) {
|
|
64
80
|
out.push(c.danger(c.strong("changed outside this run — rollback will overwrite these too:")));
|
|
65
|
-
for (const p of
|
|
81
|
+
for (const p of externalPaths)
|
|
66
82
|
out.push(c.danger(`! ${p}`));
|
|
67
83
|
}
|
|
68
|
-
if (
|
|
84
|
+
if (attributionUnknown) {
|
|
69
85
|
out.push(c.warning("this run executed shell commands; some changes below may not have been made by the run"));
|
|
70
86
|
}
|
|
71
|
-
out
|
|
72
|
-
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Render a multi-root set rollback (C.26): one run header, then each touched root
|
|
91
|
+
* as its own labeled block with its own warnings + diffs. Grouping is the point —
|
|
92
|
+
* a reviewer sees which repo each change lands in before the single approval.
|
|
93
|
+
*/
|
|
94
|
+
function renderRollbackSetPreview(preview, c) {
|
|
95
|
+
const out = [];
|
|
96
|
+
out.push(`${c.strong("restore run")} ${c.accent(preview.runId)} ${c.muted(`(${preview.createdAt})`)}`);
|
|
97
|
+
if (preview.runSummary)
|
|
98
|
+
out.push(c.muted(`run: ${preview.runSummary}`));
|
|
99
|
+
out.push(c.muted(`${preview.roots.length} root${preview.roots.length === 1 ? "" : "s"} — ` +
|
|
100
|
+
"working-tree files only; commits, pushes, and PRs made during the run are not undone"));
|
|
101
|
+
for (const r of preview.roots) {
|
|
102
|
+
out.push("");
|
|
103
|
+
out.push(`${c.strong(`root ${r.rootName}`)} ${c.muted(`checkpoint ${r.checkpointId}`)}`);
|
|
104
|
+
out.push(...rollbackWarnings(r.externalPaths, r.attributionUnknown, c));
|
|
105
|
+
out.push(...renderPatchFiles(r.files, c));
|
|
106
|
+
}
|
|
73
107
|
return out;
|
|
74
108
|
}
|
|
75
109
|
/** Split a multi-line body into trimmed-of-trailing lines, dropping a trailing blank. */
|
|
@@ -98,6 +132,9 @@ export function renderActionPreview(preview, c) {
|
|
|
98
132
|
else if (preview.type === "rollback") {
|
|
99
133
|
lines = renderRollbackPreview(preview, c);
|
|
100
134
|
}
|
|
135
|
+
else if (preview.type === "rollback-set") {
|
|
136
|
+
lines = renderRollbackSetPreview(preview, c);
|
|
137
|
+
}
|
|
101
138
|
else {
|
|
102
139
|
const header = preview.exists
|
|
103
140
|
? c.warning("OVERWRITE existing")
|
package/dist/sandbox/policy.js
CHANGED
|
@@ -30,11 +30,20 @@ export function buildPolicy(cfg, cwd, opts = {}) {
|
|
|
30
30
|
const siblingRoots = (opts.siblingRoots ?? [])
|
|
31
31
|
.map((r) => resolvePath(r))
|
|
32
32
|
.filter((r) => r !== resolvePath(cwd)) // the workdir is never a sibling
|
|
33
|
-
.map((source) =>
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
.map((source) => {
|
|
34
|
+
// A declared root is user input too — hold it to the SAME escape-hatch
|
|
35
|
+
// refusal as `sandbox.mounts` (JC-G): never mount the docker socket, the
|
|
36
|
+
// cruxy credential home, or `$HOME`/`/` even if declared as a root.
|
|
37
|
+
const forbidden = forbiddenMountSource(source);
|
|
38
|
+
if (forbidden) {
|
|
39
|
+
throw configInvalid(`refusing to mount workspace root ${forbidden}: "${source}"`);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
source,
|
|
43
|
+
target: source, // identical path, like the workdir, for path coherence
|
|
44
|
+
readonly: !writable.has(source), // RO unless explicitly escalated (R5)
|
|
45
|
+
};
|
|
46
|
+
});
|
|
38
47
|
return {
|
|
39
48
|
image: cfg.image,
|
|
40
49
|
network: cfg.network,
|
|
@@ -10,8 +10,15 @@ export interface SandboxReporter {
|
|
|
10
10
|
}
|
|
11
11
|
export interface SandboxServiceDeps {
|
|
12
12
|
config: CruxyConfig;
|
|
13
|
-
/** The
|
|
13
|
+
/** The command's own workspace root — mounted read-write as the workdir. */
|
|
14
14
|
cwd: string;
|
|
15
|
+
/** The OTHER declared roots in a multi-repo session (C.26, R5). Each is
|
|
16
|
+
* mounted READ-ONLY so cross-repo reads work but a command can't silently
|
|
17
|
+
* write a sibling. Empty/absent in a single-root session. */
|
|
18
|
+
siblingRoots?: readonly string[];
|
|
19
|
+
/** Sibling roots granted an approved cross-root-write escalation (R5): each
|
|
20
|
+
* named one flips to read-write, and ONLY that one — never blanket. */
|
|
21
|
+
writableRoots?: readonly string[];
|
|
15
22
|
/** Execution runtime seam (defaults to Docker). */
|
|
16
23
|
runtime?: SandboxRuntime;
|
|
17
24
|
/** Capability probe seam (defaults to real docker detection). */
|
package/dist/sandbox/service.js
CHANGED
|
@@ -34,7 +34,10 @@ export class SandboxService {
|
|
|
34
34
|
if (!capability.available) {
|
|
35
35
|
throw sandboxUnavailable(capability.runtime, capability.detail);
|
|
36
36
|
}
|
|
37
|
-
const policy = buildPolicy(deps.config.sandbox, deps.cwd
|
|
37
|
+
const policy = buildPolicy(deps.config.sandbox, deps.cwd, {
|
|
38
|
+
siblingRoots: deps.siblingRoots,
|
|
39
|
+
writableRoots: deps.writableRoots,
|
|
40
|
+
});
|
|
38
41
|
return new SandboxService(runtime, policy, deps.reporter);
|
|
39
42
|
}
|
|
40
43
|
/** The runtime backing this service (e.g. "docker") — for logging. */
|
package/dist/subagent/index.d.ts
CHANGED