nubos-pilot 1.2.4 → 1.3.1
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/CHANGELOG.md +17 -1
- package/README.md +2 -1
- package/SECURITY.md +3 -4
- package/bin/np-tools/_commands.cjs +3 -0
- package/bin/np-tools/_elision-proxy-entry.cjs +13 -0
- package/bin/np-tools/elision-bench.cjs +67 -0
- package/bin/np-tools/elision-get.cjs +48 -0
- package/bin/np-tools/elision-get.test.cjs +66 -0
- package/bin/np-tools/learnings.cjs +1 -1
- package/bin/np-tools/loop-run-round.cjs +25 -11
- package/bin/np-tools/plan-milestone.cjs +1 -0
- package/bin/np-tools/research-phase.cjs +1 -1
- package/bin/np-tools/resolve-model.cjs +55 -1
- package/bin/np-tools/resolve-model.test.cjs +139 -0
- package/bin/np-tools/security.cjs +1 -1
- package/bin/np-tools/spawn-headless.cjs +155 -3
- package/bin/np-tools/spawn-headless.test.cjs +108 -58
- package/bin/np-tools/spawn-offhost.cjs +93 -0
- package/bin/np-tools/spawn-offhost.test.cjs +38 -0
- package/lib/agents.cjs +16 -2
- package/lib/cache-align.cjs +78 -0
- package/lib/cache-align.test.cjs +69 -0
- package/lib/compress.cjs +495 -0
- package/lib/compress.test.cjs +267 -0
- package/lib/config-defaults.cjs +39 -0
- package/lib/config-schema.cjs +45 -5
- package/lib/elision-bench.cjs +409 -0
- package/lib/elision-bench.test.cjs +89 -0
- package/lib/elision-proxy.cjs +158 -0
- package/lib/elision-proxy.test.cjs +243 -0
- package/lib/elision.cjs +163 -0
- package/lib/elision.test.cjs +143 -0
- package/lib/learnings/extract.cjs +4 -4
- package/lib/learnings/extract.test.cjs +8 -8
- package/lib/model-providers.cjs +118 -0
- package/lib/model-providers.test.cjs +85 -0
- package/lib/nubosloop.cjs +1 -1
- package/lib/output-steering.cjs +68 -0
- package/lib/output-steering.test.cjs +74 -0
- package/lib/researcher-swarm.cjs +14 -3
- package/lib/runtime/agent-loop.cjs +94 -0
- package/lib/runtime/agent-loop.test.cjs +240 -0
- package/lib/runtime/dispatch.cjs +174 -0
- package/lib/runtime/dispatch.test.cjs +207 -0
- package/lib/runtime/preflight.cjs +68 -0
- package/lib/runtime/preflight.test.cjs +62 -0
- package/lib/runtime/providers/openai-compat.cjs +103 -0
- package/lib/runtime/providers/openai-compat.test.cjs +112 -0
- package/lib/runtime/tools/index.cjs +447 -0
- package/lib/runtime/tools/index.test.cjs +254 -0
- package/lib/schemas/data/elision-entry.v1.json +16 -0
- package/lib/security/review.cjs +4 -4
- package/lib/security/review.test.cjs +6 -6
- package/lib/token-cost.cjs +46 -0
- package/lib/token-cost.test.cjs +42 -0
- package/np-tools.cjs +3 -0
- package/package.json +1 -1
- package/workflows/add-tests.md +41 -0
- package/workflows/architect-phase.md +19 -0
- package/workflows/discuss-phase.md +29 -10
- package/workflows/execute-phase.md +93 -4
- package/workflows/plan-phase.md +57 -16
- package/workflows/research-phase.md +45 -0
- package/workflows/scan-codebase.md +21 -3
- package/workflows/validate-phase.md +30 -13
- package/workflows/verify-work.md +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,21 @@ All notable changes to nubos-pilot are documented in this file. Format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning
|
|
5
5
|
follows [SemVer](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## [1.
|
|
7
|
+
## [1.3.0] — 2026-06-17
|
|
8
|
+
|
|
9
|
+
Run any agent on any model, not only Claude.
|
|
10
|
+
|
|
11
|
+
- Per-agent model routing: two new config blocks, `model_providers` and `agent_routing`, send each agent to a specific model in the same run — planner on Claude opus, critic on OpenAI gpt-4o, executor on a local Ollama model. Any provider that speaks the OpenAI `/v1/chat/completions` dialect (OpenAI, xAI/Grok, Ollama, vLLM, LM Studio, LiteLLM) is reached through one `fetch`-based client, with no SDK added. Both blocks are optional; without them, resolution and spawning behave exactly as before.
|
|
12
|
+
- When the host can't route an agent to a non-native model — Claude Code's Agent tool only accepts Claude tiers — nubos-pilot runs the loop itself. It's a one-shot, zero-dependency tool-use harness: builds the prompt from `agents/<name>.md`, advertises the agent's tools as function schemas, runs the model's tool-calls against the workspace, and loops until a final answer. No daemon, the process exits when the loop returns.
|
|
13
|
+
- The off-host path runs through the same guards as the Claude path: working-tree safety, commit-policy, output-schema lint, the Nubosloop Rule-9 audit, and in-session security review, all unchanged. Off-host file writes are confined through `safe-path`. Off-host Bash runs only inside a slice worktree and stays off until `workflow.worktree_isolation` is on.
|
|
14
|
+
- Every workflow spawn now has an off-host branch — execute, plan, discuss, research, architect, validate, verify, scan. A test (`check-offhost-coverage`) walks the workflows and fails the suite if any spawn lacks one, so a new agent can't ship Claude-only by accident.
|
|
15
|
+
- A preflight runs before any off-host spawn and fails loud: it checks the server is reachable, the model is present, and tool-calling works, then aborts with an actionable message (`run: ollama pull <model>`) instead of dying mid-task. A routing entry that names an undefined provider is a hard config error at load time, never a quiet fallback to Claude.
|
|
16
|
+
|
|
17
|
+
Local models are weaker at multi-step tool-use than frontier Claude, so keep high-risk agents like the planner and security-reviewer on Claude — that's why the whole thing is opt-in. ADR-0021 has the full design.
|
|
18
|
+
|
|
19
|
+
Full documentation at <https://pilot.nubos.cloud>.
|
|
20
|
+
|
|
21
|
+
## [1.2.4] — 2026-06-15
|
|
8
22
|
|
|
9
23
|
Fixed a recursion fault in the in-session hooks that could spawn an unbounded cascade of headless `claude -p` processes.
|
|
10
24
|
|
|
@@ -12,6 +26,8 @@ Fixed a recursion fault in the in-session hooks that could spawn an unbounded ca
|
|
|
12
26
|
- Three independent guards back this up: the hook scripts and the `security`/`learnings` backends exit early when `NUBOS_PILOT_HEADLESS` is set; `spawn-headless` refuses to start a nested headless run (reentrancy + depth cap, default one level); and a per-agent lockfile under `.nubos-pilot/run/` bounds concurrent headless runs to one per agent even if the environment is not inherited. Headless runs already carry a hard timeout with SIGKILL, so a hung review cannot linger.
|
|
13
27
|
- Escape hatch: the guard keys off `NUBOS_PILOT_HEADLESS`, set automatically on the spawned `claude` — do not set it in your own shell or the in-session hooks will silently no-op. Raise the depth cap with `NUBOS_PILOT_MAX_HOOK_DEPTH` only if you understand the recursion risk.
|
|
14
28
|
|
|
29
|
+
Full documentation at <https://pilot.nubos.cloud>.
|
|
30
|
+
|
|
15
31
|
## [1.2.3] — 2026-06-14
|
|
16
32
|
|
|
17
33
|
Three opt-in layers that make execution cheaper, more reliable, and self-improving.
|
package/README.md
CHANGED
|
@@ -95,7 +95,7 @@ task(M001-S001-T0002): wire login handler
|
|
|
95
95
|
|
|
96
96
|
## Agents
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
Fourteen spawnable subagents are installed into the host's agent directory (alongside three `np-critic-*` audit modules consumed by `np-critic`):
|
|
99
99
|
|
|
100
100
|
- `np-planner` (opus) — breaks a milestone into slices + tasks
|
|
101
101
|
- `np-plan-checker` (opus) — adversarial goal-backward review before execution
|
|
@@ -109,6 +109,7 @@ Thirteen spawnable subagents are installed into the host's agent directory (alon
|
|
|
109
109
|
- `np-critic` (sonnet) — Nubosloop critic; audits executor output across style, tests and acceptance
|
|
110
110
|
- `np-verifier` (sonnet) — post-execution Pass/Fail/Defer per success_criterion
|
|
111
111
|
- `np-nyquist-auditor` (haiku) — requirement test-coverage audit
|
|
112
|
+
- `np-learnings-extractor` (haiku) — headless continuous-learning observer; distils reusable `{pattern, outcome}` learnings from a session's turn-diff
|
|
112
113
|
- `np-security-reviewer` (sonnet) — OWASP-aligned read-only audit (manual spawn)
|
|
113
114
|
|
|
114
115
|
Every spawn runs with an **explicit tier** (`haiku` / `sonnet` / `opus`) resolved to a concrete model via `np-tools.cjs resolve-model --profile <frontier|quality|balanced|budget|inherit>`.
|
package/SECURITY.md
CHANGED
|
@@ -18,11 +18,10 @@ versions and announced in `CHANGELOG.md`.
|
|
|
18
18
|
|
|
19
19
|
| Version | Supported |
|
|
20
20
|
|---------|-----------|
|
|
21
|
-
|
|
|
22
|
-
| <
|
|
21
|
+
| 1.3.x | ✅ active |
|
|
22
|
+
| < 1.3 | ❌ end of life |
|
|
23
23
|
|
|
24
|
-
Only the latest minor on the current major receives security patches
|
|
25
|
-
1.0 is reached.
|
|
24
|
+
Only the latest minor on the current major (1.x) receives security patches.
|
|
26
25
|
|
|
27
26
|
## Threat Model
|
|
28
27
|
|
|
@@ -89,6 +89,8 @@ const COMMANDS = [
|
|
|
89
89
|
{ name: 'knowledge-index', category: 'Utility', description: 'Build BM25-light index over .nubos-pilot/**/*.md → .nubos-pilot/state/knowledge-index.json', description_de: 'Baut BM25-Light-Index über .nubos-pilot/**/*.md → .nubos-pilot/state/knowledge-index.json' },
|
|
90
90
|
{ name: 'knowledge-search', category: 'Utility', description: 'Query the knowledge index; returns top-N JSON hits (rel_path + lines + score + preview). Pass --task <id> inside a Nubosloop task to record Rule 9 audit evidence', description_de: 'Sucht im Knowledge-Index; liefert Top-N-JSON-Treffer (rel_path + Zeilen + Score + Preview). --task <id> innerhalb eines Nubosloop-Tasks schreibt den Rule-9-Audit-Nachweis' },
|
|
91
91
|
{ name: 'knowledge-stats', category: 'Utility', description: 'Print knowledge-index size + grouping (auto-builds if missing)', description_de: 'Gibt Knowledge-Index-Größe + Gruppierung aus (baut auto bei Fehlen)' },
|
|
92
|
+
{ name: 'elision-get', category: 'Utility', description: 'Retrieve the original text behind a ⟦elided:<hash>⟧ compression marker. Reversible context compression (ADR-Elision). Positional <hash> or --hash; --json for envelope', description_de: 'Holt den Originaltext hinter einem ⟦elided:<hash>⟧ Kompressions-Marker. Reversible Kontext-Kompression (ADR-Elision). Positional <hash> oder --hash; --json für Envelope' },
|
|
93
|
+
{ name: 'elision-bench', category: 'Utility', description: 'Measure elision compression: deterministic fidelity (ratio + critical-line preservation + byte-exact reversibility) over a fixture corpus; --with-model adds answer-equivalence (raw vs compressed). --size <small|medium|large> runs a scaled corpus with a deterministic --holdout control group (--max-cases N) and an estimated token saving (--price-per-mtok <p> [--currency EUR] adds a cost estimate). --json, --tier <name>', description_de: 'Misst die Elision-Kompression: deterministische Fidelity (Ratio + Erhalt kritischer Zeilen + byte-exakte Reversibilität) über ein Fixture-Korpus; --with-model ergänzt Antwort-Äquivalenz (roh vs. komprimiert). --size <small|medium|large> fährt ein skaliertes Korpus mit deterministischer --holdout-Kontrollgruppe (--max-cases N) und geschätzter Token-Ersparnis (--price-per-mtok <p> [--currency EUR] ergänzt eine Kostenschätzung). --json, --tier <name>' },
|
|
92
94
|
{ name: 'context-stats', category: 'Utility', description: 'Aggregated context-budget stats (file counts + bytes per group, knowledge-index size)', description_de: 'Aggregierte Context-Budget-Stats (Dateien/Bytes pro Gruppe, Knowledge-Index-Größe)' },
|
|
93
95
|
{ name: 'session-snapshot-write', category: 'Utility', description: 'Capture session snapshot (current_task + recent commits + open handoffs) for resume', description_de: 'Erfasst Session-Snapshot (current_task + letzte Commits + offene Handoffs) für Resume' },
|
|
94
96
|
{ name: 'session-snapshot-read', category: 'Utility', description: 'Print last session snapshot as JSON', description_de: 'Gibt letzten Session-Snapshot als JSON aus' },
|
|
@@ -101,6 +103,7 @@ const COMMANDS = [
|
|
|
101
103
|
{ name: 'loop-audit-tool-use', category: 'Execution', description: 'Record/read the tool-use audit per spawn (Completeness Rule 9 mechanical check)', description_de: 'Tool-use Audit pro Spawn schreiben/lesen (Completeness Rule 9 mechanische Prüfung)' },
|
|
102
104
|
{ name: 'loop-stuck', category: 'Execution', description: 'Mark a task as stuck (writes loop-state + flips checkpoint status to stuck)', description_de: 'Markiert Task als stuck (schreibt Loop-State + setzt Checkpoint-Status auf stuck)' },
|
|
103
105
|
{ name: 'spawn-headless', category: 'Execution', description: 'Spawn an agent as a headless `claude -p` subprocess (ADR-0010 §L6); writes stdout to --output-path and returns exit code', description_de: 'Spawnt einen Agent als headless `claude -p` Subprozess (ADR-0010 §L6); schreibt stdout nach --output-path und liefert Exit-Code' },
|
|
106
|
+
{ name: 'spawn-offhost', category: 'Execution', description: 'Run an agent routed to an openai-compat provider (Ollama/OpenAI/Grok) as a one-shot tool-use loop (ADR-0021). Args: --agent --task|--task-file [--allow-bash] [--read-only]. Preflights the endpoint, records metrics.', description_de: 'Führt einen auf einen openai-compat-Provider (Ollama/OpenAI/Grok) gerouteten Agent als One-Shot-Tool-Use-Loop aus (ADR-0021). Args: --agent --task|--task-file [--allow-bash] [--read-only]. Preflight des Endpoints, Metrics-Aufzeichnung.' },
|
|
104
107
|
{ name: 'security', category: 'Review', description: 'In-session security review hook backend (ADR-0020). Verbs: session-start | baseline | scan | review | commit | run-review. Reads the Claude Code hook payload via --stdin; non-blocking, report-once, independent reviewer spawn.', description_de: 'Backend für die In-Session-Security-Review-Hooks (ADR-0020). Verben: session-start | baseline | scan | review | commit | run-review. Liest die Claude-Code-Hook-Payload via --stdin; non-blocking, report-once, unabhängiger Reviewer-Spawn.' },
|
|
105
108
|
{ name: 'loop-metrics', category: 'Utility', description: 'Aggregate Nubosloop telemetry across all checkpoints (commits, stuck, route distribution)', description_de: 'Aggregiert Nubosloop-Telemetrie über alle Checkpoints (Commits, Stuck, Routing)' },
|
|
106
109
|
{ name: 'learning-log', category: 'Execution', description: 'Persist a learning to the local store (or MCP adapter when configured)', description_de: 'Persistiert ein Learning im lokalen Store (oder MCP-Adapter falls konfiguriert)' },
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const proxy = require('../../lib/elision-proxy.cjs');
|
|
4
|
+
|
|
5
|
+
proxy.start({
|
|
6
|
+
cwd: process.env.ELISION_PROXY_CWD || process.cwd(),
|
|
7
|
+
upstream: process.env.ELISION_PROXY_UPSTREAM || undefined,
|
|
8
|
+
}).then(({ baseUrl }) => {
|
|
9
|
+
if (process.send) process.send({ ready: true, baseUrl });
|
|
10
|
+
}).catch((err) => {
|
|
11
|
+
if (process.send) process.send({ ready: false, error: String(err && err.message) });
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const bench = require('../../lib/elision-bench.cjs');
|
|
4
|
+
|
|
5
|
+
function _parseArgs(args) {
|
|
6
|
+
const out = { json: false, withModel: false, tier: 'frontier', size: null, holdout: 0.2, maxCases: null, charsPerToken: null, pricePerMTok: null, currency: null };
|
|
7
|
+
for (let i = 0; i < (args || []).length; i += 1) {
|
|
8
|
+
const a = args[i];
|
|
9
|
+
if (a === '--json') { out.json = true; continue; }
|
|
10
|
+
if (a === '--with-model') { out.withModel = true; continue; }
|
|
11
|
+
if (a === '--tier') { out.tier = args[++i] || out.tier; continue; }
|
|
12
|
+
if (a === '--size') { out.size = args[++i] || out.size; continue; }
|
|
13
|
+
if (a === '--holdout') { const v = parseFloat(args[++i]); if (Number.isFinite(v)) out.holdout = v; continue; }
|
|
14
|
+
if (a === '--max-cases') { const v = parseInt(args[++i], 10); if (Number.isFinite(v)) out.maxCases = v; continue; }
|
|
15
|
+
if (a === '--chars-per-token') { const v = parseFloat(args[++i]); if (Number.isFinite(v)) out.charsPerToken = v; continue; }
|
|
16
|
+
if (a === '--price-per-mtok') { const v = parseFloat(args[++i]); if (Number.isFinite(v)) out.pricePerMTok = v; continue; }
|
|
17
|
+
if (a === '--currency') { out.currency = args[++i] || out.currency; continue; }
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function run(args, ctx) {
|
|
23
|
+
const context = ctx || {};
|
|
24
|
+
const cwd = context.cwd || process.cwd();
|
|
25
|
+
const stdout = context.stdout || process.stdout;
|
|
26
|
+
const parsed = _parseArgs(args || []);
|
|
27
|
+
|
|
28
|
+
if (parsed.size) {
|
|
29
|
+
const scale = bench.runScale({ cwd, size: parsed.size, holdoutRatio: parsed.holdout, maxCases: parsed.maxCases, charsPerToken: parsed.charsPerToken, pricePerMTok: parsed.pricePerMTok, currency: parsed.currency });
|
|
30
|
+
stdout.write((parsed.json ? JSON.stringify({ scale }) : bench.formatScale(scale)) + '\n');
|
|
31
|
+
return scale.summary.invariants_ok ? 0 : 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const fidelity = bench.runFidelity({ cwd });
|
|
35
|
+
let equivalence = null;
|
|
36
|
+
|
|
37
|
+
if (parsed.withModel) {
|
|
38
|
+
const { resolveFromConfig } = require('./resolve-model.cjs');
|
|
39
|
+
const { chat } = require('../../lib/runtime/providers/openai-compat.cjs');
|
|
40
|
+
const res = resolveFromConfig({ agentOrTier: parsed.tier, cwd, format: 'json' });
|
|
41
|
+
if (!res || !res.model || !res.baseUrl) {
|
|
42
|
+
stdout.write((parsed.json ? JSON.stringify({ fidelity, equivalence: null, error: 'no-provider' }) : 'No off-host provider resolved for tier "' + parsed.tier + '" — equivalence skipped.') + '\n');
|
|
43
|
+
return fidelity.summary.invariants_ok ? 0 : 1;
|
|
44
|
+
}
|
|
45
|
+
const provider = { baseUrl: res.baseUrl, apiKeyEnv: res.apiKeyEnv, model: res.model };
|
|
46
|
+
equivalence = await bench.runEquivalence({ chatImpl: chat, provider });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (parsed.json) {
|
|
50
|
+
stdout.write(JSON.stringify({ fidelity, equivalence }) + '\n');
|
|
51
|
+
} else {
|
|
52
|
+
stdout.write(bench.formatReport(fidelity) + '\n');
|
|
53
|
+
if (equivalence) {
|
|
54
|
+
stdout.write('\nAnswer equivalence — ' + equivalence.summary.equivalent + '/' + equivalence.summary.cases
|
|
55
|
+
+ ' equivalent, regressions: ' + (equivalence.summary.regressions.length ? equivalence.summary.regressions.join(', ') : 'none') + '\n');
|
|
56
|
+
for (const c of equivalence.cases) {
|
|
57
|
+
stdout.write(' ' + (c.regression ? '✗' : '✓') + ' ' + c.name + ' (raw '
|
|
58
|
+
+ (c.raw_ok ? 'ok' : 'miss') + ', compressed ' + (c.compressed_ok ? 'ok' : 'miss') + ')\n');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const ok = fidelity.summary.invariants_ok && (!equivalence || equivalence.summary.no_regression);
|
|
64
|
+
return ok ? 0 : 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { run, _parseArgs };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
4
|
+
const elision = require('../../lib/elision.cjs');
|
|
5
|
+
|
|
6
|
+
function _parseArgs(args) {
|
|
7
|
+
const out = { hash: null, json: false };
|
|
8
|
+
const positional = [];
|
|
9
|
+
for (let i = 0; i < args.length; i++) {
|
|
10
|
+
const a = args[i];
|
|
11
|
+
if (a === '--hash') { out.hash = args[++i] || null; continue; }
|
|
12
|
+
if (a === '--json') { out.json = true; continue; }
|
|
13
|
+
positional.push(a);
|
|
14
|
+
}
|
|
15
|
+
if (out.hash == null) out.hash = positional.join('').trim() || null;
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function run(args, ctx) {
|
|
20
|
+
const context = ctx || {};
|
|
21
|
+
const cwd = context.cwd || process.cwd();
|
|
22
|
+
const stdout = context.stdout || process.stdout;
|
|
23
|
+
const parsed = _parseArgs(args || []);
|
|
24
|
+
if (!parsed.hash) {
|
|
25
|
+
throw new NubosPilotError(
|
|
26
|
+
'elision-get-missing-hash',
|
|
27
|
+
'elision-get requires a hash (positional or --hash)',
|
|
28
|
+
{ hint: 'hash is the 12-char id from a ⟦elided:…⟧ marker' },
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const result = elision.retrieve(parsed.hash, cwd);
|
|
32
|
+
if (parsed.json) {
|
|
33
|
+
stdout.write(JSON.stringify(result));
|
|
34
|
+
return result.status === 'ok' ? 0 : 1;
|
|
35
|
+
}
|
|
36
|
+
if (result.status === 'ok') {
|
|
37
|
+
stdout.write(result.original);
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
if (result.status === 'expired') {
|
|
41
|
+
stdout.write('[elision ' + parsed.hash + ' expired — original no longer cached (ttl elapsed)]\n');
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
stdout.write('[elision ' + parsed.hash + ' not found]\n');
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { run, _parseArgs };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
const elisionGet = require('./elision-get.cjs');
|
|
10
|
+
const elision = require('../../lib/elision.cjs');
|
|
11
|
+
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
12
|
+
|
|
13
|
+
function sandbox() {
|
|
14
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-elisionget-'));
|
|
15
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
|
|
16
|
+
return root;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function capture() {
|
|
20
|
+
const chunks = [];
|
|
21
|
+
return { stream: { write: (s) => chunks.push(s) }, text: () => chunks.join('') };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test('ELISIONGET-1: prints the original for a known hash', () => {
|
|
25
|
+
const root = sandbox();
|
|
26
|
+
try {
|
|
27
|
+
const hash = elision.store('the original payload', { type: 'plain' }, root);
|
|
28
|
+
const out = capture();
|
|
29
|
+
const code = elisionGet.run([hash], { cwd: root, stdout: out.stream });
|
|
30
|
+
assert.equal(code, 0);
|
|
31
|
+
assert.equal(out.text(), 'the original payload');
|
|
32
|
+
} finally {
|
|
33
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('ELISIONGET-2: unknown hash exits 1 with a not-found notice', () => {
|
|
38
|
+
const root = sandbox();
|
|
39
|
+
try {
|
|
40
|
+
const out = capture();
|
|
41
|
+
const code = elisionGet.run(['aaaaaaaaaaaa'], { cwd: root, stdout: out.stream });
|
|
42
|
+
assert.equal(code, 1);
|
|
43
|
+
assert.match(out.text(), /not found/);
|
|
44
|
+
} finally {
|
|
45
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('ELISIONGET-3: --json emits the retrieval envelope', () => {
|
|
50
|
+
const root = sandbox();
|
|
51
|
+
try {
|
|
52
|
+
const hash = elision.store('payload', { type: 'plain' }, root);
|
|
53
|
+
const out = capture();
|
|
54
|
+
const code = elisionGet.run([hash, '--json'], { cwd: root, stdout: out.stream });
|
|
55
|
+
assert.equal(code, 0);
|
|
56
|
+
const env = JSON.parse(out.text());
|
|
57
|
+
assert.equal(env.status, 'ok');
|
|
58
|
+
assert.equal(env.original, 'payload');
|
|
59
|
+
} finally {
|
|
60
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('ELISIONGET-4: missing hash throws', () => {
|
|
65
|
+
assert.throws(() => elisionGet.run([], { cwd: process.cwd(), stdout: { write() {} } }), NubosPilotError);
|
|
66
|
+
});
|
|
@@ -90,7 +90,7 @@ async function run(argv, ctx) {
|
|
|
90
90
|
if (verb === 'run-extract') {
|
|
91
91
|
const sid = args.getFlag(list, '--session') || '';
|
|
92
92
|
try {
|
|
93
|
-
const result = extract.runExtract({ cwd, sid, config: cfg });
|
|
93
|
+
const result = await extract.runExtract({ cwd, sid, config: cfg });
|
|
94
94
|
_emit(stdout, result);
|
|
95
95
|
} catch (err) {
|
|
96
96
|
_emit(stdout, { ran: false, reason: 'error', error: String(err && err.code || err) });
|
|
@@ -6,16 +6,37 @@ const path = require('node:path');
|
|
|
6
6
|
const { NubosPilotError, safeAssign } = require('../../lib/core.cjs');
|
|
7
7
|
const safePath = require('../../lib/safe-path.cjs');
|
|
8
8
|
|
|
9
|
-
function _resolveInsideCwdOrTmp(p, cwd, label, errorCode) {
|
|
10
|
-
return safePath.assertInsideCwdOrTmp(p, cwd, label, errorCode);
|
|
11
|
-
}
|
|
12
9
|
const checkpoint = require('../../lib/checkpoint.cjs');
|
|
13
10
|
const nubosloop = require('../../lib/nubosloop.cjs');
|
|
14
11
|
const messaging = require('../../lib/messaging.cjs');
|
|
12
|
+
const compress = require('../../lib/compress.cjs');
|
|
13
|
+
const elision = require('../../lib/elision.cjs');
|
|
14
|
+
const logger = require('../../lib/logger.cjs').child('loop-run-round');
|
|
15
15
|
const args = require('./_args.cjs');
|
|
16
16
|
|
|
17
17
|
const { TASK_ID_RE } = require('../../lib/ids.cjs');
|
|
18
18
|
|
|
19
|
+
function _resolveInsideCwdOrTmp(p, cwd, label, errorCode) {
|
|
20
|
+
return safePath.assertInsideCwdOrTmp(p, cwd, label, errorCode);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function _verifyExcerpt(verifyOutput, cwd) {
|
|
24
|
+
const s = String(verifyOutput);
|
|
25
|
+
const cx = elision.compressionContext(cwd);
|
|
26
|
+
const maxBytes = cx.verifyMaxBytes;
|
|
27
|
+
if (s.length <= maxBytes) return s;
|
|
28
|
+
if (cx.enabled) {
|
|
29
|
+
try {
|
|
30
|
+
const crushed = compress.crushLogToBudget(s, maxBytes);
|
|
31
|
+
if (crushed && crushed.length < s.length) {
|
|
32
|
+
const hash = cx.store ? cx.store(s, 'log') : null;
|
|
33
|
+
return crushed + (hash ? '\n' + compress.marker(hash, 'full verify log') : '');
|
|
34
|
+
}
|
|
35
|
+
} catch (err) { logger.warn('verify compression skipped', { cause: err && err.message }); }
|
|
36
|
+
}
|
|
37
|
+
return '…[truncated head, original ' + s.length + ' bytes — see VERIFY_LOG]\n' + s.slice(-maxBytes);
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
const VALID_PHASES = new Set([
|
|
20
41
|
'preflight',
|
|
21
42
|
'post-researcher',
|
|
@@ -182,16 +203,9 @@ function _runPostExecutor(taskId, list, cwd) {
|
|
|
182
203
|
}
|
|
183
204
|
}
|
|
184
205
|
const green = code === 0;
|
|
185
|
-
const VERIFY_TAIL_BYTES = 2000;
|
|
186
206
|
let verifyExcerpt = null;
|
|
187
207
|
if (verifyOutput) {
|
|
188
|
-
|
|
189
|
-
if (s.length > VERIFY_TAIL_BYTES) {
|
|
190
|
-
verifyExcerpt = '…[truncated head, original ' + s.length + ' bytes — see VERIFY_LOG]\n'
|
|
191
|
-
+ s.slice(-VERIFY_TAIL_BYTES);
|
|
192
|
-
} else {
|
|
193
|
-
verifyExcerpt = s;
|
|
194
|
-
}
|
|
208
|
+
verifyExcerpt = _verifyExcerpt(verifyOutput, cwd);
|
|
195
209
|
}
|
|
196
210
|
const merged = checkpoint.mergeCheckpoint(
|
|
197
211
|
taskId,
|
|
@@ -107,7 +107,7 @@ async function run(args, ctx) {
|
|
|
107
107
|
const tmDetail = textMode.resolveTextModeDetail(cwd);
|
|
108
108
|
|
|
109
109
|
const swarmOpts = swarm.resolveSwarmOpts(cwd);
|
|
110
|
-
const spawnSpecs = swarm.buildSpawnSpecs({ milestone: mNum, milestone_id: mIdStr, goal: def.goal || '' }, swarmOpts.k);
|
|
110
|
+
const spawnSpecs = swarm.buildSpawnSpecs({ milestone: mNum, milestone_id: mIdStr, goal: def.goal || '' }, swarmOpts.k, { cwd });
|
|
111
111
|
|
|
112
112
|
let cacheHit = null;
|
|
113
113
|
let cacheMiss = null;
|
|
@@ -2,6 +2,7 @@ const { NubosPilotError } = require('../../lib/core.cjs');
|
|
|
2
2
|
const { readConfig, _CONFIG_PARSE_CODES } = require('../../lib/config.cjs');
|
|
3
3
|
const { loadAgent, loadAgentModule } = require('../../lib/agents.cjs');
|
|
4
4
|
const { resolve: resolveAlias, MODEL_ALIAS_MAP, VALID_TIERS } = require('../../lib/model-profiles.cjs');
|
|
5
|
+
const { resolveProvider } = require('../../lib/model-providers.cjs');
|
|
5
6
|
|
|
6
7
|
let _warnedCorruptOnce = false;
|
|
7
8
|
function _readConfig(cwd) {
|
|
@@ -56,11 +57,13 @@ function resolveFromConfig({ agentOrTier, profileOverride, cwd, format }) {
|
|
|
56
57
|
const config = _readConfig(cwd);
|
|
57
58
|
|
|
58
59
|
let tier;
|
|
60
|
+
let agentName = null;
|
|
59
61
|
if (VALID_TIERS.includes(agentOrTier)) {
|
|
60
62
|
tier = agentOrTier;
|
|
61
63
|
} else {
|
|
62
64
|
const fm = _loadAgentForResolve(agentOrTier, cwd);
|
|
63
65
|
tier = fm.tier;
|
|
66
|
+
agentName = agentOrTier;
|
|
64
67
|
const override = _criticTierOverride(config, agentOrTier);
|
|
65
68
|
if (override) tier = override;
|
|
66
69
|
}
|
|
@@ -91,7 +94,32 @@ function resolveFromConfig({ agentOrTier, profileOverride, cwd, format }) {
|
|
|
91
94
|
resolved = alias;
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
|
|
97
|
+
const prov = resolveProvider({ agentName, tier, config });
|
|
98
|
+
let providerModel = prov.model;
|
|
99
|
+
if (prov.kind === 'native') {
|
|
100
|
+
if (prov.model) {
|
|
101
|
+
resolved = prov.model;
|
|
102
|
+
mode = 'full-id';
|
|
103
|
+
} else {
|
|
104
|
+
providerModel = null;
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
resolved = prov.model;
|
|
108
|
+
mode = 'provider';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
tier,
|
|
113
|
+
profile,
|
|
114
|
+
alias,
|
|
115
|
+
resolved,
|
|
116
|
+
mode,
|
|
117
|
+
provider: prov.provider,
|
|
118
|
+
kind: prov.kind,
|
|
119
|
+
model: providerModel,
|
|
120
|
+
baseUrl: prov.baseUrl,
|
|
121
|
+
apiKeyEnv: prov.apiKeyEnv,
|
|
122
|
+
};
|
|
95
123
|
}
|
|
96
124
|
|
|
97
125
|
function run(argv) {
|
|
@@ -105,12 +133,18 @@ function run(argv) {
|
|
|
105
133
|
const agentOrTier = args.shift();
|
|
106
134
|
let profileOverride = null;
|
|
107
135
|
let format = null;
|
|
136
|
+
let asJson = false;
|
|
137
|
+
let asKind = false;
|
|
108
138
|
while (args.length) {
|
|
109
139
|
const flag = args.shift();
|
|
110
140
|
if (flag === '--profile') {
|
|
111
141
|
profileOverride = args.shift();
|
|
112
142
|
} else if (flag === '--format') {
|
|
113
143
|
format = args.shift();
|
|
144
|
+
} else if (flag === '--json') {
|
|
145
|
+
asJson = true;
|
|
146
|
+
} else if (flag === '--kind') {
|
|
147
|
+
asKind = true;
|
|
114
148
|
} else if (flag === '--raw') {
|
|
115
149
|
|
|
116
150
|
}
|
|
@@ -122,6 +156,26 @@ function run(argv) {
|
|
|
122
156
|
cwd: process.cwd(),
|
|
123
157
|
format,
|
|
124
158
|
});
|
|
159
|
+
if (asKind) {
|
|
160
|
+
process.stdout.write((out.kind || 'native') + '\n');
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
if (asJson) {
|
|
164
|
+
process.stdout.write(JSON.stringify(out) + '\n');
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
if (out.kind === 'openai-compat') {
|
|
168
|
+
process.stderr.write(
|
|
169
|
+
JSON.stringify({
|
|
170
|
+
code: 'off-host-not-on-native-path',
|
|
171
|
+
message: 'agent "' + agentOrTier + '" routes to provider "' + out.provider
|
|
172
|
+
+ '" (model ' + out.model + '), which the native `claude` spawn path cannot run. '
|
|
173
|
+
+ 'Run it off-host with: np-tools spawn-offhost --agent ' + agentOrTier + ' --task <…>',
|
|
174
|
+
details: { provider: out.provider, kind: out.kind, model: out.model },
|
|
175
|
+
}) + '\n',
|
|
176
|
+
);
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
125
179
|
process.stdout.write(out.resolved + '\n');
|
|
126
180
|
return 0;
|
|
127
181
|
} catch (err) {
|
|
@@ -72,6 +72,11 @@ test('RM-1: tier branch with empty config returns alias mode, default balanced p
|
|
|
72
72
|
alias: 'opus',
|
|
73
73
|
resolved: 'opus',
|
|
74
74
|
mode: 'alias',
|
|
75
|
+
provider: 'claude',
|
|
76
|
+
kind: 'native',
|
|
77
|
+
model: null,
|
|
78
|
+
baseUrl: null,
|
|
79
|
+
apiKeyEnv: null,
|
|
75
80
|
});
|
|
76
81
|
});
|
|
77
82
|
|
|
@@ -276,3 +281,137 @@ test('RM-18: module agent without override falls back to module frontmatter tier
|
|
|
276
281
|
});
|
|
277
282
|
assert.equal(out.tier, 'haiku');
|
|
278
283
|
});
|
|
284
|
+
|
|
285
|
+
test('RM-19: agent_routing to openai-compat resolves model from provider models table by tier', () => {
|
|
286
|
+
const cwd = _sandbox({
|
|
287
|
+
model_providers: {
|
|
288
|
+
default: 'claude',
|
|
289
|
+
claude: { kind: 'native' },
|
|
290
|
+
ollama: { kind: 'openai-compat', base_url: 'http://localhost:11434/v1', models: { opus: 'qwen2.5-coder:32b' } },
|
|
291
|
+
},
|
|
292
|
+
agent_routing: { 'np-planner': { provider: 'ollama' } },
|
|
293
|
+
}, { 'np-planner': _plannerAgent });
|
|
294
|
+
const out = subcmd.resolveFromConfig({ agentOrTier: 'np-planner', cwd });
|
|
295
|
+
assert.equal(out.provider, 'ollama');
|
|
296
|
+
assert.equal(out.kind, 'openai-compat');
|
|
297
|
+
assert.equal(out.model, 'qwen2.5-coder:32b');
|
|
298
|
+
assert.equal(out.resolved, 'qwen2.5-coder:32b');
|
|
299
|
+
assert.equal(out.mode, 'provider');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('RM-20: explicit model pin in agent_routing beats provider models table', () => {
|
|
303
|
+
const cwd = _sandbox({
|
|
304
|
+
model_providers: {
|
|
305
|
+
ollama: { kind: 'openai-compat', base_url: 'http://localhost:11434/v1', models: { opus: 'fallback-model' } },
|
|
306
|
+
},
|
|
307
|
+
agent_routing: { 'np-planner': { provider: 'ollama', model: 'qwen3.5' } },
|
|
308
|
+
}, { 'np-planner': _plannerAgent });
|
|
309
|
+
const out = subcmd.resolveFromConfig({ agentOrTier: 'np-planner', cwd });
|
|
310
|
+
assert.equal(out.model, 'qwen3.5');
|
|
311
|
+
assert.equal(out.resolved, 'qwen3.5');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('RM-21: native provider with an explicit model pin forces full-id mode', () => {
|
|
315
|
+
const cwd = _sandbox({
|
|
316
|
+
model_providers: { claude: { kind: 'native' } },
|
|
317
|
+
agent_routing: { 'np-planner': { provider: 'claude', model: 'claude-opus-4-7' } },
|
|
318
|
+
}, { 'np-planner': _plannerAgent });
|
|
319
|
+
const out = subcmd.resolveFromConfig({ agentOrTier: 'np-planner', cwd });
|
|
320
|
+
assert.equal(out.kind, 'native');
|
|
321
|
+
assert.equal(out.resolved, 'claude-opus-4-7');
|
|
322
|
+
assert.equal(out.mode, 'full-id');
|
|
323
|
+
assert.equal(out.model, 'claude-opus-4-7');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('RM-22: glob routing key (np-critic*) matches a critic agent', () => {
|
|
327
|
+
const cwd = _sandbox({
|
|
328
|
+
model_providers: { openai: { kind: 'openai-compat', base_url: 'https://api.openai.com/v1', models: { sonnet: 'gpt-4o' } } },
|
|
329
|
+
agent_routing: { 'np-critic*': { provider: 'openai', model: 'gpt-4o' } },
|
|
330
|
+
}, { 'np-critic': '---\nname: np-critic\ndescription: x\ntier: sonnet\ntools: Read\n---\nbody' });
|
|
331
|
+
const out = subcmd.resolveFromConfig({ agentOrTier: 'np-critic', cwd });
|
|
332
|
+
assert.equal(out.provider, 'openai');
|
|
333
|
+
assert.equal(out.model, 'gpt-4o');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('RM-23: routing to an undefined provider fails loud (no silent claude fallback)', () => {
|
|
337
|
+
const cwd = _sandbox({
|
|
338
|
+
model_providers: { claude: { kind: 'native' } },
|
|
339
|
+
agent_routing: { 'np-planner': { provider: 'ollama' } },
|
|
340
|
+
}, { 'np-planner': _plannerAgent });
|
|
341
|
+
let thrown = null;
|
|
342
|
+
try { subcmd.resolveFromConfig({ agentOrTier: 'np-planner', cwd }); } catch (e) { thrown = e; }
|
|
343
|
+
assert.ok(thrown);
|
|
344
|
+
assert.equal(thrown.name, 'NubosPilotError');
|
|
345
|
+
assert.equal(thrown.code, 'provider-undefined');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('RM-24: openai-compat with no pin and no models[tier] fails loud', () => {
|
|
349
|
+
const cwd = _sandbox({
|
|
350
|
+
model_providers: { ollama: { kind: 'openai-compat', base_url: 'http://localhost:11434/v1', models: { haiku: 'small' } } },
|
|
351
|
+
agent_routing: { 'np-planner': { provider: 'ollama' } },
|
|
352
|
+
}, { 'np-planner': _plannerAgent });
|
|
353
|
+
let thrown = null;
|
|
354
|
+
try { subcmd.resolveFromConfig({ agentOrTier: 'np-planner', cwd }); } catch (e) { thrown = e; }
|
|
355
|
+
assert.ok(thrown);
|
|
356
|
+
assert.equal(thrown.code, 'provider-model-unresolved');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test('RM-25: bare tier never routes — stays on implicit claude-native default', () => {
|
|
360
|
+
const cwd = _sandbox({
|
|
361
|
+
model_providers: { ollama: { kind: 'openai-compat', base_url: 'http://localhost:11434/v1', models: { opus: 'qwen' } } },
|
|
362
|
+
agent_routing: { opus: { provider: 'ollama' } },
|
|
363
|
+
});
|
|
364
|
+
const out = subcmd.resolveFromConfig({ agentOrTier: 'opus', cwd });
|
|
365
|
+
assert.equal(out.provider, 'claude');
|
|
366
|
+
assert.equal(out.kind, 'native');
|
|
367
|
+
assert.equal(out.resolved, 'opus');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('RM-27: run() refuses an off-host (openai-compat) agent loud — no model id on stdout', () => {
|
|
371
|
+
const root = _sandbox({
|
|
372
|
+
model_providers: { ollama: { kind: 'openai-compat', base_url: 'http://localhost:11434/v1', models: { opus: 'qwen2.5-coder:32b' } } },
|
|
373
|
+
agent_routing: { 'np-planner': { provider: 'ollama' } },
|
|
374
|
+
}, { 'np-planner': _plannerAgent });
|
|
375
|
+
const origCwd = process.cwd();
|
|
376
|
+
process.chdir(root);
|
|
377
|
+
try {
|
|
378
|
+
const cap = _captureStdout(() => subcmd.run(['np-planner']));
|
|
379
|
+
assert.equal(cap.rc, 1);
|
|
380
|
+
assert.equal(cap.stdout, '');
|
|
381
|
+
assert.match(cap.stderr, /off-host-not-on-native-path/);
|
|
382
|
+
assert.match(cap.stderr, /spawn-offhost/);
|
|
383
|
+
} finally {
|
|
384
|
+
process.chdir(origCwd);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('RM-28: --json reports the full resolution and succeeds even for off-host', () => {
|
|
389
|
+
const root = _sandbox({
|
|
390
|
+
model_providers: { ollama: { kind: 'openai-compat', base_url: 'http://localhost:11434/v1', models: { opus: 'qwen2.5-coder:32b' } } },
|
|
391
|
+
agent_routing: { 'np-planner': { provider: 'ollama' } },
|
|
392
|
+
}, { 'np-planner': _plannerAgent });
|
|
393
|
+
const origCwd = process.cwd();
|
|
394
|
+
process.chdir(root);
|
|
395
|
+
try {
|
|
396
|
+
const cap = _captureStdout(() => subcmd.run(['np-planner', '--json']));
|
|
397
|
+
assert.equal(cap.rc, 0);
|
|
398
|
+
const out = JSON.parse(cap.stdout.trim());
|
|
399
|
+
assert.equal(out.kind, 'openai-compat');
|
|
400
|
+
assert.equal(out.provider, 'ollama');
|
|
401
|
+
assert.equal(out.baseUrl, 'http://localhost:11434/v1');
|
|
402
|
+
} finally {
|
|
403
|
+
process.chdir(origCwd);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('RM-26: model_providers.default switches the default provider for all agents', () => {
|
|
408
|
+
const cwd = _sandbox({
|
|
409
|
+
model_providers: {
|
|
410
|
+
default: 'ollama',
|
|
411
|
+
ollama: { kind: 'openai-compat', base_url: 'http://localhost:11434/v1', models: { opus: 'qwen2.5-coder:32b' } },
|
|
412
|
+
},
|
|
413
|
+
}, { 'np-planner': _plannerAgent });
|
|
414
|
+
const out = subcmd.resolveFromConfig({ agentOrTier: 'np-planner', cwd });
|
|
415
|
+
assert.equal(out.provider, 'ollama');
|
|
416
|
+
assert.equal(out.model, 'qwen2.5-coder:32b');
|
|
417
|
+
});
|
|
@@ -170,7 +170,7 @@ async function run(argv, ctx) {
|
|
|
170
170
|
if (verb === 'run-review') {
|
|
171
171
|
if (!cfg.enabled || !sid) return 0;
|
|
172
172
|
const mode = args.getFlag(list, '--mode') === 'commit' ? 'commit' : 'stop';
|
|
173
|
-
try { review.runReview({ cwd, sid, mode, config: { ...cfg, guidance_path: _resolveRel(cwd, cfg.guidance_path) } }); } catch {}
|
|
173
|
+
try { await review.runReview({ cwd, sid, mode, config: { ...cfg, guidance_path: _resolveRel(cwd, cfg.guidance_path) } }); } catch {}
|
|
174
174
|
return 0;
|
|
175
175
|
}
|
|
176
176
|
|