@sagentlab/navarch-runtime 0.1.120 → 0.1.122
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 +27 -5
- package/dist/adapters/claude.cjs +77 -6
- package/dist/adapters/codex.cjs +66 -2
- package/dist/adapters/opencode-providers.cjs +110 -0
- package/dist/adapters/opencode.cjs +9 -2
- package/dist/delivery.cjs +16 -2
- package/dist/prompt.cjs +44 -7
- package/dist/session.cjs +131 -53
- package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
- package/node_modules/jsonc-parser/LICENSE.md +21 -0
- package/node_modules/jsonc-parser/README.md +364 -0
- package/node_modules/jsonc-parser/SECURITY.md +41 -0
- package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
- package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
- package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
- package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
- package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
- package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
- package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
- package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
- package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
- package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
- package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
- package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
- package/node_modules/jsonc-parser/package.json +37 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -278,16 +278,23 @@ instance during an automatic-update handoff.
|
|
|
278
278
|
Guidance added to a task that is already running is delivered on that
|
|
279
279
|
task's next lease heartbeat (every
|
|
280
280
|
`NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS`, five minutes by default). The runtime
|
|
281
|
-
stops the current agent process and starts a **new agent turn
|
|
282
|
-
|
|
283
|
-
restart, not a new dispatch:
|
|
281
|
+
stops the current agent process and starts a **new agent turn**. This is a
|
|
282
|
+
turn restart, not a new dispatch:
|
|
284
283
|
|
|
285
284
|
- The task keeps the same session and lease, and lease heartbeats continue.
|
|
286
285
|
- The replacement turn uses the same worktree (and the same sandbox container
|
|
287
286
|
in Docker mode), so committed and uncommitted changes from the interrupted
|
|
288
287
|
turn remain available. It should inspect those changes before continuing.
|
|
289
|
-
-
|
|
290
|
-
|
|
288
|
+
- Claude Code and Codex resume the interrupted turn's CLI conversation
|
|
289
|
+
(`claude -p --resume <session>`, `codex exec resume <thread> -`) and are
|
|
290
|
+
sent only the task's one-line summary and the guidance that conversation has
|
|
291
|
+
not seen. Delivery-correction and completion-remediation turns resume the
|
|
292
|
+
same way. Every other agent, a turn that left no session id, and a resume
|
|
293
|
+
the CLI refuses before starting get a fresh invocation with the full task
|
|
294
|
+
context plus every guidance entry received since the claim. Guidance present
|
|
295
|
+
at claim time is already in the task context and is not repeated.
|
|
296
|
+
`prompt.md` in the session directory always holds what the last invocation
|
|
297
|
+
was actually sent.
|
|
291
298
|
- Transcript, token, and cost accounting is cumulative across turns: the
|
|
292
299
|
upload includes every turn under a separate label, and reported token and
|
|
293
300
|
cost totals include every turn. The lease is completed only after the
|
|
@@ -815,6 +822,21 @@ to OpenCode's remote-server config with OAuth disabled and environment-backed
|
|
|
815
822
|
headers. Missing accounting fields stay absent rather than becoming a
|
|
816
823
|
fabricated zero.
|
|
817
824
|
|
|
825
|
+
Host OpenCode sessions preserve operator provider definitions from
|
|
826
|
+
`$XDG_CONFIG_HOME/opencode/opencode.json(c)` (default
|
|
827
|
+
`~/.config/opencode/opencode.json(c)`), then `OPENCODE_CONFIG`, configs in
|
|
828
|
+
`OPENCODE_CONFIG_DIR`, and finally `OPENCODE_CONFIG_CONTENT`. JSONC comments
|
|
829
|
+
and trailing commas are supported. Nested provider settings merge in that
|
|
830
|
+
order; later arrays replace earlier arrays. Legacy `provider` entries work
|
|
831
|
+
with V1 and V2; native `providers` entries are included for V2. Custom model
|
|
832
|
+
IDs, metadata, and variants are preserved, so a pin such as
|
|
833
|
+
`--model magpie/openrouter/deepseek/deepseek-v4.1-flash-1017` works without
|
|
834
|
+
duplicating the provider configuration. API keys and header values become
|
|
835
|
+
child environment references, and file references resolve relative to their
|
|
836
|
+
original config file. Provider settings are the only imported configuration:
|
|
837
|
+
Navarch still generates the session's MCP and permissions and disables
|
|
838
|
+
project config discovery. Docker sessions do not import host providers.
|
|
839
|
+
|
|
818
840
|
OpenCode also runs with `--print-logs --log-level ERROR` by default (see its
|
|
819
841
|
[CLI flags](https://opencode.ai/docs/cli/#global-flags)), so provider errors
|
|
820
842
|
that are missing from assistant messages still reach the runtime. A rate or
|
package/dist/adapters/claude.cjs
CHANGED
|
@@ -12,6 +12,12 @@ const FALLBACK_CLAUDE_MODEL = "claude-opus-5-5";
|
|
|
12
12
|
function hasFlag(args, flag) {
|
|
13
13
|
return args.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
|
|
14
14
|
}
|
|
15
|
+
const CLAUDE_SESSION_ID = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
|
|
16
|
+
/** Operator flags that pick the conversation themselves; Navarch then never resumes one. */
|
|
17
|
+
const OPERATOR_SESSION_FLAGS = ["--resume", "-r", "--continue", "-c", "--session-id", "--fork-session"];
|
|
18
|
+
function operatorOwnsSession(extraArgs) {
|
|
19
|
+
return OPERATOR_SESSION_FLAGS.some((flag) => hasFlag(extraArgs, flag));
|
|
20
|
+
}
|
|
15
21
|
/**
|
|
16
22
|
* Headless Claude Code adapter (project-plan.md §3.9 / implementation-plan.md
|
|
17
23
|
* WP-07): `claude -p --mcp-config platform-mcp.json --output-format stream-json --verbose`
|
|
@@ -53,13 +59,13 @@ async function runClaudeCodeAdapter(options) {
|
|
|
53
59
|
if (!parsed?.is_error || !(0, adapter_capacity_cjs_1.detectAdapterCapacityLimit)(primary))
|
|
54
60
|
return primary;
|
|
55
61
|
const canResume = typeof parsed.session_id === "string" &&
|
|
56
|
-
|
|
62
|
+
CLAUDE_SESSION_ID.test(parsed.session_id) &&
|
|
57
63
|
!hasFlag(options.extraArgs, "--no-session-persistence");
|
|
58
64
|
// Never replay a task that may already have side effects without its saved
|
|
59
65
|
// conversation. A structured zero-turn rejection is safe to start afresh.
|
|
60
66
|
if (!canResume && parsed.num_turns !== 0)
|
|
61
67
|
return primary;
|
|
62
|
-
if (
|
|
68
|
+
if (operatorOwnsSession(options.extraArgs))
|
|
63
69
|
return primary;
|
|
64
70
|
const fallback = await runClaudeAttempt({
|
|
65
71
|
...options,
|
|
@@ -68,9 +74,9 @@ async function runClaudeCodeAdapter(options) {
|
|
|
68
74
|
prompt: canResume
|
|
69
75
|
? "Continue the interrupted task from the saved conversation. Check existing work before taking further action."
|
|
70
76
|
: options.prompt,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
// A zero-turn rejection without a session id reruns this same turn, which
|
|
78
|
+
// may itself be a resumed correction turn.
|
|
79
|
+
resumeSessionId: canResume ? parsed.session_id : options.resumeSessionId,
|
|
74
80
|
}, false);
|
|
75
81
|
return {
|
|
76
82
|
...fallback,
|
|
@@ -93,6 +99,19 @@ function addUsage(first, second) {
|
|
|
93
99
|
return first === undefined && second === undefined ? undefined : (first ?? 0) + (second ?? 0);
|
|
94
100
|
}
|
|
95
101
|
async function runClaudeAttempt(options, automaticFallback) {
|
|
102
|
+
if (options.resumeSessionId &&
|
|
103
|
+
(!CLAUDE_SESSION_ID.test(options.resumeSessionId) || operatorOwnsSession(options.extraArgs))) {
|
|
104
|
+
// Never hand an unvalidated id to argv, and never stack a second
|
|
105
|
+
// conversation selector on an operator's own. Nothing ran.
|
|
106
|
+
return {
|
|
107
|
+
exitCode: null,
|
|
108
|
+
timedOut: false,
|
|
109
|
+
killedByLeaseLoss: false,
|
|
110
|
+
stdout: "",
|
|
111
|
+
stderr: "Navarch did not resume the Claude session: the session id is invalid or the operator selects the conversation.",
|
|
112
|
+
resumeRejected: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
96
115
|
const args = ["-p"];
|
|
97
116
|
const hasSettingSources = options.extraArgs.some((arg) => arg === "--setting-sources" || arg.startsWith("--setting-sources="));
|
|
98
117
|
const hasExplicitPermissionMode = options.extraArgs.some((arg) => ["--permission-mode", "--permission-prompt-tool", "--dangerously-skip-permissions"].some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
|
|
@@ -145,6 +164,8 @@ async function runClaudeAttempt(options, automaticFallback) {
|
|
|
145
164
|
args.push("--verbose");
|
|
146
165
|
}
|
|
147
166
|
args.push(...options.extraArgs);
|
|
167
|
+
if (options.resumeSessionId)
|
|
168
|
+
args.push("--resume", options.resumeSessionId);
|
|
148
169
|
// Project/task policy is appended after machine-wide extra arguments so a
|
|
149
170
|
// dispatched session consistently uses the settings recorded by Navarch.
|
|
150
171
|
if (options.model)
|
|
@@ -160,7 +181,54 @@ async function runClaudeAttempt(options, automaticFallback) {
|
|
|
160
181
|
}
|
|
161
182
|
: options;
|
|
162
183
|
const raw = runOptions.dockerExec ? await runViaDocker(runOptions, args) : await runOnHost(runOptions, args);
|
|
163
|
-
return attachUsage(raw);
|
|
184
|
+
return attachSession(attachUsage(raw), options);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Reports the conversation this attempt ran in, so a later correction turn in
|
|
188
|
+
* the same lease can `--resume` it and send only what changed. Claude Code
|
|
189
|
+
* prints the session id on its first stream-json record (`system`/`init`) and
|
|
190
|
+
* again on the final result, so a turn cut short by guidance still has one.
|
|
191
|
+
* Nothing is reported when session persistence is off or an operator flag
|
|
192
|
+
* selects the conversation: there is then nothing Navarch may resume.
|
|
193
|
+
*
|
|
194
|
+
* `--resume` with an unknown id exits 1 with a zero-turn error result and no
|
|
195
|
+
* `init` record ("No conversation found with session ID"); that is the only
|
|
196
|
+
* case flagged `resumeRejected`, since no agent work can have started.
|
|
197
|
+
*/
|
|
198
|
+
function attachSession(result, options) {
|
|
199
|
+
let sessionId;
|
|
200
|
+
let started = false;
|
|
201
|
+
for (const line of result.stdout.split("\n")) {
|
|
202
|
+
const trimmed = line.trim();
|
|
203
|
+
if (!trimmed.startsWith("{"))
|
|
204
|
+
continue;
|
|
205
|
+
let record;
|
|
206
|
+
try {
|
|
207
|
+
record = JSON.parse(trimmed);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (typeof record !== "object" || record === null || Array.isArray(record))
|
|
213
|
+
continue;
|
|
214
|
+
const { type, subtype, session_id: id, num_turns: turns } = record;
|
|
215
|
+
if ((type === "system" && subtype === "init") || type === "assistant" || type === "user" ||
|
|
216
|
+
(type === "result" && typeof turns === "number" && turns > 0)) {
|
|
217
|
+
started = true;
|
|
218
|
+
}
|
|
219
|
+
if (typeof id === "string" && CLAUDE_SESSION_ID.test(id) && (type === "system" || type === "result")) {
|
|
220
|
+
sessionId = id;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const resumable = !hasFlag(options.extraArgs, "--no-session-persistence") &&
|
|
224
|
+
!operatorOwnsSession(options.extraArgs);
|
|
225
|
+
const rejected = Boolean(options.resumeSessionId) && !started && result.exitCode !== 0 &&
|
|
226
|
+
!result.timedOut && !result.killedByLeaseLoss;
|
|
227
|
+
return {
|
|
228
|
+
...result,
|
|
229
|
+
...(resumable && sessionId && !rejected ? { agentSessionId: sessionId } : {}),
|
|
230
|
+
...(rejected ? { resumeRejected: true } : {}),
|
|
231
|
+
};
|
|
164
232
|
}
|
|
165
233
|
/** Parses the final `claude -p --output-format stream-json` result record for usage and folds it onto the raw result (best-effort; leaves tokensIn/tokensOut/costUsd unset when parsing fails). */
|
|
166
234
|
function attachUsage(result) {
|
|
@@ -282,5 +350,8 @@ function shellQuote(value) {
|
|
|
282
350
|
exports.claudeCodeAdapter = {
|
|
283
351
|
agentType: "claude-code",
|
|
284
352
|
run: runClaudeCodeAdapter,
|
|
353
|
+
resumesTurns: true,
|
|
354
|
+
// `usage` is per turn; `total_cost_usd` covers the whole resumed conversation.
|
|
355
|
+
cumulativeResumeUsage: ["costUsd"],
|
|
285
356
|
conversation: claude_conversation_cjs_1.claudeConversationRunner,
|
|
286
357
|
};
|
package/dist/adapters/codex.cjs
CHANGED
|
@@ -48,7 +48,22 @@ const kill_agent_cjs_1 = require("../kill-agent.cjs");
|
|
|
48
48
|
* approval-bypass flag) without an adapter code change.
|
|
49
49
|
*/
|
|
50
50
|
async function runCodexAdapter(options) {
|
|
51
|
-
const
|
|
51
|
+
const resumable = codexCanResume(options.extraArgs);
|
|
52
|
+
if (options.resumeSessionId && (!resumable || !CODEX_THREAD_ID.test(options.resumeSessionId))) {
|
|
53
|
+
return {
|
|
54
|
+
exitCode: null,
|
|
55
|
+
timedOut: false,
|
|
56
|
+
killedByLeaseLoss: false,
|
|
57
|
+
stdout: "",
|
|
58
|
+
stderr: "Navarch did not resume the Codex thread: the thread id is invalid or the operator arguments do not apply to `codex exec resume`.",
|
|
59
|
+
resumeRejected: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// `codex exec resume <thread> -` continues a recorded thread with the
|
|
63
|
+
// stdin prompt (verified against codex-cli 0.163.0). Every other argument
|
|
64
|
+
// below is accepted by both forms, so the guard profile and MCP servers are
|
|
65
|
+
// re-applied identically on the resumed turn.
|
|
66
|
+
const args = options.resumeSessionId ? ["exec", "resume", options.resumeSessionId, "-"] : ["exec", "-"];
|
|
52
67
|
const env = { ...options.env };
|
|
53
68
|
if (options.mcpConfigPath)
|
|
54
69
|
args.push(...(await codexMcpArgs(options.mcpConfigPath, env)));
|
|
@@ -68,7 +83,53 @@ async function runCodexAdapter(options) {
|
|
|
68
83
|
const raw = runOptions.dockerExec
|
|
69
84
|
? await runViaDocker(runOptions, args)
|
|
70
85
|
: await runOnHost(runOptions, args);
|
|
71
|
-
return attachUsage(raw, options.model);
|
|
86
|
+
return attachThread(attachUsage(raw, options.model), options, resumable);
|
|
87
|
+
}
|
|
88
|
+
const CODEX_THREAD_ID = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
|
|
89
|
+
/**
|
|
90
|
+
* `codex exec` options that `codex exec resume` does not accept (codex-cli
|
|
91
|
+
* 0.163.0 `--help`), plus `--ephemeral`, which records no thread to resume.
|
|
92
|
+
* An operator argument from this list keeps every turn a fresh `codex exec`.
|
|
93
|
+
*/
|
|
94
|
+
const CODEX_NON_RESUMABLE_ARGS = [
|
|
95
|
+
"--ephemeral",
|
|
96
|
+
"--sandbox",
|
|
97
|
+
"-s",
|
|
98
|
+
"--profile",
|
|
99
|
+
"-p",
|
|
100
|
+
"--cd",
|
|
101
|
+
"-C",
|
|
102
|
+
"--add-dir",
|
|
103
|
+
"--oss",
|
|
104
|
+
"--local-provider",
|
|
105
|
+
"--color",
|
|
106
|
+
"--approve-for-me",
|
|
107
|
+
];
|
|
108
|
+
function codexCanResume(extraArgs) {
|
|
109
|
+
return !extraArgs.some((arg) => CODEX_NON_RESUMABLE_ARGS.some((flag) => arg === flag || (flag.startsWith("--") && arg.startsWith(`${flag}=`))));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Reports the thread this run recorded (`thread.started.thread_id`) so a later
|
|
113
|
+
* correction turn in the same lease can resume it. `codex exec resume` with an
|
|
114
|
+
* unknown id exits 1 before `turn.started` ("no rollout found for thread
|
|
115
|
+
* id"); only that shape is flagged `resumeRejected`, since no agent work ran.
|
|
116
|
+
*/
|
|
117
|
+
function attachThread(result, options, resumable) {
|
|
118
|
+
const events = (0, exit_conditions_cjs_1.parseCodexJsonEvents)(result.stdout);
|
|
119
|
+
const started = events.some((event) => event.type === "turn.started");
|
|
120
|
+
let threadId;
|
|
121
|
+
for (const event of events) {
|
|
122
|
+
const id = event.thread_id;
|
|
123
|
+
if (event.type === "thread.started" && typeof id === "string" && CODEX_THREAD_ID.test(id))
|
|
124
|
+
threadId = id;
|
|
125
|
+
}
|
|
126
|
+
const rejected = Boolean(options.resumeSessionId) && !started && result.exitCode !== 0 &&
|
|
127
|
+
!result.timedOut && !result.killedByLeaseLoss;
|
|
128
|
+
return {
|
|
129
|
+
...result,
|
|
130
|
+
...(resumable && threadId && !rejected ? { agentSessionId: threadId } : {}),
|
|
131
|
+
...(rejected ? { resumeRejected: true } : {}),
|
|
132
|
+
};
|
|
72
133
|
}
|
|
73
134
|
/** Operator policy wins over Navarch's generated host-mode profile. */
|
|
74
135
|
function hasExplicitPermissionPolicy(args) {
|
|
@@ -274,4 +335,7 @@ function shellQuote(value) {
|
|
|
274
335
|
exports.codexAdapter = {
|
|
275
336
|
agentType: "codex",
|
|
276
337
|
run: runCodexAdapter,
|
|
338
|
+
resumesTurns: true,
|
|
339
|
+
// `exec resume` reports the thread's total_token_usage, not the turn's.
|
|
340
|
+
cumulativeResumeUsage: ["tokensIn", "tokensOut", "cacheHitTokensIn", "costUsd"],
|
|
277
341
|
};
|
|
@@ -0,0 +1,110 @@
|
|
|
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.loadOpenCodeProviders = loadOpenCodeProviders;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const jsonc_parser_1 = require("jsonc-parser");
|
|
11
|
+
function isObject(value) {
|
|
12
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function merge(base, override) {
|
|
15
|
+
return Object.fromEntries([...new Set([...Object.keys(base), ...Object.keys(override)])].map((key) => {
|
|
16
|
+
const previous = base[key];
|
|
17
|
+
const next = override[key];
|
|
18
|
+
if (!Object.hasOwn(override, key))
|
|
19
|
+
return [key, previous];
|
|
20
|
+
return [key, isObject(previous) && isObject(next) ? merge(previous, next) : next];
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
/** Import operator providers only, before the adapter replaces OpenCode's paths.
|
|
24
|
+
* OpenCode V2 accepts legacy `provider` entries alongside native `providers`;
|
|
25
|
+
* let its own compatibility loader handle model capabilities and variants.
|
|
26
|
+
* Never discover repository config or import its plugins, MCP, or permissions.
|
|
27
|
+
*/
|
|
28
|
+
async function loadOpenCodeProviders(env, generation) {
|
|
29
|
+
const setting = (name) => env[name] ?? process.env[name];
|
|
30
|
+
const home = setting("HOME") || node_os_1.default.homedir();
|
|
31
|
+
const expand = (file) => file.startsWith("~/") ? node_path_1.default.join(home, file.slice(2)) : file;
|
|
32
|
+
const globalDir = node_path_1.default.join(setting("XDG_CONFIG_HOME") || node_path_1.default.join(home, ".config"), "opencode");
|
|
33
|
+
let result = {};
|
|
34
|
+
let secretIndex = 0;
|
|
35
|
+
// Resolve file references against their original config, then keep credentials
|
|
36
|
+
// out of both the generated file and OPENCODE_CONFIG_CONTENT. Existing env
|
|
37
|
+
// references remain references and are resolved by the OpenCode child.
|
|
38
|
+
async function prepare(value, directory, sensitive = false) {
|
|
39
|
+
if (Array.isArray(value))
|
|
40
|
+
return Promise.all(value.map((item) => prepare(item, directory, sensitive)));
|
|
41
|
+
if (isObject(value)) {
|
|
42
|
+
return Object.fromEntries(await Promise.all(Object.entries(value).map(async ([key, item]) => [
|
|
43
|
+
key, await prepare(item, directory, sensitive || /^(?:api[_-]?key|token|password|secret|headers)$/i.test(key)),
|
|
44
|
+
])));
|
|
45
|
+
}
|
|
46
|
+
if (typeof value !== "string")
|
|
47
|
+
return value;
|
|
48
|
+
const fromFile = /\{file:[^}]+\}/.test(value);
|
|
49
|
+
if (!fromFile && (!sensitive || !value || /^\{env:[^}]+\}$/.test(value)))
|
|
50
|
+
return value;
|
|
51
|
+
// Mixed strings such as "Bearer {env:TOKEN}" need expansion before moving
|
|
52
|
+
// the whole header into another env var; substitutions are not recursive.
|
|
53
|
+
let resolved = value.replace(/\{env:([^}]+)\}/g, (_match, name) => setting(name) ?? "");
|
|
54
|
+
for (const match of value.matchAll(/\{file:([^}]+)\}/g)) {
|
|
55
|
+
const file = node_path_1.default.resolve(directory, expand(match[1]));
|
|
56
|
+
const contents = (await node_fs_1.promises.readFile(file, "utf8")).trim();
|
|
57
|
+
resolved = resolved.replace(match[0], () => contents);
|
|
58
|
+
}
|
|
59
|
+
let name;
|
|
60
|
+
do {
|
|
61
|
+
name = `NAVARCH_OPENCODE_PROVIDER_${secretIndex++}`;
|
|
62
|
+
} while (setting(name) !== undefined);
|
|
63
|
+
env[name] = resolved;
|
|
64
|
+
return `{env:${name}}`;
|
|
65
|
+
}
|
|
66
|
+
async function include(text, source, directory) {
|
|
67
|
+
const errors = [];
|
|
68
|
+
const config = (0, jsonc_parser_1.parse)(text, errors, { allowTrailingComma: true });
|
|
69
|
+
if (errors.length || !isObject(config)) {
|
|
70
|
+
// Parser diagnostics can contain literal credentials; report only source.
|
|
71
|
+
throw new Error(`Invalid OpenCode JSON/JSONC configuration in ${source}.`);
|
|
72
|
+
}
|
|
73
|
+
const picked = {};
|
|
74
|
+
for (const key of generation === 1 ? ["provider"] : ["provider", "providers"]) {
|
|
75
|
+
if (config[key] === undefined)
|
|
76
|
+
continue;
|
|
77
|
+
if (!isObject(config[key]))
|
|
78
|
+
throw new Error(`OpenCode ${key} must be an object in ${source}.`);
|
|
79
|
+
picked[key] = await prepare(config[key], directory);
|
|
80
|
+
}
|
|
81
|
+
result = merge(result, picked);
|
|
82
|
+
}
|
|
83
|
+
async function file(filePath, optional) {
|
|
84
|
+
const absolute = node_path_1.default.resolve(expand(filePath));
|
|
85
|
+
let text;
|
|
86
|
+
try {
|
|
87
|
+
text = await node_fs_1.promises.readFile(absolute, "utf8");
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (optional && error.code === "ENOENT")
|
|
91
|
+
return;
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
await include(text, absolute, node_path_1.default.dirname(absolute));
|
|
95
|
+
}
|
|
96
|
+
for (const name of ["opencode.json", "opencode.jsonc"])
|
|
97
|
+
await file(node_path_1.default.join(globalDir, name), true);
|
|
98
|
+
const explicit = setting("OPENCODE_CONFIG");
|
|
99
|
+
if (explicit)
|
|
100
|
+
await file(explicit, false);
|
|
101
|
+
const directory = setting("OPENCODE_CONFIG_DIR");
|
|
102
|
+
if (directory) {
|
|
103
|
+
for (const name of ["opencode.json", "opencode.jsonc"])
|
|
104
|
+
await file(node_path_1.default.join(expand(directory), name), true);
|
|
105
|
+
}
|
|
106
|
+
const inline = setting("OPENCODE_CONFIG_CONTENT");
|
|
107
|
+
if (inline)
|
|
108
|
+
await include(inline, "OPENCODE_CONFIG_CONTENT", process.cwd());
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
@@ -12,6 +12,7 @@ const node_os_1 = __importDefault(require("node:os"));
|
|
|
12
12
|
const node_path_1 = __importDefault(require("node:path"));
|
|
13
13
|
const config_cjs_1 = require("../config.cjs");
|
|
14
14
|
const kill_agent_cjs_1 = require("../kill-agent.cjs");
|
|
15
|
+
const opencode_providers_cjs_1 = require("./opencode-providers.cjs");
|
|
15
16
|
/**
|
|
16
17
|
* Headless OpenCode adapter, fixture-pinned to the OpenCode JSON event
|
|
17
18
|
* contract shared by V1 and V2 (verified against 1.19.1, 2.0.6 and 2.0.17):
|
|
@@ -34,8 +35,9 @@ const kill_agent_cjs_1 = require("../kill-agent.cjs");
|
|
|
34
35
|
* so each Navarch session gets an isolated XDG/config directory, disables
|
|
35
36
|
* project discovery, and supplies its complete policy as both a private file
|
|
36
37
|
* and the highest-precedence inline config. Provider credentials remain in
|
|
37
|
-
* OpenCode's data store or the child environment
|
|
38
|
-
*
|
|
38
|
+
* OpenCode's data store or the child environment. Operator provider definitions
|
|
39
|
+
* are copied without importing unrelated global config; credential and MCP
|
|
40
|
+
* values use env references in the generated file.
|
|
39
41
|
*/
|
|
40
42
|
async function runOpenCodeAdapter(options) {
|
|
41
43
|
if (options.opencodeHostGuard && !options.dockerExec) {
|
|
@@ -178,10 +180,14 @@ async function prepareOpenCodeConfig(mcpConfigPath, env, inDocker, generation) {
|
|
|
178
180
|
};
|
|
179
181
|
}
|
|
180
182
|
const managedDirectories = await openCodeManagedDirectories(env, inDocker);
|
|
183
|
+
// Host provider endpoints and credentials belong to the operator's host.
|
|
184
|
+
// Docker continues to use its own provider setup, never the host's files.
|
|
185
|
+
const providers = inDocker ? {} : await (0, opencode_providers_cjs_1.loadOpenCodeProviders)(env, generation);
|
|
181
186
|
// Same policy in both dialects; in each the last matching rule wins.
|
|
182
187
|
const config = generation === 1
|
|
183
188
|
? {
|
|
184
189
|
$schema: "https://opencode.ai/config.json",
|
|
190
|
+
...providers,
|
|
185
191
|
autoupdate: false,
|
|
186
192
|
share: "disabled",
|
|
187
193
|
plugin: [],
|
|
@@ -197,6 +203,7 @@ async function prepareOpenCodeConfig(mcpConfigPath, env, inDocker, generation) {
|
|
|
197
203
|
}
|
|
198
204
|
: {
|
|
199
205
|
$schema: "https://opencode.ai/config.json",
|
|
206
|
+
...providers,
|
|
200
207
|
update: "disable",
|
|
201
208
|
share: "disabled",
|
|
202
209
|
plugins: [],
|
package/dist/delivery.cjs
CHANGED
|
@@ -24,7 +24,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
24
24
|
exports.probeWorktreeDelivery = probeWorktreeDelivery;
|
|
25
25
|
exports.evaluateDelivery = evaluateDelivery;
|
|
26
26
|
exports.renderDeliveryCorrectionPrompt = renderDeliveryCorrectionPrompt;
|
|
27
|
+
exports.renderResumedDeliveryCorrectionPrompt = renderResumedDeliveryCorrectionPrompt;
|
|
27
28
|
const sandbox_cjs_1 = require("./sandbox.cjs");
|
|
29
|
+
const prompt_cjs_1 = require("./prompt.cjs");
|
|
28
30
|
const GIT_TIMEOUT_MS = 30_000;
|
|
29
31
|
/**
|
|
30
32
|
* Reads the worktree's delivery state.
|
|
@@ -153,6 +155,19 @@ function renderDeliveryCorrectionPrompt(args) {
|
|
|
153
155
|
return [
|
|
154
156
|
args.originalPrompt.trim(),
|
|
155
157
|
"",
|
|
158
|
+
...deliveryIncompleteSection(args),
|
|
159
|
+
...(clippedReport ? ["", "### Previous turn's final report", "", clippedReport] : []),
|
|
160
|
+
].join("\n");
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The resumed counterpart of renderDeliveryCorrectionPrompt: the brief and
|
|
164
|
+
* the previous turn's final report are both already in the conversation.
|
|
165
|
+
*/
|
|
166
|
+
function renderResumedDeliveryCorrectionPrompt(args) {
|
|
167
|
+
return [(0, prompt_cjs_1.renderResumedTurnPreamble)(args.task), "", ...deliveryIncompleteSection(args)].join("\n");
|
|
168
|
+
}
|
|
169
|
+
function deliveryIncompleteSection(args) {
|
|
170
|
+
return [
|
|
156
171
|
"## Delivery is incomplete",
|
|
157
172
|
"",
|
|
158
173
|
`Your previous turn ended without delivering the task: ${args.reason}.`,
|
|
@@ -181,6 +196,5 @@ function renderDeliveryCorrectionPrompt(args) {
|
|
|
181
196
|
"",
|
|
182
197
|
"Your final report replaces the previous one. Restate it in full in the required format, preserving",
|
|
183
198
|
"the verdict, reviewed head, findings, and validation limitations. Do not claim delivery you cannot verify.",
|
|
184
|
-
|
|
185
|
-
].join("\n");
|
|
199
|
+
];
|
|
186
200
|
}
|
package/dist/prompt.cjs
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.renderPrompt = renderPrompt;
|
|
4
4
|
exports.renderGuidanceCorrectionPrompt = renderGuidanceCorrectionPrompt;
|
|
5
|
+
exports.renderMidFlightGuidanceSection = renderMidFlightGuidanceSection;
|
|
6
|
+
exports.renderResumedTurnPreamble = renderResumedTurnPreamble;
|
|
7
|
+
exports.renderResumedGuidancePrompt = renderResumedGuidancePrompt;
|
|
5
8
|
exports.renderCompletionRemediationPrompt = renderCompletionRemediationPrompt;
|
|
9
|
+
exports.renderResumedCompletionRemediationPrompt = renderResumedCompletionRemediationPrompt;
|
|
6
10
|
exports.renderHostedCheckpointNote = renderHostedCheckpointNote;
|
|
7
11
|
const CODE_GRAPH_GUIDANCE = "For JS/TS dependency inspection, the runtime provides `navarch-runtime code-graph --query <symbol-or-file> --mode search|callers|callees|impact --commit HEAD`. If the runtime is not on PATH, use its installed entry point; do not install another copy mid-task. Results describe committed source and include commit/freshness warnings; inspect current files for uncommitted changes and unresolved dependencies.";
|
|
8
12
|
const PROCESS_GUIDANCE = "Process management: this host may run other agent sessions. Keep the PID or tool handle of each preview server you start and stop only that owned process. Never use broad process-name or command-line matching (pkill, killall) to clean up servers: the agent launcher carries the task prompt in its arguments and can match those patterns, terminating your own session. Use a different port when a port is occupied by a process you do not own. Keep scratch files in the current worktree; paths in previous transcripts belong to earlier sessions. Before your final report, inspect `git status --short`, including untracked files. Preserve required evidence in task artifacts, then remove only temporary files you created and restore only tracked edits you made solely for local checks. Do not delete unfamiliar or pre-existing changes, hide leftovers with ignore rules, or commit scratch files merely to make the worktree clean. Review tasks deliver a native review and attestation, never commits, pushes, or a new PR.";
|
|
@@ -64,9 +68,12 @@ function renderPrompt(task, bundle) {
|
|
|
64
68
|
* partial edits instead of discarding mid-flight work.
|
|
65
69
|
*/
|
|
66
70
|
function renderGuidanceCorrectionPrompt(originalPrompt, guidance) {
|
|
71
|
+
return [originalPrompt.trim(), renderMidFlightGuidanceSection(guidance)].join("\n");
|
|
72
|
+
}
|
|
73
|
+
/** The guidance section shared by the full-brief and resumed guidance turns. */
|
|
74
|
+
function renderMidFlightGuidanceSection(guidance) {
|
|
67
75
|
const corrections = guidance.map((entry) => `- **${entry.given_at}**: ${entry.content}`);
|
|
68
76
|
return [
|
|
69
|
-
originalPrompt.trim(),
|
|
70
77
|
"## Mid-flight human guidance",
|
|
71
78
|
"",
|
|
72
79
|
"The task was actively steered while you were working. Human guidance takes precedence over earlier context. Continue in the same worktree: inspect the current changes, correct course, and then finish the task.",
|
|
@@ -75,6 +82,24 @@ function renderGuidanceCorrectionPrompt(originalPrompt, guidance) {
|
|
|
75
82
|
"",
|
|
76
83
|
].join("\n");
|
|
77
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Opens every turn that resumes the previous agent conversation
|
|
87
|
+
* (`AgentAdapter.resumesTurns`). The brief is already in that conversation,
|
|
88
|
+
* so the turn names the task in one line and says the brief still applies
|
|
89
|
+
* instead of re-sending 35–80K characters of it. A turn that cannot resume
|
|
90
|
+
* uses the full-brief renderers, which keep #1978's fix: a fresh agent never
|
|
91
|
+
* receives a correction without its task.
|
|
92
|
+
*/
|
|
93
|
+
function renderResumedTurnPreamble(task) {
|
|
94
|
+
return [
|
|
95
|
+
`Task: ${task.summary} (Navarch task ${task.id}).`,
|
|
96
|
+
"This turn continues the conversation above. The full task brief earlier in this conversation still applies and is not repeated here.",
|
|
97
|
+
].join("\n");
|
|
98
|
+
}
|
|
99
|
+
/** A resumed guidance turn: only the guidance this conversation has not seen. */
|
|
100
|
+
function renderResumedGuidancePrompt(task, guidance) {
|
|
101
|
+
return [renderResumedTurnPreamble(task), "", renderMidFlightGuidanceSection(guidance)].join("\n");
|
|
102
|
+
}
|
|
78
103
|
/** Keeps a long previous report from crowding the brief out of the prompt. */
|
|
79
104
|
const REMEDIATION_REPORT_LIMIT = 8_000;
|
|
80
105
|
/**
|
|
@@ -93,21 +118,33 @@ function renderCompletionRemediationPrompt(args) {
|
|
|
93
118
|
return [
|
|
94
119
|
args.originalPrompt.trim(),
|
|
95
120
|
"",
|
|
121
|
+
...completionRejectedSection(args),
|
|
122
|
+
...(clipped
|
|
123
|
+
? ["", "### Previous turn's final report", "", clipped]
|
|
124
|
+
: []),
|
|
125
|
+
"",
|
|
126
|
+
].join("\n");
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The resumed counterpart of renderCompletionRemediationPrompt: the brief and
|
|
130
|
+
* the previous turn's final report are both already in the conversation.
|
|
131
|
+
*/
|
|
132
|
+
function renderResumedCompletionRemediationPrompt(args) {
|
|
133
|
+
return [renderResumedTurnPreamble(args.task), "", ...completionRejectedSection(args), ""].join("\n");
|
|
134
|
+
}
|
|
135
|
+
function completionRejectedSection(args) {
|
|
136
|
+
return [
|
|
96
137
|
"## Completion was rejected",
|
|
97
138
|
"",
|
|
98
139
|
"Your previous turn on this task finished, but the Navarch control plane rejected its completion:",
|
|
99
140
|
"",
|
|
100
141
|
...args.rejection.trim().split("\n").map((line) => `> ${line}`),
|
|
101
142
|
"",
|
|
102
|
-
`This is correction turn ${args.attempt} of ${args.maxAttempts}. The task
|
|
143
|
+
`This is correction turn ${args.attempt} of ${args.maxAttempts}. The task is unchanged, and you are in the`,
|
|
103
144
|
"same worktree, so earlier edits, commits, and branches are still here. Reuse the work and findings from",
|
|
104
145
|
"the previous turn instead of starting over, fix what the rejection names, and finish the task.",
|
|
105
146
|
"Your final report replaces the previous one, so restate it in full in the required format.",
|
|
106
|
-
|
|
107
|
-
? ["", "### Previous turn's final report", "", clipped]
|
|
108
|
-
: []),
|
|
109
|
-
"",
|
|
110
|
-
].join("\n");
|
|
147
|
+
];
|
|
111
148
|
}
|
|
112
149
|
/**
|
|
113
150
|
* Tells the agent its worktree starts from a hosted attempt's checkpoint
|