nomarmy 0.1.0-alpha.17 โ 0.1.0-alpha.19
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 +5 -1
- package/bin/nomarmy.mjs +190 -65
- package/docker/Dockerfile +2 -2
- package/docker/Dockerfile.go +1 -1
- package/docker/Dockerfile.rust +1 -1
- package/install.sh +15 -4
- package/lib/agents.mjs +2 -1
- package/lib/connect.mjs +41 -9
- package/lib/diff-checks.mjs +18 -0
- package/lib/dispatch-schema.mjs +2 -1
- package/lib/doctor.mjs +15 -7
- package/lib/execute.mjs +4 -4
- package/lib/notify.mjs +30 -3
- package/lib/openclaw-path.mjs +28 -0
- package/lib/openclaw-run.mjs +2 -1
- package/lib/path-utils.mjs +4 -0
- package/lib/registry-python.mjs +3 -1
- package/lib/registry-secrets.mjs +16 -4
- package/lib/same-path.mjs +14 -0
- package/lib/sandbox-images.mjs +20 -7
- package/lib/scan.mjs +5 -1
- package/lib/share.mjs +82 -0
- package/lib/stats.mjs +2 -1
- package/lib/thinking.mjs +23 -0
- package/lib/user-config.mjs +106 -0
- package/lib/validators.mjs +41 -0
- package/lib/wsl-cli.mjs +152 -0
- package/lib/wsl.mjs +230 -0
- package/mcp/server.mjs +38 -16
- package/package.json +1 -1
- package/playbooks/feature.md +1 -1
- package/scripts/configure-openclaw.sh +4 -2
- package/scripts/lib.sh +9 -2
- package/scripts/select-model.mjs +12 -5
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<a href="https://github.com/rayson-tech/nomarmy/actions/workflows/ci.yml"><img src="https://github.com/rayson-tech/nomarmy/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
5
5
|
<a href="https://www.npmjs.com/package/nomarmy"><img src="https://img.shields.io/npm/v/nomarmy/alpha?label=npm%40alpha" alt="npm"></a>
|
|
6
6
|
<a href="https://github.com/rayson-tech/nomarmy/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License: Apache 2.0"></a>
|
|
7
|
+
<a href="https://github.com/rayson-tech/nomarmy/blob/main/docs/feature-runs.md"><img src="https://raw.githubusercontent.com/rayson-tech/nomarmy/main/.github/nomarmy-badge.svg" alt="nomArmy: AI worker claims checked on this repository"></a>
|
|
7
8
|
</p>
|
|
8
9
|
|
|
9
10
|
<p align="center"><em>Every byte verified.</em> ๐ช</p>
|
|
@@ -15,6 +16,7 @@ AI coding workers are confident. Their "done, all tests pass" is a claim, not ev
|
|
|
15
16
|
## TL;DR
|
|
16
17
|
|
|
17
18
|
1. **Have** Git, Node 24.16+ (or 26.1+) and [Podman](https://podman.io). On macOS, give Podman 8 GiB: `brew install podman && podman machine init --memory 8192 && podman machine start`.
|
|
19
|
+
Windows is supported with the engine in WSL2. Follow the [Windows setup](https://github.com/rayson-tech/nomarmy/blob/main/docs/install.md#windows).
|
|
18
20
|
2. **Install and set up:**
|
|
19
21
|
```bash
|
|
20
22
|
npm install -g nomarmy@alpha
|
|
@@ -50,6 +52,8 @@ Stuck? `nomarmy doctor` checks the machine and `nomarmy health` checks everythin
|
|
|
50
52
|
|
|
51
53
|
Failing verification stays failed, unconditionally. A malformed report isn't automatically a failure: if the repository changed, nomArmy verifies independently and may recover the work. And the checks aren't the General's to waive: a repo's `.nomarmy.yml` policy (on by default for new repos) makes verification and the revert check mandatory for every job.
|
|
52
54
|
|
|
55
|
+
**Showing what was checked.** When a `/feature` run finishes, the General gets a "Verified by nomArmy" block for the pull request: how many "done, tests pass" claims held up, how many nomArmy caught and why, and the new tests shown to fail without their change. `nomarmy stats --share` prints the same for any period, and `nomarmy stats --badge` writes a README badge like `nomArmy | 78 claims checked ยท 11 caught`. Every number comes from the verified records.
|
|
56
|
+
|
|
53
57
|
**Checking without building** costs nothing: `mode: verify` runs a verification profile against any branch, with no worker and no model tokens.
|
|
54
58
|
|
|
55
59
|
**Want deeper checks?** Three optional [validators](https://github.com/rayson-tech/nomarmy/blob/main/docs/validators.md) go further, each only adding review flags: mutation testing (do the tests pin down the changed lines?), Jev (do a scout's citations support its findings, does a report match its diff?) and a model judge (acceptance criteria, weakened tests).
|
|
@@ -71,7 +75,7 @@ Developed and maintained by Rayson Technologies. This is an alpha (`0.1.0-alpha`
|
|
|
71
75
|
|
|
72
76
|
| | |
|
|
73
77
|
|---|---|
|
|
74
|
-
| [Install](https://github.com/rayson-tech/nomarmy/blob/main/docs/install.md) | Every setup: hosted (API keys and subscriptions), a local model on macOS, Linux, Windows or DGX Spark, a shared model server, Bedrock |
|
|
78
|
+
| [Install](https://github.com/rayson-tech/nomarmy/blob/main/docs/install.md) | Every setup: hosted (API keys and subscriptions), a local model on macOS, Linux, [Windows with the engine in WSL2](https://github.com/rayson-tech/nomarmy/blob/main/docs/install.md#windows) or DGX Spark, a shared model server, Bedrock |
|
|
75
79
|
| [Example setup](https://github.com/rayson-tech/nomarmy/blob/main/docs/setup/example.md) | Claude Code, Codex and an API key, command by command |
|
|
76
80
|
| [Agents and the army](https://github.com/rayson-tech/nomarmy/blob/main/docs/agents-and-army.md) | Where a job can run, who does what, usage limits, picking an agent |
|
|
77
81
|
| [`/feature` runs](https://github.com/rayson-tech/nomarmy/blob/main/docs/feature-runs.md) | A feature end to end, and watching what nomArmy is doing |
|
package/bin/nomarmy.mjs
CHANGED
|
@@ -18,11 +18,13 @@ import { buildConfigProposal } from "../lib/propose.mjs";
|
|
|
18
18
|
import { detectHardware } from "../lib/hardware.mjs";
|
|
19
19
|
import { readGGUFMetadata, resolveModelPath, totalSplitBytes } from "../lib/gguf.mjs";
|
|
20
20
|
import { recommend, customRecommendation, evaluateConfig, bytesPerKvElementForCacheTypes, MIN_CONTEXT_PER_NOM } from "../lib/sizing.mjs";
|
|
21
|
-
import {
|
|
21
|
+
import { windowsFrontEnd, dropWindowsPath, markWindowsCoordinator, pickDistro, resolveWslNomarmy, mcpBridgeLaunch, writeWindowsSettings } from "../lib/wsl.mjs";
|
|
22
|
+
import { windowsPlan, windowsForward, windowsSetup, windowsDoctor } from "../lib/wsl-cli.mjs";
|
|
23
|
+
import { connectViaWsl, connectClaude, connectCodex, connectCursor, cursorAlreadyConnected, deriveWorkerModelEnv, defaultInstallDir, installMcpCopy, SCOPES, claudeUserScoped, portableServerLaunch } from "../lib/connect.mjs";
|
|
22
24
|
import { compareVersions, readPackageVersion, readInstallVersions, copyIsStale } from "../lib/install-freshness.mjs";
|
|
23
25
|
import { loadJobRecords, computeStats, formatStats, formatStatsSummary, parseSince, resolveRepo, agentLookup } from "../lib/stats.mjs";
|
|
24
26
|
import { requestJobStop } from "../lib/openclaw-run.mjs";
|
|
25
|
-
import { loadValidators, saveJevKey, removeJev, jevSettings, askJev, validatorsPath, JEV_CHECKS, saveJudge, removeJudge, judgeSettings } from "../lib/validators.mjs";
|
|
27
|
+
import { loadValidators, saveJevKey, removeJev, jevSettings, askJev, validatorsPath, JEV_CHECKS, saveJudge, removeJudge, judgeSettings, judgeAgentChoices, chooseJudgeAgent, confirmJudgeHostTools } from "../lib/validators.mjs";
|
|
26
28
|
import { probeModel } from "../lib/model-probe.mjs";
|
|
27
29
|
import { ID_RE, AUTH_ENV_NAME_RE, OPENCLAW_PROVIDER_ID_RE, openclawProviderId, isNativeProviderType } from "../lib/dispatch-schema.mjs";
|
|
28
30
|
import { loadAgents, readAgentsFile, writeAgentsFile, agentsConfigPath, apiAgentAsPoolEntry, describeAgent as describeAgentLabel, agentRunsToolsOnHost, agentProviderId, AGENT_KINDS, API_PROVIDER_TYPES, RESERVED_AGENT_NAMES, BUILTIN_LOCAL_AGENT } from "../lib/agents.mjs";
|
|
@@ -32,12 +34,30 @@ import { setupSteps, formatSetupSteps, runSetupPlaybook } from "../lib/setup-ste
|
|
|
32
34
|
import { readUsageSnapshots } from "../lib/usage-limits.mjs";
|
|
33
35
|
import { pickMachine, planResize } from "../lib/sandbox-vm.mjs";
|
|
34
36
|
import { listProcesses, staleSessions, formatStaleSessions } from "../lib/stale-sessions.mjs";
|
|
37
|
+
import { readSetting, writeSetting, writeEnvLine, userCommonPath, userProfilePath, profilePathFor, tildePath } from "../lib/user-config.mjs";
|
|
35
38
|
import { MIN_PODMAN_VM_MB } from "../lib/doctor.mjs";
|
|
36
39
|
import { liveLeases } from "../lib/slots.mjs";
|
|
37
40
|
import { ensureProviderConfig } from "../lib/openclaw-config.mjs";
|
|
38
41
|
import { recordProbeSuccess } from "../lib/health.mjs";
|
|
39
42
|
import { pruneJobRuntime } from "../lib/prune.mjs";
|
|
40
43
|
import { SUBSCRIPTION_VENDORS, parseOpenclawVersion, versionAtLeast, parseCatalogModels, parseCliLoginStatus, probeOutcome, parseMuseAuthDescriptor, extractMintedKey } from "../lib/subscription-setup.mjs";
|
|
44
|
+
import { ensureOpenClawOnPath } from "../lib/openclaw-path.mjs";
|
|
45
|
+
import { THINKING_LEVELS } from "../lib/thinking.mjs";
|
|
46
|
+
import { fileURLToPath } from "node:url";
|
|
47
|
+
// Inside WSL, only the distro's own tools (see lib/wsl.mjs).
|
|
48
|
+
dropWindowsPath();
|
|
49
|
+
// The Windows front end: whatever it forwards knows the coordinators are on Windows.
|
|
50
|
+
if (windowsFrontEnd()) markWindowsCoordinator();
|
|
51
|
+
// OpenClaw in ~/.npm-global/bin (no writable npm prefix) is found without the operator editing PATH.
|
|
52
|
+
ensureOpenClawOnPath();
|
|
53
|
+
// Windows starts the engine as `wsl.exe --exec <node> nomarmy.mjs`, with no
|
|
54
|
+
// login shell, so an nvm Node's directory isn't on PATH. Children that need
|
|
55
|
+
// `node` (install.sh, OpenClaw's `#!/usr/bin/env node`) get the one running.
|
|
56
|
+
{
|
|
57
|
+
const nodeDir = path.dirname(process.execPath);
|
|
58
|
+
const entries = (process.env.PATH ?? "").split(path.delimiter);
|
|
59
|
+
if (!entries.includes(nodeDir)) process.env.PATH = [nodeDir, ...entries.filter(Boolean)].join(path.delimiter);
|
|
60
|
+
}
|
|
41
61
|
|
|
42
62
|
// Add a new coordinator: add its name here, teach commandExists/connectTarget
|
|
43
63
|
// about it below (a JSON-file target like Cursor has no PATH binary to check
|
|
@@ -67,7 +87,7 @@ const repoDir = path.resolve(value("repo", process.cwd()));
|
|
|
67
87
|
// through the job's requested reasoning); `--no-thinking` is always false.
|
|
68
88
|
// Returns undefined when none of these flags were passed at all, so the
|
|
69
89
|
// caller can tell "not touched" apart from "explicitly set".
|
|
70
|
-
|
|
90
|
+
// Every level OpenClaw accepts (lib/thinking.mjs).
|
|
71
91
|
function resolveThinkingFlag() {
|
|
72
92
|
const level = value("thinking");
|
|
73
93
|
if (level && THINKING_LEVELS.includes(level)) return level;
|
|
@@ -155,7 +175,7 @@ Usage: nomarmy <command> [options]
|
|
|
155
175
|
[--update-mcp] (api); --provider --model
|
|
156
176
|
--owner (subscription); and optionally
|
|
157
177
|
--max-concurrent --context-window
|
|
158
|
-
--thinking [low|medium|high] --no-thinking
|
|
178
|
+
--thinking [minimal|low|medium|high|xhigh|adaptive|max|ultra] --no-thinking
|
|
159
179
|
update <name>
|
|
160
180
|
change the model (picked from what OpenClaw
|
|
161
181
|
lists; a subscription gets a real test call),
|
|
@@ -239,7 +259,7 @@ Usage: nomarmy <command> [options]
|
|
|
239
259
|
project for this repository, committed for the team
|
|
240
260
|
(.mcp.json or .cursor/mcp.json, running \`nomarmy mcp\`).
|
|
241
261
|
Codex has only the user scope.
|
|
242
|
-
stats [--since 7d|<date>] [--until <date>] [--role <role>] [--model <model>] [--details] [--all-suggestions]
|
|
262
|
+
stats [--since 7d|<date>] [--until <date>] [--role <role>] [--model <model>] [--run <id>] [--details] [--all-suggestions] [--share] [--badge [path]]
|
|
243
263
|
[--repo <path|name>] [--all-repos] [--json]
|
|
244
264
|
What nomArmy's job records show for this repository (or
|
|
245
265
|
all): volume by role and model, code committed, time,
|
|
@@ -248,7 +268,9 @@ Usage: nomarmy <command> [options]
|
|
|
248
268
|
and review flags. From verified records, never reports.
|
|
249
269
|
validators <list|add jev|test jev|remove jev|add judge|test judge|remove judge>
|
|
250
270
|
Optional semantic checks from a model you configure with
|
|
251
|
-
your own key.
|
|
271
|
+
your own key. Jev checks focused claims with TypeSafe;
|
|
272
|
+
Judge reviews diffs with one of your configured agents.
|
|
273
|
+
Setup asks for any consent it needs. \`add jev\` asks for the
|
|
252
274
|
key without echoing it (or reads --key-stdin), saves it
|
|
253
275
|
where only you can read it, and makes one test call. Its
|
|
254
276
|
answers only add review flags, and it sends excerpts of
|
|
@@ -459,7 +481,9 @@ async function cmdInit() {
|
|
|
459
481
|
// This file's own location, not --repo (the target repo being scanned) --
|
|
460
482
|
// setup/model need to find THIS package's config/ and scripts/ as siblings
|
|
461
483
|
// of bin/, the same way select-model.mjs resolves its own root.
|
|
462
|
-
|
|
484
|
+
// fileURLToPath, not URL.pathname: on Windows that gave "/C:/Users/Jason%20Pugh/...",
|
|
485
|
+
// a stray slash and an encoded space, and connect failed copying from C:\\C:\\...
|
|
486
|
+
const nomarmyRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
463
487
|
|
|
464
488
|
/** Read one KEY=VALUE line's value, or null if the file or key doesn't exist. */
|
|
465
489
|
function readEnvValue(filePath, key) {
|
|
@@ -468,15 +492,9 @@ function readEnvValue(filePath, key) {
|
|
|
468
492
|
return m ? m[1].trim() : null;
|
|
469
493
|
}
|
|
470
494
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const line = `${key}=${value}`;
|
|
475
|
-
const updated = new RegExp(`^${key}=.*$`, "m").test(existingText)
|
|
476
|
-
? existingText.replace(new RegExp(`^${key}=.*$`, "m"), line)
|
|
477
|
-
: `${existingText.trimEnd()}\n${line}\n`.replace(/^\n/, "");
|
|
478
|
-
fs.writeFileSync(filePath, updated);
|
|
479
|
-
}
|
|
495
|
+
// Your settings live in ~/.config/nomarmy/, over the package's config/
|
|
496
|
+
// defaults, so an update can't reset them (lib/user-config.mjs).
|
|
497
|
+
const setting = (key) => readSetting(key, { nomarmyRoot });
|
|
480
498
|
|
|
481
499
|
// Each entry's repo/quant/alias is verified against this project's own real
|
|
482
500
|
// usage (downloaded, loaded, dispatched against), not guessed from a model
|
|
@@ -495,6 +513,14 @@ function writeEnvLine(filePath, key, value) {
|
|
|
495
513
|
* not left to fail only at the very end via schema validation with no
|
|
496
514
|
* indication of which of several answers was the problem.
|
|
497
515
|
*/
|
|
516
|
+
/** A numbered choice, asked again until it's one of 1..count (an empty or mistyped answer used to end the whole flow). */
|
|
517
|
+
async function askChoice(rl, count) {
|
|
518
|
+
const answer = await askUntilValid(rl, "Choice: ", { pattern: new RegExp(`^([1-9]|[1-9][0-9])$`), invalidMessage: `Choose a number from 1 to ${count}.` });
|
|
519
|
+
if (Number(answer) >= 1 && Number(answer) <= count) return Number(answer) - 1;
|
|
520
|
+
console.log(c.red(` โ Choose a number from 1 to ${count}.`));
|
|
521
|
+
return askChoice(rl, count);
|
|
522
|
+
}
|
|
523
|
+
|
|
498
524
|
async function askUntilValid(rl, prompt, { pattern, invalidMessage, allowEmpty = false, fallback = "" }) {
|
|
499
525
|
for (;;) {
|
|
500
526
|
const answer = (await rl.question(c.bold(prompt))).trim();
|
|
@@ -527,15 +553,15 @@ async function askSecret(rl, prompt) {
|
|
|
527
553
|
|
|
528
554
|
const KNOWN_MODELS = {
|
|
529
555
|
default: {
|
|
530
|
-
label: "Qwen3-Coder-Next
|
|
556
|
+
label: "Qwen3-Coder-Next ยท 48 GB download ยท the most reliable in our tests (recommended)",
|
|
531
557
|
repo: "Qwen/Qwen3-Coder-Next-GGUF", quant: "Q4_K_M", alias: "qwen3-coder-next", thinking: false, recommended: true,
|
|
532
558
|
},
|
|
533
559
|
"gpt-oss-20b": {
|
|
534
|
-
label: "gpt-oss-20b
|
|
560
|
+
label: "gpt-oss-20b ยท 12 GB download ยท the fastest; keep reasoning at medium",
|
|
535
561
|
repo: "ggml-org/gpt-oss-20b-GGUF", quant: "MXFP4", alias: "gpt-oss-20b", thinking: true,
|
|
536
562
|
},
|
|
537
563
|
"qwen3.6-27b": {
|
|
538
|
-
label: "Qwen3.6-27B
|
|
564
|
+
label: "Qwen3.6-27B ยท 17 GB download ยท very reliable, slower; keep reasoning at medium",
|
|
539
565
|
repo: "unsloth/Qwen3.6-27B-GGUF", quant: "Q4_K_M", alias: "qwen3.6-27b", thinking: true,
|
|
540
566
|
},
|
|
541
567
|
};
|
|
@@ -588,14 +614,14 @@ function defaultLocalProfile() {
|
|
|
588
614
|
// `install`: the one setup recorded, else the install marker's, else the
|
|
589
615
|
// execution mode's, else (a working local install) install.sh's default.
|
|
590
616
|
function setupProfileState() {
|
|
591
|
-
const common =
|
|
592
|
-
const chosen =
|
|
617
|
+
const common = userCommonPath();
|
|
618
|
+
const chosen = setting("NOMARMY_SETUP_PROFILE");
|
|
593
619
|
const probeCommand = (binary, args) => {
|
|
594
620
|
const result = spawnSync(binary, args, { encoding: "utf8", timeout: 10000 });
|
|
595
621
|
return result.status === 0 ? result.stdout.trim() : "";
|
|
596
622
|
};
|
|
597
|
-
const profileFile = chosen
|
|
598
|
-
const root = (process.env.NOMARMY_INSTALL_ROOT || (profileFile && readEnvValue(profileFile, "NOMARMY_INSTALL_ROOT")) ||
|
|
623
|
+
const profileFile = profilePathFor(chosen, { nomarmyRoot });
|
|
624
|
+
const root = (process.env.NOMARMY_INSTALL_ROOT || (profileFile && readEnvValue(profileFile, "NOMARMY_INSTALL_ROOT")) || setting("NOMARMY_INSTALL_ROOT") || "$HOME/.local/share/nomarmy-local-agents").replace(/\$HOME|\$\{HOME\}/g, os.homedir());
|
|
599
625
|
let marker = null;
|
|
600
626
|
try { marker = JSON.parse(fs.readFileSync(path.join(root, "install.json"), "utf8")); } catch (error) { if (error.code !== "ENOENT") marker = {}; }
|
|
601
627
|
const version = probeCommand("openclaw", ["--version"]);
|
|
@@ -603,7 +629,7 @@ function setupProfileState() {
|
|
|
603
629
|
const registered = !marker && Boolean(version) && ["claude", "codex"].some((name) => spawnSync(name, ["mcp", "get", "nomarmy-local-worker"], { stdio: "ignore", timeout: 10000 }).status === 0);
|
|
604
630
|
// An install from before setup recorded its profile: the marker's, else the
|
|
605
631
|
// execution mode's, else (a working local install) install.sh's default.
|
|
606
|
-
const execution =
|
|
632
|
+
const execution = setting("NOMARMY_EXECUTION");
|
|
607
633
|
const profile = chosen ?? marker?.profile
|
|
608
634
|
?? (["hosted", "remote", "bedrock"].includes(execution) ? execution : null)
|
|
609
635
|
?? (registered ? defaultLocalProfile() : null);
|
|
@@ -614,7 +640,7 @@ function setupChecklist() {
|
|
|
614
640
|
const { common, profile, marker, version, registered } = setupProfileState();
|
|
615
641
|
const project = setupProjectDir();
|
|
616
642
|
return setupSteps({
|
|
617
|
-
mode: () => ({ profile, host:
|
|
643
|
+
mode: () => ({ profile, host: setting("NOMARMY_LLAMA_HOST"), port: setting("NOMARMY_LLAMA_PORT") }),
|
|
618
644
|
install: () => ({ marker, version, registered }),
|
|
619
645
|
agents: () => Object.keys(loadAgents(globalConfigDir()).agents),
|
|
620
646
|
army: () => {
|
|
@@ -647,11 +673,27 @@ function cmdInstall() {
|
|
|
647
673
|
}
|
|
648
674
|
|
|
649
675
|
async function cmdSetup() {
|
|
676
|
+
if (windowsFrontEnd()) {
|
|
677
|
+
process.exitCode = await windowsSetup(argv, { ask: async (prompt) => {
|
|
678
|
+
const rl = createInterface({ input, output });
|
|
679
|
+
try { return await rl.question(prompt); } finally { rl.close(); }
|
|
680
|
+
} });
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
650
683
|
if (flag("status") || (!flag("choose") && !flag("hosted") && !flag("llama-url") && !json)) {
|
|
651
684
|
if (flag("status") || !process.stdin.isTTY) {
|
|
652
685
|
const steps = setupChecklist();
|
|
653
686
|
return json ? out(steps) : console.log(formatSetupSteps(steps));
|
|
654
687
|
}
|
|
688
|
+
// What's ahead, before the first question: a practice run found setup
|
|
689
|
+
// felt heavy mostly because nothing said how much of it there was.
|
|
690
|
+
if (setupChecklist().find((step) => step.status === "todo")?.id === "mode") {
|
|
691
|
+
console.log(`${c.bold("๐ช nomArmy setup")}\n\nAbout 10 to 20 minutes, one step at a time; stop any time and ${c.cyan("nomarmy setup")} picks up where you left off.`);
|
|
692
|
+
console.log(c.dim(" 1. Where models run: your API and subscription agents, or a model on this machine"));
|
|
693
|
+
console.log(c.dim(" 2. Install: OpenClaw (runs the models) and Podman (the sandbox every job runs in; on macOS a small VM, about a 1 GB download)"));
|
|
694
|
+
console.log(c.dim(" 3. Agents: log in to your subscription or add an API key"));
|
|
695
|
+
console.log(c.dim(" 4. Roles, this repo's checks, and a final health check\n"));
|
|
696
|
+
}
|
|
655
697
|
process.exitCode = await runSetupPlaybook({
|
|
656
698
|
evaluate: setupChecklist, print: console.log, run: runSetupChild,
|
|
657
699
|
ask: async (prompt) => {
|
|
@@ -670,10 +712,8 @@ async function cmdSetup() {
|
|
|
670
712
|
if (choice === "1") argv.push("--hosted");
|
|
671
713
|
if (choice === "3") argv.push("--llama-url", (await rl.question("Server URL: ")).trim());
|
|
672
714
|
if (choice === "4") {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
writeEnvLine(common, "NOMARMY_EXECUTION", "bedrock");
|
|
676
|
-
writeEnvLine(common, "NOMARMY_SETUP_PROFILE", "bedrock");
|
|
715
|
+
writeSetting("NOMARMY_EXECUTION", "bedrock");
|
|
716
|
+
writeSetting("NOMARMY_SETUP_PROFILE", "bedrock");
|
|
677
717
|
console.log("Next: nomarmy install");
|
|
678
718
|
return;
|
|
679
719
|
}
|
|
@@ -684,7 +724,7 @@ async function cmdSetup() {
|
|
|
684
724
|
if (hosted && hasLlamaUrl) throw new Error("--hosted and --llama-url cannot be used together.");
|
|
685
725
|
|
|
686
726
|
if (hosted || hasLlamaUrl) {
|
|
687
|
-
const commonPath =
|
|
727
|
+
const commonPath = userCommonPath();
|
|
688
728
|
|
|
689
729
|
if (hosted) {
|
|
690
730
|
const next = [
|
|
@@ -692,11 +732,10 @@ async function cmdSetup() {
|
|
|
692
732
|
"nomarmy agents add",
|
|
693
733
|
"nomarmy army init --agent <name>",
|
|
694
734
|
];
|
|
695
|
-
fs.mkdirSync(path.dirname(commonPath), { recursive: true });
|
|
696
735
|
writeEnvLine(commonPath, "NOMARMY_EXECUTION", "hosted");
|
|
697
736
|
writeEnvLine(commonPath, "NOMARMY_SETUP_PROFILE", "hosted");
|
|
698
737
|
if (json) return out({ written: commonPath, execution: "hosted", next });
|
|
699
|
-
console.log(c.green(`โ
|
|
738
|
+
console.log(c.green(`โ Jobs will run on your API and subscription agents (saved in ${tildePath(commonPath)}).`));
|
|
700
739
|
console.log(c.dim("\nNext:"));
|
|
701
740
|
for (const step of next) console.log(` ${c.bold(step)}`);
|
|
702
741
|
return;
|
|
@@ -724,7 +763,7 @@ async function cmdSetup() {
|
|
|
724
763
|
console.log(reachable
|
|
725
764
|
? c.green(`โ llama-server is reachable at ${healthUrl}.`)
|
|
726
765
|
: c.yellow(`โ llama-server is not reachable at ${healthUrl} right now; configuration was still written.`));
|
|
727
|
-
console.log(c.green(`โ Wrote the remote llama-server settings to ${
|
|
766
|
+
console.log(c.green(`โ Wrote the remote llama-server settings to ${tildePath(commonPath)}.`));
|
|
728
767
|
console.log(c.dim("\nNext:"));
|
|
729
768
|
console.log(` ${c.bold(next)}`);
|
|
730
769
|
return;
|
|
@@ -753,10 +792,10 @@ async function cmdSetup() {
|
|
|
753
792
|
console.log(isCloud
|
|
754
793
|
? `Execution is '${execution}' -- hosted inference, local hardware does not bound this.\n`
|
|
755
794
|
: `Hardware: ${c.cyan(`${hardware.platform}/${hardware.arch}`)}, ${hardware.cpu?.logicalCores ?? "?"} logical cores, ${(hardware.memory?.totalBytes / 1024 ** 3).toFixed(1)} GiB RAM\n`);
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
795
|
+
// Plain words; the exact settings are shown before anything is written.
|
|
796
|
+
const plain = (env) => { const n = Number(env?.NOMARMY_MAX_WORKERS ?? env?.NOMARMY_LLAMA_PARALLEL ?? 1), ctx = Math.round(Number(env?.NOMARMY_LLAMA_CONTEXT ?? 0) / Math.max(1, Number(env?.NOMARMY_LLAMA_PARALLEL ?? 1)) / 1024); return `${n} local job${n === 1 ? "" : "s"} at a time${ctx ? `, ${ctx}K tokens of context each` : ""}`; };
|
|
797
|
+
console.log(`This machine fits ${c.green(plain(res.env))}${res.confidence === "low" ? c.dim(" (an estimate)") : ""}.`);
|
|
798
|
+
if (res.nominal && !res.nominal.fits) console.log(c.red("Even one local job doesn't fit in this machine's memory: choose hosted instead (nomarmy setup --choose)."));
|
|
760
799
|
}
|
|
761
800
|
|
|
762
801
|
// "More noms" fits as many noms as memory allows; "nominal" is 1 worker
|
|
@@ -774,8 +813,8 @@ async function cmdSetup() {
|
|
|
774
813
|
if (sizingTier !== "more" && sizingTier !== "nominal") throw new Error('--tier must be "more" or "nominal".');
|
|
775
814
|
} else {
|
|
776
815
|
console.log(`\n${c.bold("Which sizing?")}`);
|
|
777
|
-
console.log(` ${c.cyan("1.")}
|
|
778
|
-
console.log(` ${c.cyan("2.")}
|
|
816
|
+
console.log(` ${c.cyan("1.")} As many at once as fit in memory`);
|
|
817
|
+
console.log(` ${c.cyan("2.")} One at a time (lighter on the machine)`);
|
|
779
818
|
const choice = (await rl.question(c.bold("Choice [1]: "))).trim() || "1";
|
|
780
819
|
sizingTier = choice === "2" ? "nominal" : "more";
|
|
781
820
|
}
|
|
@@ -794,8 +833,8 @@ async function cmdSetup() {
|
|
|
794
833
|
}
|
|
795
834
|
|
|
796
835
|
const profileName = nonInteractive ? value("profile-name") : (await rl.question(`\nProfile name [${hardware?.appleSilicon ? "macbook-pro" : "custom"}]: `)).trim() || (hardware?.appleSilicon ? "macbook-pro" : "custom");
|
|
797
|
-
const profilePath =
|
|
798
|
-
const commonPath =
|
|
836
|
+
const profilePath = userProfilePath(profileName);
|
|
837
|
+
const commonPath = userCommonPath();
|
|
799
838
|
|
|
800
839
|
const profileWrites = { ...sizingEnv };
|
|
801
840
|
if (!isCloud) {
|
|
@@ -808,10 +847,10 @@ async function cmdSetup() {
|
|
|
808
847
|
}
|
|
809
848
|
|
|
810
849
|
if (!json) {
|
|
811
|
-
console.log(c.bold(`\nAbout to write ${
|
|
850
|
+
console.log(c.bold(`\nAbout to write ${tildePath(profilePath)}:`));
|
|
812
851
|
for (const [k, v] of Object.entries(profileWrites)) console.log(c.dim(` ${k}=${v}`));
|
|
813
852
|
if (model?.kind === "known") {
|
|
814
|
-
console.log(c.bold(`\nAnd ${
|
|
853
|
+
console.log(c.bold(`\nAnd ${tildePath(commonPath)}:`));
|
|
815
854
|
console.log(c.dim(` NOMARMY_MODEL_REPO=${model.repo}`));
|
|
816
855
|
console.log(c.dim(` NOMARMY_MODEL_QUANT=${model.quant}`));
|
|
817
856
|
console.log(c.dim(` NOMARMY_MODEL_ALIAS=${model.alias}`));
|
|
@@ -819,8 +858,9 @@ async function cmdSetup() {
|
|
|
819
858
|
console.log(c.dim(` NOMARMY_MODEL_THINKING=${model.thinking}`));
|
|
820
859
|
}
|
|
821
860
|
if (!nonInteractive) {
|
|
822
|
-
|
|
823
|
-
|
|
861
|
+
// Yes by default: someone who accepted every suggestion shouldn't lose it all at the last Enter.
|
|
862
|
+
const answer = (await rl.question(c.bold("\nWrite this configuration? [Y/n] "))).trim().toLowerCase();
|
|
863
|
+
if (answer === "n" || answer === "no") { console.log(c.dim("Canceled; nothing written.")); return; }
|
|
824
864
|
}
|
|
825
865
|
}
|
|
826
866
|
|
|
@@ -842,7 +882,7 @@ async function cmdSetup() {
|
|
|
842
882
|
writeEnvLine(commonPath, "NOMARMY_WORKER_MODEL", model.alias);
|
|
843
883
|
writeEnvLine(commonPath, "NOMARMY_MODEL_THINKING", String(model.thinking));
|
|
844
884
|
} else if (model?.kind === "search") {
|
|
845
|
-
const searchedAlias =
|
|
885
|
+
const searchedAlias = setting("NOMARMY_MODEL_ALIAS");
|
|
846
886
|
if (searchedAlias) {
|
|
847
887
|
writeEnvLine(commonPath, "NOMARMY_WORKER_MODEL", searchedAlias);
|
|
848
888
|
writeEnvLine(commonPath, "NOMARMY_MODEL_THINKING", String(model.thinking));
|
|
@@ -851,7 +891,7 @@ async function cmdSetup() {
|
|
|
851
891
|
|
|
852
892
|
const installCmd = "nomarmy install";
|
|
853
893
|
if (json) return out({ written: { profile: profilePath, common: model?.kind === "known" ? commonPath : null }, env: profileWrites, sizingTier, installCommand: installCmd });
|
|
854
|
-
console.log(c.green(`\nโ Wrote ${
|
|
894
|
+
console.log(c.green(`\nโ Wrote ${tildePath(profilePath)}${model?.kind === "known" ? ` and ${tildePath(commonPath)}` : ""}.`));
|
|
855
895
|
console.log(c.dim("\nThis proposes; it does not install. Run:\n"));
|
|
856
896
|
console.log(` ${c.bold(installCmd)}\n`);
|
|
857
897
|
} finally {
|
|
@@ -880,7 +920,7 @@ function restartInference() {
|
|
|
880
920
|
runScript("start-inference.sh", []);
|
|
881
921
|
}
|
|
882
922
|
async function cmdModel() {
|
|
883
|
-
const commonPath =
|
|
923
|
+
const commonPath = userCommonPath();
|
|
884
924
|
if (json) {
|
|
885
925
|
const which = value("model");
|
|
886
926
|
if (!KNOWN_MODELS[which]) throw new Error(`--json requires --model one of ${Object.keys(KNOWN_MODELS).join(", ")} (Hugging Face search is interactive-only).`);
|
|
@@ -904,17 +944,17 @@ async function cmdModel() {
|
|
|
904
944
|
const model = await chooseModel(rl);
|
|
905
945
|
let alias;
|
|
906
946
|
if (model.kind === "search") {
|
|
907
|
-
console.log(c.green("\nโ Done") +
|
|
908
|
-
alias =
|
|
947
|
+
console.log(c.green("\nโ Done") + ` -- ${tildePath(commonPath)} was already updated by the search above.`);
|
|
948
|
+
alias = setting("NOMARMY_MODEL_ALIAS");
|
|
909
949
|
} else {
|
|
910
|
-
console.log(c.bold(`\nAbout to write ${
|
|
950
|
+
console.log(c.bold(`\nAbout to write ${tildePath(commonPath)}:`));
|
|
911
951
|
console.log(c.dim(` NOMARMY_MODEL_REPO=${model.repo}\n NOMARMY_MODEL_QUANT=${model.quant}\n NOMARMY_MODEL_ALIAS=${model.alias}`));
|
|
912
952
|
const answer = (await rl.question(c.bold("\nApply this model configuration? [y/N] "))).trim().toLowerCase();
|
|
913
953
|
if (answer !== "y") { console.log(c.dim("Canceled; nothing changed.")); return; }
|
|
914
954
|
writeEnvLine(commonPath, "NOMARMY_MODEL_REPO", model.repo);
|
|
915
955
|
writeEnvLine(commonPath, "NOMARMY_MODEL_QUANT", model.quant);
|
|
916
956
|
writeEnvLine(commonPath, "NOMARMY_MODEL_ALIAS", model.alias);
|
|
917
|
-
console.log(c.green(`โ Wrote ${
|
|
957
|
+
console.log(c.green(`โ Wrote ${tildePath(commonPath)}.`));
|
|
918
958
|
alias = model.alias;
|
|
919
959
|
}
|
|
920
960
|
if (alias) {
|
|
@@ -1371,8 +1411,7 @@ async function cmdAgentsAdd() {
|
|
|
1371
1411
|
console.log(` ${c.cyan("1.")} local ${c.dim("the local model on this machine (no per-token bill, private; slower)")}`);
|
|
1372
1412
|
console.log(` ${c.cyan("2.")} api ${c.dim("a metered API key (xAI, OpenAI, Anthropic, DeepSeek, ...)")}`);
|
|
1373
1413
|
console.log(` ${c.cyan("3.")} subscription ${c.dim("your own Claude, ChatGPT or Muse Code plan (never shared)")}`);
|
|
1374
|
-
kind = AGENT_KINDS[
|
|
1375
|
-
if (!kind) throw new Error("Not a valid choice.");
|
|
1414
|
+
kind = AGENT_KINDS[await askChoice(rl, AGENT_KINDS.length)];
|
|
1376
1415
|
}
|
|
1377
1416
|
if (kind === "local") return await addLocalAgent(rl, agents);
|
|
1378
1417
|
if (kind === "api") return await addApiAgent(rl, agents);
|
|
@@ -1409,8 +1448,7 @@ async function addLocalAgent(rl, agents) {
|
|
|
1409
1448
|
async function addApiAgent(rl, agents) {
|
|
1410
1449
|
console.log("\n" + c.bold("Which provider?"));
|
|
1411
1450
|
API_PROVIDER_TYPES.forEach((t, i) => console.log(` ${c.cyan(`${i + 1}.`)} ${KNOWN_PROVIDERS[t]?.label ?? t}`));
|
|
1412
|
-
const provider = API_PROVIDER_TYPES[
|
|
1413
|
-
if (!provider) throw new Error("Not a valid choice.");
|
|
1451
|
+
const provider = API_PROVIDER_TYPES[await askChoice(rl, API_PROVIDER_TYPES.length)];
|
|
1414
1452
|
const info = KNOWN_PROVIDERS[provider] ?? {};
|
|
1415
1453
|
const agent = { kind: "api", provider };
|
|
1416
1454
|
|
|
@@ -1485,8 +1523,7 @@ async function addSubscriptionAgent(rl, agents) {
|
|
|
1485
1523
|
if (!SUBSCRIPTION_VENDORS[vendorKey]) {
|
|
1486
1524
|
console.log("\n" + c.bold("Which subscription?"));
|
|
1487
1525
|
vendorKeys.forEach((k, i) => console.log(` ${c.cyan(`${i + 1}.`)} ${SUBSCRIPTION_VENDORS[k].label}`));
|
|
1488
|
-
vendorKey = vendorKeys[
|
|
1489
|
-
if (!vendorKey) throw new Error(`Not a valid choice. Supported: ${vendorKeys.join(", ")}. DeepSeek and others without a plan are api agents.`);
|
|
1526
|
+
vendorKey = vendorKeys[await askChoice(rl, vendorKeys.length)];
|
|
1490
1527
|
}
|
|
1491
1528
|
const vendor = SUBSCRIPTION_VENDORS[vendorKey];
|
|
1492
1529
|
|
|
@@ -1860,6 +1897,9 @@ async function cmdConnect() {
|
|
|
1860
1897
|
const requested = argv.slice(1).filter((a, i) => !a.startsWith("--") && !flagValues.has(i + 1));
|
|
1861
1898
|
const scope = value("scope", "user");
|
|
1862
1899
|
if (!SCOPES.includes(scope)) throw new Error(`--scope must be one of ${SCOPES.join(", ")}, got "${scope}".`);
|
|
1900
|
+
const nativeWindows = windowsFrontEnd();
|
|
1901
|
+
if (nativeWindows && scope !== "user") throw new Error("per-repo registration on Windows isn't supported yet; use the default --scope user");
|
|
1902
|
+
if (nativeWindows && flag("copy-only")) throw new Error("Windows runs nomArmy inside WSL; run nomarmy connect <target> instead of --copy-only");
|
|
1863
1903
|
let projectDir = null;
|
|
1864
1904
|
if (scope !== "user") {
|
|
1865
1905
|
try { projectDir = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: repoDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); }
|
|
@@ -1894,6 +1934,17 @@ async function cmdConnect() {
|
|
|
1894
1934
|
}
|
|
1895
1935
|
try {
|
|
1896
1936
|
if (!json) console.log(c.bold(`\n๐ช Connecting nomArmy to ${target}...`));
|
|
1937
|
+
if (nativeWindows) {
|
|
1938
|
+
const capture = (cmd, args, opts = {}) => execFileSync(cmd, args, { stdio: ["ignore", "pipe", "pipe"], ...opts });
|
|
1939
|
+
const distro = pickDistro({ run: capture });
|
|
1940
|
+
const resolved = resolveWslNomarmy({ distro, run: capture });
|
|
1941
|
+
const launch = mcpBridgeLaunch({ distro, ...resolved });
|
|
1942
|
+
const result = connectViaWsl({ target, distro, launch, run, nomarmyRoot });
|
|
1943
|
+
writeWindowsSettings({ distro, ...resolved });
|
|
1944
|
+
results.push({ target, connected: true, ...result });
|
|
1945
|
+
if (!json) console.log(c.green(`โ Registered nomArmy with ${target}, running inside WSL (${distro})`));
|
|
1946
|
+
continue;
|
|
1947
|
+
}
|
|
1897
1948
|
const result = connectTarget(target, { nomarmyRoot, run, scope, projectDir });
|
|
1898
1949
|
results.push({ target, connected: true, ...result });
|
|
1899
1950
|
if (!json) console.log(c.green(`โ Registered nomarmy-local-worker with ${target}${scope === "user" ? "." : scope === "local" ? ` for ${projectDir} only (not committed).` : ` in ${path.relative(projectDir, result.configPath ?? path.join(projectDir, ".mcp.json"))}, for everyone who clones this repository.`}`));
|
|
@@ -2045,7 +2096,7 @@ async function maybeRemoveAgentsDir({ force }) {
|
|
|
2045
2096
|
* needs manual cleanup -- an honest, bounded scope beats guessing at which
|
|
2046
2097
|
* cache entries are "ours". */
|
|
2047
2098
|
function resolveConfiguredModelRepos() {
|
|
2048
|
-
const repo =
|
|
2099
|
+
const repo = setting("NOMARMY_MODEL_REPO");
|
|
2049
2100
|
return repo ? [repo] : [];
|
|
2050
2101
|
}
|
|
2051
2102
|
|
|
@@ -2757,6 +2808,25 @@ async function cmdStatusline() {
|
|
|
2757
2808
|
process.stdout.write(`${statusLineText({ session })}\n`);
|
|
2758
2809
|
}
|
|
2759
2810
|
|
|
2811
|
+
// `stats --share`: markdown for a PR description or README; `--badge [path]`:
|
|
2812
|
+
// an SVG badge to commit, with the README line for it (lib/share.mjs).
|
|
2813
|
+
async function shareStats(stats) {
|
|
2814
|
+
const { shareMarkdown, badgeSvg, badgeMarkdown } = await import("../lib/share.mjs");
|
|
2815
|
+
const scope = value("run") ? "this feature run" : value("since") ? `since ${value("since")}` : null;
|
|
2816
|
+
if (flag("share")) console.log(shareMarkdown(stats, { scope }));
|
|
2817
|
+
if (flag("badge")) {
|
|
2818
|
+
const given = value("badge");
|
|
2819
|
+
const top = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
2820
|
+
const root = top.status === 0 ? top.stdout.trim() : process.cwd();
|
|
2821
|
+
const file = path.resolve(root, given ?? path.join(".github", "nomarmy-badge.svg"));
|
|
2822
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
2823
|
+
fs.writeFileSync(file, badgeSvg(stats));
|
|
2824
|
+
const rel = path.relative(root, file).split(path.sep).join("/");
|
|
2825
|
+
console.log(`${flag("share") ? "\n" : ""}${c.green("โ")} Wrote ${rel}. Commit it and add this to your README:\n\n ${badgeMarkdown(rel)}\n`);
|
|
2826
|
+
console.log(c.dim("Re-run nomarmy stats --badge after more jobs to refresh the numbers."));
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2760
2830
|
function cmdStats() {
|
|
2761
2831
|
const stateRoot = process.env.NOMARMY_AGENT_STATE || path.join(os.homedir(), ".local", "share", "nomarmy-local-agents");
|
|
2762
2832
|
const records = loadJobRecords(path.join(stateRoot, "jobs"));
|
|
@@ -2768,8 +2838,9 @@ function cmdStats() {
|
|
|
2768
2838
|
}
|
|
2769
2839
|
let agentFor = () => null;
|
|
2770
2840
|
try { agentFor = agentLookup(loadAgents(globalConfigDir()).agents, agentProviderId); } catch { /* no agents.yml: commands name <agent> */ }
|
|
2771
|
-
const stats = computeStats(records, { repo, sinceMs: parseSince(value("since")), untilMs: parseSince(value("until")), role: value("role"), model: value("model"), agentFor, allSuggestions: flag("all-suggestions") });
|
|
2841
|
+
const stats = computeStats(records, { repo, sinceMs: parseSince(value("since")), untilMs: parseSince(value("until")), role: value("role"), model: value("model"), runId: value("run"), agentFor, allSuggestions: flag("all-suggestions") });
|
|
2772
2842
|
if (json) return out(stats);
|
|
2843
|
+
if (flag("share") || flag("badge")) return shareStats(stats);
|
|
2773
2844
|
console.log(flag("details") ? formatStats(stats) : formatStatsSummary(stats, { c }));
|
|
2774
2845
|
}
|
|
2775
2846
|
|
|
@@ -2857,16 +2928,58 @@ async function cmdValidatorsJudge(sub) {
|
|
|
2857
2928
|
const resolve = () => judgeSettings({ agents, providerOf: agentProviderId, runsOnHost: agentRunsToolsOnHost });
|
|
2858
2929
|
const probe = async (settings) => probeModel({ provider: settings.provider, model: settings.model, stateRoot: process.env.NOMARMY_AGENT_STATE || path.join(os.homedir(), ".local", "share", "nomarmy-local-agents") });
|
|
2859
2930
|
if (sub === "add") {
|
|
2860
|
-
|
|
2861
|
-
|
|
2931
|
+
let agent = value("agent"), model = value("model"), dominantBuilderVendor = null;
|
|
2932
|
+
let rl = null;
|
|
2933
|
+
const question = async (prompt) => {
|
|
2934
|
+
rl ??= createInterface({ input, output });
|
|
2935
|
+
return rl.question(c.bold(prompt));
|
|
2936
|
+
};
|
|
2937
|
+
if (!agent) {
|
|
2938
|
+
if (!process.stdin.isTTY || json) throw new Error("Usage: nomarmy validators add judge --agent <name> --model <model> [--host-tools]");
|
|
2939
|
+
const roles = loadArmy({ projectDir: repoDir }).army.roles;
|
|
2940
|
+
const guided = judgeAgentChoices({ agents, roles, providerOf: agentProviderId, runsOnHost: agentRunsToolsOnHost });
|
|
2941
|
+
dominantBuilderVendor = guided.dominantBuilderVendor;
|
|
2942
|
+
if (!guided.choices.length) throw new Error("No api or subscription agents are configured in agents.yml.");
|
|
2943
|
+
agent = (await chooseJudgeAgent({ choices: guided.choices, ask: question, write: (line) => console.log(line) })).name;
|
|
2944
|
+
}
|
|
2862
2945
|
if (!agents[agent]) throw new Error(`"${agent}" isn't an agent in agents.yml. Agents: ${Object.keys(agents).join(", ") || "(none)"}`);
|
|
2863
|
-
if (
|
|
2864
|
-
|
|
2946
|
+
if (!model) {
|
|
2947
|
+
if (!process.stdin.isTTY || json) throw new Error("Usage: nomarmy validators add judge --agent <name> --model <model> [--host-tools]");
|
|
2948
|
+
const listed = catalogModelsFor(agentProviderId(agents[agent]));
|
|
2949
|
+
if (listed.length) {
|
|
2950
|
+
console.log(`Models for ${agent}:`);
|
|
2951
|
+
listed.forEach((item, index) => console.log(` ${index + 1}. ${item}`));
|
|
2952
|
+
}
|
|
2953
|
+
const fallback = agents[agent].model ?? "";
|
|
2954
|
+
const answer = String(await question(`Model id${fallback ? ` [${fallback}]` : ""}: `)).trim();
|
|
2955
|
+
model = /^\d+$/.test(answer) && listed[Number(answer) - 1] ? listed[Number(answer) - 1] : answer || fallback;
|
|
2956
|
+
if (!model) { rl?.close(); throw new Error("A model id is required."); }
|
|
2957
|
+
}
|
|
2958
|
+
let hostTools = flag("host-tools");
|
|
2959
|
+
if (agentRunsToolsOnHost(agents[agent]) && !hostTools) {
|
|
2960
|
+
if (!process.stdin.isTTY || json) {
|
|
2961
|
+
const quote = (arg) => /^[A-Za-z0-9_./:-]+$/.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
|
|
2962
|
+
const rerun = ["nomarmy", ...argv, "--host-tools"].map(quote).join(" ");
|
|
2963
|
+
throw new Error(`agent "${agent}" runs its tools on this machine, and a judge reads text the worker wrote. Refusing without explicit consent. Re-run: ${rerun}`);
|
|
2964
|
+
}
|
|
2965
|
+
const allowed = await confirmJudgeHostTools({ agent, ask: question, write: (line) => console.log(line) });
|
|
2966
|
+
if (!allowed) { rl?.close(); console.log(c.dim("Canceled; nothing written.")); return; }
|
|
2967
|
+
hostTools = true;
|
|
2968
|
+
}
|
|
2969
|
+
rl?.close();
|
|
2970
|
+
const saved = saveJudge({ agent, model, hostTools });
|
|
2865
2971
|
const settings = resolve();
|
|
2866
2972
|
if (settings?.problem) throw new Error(settings.problem);
|
|
2867
2973
|
const test = await probe(settings);
|
|
2868
2974
|
if (json) return out({ saved: true, configPath: saved.configPath, test: test.ok ? "pass" : test.refused ? "refused" : "inconclusive", reason: test.reason });
|
|
2869
2975
|
console.log(c.green(`โ The judge is ${agent}/${model}, in ${saved.configPath}.`));
|
|
2976
|
+
if (dominantBuilderVendor === null) {
|
|
2977
|
+
try {
|
|
2978
|
+
const roles = loadArmy({ projectDir: repoDir }).army.roles;
|
|
2979
|
+
dominantBuilderVendor = judgeAgentChoices({ agents, roles, providerOf: agentProviderId, runsOnHost: agentRunsToolsOnHost }).dominantBuilderVendor;
|
|
2980
|
+
} catch { /* no readable army means there is no builder comparison */ }
|
|
2981
|
+
}
|
|
2982
|
+
if (dominantBuilderVendor && agentProviderId(agents[agent]) === dominantBuilderVendor) console.log(c.yellow("Note: this judge uses the same vendor as most build roles, so its verdicts are not independent of those builders."));
|
|
2870
2983
|
console.log(test.ok ? c.green("โ Test call answered. New implement jobs use it; restart open coordinator sessions to pick it up.") : c.red(`โ Test call ${test.refused ? "refused" : "didn't answer"}: ${test.reason ?? "no answer"}`));
|
|
2871
2984
|
if (!test.ok) process.exitCode = 1;
|
|
2872
2985
|
return;
|
|
@@ -2900,12 +3013,24 @@ function cmdMcp() {
|
|
|
2900
3013
|
const commands = { stats: cmdStats, validators: cmdValidators, mcp: cmdMcp, scan: cmdScan, validate: cmdValidate, sizing: cmdSizing, init: cmdInit, setup: cmdSetup, install: cmdInstall, model: cmdModel, agents: cmdAgents, army: cmdArmy, jobs: cmdJobs, statusline: cmdStatusline, health: cmdHealth, config: cmdConfig, update: cmdUpdate, connect: cmdConnect, sandbox: cmdSandbox, start: cmdStart, stop: cmdStop, uninstall: cmdUninstall, help: () => usage(0) };
|
|
2901
3014
|
// doctor command
|
|
2902
3015
|
async function cmdDoctor() {
|
|
3016
|
+
if (windowsFrontEnd()) {
|
|
3017
|
+
process.exitCode = windowsDoctor({ json, argv });
|
|
3018
|
+
return;
|
|
3019
|
+
}
|
|
2903
3020
|
// Import lazily to avoid circular dependencies
|
|
2904
3021
|
const { runDoctor } = await import("../lib/doctor.mjs");
|
|
2905
3022
|
await runDoctor({ json, exit: true, env: installEnv() });
|
|
2906
3023
|
}
|
|
2907
3024
|
commands.doctor = cmdDoctor;
|
|
2908
|
-
if (
|
|
3025
|
+
if (windowsFrontEnd() && windowsPlan(argv) === "FORWARD") {
|
|
3026
|
+
process.exit(windowsForward(argv));
|
|
3027
|
+
}
|
|
3028
|
+
if (!command && !flag("help")) {
|
|
3029
|
+
// New users typed `nomarmy` and got the whole command reference.
|
|
3030
|
+
console.log(`${c.bold("nomArmy")}: bounded coding workers with independently verified results.\n\n New here? ${c.cyan("nomarmy setup")} walks you through it, one step at a time\n All commands: ${c.cyan("nomarmy help")}\n Docs: https://github.com/rayson-tech/nomarmy`);
|
|
3031
|
+
process.exit(0);
|
|
3032
|
+
}
|
|
3033
|
+
if (flag("help") || !commands[command]) usage(command && !commands[command] ? 2 : 0);
|
|
2909
3034
|
|
|
2910
3035
|
try {
|
|
2911
3036
|
await commands[command]();
|
package/docker/Dockerfile
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
FROM node:
|
|
1
|
+
FROM node:26-bookworm-slim
|
|
2
2
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
3
3
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
4
4
|
bash ca-certificates curl git jq python3 python3-pip python3-venv ripgrep \
|
|
5
5
|
&& rm -rf /var/lib/apt/lists/*
|
|
6
|
-
# node
|
|
6
|
+
# The node:*-bookworm-slim images ship a non-root "node" user at uid/gid 1000,
|
|
7
7
|
# the same uid this Dockerfile used to try to create a "sandbox" user at.
|
|
8
8
|
# useradd silently failed on every build (masked by `|| true`), so `USER
|
|
9
9
|
# sandbox` below referred to a user that was never actually created --
|
package/docker/Dockerfile.go
CHANGED
package/docker/Dockerfile.rust
CHANGED