@sagentlab/navarch-runtime 0.1.39 → 0.1.41
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/README.md +57 -12
- package/dist/adapters/acp.cjs +268 -0
- package/dist/adapters/index.cjs +6 -1
- package/dist/cli.cjs +11 -5
- package/dist/code-graph.cjs +198 -0
- package/dist/config.cjs +4 -1
- package/dist/machine-store.cjs +3 -1
- package/dist/prompt.cjs +3 -1
- package/dist/session.cjs +7 -3
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
The machine-side half of Navarch (WP-07): registers this machine with the
|
|
4
4
|
control plane, then loops claim → sandbox → agent CLI → report until you stop
|
|
5
|
-
it. Plain Node/TypeScript,
|
|
5
|
+
it. Plain Node/TypeScript, no Next.js coupling, and no shared app-code coupling
|
|
6
6
|
— this directory is a self-contained package you can `npx` on any fresh
|
|
7
7
|
machine.
|
|
8
8
|
|
|
9
9
|
The machine operator selects **Claude Code**, **OpenAI Codex**, **Google
|
|
10
|
-
Gemini CLI**, or **
|
|
10
|
+
Gemini CLI**, **OpenCode**, or an **Agent Client Protocol (ACP)** agent when connecting a worker. Any selected agent can
|
|
11
11
|
run any project task; task eligibility depends on capabilities and project
|
|
12
12
|
gates, not agent type — see "Choosing an agent" below.
|
|
13
13
|
|
|
@@ -29,7 +29,7 @@ Before generating the command, prepare the machine:
|
|
|
29
29
|
- install Node.js 20 or later (the standard installation includes `npm` and
|
|
30
30
|
`npx`) and Git;
|
|
31
31
|
- install the coding-agent CLI selected in Navarch — `claude`, `codex`,
|
|
32
|
-
`gemini`, or `
|
|
32
|
+
`gemini`, `opencode`, or `dsh` for the default ACP integration — and complete its normal authentication flow; and
|
|
33
33
|
- for Docker-backed sessions, install and start Docker and use an image that
|
|
34
34
|
contains the selected agent CLI and the repository toolchain, with provider
|
|
35
35
|
credentials supplied through the approved machine or project configuration.
|
|
@@ -44,7 +44,7 @@ node --version # 20 or later
|
|
|
44
44
|
npm --version
|
|
45
45
|
npx --version
|
|
46
46
|
git --version
|
|
47
|
-
codex --version # or: claude
|
|
47
|
+
codex --version # or: claude / gemini / opencode / dsh --version
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
Every command above must succeed before the long-running worker starts. See
|
|
@@ -152,10 +152,10 @@ The from-source flow — `git clone` + `./install.sh` + `node bin/navarch.cjs
|
|
|
152
152
|
|
|
153
153
|
| Command | Purpose |
|
|
154
154
|
|---|---|
|
|
155
|
-
| `register --token <t> --name <n> [--agent claude-code\|codex\|gemini\|opencode] […]` | Registers this machine, saves its local agent choice, and prints the token once. |
|
|
156
|
-
| `connect --token <t> --name <n> [--agent claude-code\|codex\|gemini\|opencode] [--project <id>] […]` | Connects this machine to one project, saves its local agent choice, and prints the token once. |
|
|
157
|
-
| `start [--agent claude-code\|codex\|gemini\|opencode]` | Runs the daemon. A start-time agent choice overrides the saved choice. |
|
|
158
|
-
| `supervise [--agent claude-code\|codex\|gemini\|opencode]` | Runs the daemon under the update supervisor, enabling drain-safe automatic updates and rollback. |
|
|
155
|
+
| `register --token <t> --name <n> [--agent claude-code\|codex\|gemini\|opencode\|acp] […]` | Registers this machine, saves its local agent choice, and prints the token once. |
|
|
156
|
+
| `connect --token <t> --name <n> [--agent claude-code\|codex\|gemini\|opencode\|acp] [--project <id>] […]` | Connects this machine to one project, saves its local agent choice, and prints the token once. |
|
|
157
|
+
| `start [--agent claude-code\|codex\|gemini\|opencode\|acp]` | Runs the daemon. A start-time agent choice overrides the saved choice. |
|
|
158
|
+
| `supervise [--agent claude-code\|codex\|gemini\|opencode\|acp]` | Runs the daemon under the update supervisor, enabling drain-safe automatic updates and rollback. |
|
|
159
159
|
| `doctor` | Prints resolved config + Docker/registration status; no side effects. |
|
|
160
160
|
|
|
161
161
|
### Running multiple agents on one machine
|
|
@@ -311,7 +311,7 @@ unchanged across the deployment.
|
|
|
311
311
|
| `NAVARCH_SANDBOX_PROFILE` | `trusted-development` | Named security profile for Docker sessions (`src/sandbox-profile.cts`): `trusted-development` (image-default user, uncapped, open egress), `untrusted-code` (non-root, 2 CPU / 4g / 512 PIDs, deny-by-default egress, read-only shared git), `elevated-verification` (non-root, 4 CPU / 8g / 2048 PIDs, egress limited to GitHub plus package registries). The default is the exact pre-profile flag set. |
|
|
312
312
|
| `NAVARCH_SANDBOX_EGRESS_NETWORK` | _(unset)_ | Docker network that enforces a profile's egress allowlist. Docker cannot filter by domain itself, so an allowlist profile without this fails closed to `--network=none` and records the denial. |
|
|
313
313
|
| `NAVARCH_DOCKER_IMAGE` | `ghcr.io/sagentlab/navarch-sandbox-agent:0.1.0` | Version-pinned per-session image with Node 20, git, GitHub CLI, ripgrep, jq, SSH, and Claude Code 2.1.218. Override with an image tag or digest you control. |
|
|
314
|
-
| `NAVARCH_AGENT` | saved choice, then `claude-code` | Local choice of agent
|
|
314
|
+
| `NAVARCH_AGENT` | saved choice, then `claude-code` | Local choice of agent adapter: `claude-code`, `codex`, `gemini`, `opencode`, or `acp`. Overrides the choice saved by `connect`/`register`; `start --agent` has highest priority. |
|
|
315
315
|
| `NAVARCH_RUNTIMES` | selected `NAVARCH_AGENT` | Comma list of installed/authenticated adapters advertised to dispatch. The control plane chooses among these per project/task. |
|
|
316
316
|
| `NAVARCH_UPDATE_CHANNEL` | `stable` | Release channel advertised by the worker (`stable` or `canary`); the server-managed machine channel remains authoritative. |
|
|
317
317
|
| `NAVARCH_AUTO_UPDATE` | on under `supervise` | Set `off`, `false`, or `0` to report releases without staging or activating them. Automatic activation is always off under plain `start`. |
|
|
@@ -323,6 +323,8 @@ unchanged across the deployment.
|
|
|
323
323
|
| `NAVARCH_GEMINI_EXTRA_ARGS` | — | Comma list of extra CLI args appended after the generated MCP settings, `stream-json`, and unattended defaults (Gemini). |
|
|
324
324
|
| `NAVARCH_OPENCODE_BIN` | `opencode` | Path/name of the OpenCode CLI binary. |
|
|
325
325
|
| `NAVARCH_OPENCODE_EXTRA_ARGS` | — | Comma list of extra CLI args appended after `run`, the prompt, and the generated `--format json` argument. Explicit `--format`, `--model`, or `--variant` values replace the corresponding per-session default. |
|
|
326
|
+
| `NAVARCH_ACP_BIN` | `dsh` | Path/name of an Agent Client Protocol v1 stdio server. DeepSeek Harness is the default implementation. |
|
|
327
|
+
| `NAVARCH_ACP_EXTRA_ARGS` | `--profile,acp` | Comma list of arguments used to start the ACP server. Override this together with `NAVARCH_ACP_BIN` for another ACP-compatible coding agent. |
|
|
326
328
|
| `NAVARCH_MCP_CONFIG_PATH` | — | Path to the platform MCP config passed as `--mcp-config`. |
|
|
327
329
|
| `NAVARCH_WORKTREE_GUARD` | on | Host-mode sessions get an adapter-native per-session worktree boundary guard (see below). Set `off` to disable. |
|
|
328
330
|
| `NAVARCH_GUARD_EXTRA_ROOTS` | — | `path.delimiter`-separated (`:` on POSIX) extra directories the worktree guard allows beyond the session worktree, shared bare repo, and temp dirs. |
|
|
@@ -383,6 +385,10 @@ as an explicit read-only mount.
|
|
|
383
385
|
OpenCode receives a private, per-session config, project config discovery is
|
|
384
386
|
disabled, and lease MCP headers are referenced through child-only environment
|
|
385
387
|
variables instead of being copied into its config file or argv.
|
|
388
|
+
- **ACP agents:** the protocol client rejects every unattended permission
|
|
389
|
+
escalation. The agent remains responsible for enforcing its declared file
|
|
390
|
+
sandbox; use Navarch Docker mode when the ACP server itself does not provide
|
|
391
|
+
a trustworthy workspace boundary.
|
|
386
392
|
|
|
387
393
|
The resulting boundary is:
|
|
388
394
|
|
|
@@ -443,8 +449,12 @@ export NAVARCH_AGENT=gemini
|
|
|
443
449
|
# use a Docker image containing OpenCode for the normal isolated path.
|
|
444
450
|
export NAVARCH_AGENT=opencode
|
|
445
451
|
|
|
452
|
+
# Agent Client Protocol — defaults to DeepSeek Harness `dsh --profile acp`.
|
|
453
|
+
# Override NAVARCH_ACP_BIN / NAVARCH_ACP_EXTRA_ARGS for another ACP v1 server.
|
|
454
|
+
export NAVARCH_AGENT=acp
|
|
455
|
+
|
|
446
456
|
# Advanced compatibility mode: advertise every installed adapter.
|
|
447
|
-
export NAVARCH_RUNTIMES=claude-code,codex,gemini,opencode
|
|
457
|
+
export NAVARCH_RUNTIMES=claude-code,codex,gemini,opencode,acp
|
|
448
458
|
```
|
|
449
459
|
|
|
450
460
|
For the legacy single-runtime setting, priority is `start --agent` →
|
|
@@ -453,7 +463,7 @@ For the legacy single-runtime setting, priority is `start --agent` →
|
|
|
453
463
|
the locally selected `NAVARCH_AGENT` handles every claimed task. Sandbox
|
|
454
464
|
projects remain constrained to Claude Code.
|
|
455
465
|
|
|
456
|
-
All
|
|
466
|
+
All five BYO adapters implement the same `AgentAdapter` interface
|
|
457
467
|
(`src/adapters/types.cts`) and run either directly on the host or via
|
|
458
468
|
`docker exec` in the session's sandbox container, exactly like the Claude
|
|
459
469
|
adapter always has — `session.cts` picks one (`src/adapters/index.cts`'s
|
|
@@ -469,7 +479,8 @@ effort on completion. Gemini currently uses its `auto` model default and does
|
|
|
469
479
|
not expose a reasoning-effort flag. OpenCode accepts any `provider/model`
|
|
470
480
|
reference (the platform default is `opencode/x-preview-f-free`, Ox Alpha Free
|
|
471
481
|
(Unlimited) on OpenCode Zen), or `default` to preserve the authenticated
|
|
472
|
-
account's selection.
|
|
482
|
+
account's selection. ACP sessions use the agent's advertised default model and
|
|
483
|
+
set the standard `reasoning_effort` configuration option when it is available.
|
|
473
484
|
Machine-wide extra arguments still configure other CLI behavior; dispatched
|
|
474
485
|
model policy wins.
|
|
475
486
|
|
|
@@ -527,6 +538,15 @@ to OpenCode's remote-server config with OAuth disabled and environment-backed
|
|
|
527
538
|
headers. Missing accounting fields stay absent rather than becoming a
|
|
528
539
|
fabricated zero.
|
|
529
540
|
|
|
541
|
+
The ACP adapter speaks the standard [Agent Client Protocol](https://agentclientprotocol.com)
|
|
542
|
+
v1 JSON-RPC transport over stdio. It creates one protocol session in the task
|
|
543
|
+
worktree, converts Navarch's HTTP/stdio MCP entries, consumes semantic message,
|
|
544
|
+
thought, and usage updates, and fails permission requests closed because no
|
|
545
|
+
human is attached to a worker turn. [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
|
|
546
|
+
ships that server as `dsh --profile acp`. This is intentionally not the
|
|
547
|
+
REST-based [Agent Communication Protocol](https://agentcommunicationprotocol.dev),
|
|
548
|
+
which is a different protocol now maintained as part of A2A.
|
|
549
|
+
|
|
530
550
|
## Architecture
|
|
531
551
|
|
|
532
552
|
```
|
|
@@ -547,6 +567,8 @@ cli.cts
|
|
|
547
567
|
- claudeCodeAdapter (adapters/claude.cts) — `claude -p <prompt> --mcp-config <path>`
|
|
548
568
|
- codexAdapter (adapters/codex.cts) — `codex exec <prompt> --json -c mcp_servers.*=...`
|
|
549
569
|
- geminiAdapter (adapters/gemini.cts) — `gemini --prompt <prompt> --output-format stream-json`
|
|
570
|
+
- openCodeAdapter (adapters/opencode.cts) — `opencode run <prompt> --format json`
|
|
571
|
+
- acpAdapter (adapters/acp.cts) — ACP v1 JSON-RPC/stdio; defaults to `dsh --profile acp`
|
|
550
572
|
heartbeating the lease every NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS throughout either;
|
|
551
573
|
a failed heartbeat aborts the run (kills the process) and marks the outcome as lease-lost
|
|
552
574
|
5. mapExitCondition (exit-conditions.cts) → redact.cts scrubs the transcript → upload.cts PUTs it
|
|
@@ -716,3 +738,26 @@ not match, making it a clean, zero-touch way to keep this package fully
|
|
|
716
738
|
isolated. Vite/Vitest's default esbuild transform filter also excludes
|
|
717
739
|
`.cts` by default — `vitest.config.cts` overrides it (`esbuild.include`) so
|
|
718
740
|
tests are actually type-stripped and run.
|
|
741
|
+
|
|
742
|
+
## Inspect code dependencies
|
|
743
|
+
|
|
744
|
+
`navarch-runtime code-graph --query saveInvoice --mode impact --commit HEAD`
|
|
745
|
+
returns JSON with the indexed commit, checkout HEAD, dirty-worktree warning,
|
|
746
|
+
source paths and line numbers, and bounded dependency edges. Modes are `search`,
|
|
747
|
+
`callers`, `callees`, and `impact`; impact accepts `--depth 1` through `5`.
|
|
748
|
+
Use an exact node ID to disambiguate duplicate symbol names. A file query includes
|
|
749
|
+
its functions and file import dependencies. `--repo` defaults to the current
|
|
750
|
+
working directory. The CLI works without a Navarch login or network access.
|
|
751
|
+
|
|
752
|
+
This first implementation analyzes committed JavaScript and TypeScript with the
|
|
753
|
+
TypeScript compiler. It resolves static imports and named functions/methods,
|
|
754
|
+
including imported aliases. It does not execute repository code, follow symlinks,
|
|
755
|
+
load tsconfig plugins, or upload source. Root tsconfig path aliases are supported;
|
|
756
|
+
extended configs, dynamic dispatch, external dependencies and other languages
|
|
757
|
+
are not fully resolved. Warnings report omissions. No graph is an assurance that
|
|
758
|
+
a change has no other effects: inspect the actual diff and run relevant tests.
|
|
759
|
+
|
|
760
|
+
Each invocation rebuilds from Git blobs at the requested commit, avoiding stale
|
|
761
|
+
persistent indexes. Bounds are 2,000 files, 10 MB total source, 500 KB per file,
|
|
762
|
+
100 returned nodes and 300 edges. A later persistent index can optimize large
|
|
763
|
+
repositories without changing the query format.
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.acpAdapter = void 0;
|
|
4
|
+
exports.runAcpAdapter = runAcpAdapter;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const node_stream_1 = require("node:stream");
|
|
8
|
+
/**
|
|
9
|
+
* How long a cancelled agent may take to observe `session/cancel` and settle
|
|
10
|
+
* the in-flight `session/prompt` before the subprocess is terminated. The
|
|
11
|
+
* notification is only written to the child's stdin; without this window the
|
|
12
|
+
* SIGTERM in `terminateChild` can land before the agent's event loop ever
|
|
13
|
+
* reads the line, so the protocol cancellation is silently lost and the agent
|
|
14
|
+
* gets no chance to release what it is holding.
|
|
15
|
+
*/
|
|
16
|
+
const CANCEL_GRACE_MS = 2_000;
|
|
17
|
+
/**
|
|
18
|
+
* Generic Agent Client Protocol v1 adapter. DeepSeek Harness is the default
|
|
19
|
+
* implementation (`dsh --profile acp`), while NAVARCH_ACP_BIN and
|
|
20
|
+
* NAVARCH_ACP_EXTRA_ARGS let an operator substitute any ACP-compatible coding
|
|
21
|
+
* agent without adding another Navarch adapter.
|
|
22
|
+
*
|
|
23
|
+
* The similarly named Agent Communication Protocol at
|
|
24
|
+
* agentcommunicationprotocol.dev is a different, REST-based agent-to-agent
|
|
25
|
+
* protocol. Coding-agent subprocesses such as dsh expose Agent Client Protocol
|
|
26
|
+
* over newline-delimited JSON-RPC on stdio, which is the boundary implemented
|
|
27
|
+
* here.
|
|
28
|
+
*/
|
|
29
|
+
async function runAcpAdapter(options) {
|
|
30
|
+
const acp = await import("@agentclientprotocol/sdk");
|
|
31
|
+
const mcpServers = await readMcpServers(options.mcpConfigPath, options.env);
|
|
32
|
+
const child = spawnAcpAgent(options);
|
|
33
|
+
const rawStdout = [];
|
|
34
|
+
const stderr = [];
|
|
35
|
+
const protocolInput = new node_stream_1.PassThrough();
|
|
36
|
+
const messages = [];
|
|
37
|
+
const thoughts = [];
|
|
38
|
+
let costUsd;
|
|
39
|
+
let timedOut = false;
|
|
40
|
+
let killedByLeaseLoss = false;
|
|
41
|
+
let sessionId;
|
|
42
|
+
let stopReason;
|
|
43
|
+
// Resolved once the in-flight prompt request settles, however it settles, so
|
|
44
|
+
// cancellation can wait for the agent instead of racing the kill.
|
|
45
|
+
let settlePrompt = () => undefined;
|
|
46
|
+
const promptSettled = new Promise((resolve) => {
|
|
47
|
+
settlePrompt = resolve;
|
|
48
|
+
});
|
|
49
|
+
child.stdout.on("data", (chunk) => {
|
|
50
|
+
rawStdout.push(chunk);
|
|
51
|
+
protocolInput.write(chunk);
|
|
52
|
+
});
|
|
53
|
+
child.stdout.on("end", () => protocolInput.end());
|
|
54
|
+
child.stderr.setEncoding("utf8");
|
|
55
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
56
|
+
child.on("error", (error) => {
|
|
57
|
+
stderr.push(`\n${describeError(error)}`);
|
|
58
|
+
protocolInput.end();
|
|
59
|
+
});
|
|
60
|
+
const stream = acp.ndJsonStream(node_stream_1.Writable.toWeb(child.stdin), node_stream_1.Readable.toWeb(protocolInput));
|
|
61
|
+
const client = acp.client({ name: "navarch-runtime" })
|
|
62
|
+
.onNotification(acp.methods.client.session.update, ({ params }) => {
|
|
63
|
+
collectUpdate(params, messages, thoughts, (value) => {
|
|
64
|
+
costUsd = value;
|
|
65
|
+
});
|
|
66
|
+
})
|
|
67
|
+
// Navarch sessions are unattended. Permission escalation therefore fails
|
|
68
|
+
// closed instead of inventing human consent on the worker's behalf.
|
|
69
|
+
.onRequest(acp.methods.client.session.requestPermission, ({ params }) => {
|
|
70
|
+
const rejection = params.options.find((option) => option.kind === "reject_always")
|
|
71
|
+
?? params.options.find((option) => option.kind === "reject_once");
|
|
72
|
+
return rejection
|
|
73
|
+
? { outcome: { outcome: "selected", optionId: rejection.optionId } }
|
|
74
|
+
: { outcome: { outcome: "cancelled" } };
|
|
75
|
+
});
|
|
76
|
+
const connection = client.connect(stream);
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
let abortProtocol = Promise.resolve();
|
|
79
|
+
controller.signal.addEventListener("abort", () => {
|
|
80
|
+
abortProtocol = (async () => {
|
|
81
|
+
if (sessionId) {
|
|
82
|
+
await connection.agent.notify(acp.methods.agent.session.cancel, { sessionId }).catch(() => undefined);
|
|
83
|
+
await Promise.race([
|
|
84
|
+
promptSettled,
|
|
85
|
+
new Promise((resolve) => setTimeout(resolve, CANCEL_GRACE_MS)),
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
connection.close();
|
|
89
|
+
})();
|
|
90
|
+
}, { once: true });
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
timedOut = true;
|
|
93
|
+
controller.abort();
|
|
94
|
+
}, options.timeoutMs);
|
|
95
|
+
const onAbort = () => {
|
|
96
|
+
killedByLeaseLoss = true;
|
|
97
|
+
controller.abort();
|
|
98
|
+
};
|
|
99
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
100
|
+
if (options.signal?.aborted)
|
|
101
|
+
onAbort();
|
|
102
|
+
try {
|
|
103
|
+
const context = connection.agent;
|
|
104
|
+
await context.request(acp.methods.agent.initialize, {
|
|
105
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
106
|
+
clientCapabilities: {},
|
|
107
|
+
});
|
|
108
|
+
const created = await context.request(acp.methods.agent.session.new, {
|
|
109
|
+
cwd: options.cwd ?? process.cwd(),
|
|
110
|
+
mcpServers,
|
|
111
|
+
});
|
|
112
|
+
sessionId = created.sessionId;
|
|
113
|
+
let configOptions = created.configOptions ?? [];
|
|
114
|
+
configOptions = await setConfigOption(context, acp.methods.agent.session.setConfigOption, sessionId, configOptions, "model", options.model === "default" ? undefined : options.model);
|
|
115
|
+
await setConfigOption(context, acp.methods.agent.session.setConfigOption, sessionId, configOptions, "reasoning_effort", options.reasoningEffort);
|
|
116
|
+
let response;
|
|
117
|
+
try {
|
|
118
|
+
response = await context.request(acp.methods.agent.session.prompt, {
|
|
119
|
+
sessionId,
|
|
120
|
+
prompt: [{ type: "text", text: options.prompt }],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
settlePrompt();
|
|
125
|
+
}
|
|
126
|
+
stopReason = response.stopReason;
|
|
127
|
+
await Promise.race([
|
|
128
|
+
context.request(acp.methods.agent.session.close, { sessionId }).catch(() => undefined),
|
|
129
|
+
new Promise((resolve) => setTimeout(resolve, Math.min(5_000, options.timeoutMs))),
|
|
130
|
+
]);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (!controller.signal.aborted)
|
|
134
|
+
stderr.push(`\n${describeError(error)}`);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
139
|
+
await abortProtocol;
|
|
140
|
+
connection.close();
|
|
141
|
+
await terminateChild(child, options, controller.signal.aborted);
|
|
142
|
+
}
|
|
143
|
+
if (thoughts.length > 0)
|
|
144
|
+
stderr.push(`\n# ACP agent thoughts\n${thoughts.join("\n")}`);
|
|
145
|
+
const reportText = messages.join("").trim();
|
|
146
|
+
const successfulStop = stopReason === "end_turn" || stopReason === "max_tokens" || stopReason === "max_turn_requests";
|
|
147
|
+
return {
|
|
148
|
+
exitCode: successfulStop ? 0 : timedOut || killedByLeaseLoss ? null : 1,
|
|
149
|
+
timedOut,
|
|
150
|
+
killedByLeaseLoss,
|
|
151
|
+
stdout: Buffer.concat(rawStdout).toString("utf8"),
|
|
152
|
+
stderr: stderr.join(""),
|
|
153
|
+
...(reportText ? { reportText } : {}),
|
|
154
|
+
...(costUsd !== undefined ? { costUsd } : {}),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function collectUpdate(notification, messages, thoughts, setCostUsd) {
|
|
158
|
+
const update = notification.update;
|
|
159
|
+
if ((update.sessionUpdate === "agent_message_chunk" || update.sessionUpdate === "agent_thought_chunk")
|
|
160
|
+
&& update.content.type === "text") {
|
|
161
|
+
(update.sessionUpdate === "agent_message_chunk" ? messages : thoughts).push(update.content.text);
|
|
162
|
+
}
|
|
163
|
+
if (update.sessionUpdate === "usage_update"
|
|
164
|
+
&& update.cost?.currency.toUpperCase() === "USD"
|
|
165
|
+
&& Number.isFinite(update.cost.amount)
|
|
166
|
+
&& update.cost.amount >= 0) {
|
|
167
|
+
setCostUsd(update.cost.amount);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function setConfigOption(context, method, sessionId, options, configId, desired) {
|
|
171
|
+
if (!desired)
|
|
172
|
+
return options;
|
|
173
|
+
const expectedCategory = configId === "model" ? "model" : "thought_level";
|
|
174
|
+
const option = options.find((candidate) => ((candidate.id === configId || candidate.category === expectedCategory)
|
|
175
|
+
&& candidate.type === "select"));
|
|
176
|
+
if (!option || option.type !== "select")
|
|
177
|
+
return options;
|
|
178
|
+
const choices = option.options.flatMap((candidate) => "group" in candidate ? candidate.options : [candidate]);
|
|
179
|
+
const selected = choices.find((candidate) => candidate.value === desired || candidate.name === desired);
|
|
180
|
+
if (!selected || selected.value === option.currentValue)
|
|
181
|
+
return options;
|
|
182
|
+
const response = await context.request(method, { sessionId, configId, value: selected.value });
|
|
183
|
+
return response.configOptions ?? options;
|
|
184
|
+
}
|
|
185
|
+
async function readMcpServers(configPath, env) {
|
|
186
|
+
if (!configPath)
|
|
187
|
+
return [];
|
|
188
|
+
const parsed = JSON.parse(await node_fs_1.promises.readFile(configPath, "utf8"));
|
|
189
|
+
return Object.entries(parsed.mcpServers ?? {}).flatMap(([name, server]) => {
|
|
190
|
+
const url = server.httpUrl ?? server.url;
|
|
191
|
+
if (url) {
|
|
192
|
+
return [{
|
|
193
|
+
type: "http",
|
|
194
|
+
name,
|
|
195
|
+
url,
|
|
196
|
+
headers: Object.entries(server.headers ?? {}).map(([header, value]) => ({
|
|
197
|
+
name: header,
|
|
198
|
+
value: resolveEnvReference(value, env),
|
|
199
|
+
})),
|
|
200
|
+
}];
|
|
201
|
+
}
|
|
202
|
+
if (!server.command)
|
|
203
|
+
return [];
|
|
204
|
+
return [{
|
|
205
|
+
name,
|
|
206
|
+
command: server.command,
|
|
207
|
+
args: server.args ?? [],
|
|
208
|
+
env: Object.entries(server.env ?? {}).map(([key, value]) => ({
|
|
209
|
+
name: key,
|
|
210
|
+
value: resolveEnvReference(value, env),
|
|
211
|
+
})),
|
|
212
|
+
}];
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function resolveEnvReference(value, env) {
|
|
216
|
+
const name = value.match(/^\{env:([A-Z_][A-Z0-9_]*)\}$/)?.[1];
|
|
217
|
+
if (!name)
|
|
218
|
+
return value;
|
|
219
|
+
const resolved = env[name] ?? process.env[name];
|
|
220
|
+
if (resolved === undefined)
|
|
221
|
+
throw new Error(`ACP MCP configuration references missing environment variable ${name}.`);
|
|
222
|
+
return resolved;
|
|
223
|
+
}
|
|
224
|
+
function spawnAcpAgent(options) {
|
|
225
|
+
const env = { ...process.env, ...options.env };
|
|
226
|
+
if (!options.dockerExec) {
|
|
227
|
+
return (0, node_child_process_1.spawn)(options.bin, options.extraArgs, {
|
|
228
|
+
cwd: options.cwd,
|
|
229
|
+
env,
|
|
230
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
const { containerName } = options.dockerExec;
|
|
234
|
+
const forwardedEnv = Object.keys(options.env).flatMap((name) => ["--env", name]);
|
|
235
|
+
return (0, node_child_process_1.spawn)("docker", [
|
|
236
|
+
"exec",
|
|
237
|
+
"-i",
|
|
238
|
+
...forwardedEnv,
|
|
239
|
+
containerName,
|
|
240
|
+
"sh",
|
|
241
|
+
"-c",
|
|
242
|
+
'[ -f /tmp/session.env ] && . /tmp/session.env; exec "$@"',
|
|
243
|
+
"sh",
|
|
244
|
+
options.bin,
|
|
245
|
+
...options.extraArgs,
|
|
246
|
+
], { env, stdio: ["pipe", "pipe", "pipe"] });
|
|
247
|
+
}
|
|
248
|
+
async function terminateChild(child, options, forceContainerKill) {
|
|
249
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
250
|
+
child.kill("SIGTERM");
|
|
251
|
+
await Promise.race([
|
|
252
|
+
new Promise((resolve) => child.once("close", () => resolve())),
|
|
253
|
+
new Promise((resolve) => setTimeout(resolve, 1_000)),
|
|
254
|
+
]);
|
|
255
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
256
|
+
child.kill("SIGKILL");
|
|
257
|
+
}
|
|
258
|
+
if (options.dockerExec && forceContainerKill) {
|
|
259
|
+
await options.dockerExec.runner.run("docker", ["kill", options.dockerExec.containerName]).catch(() => undefined);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function describeError(error) {
|
|
263
|
+
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
264
|
+
}
|
|
265
|
+
exports.acpAdapter = {
|
|
266
|
+
agentType: "acp",
|
|
267
|
+
run: runAcpAdapter,
|
|
268
|
+
};
|
package/dist/adapters/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.runPiAdapter = exports.piAdapter = exports.runOpenCodeAdapter = exports.openCodeAdapter = exports.runGeminiAdapter = exports.geminiAdapter = exports.runCodexAdapter = exports.codexAdapter = exports.runClaudeCodeAdapter = exports.claudeCodeAdapter = void 0;
|
|
3
|
+
exports.runAcpAdapter = exports.acpAdapter = exports.runPiAdapter = exports.piAdapter = exports.runOpenCodeAdapter = exports.openCodeAdapter = exports.runGeminiAdapter = exports.geminiAdapter = exports.runCodexAdapter = exports.codexAdapter = exports.runClaudeCodeAdapter = exports.claudeCodeAdapter = void 0;
|
|
4
4
|
exports.selectAdapter = selectAdapter;
|
|
5
5
|
const claude_cjs_1 = require("./claude.cjs");
|
|
6
6
|
Object.defineProperty(exports, "claudeCodeAdapter", { enumerable: true, get: function () { return claude_cjs_1.claudeCodeAdapter; } });
|
|
@@ -17,6 +17,9 @@ Object.defineProperty(exports, "runOpenCodeAdapter", { enumerable: true, get: fu
|
|
|
17
17
|
const pi_cjs_1 = require("./pi.cjs");
|
|
18
18
|
Object.defineProperty(exports, "piAdapter", { enumerable: true, get: function () { return pi_cjs_1.piAdapter; } });
|
|
19
19
|
Object.defineProperty(exports, "runPiAdapter", { enumerable: true, get: function () { return pi_cjs_1.runPiAdapter; } });
|
|
20
|
+
const acp_cjs_1 = require("./acp.cjs");
|
|
21
|
+
Object.defineProperty(exports, "acpAdapter", { enumerable: true, get: function () { return acp_cjs_1.acpAdapter; } });
|
|
22
|
+
Object.defineProperty(exports, "runAcpAdapter", { enumerable: true, get: function () { return acp_cjs_1.runAcpAdapter; } });
|
|
20
23
|
/**
|
|
21
24
|
* Picks the AgentAdapter (adapters/types.cts) session.cts should run a
|
|
22
25
|
* session with, keyed off config.cts's `agentType` (NAVARCH_AGENT). This is
|
|
@@ -32,6 +35,8 @@ function selectAdapter(agentType) {
|
|
|
32
35
|
return gemini_cjs_1.geminiAdapter;
|
|
33
36
|
case "opencode":
|
|
34
37
|
return opencode_cjs_1.openCodeAdapter;
|
|
38
|
+
case "acp":
|
|
39
|
+
return acp_cjs_1.acpAdapter;
|
|
35
40
|
case "claude-code":
|
|
36
41
|
return claude_cjs_1.claudeCodeAdapter;
|
|
37
42
|
case "pi":
|
package/dist/cli.cjs
CHANGED
|
@@ -66,7 +66,7 @@ function agentFromFlag(flags) {
|
|
|
66
66
|
if (value === undefined)
|
|
67
67
|
return undefined;
|
|
68
68
|
if (!(0, config_cjs_1.isRuntimeAgentType)(value)) {
|
|
69
|
-
throw new Error("--agent must be one of 'claude-code', 'codex', 'gemini', or '
|
|
69
|
+
throw new Error("--agent must be one of 'claude-code', 'codex', 'gemini', 'opencode', or 'acp'.");
|
|
70
70
|
}
|
|
71
71
|
return value;
|
|
72
72
|
}
|
|
@@ -301,12 +301,13 @@ function helpText() {
|
|
|
301
301
|
|
|
302
302
|
Usage:
|
|
303
303
|
navarch-runtime register --token <enrollment-token> --name <machine-name> \\
|
|
304
|
-
[--config-dir <path>] [--agent claude-code|codex|gemini|opencode] [--capabilities a,b] [--max-sessions N] [--owner-zone z] [--api-base url]
|
|
304
|
+
[--config-dir <path>] [--agent claude-code|codex|gemini|opencode|acp] [--capabilities a,b] [--max-sessions N] [--owner-zone z] [--api-base url]
|
|
305
305
|
navarch-runtime connect --token <enrollment-token> --name <machine-name> \\
|
|
306
|
-
[--config-dir <path>] [--agent claude-code|codex|gemini|opencode] [--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
|
|
307
|
-
navarch-runtime start [--config-dir <path>] [--agent claude-code|codex|gemini|opencode]
|
|
308
|
-
navarch-runtime supervise [--config-dir <path>] [--agent claude-code|codex|gemini|opencode]
|
|
306
|
+
[--config-dir <path>] [--agent claude-code|codex|gemini|opencode|acp] [--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
|
|
307
|
+
navarch-runtime start [--config-dir <path>] [--agent claude-code|codex|gemini|opencode|acp]
|
|
308
|
+
navarch-runtime supervise [--config-dir <path>] [--agent claude-code|codex|gemini|opencode|acp]
|
|
309
309
|
navarch-runtime doctor [--config-dir <path>]
|
|
310
|
+
navarch-runtime code-graph --query <symbol-or-file> [--mode search|callers|callees|impact] [--commit HEAD] [--depth 1..5] [--repo <path>]
|
|
310
311
|
|
|
311
312
|
Use a different --config-dir (or NAVARCH_CONFIG_DIR) for every agent instance.
|
|
312
313
|
Other configuration is via NAVARCH_* environment variables; see runtime/README.md.
|
|
@@ -328,6 +329,11 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
328
329
|
case "supervise":
|
|
329
330
|
await superviseCommand(flags);
|
|
330
331
|
break;
|
|
332
|
+
case "code-graph": {
|
|
333
|
+
const { codeGraphCommand } = await import("./code-graph.cjs");
|
|
334
|
+
console.log(JSON.stringify(codeGraphCommand(flags), null, 2));
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
331
337
|
case "doctor":
|
|
332
338
|
await doctorCommand(flags);
|
|
333
339
|
break;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readCodeSnapshot = readCodeSnapshot;
|
|
7
|
+
exports.buildCodeGraph = buildCodeGraph;
|
|
8
|
+
exports.queryCodeGraph = queryCodeGraph;
|
|
9
|
+
exports.codeGraphCommand = codeGraphCommand;
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
13
|
+
const ROOT = "/navarch-repo/";
|
|
14
|
+
const MAX_BYTES = 10_000_000;
|
|
15
|
+
const MAX_FILES = 2_000;
|
|
16
|
+
/** Reads committed blobs only. Never follows worktree symlinks, runs project code, or loads plugins. */
|
|
17
|
+
function readCodeSnapshot(cwd, ref = "HEAD") {
|
|
18
|
+
const git = (args, input) => (0, node_child_process_1.execFileSync)("git", args, {
|
|
19
|
+
cwd, input, encoding: "utf8", maxBuffer: 16_000_000, timeout: 15_000,
|
|
20
|
+
});
|
|
21
|
+
const commit = git(["rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`]).trim();
|
|
22
|
+
const head = git(["rev-parse", "HEAD"]).trim();
|
|
23
|
+
const dirty = Boolean(git(["status", "--porcelain", "--untracked-files=no"]).trim());
|
|
24
|
+
const warnings = [];
|
|
25
|
+
if (dirty)
|
|
26
|
+
warnings.push("Tracked worktree edits are not indexed; this graph describes committed code.");
|
|
27
|
+
if (commit !== head)
|
|
28
|
+
warnings.push("Indexed commit differs from checkout HEAD.");
|
|
29
|
+
const files = [];
|
|
30
|
+
let bytes = 0;
|
|
31
|
+
for (const entry of git(["ls-tree", "--full-tree", "-rlz", commit]).split("\0")) {
|
|
32
|
+
const match = /^(100644|100755) blob ([a-f0-9]+) +([0-9]+)\t(.+)$/s.exec(entry);
|
|
33
|
+
if (!match)
|
|
34
|
+
continue;
|
|
35
|
+
const file = match[4];
|
|
36
|
+
if ((!/\.(?:[cm]?[jt]sx?)$/.test(file) && file !== "tsconfig.json") ||
|
|
37
|
+
/(^|\/)(node_modules|dist|build|vendor|\.next)\//.test(file))
|
|
38
|
+
continue;
|
|
39
|
+
const size = Number(match[3]);
|
|
40
|
+
if (/[\r\n]/.test(file) || size > 500_000 || files.length >= MAX_FILES || bytes + size > MAX_BYTES) {
|
|
41
|
+
warnings.push(`Skipped source: ${file}`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
files.push({ file, sha: match[2], size });
|
|
45
|
+
bytes += size;
|
|
46
|
+
}
|
|
47
|
+
const contents = {};
|
|
48
|
+
if (files.length) {
|
|
49
|
+
// Object IDs, rather than paths, form the batch protocol input.
|
|
50
|
+
const output = (0, node_child_process_1.execFileSync)("git", ["cat-file", "--batch"], {
|
|
51
|
+
cwd, input: files.map((f) => f.sha).join("\n") + "\n", maxBuffer: 16_000_000, timeout: 15_000,
|
|
52
|
+
});
|
|
53
|
+
let offset = 0;
|
|
54
|
+
for (const file of files) {
|
|
55
|
+
const end = output.indexOf(10, offset);
|
|
56
|
+
if (end < 0 || output.subarray(offset, end).toString() !== `${file.sha} blob ${file.size}`) {
|
|
57
|
+
throw new Error("Invalid git object batch");
|
|
58
|
+
}
|
|
59
|
+
offset = end + 1;
|
|
60
|
+
contents[file.file] = output.subarray(offset, offset + file.size).toString("utf8");
|
|
61
|
+
offset += file.size + 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { commit, head, dirty, contents, warnings };
|
|
65
|
+
}
|
|
66
|
+
function buildCodeGraph(snapshot) {
|
|
67
|
+
const warnings = [...snapshot.warnings];
|
|
68
|
+
const sources = new Map(Object.entries(snapshot.contents).map(([file, text]) => [ROOT + file, text]));
|
|
69
|
+
const config = sources.get(ROOT + "tsconfig.json");
|
|
70
|
+
const raw = config ? typescript_1.default.parseConfigFileTextToJson("tsconfig.json", config) : { config: {} };
|
|
71
|
+
if (raw.error)
|
|
72
|
+
warnings.push("Could not parse tsconfig.json; using default module resolution.");
|
|
73
|
+
if (raw.config?.extends)
|
|
74
|
+
warnings.push("Inherited tsconfig options are not loaded; unresolved aliases may be omitted.");
|
|
75
|
+
const converted = typescript_1.default.convertCompilerOptionsFromJson(raw.config?.compilerOptions ?? {}, ROOT);
|
|
76
|
+
const options = { ...converted.options, allowJs: true, noLib: true, noEmit: true };
|
|
77
|
+
const canonical = (file) => node_path_1.default.posix.normalize(file);
|
|
78
|
+
const host = {
|
|
79
|
+
fileExists: (file) => sources.has(canonical(file)),
|
|
80
|
+
readFile: (file) => sources.get(canonical(file)),
|
|
81
|
+
getSourceFile: (file, languageVersion) => {
|
|
82
|
+
const text = sources.get(canonical(file));
|
|
83
|
+
return text === undefined ? undefined : typescript_1.default.createSourceFile(file, text, languageVersion, true);
|
|
84
|
+
},
|
|
85
|
+
getDefaultLibFileName: () => "", writeFile: () => { }, getCurrentDirectory: () => ROOT,
|
|
86
|
+
getDirectories: () => [], directoryExists: (dir) => [...sources.keys()].some((f) => f.startsWith(dir.replace(/\/$/, "") + "/")),
|
|
87
|
+
getCanonicalFileName: canonical, useCaseSensitiveFileNames: () => true, getNewLine: () => "\n",
|
|
88
|
+
};
|
|
89
|
+
const program = typescript_1.default.createProgram([...sources.keys()].filter((f) => f !== ROOT + "tsconfig.json"), options, host);
|
|
90
|
+
const checker = program.getTypeChecker();
|
|
91
|
+
const nodes = [];
|
|
92
|
+
const declarations = new Map();
|
|
93
|
+
const ownedFunctions = new Map();
|
|
94
|
+
const sourceFiles = program.getSourceFiles().filter((f) => sources.has(f.fileName));
|
|
95
|
+
for (const source of sourceFiles) {
|
|
96
|
+
const file = source.fileName.slice(ROOT.length);
|
|
97
|
+
const fileId = `file:${file}`;
|
|
98
|
+
nodes.push({ id: fileId, name: file, file, line: 1, kind: "file" });
|
|
99
|
+
declarations.set(source, fileId);
|
|
100
|
+
const collect = (node) => {
|
|
101
|
+
let name;
|
|
102
|
+
let body;
|
|
103
|
+
if (typescript_1.default.isFunctionDeclaration(node) || typescript_1.default.isMethodDeclaration(node) || typescript_1.default.isGetAccessorDeclaration(node) || typescript_1.default.isSetAccessorDeclaration(node)) {
|
|
104
|
+
name = node.name;
|
|
105
|
+
body = node.body;
|
|
106
|
+
}
|
|
107
|
+
else if (typescript_1.default.isVariableDeclaration(node) && node.initializer &&
|
|
108
|
+
(typescript_1.default.isArrowFunction(node.initializer) || typescript_1.default.isFunctionExpression(node.initializer))) {
|
|
109
|
+
name = node.name;
|
|
110
|
+
body = node.initializer;
|
|
111
|
+
}
|
|
112
|
+
if (name && body) {
|
|
113
|
+
const position = source.getLineAndCharacterOfPosition(name.getStart(source));
|
|
114
|
+
const line = position.line + 1;
|
|
115
|
+
const id = `${file}:${line}:${position.character + 1}:${name.getText(source)}`;
|
|
116
|
+
nodes.push({ id, name: name.getText(source), file, line, kind: "function" });
|
|
117
|
+
declarations.set(node, id);
|
|
118
|
+
ownedFunctions.set(body, id);
|
|
119
|
+
}
|
|
120
|
+
typescript_1.default.forEachChild(node, collect);
|
|
121
|
+
};
|
|
122
|
+
collect(source);
|
|
123
|
+
}
|
|
124
|
+
const edges = [];
|
|
125
|
+
let unresolved = 0;
|
|
126
|
+
const target = (node) => {
|
|
127
|
+
let symbol = checker.getSymbolAtLocation(node);
|
|
128
|
+
if (symbol && symbol.flags & typescript_1.default.SymbolFlags.Alias)
|
|
129
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
130
|
+
const targets = new Set(symbol?.declarations?.map((d) => declarations.get(d)).filter((id) => Boolean(id)));
|
|
131
|
+
return targets.size === 1 ? [...targets][0] : undefined;
|
|
132
|
+
};
|
|
133
|
+
for (const source of sourceFiles) {
|
|
134
|
+
const visit = (node, parent) => {
|
|
135
|
+
const owner = ownedFunctions.get(node) ?? parent;
|
|
136
|
+
if (typescript_1.default.isCallExpression(node)) {
|
|
137
|
+
const expression = typescript_1.default.isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression;
|
|
138
|
+
const to = target(expression);
|
|
139
|
+
if (to)
|
|
140
|
+
edges.push({ from: owner, to, kind: "call" });
|
|
141
|
+
else
|
|
142
|
+
unresolved++;
|
|
143
|
+
}
|
|
144
|
+
if (typescript_1.default.isImportDeclaration(node) || (typescript_1.default.isExportDeclaration(node) && node.moduleSpecifier)) {
|
|
145
|
+
const to = node.moduleSpecifier ? target(node.moduleSpecifier) : undefined;
|
|
146
|
+
if (to)
|
|
147
|
+
edges.push({ from: declarations.get(source), to, kind: "import" });
|
|
148
|
+
}
|
|
149
|
+
typescript_1.default.forEachChild(node, (child) => visit(child, owner));
|
|
150
|
+
};
|
|
151
|
+
visit(source, declarations.get(source));
|
|
152
|
+
}
|
|
153
|
+
warnings.push(`Static JS/TS analysis only; ${unresolved} external or unresolved calls omitted. Dynamic dispatch and runtime dependencies may be missing.`);
|
|
154
|
+
return { commit: snapshot.commit, head: snapshot.head, dirty: snapshot.dirty, nodes,
|
|
155
|
+
edges: [...new Map(edges.map((e) => [`${e.from}\0${e.to}\0${e.kind}`, e])).values()], warnings };
|
|
156
|
+
}
|
|
157
|
+
function queryCodeGraph(graph, query, mode = "search", depth = 3) {
|
|
158
|
+
if (!query.trim() || query.length > 500)
|
|
159
|
+
throw new Error("Provide a non-empty --query of at most 500 characters");
|
|
160
|
+
if (!["search", "callers", "callees", "impact"].includes(mode))
|
|
161
|
+
throw new Error("--mode must be search, callers, callees, or impact");
|
|
162
|
+
if (!Number.isInteger(depth) || depth < 1 || depth > 5)
|
|
163
|
+
throw new Error("--depth must be 1 through 5");
|
|
164
|
+
const exact = graph.nodes.filter((n) => n.id === query || n.name === query || n.file === query);
|
|
165
|
+
const matches = exact.length ? exact : graph.nodes.filter((n) => n.name.toLowerCase().includes(query.toLowerCase()));
|
|
166
|
+
const selected = new Set(matches.slice(0, 100).map((n) => n.id));
|
|
167
|
+
const paths = [];
|
|
168
|
+
let truncated = matches.length > 100;
|
|
169
|
+
if (mode !== "search") {
|
|
170
|
+
for (let hop = 0; hop < (mode === "impact" ? depth : 1); hop++) {
|
|
171
|
+
const frontier = new Set(selected);
|
|
172
|
+
for (const edge of graph.edges) {
|
|
173
|
+
const from = mode === "callees" ? edge.from : edge.to;
|
|
174
|
+
const to = mode === "callees" ? edge.to : edge.from;
|
|
175
|
+
if (!frontier.has(from))
|
|
176
|
+
continue;
|
|
177
|
+
if (!selected.has(to) && selected.size >= 100) {
|
|
178
|
+
truncated = true;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (paths.length >= 300) {
|
|
182
|
+
truncated = true;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
selected.add(to);
|
|
186
|
+
if (!paths.some((p) => p.from === edge.from && p.to === edge.to && p.kind === edge.kind))
|
|
187
|
+
paths.push(edge);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { commit: graph.commit, checkout_head: graph.head, dirty: graph.dirty, mode,
|
|
192
|
+
nodes: graph.nodes.filter((n) => selected.has(n.id)), edges: paths,
|
|
193
|
+
truncated, warnings: graph.warnings };
|
|
194
|
+
}
|
|
195
|
+
function codeGraphCommand(flags) {
|
|
196
|
+
const graph = buildCodeGraph(readCodeSnapshot(flags.repo ?? process.cwd(), flags.commit ?? "HEAD"));
|
|
197
|
+
return queryCodeGraph(graph, flags.query ?? "", flags.mode ?? "search", Number(flags.depth ?? 3));
|
|
198
|
+
}
|
package/dist/config.cjs
CHANGED
|
@@ -15,7 +15,8 @@ function isRuntimeAgentType(value) {
|
|
|
15
15
|
return (value === "claude-code" ||
|
|
16
16
|
value === "codex" ||
|
|
17
17
|
value === "gemini" ||
|
|
18
|
-
value === "opencode"
|
|
18
|
+
value === "opencode" ||
|
|
19
|
+
value === "acp");
|
|
19
20
|
}
|
|
20
21
|
function envInt(env, name, fallback) {
|
|
21
22
|
const raw = env[name];
|
|
@@ -83,6 +84,8 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
83
84
|
geminiExtraArgs: envList(env, "NAVARCH_GEMINI_EXTRA_ARGS", []),
|
|
84
85
|
opencodeBin: env.NAVARCH_OPENCODE_BIN ?? "opencode",
|
|
85
86
|
opencodeExtraArgs: envList(env, "NAVARCH_OPENCODE_EXTRA_ARGS", []),
|
|
87
|
+
acpBin: env.NAVARCH_ACP_BIN ?? "dsh",
|
|
88
|
+
acpExtraArgs: envList(env, "NAVARCH_ACP_EXTRA_ARGS", ["--profile", "acp"]),
|
|
86
89
|
gitAuthorName: env.NAVARCH_GIT_AUTHOR_NAME ?? "sagentlab",
|
|
87
90
|
gitAuthorEmail: env.NAVARCH_GIT_AUTHOR_EMAIL ?? "z@sagentlab.com",
|
|
88
91
|
mcpConfigPath: env.NAVARCH_MCP_CONFIG_PATH ?? null,
|
package/dist/machine-store.cjs
CHANGED
|
@@ -50,7 +50,9 @@ async function resolveMachineIdentity(configDir, apiBaseFallback) {
|
|
|
50
50
|
api_base: process.env.NAVARCH_API_BASE ?? apiBaseFallback,
|
|
51
51
|
agent_type: process.env.NAVARCH_AGENT === "codex" ||
|
|
52
52
|
process.env.NAVARCH_AGENT === "claude-code" ||
|
|
53
|
-
process.env.NAVARCH_AGENT === "gemini"
|
|
53
|
+
process.env.NAVARCH_AGENT === "gemini" ||
|
|
54
|
+
process.env.NAVARCH_AGENT === "opencode" ||
|
|
55
|
+
process.env.NAVARCH_AGENT === "acp"
|
|
54
56
|
? process.env.NAVARCH_AGENT
|
|
55
57
|
: undefined,
|
|
56
58
|
};
|
package/dist/prompt.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.renderPrompt = renderPrompt;
|
|
4
4
|
exports.renderGuidanceCorrectionPrompt = renderGuidanceCorrectionPrompt;
|
|
5
|
+
const CODE_GRAPH_GUIDANCE = "For JS/TS dependency inspection, the runtime provides `navarch-runtime code-graph --query <symbol-or-file> --mode search|callers|callees|impact --commit HEAD`. If the runtime is not on PATH, use its installed entry point; do not install another copy mid-task. Results describe committed source and include commit/freshness warnings; inspect current files for uncommitted changes and unresolved dependencies.";
|
|
5
6
|
/**
|
|
6
7
|
* Renders the four-layer context bundle (project-plan.md §3.8: steering docs,
|
|
7
8
|
* task itself, task-type playbook, depends_on reports) into the single prompt
|
|
@@ -14,7 +15,7 @@ function renderPrompt(task, bundle) {
|
|
|
14
15
|
const repositoryContext = bundle.repository
|
|
15
16
|
? `Repository: ${bundle.repository.full_name} (${bundle.repository.url})\nLocal checkout: this session's isolated git worktree (current working directory).`
|
|
16
17
|
: null;
|
|
17
|
-
return [repositoryContext, bundle.task_context.trim(), bundle.retry_context?.trim()]
|
|
18
|
+
return [repositoryContext, bundle.task_context.trim(), CODE_GRAPH_GUIDANCE, bundle.retry_context?.trim()]
|
|
18
19
|
.filter((section) => Boolean(section))
|
|
19
20
|
.join("\n\n") + "\n";
|
|
20
21
|
}
|
|
@@ -42,6 +43,7 @@ function renderPrompt(task, bundle) {
|
|
|
42
43
|
}
|
|
43
44
|
sections.push("## Playbook");
|
|
44
45
|
sections.push(bundle.playbook);
|
|
46
|
+
sections.push(CODE_GRAPH_GUIDANCE);
|
|
45
47
|
return sections.join("\n\n");
|
|
46
48
|
}
|
|
47
49
|
/**
|
package/dist/session.cjs
CHANGED
|
@@ -67,7 +67,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
67
67
|
? "gpt-6-astra"
|
|
68
68
|
: runtime === "gemini"
|
|
69
69
|
? "auto"
|
|
70
|
-
: runtime === "opencode"
|
|
70
|
+
: runtime === "opencode" || runtime === "acp"
|
|
71
71
|
? "default"
|
|
72
72
|
: "claude-fable-5-1",
|
|
73
73
|
reasoning_effort: "medium",
|
|
@@ -105,7 +105,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
105
105
|
? "gpt-6-astra"
|
|
106
106
|
: runtime === "gemini"
|
|
107
107
|
? "auto"
|
|
108
|
-
: runtime === "opencode"
|
|
108
|
+
: runtime === "opencode" || runtime === "acp"
|
|
109
109
|
? "default"
|
|
110
110
|
: "claude-fable-5-1",
|
|
111
111
|
reasoning_effort: "medium",
|
|
@@ -392,7 +392,9 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
392
392
|
codexGuardArgs,
|
|
393
393
|
geminiSandboxMounts: geminiGuardMounts,
|
|
394
394
|
opencodeHostGuard: runtime === "opencode" && config.sandboxMode === "host" && config.worktreeGuard,
|
|
395
|
-
cwd
|
|
395
|
+
// ACP requires an absolute session cwd. Docker mounts the worktree at
|
|
396
|
+
// this same absolute path, while the other Docker adapters ignore cwd.
|
|
397
|
+
cwd: gitWorktree.worktreePath,
|
|
396
398
|
dockerExec: sandbox ? { containerName: sandbox.name, runner: sandbox_cjs_1.nodeCommandRunner } : undefined,
|
|
397
399
|
signal: activeAbortController.signal,
|
|
398
400
|
});
|
|
@@ -616,6 +618,8 @@ function adapterCommand(config, runtime) {
|
|
|
616
618
|
return { bin: config.geminiBin, extraArgs: config.geminiExtraArgs };
|
|
617
619
|
case "opencode":
|
|
618
620
|
return { bin: config.opencodeBin, extraArgs: config.opencodeExtraArgs };
|
|
621
|
+
case "acp":
|
|
622
|
+
return { bin: config.acpBin, extraArgs: config.acpExtraArgs };
|
|
619
623
|
}
|
|
620
624
|
}
|
|
621
625
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sagentlab/navarch-runtime",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Navarch machine-side session manager: claims delivery tasks and runs them through
|
|
3
|
+
"version": "0.1.41",
|
|
4
|
+
"description": "Navarch machine-side session manager: claims delivery tasks and runs them through local coding agents.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"codex",
|
|
18
18
|
"gemini-cli",
|
|
19
19
|
"opencode",
|
|
20
|
+
"agent-client-protocol",
|
|
21
|
+
"deepseek-harness",
|
|
20
22
|
"task-runner"
|
|
21
23
|
],
|
|
22
24
|
"bin": {
|
|
@@ -47,7 +49,10 @@
|
|
|
47
49
|
},
|
|
48
50
|
"devDependencies": {
|
|
49
51
|
"@types/node": "^20.14.0",
|
|
50
|
-
"typescript": "^5.7.2",
|
|
51
52
|
"vitest": "^4.1.10"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@agentclientprotocol/sdk": "^1.4.0",
|
|
56
|
+
"typescript": "^5.7.2"
|
|
52
57
|
}
|
|
53
58
|
}
|