@sagentlab/navarch-runtime 0.1.125 → 0.1.127
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 +47 -1
- package/dist/agent-types.cjs +37 -0
- package/dist/cli.cjs +32 -19
- package/dist/config.cjs +21 -20
- package/dist/conversation/capability.cjs +2 -2
- package/dist/dependency-cache.cjs +566 -0
- package/dist/github-credential.cjs +43 -0
- package/dist/machine-store.cjs +2 -2
- package/dist/session.cjs +57 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -507,6 +507,50 @@ unchanged across the deployment.
|
|
|
507
507
|
| `NAVARCH_CONVERSATION_MIRROR` | on | Seed new conversation checkouts from a per-repository mirror under `<workspaceRoot>/conversation-mirrors/` (fetch + local clone with independent object files) instead of a full network clone. Set `off` to always clone directly. |
|
|
508
508
|
| `NAVARCH_CONVERSATION_TURN_TIMEOUT_MS` | `540000` | Time limit for one conversation turn's agent process (at most nine minutes). |
|
|
509
509
|
| `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. |
|
|
510
|
+
| `NAVARCH_DEPENDENCY_CACHE` | `verify` | Comma-separated task types whose host-mode worktrees receive prepared `node_modules` from the lockfile-keyed dependency cache under `<workspaceRoot>/dependency-cache/` (see below). Set `off` to disable. |
|
|
511
|
+
| `NAVARCH_DEPENDENCY_CACHE_TIMEOUT_MS` | `600000` | Time limit for one `npm ci` cache build. A timeout records the directory as not prepared; the session still starts. |
|
|
512
|
+
|
|
513
|
+
## Dependency cache (host mode)
|
|
514
|
+
|
|
515
|
+
Coding-agent sandboxes cannot reach a package registry, so a verify session
|
|
516
|
+
could not install dependencies for local tests. Before the agent starts, the
|
|
517
|
+
session manager (which is not sandboxed) prepares them
|
|
518
|
+
(`src/dependency-cache.cts`):
|
|
519
|
+
|
|
520
|
+
- It finds the tracked `package-lock.json` at the root and in first-level
|
|
521
|
+
directories (at most four) and handles each one separately.
|
|
522
|
+
- The cache key is a SHA-256 over the repository, lockfile, `package.json`,
|
|
523
|
+
project `.npmrc`, Node and npm versions, platform, and architecture. Entries
|
|
524
|
+
never cross repositories.
|
|
525
|
+
- It refuses repository content that could point npm at the host. Every
|
|
526
|
+
lockfile entry must be an HTTPS tarball with an `integrity` hash (no
|
|
527
|
+
`file:`, link, or git entries, lockfileVersion 2 or later). The project
|
|
528
|
+
`.npmrc` may set only `registry`, `@scope:registry`, and resolution rules
|
|
529
|
+
such as `legacy-peer-deps`. Inputs must be regular files, not symlinks.
|
|
530
|
+
- On a miss it runs `npm ci --ignore-scripts --strict-ssl=true
|
|
531
|
+
--install-links=false` in a staging directory, with a per-repository npm
|
|
532
|
+
cache, writes `manifest.json`, and publishes the entry by rename. npm checks
|
|
533
|
+
each tarball against the lockfile `integrity`. No dependency code runs on
|
|
534
|
+
the host. npm gets an allowlisted environment without Navarch credentials or
|
|
535
|
+
provider tokens, but it reads the operator's `~/.npmrc`, so private registry
|
|
536
|
+
credentials there are used for the download.
|
|
537
|
+
- It copies the entry into the worktree (copy-on-write where the filesystem
|
|
538
|
+
supports it), staging the copy in the session directory outside the
|
|
539
|
+
checkout, so a session cannot change the cached tree. The cache runs only
|
|
540
|
+
when the worktree guard is on, because the guard keeps the agent out of
|
|
541
|
+
`<workspaceRoot>/dependency-cache/`.
|
|
542
|
+
- It writes only an absent `node_modules` that git ignores. npm workspaces
|
|
543
|
+
are skipped.
|
|
544
|
+
- It keeps the two most recently used entries for each repository and lockfile
|
|
545
|
+
directory, removes entries unused for 14 days, never removes an entry used
|
|
546
|
+
in the last 30 minutes, and removes staging directories older than one day.
|
|
547
|
+
The per-repository npm tarball caches are not pruned.
|
|
548
|
+
- The prompt gets a **Prepared dependencies** section and the log records each
|
|
549
|
+
result. A failure never stops the session.
|
|
550
|
+
|
|
551
|
+
Docker mode is not covered: a tree built on the host does not match the
|
|
552
|
+
image's platform. The `elevated-verification` profile with an enforcing
|
|
553
|
+
`NAVARCH_SANDBOX_EGRESS_NETWORK` is the Docker path.
|
|
510
554
|
|
|
511
555
|
## Worktree boundary guard (host mode)
|
|
512
556
|
|
|
@@ -691,7 +735,9 @@ export NAVARCH_RUNTIMES=claude-code,codex,antigravity,opencode,acp,qoder,muse
|
|
|
691
735
|
```
|
|
692
736
|
|
|
693
737
|
For the legacy single-runtime setting, priority is `start --agent` →
|
|
694
|
-
`NAVARCH_AGENT` → the locally saved choice → `claude-code`.
|
|
738
|
+
`NAVARCH_AGENT` → the locally saved choice → `claude-code`. An unrecognized
|
|
739
|
+
`NAVARCH_AGENT` (for example `gemni`) is skipped with a warning that names it,
|
|
740
|
+
so the saved choice still applies.
|
|
695
741
|
`NAVARCH_RUNTIMES` expands what the worker advertises. For normal projects,
|
|
696
742
|
the locally selected `NAVARCH_AGENT` handles every unpinned task. A task with a
|
|
697
743
|
runtime override (for example `antigravity`) is claimed only by a worker that
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The one list of adapter ids. Every other adapter union in the runtime
|
|
3
|
+
// (wire types in types.cts, AgentType in adapters/types.cts) derives from the
|
|
4
|
+
// lists below, so adding an adapter here is the only edit that widens them.
|
|
5
|
+
// Kept dependency-free so type-only importers (types.cts) and modules that
|
|
6
|
+
// config.cts itself imports can read it without a module cycle.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.AGENT_TYPES = exports.MANAGED_AGENT_TYPES = exports.RETIRED_AGENT_TYPES = exports.RUNTIME_AGENT_TYPES = void 0;
|
|
9
|
+
exports.isRuntimeAgentType = isRuntimeAgentType;
|
|
10
|
+
/**
|
|
11
|
+
* Adapter ids the runtime accepts for `NAVARCH_AGENT` / `--agent`, in display
|
|
12
|
+
* order. `NAVARCH_RUNTIMES` is validated against this same list, and the CLI
|
|
13
|
+
* usage/error copy is generated from it so the advertised set cannot drift.
|
|
14
|
+
*/
|
|
15
|
+
exports.RUNTIME_AGENT_TYPES = [
|
|
16
|
+
"claude-code",
|
|
17
|
+
"codex",
|
|
18
|
+
"antigravity",
|
|
19
|
+
"opencode",
|
|
20
|
+
"acp",
|
|
21
|
+
"qoder",
|
|
22
|
+
"muse",
|
|
23
|
+
"pi-local",
|
|
24
|
+
];
|
|
25
|
+
function isRuntimeAgentType(value) {
|
|
26
|
+
return typeof value === "string" && exports.RUNTIME_AGENT_TYPES.includes(value);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Retired adapters the control plane can still send (historical sessions and
|
|
30
|
+
* existing runtime pins). A machine never selects one locally: config.cts
|
|
31
|
+
* rejects `NAVARCH_AGENT=gemini` with migration guidance.
|
|
32
|
+
*/
|
|
33
|
+
exports.RETIRED_AGENT_TYPES = ["gemini"];
|
|
34
|
+
/** Managed hosted runtimes (#885) that a BYO machine cannot advertise. */
|
|
35
|
+
exports.MANAGED_AGENT_TYPES = ["pi"];
|
|
36
|
+
/** Every adapter a session can run on, in support-matrix order. */
|
|
37
|
+
exports.AGENT_TYPES = [...exports.RUNTIME_AGENT_TYPES, ...exports.RETIRED_AGENT_TYPES, ...exports.MANAGED_AGENT_TYPES];
|
package/dist/cli.cjs
CHANGED
|
@@ -72,12 +72,17 @@ function configFromFlags(flags, identity) {
|
|
|
72
72
|
if (flags["config-dir"])
|
|
73
73
|
env.NAVARCH_CONFIG_DIR = node_path_1.default.resolve(flags["config-dir"]);
|
|
74
74
|
// Resolve the preferred adapter before deriving the advertised runtimes and
|
|
75
|
-
// applying host guards. Keep an explicit NAVARCH_RUNTIMES list intact.
|
|
76
|
-
|
|
75
|
+
// applying host guards. Keep an explicit NAVARCH_RUNTIMES list intact. An
|
|
76
|
+
// unrecognized NAVARCH_AGENT selects nothing, so the saved adapter still
|
|
77
|
+
// applies; the caller warns once with the id (warnUnknownAdapters).
|
|
78
|
+
const unknownAgent = (0, config_cjs_1.unknownAgentId)(env);
|
|
79
|
+
const agentType = agentFromFlag(flags) ?? ((unknownAgent ? undefined : env.NAVARCH_AGENT) || identity?.agent_type);
|
|
77
80
|
if (agentType)
|
|
78
81
|
env.NAVARCH_AGENT = agentType;
|
|
82
|
+
else
|
|
83
|
+
delete env.NAVARCH_AGENT;
|
|
79
84
|
const config = (0, config_cjs_1.loadRuntimeConfig)(env);
|
|
80
|
-
return { ...config, model: modelFromFlags(flags, config.agentType, identity) };
|
|
85
|
+
return { ...config, unknownAgent, model: modelFromFlags(flags, config.agentType, identity) };
|
|
81
86
|
}
|
|
82
87
|
function agentFromFlag(flags) {
|
|
83
88
|
const value = flags.agent;
|
|
@@ -97,18 +102,25 @@ function formatChoices(values) {
|
|
|
97
102
|
return `${quoted.slice(0, -1).join(", ")}, or ${last}`;
|
|
98
103
|
}
|
|
99
104
|
/**
|
|
100
|
-
* A typo in NAVARCH_RUNTIMES must not silently
|
|
101
|
-
* set, so name every dropped id on
|
|
102
|
-
* per config load) to avoid
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
+
* A typo in NAVARCH_AGENT or NAVARCH_RUNTIMES must not silently change the
|
|
106
|
+
* selected adapter or shrink the advertised set, so name every dropped id on
|
|
107
|
+
* stderr. Runs once per top-level command (not per config load) to avoid
|
|
108
|
+
* duplicate lines when start/supervise/doctor resolve the identity and reload
|
|
109
|
+
* config. A supervised worker inherits both variables from the `supervise`
|
|
110
|
+
* parent that already warned, so it stays quiet.
|
|
105
111
|
*/
|
|
106
|
-
function
|
|
107
|
-
|
|
108
|
-
if (config.supervised || unknown.length === 0)
|
|
112
|
+
function warnUnknownAdapters(config) {
|
|
113
|
+
if (config.supervised)
|
|
109
114
|
return;
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
const known = `Known adapters: ${config_cjs_1.RUNTIME_AGENT_TYPES.join(", ")}.`;
|
|
116
|
+
if (config.unknownAgent) {
|
|
117
|
+
const using = config.agentType ? ` Using ${config.agentType}.` : "";
|
|
118
|
+
log.warn(`NAVARCH_AGENT ignores unrecognized adapter id: '${config.unknownAgent}'.${using} ${known}`);
|
|
119
|
+
}
|
|
120
|
+
const unknown = config.unknownRuntimes;
|
|
121
|
+
if (unknown.length > 0) {
|
|
122
|
+
log.warn(`NAVARCH_RUNTIMES ignores unrecognized adapter id(s): ${unknown.join(", ")}. ${known}`);
|
|
123
|
+
}
|
|
112
124
|
}
|
|
113
125
|
/** Saved pins only apply to the adapter they were selected for. */
|
|
114
126
|
function modelFromFlags(flags, agentType, identity) {
|
|
@@ -144,7 +156,7 @@ function parseArgs(argv) {
|
|
|
144
156
|
*/
|
|
145
157
|
async function registerCommand(flags) {
|
|
146
158
|
const config = configFromFlags(flags);
|
|
147
|
-
|
|
159
|
+
warnUnknownAdapters(config);
|
|
148
160
|
const apiBase = flags["api-base"] ?? config.apiBase;
|
|
149
161
|
const enrollmentToken = flags.token ?? process.env.NAVARCH_ENROLLMENT_TOKEN;
|
|
150
162
|
const name = flags.name ?? process.env.NAVARCH_MACHINE_NAME;
|
|
@@ -195,7 +207,7 @@ async function registerCommand(flags) {
|
|
|
195
207
|
*/
|
|
196
208
|
async function connectCommand(flags) {
|
|
197
209
|
const config = configFromFlags(flags);
|
|
198
|
-
|
|
210
|
+
warnUnknownAdapters(config);
|
|
199
211
|
const apiBase = flags["api-base"] ?? config.apiBase;
|
|
200
212
|
const enrollmentToken = flags.token ?? process.env.NAVARCH_ENROLLMENT_TOKEN;
|
|
201
213
|
const name = flags.name ?? process.env.NAVARCH_MACHINE_NAME;
|
|
@@ -239,7 +251,7 @@ async function startCommand(flags) {
|
|
|
239
251
|
const baseConfig = configFromFlags(flags);
|
|
240
252
|
const identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(baseConfig.configDir, baseConfig.apiBase);
|
|
241
253
|
const configuredConfig = configFromFlags(flags, identity);
|
|
242
|
-
|
|
254
|
+
warnUnknownAdapters(configuredConfig);
|
|
243
255
|
// Probe before the first heartbeat or claim so browser-use is never
|
|
244
256
|
// advertised on a machine whose agents cannot start a browser. The verdict
|
|
245
257
|
// holds for this process; restarting the runtime probes again.
|
|
@@ -346,7 +358,7 @@ async function superviseCommand(flags) {
|
|
|
346
358
|
const baseConfig = configFromFlags(flags);
|
|
347
359
|
const identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(baseConfig.configDir, baseConfig.apiBase);
|
|
348
360
|
const config = configFromFlags(flags, identity);
|
|
349
|
-
|
|
361
|
+
warnUnknownAdapters(config);
|
|
350
362
|
// Resolve and pin the adapter before spawning the worker. The supervisor
|
|
351
363
|
// passes machine credentials through the environment, so the child no
|
|
352
364
|
// longer reads machine.json for identity fields (including agent_type).
|
|
@@ -385,14 +397,15 @@ async function doctorCommand(flags) {
|
|
|
385
397
|
}
|
|
386
398
|
if (identity)
|
|
387
399
|
config = configFromFlags(flags, identity);
|
|
388
|
-
|
|
400
|
+
warnUnknownAdapters(config);
|
|
389
401
|
}
|
|
390
402
|
catch (err) {
|
|
391
403
|
// The typo may be why the config is unusable (e.g. `opencode,gemni` on a
|
|
392
404
|
// guarded host leaves only OpenCode), so still name it.
|
|
393
|
-
|
|
405
|
+
warnUnknownAdapters({
|
|
394
406
|
supervised: process.env.NAVARCH_SUPERVISED === "1",
|
|
395
407
|
unknownRuntimes: (0, config_cjs_1.unknownRuntimeIds)(process.env),
|
|
408
|
+
unknownAgent: (0, config_cjs_1.unknownAgentId)(process.env),
|
|
396
409
|
});
|
|
397
410
|
console.log(`config: UNUSABLE — ${err instanceof Error ? err.message : String(err)}`);
|
|
398
411
|
process.exitCode = 1;
|
package/dist/config.cjs
CHANGED
|
@@ -3,39 +3,35 @@ 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.OPENCODE_HOST_GUARD_MESSAGE = exports.ANTIGRAVITY_HOST_GUARD_MESSAGE = exports.RUNTIME_AGENT_TYPES = exports.DEFAULT_SANDBOX_IMAGE = void 0;
|
|
7
|
-
exports.
|
|
6
|
+
exports.OPENCODE_HOST_GUARD_MESSAGE = exports.ANTIGRAVITY_HOST_GUARD_MESSAGE = exports.isRuntimeAgentType = exports.RUNTIME_AGENT_TYPES = exports.DEFAULT_SANDBOX_IMAGE = void 0;
|
|
7
|
+
exports.unknownAgentId = unknownAgentId;
|
|
8
8
|
exports.unknownRuntimeIds = unknownRuntimeIds;
|
|
9
9
|
exports.opencodeBlockedOnHost = opencodeBlockedOnHost;
|
|
10
10
|
exports.loadRuntimeConfig = loadRuntimeConfig;
|
|
11
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
12
12
|
const node_os_1 = __importDefault(require("node:os"));
|
|
13
13
|
const muse_cjs_1 = require("./adapters/muse.cjs");
|
|
14
|
+
const agent_types_cjs_1 = require("./agent-types.cjs");
|
|
14
15
|
const sandbox_profile_cjs_1 = require("./sandbox-profile.cjs");
|
|
15
16
|
/** Published image containing git, GitHub CLI, and the pinned Claude Code CLI. */
|
|
16
17
|
exports.DEFAULT_SANDBOX_IMAGE = "ghcr.io/sagentlab/navarch-sandbox-agent:0.1.0";
|
|
18
|
+
var agent_types_cjs_2 = require("./agent-types.cjs");
|
|
19
|
+
Object.defineProperty(exports, "RUNTIME_AGENT_TYPES", { enumerable: true, get: function () { return agent_types_cjs_2.RUNTIME_AGENT_TYPES; } });
|
|
20
|
+
Object.defineProperty(exports, "isRuntimeAgentType", { enumerable: true, get: function () { return agent_types_cjs_2.isRuntimeAgentType; } });
|
|
17
21
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
22
|
+
* NAVARCH_AGENT when it is set but names no known adapter. Retired ids
|
|
23
|
+
* (`gemini`) are not reported here: loadRuntimeConfig rejects them with
|
|
24
|
+
* migration guidance.
|
|
21
25
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
"opencode",
|
|
27
|
-
"acp",
|
|
28
|
-
"qoder",
|
|
29
|
-
"muse",
|
|
30
|
-
"pi-local",
|
|
31
|
-
];
|
|
32
|
-
function isRuntimeAgentType(value) {
|
|
33
|
-
return typeof value === "string" && exports.RUNTIME_AGENT_TYPES.includes(value);
|
|
26
|
+
function unknownAgentId(env) {
|
|
27
|
+
const value = env.NAVARCH_AGENT;
|
|
28
|
+
const retired = agent_types_cjs_1.RETIRED_AGENT_TYPES.includes(value ?? "");
|
|
29
|
+
return value && !retired && !(0, agent_types_cjs_1.isRuntimeAgentType)(value) ? value : undefined;
|
|
34
30
|
}
|
|
35
31
|
/** Unrecognized ids in NAVARCH_RUNTIMES, deduplicated, in the order given. */
|
|
36
32
|
function unknownRuntimeIds(env) {
|
|
37
33
|
const ids = envList(env, "NAVARCH_RUNTIMES", []);
|
|
38
|
-
return [...new Set(ids.filter((runtime) => !isRuntimeAgentType(runtime)))];
|
|
34
|
+
return [...new Set(ids.filter((runtime) => !(0, agent_types_cjs_1.isRuntimeAgentType)(runtime)))];
|
|
39
35
|
}
|
|
40
36
|
function envInt(env, name, fallback) {
|
|
41
37
|
const raw = env[name];
|
|
@@ -101,7 +97,7 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
101
97
|
// prerequisite for claiming ordinary shell work.
|
|
102
98
|
const sandboxMode = env.NAVARCH_SANDBOX_MODE === "docker" ? "docker" : "host";
|
|
103
99
|
const defaultCapabilities = sandboxMode === "docker" ? ["docker-sandbox", "shell"] : ["shell", "browser-use"];
|
|
104
|
-
const requestedAgentType = isRuntimeAgentType(env.NAVARCH_AGENT) ? env.NAVARCH_AGENT : "claude-code";
|
|
100
|
+
const requestedAgentType = (0, agent_types_cjs_1.isRuntimeAgentType)(env.NAVARCH_AGENT) ? env.NAVARCH_AGENT : "claude-code";
|
|
105
101
|
// Multiple sessions share one machine; keeping each agent inside its own
|
|
106
102
|
// worktree is the safe default, so disabling is the explicit opt-out.
|
|
107
103
|
const worktreeGuard = !["off", "false", "0"].includes(env.NAVARCH_WORKTREE_GUARD ?? "");
|
|
@@ -109,7 +105,7 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
109
105
|
// Keep unknown ids visible so a typo surfaces (CLI warns) instead of the
|
|
110
106
|
// machine quietly shrinking the adapters it advertises.
|
|
111
107
|
const unknownRuntimes = unknownRuntimeIds(env);
|
|
112
|
-
const requestedRuntimes = requestedRuntimeIds.filter(isRuntimeAgentType);
|
|
108
|
+
const requestedRuntimes = requestedRuntimeIds.filter(agent_types_cjs_1.isRuntimeAgentType);
|
|
113
109
|
// OpenCode's CLI cannot express the host worktree boundary, so its adapter
|
|
114
110
|
// fails closed there (adapters/opencode.cts). Advertising it anyway meant
|
|
115
111
|
// the machine claimed OpenCode tasks it could only fail, one lease at a
|
|
@@ -157,6 +153,7 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
157
153
|
agentType,
|
|
158
154
|
runtimes: runtimes.length > 0 ? runtimes : [agentType],
|
|
159
155
|
unknownRuntimes,
|
|
156
|
+
unknownAgent: unknownAgentId(env),
|
|
160
157
|
claudeBin: env.NAVARCH_CLAUDE_BIN ?? "claude",
|
|
161
158
|
claudeExtraArgs: envList(env, "NAVARCH_CLAUDE_EXTRA_ARGS", []),
|
|
162
159
|
codexBin: env.NAVARCH_CODEX_BIN ?? "codex",
|
|
@@ -192,6 +189,10 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
192
189
|
.split(node_path_1.default.delimiter)
|
|
193
190
|
.map((s) => s.trim())
|
|
194
191
|
.filter(Boolean),
|
|
192
|
+
dependencyCacheTaskTypes: env.NAVARCH_DEPENDENCY_CACHE?.trim().toLowerCase() === "off"
|
|
193
|
+
? []
|
|
194
|
+
: envList(env, "NAVARCH_DEPENDENCY_CACHE", ["verify"]),
|
|
195
|
+
dependencyCacheTimeoutMs: envInt(env, "NAVARCH_DEPENDENCY_CACHE_TIMEOUT_MS", 10 * 60_000),
|
|
195
196
|
updateChannel: env.NAVARCH_UPDATE_CHANNEL === "canary" ? "canary" : "stable",
|
|
196
197
|
supervised: env.NAVARCH_SUPERVISED === "1",
|
|
197
198
|
autoUpdate: env.NAVARCH_SUPERVISED === "1" &&
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.conversationCapability = conversationCapability;
|
|
4
4
|
exports.conversationSupportMatrix = conversationSupportMatrix;
|
|
5
|
+
const agent_types_cjs_1 = require("../agent-types.cjs");
|
|
5
6
|
const index_cjs_1 = require("../adapters/index.cjs");
|
|
6
7
|
/**
|
|
7
8
|
* Whether this runtime can execute a Navi conversation turn, with the reason
|
|
@@ -40,8 +41,7 @@ function conversationCapability(config) {
|
|
|
40
41
|
}
|
|
41
42
|
/** Per-adapter support table for docs and diagnostics (host sandbox, guard on, feature enabled). */
|
|
42
43
|
function conversationSupportMatrix() {
|
|
43
|
-
|
|
44
|
-
return agents.map((agentType) => ({
|
|
44
|
+
return agent_types_cjs_1.AGENT_TYPES.map((agentType) => ({
|
|
45
45
|
agentType,
|
|
46
46
|
capability: conversationCapability({
|
|
47
47
|
agentType,
|
|
@@ -0,0 +1,566 @@
|
|
|
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.DEPENDENCY_CACHE_DIRNAME = void 0;
|
|
7
|
+
exports.npmEnvironment = npmEnvironment;
|
|
8
|
+
exports.prepareDependencyCache = prepareDependencyCache;
|
|
9
|
+
exports.dependencyCacheKey = dependencyCacheKey;
|
|
10
|
+
exports.renderDependencyCacheNote = renderDependencyCacheNote;
|
|
11
|
+
exports.lockfileRefusal = lockfileRefusal;
|
|
12
|
+
exports.npmrcRefusal = npmrcRefusal;
|
|
13
|
+
exports.copyTree = copyTree;
|
|
14
|
+
const node_crypto_1 = require("node:crypto");
|
|
15
|
+
const node_fs_1 = require("node:fs");
|
|
16
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
+
const logger_cjs_1 = require("./logger.cjs");
|
|
18
|
+
const sandbox_cjs_1 = require("./sandbox.cjs");
|
|
19
|
+
/**
|
|
20
|
+
* Lockfile-keyed dependency cache for host-mode sessions (issue #667).
|
|
21
|
+
*
|
|
22
|
+
* A coding-agent sandbox has no npm registry egress (worktree-guard.cts allows
|
|
23
|
+
* GitHub only), so a fresh worktree cannot run `npm ci`. The session manager
|
|
24
|
+
* is not sandboxed, so before the agent starts it installs each tracked
|
|
25
|
+
* `package-lock.json` once with `npm ci --ignore-scripts`, keeps the result
|
|
26
|
+
* under `<workspaceRoot>/dependency-cache/<key>/`, and copies that tree into
|
|
27
|
+
* the worktree as the ignored `node_modules` directory.
|
|
28
|
+
*
|
|
29
|
+
* Policy, in one place:
|
|
30
|
+
* - Key: repository, lockfile, package.json and project .npmrc bytes, Node,
|
|
31
|
+
* npm, platform and arch. A cache entry is never shared across
|
|
32
|
+
* repositories, and any input change produces a new entry.
|
|
33
|
+
* - Lifecycle scripts never run. Dependency code does not execute on the
|
|
34
|
+
* host; npm still checks every tarball against the lockfile's `integrity`.
|
|
35
|
+
* - Repository content cannot point npm at the host: every lockfile entry
|
|
36
|
+
* must be an HTTPS tarball with an integrity hash (no `file:`, link, or git
|
|
37
|
+
* entries), the project `.npmrc` may only choose registries and resolution
|
|
38
|
+
* rules, inputs must be regular files, and TLS, link and log settings are
|
|
39
|
+
* fixed on the command line. npm sees an allowlisted environment, but it
|
|
40
|
+
* does read the operator's `~/.npmrc` so private registries keep working.
|
|
41
|
+
* - Entries are built in a temporary directory and published by rename, so a
|
|
42
|
+
* partial install is never visible. The cache root is outside every
|
|
43
|
+
* session's guard roots, and the worktree gets a copy (copy-on-write where
|
|
44
|
+
* the filesystem supports it), so a session cannot change what the next
|
|
45
|
+
* session receives.
|
|
46
|
+
* - Only an ignored, absent `node_modules` is written, so the copy cannot
|
|
47
|
+
* become part of a commit or replace files the checkout already has.
|
|
48
|
+
* - npm workspaces are skipped: their install also writes into the
|
|
49
|
+
* workspace packages, which a root-level copy does not reproduce.
|
|
50
|
+
*
|
|
51
|
+
* Every step degrades to a recorded `skipped`/`failed` result. Dependency
|
|
52
|
+
* preparation never fails the session.
|
|
53
|
+
*/
|
|
54
|
+
const log = (0, logger_cjs_1.createLogger)("dependency-cache");
|
|
55
|
+
exports.DEPENDENCY_CACHE_DIRNAME = "dependency-cache";
|
|
56
|
+
const MANIFEST_FILENAME = "manifest.json";
|
|
57
|
+
const KEY_VERSION = 1;
|
|
58
|
+
const LOCKFILE = "package-lock.json";
|
|
59
|
+
/** At most this many lockfiles per worktree; deeper or extra ones are ignored. */
|
|
60
|
+
const MAX_LOCKFILES = 4;
|
|
61
|
+
const DEFAULT_TIMEOUT_MS = 10 * 60_000;
|
|
62
|
+
/** Entries kept per repository + lockfile directory; older ones are pruned. */
|
|
63
|
+
const DEFAULT_KEEP_PER_LOCKFILE = 2;
|
|
64
|
+
const STALE_STAGING_MS = 24 * 60 * 60_000;
|
|
65
|
+
/** Entries used this recently are never pruned: another session may be copying them. */
|
|
66
|
+
const IN_USE_GRACE_MS = 30 * 60_000;
|
|
67
|
+
/** Entries unused this long are pruned for any repository. */
|
|
68
|
+
const UNUSED_ENTRY_MS = 14 * 24 * 60 * 60_000;
|
|
69
|
+
/** Largest package.json / package-lock.json / .npmrc the cache reads. */
|
|
70
|
+
const MAX_INPUT_BYTES = 64 * 1024 * 1024;
|
|
71
|
+
/** Environment variables npm may see. Everything else (tokens, NAVARCH_*) is dropped. */
|
|
72
|
+
const NPM_ENV_ALLOWLIST = [
|
|
73
|
+
"PATH", "HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "TMPDIR",
|
|
74
|
+
"NVM_DIR", "NVM_BIN", "MISE_DATA_DIR", "XDG_DATA_HOME", "XDG_CONFIG_HOME",
|
|
75
|
+
"HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy",
|
|
76
|
+
"NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE", "SSL_CERT_DIR",
|
|
77
|
+
];
|
|
78
|
+
function npmEnvironment(env) {
|
|
79
|
+
const out = {};
|
|
80
|
+
for (const name of NPM_ENV_ALLOWLIST) {
|
|
81
|
+
if (env[name] !== undefined)
|
|
82
|
+
out[name] = env[name];
|
|
83
|
+
}
|
|
84
|
+
out.npm_config_update_notifier = "false";
|
|
85
|
+
out.npm_config_fund = "false";
|
|
86
|
+
out.npm_config_audit = "false";
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
async function prepareDependencyCache(options) {
|
|
90
|
+
const runner = options.runner ?? sandbox_cjs_1.nodeCommandRunner;
|
|
91
|
+
const npmBin = options.npmBin ?? "npm";
|
|
92
|
+
const env = npmEnvironment(options.env ?? process.env);
|
|
93
|
+
const dirs = await trackedLockfileDirs(runner, options.worktreePath);
|
|
94
|
+
if (dirs.length === 0)
|
|
95
|
+
return [];
|
|
96
|
+
let npmVersion = null;
|
|
97
|
+
const results = [];
|
|
98
|
+
for (const dir of dirs) {
|
|
99
|
+
if (options.signal?.aborted)
|
|
100
|
+
break;
|
|
101
|
+
try {
|
|
102
|
+
const target = node_path_1.default.join(options.worktreePath, dir, "node_modules");
|
|
103
|
+
if (await exists(target)) {
|
|
104
|
+
results.push({ dir, status: "present" });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!(await isIgnored(runner, options.worktreePath, node_path_1.default.join(dir, "node_modules")))) {
|
|
108
|
+
results.push({ dir, status: "skipped", detail: "node_modules is not ignored by git in this checkout" });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const packageJson = await readInput(node_path_1.default.join(options.worktreePath, dir, "package.json"));
|
|
112
|
+
const lockfile = await readInput(node_path_1.default.join(options.worktreePath, dir, LOCKFILE));
|
|
113
|
+
const npmrc = await readInput(node_path_1.default.join(options.worktreePath, dir, ".npmrc"));
|
|
114
|
+
const unreadable = [packageJson, lockfile, npmrc].find((input) => input.state === "invalid");
|
|
115
|
+
if (unreadable?.state === "invalid") {
|
|
116
|
+
results.push({ dir, status: "skipped", detail: unreadable.detail });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (packageJson.state !== "file" || lockfile.state !== "file") {
|
|
120
|
+
results.push({ dir, status: "skipped", detail: "no package.json next to package-lock.json" });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (usesWorkspaces(packageJson.data)) {
|
|
124
|
+
results.push({ dir, status: "skipped", detail: "npm workspaces are not supported by the dependency cache" });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const refusal = lockfileRefusal(lockfile.data) ?? (npmrc.state === "file" ? npmrcRefusal(npmrc.data) : null);
|
|
128
|
+
if (refusal) {
|
|
129
|
+
results.push({ dir, status: "skipped", detail: refusal });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
npmVersion ??= await readNpmVersion(runner, npmBin, env);
|
|
133
|
+
if (!npmVersion) {
|
|
134
|
+
results.push({ dir, status: "failed", detail: "npm is not available to the runtime" });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const inputs = {
|
|
138
|
+
packageJson: packageJson.data,
|
|
139
|
+
lockfile: lockfile.data,
|
|
140
|
+
npmrc: npmrc.state === "file" ? npmrc.data : null,
|
|
141
|
+
};
|
|
142
|
+
const key = dependencyCacheKey({
|
|
143
|
+
repository: options.repository,
|
|
144
|
+
dir,
|
|
145
|
+
...inputs,
|
|
146
|
+
npmVersion,
|
|
147
|
+
});
|
|
148
|
+
const entry = node_path_1.default.join(options.cacheRoot, key);
|
|
149
|
+
const build = () => buildEntry({
|
|
150
|
+
...options,
|
|
151
|
+
runner,
|
|
152
|
+
npmBin,
|
|
153
|
+
env,
|
|
154
|
+
dir,
|
|
155
|
+
key,
|
|
156
|
+
entry,
|
|
157
|
+
files: {
|
|
158
|
+
"package.json": inputs.packageJson,
|
|
159
|
+
[LOCKFILE]: inputs.lockfile,
|
|
160
|
+
...(inputs.npmrc ? { ".npmrc": inputs.npmrc } : {}),
|
|
161
|
+
},
|
|
162
|
+
manifest: {
|
|
163
|
+
version: KEY_VERSION,
|
|
164
|
+
key,
|
|
165
|
+
repository: options.repository,
|
|
166
|
+
dir,
|
|
167
|
+
lockfile_sha256: sha256(inputs.lockfile),
|
|
168
|
+
node: process.version,
|
|
169
|
+
npm: npmVersion,
|
|
170
|
+
platform: process.platform,
|
|
171
|
+
arch: process.arch,
|
|
172
|
+
lifecycle_scripts: "ignored",
|
|
173
|
+
created_at: new Date().toISOString(),
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
let status = "hit";
|
|
177
|
+
let failure = null;
|
|
178
|
+
// A second pass covers an entry another session pruned between the
|
|
179
|
+
// publish check and the copy: the copy fails and the entry is rebuilt.
|
|
180
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
181
|
+
failure = null;
|
|
182
|
+
if (!(await isPublished(entry))) {
|
|
183
|
+
failure = await build();
|
|
184
|
+
if (failure)
|
|
185
|
+
break;
|
|
186
|
+
status = "built";
|
|
187
|
+
}
|
|
188
|
+
await touch(node_path_1.default.join(entry, MANIFEST_FILENAME));
|
|
189
|
+
try {
|
|
190
|
+
await materialize(node_path_1.default.join(entry, "node_modules"), target, options.stagingDir);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (attempt === 1 || (await isPublished(entry)))
|
|
195
|
+
throw error;
|
|
196
|
+
failure = "the cache entry was removed during the copy";
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (failure) {
|
|
200
|
+
results.push({ dir, status: "failed", key, detail: failure });
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
results.push({ dir, status, key });
|
|
204
|
+
await pruneEntries(options.cacheRoot, options.repository, dir, options.keepPerLockfile ?? DEFAULT_KEEP_PER_LOCKFILE)
|
|
205
|
+
.catch((error) => log.warn(`dependency cache prune failed: ${String(error)}`));
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
results.push({ dir, status: "failed", detail: `unexpected error: ${error.message ?? String(error)}` });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return results;
|
|
212
|
+
}
|
|
213
|
+
function dependencyCacheKey(input) {
|
|
214
|
+
return sha256(Buffer.from(JSON.stringify({
|
|
215
|
+
v: KEY_VERSION,
|
|
216
|
+
repository: input.repository.toLowerCase(),
|
|
217
|
+
dir: input.dir,
|
|
218
|
+
lockfile: sha256(input.lockfile),
|
|
219
|
+
package_json: sha256(input.packageJson),
|
|
220
|
+
npmrc: input.npmrc ? sha256(input.npmrc) : null,
|
|
221
|
+
node: input.node ?? process.version,
|
|
222
|
+
npm: input.npmVersion,
|
|
223
|
+
platform: input.platform ?? process.platform,
|
|
224
|
+
arch: input.arch ?? process.arch,
|
|
225
|
+
scripts: "ignored",
|
|
226
|
+
})));
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Prompt section that tells the agent what the runtime prepared. Returns null
|
|
230
|
+
* when the worktree has no tracked lockfile, so unrelated sessions see nothing.
|
|
231
|
+
*/
|
|
232
|
+
function renderDependencyCacheNote(results) {
|
|
233
|
+
if (results.length === 0)
|
|
234
|
+
return null;
|
|
235
|
+
const label = (dir) => (dir === "." ? "`node_modules`" : `\`${dir}/node_modules\``);
|
|
236
|
+
const ready = results.filter((r) => r.status === "hit" || r.status === "built");
|
|
237
|
+
const notReady = results.filter((r) => r.status === "skipped" || r.status === "failed");
|
|
238
|
+
const lines = ["## Prepared dependencies", ""];
|
|
239
|
+
if (ready.length > 0) {
|
|
240
|
+
lines.push(`Navarch installed locked dependencies before this session started: ${ready.map((r) => `${label(r.dir)} (cache ${r.status === "hit" ? "hit" : "built"}, key \`${r.key.slice(0, 12)}\`)`).join(", ")}.`, "The runtime ran `npm ci --ignore-scripts` outside the sandbox, keyed on the repository, `package-lock.json`, `package.json`, `.npmrc`, Node, npm, and platform, and copied the result into this checkout.", "Use these directories for local tests, type checks, and builds. Do not run `npm ci` or `npm install`: the sandbox has no registry access.", "Lifecycle scripts did not run. If a step fails because a package needs its install script (for example a native addon build), report that exact package and command as a capability gap.");
|
|
241
|
+
}
|
|
242
|
+
for (const r of results.filter((result) => result.status === "present")) {
|
|
243
|
+
lines.push(`${label(r.dir)} was already in the checkout, so the runtime left it unchanged.`);
|
|
244
|
+
}
|
|
245
|
+
for (const r of notReady) {
|
|
246
|
+
lines.push(`${label(r.dir)} was not prepared (${r.status}): ${r.detail ?? "no detail"}.`);
|
|
247
|
+
}
|
|
248
|
+
return lines.join("\n");
|
|
249
|
+
}
|
|
250
|
+
async function trackedLockfileDirs(runner, worktreePath) {
|
|
251
|
+
const listed = await runner.run("git", ["-C", worktreePath, "ls-files", "-z", "--", LOCKFILE, `*/${LOCKFILE}`], { timeoutMs: 30_000 });
|
|
252
|
+
if (listed.code !== 0)
|
|
253
|
+
return [];
|
|
254
|
+
const dirs = listed.stdout
|
|
255
|
+
.split("\0")
|
|
256
|
+
.filter(Boolean)
|
|
257
|
+
.map((file) => node_path_1.default.posix.dirname(file))
|
|
258
|
+
// Root and first-level packages only, never a vendored node_modules copy.
|
|
259
|
+
.filter((dir) => dir === "." || (!dir.includes("/") && dir !== "node_modules"));
|
|
260
|
+
return [...new Set(dirs)].sort((a, b) => (a === "." ? -1 : b === "." ? 1 : a.localeCompare(b))).slice(0, MAX_LOCKFILES);
|
|
261
|
+
}
|
|
262
|
+
async function isIgnored(runner, worktreePath, relativePath) {
|
|
263
|
+
const result = await runner.run("git", ["-C", worktreePath, "check-ignore", "-q", "--", `${relativePath.split(node_path_1.default.sep).join("/")}/`], { timeoutMs: 30_000 });
|
|
264
|
+
return result.code === 0;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Why a lockfile cannot be installed outside the checkout, or null.
|
|
268
|
+
*
|
|
269
|
+
* npm resolves `file:` and link entries relative to the staging directory and
|
|
270
|
+
* reads any host path they name, so a crafted lockfile could copy host files
|
|
271
|
+
* (other sessions, other repositories) into this checkout. Only registry
|
|
272
|
+
* tarballs fetched over HTTPS and pinned by `integrity` are accepted; bundled
|
|
273
|
+
* dependencies ship inside such a tarball.
|
|
274
|
+
*/
|
|
275
|
+
function lockfileRefusal(lockfile) {
|
|
276
|
+
let parsed;
|
|
277
|
+
try {
|
|
278
|
+
parsed = JSON.parse(lockfile.toString("utf8"));
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return "package-lock.json is not valid JSON";
|
|
282
|
+
}
|
|
283
|
+
if (typeof parsed.lockfileVersion !== "number" || parsed.lockfileVersion < 2
|
|
284
|
+
|| typeof parsed.packages !== "object" || parsed.packages === null) {
|
|
285
|
+
return "package-lock.json must be lockfileVersion 2 or later";
|
|
286
|
+
}
|
|
287
|
+
for (const [location, value] of Object.entries(parsed.packages)) {
|
|
288
|
+
if (location === "")
|
|
289
|
+
continue;
|
|
290
|
+
const entry = (typeof value === "object" && value !== null ? value : {});
|
|
291
|
+
const segments = location.split("/");
|
|
292
|
+
if (segments[0] !== "node_modules" || segments.includes("..") || segments.includes(".")) {
|
|
293
|
+
return `package-lock.json entry "${location.slice(0, 120)}" is outside node_modules`;
|
|
294
|
+
}
|
|
295
|
+
if (entry.link === true)
|
|
296
|
+
return `package-lock.json entry "${location.slice(0, 120)}" is a link`;
|
|
297
|
+
if (entry.inBundle === true)
|
|
298
|
+
continue;
|
|
299
|
+
if (typeof entry.resolved !== "string" || !entry.resolved.startsWith("https://")) {
|
|
300
|
+
return `package-lock.json entry "${location.slice(0, 120)}" does not resolve to an HTTPS registry tarball`;
|
|
301
|
+
}
|
|
302
|
+
if (typeof entry.integrity !== "string" || entry.integrity === "") {
|
|
303
|
+
return `package-lock.json entry "${location.slice(0, 120)}" has no integrity hash`;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
/** Project `.npmrc` keys that only choose a registry or resolution rules. */
|
|
309
|
+
const NPMRC_ALLOWED_KEYS = new Set([
|
|
310
|
+
"registry",
|
|
311
|
+
"legacy-peer-deps",
|
|
312
|
+
"strict-peer-deps",
|
|
313
|
+
"engine-strict",
|
|
314
|
+
"save-exact",
|
|
315
|
+
"save-prefix",
|
|
316
|
+
"fund",
|
|
317
|
+
"audit",
|
|
318
|
+
"update-notifier",
|
|
319
|
+
]);
|
|
320
|
+
/**
|
|
321
|
+
* Why a project `.npmrc` cannot be used by the unsandboxed npm, or null.
|
|
322
|
+
* Keys such as `strict-ssl`, `https-proxy`, `cafile`, `git`, `install-links`,
|
|
323
|
+
* or `logs-dir` would let repository content change how npm uses the host's
|
|
324
|
+
* network and credentials, so any key outside the allowlist skips the cache.
|
|
325
|
+
*/
|
|
326
|
+
function npmrcRefusal(npmrc) {
|
|
327
|
+
for (const raw of npmrc.toString("utf8").split(/\r?\n/)) {
|
|
328
|
+
const line = raw.trim();
|
|
329
|
+
if (line === "" || line.startsWith("#") || line.startsWith(";"))
|
|
330
|
+
continue;
|
|
331
|
+
const key = line.split("=", 1)[0].trim();
|
|
332
|
+
if (NPMRC_ALLOWED_KEYS.has(key) || /^@[^/:\s]+:registry$/.test(key))
|
|
333
|
+
continue;
|
|
334
|
+
return `.npmrc sets "${key}", which the dependency cache does not allow`;
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
function usesWorkspaces(packageJson) {
|
|
339
|
+
try {
|
|
340
|
+
const parsed = JSON.parse(packageJson.toString("utf8"));
|
|
341
|
+
return parsed.workspaces !== undefined;
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
// npm ci reports an invalid package.json itself.
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async function readNpmVersion(runner, npmBin, env) {
|
|
349
|
+
try {
|
|
350
|
+
const result = await runner.run(npmBin, ["--version"], { timeoutMs: 30_000, env });
|
|
351
|
+
const version = result.stdout.trim();
|
|
352
|
+
return result.code === 0 && version ? version : null;
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/** Builds and publishes one entry. Returns a failure detail, or null on success. */
|
|
359
|
+
async function buildEntry(input) {
|
|
360
|
+
await node_fs_1.promises.mkdir(input.cacheRoot, { recursive: true, mode: 0o700 });
|
|
361
|
+
const staging = node_path_1.default.join(input.cacheRoot, `.tmp-${input.key.slice(0, 16)}-${(0, node_crypto_1.randomBytes)(4).toString("hex")}`);
|
|
362
|
+
await node_fs_1.promises.mkdir(staging, { mode: 0o700 });
|
|
363
|
+
try {
|
|
364
|
+
for (const [name, contents] of Object.entries(input.files)) {
|
|
365
|
+
await node_fs_1.promises.writeFile(node_path_1.default.join(staging, name), contents);
|
|
366
|
+
}
|
|
367
|
+
const started = Date.now();
|
|
368
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
369
|
+
// npm's own tarball store is per repository too, so a private package
|
|
370
|
+
// fetched for one repository is never served to another.
|
|
371
|
+
const repositoryTag = sha256(Buffer.from(input.manifest.repository.toLowerCase())).slice(0, 16);
|
|
372
|
+
const result = await input.runner.run(input.npmBin, [
|
|
373
|
+
"ci",
|
|
374
|
+
"--ignore-scripts",
|
|
375
|
+
"--no-audit",
|
|
376
|
+
"--no-fund",
|
|
377
|
+
"--strict-ssl=true",
|
|
378
|
+
"--install-links=false",
|
|
379
|
+
"--cache",
|
|
380
|
+
node_path_1.default.join(input.cacheRoot, `.npm-${repositoryTag}`),
|
|
381
|
+
"--logs-dir",
|
|
382
|
+
node_path_1.default.join(input.cacheRoot, ".logs"),
|
|
383
|
+
], { cwd: staging, env: input.env, timeoutMs, signal: input.signal });
|
|
384
|
+
if (result.code !== 0) {
|
|
385
|
+
log.warn(`npm ci for ${input.dir} (key ${input.key.slice(0, 12)}) exited ${result.code}: ${tail(result.stderr || result.stdout)}`);
|
|
386
|
+
if (input.signal?.aborted)
|
|
387
|
+
return "the session stopped before npm ci finished";
|
|
388
|
+
if (result.code === -1) {
|
|
389
|
+
return Date.now() - started >= timeoutMs
|
|
390
|
+
? "npm ci did not finish before the dependency-cache timeout"
|
|
391
|
+
: "npm ci was stopped by a signal";
|
|
392
|
+
}
|
|
393
|
+
return `npm ci exited with code ${result.code}`;
|
|
394
|
+
}
|
|
395
|
+
if (!(await exists(node_path_1.default.join(staging, "node_modules")))) {
|
|
396
|
+
await node_fs_1.promises.mkdir(node_path_1.default.join(staging, "node_modules"));
|
|
397
|
+
}
|
|
398
|
+
for (const name of Object.keys(input.files))
|
|
399
|
+
await node_fs_1.promises.rm(node_path_1.default.join(staging, name), { force: true });
|
|
400
|
+
await node_fs_1.promises.writeFile(node_path_1.default.join(staging, MANIFEST_FILENAME), `${JSON.stringify(input.manifest, null, 2)}\n`);
|
|
401
|
+
try {
|
|
402
|
+
await node_fs_1.promises.rename(staging, input.entry);
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
// Another session published the same key first; its tree is equivalent.
|
|
406
|
+
if (!(await isPublished(input.entry)))
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
log.info(`built dependency cache ${input.key.slice(0, 12)} for ${input.dir} in ${Math.round((Date.now() - started) / 1000)}s`);
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
finally {
|
|
413
|
+
await node_fs_1.promises.rm(staging, { recursive: true, force: true }).catch(() => undefined);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/** Copies the cached tree in under a temporary name, then renames it into place. */
|
|
417
|
+
async function materialize(source, target, stagingDir) {
|
|
418
|
+
const staging = stagingDir
|
|
419
|
+
? node_path_1.default.join(stagingDir, `node_modules.navarch-${(0, node_crypto_1.randomBytes)(4).toString("hex")}`)
|
|
420
|
+
: `${target}.navarch-${(0, node_crypto_1.randomBytes)(4).toString("hex")}`;
|
|
421
|
+
try {
|
|
422
|
+
await copyTree(source, staging);
|
|
423
|
+
await node_fs_1.promises.rename(staging, target);
|
|
424
|
+
}
|
|
425
|
+
finally {
|
|
426
|
+
await node_fs_1.promises.rm(staging, { recursive: true, force: true }).catch(() => undefined);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/** Files copied at once. A node_modules tree is tens of thousands of small files. */
|
|
430
|
+
const COPY_CONCURRENCY = 32;
|
|
431
|
+
/**
|
|
432
|
+
* Recursive copy with file copies in parallel, copy-on-write where the
|
|
433
|
+
* filesystem supports it. `fs.cp` copies one file at a time, which made a
|
|
434
|
+
* 1 GB tree take about 90 s instead of about 8 s. Symlinks are recreated with
|
|
435
|
+
* their original (relative) targets; `copyFile` keeps each file's mode.
|
|
436
|
+
*/
|
|
437
|
+
async function copyTree(source, target) {
|
|
438
|
+
const files = [];
|
|
439
|
+
const links = [];
|
|
440
|
+
const walk = async (relative) => {
|
|
441
|
+
await node_fs_1.promises.mkdir(node_path_1.default.join(target, relative), { recursive: true });
|
|
442
|
+
const subdirs = [];
|
|
443
|
+
for (const dirent of await node_fs_1.promises.readdir(node_path_1.default.join(source, relative), { withFileTypes: true })) {
|
|
444
|
+
const child = node_path_1.default.join(relative, dirent.name);
|
|
445
|
+
if (dirent.isDirectory())
|
|
446
|
+
subdirs.push(child);
|
|
447
|
+
else if (dirent.isSymbolicLink())
|
|
448
|
+
links.push(child);
|
|
449
|
+
else if (dirent.isFile())
|
|
450
|
+
files.push(child);
|
|
451
|
+
}
|
|
452
|
+
for (const subdir of subdirs)
|
|
453
|
+
await walk(subdir);
|
|
454
|
+
};
|
|
455
|
+
await walk("");
|
|
456
|
+
let next = 0;
|
|
457
|
+
const worker = async () => {
|
|
458
|
+
for (let relative = files[next++]; relative !== undefined; relative = files[next++]) {
|
|
459
|
+
await node_fs_1.promises.copyFile(node_path_1.default.join(source, relative), node_path_1.default.join(target, relative), node_fs_1.constants.COPYFILE_FICLONE);
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
await Promise.all(Array.from({ length: COPY_CONCURRENCY }, worker));
|
|
463
|
+
for (const relative of links) {
|
|
464
|
+
await node_fs_1.promises.symlink(await node_fs_1.promises.readlink(node_path_1.default.join(source, relative)), node_path_1.default.join(target, relative));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async function pruneEntries(cacheRoot, repository, dir, keep) {
|
|
468
|
+
const now = Date.now();
|
|
469
|
+
const entries = await node_fs_1.promises.readdir(cacheRoot, { withFileTypes: true });
|
|
470
|
+
const matching = [];
|
|
471
|
+
const expired = [];
|
|
472
|
+
for (const dirent of entries) {
|
|
473
|
+
if (dirent.isDirectory() && dirent.name.startsWith(".tmp-")) {
|
|
474
|
+
// Staging left behind by a crashed runtime; a live build is far younger.
|
|
475
|
+
const staging = node_path_1.default.join(cacheRoot, dirent.name);
|
|
476
|
+
const stat = await node_fs_1.promises.stat(staging).catch(() => null);
|
|
477
|
+
if (stat && now - stat.mtimeMs > STALE_STAGING_MS) {
|
|
478
|
+
await node_fs_1.promises.rm(staging, { recursive: true, force: true }).catch(() => undefined);
|
|
479
|
+
}
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (!dirent.isDirectory() || dirent.name.startsWith("."))
|
|
483
|
+
continue;
|
|
484
|
+
const entry = node_path_1.default.join(cacheRoot, dirent.name);
|
|
485
|
+
const manifestPath = node_path_1.default.join(entry, MANIFEST_FILENAME);
|
|
486
|
+
try {
|
|
487
|
+
const manifest = JSON.parse(await node_fs_1.promises.readFile(manifestPath, "utf8"));
|
|
488
|
+
const usedAt = (await node_fs_1.promises.stat(manifestPath)).mtimeMs;
|
|
489
|
+
// An entry another session touched recently may be mid-copy.
|
|
490
|
+
if (now - usedAt < IN_USE_GRACE_MS)
|
|
491
|
+
continue;
|
|
492
|
+
if (now - usedAt > UNUSED_ENTRY_MS)
|
|
493
|
+
expired.push(entry);
|
|
494
|
+
else if (manifest.repository?.toLowerCase() === repository.toLowerCase() && manifest.dir === dir) {
|
|
495
|
+
matching.push({ entry, usedAt });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// `keep` counts the entry this session just used, which the grace period skipped.
|
|
503
|
+
matching.sort((a, b) => b.usedAt - a.usedAt);
|
|
504
|
+
const stale = [...matching.slice(Math.max(keep - 1, 0)).map((m) => m.entry), ...expired];
|
|
505
|
+
for (const entry of stale) {
|
|
506
|
+
// Rename first so a concurrent reader never sees a half-deleted entry as published.
|
|
507
|
+
const doomed = node_path_1.default.join(cacheRoot, `.tmp-prune-${(0, node_crypto_1.randomBytes)(4).toString("hex")}`);
|
|
508
|
+
try {
|
|
509
|
+
await node_fs_1.promises.rename(entry, doomed);
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
if (error.code === "ENOENT")
|
|
513
|
+
continue; // Another prune won.
|
|
514
|
+
throw error;
|
|
515
|
+
}
|
|
516
|
+
await node_fs_1.promises.rm(doomed, { recursive: true, force: true });
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
async function isPublished(entry) {
|
|
520
|
+
return (await exists(node_path_1.default.join(entry, MANIFEST_FILENAME))) && (await exists(node_path_1.default.join(entry, "node_modules")));
|
|
521
|
+
}
|
|
522
|
+
async function exists(target) {
|
|
523
|
+
try {
|
|
524
|
+
await node_fs_1.promises.lstat(target);
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Reads an install input without following a symlink, so a planted link
|
|
533
|
+
* cannot stage a host file, block on a FIFO, or stream /dev/zero.
|
|
534
|
+
*/
|
|
535
|
+
async function readInput(file) {
|
|
536
|
+
let stat;
|
|
537
|
+
try {
|
|
538
|
+
stat = await node_fs_1.promises.lstat(file);
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
return { state: "absent" };
|
|
542
|
+
}
|
|
543
|
+
const name = node_path_1.default.basename(file);
|
|
544
|
+
if (!stat.isFile())
|
|
545
|
+
return { state: "invalid", detail: `${name} is not a regular file` };
|
|
546
|
+
if (stat.size > MAX_INPUT_BYTES)
|
|
547
|
+
return { state: "invalid", detail: `${name} is larger than 64 MB` };
|
|
548
|
+
const handle = await node_fs_1.promises.open(file, node_fs_1.constants.O_RDONLY | (node_fs_1.constants.O_NOFOLLOW ?? 0));
|
|
549
|
+
try {
|
|
550
|
+
return { state: "file", data: await handle.readFile() };
|
|
551
|
+
}
|
|
552
|
+
finally {
|
|
553
|
+
await handle.close();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
async function touch(file) {
|
|
557
|
+
const now = new Date();
|
|
558
|
+
await node_fs_1.promises.utimes(file, now, now).catch(() => undefined);
|
|
559
|
+
}
|
|
560
|
+
function sha256(data) {
|
|
561
|
+
return (0, node_crypto_1.createHash)("sha256").update(data).digest("hex");
|
|
562
|
+
}
|
|
563
|
+
function tail(text) {
|
|
564
|
+
const trimmed = text.trim();
|
|
565
|
+
return trimmed.length > 500 ? `…${trimmed.slice(-500)}` : trimmed;
|
|
566
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GITHUB_CREDENTIAL_SECRET_NAME = void 0;
|
|
4
|
+
exports.githubCredentialDenialSummary = githubCredentialDenialSummary;
|
|
5
|
+
/** Wire name the session requests its repository credential under. */
|
|
6
|
+
exports.GITHUB_CREDENTIAL_SECRET_NAME = "github-pat";
|
|
7
|
+
/**
|
|
8
|
+
* Denials that mean no repository credential exists for this lease, rather
|
|
9
|
+
* than one being refused: the project has neither a GitHub App installation
|
|
10
|
+
* nor a `github-pat` secret (`not_found`), or a standalone PR review on a
|
|
11
|
+
* PAT-connected project, which deliberately gets none
|
|
12
|
+
* (`standalone_review_read_only`). The machine's own git login has always
|
|
13
|
+
* been the clone credential then, so those sessions keep running.
|
|
14
|
+
*/
|
|
15
|
+
const NO_CREDENTIAL_REASONS = new Set(["not_found", "standalone_review_read_only"]);
|
|
16
|
+
const MEMBER_FIX = "or have a project owner move this agent to the project GitHub identity, then resume the agent on the Agents page.";
|
|
17
|
+
const REASON_TEXT = {
|
|
18
|
+
member_github_missing: `this agent acts as a project member whose GitHub account is not connected. Connect GitHub on that member's Account page, ${MEMBER_FIX}`,
|
|
19
|
+
member_github_revoked: `this agent acts as a project member whose GitHub authorization was revoked. Reconnect GitHub on that member's Account page, ${MEMBER_FIX}`,
|
|
20
|
+
member_github_unavailable: `this agent acts as a project member whose GitHub credential could not be read. Reconnect GitHub on that member's Account page, ${MEMBER_FIX}`,
|
|
21
|
+
member_not_in_project: `this agent acts as a member who does not belong to this project. Add them to the project, ${MEMBER_FIX}`,
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The failure summary for a lease whose repository credential the broker
|
|
25
|
+
* refused, or null when the session may proceed.
|
|
26
|
+
*
|
|
27
|
+
* Without this check a refused credential left the clone to whatever git
|
|
28
|
+
* credential helper the host had configured — on pebble-agent-1721
|
|
29
|
+
* (2026-10-21) that was `gh auth git-credential` for an account that cannot
|
|
30
|
+
* see the repository, so every claim failed with an unexplained "Repository
|
|
31
|
+
* not found". The summary leads with `GitHub credential denied (<reason>):`,
|
|
32
|
+
* which the control plane's failure attribution anchors on to pause the
|
|
33
|
+
* machine instead of spending the task's retries
|
|
34
|
+
* (lib/navarch/failure-attribution.ts MEMBER_GITHUB_RULE).
|
|
35
|
+
*/
|
|
36
|
+
function githubCredentialDenialSummary(denied) {
|
|
37
|
+
const denial = denied?.find((entry) => entry.name === exports.GITHUB_CREDENTIAL_SECRET_NAME);
|
|
38
|
+
if (!denial || NO_CREDENTIAL_REASONS.has(denial.reason))
|
|
39
|
+
return null;
|
|
40
|
+
const explanation = REASON_TEXT[denial.reason] ?? "the control plane refused the repository credential for this lease.";
|
|
41
|
+
return (`GitHub credential denied (${denial.reason}): ${explanation} ` +
|
|
42
|
+
"The session stopped before cloning instead of falling back to this machine's own git login.");
|
|
43
|
+
}
|
package/dist/machine-store.cjs
CHANGED
|
@@ -8,7 +8,7 @@ exports.loadMachineIdentity = loadMachineIdentity;
|
|
|
8
8
|
exports.resolveMachineIdentity = resolveMachineIdentity;
|
|
9
9
|
const node_fs_1 = require("node:fs");
|
|
10
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
-
const
|
|
11
|
+
const agent_types_cjs_1 = require("./agent-types.cjs");
|
|
12
12
|
function storePath(configDir) {
|
|
13
13
|
return node_path_1.default.join(configDir, "machine.json");
|
|
14
14
|
}
|
|
@@ -52,7 +52,7 @@ async function resolveMachineIdentity(configDir, apiBaseFallback) {
|
|
|
52
52
|
// Validate against the one accepted adapter list so a new adapter cannot
|
|
53
53
|
// be added to `--agent`/NAVARCH_AGENT while this env-based identity path
|
|
54
54
|
// silently drops it.
|
|
55
|
-
agent_type: (0,
|
|
55
|
+
agent_type: (0, agent_types_cjs_1.isRuntimeAgentType)(process.env.NAVARCH_AGENT)
|
|
56
56
|
? process.env.NAVARCH_AGENT
|
|
57
57
|
: undefined,
|
|
58
58
|
};
|
package/dist/session.cjs
CHANGED
|
@@ -22,6 +22,7 @@ const mcp_config_cjs_1 = require("./mcp-config.cjs");
|
|
|
22
22
|
const user_mcp_servers_cjs_1 = require("./user-mcp-servers.cjs");
|
|
23
23
|
const logger_cjs_1 = require("./logger.cjs");
|
|
24
24
|
const git_worktree_cjs_1 = require("./git-worktree.cjs");
|
|
25
|
+
const dependency_cache_cjs_1 = require("./dependency-cache.cjs");
|
|
25
26
|
const worktree_janitor_cjs_1 = require("./worktree-janitor.cjs");
|
|
26
27
|
const session_processes_cjs_1 = require("./session-processes.cjs");
|
|
27
28
|
const github_pr_cjs_1 = require("./github-pr.cjs");
|
|
@@ -33,6 +34,7 @@ const completion_retry_cjs_1 = require("./completion-retry.cjs");
|
|
|
33
34
|
const adapter_capacity_cjs_1 = require("./adapter-capacity.cjs");
|
|
34
35
|
const agent_auth_cjs_1 = require("./agent-auth.cjs");
|
|
35
36
|
const agent_model_cjs_1 = require("./agent-model.cjs");
|
|
37
|
+
const github_credential_cjs_1 = require("./github-credential.cjs");
|
|
36
38
|
const browser_evidence_cjs_1 = require("./browser-evidence.cjs");
|
|
37
39
|
/** Filename the generated platform MCP config is written under inside the session metadata directory. */
|
|
38
40
|
const MCP_CONFIG_FILENAME = "mcp-config.json";
|
|
@@ -163,6 +165,24 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
163
165
|
secrets = issued.secrets;
|
|
164
166
|
managedGithubCredential = issued.github_app_issued === true;
|
|
165
167
|
registry.registerAll(secrets);
|
|
168
|
+
const credentialDenial = (0, github_credential_cjs_1.githubCredentialDenialSummary)(issued.denied);
|
|
169
|
+
if (credentialDenial) {
|
|
170
|
+
log.error(`session ${leaseId}: ${credentialDenial}`);
|
|
171
|
+
await api.completeLease(leaseId, {
|
|
172
|
+
status: "failed",
|
|
173
|
+
report: credentialDenial,
|
|
174
|
+
failure_summary: credentialDenial,
|
|
175
|
+
failed_before_start: true,
|
|
176
|
+
evidence_urls: [],
|
|
177
|
+
cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
|
|
178
|
+
exit_status: "crashed",
|
|
179
|
+
agent_type: runtime,
|
|
180
|
+
...executionReport,
|
|
181
|
+
});
|
|
182
|
+
secrets = {};
|
|
183
|
+
await node_fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
166
186
|
}
|
|
167
187
|
let gitCredentialRefresh;
|
|
168
188
|
if (managedGithubCredential && secrets["github-pat"]) {
|
|
@@ -523,6 +543,43 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
523
543
|
promptText = `${promptText}\n\n${(0, prompt_cjs_1.renderHostedCheckpointNote)(restoredCheckpoint, gitWorktree.branch)}`;
|
|
524
544
|
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), promptText, "utf8");
|
|
525
545
|
}
|
|
546
|
+
// The sandbox cannot reach a package registry, so the unsandboxed session
|
|
547
|
+
// manager installs locked dependencies for the selected task types
|
|
548
|
+
// (dependency-cache.cts). Host mode only: a host-built tree does not match
|
|
549
|
+
// a Docker image's platform. The worktree guard is what keeps the agent
|
|
550
|
+
// out of the shared cache, so an unguarded session gets no cache.
|
|
551
|
+
if (config.sandboxMode === "host" && config.worktreeGuard
|
|
552
|
+
&& config.dependencyCacheTaskTypes.includes(task.task_type)) {
|
|
553
|
+
const dependencyAbort = new AbortController();
|
|
554
|
+
activeAbortController = dependencyAbort;
|
|
555
|
+
if (leaseLost || deps.shutdownSignal?.aborted)
|
|
556
|
+
dependencyAbort.abort();
|
|
557
|
+
const dependencyResults = await (0, dependency_cache_cjs_1.prepareDependencyCache)({
|
|
558
|
+
cacheRoot: node_path_1.default.join(config.workspaceRoot, dependency_cache_cjs_1.DEPENDENCY_CACHE_DIRNAME),
|
|
559
|
+
worktreePath: gitWorktree.worktreePath,
|
|
560
|
+
stagingDir: workDir,
|
|
561
|
+
repository: task.repo,
|
|
562
|
+
timeoutMs: config.dependencyCacheTimeoutMs,
|
|
563
|
+
signal: dependencyAbort.signal,
|
|
564
|
+
}).catch((error) => {
|
|
565
|
+
log.warn(`dependency cache for ${leaseId} failed: ${(0, redact_cjs_1.redactText)(String(error), registry.list())}`);
|
|
566
|
+
return [];
|
|
567
|
+
});
|
|
568
|
+
activeAbortController = null;
|
|
569
|
+
for (const result of dependencyResults) {
|
|
570
|
+
const message = `dependency cache for ${leaseId} ${result.dir}: ${result.status}` +
|
|
571
|
+
`${result.key ? ` (key ${result.key.slice(0, 12)})` : ""}${result.detail ? ` — ${result.detail}` : ""}`;
|
|
572
|
+
if (result.status === "failed")
|
|
573
|
+
log.warn(message);
|
|
574
|
+
else
|
|
575
|
+
log.info(message);
|
|
576
|
+
}
|
|
577
|
+
const dependencyNote = (0, dependency_cache_cjs_1.renderDependencyCacheNote)(dependencyResults);
|
|
578
|
+
if (dependencyNote) {
|
|
579
|
+
promptText = `${promptText}\n\n${dependencyNote}`;
|
|
580
|
+
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), promptText, "utf8");
|
|
581
|
+
}
|
|
582
|
+
}
|
|
526
583
|
// Preparation may resume an existing task branch with an older slug or ID format.
|
|
527
584
|
if (!allowBranchRename) {
|
|
528
585
|
sessionEnv.NAVARCH_DELIVERY_BRANCH = gitWorktree.branch;
|
package/package.json
CHANGED