nomarmy 0.1.0-alpha.0 → 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,9 +8,29 @@
8
8
 
9
9
  <p align="center"><em>Tiny coders, big appetites for bounded tickets.</em> 🍪</p>
10
10
 
11
- **An agent harness where a worker's claims are never trusted, and the environment your tests need is declared, disposable and reproducible.**
11
+ **Your coding assistant plans; sandboxed workers build; nothing counts until nomArmy has checked it.**
12
12
 
13
- Your coding assistant (Claude Code, Codex or Cursor) stays in charge as the **General**: it decides what gets built and whether the result is acceptable. The work goes to **noms**, workers that implement, test and repair in their own git worktree and sandbox. A nom can run on a local model at no token cost, on an API key, or on your own Claude, ChatGPT or Muse Code subscription. nomArmy owns everything in between: worktrees, git, sandboxes, verification, and the evidence that decides whether work is accepted.
13
+ ## TL;DR
14
+
15
+ 1. **Have** Git, Node 20+ and [Podman](https://podman.io) (on macOS: `brew install podman && podman machine init && podman machine start`).
16
+ 2. **Install** (builds the local model server, OpenClaw and the sandbox, and registers nomArmy with Claude Code):
17
+ ```bash
18
+ git clone https://github.com/rayson-tech/nomarmy.git && cd nomarmy
19
+ ./install.sh --profile macbook-pro # or nvidia-linux, cpu-linux, dgx-spark, bedrock
20
+ ./e2e.sh --profile macbook-pro # should end with: === E2E PASS ===
21
+ ```
22
+ No local model? See [hosted workers only](#hosted-workers-only) or [a shared model server](#a-shared-model-server).
23
+ 3. **Set up your repo:** in the project, run `nomarmy init`. It proposes a `.nomarmy.yml` with your test command.
24
+ 4. **Use it:** restart Claude Code in that project and ask it to use nomArmy for one small bug that has a test. When that works, try `/feature <what you want built>`.
25
+ 5. **Optional:** add hosted workers with `nomarmy agents add`, give roles to them with `nomarmy army init`, or connect Codex or Cursor with `nomarmy connect codex cursor`.
26
+
27
+ Stuck? `nomarmy doctor` checks the machine, and `nomarmy health` checks everything nomArmy runs on.
28
+
29
+ ## What it is
30
+
31
+ Your coding assistant (Claude Code, Codex or Cursor) stays in charge as the **General**: it decides what gets built and whether the result is acceptable. The work goes to **noms**, workers that implement, test and repair in their own git worktree and sandbox, on a local model, an API key, or your own ChatGPT or Muse Code subscription. nomArmy owns everything in between: worktrees, git, sandboxes, verification, and the evidence that decides whether work is accepted.
32
+
33
+ **What you get is work you don't have to take on faith**, not cheaper work. Delegating costs the General tokens too: briefing and reviewing. On small, already-diagnosed tickets we measured 4 to 8 times more of the General's tokens than fixing the bug directly, and break-even at roughly 150 lines of context a fix needs to read ([the measurements](docs/experiments/2026-09-20-model-bakeoff-and-economics.md)). It pays off on bigger tickets, on parallel work, and anywhere you'd otherwise have to trust an agent's say-so.
14
34
 
15
35
  Developed and maintained by Rayson Technologies. This is an alpha (`0.1.0-alpha`).
16
36
 
@@ -22,35 +42,10 @@ Developed and maintained by Rayson Technologies. This is an alpha (`0.1.0-alpha`
22
42
  4. nomArmy treats that report as a claim. It reads the real diff from git, runs your verification profile itself in a fresh sandbox, reverts the production change to check the tests actually fail without it, and scans for secrets.
23
43
  5. Only then does it commit, on the worker's own branch. It never merges into yours: reviewing and integrating stay with the General, and with you.
24
44
 
25
- A malformed report isn't automatically a failure: if the repository changed, nomArmy verifies independently and may recover the work. Failing verification stays failed, unconditionally.
45
+ A malformed report isn't automatically a failure: if the repository changed, nomArmy verifies independently and may recover the work. Failing verification stays failed, unconditionally. 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.
26
46
 
27
47
  Around that core: **agents** say where a job can run, the **army** says which role runs on which agent, and **`/feature`** runs a whole feature end to end, from plan through build, review and acceptance, handing you a branch to merge.
28
48
 
29
- ## Quick start
30
-
31
- You need Git, Node 20+, and [Podman](https://podman.io).
32
-
33
- ```bash
34
- npm install -g nomarmy@alpha
35
- nomarmy doctor # what's missing on this machine, and how to fix it
36
- nomarmy setup # pick a profile and model; prints the install.sh command to run
37
- nomarmy connect claude # or codex, cursor: registers nomArmy with your coordinator
38
- ```
39
-
40
- Installing from a clone is the most tested path, and it's what `install.sh` expects: see [Install](#install). `install.sh` builds llama.cpp for local inference, installs and configures [OpenClaw](https://github.com/openclaw/openclaw) (the host-side broker every model call goes through), builds the sandbox image and registers the MCP server.
41
-
42
- `nomarmy connect` also installs the `/feature` command, Claude Code's status line and (on macOS) nomArmy's notifier. The coordinator gets nomArmy's instructions from the MCP server itself, so there's nothing to copy into your projects.
43
-
44
- Then, in any git repository:
45
-
46
- ```bash
47
- nomarmy init # propose a .nomarmy.yml with your test command
48
- nomarmy agents add # optional: an API key or a subscription
49
- nomarmy army init # optional: the default roster of roles
50
- ```
51
-
52
- Ask your coordinator to delegate one small, well-tested ticket before anything bigger. When that works, try `/feature <what you want built>`.
53
-
54
49
  ## Install
55
50
 
56
51
  | Platform | Guide |
@@ -59,10 +54,16 @@ Ask your coordinator to delegate one small, well-tested ticket before anything b
59
54
  | Linux, with or without an NVIDIA GPU | [Linux](#linux) |
60
55
  | Windows | [Windows](#windows) |
61
56
  | NVIDIA DGX Spark | [DGX Spark](#dgx-spark) |
62
- | No GPU, or no local inference | [Cloud (Bedrock)](#cloud-bedrock) |
57
+ | No local model: API keys and subscriptions only | [Hosted workers only](#hosted-workers-only) |
58
+ | A shared GPU server (or a tunnel to one) | [A shared model server](#a-shared-model-server) |
59
+ | No GPU, with Bedrock | [Cloud (Bedrock)](#cloud-bedrock) |
63
60
 
64
61
  Every platform needs Git and Podman. `nomarmy doctor` checks the host and prints a fix for anything missing.
65
62
 
63
+ `install.sh` builds llama.cpp for local inference, installs and configures [OpenClaw](https://github.com/openclaw/openclaw) (the host-side broker every model call goes through), builds the sandbox image, and registers the MCP server if Claude Code is installed. `nomarmy connect` (run by `install.sh`, or by hand for Codex and Cursor) also installs the `/feature` command, Claude Code's status line and, on macOS, nomArmy's notifier. The coordinator gets nomArmy's instructions from the MCP server itself, so there's nothing to copy into your projects.
64
+
65
+ **From npm:** `npm install -g nomarmy@alpha` gives you the `nomarmy` command; `nomarmy setup` then picks a profile and model and prints the `install.sh` command to run (`nomarmy setup --hosted` or `--llama-url <server>` without a local model). Installing from a clone, as in the TL;DR, is the most tested path.
66
+
66
67
  ### macOS (Apple Silicon)
67
68
 
68
69
  ```bash
@@ -105,7 +106,7 @@ CPU-only Linux works but is slow for interactive use: see [Sizing](#sizing).
105
106
 
106
107
  1. **WSL2** (the supported path): install a Linux distro under WSL2, install Podman inside it, and follow the [Linux](#linux) guide entirely inside the distro. Watch WSL2's default cap of about half your RAM (`.wslconfig`), and set `git config --global core.longpaths true` (`nomarmy doctor` checks this).
107
108
  2. **Native llama.cpp on Windows**, built from source: more RAM, more setup. Prebuilt binaries aren't a safe shortcut; some CPUs crash every backend at startup.
108
- 3. **No local inference**: `./install.sh --profile bedrock` (see [Cloud (Bedrock)](#cloud-bedrock)), or hosted agents only.
109
+ 3. **No local inference**: [hosted workers only](#hosted-workers-only), or [Bedrock](#cloud-bedrock).
109
110
 
110
111
  `nomarmy sizing` reports what your hardware can support.
111
112
 
@@ -121,6 +122,34 @@ chmod +x install.sh e2e.sh scripts/*.sh
121
122
 
122
123
  Moving from a Mac install? Don't copy a Mac binary or model cache over: clone fresh and let `install.sh` build llama.cpp for CUDA on that machine.
123
124
 
125
+ ### Hosted workers only
126
+
127
+ No GPU and no local model: every job runs on an API key or a subscription (ChatGPT, Muse Code) you add as an agent. Git worktrees, the sandbox and verification still run on your machine, so you still need Git, Node and Podman.
128
+
129
+ ```bash
130
+ git clone https://github.com/rayson-tech/nomarmy.git && cd nomarmy
131
+ npm install && npm link # or: npm install -g nomarmy@alpha
132
+ nomarmy setup --hosted # records that this install has no local model
133
+ ./install.sh --profile hosted # OpenClaw, the sandbox, and the Claude Code registration; no llama.cpp
134
+ nomarmy agents add # an API key or a subscription login
135
+ nomarmy army init --agent <name> # every role on that agent (add --model <model> to pick one)
136
+ nomarmy doctor
137
+ ```
138
+
139
+ A hosted install refuses a job that names no role or agent, rather than falling back to a local model that isn't there. `nomarmy health` warns about any role still on `local`. `e2e.sh` tests the local model, so it has nothing to do here; `nomarmy army assign` tests each role's route instead.
140
+
141
+ ### A shared model server
142
+
143
+ A team GPU box (a DGX, a workstation) runs one llama-server; everyone else points nomArmy at it. That works through an SSH tunnel too (`ssh -L 8080:localhost:8080 gpu-box`, then `http://127.0.0.1:8080`).
144
+
145
+ ```bash
146
+ nomarmy setup --llama-url http://gpu-box:8080 # checks /health, records the address
147
+ ./install.sh --profile remote # no llama.cpp build; OpenClaw points at that server
148
+ ./e2e.sh --profile remote
149
+ ```
150
+
151
+ nomArmy doesn't start, stop or size that server: whoever runs it sets its model, context and slots, and `install.sh` reads the model name and context from the server. Your machine still runs the sandbox and verification for your jobs. Several people sharing one server share its slots, so keep `NOMARMY_MAX_WORKERS` low (the profile sets 1). Run the server itself with any local profile's `install.sh` on the GPU machine, with `NOMARMY_LLAMA_HOST=0.0.0.0` so others can reach it, on a network you trust: llama-server has no authentication.
152
+
124
153
  ### Cloud (Bedrock)
125
154
 
126
155
  No GPU, no local build:
@@ -179,6 +208,8 @@ Changes apply to the next job with no restart. The exception is a **new** api ag
179
208
 
180
209
  **Your plan decides which models run.** A model can be listed and still refused: on a ChatGPT plan, the Codex route runs gpt-6-astra and the gpt-5.6 models but refuses gpt-6-sol and gpt-6-luna. `army assign` and `agents update --probe` test the exact route a job takes, so they catch this before a job does.
181
210
 
211
+ **Vendor terms and platform risk.** Every model call goes through [OpenClaw](https://github.com/openclaw/openclaw), and subscriptions are reached through each vendor's own CLI or login. We've read the terms that apply (see above), but using a personal subscription through a harness is exactly the kind of use vendors tighten, and a change in a vendor's terms or in OpenClaw can stop a subscription agent from working. Local models and API keys don't carry that risk. Plan on subscriptions as a convenience, not the only way your roles can run.
212
+
182
213
  **Picking an agent.** Build work goes to a sandboxed agent: `local`, an api key, Codex or Muse. `local` for a bounded change against a written spec with a test; your code never leaves your machine. An api or subscription agent when the work needs more than the local model, knowing it sends code to that vendor. That's a decision about where your source travels, separate from the trust boundary, which is the same for every agent. The General itself when the answer isn't known yet.
183
214
 
184
215
  ## The army: who does what
@@ -270,6 +301,18 @@ verification:
270
301
 
271
302
  `nomarmy validate` checks the file against the schema; `nomarmy scan --check` diffs it against what the repo actually contains.
272
303
 
304
+ **Policy: what no job can skip.**
305
+
306
+ ```yaml
307
+ policy:
308
+ require_verification: true # every implement job needs a verification profile; only passing work commits
309
+ require_regression_check: true # verify_regression can't be switched off per job
310
+ ```
311
+
312
+ `nomarmy init` proposes both for new repos. Without them, a job with no verification profile still commits (flagged for review, not blocked), and the General decides per job whether to run the revert check. With them, those are the repo's rules, not the General's judgment calls, and since nomArmy reads this file only from your checkout, neither the General nor a worker can relax it.
313
+
314
+ **Refactors.** Reverting a behavior-preserving change restores code that works, so the revert check can't prove anything about it. A job can declare `refactor: true` instead: nomArmy then commits it only if verification passes **and no test file was added, changed or deleted**. The existing tests passing unchanged is the evidence. A change that alters behavior has to alter tests to show it, so it can't pass as a refactor.
315
+
273
316
  **Add a check for what unit tests can't see.** A module left out of a deploy bundle passes every unit test and crashes at deploy. When `nomarmy init` sees a bundle or packaging step (Lambda asset scripts, SAM, Serverless, CDK), it suggests a profile that runs it and then imports each entry point from the built bundle.
274
317
 
275
318
  ### Languages and dependencies
@@ -458,7 +501,7 @@ What we've learned from real runs, including where delegating pays and where it
458
501
  - **Deploy-time failures need your own check.** See [Add a check for what unit tests can't see](#nomarmyyml).
459
502
  - **Node dependencies install only from npm lockfiles**, one per package (no workspaces, yarn, pnpm or bun yet), and only from the public registry: the image build has no credentials for a private one.
460
503
  - **Verification needing services** (a database, a mock server) reports `not_run` instead of running without them. The compose parser doesn't resolve YAML anchors.
461
- - **Same-host only**: the MCP server and OpenClaw run on the machine with the coordinator. There's no remote-worker mode yet.
504
+ - **Same-host sandboxes**: the MCP server, OpenClaw and every job's sandbox run on the machine with the coordinator. Only the model can be elsewhere (an agent, or [a shared model server](#a-shared-model-server)).
462
505
 
463
506
  ## Troubleshooting
464
507
 
package/bin/nomarmy.mjs CHANGED
@@ -18,10 +18,11 @@ 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 { connectClaude, connectCodex, connectCursor, cursorAlreadyConnected } from "../lib/connect.mjs";
21
+ import { connectClaude, connectCodex, connectCursor, cursorAlreadyConnected, deriveWorkerModelEnv } from "../lib/connect.mjs";
22
22
  import { ID_RE, AUTH_ENV_NAME_RE, OPENCLAW_PROVIDER_ID_RE, openclawProviderId, isNativeProviderType } from "../lib/dispatch-schema.mjs";
23
- import { loadAgents, readAgentsFile, writeAgentsFile, agentsConfigPath, apiAgentAsPoolEntry, describeAgent as describeAgentLabel, AGENT_KINDS, API_PROVIDER_TYPES, RESERVED_AGENT_NAMES, BUILTIN_LOCAL_AGENT } from "../lib/agents.mjs";
23
+ import { loadAgents, readAgentsFile, writeAgentsFile, agentsConfigPath, apiAgentAsPoolEntry, describeAgent as describeAgentLabel, agentRunsToolsOnHost, AGENT_KINDS, API_PROVIDER_TYPES, RESERVED_AGENT_NAMES, BUILTIN_LOCAL_AGENT } from "../lib/agents.mjs";
24
24
  import { loadArmy, describeArmy, readArmyFile, updateArmyInFile, assignRoleInFile, parseTargetSpec, armyLayerPath, globalConfigDir, DEFAULT_ARMY, ARMY_PHASES, LOCAL_CONFIG_FILENAME } from "../lib/army.mjs";
25
+ import { parseLlamaUrl } from "../lib/execution.mjs";
25
26
  import { ensureProviderConfig } from "../lib/openclaw-config.mjs";
26
27
  import { recordProbeSuccess } from "../lib/health.mjs";
27
28
  import { pruneJobRuntime } from "../lib/prune.mjs";
@@ -93,6 +94,8 @@ Usage: nomarmy <command> [options]
93
94
  write config/profiles/<name>.env (+ config/common.env).
94
95
  Prints the install.sh command; never runs it.
95
96
  --tier <more|nominal> with --json, skip the prompt
97
+ --hosted skip hardware/model questions
98
+ --llama-url <url> use a llama-server on another machine
96
99
  model Change the configured model later, without the rest of
97
100
  setup's questions. Offers to also resync the MCP
98
101
  registration's worker-routing env vars, and to restart
@@ -168,7 +171,9 @@ Usage: nomarmy <command> [options]
168
171
  UI/UX, data architect, security analyst, PM,
169
172
  PO, stakeholder, all on \`local\`) to --global
170
173
  (default), --project or --local; --force
171
- replaces an existing one
174
+ replaces an existing one; --agent <name>
175
+ starts every role there, optionally with
176
+ --model <model|auto>
172
177
  assign <role> <agent|none> [model|auto]
173
178
  give a role an agent, and optionally the model
174
179
  to run on it ("auto" lets the General pick per
@@ -516,6 +521,55 @@ async function chooseModel(rl) {
516
521
  * command itself does.
517
522
  */
518
523
  async function cmdSetup() {
524
+ const hosted = flag("hosted");
525
+ const hasLlamaUrl = flag("llama-url");
526
+ if (hosted && hasLlamaUrl) throw new Error("--hosted and --llama-url cannot be used together.");
527
+
528
+ if (hosted || hasLlamaUrl) {
529
+ const commonPath = path.join(nomarmyRoot, "config", "common.env");
530
+
531
+ if (hosted) {
532
+ const next = [
533
+ `${path.join(nomarmyRoot, "install.sh")} --profile hosted`,
534
+ "nomarmy agents add",
535
+ "nomarmy army init --agent <name>",
536
+ ];
537
+ fs.mkdirSync(path.dirname(commonPath), { recursive: true });
538
+ writeEnvLine(commonPath, "NOMARMY_EXECUTION", "hosted");
539
+ if (json) return out({ written: commonPath, execution: "hosted", next });
540
+ console.log(c.green(`✓ Wrote NOMARMY_EXECUTION=hosted to ${path.relative(nomarmyRoot, commonPath)}.`));
541
+ console.log(c.dim("\nNext:"));
542
+ for (const step of next) console.log(` ${c.bold(step)}`);
543
+ return;
544
+ }
545
+
546
+ const llamaInput = value("llama-url");
547
+ if (!llamaInput) throw new Error("--llama-url requires an http:// URL, for example http://server:8080.");
548
+ const { host: llamaHost, port: llamaPort } = parseLlamaUrl(llamaInput);
549
+ const urlHost = llamaHost.includes(":") ? `[${llamaHost}]` : llamaHost;
550
+ const healthUrl = `http://${urlHost}:${llamaPort}/health`;
551
+ let reachable = false;
552
+ try {
553
+ await fetch(healthUrl, { signal: AbortSignal.timeout(5000) });
554
+ reachable = true;
555
+ } catch {
556
+ // A server may simply be offline during setup; retain its validated address.
557
+ }
558
+ fs.mkdirSync(path.dirname(commonPath), { recursive: true });
559
+ writeEnvLine(commonPath, "NOMARMY_EXECUTION", "remote");
560
+ writeEnvLine(commonPath, "NOMARMY_LLAMA_HOST", llamaHost);
561
+ writeEnvLine(commonPath, "NOMARMY_LLAMA_PORT", llamaPort);
562
+ const next = `${path.join(nomarmyRoot, "install.sh")} --profile remote`;
563
+ if (json) return out({ written: commonPath, execution: "remote", llamaHost, llamaPort, reachable, next });
564
+ console.log(reachable
565
+ ? c.green(`✓ llama-server is reachable at ${healthUrl}.`)
566
+ : c.yellow(`⚠ llama-server is not reachable at ${healthUrl} right now; configuration was still written.`));
567
+ console.log(c.green(`✓ Wrote the remote llama-server settings to ${path.relative(nomarmyRoot, commonPath)}.`));
568
+ console.log(c.dim("\nNext:"));
569
+ console.log(` ${c.bold(next)}`);
570
+ return;
571
+ }
572
+
519
573
  const execution = value("execution", process.env.NOMARMY_EXECUTION || "local");
520
574
  const isCloud = execution !== "local";
521
575
  const hardware = isCloud ? null : await detectHardware();
@@ -1962,16 +2016,45 @@ async function cmdArmyShow() {
1962
2016
  async function cmdArmyInit() {
1963
2017
  const layer = armyLayerFlag("global");
1964
2018
  const filePath = armyLayerPath(layer, { projectDir: repoDir });
2019
+ const agentName = value("agent");
2020
+ let selectedAgent = null;
2021
+ let roleModel = null;
2022
+ if (flag("agent") && !agentName) throw new Error("--agent requires a name from agents.yml.");
2023
+ if (agentName) {
2024
+ const agents = loadAgentsOrExit().agents;
2025
+ if (!Object.prototype.hasOwnProperty.call(agents, agentName)) {
2026
+ throw new Error(`Unknown agent "${agentName}". Pick one of: ${Object.keys(agents).join(", ")}`);
2027
+ }
2028
+ selectedAgent = agents[agentName];
2029
+ if (flag("model") && !value("model")) throw new Error("--model requires a model name or auto.");
2030
+ roleModel = value("model") ?? (selectedAgent.model ? null : "auto");
2031
+ }
1965
2032
  const existing = readArmyFile(filePath, { armyOnly: layer !== "project" });
1966
2033
  if (existing?.roles && Object.keys(existing.roles).length && !flag("force")) {
1967
2034
  throw new Error(`${filePath} already defines an army (${Object.keys(existing.roles).join(", ")}). Re-run with --force to replace it.`);
1968
2035
  }
1969
2036
  // Keep a General already defined in this layer; the roster is what init resets.
1970
- updateArmyInFile(filePath, (army) => ({ ...structuredClone(DEFAULT_ARMY), ...(army.general ? { general: army.general } : {}) }));
2037
+ const roster = structuredClone(DEFAULT_ARMY);
2038
+ if (agentName) {
2039
+ for (const role of Object.values(roster.roles)) {
2040
+ role.agent = agentName;
2041
+ if (roleModel) role.model = roleModel;
2042
+ }
2043
+ }
2044
+ updateArmyInFile(filePath, (army) => ({ ...roster, ...(army.general ? { general: army.general } : {}) }));
1971
2045
  if (layer === "local") ensureLocalLayerIgnored();
1972
2046
  if (json) return out({ written: filePath, layer, roles: Object.keys(DEFAULT_ARMY.roles) });
1973
2047
  console.log(c.green(`✓ Wrote the default army to ${filePath} (${layer}).`));
1974
- console.log(c.dim("Every role starts on the local model. Next: `nomarmy army general <agent>` (the agent your coordinator session runs on), then `nomarmy army assign <role> <agent>` for any role you want elsewhere."));
2048
+ if (!agentName) {
2049
+ console.log(c.dim("Every role starts on the local model. Next: `nomarmy army general <agent>` (the agent your coordinator session runs on), then `nomarmy army assign <role> <agent>` for any role you want elsewhere."));
2050
+ return;
2051
+ }
2052
+ console.log(c.dim(roleModel === "auto"
2053
+ ? `Every role starts on ${agentName} with model auto; the coordinator picks a model per job.`
2054
+ : `Every role starts on ${agentName}${roleModel ? ` with model ${roleModel}` : ` using its default model ${selectedAgent.model}`}.`));
2055
+ if (agentRunsToolsOnHost(selectedAgent)) {
2056
+ console.log(c.yellow(`⚠ ${agentName} runs its tools on the host. Build roles on it will be refused unless allow_host_tools is set in agents.yml.`));
2057
+ }
1975
2058
  }
1976
2059
 
1977
2060
  async function cmdArmyAssign() {
@@ -2206,10 +2289,18 @@ async function cmdJobs() {
2206
2289
 
2207
2290
  // `nomarmy health`: run the checks now (the MCP server also runs them every
2208
2291
  // 6 hours) and record them, which also refreshes the status line's warning.
2292
+ // This install's settings from config/common.env (the execution mode and the
2293
+ // model server's address, as `nomarmy connect` gives the MCP server), under
2294
+ // anything set (non-empty) in the environment.
2295
+ function installEnv() {
2296
+ const set = Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== ""));
2297
+ return { ...deriveWorkerModelEnv(nomarmyRoot), ...set };
2298
+ }
2299
+
2209
2300
  async function cmdHealth() {
2210
2301
  const { checkAndRecordHealth } = await import("../lib/health.mjs");
2211
2302
  const stateRoot = process.env.NOMARMY_AGENT_STATE || path.join(os.homedir(), ".local", "share", "nomarmy-local-agents");
2212
- const { result } = await checkAndRecordHealth({ projectDir: repoDir, stateRoot, configDir: globalConfigDir() });
2303
+ const { result } = await checkAndRecordHealth({ projectDir: repoDir, stateRoot, configDir: globalConfigDir(), env: installEnv() });
2213
2304
  if (json) return out(result);
2214
2305
  console.log(c.bold("🍪 nomArmy health") + c.dim(` ${new Date(result.checkedAt).toLocaleString()}`));
2215
2306
  if (!result.issues.length) { console.log(c.green("\n✓ Nothing to fix.")); return; }
@@ -2234,7 +2325,7 @@ const commands = { scan: cmdScan, validate: cmdValidate, sizing: cmdSizing, init
2234
2325
  async function cmdDoctor() {
2235
2326
  // Import lazily to avoid circular dependencies
2236
2327
  const { runDoctor } = await import("../lib/doctor.mjs");
2237
- await runDoctor({ json, exit: true });
2328
+ await runDoctor({ json, exit: true, env: installEnv() });
2238
2329
  }
2239
2330
  commands.doctor = cmdDoctor;
2240
2331
  if (!command || flag("help") || !commands[command]) usage(command && !commands[command] ? 2 : 0);
package/config/common.env CHANGED
@@ -16,8 +16,10 @@ NOMARMY_MAX_WORKERS=1
16
16
  NOMARMY_AGENT_IMAGE=openclaw-nomarmy-coder:bookworm
17
17
  NOMARMY_INSTALL_ROOT=$HOME/.local/share/nomarmy-local-agents
18
18
 
19
- # Execution layer: 'local' runs llama-server on this machine, 'bedrock' calls a
20
- # hosted OpenAI-compatible endpoint. Profiles override this.
19
+ # Where models run: 'local' runs llama-server on this machine, 'remote' uses
20
+ # one nomArmy doesn't run (nomarmy setup --llama-url), 'hosted' has no local
21
+ # model at all (nomarmy setup --hosted), 'bedrock' calls Bedrock. Profiles
22
+ # override this.
21
23
  NOMARMY_EXECUTION=local
22
24
 
23
25
  # Worker model routing. The MCP server composes "<provider>/<model>" for
@@ -0,0 +1,6 @@
1
+ # Hosted workers only: no local model on this machine. Every job runs on an
2
+ # api or subscription agent (nomarmy agents add); the sandbox, git worktrees
3
+ # and verification still run here. `nomarmy setup --hosted` writes this
4
+ # mode into config/common.env as well, so the MCP server knows it.
5
+ NOMARMY_PROFILE=hosted
6
+ NOMARMY_EXECUTION=hosted
@@ -0,0 +1,9 @@
1
+ # A llama-server nomArmy doesn't run, e.g. a team's GPU server or an SSH
2
+ # tunnel to one. Set its address with `nomarmy setup --llama-url
3
+ # http://host:8080`, which writes NOMARMY_LLAMA_HOST / NOMARMY_LLAMA_PORT
4
+ # into config/common.env. nomArmy
5
+ # doesn't start, stop or size that server; the sandbox, git worktrees and
6
+ # verification still run on this machine.
7
+ NOMARMY_PROFILE=remote
8
+ NOMARMY_EXECUTION=remote
9
+ NOMARMY_MAX_WORKERS=1
package/e2e.sh CHANGED
@@ -24,6 +24,12 @@ load_profile "$PROFILE"
24
24
 
25
25
  echo "=== nomArmy E2E: $NOMARMY_PROFILE ==="
26
26
 
27
+ if [[ "$(nomarmy_execution_mode)" == hosted ]]; then
28
+ echo "e2e.sh runs a job on the local model, and profile '$NOMARMY_PROFILE' has none."
29
+ echo "Check each agent with: nomarmy doctor"
30
+ exit 0
31
+ fi
32
+
27
33
  "$ROOT/scripts/start-inference.sh" "$NOMARMY_PROFILE"
28
34
 
29
35
  if nomarmy_is_cloud; then
@@ -56,18 +62,35 @@ fi
56
62
  # bind-mounting paths under the host home directory, and the cleanup step
57
63
  # below bind-mounts this directory into a container.
58
64
  TMP="$(mktemp -d "$HOME/.nomarmy-e2e.XXXXXX")"
65
+ # OpenClaw's own state for this run, also under $HOME, the way nomArmy's
66
+ # real dispatch keeps it (--state-dir). Left unset, OpenClaw puts its
67
+ # working files in the system temp folder, which the Podman sandbox can't
68
+ # mount on macOS, and it uses the operator's main state, whose memory index
69
+ # holds their past sessions.
70
+ STATE="$(mktemp -d "$HOME/.nomarmy-e2e-state.XXXXXX")"
59
71
 
60
72
  cleanup() {
61
73
  local exit_code=$?
62
74
 
63
- if [[ -n "${TMP:-}" && -d "$TMP" ]]; then
75
+ # OpenClaw leaves this run's sandbox container running; its name carries
76
+ # the hash recorded under the state dir (as nomArmy's job cleanup does).
77
+ if [[ -n "${STATE:-}" && -d "$STATE/state/sandbox/skills-workspaces" ]] && command -v podman >/dev/null 2>&1; then
78
+ for ws in "$STATE"/state/sandbox/skills-workspaces/workspace-*; do
79
+ [[ -d "$ws" ]] || continue
80
+ podman ps -a --filter "name=${ws##*/workspace-}" --format '{{.Names}}' 2>/dev/null \
81
+ | xargs -r podman rm -f -v >/dev/null 2>&1 || true
82
+ done
83
+ fi
84
+
85
+ for dir in "${TMP:-}" "${STATE:-}"; do
86
+ if [[ -n "$dir" && -d "$dir" ]]; then
64
87
 
65
88
  # OpenClaw's Podman sandbox may create files that the host user
66
89
  # cannot delete directly. Use a disposable container to clean
67
90
  # the temporary workspace first.
68
91
  if command -v podman >/dev/null 2>&1 && podman info >/dev/null 2>&1; then
69
92
  podman run --rm \
70
- -v "$TMP:/cleanup" \
93
+ -v "$dir:/cleanup" \
71
94
  alpine:3.20 \
72
95
  sh -c '
73
96
  find /cleanup -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + \
@@ -76,13 +99,14 @@ cleanup() {
76
99
  >/dev/null 2>&1 || true
77
100
  fi
78
101
 
79
- rm -rf "$TMP" 2>/dev/null || true
102
+ rm -rf "$dir" 2>/dev/null || true
80
103
 
81
- if [[ -d "$TMP" ]]; then
104
+ if [[ -d "$dir" ]]; then
82
105
  echo "WARN: E2E temporary directory could not be completely removed:"
83
- echo " $TMP"
106
+ echo " $dir"
84
107
  fi
85
108
  fi
109
+ done
86
110
 
87
111
  exit "$exit_code"
88
112
  }
@@ -126,15 +150,29 @@ PROMPT='Fix the bug so npm test passes. Work only in the workspace. Run npm test
126
150
 
127
151
  OUT="$TMP/openclaw.json"
128
152
 
153
+ # The same privacy settings every nomArmy job gets (lib/openclaw-run.mjs
154
+ # withJobPrivacy): OpenClaw's memory search and session-memory hook off, so
155
+ # nothing is indexed or sent for embedding.
156
+ CONFIG="$STATE/openclaw.job.json"
157
+ mkdir -p "$STATE/state"
158
+ node --input-type=module -e '
159
+ import fs from "node:fs";
160
+ import { readOpenclawConfig } from "'"$ROOT"'/lib/openclaw-config.mjs";
161
+ import { withJobPrivacy } from "'"$ROOT"'/lib/openclaw-run.mjs";
162
+ fs.writeFileSync(process.argv[1], JSON.stringify(withJobPrivacy(readOpenclawConfig() ?? {})), { mode: 0o600 });
163
+ ' "$CONFIG"
164
+
129
165
  openclaw agent exec "$PROMPT" \
130
166
  --model "$NOMARMY_WORKER_PROVIDER/$NOMARMY_WORKER_MODEL" \
131
167
  --cwd "$TMP" \
168
+ --state-dir "$STATE/state" \
169
+ --config "$CONFIG" \
132
170
  --code-mode direct \
133
171
  --local-model-lean \
134
172
  --thinking off \
135
173
  --timeout 600 \
136
174
  --json \
137
- > "$OUT"
175
+ > "$OUT" 2> "$STATE/openclaw.stderr.log"
138
176
 
139
177
  # Independent verification. We do not trust the worker's claim
140
178
  # that its implementation is correct.
package/install.sh CHANGED
@@ -15,7 +15,7 @@ case "$OS_NAME" in
15
15
  command -v curl >/dev/null 2>&1 || brew install curl
16
16
  command -v git >/dev/null 2>&1 || brew install git
17
17
  command -v node >/dev/null 2>&1 || brew install node
18
- if ! nomarmy_is_cloud; then
18
+ if nomarmy_manages_model_server; then
19
19
  command -v xcode-select >/dev/null 2>&1 && xcode-select -p >/dev/null 2>&1 || { echo 'ERROR: Xcode Command Line Tools are required. Run: xcode-select --install'; exit 1; }
20
20
  command -v cmake >/dev/null 2>&1 || brew install cmake
21
21
  fi
@@ -24,8 +24,8 @@ case "$OS_NAME" in
24
24
  command -v podman >/dev/null 2>&1 || brew install podman
25
25
  ;;
26
26
  Linux)
27
- if nomarmy_is_cloud; then
28
- # No local model is built or served, so the C++ toolchain is not needed.
27
+ if ! nomarmy_manages_model_server; then
28
+ # No model is built or served here, so the C++ toolchain is not needed.
29
29
  need curl; need git; need node; need npm
30
30
  else
31
31
  if ! command -v cmake >/dev/null 2>&1 || ! command -v c++ >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1 || ! command -v git >/dev/null 2>&1 || ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then install_build_dependencies; fi
@@ -105,7 +105,7 @@ if ! command -v openclaw >/dev/null 2>&1; then
105
105
  export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH"
106
106
  fi
107
107
  need openclaw
108
- if ! nomarmy_is_cloud; then openclaw plugins install @openclaw/llama-cpp-provider || true; fi
108
+ if nomarmy_has_local_model; then openclaw plugins install @openclaw/llama-cpp-provider || true; fi
109
109
  "$ROOT/scripts/start-inference.sh" "$NOMARMY_PROFILE"
110
110
  # Sandbox before provider config: configure-openclaw.sh refuses to store a real
111
111
  # Bedrock credential unless the coder sandbox is already network-isolated.
@@ -122,4 +122,8 @@ if nomarmy_is_cloud && [[ "${NOMARMY_ORCHESTRATOR_RUNTIME:-}" == "claude-code" ]
122
122
  echo " ./scripts/configure-orchestrator.sh $NOMARMY_PROFILE # print the settings"
123
123
  echo " ./scripts/configure-orchestrator.sh $NOMARMY_PROFILE --apply # write them to Claude Code"
124
124
  fi
125
- echo "==> Install complete. Run: ./e2e.sh --profile $NOMARMY_PROFILE"
125
+ if [[ "$(nomarmy_execution_mode)" == hosted ]]; then
126
+ echo "==> Install complete. Add an agent (nomarmy agents add), give roles to it (nomarmy army init --agent <name>), then run: nomarmy doctor"
127
+ else
128
+ echo "==> Install complete. Run: ./e2e.sh --profile $NOMARMY_PROFILE"
129
+ fi