@sagentlab/navarch-runtime 0.1.0 → 0.1.2

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 CHANGED
@@ -7,21 +7,22 @@ it. Plain Node/TypeScript, zero production dependencies, no Next.js coupling
7
7
  machine.
8
8
 
9
9
  The agent CLI a session runs is either **Claude Code** or **OpenAI Codex**,
10
- selected per machine via `FLOTILLA_AGENT` (default `claude-code`) — see
10
+ selected by the local machine via `--agent` or `NAVARCH_AGENT` (default
11
+ `claude-code`) — see
11
12
  "Choosing an agent (Claude Code vs. Codex)" below.
12
13
 
13
14
  See [`docs/agent-platform-project-plan.md`](../docs/agent-platform-project-plan.md)
14
- §3.8/§3.9/§3.11 and [`docs/flotilla/implementation-plan.md`](../docs/flotilla/implementation-plan.md)
15
+ §3.8/§3.9/§3.11 and [`docs/navarch/implementation-plan.md`](../docs/navarch/implementation-plan.md)
15
16
  WP-07 for the design this implements, and
16
- [`docs/flotilla/schema-design.md`](../docs/flotilla/schema-design.md) §7 for
17
+ [`docs/navarch/schema-design.md`](../docs/navarch/schema-design.md) §7 for
17
18
  the API contract.
18
19
 
19
20
  ## Quick start on a fresh machine
20
21
 
21
22
  ```sh
22
23
  git clone <this repo> && cd sagentlab/runtime
23
- ./install.sh # checks node/docker, npm install, npm run build
24
- export FLOTILLA_API_BASE=https://flotilla.example.com
24
+ ./install.sh # checks node, npm install, npm run build
25
+ export NAVARCH_API_BASE=https://navarch.example.com
25
26
  node bin/navarch.cjs register --token <enrollment-token> --name my-machine-1
26
27
  node bin/navarch.cjs start
27
28
  ```
@@ -30,15 +31,15 @@ Or, once dependencies are installed:
30
31
 
31
32
  ```sh
32
33
  npm run build
33
- FLOTILLA_API_BASE=https://flotilla.example.com npm run register -- --token <enrollment-token> --name my-machine-1
34
+ NAVARCH_API_BASE=https://navarch.example.com npm run register -- --token <enrollment-token> --name my-machine-1
34
35
  npm start
35
36
  ```
36
37
 
37
38
  `register` is the one command that prints the machine's auth token — exactly
38
- once, to stdout. It is stored at `$FLOTILLA_CONFIG_DIR/machine.json` (default
39
- `~/.flotilla/machine.json`, mode `0600`) and never echoed again. For
39
+ once, to stdout. It is stored at `$NAVARCH_CONFIG_DIR/machine.json` (default
40
+ `~/.navarch/machine.json`, mode `0600`) and never echoed again. For
40
41
  twelve-factor deployments (systemd `EnvironmentFile`, container secrets),
41
- skip `register` and set `FLOTILLA_MACHINE_TOKEN` + `FLOTILLA_MACHINE_ID`
42
+ skip `register` and set `NAVARCH_MACHINE_TOKEN` + `NAVARCH_MACHINE_ID`
42
43
  directly.
43
44
 
44
45
  Run `node bin/navarch.cjs doctor` any time to print resolved config, Docker
@@ -48,78 +49,83 @@ availability, and registration status without starting the daemon.
48
49
 
49
50
  `connect` is the project-scoped sibling of `register` — "Connect an agent to
50
51
  a project" (self-hosted-runner style, like a GitHub Actions self-hosted
51
- runner token): a project **owner** mints a short-lived (60 minute),
52
- single-use token from the platform UI (the project settings page's "Connect
52
+ runner token): a project **owner** mints a single-use token from the platform
53
+ UI (the project settings page's "Connect
53
54
  an agent" panel, or the onboarding wizard's Agent step),
54
55
  `POST /api/projects/:id/enrollment-tokens`, and pastes you the ready-to-run
55
- command. Unlike `register`, no `FLOTILLA_ENROLLMENT_SECRET` or
56
+ command. Unlike `register`, no `NAVARCH_ENROLLMENT_SECRET` or
56
57
  `--owner-zone` is needed — the token is already scoped to exactly one
57
58
  project, and the resulting machine's `project_bindings` is set to that
58
59
  project only (it will never be dispatched work from any other project).
59
60
 
60
61
  ```sh
61
62
  npx @sagentlab/navarch-runtime connect --token flmt_<...> --project <project-id> \
62
- --name my-agent-1 --api-base https://flotilla.example.com
63
+ --name my-agent-1 --agent codex --api-base https://navarch.example.com
63
64
  node bin/navarch.cjs start
64
65
  ```
65
66
 
66
- (`@sagentlab/navarch-runtime` is not yet published to npm see "What needs
67
- live verification" below for the from-source equivalent, i.e. the
68
- `git clone` + `./install.sh` + `node bin/navarch.cjs connect ...` flow
69
- above.)
67
+ (`@sagentlab/navarch-runtime` is published to npm, so `npx
68
+ @sagentlab/navarch-runtime <cmd>` works on a fresh machine with no clone.
69
+ The from-source flow — `git clone` + `./install.sh` + `node bin/navarch.cjs
70
+ <cmd>` — remains available for local development.)
70
71
 
71
72
  ## Commands
72
73
 
73
74
  | Command | Purpose |
74
75
  |---|---|
75
- | `register --token <t> --name <n> [--capabilities a,b] [--max-sessions N] [--owner-zone z] [--api-base url]` | Registers this machine (global enrollment secret), prints the token once. |
76
- | `connect --token <t> --name <n> [--project <id>] [--capabilities a,b] [--max-sessions N] [--api-base url]` | Connects this machine to exactly one project (project-scoped enrollment token), prints the token once. |
77
- | `start` | Runs the daemon: machine heartbeat loop + claim loop, `FLOTILLA_MAX_SESSIONS` concurrent task sessions. |
76
+ | `register --token <t> --name <n> [--agent claude-code\|codex] []` | Registers this machine, saves its local agent choice, and prints the token once. |
77
+ | `connect --token <t> --name <n> [--agent claude-code\|codex] [--project <id>] []` | Connects this machine to one project, saves its local agent choice, and prints the token once. |
78
+ | `start [--agent claude-code\|codex]` | Runs the daemon. A start-time agent choice overrides the saved choice. |
78
79
  | `doctor` | Prints resolved config + Docker/registration status; no side effects. |
79
80
 
80
- ## Configuration (`FLOTILLA_*` env vars)
81
+ ## Configuration (`NAVARCH_*` env vars)
81
82
 
82
83
  | Var | Default | Meaning |
83
84
  |---|---|---|
84
- | `FLOTILLA_API_BASE` | `http://localhost:3000` | Control-plane base URL. |
85
- | `FLOTILLA_MACHINE_TOKEN` / `FLOTILLA_MACHINE_ID` | — | Skip `register`/the config file; twelve-factor auth. |
86
- | `FLOTILLA_MACHINE_NAME` | — | Used by `register`. |
87
- | `FLOTILLA_ENROLLMENT_TOKEN` | — | Alternative to `register --token` / `connect --token`. |
88
- | `FLOTILLA_PROJECT_ID` | — | Alternative to `connect --project`. |
89
- | `FLOTILLA_CONFIG_DIR` | `~/.flotilla` | Where `machine.json` lives. |
90
- | `FLOTILLA_WORKSPACE_ROOT` | `<config dir>/sandboxes` | Per-session working directories (host side; mounted into each container). |
91
- | `FLOTILLA_MAX_SESSIONS` | `3` | Capacity cap — see `src/capacity.cts`. |
92
- | `FLOTILLA_CAPABILITIES` | `docker-sandbox,shell` | Comma list reported at heartbeat/claim time. |
93
- | `FLOTILLA_OWNER_ZONE` | `sagentlab` | `sagentlab` or `customer-<slug>-premises` (project-plan.md §3.11). |
94
- | `FLOTILLA_POLL_INTERVAL_MS` | `5000` | Claim-loop poll interval. |
95
- | `FLOTILLA_HEARTBEAT_INTERVAL_MS` | `60000` | Machine-level heartbeat interval. |
96
- | `FLOTILLA_LEASE_HEARTBEAT_INTERVAL_MS` | `300000` | Per-lease heartbeat interval; must stay well under the 15-minute lease TTL (schema-design.md §4). |
97
- | `FLOTILLA_SESSION_TIMEOUT_MS` | `2700000` (45 min) | Hard kill timeout for a single session. |
98
- | `FLOTILLA_SANDBOX_MODE` | `docker` | `docker` or `host` (host mode skips the container dev/debug only, no isolation). |
99
- | `FLOTILLA_DOCKER_IMAGE` | `node:20-slim` | Image used for the per-session container. |
100
- | `FLOTILLA_AGENT` | `claude-code` | Which agent CLI runs sessions: `claude-code` or `codex`. Any other value falls back to `claude-code`. Recorded as `agent_type` on the `complete()` call so the sessions row shows which one ran. |
101
- | `FLOTILLA_CLAUDE_BIN` | `claude` | Path/name of the Claude Code CLI binary. |
102
- | `FLOTILLA_CLAUDE_EXTRA_ARGS` | — | Comma list of extra CLI args appended after `--mcp-config` (Claude Code). |
103
- | `FLOTILLA_CODEX_BIN` | `codex` | Path/name of the Codex CLI binary. |
104
- | `FLOTILLA_CODEX_EXTRA_ARGS` | — | Comma list of extra CLI args appended after `--mcp-config`/`--json` (Codex). |
105
- | `FLOTILLA_MCP_CONFIG_PATH` | — | Path to the platform MCP config passed as `--mcp-config`. |
85
+ | `NAVARCH_API_BASE` | `http://localhost:3000` | Control-plane base URL. |
86
+ | `NAVARCH_MACHINE_TOKEN` / `NAVARCH_MACHINE_ID` | — | Skip `register`/the config file; twelve-factor auth. |
87
+ | `NAVARCH_MACHINE_NAME` | — | Used by `register`. |
88
+ | `NAVARCH_ENROLLMENT_TOKEN` | — | Alternative to `register --token` / `connect --token`. |
89
+ | `NAVARCH_PROJECT_ID` | — | Alternative to `connect --project`. |
90
+ | `NAVARCH_CONFIG_DIR` | `~/.navarch` | Where `machine.json` lives. |
91
+ | `NAVARCH_WORKSPACE_ROOT` | `<config dir>/sandboxes` | Persistent bare repo caches plus isolated per-session worktrees. |
92
+ | `NAVARCH_MAX_SESSIONS` | `5` | Local concurrent-session capacity cap — see `src/capacity.cts`. |
93
+ | `NAVARCH_CAPABILITIES` | `shell` (`docker-sandbox,shell` in Docker mode) | Comma list reported at heartbeat/claim time. |
94
+ | `NAVARCH_OWNER_ZONE` | `sagentlab` | `sagentlab` or `customer-<slug>-premises` (project-plan.md §3.11). |
95
+ | `NAVARCH_POLL_INTERVAL_MS` | `5000` | Claim-loop poll interval. |
96
+ | `NAVARCH_HEARTBEAT_INTERVAL_MS` | `60000` | Machine-level heartbeat interval. |
97
+ | `NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS` | `300000` | Per-lease heartbeat interval; must stay well under the 15-minute lease TTL (schema-design.md §4). |
98
+ | `NAVARCH_SESSION_TIMEOUT_MS` | `2700000` (45 min) | Hard kill timeout for a single session. |
99
+ | `NAVARCH_SANDBOX_MODE` | `host` | `host` uses the resources already available to the agent process. Set `docker` explicitly for container isolation. |
100
+ | `NAVARCH_DOCKER_IMAGE` | `node:20-slim` | Image used for the per-session container. |
101
+ | `NAVARCH_AGENT` | saved choice, then `claude-code` | Local choice of agent CLI: `claude-code` or `codex`. Overrides the choice saved by `connect`/`register`; `start --agent` has highest priority. |
102
+ | `NAVARCH_CLAUDE_BIN` | `claude` | Path/name of the Claude Code CLI binary. |
103
+ | `NAVARCH_CLAUDE_EXTRA_ARGS` | — | Comma list of extra CLI args appended after `--mcp-config` (Claude Code). |
104
+ | `NAVARCH_CODEX_BIN` | `codex` | Path/name of the Codex CLI binary. |
105
+ | `NAVARCH_CODEX_EXTRA_ARGS` | — | Comma list of extra CLI args appended after `--mcp-config`/`--json` (Codex). |
106
+ | `NAVARCH_MCP_CONFIG_PATH` | — | Path to the platform MCP config passed as `--mcp-config`. |
106
107
 
107
108
  ## Choosing an agent (Claude Code vs. Codex)
108
109
 
109
- Each machine runs sessions with exactly one agent CLI, set once via
110
- `FLOTILLA_AGENT`:
110
+ Each machine chooses its own agent CLI. Pass `--agent` while connecting to
111
+ persist the choice in local `machine.json`, override it for one daemon start
112
+ with `start --agent`, or set `NAVARCH_AGENT` in the machine's service
113
+ environment:
111
114
 
112
115
  ```sh
113
116
  # Claude Code (default) — requires the `claude` CLI installed and
114
- # authenticated on this machine (or FLOTILLA_CLAUDE_BIN pointing at it).
115
- export FLOTILLA_AGENT=claude-code
117
+ # authenticated on this machine (or NAVARCH_CLAUDE_BIN pointing at it).
118
+ export NAVARCH_AGENT=claude-code
116
119
 
117
120
  # OpenAI Codex — requires the `codex` CLI installed and authenticated on
118
- # this machine (or FLOTILLA_CODEX_BIN pointing at it), analogous to the
121
+ # this machine (or NAVARCH_CODEX_BIN pointing at it), analogous to the
119
122
  # Claude Code prerequisite above.
120
- export FLOTILLA_AGENT=codex
123
+ export NAVARCH_AGENT=codex
121
124
  ```
122
125
 
126
+ Priority is `start --agent` → `NAVARCH_AGENT` → the locally saved choice →
127
+ `claude-code`. The control plane does not choose the adapter.
128
+
123
129
  Both adapters implement the same `AgentAdapter` interface
124
130
  (`src/adapters/types.cts`) and run either directly on the host or via
125
131
  `docker exec` in the session's sandbox container, exactly like the Claude
@@ -144,21 +150,24 @@ cli.cts
144
150
  ├─ register → api.registerMachine() → machine-store.cts (writes machine.json once)
145
151
  ├─ connect → api.connectMachine() → machine-store.cts (writes machine.json once)
146
152
  └─ start
147
- ├─ MachineHeartbeatLoop (heartbeat-loop.cts) → api.machineHeartbeat() [every FLOTILLA_HEARTBEAT_INTERVAL_MS]
148
- └─ ClaimLoop (claim-loop.cts) → api.claim() [every FLOTILLA_POLL_INTERVAL_MS, gated by CapacityTracker]
149
- └─ runSession (session.cts), one per claimed lease, run concurrently up to FLOTILLA_MAX_SESSIONS:
153
+ ├─ MachineHeartbeatLoop (heartbeat-loop.cts) → api.machineHeartbeat() [every NAVARCH_HEARTBEAT_INTERVAL_MS]
154
+ └─ ClaimLoop (claim-loop.cts) → api.claim() [every NAVARCH_POLL_INTERVAL_MS, gated by CapacityTracker]
155
+ └─ runSession (session.cts), one per claimed lease, run concurrently up to NAVARCH_MAX_SESSIONS:
150
156
  1. write prompt.md (prompt.cts renders the 4-layer context bundle)
151
157
  2. api.issueSecrets() → held in memory only
152
- 3. DockerSandbox.create/injectEnv/cloneRepo (sandbox.cts)
158
+ 3. fetch the project's bare repository cache and create a
159
+ unique git worktree for this session; optionally mount it
160
+ into Docker when NAVARCH_SANDBOX_MODE=docker
153
161
  4. selectAdapter(config.agentType) (adapters/index.cts) picks one AgentAdapter
154
- (adapters/types.cts) by FLOTILLA_AGENT, then .run(...):
162
+ (adapters/types.cts) by NAVARCH_AGENT, then .run(...):
155
163
  - claudeCodeAdapter (adapters/claude.cts) — `claude -p <prompt> --mcp-config <path>`
156
164
  - codexAdapter (adapters/codex.cts) — `codex exec <prompt> --json --mcp-config <path>` (ASSUMED)
157
- heartbeating the lease every FLOTILLA_LEASE_HEARTBEAT_INTERVAL_MS throughout either;
165
+ heartbeating the lease every NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS throughout either;
158
166
  a failed heartbeat aborts the run (kills the process) and marks the outcome as lease-lost
159
167
  5. mapExitCondition (exit-conditions.cts) → redact.cts scrubs the transcript → upload.cts PUTs it
160
168
  6. api.completeLease(), reporting agent_type: config.agentType
161
- 7. sandbox.wipe() unconditionally (finally block)
169
+ 7. sandbox.wipe() when present; remove the session workspace
170
+ unconditionally (finally block)
162
171
  ```
163
172
 
164
173
  `adapter.cts` (top-level) is now a backward-compat re-export of
@@ -229,13 +238,13 @@ everything that can be verified without those:
229
238
  plus Claude's and Codex's usage/report parsing (`tests/exit-conditions.test.cts`).
230
239
  - `redact.cts` — exact-value and pattern-based redaction (`tests/redact.test.cts`).
231
240
  - `capacity.cts` — capacity math and acquire/release bookkeeping (`tests/capacity.test.cts`).
232
- - `config.cts` — env var parsing and defaults, including `FLOTILLA_AGENT`/
233
- `FLOTILLA_CODEX_BIN`/`FLOTILLA_CODEX_EXTRA_ARGS` (`tests/config.test.cts`).
241
+ - `config.cts` — env var parsing and defaults, including `NAVARCH_AGENT`/
242
+ `NAVARCH_CODEX_BIN`/`NAVARCH_CODEX_EXTRA_ARGS` (`tests/config.test.cts`).
234
243
  - `sandbox.cts` — command construction (flags, env-via-stdin, credential-helper
235
244
  argv hygiene) against an injected fake `CommandRunner`, plus `isDockerAvailable()`
236
245
  degrading to `false` instead of throwing when Docker is absent (`tests/sandbox.test.cts`).
237
246
  - `adapters/index.cts#selectAdapter` — picks the right `AgentAdapter` for every
238
- `FLOTILLA_AGENT` value, including the claude-code fallback for an unrecognized
247
+ `NAVARCH_AGENT` value, including the claude-code fallback for an unrecognized
239
248
  one (`tests/adapters/index.test.cts`).
240
249
  - `adapters/codex.cts` — arg construction on both the host path (mocked `spawn`)
241
250
  and the docker-exec path (fake `CommandRunner`), and usage/report-text
@@ -287,7 +296,7 @@ secrets absent from disk after exit"):
287
296
  `--dangerously-bypass-approvals-and-sandbox` per published Codex CLI
288
297
  docs) to avoid blocking on an approval prompt inside the already-isolated
289
298
  Docker sandbox — deliberately **not** hardcoded, left to
290
- `FLOTILLA_CODEX_EXTRA_ARGS` until confirmed, since guessing the wrong
299
+ `NAVARCH_CODEX_EXTRA_ARGS` until confirmed, since guessing the wrong
291
300
  flag here could silently disable sandboxing rather than just fail loudly.
292
301
  - Whether the Codex CLI even authenticates/runs non-interactively the same
293
302
  way `claude` does (API key vs. ChatGPT-account OAuth device flow) — this
@@ -315,13 +324,14 @@ secrets absent from disk after exit"):
315
324
  that one project. Unit-tested here: `connectMachine()`'s request shape
316
325
  (`tests/api.test.cts`) and the `connect` command's flag/env parsing
317
326
  (`tests/cli.test.cts`); not tested here: the real Postgres round trip
318
- (`lib/flotilla/__tests__/enrollment.test.ts` and the
327
+ (`lib/navarch/__tests__/enrollment.test.ts` and the
319
328
  `app/api/machines/connect` / `app/api/projects/[id]/enrollment-tokens`
320
329
  route tests cover that with a mocked admin client, not a live database).
321
- - `@sagentlab/navarch-runtime` is not yet published to npm — every `npx
330
+ - `@sagentlab/navarch-runtime` is published to npm — every `npx
322
331
  @sagentlab/navarch-runtime ...` command shown above (and in
323
- `ConnectAgentPanel`) currently requires the from-source flow instead
324
- (`git clone` + `./install.sh` + `node bin/navarch.cjs connect ...`).
332
+ `ConnectAgentPanel`) resolves the published package directly; the
333
+ from-source flow (`git clone` + `./install.sh` + `node bin/navarch.cjs
334
+ connect ...`) is an equivalent local-development alternative.
325
335
 
326
336
  ## A note on `.cts` instead of `.ts`
327
337
 
package/dist/adapter.cjs CHANGED
@@ -7,7 +7,7 @@ exports.claudeCodeAdapter = exports.runClaudeCodeAdapter = void 0;
7
7
  * The Claude Code adapter that used to live entirely in this file now lives
8
8
  * in adapters/claude.cts, implementing the generalized `AgentAdapter`
9
9
  * interface (adapters/types.cts) alongside adapters/codex.cts's Codex CLI
10
- * sibling — session.cts picks between the two via FLOTILLA_AGENT
10
+ * sibling — session.cts picks between the two via NAVARCH_AGENT
11
11
  * (config.cts's `agentType`) through adapters/index.cts#selectAdapter.
12
12
  *
13
13
  * This file is kept, unchanged in its exported names, so any existing
@@ -14,7 +14,7 @@ const exit_conditions_cjs_1 = require("../exit-conditions.cjs");
14
14
  *
15
15
  * This is one of two implementations of the AgentAdapter interface
16
16
  * (adapters/types.cts) — see adapters/codex.cts for the Codex CLI sibling
17
- * session.cts picks between via FLOTILLA_AGENT (config.cts's `agentType`).
17
+ * session.cts picks between via NAVARCH_AGENT (config.cts's `agentType`).
18
18
  *
19
19
  * NEEDS LIVE VERIFICATION (WP-13): `--output-format json` is assumed to make
20
20
  * `claude -p` print a single JSON result object with a `usage` block, per
@@ -128,7 +128,7 @@ async function runViaDocker(options, args) {
128
128
  function shellQuote(value) {
129
129
  return `'${value.replace(/'/g, `'\\''`)}'`;
130
130
  }
131
- /** The AgentAdapter (adapters/types.cts) wrapper session.cts selects via FLOTILLA_AGENT=claude-code (the default). */
131
+ /** The AgentAdapter (adapters/types.cts) wrapper session.cts selects via NAVARCH_AGENT=claude-code (the default). */
132
132
  exports.claudeCodeAdapter = {
133
133
  agentType: "claude-code",
134
134
  run: runClaudeCodeAdapter,
@@ -8,7 +8,7 @@ const exit_conditions_cjs_1 = require("../exit-conditions.cjs");
8
8
  * Headless OpenAI Codex CLI adapter — the Codex sibling of claude.cts's
9
9
  * `runClaudeCodeAdapter`, implementing the same AgentAdapter interface
10
10
  * (adapters/types.cts) so session.cts can pick either one at runtime via
11
- * FLOTILLA_AGENT (config.cts's `agentType`). Structurally this mirrors
11
+ * NAVARCH_AGENT (config.cts's `agentType`). Structurally this mirrors
12
12
  * claude.cts exactly: same host-vs-docker-exec split, same
13
13
  * timeout/AbortSignal handling, same "attach best-effort usage onto the raw
14
14
  * result" shape — only the CLI invocation and output parsing differ.
@@ -41,14 +41,14 @@ const exit_conditions_cjs_1 = require("../exit-conditions.cjs");
41
41
  * - Sandboxing/approvals: a real `codex exec` may prompt for
42
42
  * approval/sandbox-escalation on some actions by default; because this
43
43
  * runtime already isolates the session in its own Docker container (or,
44
- * in `FLOTILLA_SANDBOX_MODE=host`, trusts the host), the intent is to pass
44
+ * in `NAVARCH_SANDBOX_MODE=host`, trusts the host), the intent is to pass
45
45
  * whatever flag disables Codex's own approval gate for a fully
46
46
  * non-interactive run (something like `--full-auto` or
47
47
  * `--dangerously-bypass-approvals-and-sandbox` in published Codex CLI
48
48
  * documentation) — deliberately NOT hardcoded here since getting an
49
49
  * unverified flag wrong could silently change sandboxing behavior; left to
50
- * be supplied via FLOTILLA_CODEX_EXTRA_ARGS until confirmed.
51
- * - `FLOTILLA_CODEX_EXTRA_ARGS` (`extraArgs`) wins over the default `--json`
50
+ * be supplied via NAVARCH_CODEX_EXTRA_ARGS until confirmed.
51
+ * - `NAVARCH_CODEX_EXTRA_ARGS` (`extraArgs`) wins over the default `--json`
52
52
  * exactly like the Claude adapter's `--output-format` opt-out, so an
53
53
  * operator can fall back to plain-text output (or add the real
54
54
  * approval-bypass flag) without an adapter code change.
@@ -160,7 +160,7 @@ async function runViaDocker(options, args) {
160
160
  function shellQuote(value) {
161
161
  return `'${value.replace(/'/g, `'\\''`)}'`;
162
162
  }
163
- /** The AgentAdapter (adapters/types.cts) wrapper session.cts selects via FLOTILLA_AGENT=codex. */
163
+ /** The AgentAdapter (adapters/types.cts) wrapper session.cts selects via NAVARCH_AGENT=codex. */
164
164
  exports.codexAdapter = {
165
165
  agentType: "codex",
166
166
  run: runCodexAdapter,
@@ -10,7 +10,7 @@ Object.defineProperty(exports, "codexAdapter", { enumerable: true, get: function
10
10
  Object.defineProperty(exports, "runCodexAdapter", { enumerable: true, get: function () { return codex_cjs_1.runCodexAdapter; } });
11
11
  /**
12
12
  * Picks the AgentAdapter (adapters/types.cts) session.cts should run a
13
- * session with, keyed off config.cts's `agentType` (FLOTILLA_AGENT). This is
13
+ * session with, keyed off config.cts's `agentType` (NAVARCH_AGENT). This is
14
14
  * the one place agent-type branching happens outside config loading itself —
15
15
  * session.cts calls the returned adapter's `run()` uniformly regardless of
16
16
  * which one it got.
package/dist/api.cjs CHANGED
@@ -74,11 +74,11 @@ class NavarchApiClient {
74
74
  }
75
75
  /**
76
76
  * `POST /api/machines/connect` — "Connect an agent to a project"
77
- * (docs/flotilla/schema-design.md §7 "Agent connect"). CONFIRMED endpoint,
77
+ * (docs/navarch/schema-design.md §7 "Agent connect"). CONFIRMED endpoint,
78
78
  * the project-scoped sibling of {@link registerMachine}: redeems a
79
79
  * single-use, project-scoped enrollment token a project owner minted
80
80
  * (POST /api/projects/:id/enrollment-tokens) instead of the global
81
- * FLOTILLA_ENROLLMENT_SECRET. Unauthenticated like registerMachine — the
81
+ * NAVARCH_ENROLLMENT_SECRET. Unauthenticated like registerMachine — the
82
82
  * enrollment token in the body is the auth.
83
83
  */
84
84
  async connectMachine(req) {
package/dist/capacity.cjs CHANGED
@@ -9,7 +9,7 @@ function computeAvailableCapacity(maxSessions, activeSessions) {
9
9
  /**
10
10
  * Tracks which session (lease) ids currently occupy a slot on this machine so
11
11
  * the heartbeat loop reports accurate available_capacity and the claim loop
12
- * never over-claims beyond FLOTILLA_MAX_SESSIONS.
12
+ * never over-claims beyond NAVARCH_MAX_SESSIONS.
13
13
  */
14
14
  class CapacityTracker {
15
15
  maxSessions;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ClaimLoop = void 0;
4
4
  const node_crypto_1 = require("node:crypto");
5
+ const api_cjs_1 = require("./api.cjs");
5
6
  const logger_cjs_1 = require("./logger.cjs");
6
7
  const log = (0, logger_cjs_1.createLogger)("claim");
7
8
  /**
@@ -46,6 +47,7 @@ class ClaimLoop {
46
47
  const claimed = await this.api.claim({
47
48
  available_capacity: this.capacity.available(),
48
49
  capabilities: this.config.capabilities,
50
+ agent_type: this.config.agentType,
49
51
  session_id: sessionId,
50
52
  });
51
53
  if (!claimed)
@@ -57,7 +59,17 @@ class ClaimLoop {
57
59
  .finally(() => this.capacity.release(claimed.lease_id));
58
60
  }
59
61
  catch (err) {
60
- log.warn(`claim failed: ${String(err)}`);
62
+ // NavarchApiError's message is only the status line ("… failed with
63
+ // 500"); the control plane's actual error text lives in `.body`. Log it
64
+ // so a server-side claim failure is diagnosable from the runtime alone
65
+ // instead of an opaque bare status.
66
+ if (err instanceof api_cjs_1.NavarchApiError) {
67
+ const detail = typeof err.body === "string" ? err.body : JSON.stringify(err.body);
68
+ log.warn(`claim failed: ${err.message}${detail ? ` — ${detail}` : ""}`);
69
+ }
70
+ else {
71
+ log.warn(`claim failed: ${String(err)}`);
72
+ }
61
73
  }
62
74
  }
63
75
  }
package/dist/cli.cjs CHANGED
@@ -11,6 +11,33 @@ const session_cjs_1 = require("./session.cjs");
11
11
  const sandbox_cjs_1 = require("./sandbox.cjs");
12
12
  const logger_cjs_1 = require("./logger.cjs");
13
13
  const log = (0, logger_cjs_1.createLogger)("cli");
14
+ const PACKAGE_NAME = "@sagentlab/navarch-runtime";
15
+ /**
16
+ * How to tell the user to re-invoke this CLI, matching however THEY launched
17
+ * it. The bare `navarch-runtime` bin only exists on PATH after a global install
18
+ * (`npm i -g`) or `npm link`; the documented onboarding path is `npx
19
+ * @sagentlab/navarch-runtime …`, where the bare name is NOT on PATH. Printing
20
+ * `navarch-runtime start` to someone who ran us via npx sends them straight
21
+ * into `command not found`, so detect that case and echo the form that works.
22
+ */
23
+ function invocation(subcommand) {
24
+ const scriptPath = process.argv[1] ?? "";
25
+ const viaNpx = scriptPath.includes("/_npx/") ||
26
+ scriptPath.includes("\\_npx\\") ||
27
+ process.env.npm_command === "exec" ||
28
+ process.env.npm_lifecycle_event === "npx";
29
+ const prefix = viaNpx ? `npx ${PACKAGE_NAME}` : "navarch-runtime";
30
+ return `${prefix} ${subcommand}`;
31
+ }
32
+ function agentFromFlag(flags) {
33
+ const value = flags.agent;
34
+ if (value === undefined)
35
+ return undefined;
36
+ if (!(0, config_cjs_1.isRuntimeAgentType)(value)) {
37
+ throw new Error("--agent must be either 'claude-code' or 'codex'.");
38
+ }
39
+ return value;
40
+ }
14
41
  function parseArgs(argv) {
15
42
  const [command, ...rest] = argv;
16
43
  const flags = {};
@@ -38,13 +65,13 @@ function parseArgs(argv) {
38
65
  async function registerCommand(flags) {
39
66
  const config = (0, config_cjs_1.loadRuntimeConfig)();
40
67
  const apiBase = flags["api-base"] ?? config.apiBase;
41
- const enrollmentToken = flags.token ?? process.env.FLOTILLA_ENROLLMENT_TOKEN;
42
- const name = flags.name ?? process.env.FLOTILLA_MACHINE_NAME;
68
+ const enrollmentToken = flags.token ?? process.env.NAVARCH_ENROLLMENT_TOKEN;
69
+ const name = flags.name ?? process.env.NAVARCH_MACHINE_NAME;
43
70
  if (!enrollmentToken) {
44
- throw new Error("Missing --token (or FLOTILLA_ENROLLMENT_TOKEN) — get one from the admin console.");
71
+ throw new Error("Missing --token (or NAVARCH_ENROLLMENT_TOKEN) — get one from the admin console.");
45
72
  }
46
73
  if (!name) {
47
- throw new Error("Missing --name (or FLOTILLA_MACHINE_NAME) for this machine.");
74
+ throw new Error("Missing --name (or NAVARCH_MACHINE_NAME) for this machine.");
48
75
  }
49
76
  const maxSessions = Number(flags["max-sessions"] ?? config.maxSessions);
50
77
  const capabilities = (flags.capabilities ?? config.capabilities.join(","))
@@ -52,6 +79,7 @@ async function registerCommand(flags) {
52
79
  .map((s) => s.trim())
53
80
  .filter(Boolean);
54
81
  const ownerZone = flags["owner-zone"] ?? config.ownerZone;
82
+ const agentType = agentFromFlag(flags) ?? config.agentType;
55
83
  const client = new api_cjs_1.NavarchApiClient({ baseUrl: apiBase });
56
84
  const result = await client.registerMachine({
57
85
  enrollment_token: enrollmentToken,
@@ -65,39 +93,41 @@ async function registerCommand(flags) {
65
93
  token: result.token,
66
94
  name,
67
95
  api_base: apiBase,
96
+ agent_type: agentType,
68
97
  });
69
98
  // Printed exactly once. Never logged or echoed again after this point.
70
99
  console.log("Machine registered.");
71
100
  console.log(` machine_id: ${result.machine_id}`);
72
101
  console.log(` token: ${result.token}`);
73
- console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`navarch-runtime start\` to begin serving tasks.`);
102
+ console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${invocation("start")}\` to begin serving tasks.`);
74
103
  }
75
104
  /**
76
105
  * `navarch-runtime connect` — "Connect an agent to a project"
77
- * (docs/flotilla/schema-design.md §7 "Agent connect"; self-hosted-runner
78
- * style). The project-scoped sibling of `register`: redeems a short-lived,
79
- * single-use enrollment token a project owner minted from the platform UI
106
+ * (docs/navarch/schema-design.md §7 "Agent connect"; self-hosted-runner
107
+ * style). The project-scoped sibling of `register`: redeems a single-use
108
+ * enrollment token a project owner minted from the platform UI
80
109
  * (ConnectAgentPanel → POST /api/projects/:id/enrollment-tokens) instead of
81
- * the global FLOTILLA_ENROLLMENT_SECRET `register` needs. Prints the
110
+ * the global NAVARCH_ENROLLMENT_SECRET `register` needs. Prints the
82
111
  * machine token exactly once, same discipline as `register`.
83
112
  */
84
113
  async function connectCommand(flags) {
85
114
  const config = (0, config_cjs_1.loadRuntimeConfig)();
86
115
  const apiBase = flags["api-base"] ?? config.apiBase;
87
- const enrollmentToken = flags.token ?? process.env.FLOTILLA_ENROLLMENT_TOKEN;
88
- const name = flags.name ?? process.env.FLOTILLA_MACHINE_NAME;
89
- const projectId = flags.project ?? process.env.FLOTILLA_PROJECT_ID;
116
+ const enrollmentToken = flags.token ?? process.env.NAVARCH_ENROLLMENT_TOKEN;
117
+ const name = flags.name ?? process.env.NAVARCH_MACHINE_NAME;
118
+ const projectId = flags.project ?? process.env.NAVARCH_PROJECT_ID;
90
119
  if (!enrollmentToken) {
91
- throw new Error("Missing --token (or FLOTILLA_ENROLLMENT_TOKEN) — get one from a project owner's \"Connect an agent\" panel.");
120
+ throw new Error("Missing --token (or NAVARCH_ENROLLMENT_TOKEN) — get one from a project owner's \"Connect an agent\" panel.");
92
121
  }
93
122
  if (!name) {
94
- throw new Error("Missing --name (or FLOTILLA_MACHINE_NAME) for this machine.");
123
+ throw new Error("Missing --name (or NAVARCH_MACHINE_NAME) for this machine.");
95
124
  }
96
125
  const maxSessions = Number(flags["max-sessions"] ?? config.maxSessions);
97
126
  const capabilities = (flags.capabilities ?? config.capabilities.join(","))
98
127
  .split(",")
99
128
  .map((s) => s.trim())
100
129
  .filter(Boolean);
130
+ const agentType = agentFromFlag(flags) ?? config.agentType;
101
131
  const client = new api_cjs_1.NavarchApiClient({ baseUrl: apiBase });
102
132
  const result = await client.connectMachine({
103
133
  enrollment_token: enrollmentToken,
@@ -111,23 +141,30 @@ async function connectCommand(flags) {
111
141
  token: result.token,
112
142
  name,
113
143
  api_base: apiBase,
144
+ agent_type: agentType,
114
145
  });
115
146
  // Printed exactly once. Never logged or echoed again after this point.
116
147
  console.log("Machine connected.");
117
148
  console.log(` machine_id: ${result.machine_id}`);
118
149
  console.log(` token: ${result.token}`);
119
- console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`navarch-runtime start\` to begin serving tasks.`);
150
+ console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${invocation("start")}\` to begin serving tasks.`);
120
151
  }
121
- async function startCommand() {
122
- const config = (0, config_cjs_1.loadRuntimeConfig)();
123
- const identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(config.configDir, config.apiBase);
152
+ async function startCommand(flags) {
153
+ const baseConfig = (0, config_cjs_1.loadRuntimeConfig)();
154
+ const identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(baseConfig.configDir, baseConfig.apiBase);
155
+ // Adapter selection belongs to the local machine: an explicit start flag
156
+ // wins, followed by NAVARCH_AGENT, the choice saved at connect/register
157
+ // time, and finally the backwards-compatible Claude Code default.
158
+ const agentType = agentFromFlag(flags) ??
159
+ (process.env.NAVARCH_AGENT ? baseConfig.agentType : identity.agent_type ?? baseConfig.agentType);
160
+ const config = { ...baseConfig, agentType };
124
161
  const api = new api_cjs_1.NavarchApiClient({ baseUrl: identity.api_base, token: identity.token });
125
162
  const capacity = new capacity_cjs_1.CapacityTracker(config.maxSessions);
126
163
  const heartbeat = new heartbeat_loop_cjs_1.MachineHeartbeatLoop(api, identity.machine_id, config, capacity);
127
164
  const claimLoop = new claim_loop_cjs_1.ClaimLoop(api, config, capacity, (claimed, sessionId) => (0, session_cjs_1.runSession)({ api, config }, claimed, sessionId));
128
165
  heartbeat.start();
129
166
  claimLoop.start();
130
- log.info(`navarch-runtime started: machine=${identity.name} max_sessions=${config.maxSessions} api_base=${identity.api_base}`);
167
+ log.info(`navarch-runtime started: machine=${identity.name} agent=${config.agentType} max_sessions=${config.maxSessions} api_base=${identity.api_base}`);
131
168
  const shutdown = () => {
132
169
  log.info("shutting down...");
133
170
  heartbeat.stop();
@@ -137,7 +174,7 @@ async function startCommand() {
137
174
  process.on("SIGINT", shutdown);
138
175
  process.on("SIGTERM", shutdown);
139
176
  }
140
- async function doctorCommand() {
177
+ async function doctorCommand(flags) {
141
178
  const config = (0, config_cjs_1.loadRuntimeConfig)();
142
179
  const dockerOk = await (0, sandbox_cjs_1.isDockerAvailable)();
143
180
  console.log(`api_base: ${config.apiBase}`);
@@ -147,26 +184,31 @@ async function doctorCommand() {
147
184
  console.log(`capabilities: ${config.capabilities.join(", ")}`);
148
185
  console.log(`sandbox_mode: ${config.sandboxMode}`);
149
186
  console.log(`docker: ${dockerOk ? "available" : "NOT AVAILABLE (docker-backed sessions will fail)"}`);
187
+ let identity;
150
188
  try {
151
- const identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(config.configDir, config.apiBase);
152
- console.log(`machine: ${identity.name} (${identity.machine_id})`);
189
+ identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(config.configDir, config.apiBase);
153
190
  }
154
191
  catch {
155
- console.log("machine: not registered — run `navarch-runtime register`");
192
+ console.log(`machine: not registered — run \`${invocation("register")}\``);
193
+ return;
156
194
  }
195
+ const agentType = agentFromFlag(flags) ??
196
+ (process.env.NAVARCH_AGENT ? config.agentType : identity.agent_type ?? config.agentType);
197
+ console.log(`machine: ${identity.name} (${identity.machine_id})`);
198
+ console.log(`agent: ${agentType}`);
157
199
  }
158
200
  function helpText() {
159
201
  return `navarch-runtime — Navarch machine-side session manager
160
202
 
161
203
  Usage:
162
204
  navarch-runtime register --token <enrollment-token> --name <machine-name> \\
163
- [--capabilities a,b] [--max-sessions N] [--owner-zone z] [--api-base url]
205
+ [--agent claude-code|codex] [--capabilities a,b] [--max-sessions N] [--owner-zone z] [--api-base url]
164
206
  navarch-runtime connect --token <enrollment-token> --name <machine-name> \\
165
- [--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
166
- navarch-runtime start
207
+ [--agent claude-code|codex] [--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
208
+ navarch-runtime start [--agent claude-code|codex]
167
209
  navarch-runtime doctor
168
210
 
169
- Configuration is via FLOTILLA_* environment variables; see runtime/README.md.
211
+ Configuration is via NAVARCH_* environment variables; see runtime/README.md.
170
212
  `;
171
213
  }
172
214
  async function main(argv = process.argv.slice(2)) {
@@ -180,10 +222,10 @@ async function main(argv = process.argv.slice(2)) {
180
222
  await connectCommand(flags);
181
223
  break;
182
224
  case "start":
183
- await startCommand();
225
+ await startCommand(flags);
184
226
  break;
185
227
  case "doctor":
186
- await doctorCommand();
228
+ await doctorCommand(flags);
187
229
  break;
188
230
  case "help":
189
231
  case "--help":
package/dist/config.cjs CHANGED
@@ -3,9 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isRuntimeAgentType = isRuntimeAgentType;
6
7
  exports.loadRuntimeConfig = loadRuntimeConfig;
7
8
  const node_path_1 = __importDefault(require("node:path"));
8
9
  const node_os_1 = __importDefault(require("node:os"));
10
+ function isRuntimeAgentType(value) {
11
+ return value === "claude-code" || value === "codex";
12
+ }
9
13
  function envInt(env, name, fallback) {
10
14
  const raw = env[name];
11
15
  if (!raw)
@@ -23,33 +27,38 @@ function envList(env, name, fallback) {
23
27
  .filter(Boolean);
24
28
  }
25
29
  /**
26
- * Loads runtime config from FLOTILLA_* env vars, with sane defaults for a
30
+ * Loads runtime config from NAVARCH_* env vars, with sane defaults for a
27
31
  * fresh machine. Accepts an explicit env map (defaulting to process.env) so
28
32
  * it is trivially unit-testable without mutating global state.
29
33
  */
30
34
  function loadRuntimeConfig(env = process.env) {
31
- const configDir = env.FLOTILLA_CONFIG_DIR ?? node_path_1.default.join(node_os_1.default.homedir(), ".flotilla");
32
- const leaseHeartbeatIntervalMs = envInt(env, "FLOTILLA_LEASE_HEARTBEAT_INTERVAL_MS", 5 * 60 * 1000);
35
+ const configDir = env.NAVARCH_CONFIG_DIR ?? node_path_1.default.join(node_os_1.default.homedir(), ".navarch");
36
+ const leaseHeartbeatIntervalMs = envInt(env, "NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS", 5 * 60 * 1000);
37
+ // Run the agent with the resources already available on its machine by
38
+ // default. Docker isolation is an explicit operator opt-in, not a
39
+ // prerequisite for claiming ordinary shell work.
40
+ const sandboxMode = env.NAVARCH_SANDBOX_MODE === "docker" ? "docker" : "host";
41
+ const defaultCapabilities = sandboxMode === "docker" ? ["docker-sandbox", "shell"] : ["shell"];
33
42
  return {
34
- apiBase: env.FLOTILLA_API_BASE ?? "http://localhost:3000",
35
- workspaceRoot: env.FLOTILLA_WORKSPACE_ROOT ?? node_path_1.default.join(configDir, "sandboxes"),
43
+ apiBase: env.NAVARCH_API_BASE ?? "http://localhost:3000",
44
+ workspaceRoot: env.NAVARCH_WORKSPACE_ROOT ?? node_path_1.default.join(configDir, "sandboxes"),
36
45
  configDir,
37
- maxSessions: envInt(env, "FLOTILLA_MAX_SESSIONS", 3),
38
- capabilities: envList(env, "FLOTILLA_CAPABILITIES", ["docker-sandbox", "shell"]),
39
- ownerZone: env.FLOTILLA_OWNER_ZONE ?? "sagentlab",
40
- pollIntervalMs: envInt(env, "FLOTILLA_POLL_INTERVAL_MS", 5000),
41
- machineHeartbeatIntervalMs: envInt(env, "FLOTILLA_HEARTBEAT_INTERVAL_MS", 60_000),
46
+ maxSessions: envInt(env, "NAVARCH_MAX_SESSIONS", 5),
47
+ capabilities: envList(env, "NAVARCH_CAPABILITIES", defaultCapabilities),
48
+ ownerZone: env.NAVARCH_OWNER_ZONE ?? "sagentlab",
49
+ pollIntervalMs: envInt(env, "NAVARCH_POLL_INTERVAL_MS", 5000),
50
+ machineHeartbeatIntervalMs: envInt(env, "NAVARCH_HEARTBEAT_INTERVAL_MS", 60_000),
42
51
  // leases.expires_at = claimed_at + 15 min (schema-design.md §4) — default renewal
43
52
  // interval must stay comfortably under that TTL.
44
53
  leaseHeartbeatIntervalMs,
45
- sessionTimeoutMs: envInt(env, "FLOTILLA_SESSION_TIMEOUT_MS", 45 * 60 * 1000),
46
- agentType: env.FLOTILLA_AGENT === "codex" ? "codex" : "claude-code",
47
- claudeBin: env.FLOTILLA_CLAUDE_BIN ?? "claude",
48
- claudeExtraArgs: envList(env, "FLOTILLA_CLAUDE_EXTRA_ARGS", []),
49
- codexBin: env.FLOTILLA_CODEX_BIN ?? "codex",
50
- codexExtraArgs: envList(env, "FLOTILLA_CODEX_EXTRA_ARGS", []),
51
- mcpConfigPath: env.FLOTILLA_MCP_CONFIG_PATH ?? null,
52
- sandboxMode: env.FLOTILLA_SANDBOX_MODE === "host" ? "host" : "docker",
53
- dockerImage: env.FLOTILLA_DOCKER_IMAGE ?? "node:20-slim",
54
+ sessionTimeoutMs: envInt(env, "NAVARCH_SESSION_TIMEOUT_MS", 45 * 60 * 1000),
55
+ agentType: isRuntimeAgentType(env.NAVARCH_AGENT) ? env.NAVARCH_AGENT : "claude-code",
56
+ claudeBin: env.NAVARCH_CLAUDE_BIN ?? "claude",
57
+ claudeExtraArgs: envList(env, "NAVARCH_CLAUDE_EXTRA_ARGS", []),
58
+ codexBin: env.NAVARCH_CODEX_BIN ?? "codex",
59
+ codexExtraArgs: envList(env, "NAVARCH_CODEX_EXTRA_ARGS", []),
60
+ mcpConfigPath: env.NAVARCH_MCP_CONFIG_PATH ?? null,
61
+ sandboxMode,
62
+ dockerImage: env.NAVARCH_DOCKER_IMAGE ?? "node:20-slim",
54
63
  };
55
64
  }
@@ -0,0 +1,142 @@
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.GitWorktree = void 0;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const node_fs_1 = require("node:fs");
9
+ const sandbox_cjs_1 = require("./sandbox.cjs");
10
+ const repositoryLocks = new Map();
11
+ /**
12
+ * Maintains one bare repository cache per project and checks out each session
13
+ * into its own uniquely named worktree. The cache avoids N full clones while
14
+ * git's worktree metadata keeps concurrent agents from sharing an index or
15
+ * working directory.
16
+ */
17
+ class GitWorktree {
18
+ sessionRoot;
19
+ worktreePath;
20
+ repositoryPath;
21
+ branch;
22
+ runner;
23
+ cloneUrl;
24
+ githubToken;
25
+ constructor(options) {
26
+ const projectKey = safePathSegment(options.projectId);
27
+ const sessionKey = safePathSegment(options.sessionId);
28
+ this.sessionRoot = node_path_1.default.join(options.workspaceRoot, "sessions", sessionKey);
29
+ this.worktreePath = node_path_1.default.join(this.sessionRoot, "repo");
30
+ this.repositoryPath = node_path_1.default.join(options.workspaceRoot, "repositories", `${projectKey}.git`);
31
+ this.branch = `navarch/${safePathSegment(options.taskId).slice(0, 32)}-${sessionKey.slice(0, 12)}`;
32
+ this.runner = options.runner ?? sandbox_cjs_1.nodeCommandRunner;
33
+ this.cloneUrl = options.cloneUrl;
34
+ this.githubToken = options.githubToken;
35
+ }
36
+ async prepare() {
37
+ await node_fs_1.promises.mkdir(node_path_1.default.dirname(this.repositoryPath), { recursive: true });
38
+ await node_fs_1.promises.mkdir(this.sessionRoot, { recursive: true });
39
+ await withRepositoryLock(this.repositoryPath, async () => {
40
+ if (!(await pathExists(node_path_1.default.join(this.repositoryPath, "HEAD")))) {
41
+ await this.runGit(["clone", "--bare", this.cloneUrl, this.repositoryPath], true);
42
+ }
43
+ else {
44
+ const origin = await this.runGit(["--git-dir", this.repositoryPath, "remote", "get-url", "origin"], false);
45
+ if (normalizeCloneUrl(origin.stdout) !== normalizeCloneUrl(this.cloneUrl)) {
46
+ await this.runGit(["--git-dir", this.repositoryPath, "remote", "set-url", "origin", this.cloneUrl], false);
47
+ }
48
+ }
49
+ const remoteHead = await this.runGit(["ls-remote", "--symref", "origin", "HEAD"], true);
50
+ const startRef = parseRemoteHead(remoteHead.stdout) ?? "HEAD";
51
+ await this.runGit([
52
+ "--git-dir",
53
+ this.repositoryPath,
54
+ "fetch",
55
+ "--prune",
56
+ "origin",
57
+ "+refs/heads/*:refs/heads/*",
58
+ ], true);
59
+ await this.runGit([
60
+ "--git-dir",
61
+ this.repositoryPath,
62
+ "worktree",
63
+ "add",
64
+ "-b",
65
+ this.branch,
66
+ this.worktreePath,
67
+ startRef,
68
+ ], false);
69
+ });
70
+ }
71
+ async cleanup() {
72
+ await withRepositoryLock(this.repositoryPath, async () => {
73
+ await this.runner
74
+ .run("git", ["--git-dir", this.repositoryPath, "worktree", "remove", "--force", this.worktreePath])
75
+ .catch(() => undefined);
76
+ await this.runner
77
+ .run("git", ["--git-dir", this.repositoryPath, "branch", "-D", this.branch])
78
+ .catch(() => undefined);
79
+ await this.runner
80
+ .run("git", ["--git-dir", this.repositoryPath, "worktree", "prune"])
81
+ .catch(() => undefined);
82
+ });
83
+ }
84
+ async runGit(args, authenticated) {
85
+ const credentialArgs = authenticated && this.githubToken
86
+ ? [
87
+ "-c",
88
+ 'credential.helper=!f() { echo username=x-access-token; echo "password=$GITHUB_TOKEN"; }; f',
89
+ ]
90
+ : [];
91
+ const result = await this.runner.run("git", [...credentialArgs, ...args], {
92
+ env: this.githubToken
93
+ ? { ...process.env, GITHUB_TOKEN: this.githubToken, GIT_TERMINAL_PROMPT: "0" }
94
+ : { ...process.env, GIT_TERMINAL_PROMPT: "0" },
95
+ });
96
+ if (result.code !== 0) {
97
+ throw new Error(`git ${args[0] ?? "command"} failed: ${result.stderr || result.stdout}`);
98
+ }
99
+ return result;
100
+ }
101
+ }
102
+ exports.GitWorktree = GitWorktree;
103
+ async function withRepositoryLock(key, work) {
104
+ const previous = repositoryLocks.get(key) ?? Promise.resolve();
105
+ let release;
106
+ const current = new Promise((resolve) => {
107
+ release = resolve;
108
+ });
109
+ const queued = previous.then(() => current);
110
+ repositoryLocks.set(key, queued);
111
+ await previous;
112
+ try {
113
+ return await work();
114
+ }
115
+ finally {
116
+ release();
117
+ if (repositoryLocks.get(key) === queued)
118
+ repositoryLocks.delete(key);
119
+ }
120
+ }
121
+ async function pathExists(value) {
122
+ try {
123
+ await node_fs_1.promises.access(value);
124
+ return true;
125
+ }
126
+ catch {
127
+ return false;
128
+ }
129
+ }
130
+ function safePathSegment(value) {
131
+ const safe = value.replace(/[^A-Za-z0-9_.-]/g, "-").replace(/^-+|-+$/g, "");
132
+ if (!safe)
133
+ throw new Error("Cannot create a git worktree without a valid project/session identifier.");
134
+ return safe;
135
+ }
136
+ function normalizeCloneUrl(value) {
137
+ return value.trim().replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
138
+ }
139
+ function parseRemoteHead(output) {
140
+ const match = output.match(/^ref:\s+(refs\/heads\/[A-Za-z0-9._/-]+)\s+HEAD$/m);
141
+ return match?.[1] ?? null;
142
+ }
@@ -40,19 +40,22 @@ async function loadMachineIdentity(configDir) {
40
40
  * per-task broker-issued secrets, handled entirely in session.cts/sandbox.cts.
41
41
  */
42
42
  async function resolveMachineIdentity(configDir, apiBaseFallback) {
43
- const envToken = process.env.FLOTILLA_MACHINE_TOKEN;
44
- const envId = process.env.FLOTILLA_MACHINE_ID;
43
+ const envToken = process.env.NAVARCH_MACHINE_TOKEN;
44
+ const envId = process.env.NAVARCH_MACHINE_ID;
45
45
  if (envToken && envId) {
46
46
  return {
47
47
  machine_id: envId,
48
48
  token: envToken,
49
- name: process.env.FLOTILLA_MACHINE_NAME ?? envId,
50
- api_base: process.env.FLOTILLA_API_BASE ?? apiBaseFallback,
49
+ name: process.env.NAVARCH_MACHINE_NAME ?? envId,
50
+ api_base: process.env.NAVARCH_API_BASE ?? apiBaseFallback,
51
+ agent_type: process.env.NAVARCH_AGENT === "codex" || process.env.NAVARCH_AGENT === "claude-code"
52
+ ? process.env.NAVARCH_AGENT
53
+ : undefined,
51
54
  };
52
55
  }
53
56
  const stored = await loadMachineIdentity(configDir);
54
57
  if (stored)
55
58
  return stored;
56
59
  throw new Error("No machine identity found. Run `navarch-runtime register` first, or set " +
57
- "FLOTILLA_MACHINE_TOKEN + FLOTILLA_MACHINE_ID for a twelve-factor deployment.");
60
+ "NAVARCH_MACHINE_TOKEN + NAVARCH_MACHINE_ID for a twelve-factor deployment.");
58
61
  }
@@ -5,27 +5,27 @@
5
5
  // platform MCP server".
6
6
  //
7
7
  // Previously `mcpConfigPath` was just a static, operator-supplied path
8
- // (config.cts's `FLOTILLA_MCP_CONFIG_PATH`) with nothing that ever wrote a
8
+ // (config.cts's `NAVARCH_MCP_CONFIG_PATH`) with nothing that ever wrote a
9
9
  // real config file -- there was no way for a session to actually reach the
10
10
  // platform MCP server with the auth it needs. This module is the fix: one
11
11
  // config, generated fresh per session, carrying the machine's bearer token
12
12
  // and this session's lease id (the same two-layer auth app/api/mcp/route.ts
13
- // requires -- see lib/flotilla/mcp/context.ts).
13
+ // requires -- see lib/navarch/mcp/context.ts).
14
14
  //
15
15
  // Pure and side-effect free (no filesystem access) so it's trivially unit
16
16
  // testable; session.cts is the only caller that writes the result to disk.
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.FLOTILLA_LEASE_HEADER = void 0;
18
+ exports.NAVARCH_LEASE_HEADER = void 0;
19
19
  exports.buildNavarchMcpConfig = buildNavarchMcpConfig;
20
- /** The header app/api/mcp/route.ts's lib/flotilla/mcp/context.ts reads to identify which lease is calling. */
21
- exports.FLOTILLA_LEASE_HEADER = "X-Navarch-Lease-Id";
20
+ /** The header app/api/mcp/route.ts's lib/navarch/mcp/context.ts reads to identify which lease is calling. */
21
+ exports.NAVARCH_LEASE_HEADER = "X-Navarch-Lease-Id";
22
22
  /**
23
23
  * Builds the `.mcp.json`-shaped config object Claude Code's `--mcp-config`
24
24
  * flag expects: a remote "http" (streamable HTTP) server entry with the
25
25
  * bearer token and lease id as request headers.
26
26
  */
27
27
  function buildNavarchMcpConfig(opts) {
28
- const serverName = opts.serverName ?? "flotilla";
28
+ const serverName = opts.serverName ?? "navarch";
29
29
  return {
30
30
  mcpServers: {
31
31
  [serverName]: {
@@ -33,7 +33,7 @@ function buildNavarchMcpConfig(opts) {
33
33
  url: `${opts.apiBase.replace(/\/+$/, "")}/api/mcp`,
34
34
  headers: {
35
35
  Authorization: `Bearer ${opts.machineToken}`,
36
- [exports.FLOTILLA_LEASE_HEADER]: opts.leaseId,
36
+ [exports.NAVARCH_LEASE_HEADER]: opts.leaseId,
37
37
  },
38
38
  },
39
39
  },
package/dist/prompt.cjs CHANGED
@@ -9,9 +9,20 @@ exports.renderPrompt = renderPrompt;
9
9
  * WP-07 "write prompt file".
10
10
  */
11
11
  function renderPrompt(task, bundle) {
12
+ if (bundle.task_context) {
13
+ const repositoryContext = bundle.repository
14
+ ? `Repository: ${bundle.repository.full_name} (${bundle.repository.url})\nLocal checkout: this session's isolated git worktree (current working directory).`
15
+ : null;
16
+ return [repositoryContext, bundle.task_context.trim(), bundle.retry_context?.trim()]
17
+ .filter((section) => Boolean(section))
18
+ .join("\n\n") + "\n";
19
+ }
12
20
  const sections = [];
13
21
  sections.push(`# Task: ${task.summary}`);
14
22
  sections.push(`Type: ${task.task_type} | Repo: ${task.repo} | Environment: ${task.environment_scope}`);
23
+ if (bundle.repository) {
24
+ sections.push(`Repository URL: ${bundle.repository.url}\nLocal checkout: this session's isolated git worktree (current working directory).`);
25
+ }
15
26
  if (task.github_issue_url) {
16
27
  sections.push(`GitHub issue: ${task.github_issue_url}`);
17
28
  }
package/dist/sandbox.cjs CHANGED
@@ -15,7 +15,7 @@ exports.SandboxUnavailableError = SandboxUnavailableError;
15
15
  exports.nodeCommandRunner = {
16
16
  run(cmd, args, opts = {}) {
17
17
  return new Promise((resolve, reject) => {
18
- const child = (0, node_child_process_1.spawn)(cmd, args, { cwd: opts.cwd });
18
+ const child = (0, node_child_process_1.spawn)(cmd, args, { cwd: opts.cwd, env: opts.env });
19
19
  let stdout = "";
20
20
  let stderr = "";
21
21
  let settled = false;
@@ -69,7 +69,7 @@ async function isDockerAvailable(runner = exports.nodeCommandRunner) {
69
69
  }
70
70
  }
71
71
  function containerName(sessionId) {
72
- return `flotilla-${sessionId}`;
72
+ return `navarch-${sessionId}`;
73
73
  }
74
74
  /**
75
75
  * One Docker container per session (implementation-plan.md WP-07 "Sandbox:
@@ -91,14 +91,27 @@ class DockerSandbox {
91
91
  runner;
92
92
  workDir;
93
93
  image;
94
+ containerWorkDir;
95
+ sharedGitDir;
94
96
  constructor(opts) {
95
97
  this.name = containerName(opts.sessionId);
96
98
  this.runner = opts.runner ?? exports.nodeCommandRunner;
97
99
  this.workDir = node_path_1.default.join(opts.workspaceRoot, opts.sessionId);
98
100
  this.image = opts.image;
101
+ this.containerWorkDir = opts.containerWorkDir ?? null;
102
+ this.sharedGitDir = opts.sharedGitDir ?? null;
99
103
  }
100
104
  async create() {
101
105
  await node_fs_1.promises.mkdir(this.workDir, { recursive: true });
106
+ const mounts = this.containerWorkDir
107
+ ? [
108
+ "-v",
109
+ `${this.workDir}:${this.workDir}`,
110
+ ...(this.sharedGitDir ? ["-v", `${this.sharedGitDir}:${this.sharedGitDir}`] : []),
111
+ "-w",
112
+ this.containerWorkDir,
113
+ ]
114
+ : ["-v", `${this.workDir}:/workspace`, "-w", "/workspace"];
102
115
  const result = await this.runner.run("docker", [
103
116
  "run",
104
117
  "-d",
@@ -110,10 +123,7 @@ class DockerSandbox {
110
123
  "/tmp",
111
124
  "--tmpfs",
112
125
  "/run",
113
- "-v",
114
- `${this.workDir}:/workspace`,
115
- "-w",
116
- "/workspace",
126
+ ...mounts,
117
127
  this.image,
118
128
  "tail",
119
129
  "-f",
@@ -156,9 +166,13 @@ class DockerSandbox {
156
166
  }
157
167
  /** Force-removes the container and the host-side workspace mount. Best-effort: never throws. */
158
168
  async wipe() {
159
- await this.runner.run("docker", ["rm", "-f", this.name]).catch(() => undefined);
169
+ await this.stop();
160
170
  await node_fs_1.promises.rm(this.workDir, { recursive: true, force: true }).catch(() => undefined);
161
171
  }
172
+ /** Stops/removes only the container; the session orchestrator owns worktree cleanup. */
173
+ async stop() {
174
+ await this.runner.run("docker", ["rm", "-f", this.name]).catch(() => undefined);
175
+ }
162
176
  }
163
177
  exports.DockerSandbox = DockerSandbox;
164
178
  function shellQuote(value) {
package/dist/session.cjs CHANGED
@@ -14,26 +14,27 @@ const upload_cjs_1 = require("./upload.cjs");
14
14
  const prompt_cjs_1 = require("./prompt.cjs");
15
15
  const mcp_config_cjs_1 = require("./mcp-config.cjs");
16
16
  const logger_cjs_1 = require("./logger.cjs");
17
- /** Filename the generated platform MCP config is written under, inside the session's workDir (host-side; also /workspace inside the Docker sandbox -- see sandbox.cts's bind mount). */
17
+ const git_worktree_cjs_1 = require("./git-worktree.cjs");
18
+ /** Filename the generated platform MCP config is written under inside the session metadata directory. */
18
19
  const MCP_CONFIG_FILENAME = "mcp-config.json";
19
20
  const log = (0, logger_cjs_1.createLogger)("session");
20
21
  /**
21
22
  * Runs one claimed task end to end (implementation-plan.md WP-07):
22
23
  * 1. write the prompt file
23
24
  * 2. fetch secrets from the broker once, at session start
24
- * 3. stand up a Docker sandbox, clone the repo, inject env vars
25
+ * 3. optionally stand up a Docker sandbox when explicitly configured
25
26
  * 4. run the configured agent adapter (Claude Code or Codex, per
26
- * FLOTILLA_AGENT — adapters/index.cts#selectAdapter), heartbeating the
27
+ * NAVARCH_AGENT — adapters/index.cts#selectAdapter), heartbeating the
27
28
  * lease throughout
28
29
  * 5. redact + upload the transcript, map the exit condition, complete the
29
30
  * lease (recording which agent_type ran it)
30
31
  * 6. wipe the sandbox unconditionally
31
32
  *
32
- * NEEDS LIVE VERIFICATION: the full path requires a real Docker daemon, a
33
- * real `claude` (or `codex`) binary, and a live control-plane API none of
34
- * which are available in this offline build environment. Unit tests exercise
35
- * each collaborator (api.cts, sandbox.cts, exit-conditions.cts, redact.cts,
36
- * adapters/*.cts) in isolation instead; see runtime/README.md.
33
+ * NEEDS LIVE VERIFICATION: the full path requires a real `claude` (or
34
+ * `codex`) binary and a live control-plane API; Docker-backed execution also
35
+ * requires a real Docker daemon. Unit tests exercise each collaborator
36
+ * (api.cts, sandbox.cts, exit-conditions.cts, redact.cts, adapters/*.cts) in
37
+ * isolation instead; see runtime/README.md.
37
38
  */
38
39
  async function runSession(deps, claimed, sessionId) {
39
40
  const { api, config } = deps;
@@ -41,7 +42,7 @@ async function runSession(deps, claimed, sessionId) {
41
42
  // The session's identity is the pre-allocated session id sent at claim time
42
43
  // (recorded on the lease by the dispatcher). Lease-scoped API calls
43
44
  // (heartbeat/complete/issue/transcript) still key on leaseId.
44
- const workDir = node_path_1.default.join(config.workspaceRoot, sessionId);
45
+ const workDir = node_path_1.default.join(config.workspaceRoot, "sessions", sessionId);
45
46
  await node_fs_1.promises.mkdir(workDir, { recursive: true });
46
47
  const promptText = (0, prompt_cjs_1.renderPrompt)(task, bundle);
47
48
  await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), promptText, "utf8");
@@ -56,6 +57,31 @@ async function runSession(deps, claimed, sessionId) {
56
57
  secrets = issued.secrets;
57
58
  registry.registerAll(secrets);
58
59
  }
60
+ const cloneUrl = bundle.repository?.clone_url ??
61
+ (task.repo ? `https://github.com/${task.repo.replace(/\.git$/, "")}.git` : null);
62
+ if (!cloneUrl) {
63
+ const failureSummary = `Project ${task.project_id} has no GitHub repository URL. Set it in Project settings before dispatching work.`;
64
+ await api.completeLease(leaseId, {
65
+ status: "failed",
66
+ report: failureSummary,
67
+ failure_summary: failureSummary,
68
+ evidence_urls: [],
69
+ cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
70
+ exit_status: "crashed",
71
+ agent_type: config.agentType,
72
+ });
73
+ secrets = {};
74
+ await node_fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
75
+ return;
76
+ }
77
+ const gitWorktree = new git_worktree_cjs_1.GitWorktree({
78
+ workspaceRoot: config.workspaceRoot,
79
+ projectId: task.project_id,
80
+ taskId: task.id,
81
+ sessionId,
82
+ cloneUrl,
83
+ githubToken: secrets[bundle.repository?.credential_secret_name ?? "github-pat"],
84
+ });
59
85
  const abortController = new AbortController();
60
86
  let leaseLost = false;
61
87
  const heartbeatTimer = setInterval(() => {
@@ -73,25 +99,34 @@ async function runSession(deps, claimed, sessionId) {
73
99
  .completeLease(leaseId, {
74
100
  status: "failed",
75
101
  report: "Docker sandbox unavailable on this machine.",
102
+ failure_summary: "Docker sandbox unavailable on this machine.",
76
103
  evidence_urls: [],
77
104
  cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
78
105
  exit_status: "crashed",
79
106
  agent_type: config.agentType,
80
107
  })
81
108
  .catch((err) => log.warn(`complete() after docker-unavailable also failed: ${String(err)}`));
109
+ secrets = {};
110
+ await node_fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
82
111
  return;
83
112
  }
84
113
  const sandbox = dockerAvailable
85
- ? new sandbox_cjs_1.DockerSandbox({ sessionId, workspaceRoot: config.workspaceRoot, image: config.dockerImage })
114
+ ? new sandbox_cjs_1.DockerSandbox({
115
+ sessionId,
116
+ workspaceRoot: node_path_1.default.join(config.workspaceRoot, "sessions"),
117
+ image: config.dockerImage,
118
+ containerWorkDir: gitWorktree.worktreePath,
119
+ sharedGitDir: gitWorktree.repositoryPath,
120
+ })
86
121
  : null;
87
122
  // Platform MCP config (implementation-plan.md WP-07: "--mcp-config
88
123
  // pointing at the platform MCP server"): generated fresh per session,
89
124
  // carrying this machine's bearer token and this session's lease id (the
90
- // auth app/api/mcp/route.ts requires -- see lib/flotilla/mcp/context.ts),
91
- // unless the operator pinned a static override via FLOTILLA_MCP_CONFIG_PATH
125
+ // auth app/api/mcp/route.ts requires -- see lib/navarch/mcp/context.ts),
126
+ // unless the operator pinned a static override via NAVARCH_MCP_CONFIG_PATH
92
127
  // (e.g. pointing at a fake MCP server in local testing). Written to
93
- // workDir (host path) so it also lands at /workspace/mcp-config.json
94
- // inside the Docker sandbox (sandbox.cts binds workDir at /workspace) --
128
+ // workDir (host path), which Docker mode mounts at that same absolute path
129
+ // so worktree .git pointers and this config path remain valid --
95
130
  // the selected adapter (adapters/claude.cts or adapters/codex.cts) needs a
96
131
  // path valid in whichever environment it actually runs.
97
132
  let mcpConfigPath = config.mcpConfigPath;
@@ -102,15 +137,15 @@ async function runSession(deps, claimed, sessionId) {
102
137
  leaseId,
103
138
  });
104
139
  await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, MCP_CONFIG_FILENAME), JSON.stringify(mcpConfig, null, 2), "utf8");
105
- mcpConfigPath = sandbox ? `/workspace/${MCP_CONFIG_FILENAME}` : node_path_1.default.join(workDir, MCP_CONFIG_FILENAME);
140
+ mcpConfigPath = node_path_1.default.join(workDir, MCP_CONFIG_FILENAME);
106
141
  }
107
142
  try {
143
+ await gitWorktree.prepare();
108
144
  if (sandbox) {
109
145
  await sandbox.create();
110
146
  await sandbox.injectEnv(toEnvMap(secrets));
111
- await sandbox.cloneRepo(task.repo, Boolean(secrets["github-pat"]));
112
147
  }
113
- // Picks the Claude Code or Codex adapter per FLOTILLA_AGENT
148
+ // Picks the Claude Code or Codex adapter per NAVARCH_AGENT
114
149
  // (config.cts's `agentType`) — see adapters/index.cts#selectAdapter.
115
150
  // Both adapters implement the same AgentAdapter.run() shape
116
151
  // (adapters/types.cts), so nothing else in this function branches on
@@ -124,8 +159,8 @@ async function runSession(deps, claimed, sessionId) {
124
159
  bin,
125
160
  extraArgs,
126
161
  timeoutMs: config.sessionTimeoutMs,
127
- env: secrets,
128
- cwd: sandbox ? undefined : workDir,
162
+ env: toEnvMap(secrets),
163
+ cwd: sandbox ? undefined : gitWorktree.worktreePath,
129
164
  dockerExec: sandbox ? { containerName: sandbox.name, runner: sandbox_cjs_1.nodeCommandRunner } : undefined,
130
165
  signal: abortController.signal,
131
166
  });
@@ -164,10 +199,12 @@ async function runSession(deps, claimed, sessionId) {
164
199
  }
165
200
  catch (err) {
166
201
  log.error(`session ${leaseId} threw before completing: ${String(err)}`);
202
+ const failureSummary = (0, redact_cjs_1.redactText)(`Session crashed: ${String(err)}`, registry.list());
167
203
  await api
168
204
  .completeLease(leaseId, {
169
205
  status: "failed",
170
- report: (0, redact_cjs_1.redactText)(`Session crashed: ${String(err)}`, registry.list()),
206
+ report: failureSummary,
207
+ failure_summary: failureSummary,
171
208
  evidence_urls: [],
172
209
  cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
173
210
  exit_status: "crashed",
@@ -179,7 +216,8 @@ async function runSession(deps, claimed, sessionId) {
179
216
  clearInterval(heartbeatTimer);
180
217
  secrets = {};
181
218
  if (sandbox)
182
- await sandbox.wipe();
219
+ await sandbox.stop();
220
+ await gitWorktree.cleanup();
183
221
  await node_fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
184
222
  }
185
223
  }
@@ -191,6 +229,14 @@ function toEnvMap(secrets) {
191
229
  }
192
230
  if (secrets["github-pat"] && !out.GITHUB_TOKEN) {
193
231
  out.GITHUB_TOKEN = secrets["github-pat"];
232
+ // Make ordinary `git push` calls from the agent use the in-memory token.
233
+ // The helper contains only an env-var reference; the token itself never
234
+ // lands in argv, git config, or the worktree.
235
+ out.GIT_CONFIG_COUNT = "1";
236
+ out.GIT_CONFIG_KEY_0 = "credential.helper";
237
+ out.GIT_CONFIG_VALUE_0 =
238
+ '!f() { echo username=x-access-token; echo "password=$GITHUB_TOKEN"; }; f';
239
+ out.GIT_TERMINAL_PROMPT = "0";
194
240
  }
195
241
  return out;
196
242
  }
package/dist/types.cjs CHANGED
@@ -2,8 +2,8 @@
2
2
  // Navarch runtime — shared wire types.
3
3
  //
4
4
  // These interfaces mirror the JSON shapes described in:
5
- // - docs/flotilla/schema-design.md (table columns => API field names, esp. §4, §5, §7)
6
- // - docs/flotilla/implementation-plan.md (WP-07 behavioral contract)
5
+ // - docs/navarch/schema-design.md (table columns => API field names, esp. §4, §5, §7)
6
+ // - docs/navarch/implementation-plan.md (WP-07 behavioral contract)
7
7
  // - docs/agent-platform-project-plan.md (§3.8 dispatch/§3.9 adapter contract)
8
8
  //
9
9
  // The control-plane API is being built in parallel (WP-01/WP-04/WP-05) in
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.0",
4
- "description": "Navarch machine-side session manager: registers a machine, claims tasks from the control-plane dispatcher, runs them in a Docker sandbox via the Claude Code or Codex adapter, and reports results back.",
3
+ "version": "0.1.2",
4
+ "description": "Navarch machine-side session manager: registers a machine, claims tasks from the control-plane dispatcher, runs them via the Claude Code or Codex adapter, and reports results back.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -45,6 +45,6 @@
45
45
  "devDependencies": {
46
46
  "@types/node": "^20.14.0",
47
47
  "typescript": "^5.7.2",
48
- "vitest": "^2.1.8"
48
+ "vitest": "^4.1.10"
49
49
  }
50
50
  }