nomarmy 0.1.0-alpha.20 → 0.1.0-alpha.22

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.
Files changed (51) hide show
  1. package/bin/nomarmy.mjs +220 -34
  2. package/lib/acceptance-fill.mjs +180 -0
  3. package/lib/acceptance-impact.mjs +215 -0
  4. package/lib/acceptance-result.mjs +21 -0
  5. package/lib/acceptance.mjs +166 -0
  6. package/lib/admission.mjs +84 -15
  7. package/lib/army.mjs +8 -6
  8. package/lib/codex-link.mjs +42 -0
  9. package/lib/connect.mjs +9 -5
  10. package/lib/continue-from.mjs +29 -12
  11. package/lib/coordinator-instructions.mjs +4 -0
  12. package/lib/diff-checks.mjs +9 -3
  13. package/lib/doctor.mjs +7 -2
  14. package/lib/execute.mjs +187 -32
  15. package/lib/git-record.mjs +9 -2
  16. package/lib/health.mjs +97 -24
  17. package/lib/install-freshness.mjs +2 -2
  18. package/lib/jev-checks.mjs +13 -0
  19. package/lib/job-format.mjs +3 -1
  20. package/lib/judge.mjs +26 -5
  21. package/lib/line-diff.mjs +46 -0
  22. package/lib/mutation.mjs +27 -13
  23. package/lib/openclaw-run.mjs +21 -7
  24. package/lib/openclaw-runtime-health.mjs +56 -0
  25. package/lib/process.mjs +30 -4
  26. package/lib/repo-query.mjs +4 -4
  27. package/lib/sandbox-images.mjs +31 -4
  28. package/lib/schema.mjs +28 -0
  29. package/lib/server-context.mjs +3 -2
  30. package/lib/share.mjs +53 -2
  31. package/lib/stats.mjs +11 -0
  32. package/lib/statusline.mjs +2 -2
  33. package/lib/subscription-setup.mjs +13 -0
  34. package/lib/trust-checks.mjs +269 -0
  35. package/lib/trust-files.mjs +123 -0
  36. package/lib/trust-judgment.mjs +157 -0
  37. package/lib/trust-learning.mjs +153 -0
  38. package/lib/trust-map.mjs +149 -0
  39. package/lib/trust-reach.mjs +220 -0
  40. package/lib/trust.mjs +227 -0
  41. package/lib/usage-limits.mjs +224 -21
  42. package/lib/validators.mjs +7 -0
  43. package/lib/verification-flow.mjs +25 -18
  44. package/lib/verification-network.mjs +2 -2
  45. package/lib/verify.mjs +46 -3
  46. package/lib/worktree-pointer.mjs +246 -0
  47. package/lib/worktree-write.mjs +128 -0
  48. package/mcp/server.mjs +43 -19
  49. package/package.json +1 -1
  50. package/playbooks/feature.md +5 -5
  51. package/scripts/trust-measure.mjs +139 -0
package/bin/nomarmy.mjs CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import { acknowledgeTrust, collectReviewEvidence, trustSuggestions, reviewTrustLearning, trustAcknowledgment } from "../lib/trust-learning.mjs";
3
+ import { writeTrustScoutBrief, writeTrustProposal, loadTrustProposal, trustProposalOrigins } from "../lib/trust-map.mjs";
4
+ import { fillAcceptance, gatherAcceptanceProposals } from "../lib/acceptance-fill.mjs";
2
5
  // nomArmy CLI. Every command proposes before it writes anything -- init,
3
6
  // setup, model and update all show exactly what would change and write only
4
7
  // after explicit confirmation ([y/N]) or an explicit non-interactive flag
@@ -9,10 +12,12 @@
9
12
  import fs from "node:fs";
10
13
  import os from "node:os";
11
14
  import path from "node:path";
15
+ import { execHostGitSync, spawnHostGitSync } from "../lib/worktree-pointer.mjs";
12
16
  import { spawn, spawnSync, execFileSync } from "node:child_process";
13
17
  import { createInterface } from "node:readline/promises";
14
18
  import { stdin as input, stdout as output } from "node:process";
15
19
  import { loadConfig, validateConfig, stringifyConfig, findConfigFile, parseYaml, CONFIG_FILENAMES } from "../lib/config.mjs";
20
+ import { loadContract, loadContracts, checkContract, contractDisplayPath } from "../lib/acceptance.mjs";
16
21
  import { scanRepository, compareEvidence } from "../lib/scan.mjs";
17
22
  import { buildConfigProposal } from "../lib/propose.mjs";
18
23
  import { detectHardware } from "../lib/hardware.mjs";
@@ -24,23 +29,24 @@ import { connectViaWsl, connectClaude, connectCodex, connectCursor, cursorAlread
24
29
  import { compareVersions, readPackageVersion, readInstallVersions, copyIsStale } from "../lib/install-freshness.mjs";
25
30
  import { loadJobRecords, computeStats, formatStats, formatStatsSummary, parseSince, resolveRepo, agentLookup } from "../lib/stats.mjs";
26
31
  import { requestJobStop } from "../lib/openclaw-run.mjs";
27
- import { loadValidators, saveJevKey, removeJev, jevSettings, askJev, validatorsPath, JEV_CHECKS, saveJudge, removeJudge, judgeSettings, judgeAgentChoices, chooseJudgeAgent, confirmJudgeHostTools } from "../lib/validators.mjs";
32
+ import { loadValidators, saveJevKey, removeJev, jevSettings, askJev, validatorsPath, JEV_CHECKS, saveJudge, removeJudge, judgeSettings, judgeAgentChoices, chooseJudgeAgent, confirmJudgeHostTools, validatorTrustDisclosure } from "../lib/validators.mjs";
28
33
  import { probeModel } from "../lib/model-probe.mjs";
29
34
  import { ID_RE, AUTH_ENV_NAME_RE, OPENCLAW_PROVIDER_ID_RE, openclawProviderId, isNativeProviderType } from "../lib/dispatch-schema.mjs";
30
35
  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";
31
36
  import { loadArmy, mergeArmy, describeArmy, readArmyFile, updateArmyInFile, assignRoleInFile, parseTargetSpec, armyLayerPath, globalConfigDir, DEFAULT_ARMY, ARMY_PHASES, LOCAL_CONFIG_FILENAME } from "../lib/army.mjs";
32
37
  import { parseLlamaUrl } from "../lib/execution.mjs";
33
38
  import { setupSteps, formatSetupSteps, runSetupPlaybook } from "../lib/setup-steps.mjs";
34
- import { readUsageSnapshots } from "../lib/usage-limits.mjs";
39
+ import { readUsageSnapshots, refreshStaleOverLimitReadings } from "../lib/usage-limits.mjs";
35
40
  import { pickMachine, planResize } from "../lib/sandbox-vm.mjs";
36
41
  import { listProcesses, staleSessions, formatStaleSessions } from "../lib/stale-sessions.mjs";
37
42
  import { readSetting, writeSetting, writeEnvLine, userCommonPath, userProfilePath, profilePathFor, tildePath } from "../lib/user-config.mjs";
38
43
  import { MIN_PODMAN_VM_MB } from "../lib/doctor.mjs";
39
44
  import { liveLeases } from "../lib/slots.mjs";
40
45
  import { ensureProviderConfig } from "../lib/openclaw-config.mjs";
41
- import { recordProbeSuccess, parseOpenclawAuthProfiles } from "../lib/health.mjs";
46
+ import { linkCodex } from "../lib/codex-link.mjs";
47
+ import { recordProbeSuccess, parseOpenclawAuthProfiles, codexImportRecovery } from "../lib/health.mjs";
42
48
  import { pruneJobRuntime } from "../lib/prune.mjs";
43
- import { SUBSCRIPTION_VENDORS, parseOpenclawVersion, versionAtLeast, parseCatalogModels, parseCliLoginStatus, probeOutcome, parseMuseAuthDescriptor, extractMintedKey } from "../lib/subscription-setup.mjs";
49
+ import { SUBSCRIPTION_VENDORS, parseOpenclawVersion, versionAtLeast, parseCatalogModels, parseCliLoginStatus, probeOutcome, openclawSignInFailure, parseMuseAuthDescriptor, extractMintedKey } from "../lib/subscription-setup.mjs";
44
50
  import { PINNED_OPENCLAW_VERSION, repairOpenclaw, verifyOpenclaw, configuredSubscriptionVendors } from "../lib/openclaw-install.mjs";
45
51
  import { ensureOpenClawOnPath } from "../lib/openclaw-path.mjs";
46
52
  import { THINKING_LEVELS } from "../lib/thinking.mjs";
@@ -115,6 +121,11 @@ function usage(code = 0) {
115
121
 
116
122
  Usage: nomarmy <command> [options]
117
123
 
124
+ acceptance fill <run-id|job-id> [--dry-run] [--json]
125
+ Append proposed test proofs to feature contracts.
126
+ acceptance check [file...] [--json] [--strict]
127
+ Check feature contracts in acceptance/*.yml; runs this repository's tests on this machine.
128
+ --strict fails on unproven criteria as well as broken or missing.
118
129
  scan Inspect this repository and report its execution environment.
119
130
  --check compare the evidence against a committed .nomarmy.yml
120
131
  init Propose a .nomarmy.yml from this repository's scan evidence
@@ -145,6 +156,11 @@ Usage: nomarmy <command> [options]
145
156
  update Update nomArmy and reconnect your coordinators: installs
146
157
  npm's latest alpha, or for a git checkout pulls (fast-forward
147
158
  only; refuses on local changes). Then restart open sessions.
159
+ trust map Write a read-only review scout brief for the General.
160
+ --from <scout.yml> --scout-job <job-id> records the cited scout origin.
161
+ trust review Accept, drop or edit scout and evidence-backed proposals.
162
+ --json lists; --accept-all or --decisions '<JSON array>' writes.
163
+ trust ack <job> --accept|--reject [--reason "text"] Sign off a human gate.
148
164
  agents <list|add|update|remove>
149
165
  Every account a job can run on, in one list:
150
166
  ~/.config/nomarmy/agents.yml (or NOMARMY_CONFIG_DIR).
@@ -174,7 +190,13 @@ Usage: nomarmy <command> [options]
174
190
  --model --auth-env [--base-url]
175
191
  [--openclaw-provider --plugin] [--register]
176
192
  [--update-mcp] (api); --provider --model
177
- --owner (subscription); and optionally
193
+ --owner (subscription). Codex JSON setup can
194
+ --link-openclaw after CLI login; removing a
195
+ capturing email profile non-interactively needs
196
+ --remove-email-profiles. Interactive Codex setup
197
+ asks before copying the CLI login (default yes),
198
+ and offers email-profile removal separately.
199
+ For any kind, optionally
178
200
  --max-concurrent --context-window
179
201
  --thinking [minimal|low|medium|high|xhigh|adaptive|max|ultra] --no-thinking
180
202
  update <name>
@@ -186,7 +208,7 @@ Usage: nomarmy <command> [options]
186
208
  --no-model clears the default model, so every
187
209
  role or job names its own.
188
210
  (--json with the matching flags; --probe to
189
- test-call a subscription's new model first)
211
+ test-call the agent; without a name, all agents)
190
212
  remove <name>
191
213
  remove one agent
192
214
  army <show|init|assign|general>
@@ -1256,6 +1278,7 @@ function catalogModelsFor(provider) {
1256
1278
  /** One real, one-token completion through OpenClaw to test a model. */
1257
1279
  // Why the last probeWorker() call failed, in the vendor's words when it said.
1258
1280
  let lastProbeFailure = null;
1281
+ let lastProbeText = "";
1259
1282
  function probeWorker(provider, model) {
1260
1283
  // The route a job takes: the ambient OpenClaw config and a state dir of
1261
1284
  // its own, never --isolated. --isolated skips that config, and with it the
@@ -1271,7 +1294,10 @@ function probeWorker(provider, model) {
1271
1294
  "--json", "--cwd", cwd, "--state-dir", stateDir, "--timeout", "90"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], cwd });
1272
1295
  // Streams kept apart: OpenClaw logs a "run ... ended" line to stderr
1273
1296
  // AFTER the JSON envelope, and the merged text doesn't parse.
1274
- const outcome = probeOutcome({ stdout: result.stdout ?? "", stderr: result.stderr ?? "" });
1297
+ const stdout = result.stdout ?? "";
1298
+ const stderr = result.stderr ?? "";
1299
+ const outcome = probeOutcome({ stdout, stderr });
1300
+ lastProbeText = `${stderr}\n${stdout}`;
1275
1301
  lastProbeFailure = outcome.ok ? null : outcome.reason;
1276
1302
  if (outcome.ok) recordProbeSuccess(agentStateRoot(), `${provider}/${model}`);
1277
1303
  return outcome.ok;
@@ -1303,10 +1329,15 @@ function reapProbeSandbox(stateDir) {
1303
1329
  /**
1304
1330
  * OpenClaw's own provider login, for the vendors whose plugin wants one on
1305
1331
  * top of the vendor CLI's login (Claude doesn't: OpenClaw reuses the CLI
1306
- * session directly). Run when the catalog or auth-profile check shows
1307
- * linking is needed, never preemptively.
1332
+ * session directly). Codex always refreshes its imported copy after CLI
1333
+ * login is confirmed; other providers link when catalog or auth checks fail.
1308
1334
  */
1309
- function openclawProviderLogin(vendor) {
1335
+ async function openclawProviderLogin(vendor, rl) {
1336
+ if (vendor === SUBSCRIPTION_VENDORS.codex) return linkCodex({
1337
+ run: runQuiet, command: openclawCmd(), isTTY: Boolean(input.isTTY),
1338
+ importLogin: flag("link-openclaw"), removeEmailProfiles: flag("remove-email-profiles"),
1339
+ confirm: (prompt, opts) => confirm(rl, prompt, opts), print: (message) => console.log(c.dim(message)),
1340
+ });
1310
1341
  const provider = vendor.credential.loginProvider ?? vendor.provider;
1311
1342
  console.log(c.dim(`Linking OpenClaw to it (\`openclaw models auth login --provider ${provider}\`) -- follow its prompts:`));
1312
1343
  const ok = runInteractive(openclawCmd(), ["models", "auth", "login", "--provider", provider]);
@@ -1542,7 +1573,7 @@ async function addApiAgent(rl, agents) {
1542
1573
  const OWNER_EMAIL_RE = /^[^@\s]+@[^@\s]+$/;
1543
1574
  function validOwnerEmail(email) { return typeof email === "string" && OWNER_EMAIL_RE.test(email); }
1544
1575
  function gitUserEmail() {
1545
- const result = spawnSync("git", ["config", "user.email"], { cwd: repoDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
1576
+ const result = spawnHostGitSync(repoDir, ["config", "user.email"], { cwd: repoDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
1546
1577
  const email = result.status === 0 ? result.stdout.trim() : "";
1547
1578
  return validOwnerEmail(email) ? email : "";
1548
1579
  }
@@ -1575,16 +1606,23 @@ async function addSubscriptionAgent(rl, agents) {
1575
1606
  console.log(`\n${c.bold("→")} Models`);
1576
1607
  const needsOpenclawLogin = vendor.credential.kind === "openclaw-login";
1577
1608
  let linkedOpenclaw = false;
1609
+ if (vendorKey === "codex") {
1610
+ linkedOpenclaw = await openclawProviderLogin(vendor, rl);
1611
+ if (!linkedOpenclaw) {
1612
+ console.log(c.red("✗ Sign-in has no usable auth profile. Retry with `nomarmy agents add subscription codex`."));
1613
+ process.exitCode = 1; return;
1614
+ }
1615
+ }
1578
1616
  let models = catalogModelsFor(vendor.provider);
1579
- if (!models.length && needsOpenclawLogin) {
1580
- linkedOpenclaw = openclawProviderLogin(vendor);
1617
+ if (!models.length && needsOpenclawLogin && !linkedOpenclaw) {
1618
+ linkedOpenclaw = await openclawProviderLogin(vendor, rl);
1581
1619
  if (!linkedOpenclaw) { console.log(c.red(`✗ Sign-in failed or was canceled. Retry with \`nomarmy agents add subscription ${vendorKey}\`.`)); process.exitCode = 1; return; }
1582
1620
  models = catalogModelsFor(vendor.provider);
1583
1621
  }
1584
1622
  // Catalog entries can be cached, and a particular model can refuse a
1585
1623
  // working login. Check OpenClaw's auth profile before asking for details.
1586
1624
  if (needsOpenclawLogin && !hasUsableOpenclawAuthProfile(vendor.provider)) {
1587
- if (linkedOpenclaw || !openclawProviderLogin(vendor) || !hasUsableOpenclawAuthProfile(vendor.provider)) {
1625
+ if (linkedOpenclaw || !(await openclawProviderLogin(vendor, rl)) || !hasUsableOpenclawAuthProfile(vendor.provider)) {
1588
1626
  console.log(c.red(`✗ Sign-in has no usable auth profile. Retry with \`nomarmy agents add subscription ${vendorKey}\`.`));
1589
1627
  process.exitCode = 1;
1590
1628
  return;
@@ -1598,7 +1636,8 @@ async function addSubscriptionAgent(rl, agents) {
1598
1636
  const pick = (await rl.question(c.bold(`Default model, optional${models.length ? " (a number or an id)" : ""}; blank = pick per role: `))).trim();
1599
1637
  const model = /^\d+$/.test(pick) && models.length ? models[Number(pick) - 1] : pick || null;
1600
1638
  if (pick && !model) throw new Error(`Not a valid choice: "${pick}".`);
1601
- // The test call needs some model; it checks the model, not the login.
1639
+ // A usable profile is only a fast precondition. The test call is what
1640
+ // proves sign-in: an unexpired Codex import can still answer 401.
1602
1641
  const probeModel = model ?? models[0] ?? vendor.defaultModel;
1603
1642
 
1604
1643
  const ownerDefault = validOwnerEmail(auth.email) ? auth.email : gitUserEmail();
@@ -1611,9 +1650,17 @@ async function addSubscriptionAgent(rl, agents) {
1611
1650
  if (!name) { console.log(c.dim("Stopped; nothing was written.")); return; }
1612
1651
 
1613
1652
  console.log(`\n${c.bold("→")} Test call`);
1614
- const works = probeModel ? probeWorker(vendor.provider, probeModel) : false;
1653
+ const probed = Boolean(probeModel);
1654
+ const works = probed ? probeWorker(vendor.provider, probeModel) : false;
1615
1655
  if (works) console.log(c.green(`✓ ${vendor.provider}/${probeModel} answered a real test prompt.`));
1616
- else {
1656
+ else if (needsOpenclawLogin && probed && openclawSignInFailure(`${lastProbeFailure ?? ""}\n${lastProbeText}`)) {
1657
+ const why = lastProbeFailure ? `: ${lastProbeFailure}` : "";
1658
+ console.log(c.red(`✗ Sign-in failed${why}.`));
1659
+ console.log(` fix: ${codexImportRecovery()}`);
1660
+ console.log(c.dim("Stopped; nothing was written."));
1661
+ process.exitCode = 1;
1662
+ return;
1663
+ } else {
1617
1664
  console.log(c.yellow(probeModel ? `⚠ The login works, but ${vendor.provider}/${probeModel} didn't answer a real test prompt.` : "⚠ The login works, but no model is available for a test call."));
1618
1665
  console.log(c.dim(`You can retry later with \`nomarmy agents update ${name} --probe\`.`));
1619
1666
  if (!(await confirm(rl, "Save the agent anyway?", { defaultYes: false }))) {
@@ -1648,6 +1695,17 @@ async function cmdAgentsAddJson() {
1648
1695
  const thinking = resolveThinkingFlag();
1649
1696
  if (thinking !== undefined) agent.thinking = thinking;
1650
1697
  }
1698
+ if (kind === "subscription" && agent.provider === "openai" && (flag("link-openclaw") || flag("remove-email-profiles"))) {
1699
+ if (!readLoginStatus("codex").loggedIn) throw new Error("Confirm Codex CLI login first: codex login");
1700
+ const linked = await linkCodex({ run: runQuiet, command: openclawCmd(),
1701
+ importLogin: flag("link-openclaw"), removeEmailProfiles: flag("remove-email-profiles"), print: (message) => console.error(message) });
1702
+ if (!linked) throw new Error(flag("link-openclaw")
1703
+ ? "Codex import failed; nothing was written."
1704
+ : "Codex login was not imported: pass --link-openclaw to authorize copying it into OpenClaw. Nothing was written.");
1705
+ if (!probeWorker("openai", agent.model ?? SUBSCRIPTION_VENDORS.codex.defaultModel)) {
1706
+ throw new Error(`Codex test call failed; nothing was written. Fix: ${codexImportRecovery()}`);
1707
+ }
1708
+ }
1651
1709
  const written = saveAgents({ ...agents, [name]: agent });
1652
1710
  const saved = written[name];
1653
1711
  let registered = null, mcpUpdated = false;
@@ -1730,7 +1788,7 @@ async function cmdAgentsUpdate() {
1730
1788
  if (flag("owner") || value("owner") !== null || value("provider") !== null || value("kind") !== null) {
1731
1789
  throw new Error("Kind, provider and owner can't be changed -- that's a different agent. Use `nomarmy agents add`.");
1732
1790
  }
1733
- probe = flag("probe") && current.kind === "subscription";
1791
+ probe = flag("probe");
1734
1792
  if (!Object.keys(changes).length && !probe) throw new Error("Nothing to update -- pass at least one field flag (see `nomarmy agents update` usage).");
1735
1793
  } else {
1736
1794
  if (!process.stdin.isTTY) throw new Error("nomarmy agents update needs an interactive terminal, or the flags to change (e.g. --max-concurrent 3).");
@@ -1779,7 +1837,7 @@ async function cmdAgentsUpdate() {
1779
1837
  }
1780
1838
 
1781
1839
  if (probe && !Object.keys(changes).length) { probeConfiguredAgent(name, current); return; }
1782
- if (probe && changes.model && !probeWorker(current.provider, changes.model)) {
1840
+ if (probe && changes.model && !probeWorker(agentProviderId(current), changes.model)) {
1783
1841
  out({ error: `a real test prompt to ${current.provider}/${changes.model} didn't come back -- nothing was written` });
1784
1842
  process.exit(1);
1785
1843
  }
@@ -1802,7 +1860,7 @@ async function cmdAgents() {
1802
1860
  }
1803
1861
 
1804
1862
  function git(args) {
1805
- try { return execFileSync("git", args, { cwd: nomarmyRoot, encoding: "utf8" }).trim(); }
1863
+ try { return execHostGitSync(nomarmyRoot, args, { cwd: nomarmyRoot, encoding: "utf8" }).trim(); }
1806
1864
  catch (error) { throw new Error(`git ${args.join(" ")} failed: ${error.stderr ? String(error.stderr).trim() : error.message}`); }
1807
1865
  }
1808
1866
 
@@ -1988,7 +2046,7 @@ async function cmdConnect() {
1988
2046
  if (nativeWindows && flag("copy-only")) throw new Error("Windows runs nomArmy inside WSL; run nomarmy connect <target> instead of --copy-only");
1989
2047
  let projectDir = null;
1990
2048
  if (scope !== "user") {
1991
- try { projectDir = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: repoDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); }
2049
+ try { projectDir = execHostGitSync(repoDir, ["rev-parse", "--show-toplevel"], { cwd: repoDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); }
1992
2050
  catch { throw new Error(`--scope ${scope} registers nomArmy for one repository, and ${repoDir} isn't inside a git repository. Run it from the repository (or pass --repo <dir>).`); }
1993
2051
  }
1994
2052
  if (flag("copy-only")) {
@@ -2405,7 +2463,7 @@ function armyLayerFlag(fallback = "global") {
2405
2463
  return chosen[0] ?? fallback;
2406
2464
  }
2407
2465
 
2408
- function loadArmyForCli({ globalOnly = false } = {}) {
2466
+ function loadArmyForCli({ globalOnly = false, usageRefresh = null } = {}) {
2409
2467
  const agents = loadAgentsOrExit().agents;
2410
2468
  const loaded = globalOnly
2411
2469
  ? (() => {
@@ -2414,8 +2472,8 @@ function loadArmyForCli({ globalOnly = false } = {}) {
2414
2472
  return { ...mergeArmy([{ layer: "global", army }]), layers: [{ layer: "global", path: filePath, exists: fs.existsSync(filePath), hasArmy: Boolean(army) }] };
2415
2473
  })()
2416
2474
  : loadArmy({ projectDir: repoDir });
2417
- const usageSnapshots = readUsageSnapshots(process.env.NOMARMY_AGENT_STATE || path.join(os.homedir(), ".local", "share", "nomarmy-local-agents"));
2418
- return { loaded, agents, summary: describeArmy(loaded, { agents, describeAgent: describeAgentLabel, usageSnapshots, agentProviderId }) };
2475
+ const usageSnapshots = usageRefresh?.snapshots ?? readUsageSnapshots(process.env.NOMARMY_AGENT_STATE || path.join(os.homedir(), ".local", "share", "nomarmy-local-agents"));
2476
+ return { loaded, agents, summary: describeArmy(loaded, { agents, describeAgent: describeAgentLabel, usageSnapshots, agentProviderId, usageRefreshError: usageRefresh?.error ?? null, usageRefreshFailed: usageRefresh?.failedProviders ?? null }) };
2419
2477
  }
2420
2478
 
2421
2479
  // Claude Code adds settings.local.json to .gitignore for the same reason:
@@ -2453,7 +2511,9 @@ function repositoryHere(dir) {
2453
2511
 
2454
2512
  async function cmdArmyShow() {
2455
2513
  const inRepository = repositoryHere(repoDir);
2456
- const { summary } = loadArmyForCli({ globalOnly: !inRepository });
2514
+ const stateRoot = process.env.NOMARMY_AGENT_STATE || path.join(os.homedir(), ".local", "share", "nomarmy-local-agents");
2515
+ const usageRefresh = await refreshStaleOverLimitReadings(stateRoot);
2516
+ const { summary } = loadArmyForCli({ globalOnly: !inRepository, usageRefresh });
2457
2517
  if (json) return out(summary);
2458
2518
  const g = summary.general;
2459
2519
  console.log(c.bold("🪖 nomArmy") + (inRepository ? c.dim(` (${repoDir})`) : ""));
@@ -2727,6 +2787,7 @@ function collectJobs({ recent = 8, runId = null, projectDir = null } = {}) {
2727
2787
  lastTool: status.lastTool ? `${status.lastTool.tool}${status.lastTool.target ? ` ${String(status.lastTool.target).slice(0, 40)}` : ""}` : null,
2728
2788
  filesChanged: status.filesChangedLive ?? meta?.git?.filesChanged?.length ?? null,
2729
2789
  heartbeatAgeSeconds: status.heartbeatAt ? Math.round((Date.now() - Date.parse(status.heartbeatAt)) / 1000) : null,
2790
+ ...(meta?.trust ? { trust: meta.trust } : {}),
2730
2791
  started, dir,
2731
2792
  };
2732
2793
  }).filter(Boolean).sort((a, b) => b.started - a.started);
@@ -2746,6 +2807,10 @@ function renderJobs({ running, recent }) {
2746
2807
  }
2747
2808
  lines.push("", c.bold("Recent"));
2748
2809
  for (const j of recent) lines.push(` ${j.jobId.padEnd(40)} ${String(j.phase).padEnd(20)} ${fmtSeconds(j.elapsedSeconds).padStart(7)} ${c.dim(`${j.agent ?? ""}${j.model ? `/${j.model}` : ""}`)}`);
2810
+ for (const j of [...running, ...recent]) if (j.trust?.level === "human") {
2811
+ const ack = trustAcknowledgment(j.trust);
2812
+ lines.push(` ${j.jobId}: human review ${ack ? `acknowledged (${ack.decision}) by ${ack.who} at ${ack.when}${ack.reason ? `: ${ack.reason}` : ""}` : "pending"}`);
2813
+ }
2749
2814
  return lines.join("\n");
2750
2815
  }
2751
2816
 
@@ -2920,6 +2985,7 @@ async function cmdHealth() {
2920
2985
  const { checkAndRecordHealth } = await import("../lib/health.mjs");
2921
2986
  const stateRoot = process.env.NOMARMY_AGENT_STATE || path.join(os.homedir(), ".local", "share", "nomarmy-local-agents");
2922
2987
  const { result } = await checkAndRecordHealth({ projectDir: repoDir, stateRoot, configDir: globalConfigDir(), env: installEnv() });
2988
+ if (result.issues.some((i) => i.severity === "error")) process.exitCode = 1;
2923
2989
  if (json) return out(result);
2924
2990
  console.log(c.bold("🍪 nomArmy health") + c.dim(` ${new Date(result.checkedAt).toLocaleString()}`));
2925
2991
  if (!result.issues.length) { console.log(c.green("\n✓ Nothing to fix.")); return; }
@@ -2947,7 +3013,7 @@ async function shareStats(stats) {
2947
3013
  if (flag("share")) console.log(shareMarkdown(stats, { scope }));
2948
3014
  if (flag("badge")) {
2949
3015
  const given = value("badge");
2950
- const top = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" });
3016
+ const top = spawnHostGitSync(process.cwd(), ["rev-parse", "--show-toplevel"], { encoding: "utf8" });
2951
3017
  const root = top.status === 0 ? top.stdout.trim() : process.cwd();
2952
3018
  const file = path.resolve(root, given ?? path.join(".github", "nomarmy-badge.svg"));
2953
3019
  fs.mkdirSync(path.dirname(file), { recursive: true });
@@ -2964,7 +3030,7 @@ function cmdStats() {
2964
3030
  let repo = null;
2965
3031
  if (value("repo")) repo = resolveRepo(records, value("repo"));
2966
3032
  else if (!flag("all-repos")) {
2967
- try { repo = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: repoDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); }
3033
+ try { repo = execHostGitSync(repoDir, ["rev-parse", "--show-toplevel"], { cwd: repoDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); }
2968
3034
  catch { throw new Error(`${repoDir} isn't inside a git repository; run nomarmy stats from one, or pass --repo <name> or --all-repos`); }
2969
3035
  }
2970
3036
  let agentFor = () => null;
@@ -3010,10 +3076,18 @@ async function cmdValidators() {
3010
3076
  try { config = loadValidators(); } catch (error) { if (json) return out({ error: error.message }); console.log(c.red(error.message)); process.exitCode = 1; return; }
3011
3077
  const judge = config.judge ? { enabled: config.judge.enabled, agent: config.judge.agent, model: config.judge.model, checks: config.judge.checks, hostTools: config.judge.host_tools } : null;
3012
3078
  const jev = config.jev ? { enabled: config.jev.enabled, checks: config.jev.checks, model: config.jev.model, key: config.jev.key_env ? `env ${config.jev.key_env}` : config.jev.key_file, keyReadable: Boolean(jevSettings()) } : null;
3079
+ if (jev) jev.trustJudgment = validatorTrustDisclosure("Jev", "TypeSafe");
3080
+ if (judge) {
3081
+ const agents = loadAgents(globalConfigDir()).agents;
3082
+ const vendor = agentProviderId(agents[judge.agent]) ?? `the configured vendor for agent "${judge.agent}" (agent unavailable)`;
3083
+ judge.trustJudgment = validatorTrustDisclosure("the judge", vendor);
3084
+ }
3013
3085
  if (json) return out({ path: validatorsPath(), jev, judge });
3014
3086
  if (!jev && !judge) { console.log("No validators configured. Add one with: nomarmy validators add jev, or nomarmy validators add judge --agent <name> --model <model>"); return; }
3015
3087
  if (jev) console.log(`Jev: ${jev.enabled ? c.green("on") : "off"} (${jev.model}); checks: ${jev.checks.join(", ")}; key: ${jev.key}${jev.keyReadable ? "" : c.red(" (not readable)")}`);
3088
+ if (jev) console.log(` Also drives the trust judgment. ${jev.trustJudgment}`);
3016
3089
  if (judge) console.log(`Judge: ${judge.enabled ? c.green("on") : "off"} (${judge.agent}/${judge.model}); checks: ${judge.checks.join(", ")}${judge.hostTools ? c.yellow("; host tools allowed") : ""}`);
3090
+ if (judge) console.log(` Also drives the trust judgment. ${judge.trustJudgment}`);
3017
3091
  return;
3018
3092
  }
3019
3093
  if (name === "judge") return cmdValidatorsJudge(sub);
@@ -3025,12 +3099,14 @@ async function cmdValidators() {
3025
3099
  console.log("Its answers only add review flags; they never pass a check or allow a commit.");
3026
3100
  console.log(c.yellow("It sends excerpts of your code (findings, cited lines, diffs, worker reports) to TypeSafe.\n"));
3027
3101
  }
3102
+ const trustJudgment = validatorTrustDisclosure("Jev", "TypeSafe");
3103
+ (json ? console.error : console.log)(`Before saving: ${trustJudgment}`);
3028
3104
  const key = flag("key-stdin") ? await readStdin() : await readHiddenLine("TypeSafe API key (not shown): ");
3029
3105
  const saved = saveJevKey(key);
3030
3106
  let ok = false, why = null;
3031
3107
  try { ok = await testJev(jevSettings()); } catch (error) { why = error.message; }
3032
- if (json) return out({ saved: true, keyFile: saved.keyFile, configPath: saved.configPath, test: ok ? "pass" : "fail", reason: why });
3033
- console.log(c.green(`✓ Saved the key to ${saved.keyFile} (readable only by you) and turned Jev on in ${saved.configPath}.`));
3108
+ if (json) return out({ saved: true, keyFile: saved.keyFile, configPath: saved.configPath, test: ok ? "pass" : "fail", reason: why, trustJudgment });
3109
+ console.log(c.green(`✓ Saved the key to ${saved.keyFile} (readable only by you) and turned Jev on in ${saved.configPath}. ${trustJudgment}`));
3034
3110
  console.log(ok ? c.green("✓ Test call answered. New jobs use it; restart open coordinator sessions to pick it up.") : c.red(`✗ Test call failed: ${why ?? "no answer"}. Check the key, then: nomarmy validators test jev`));
3035
3111
  if (!ok) process.exitCode = 1;
3036
3112
  return;
@@ -3086,6 +3162,8 @@ async function cmdValidatorsJudge(sub) {
3086
3162
  model = /^\d+$/.test(answer) && listed[Number(answer) - 1] ? listed[Number(answer) - 1] : answer || fallback;
3087
3163
  if (!model) { rl?.close(); throw new Error("A model id is required."); }
3088
3164
  }
3165
+ const trustJudgment = validatorTrustDisclosure("the judge", agentProviderId(agents[agent]));
3166
+ (json ? console.error : console.log)(`Before saving: ${trustJudgment}`);
3089
3167
  let hostTools = flag("host-tools");
3090
3168
  if (agentRunsToolsOnHost(agents[agent]) && !hostTools) {
3091
3169
  if (!process.stdin.isTTY || json) {
@@ -3102,8 +3180,8 @@ async function cmdValidatorsJudge(sub) {
3102
3180
  const settings = resolve();
3103
3181
  if (settings?.problem) throw new Error(settings.problem);
3104
3182
  const test = await probe(settings);
3105
- if (json) return out({ saved: true, configPath: saved.configPath, test: test.ok ? "pass" : test.refused ? "refused" : "inconclusive", reason: test.reason });
3106
- console.log(c.green(`✓ The judge is ${agent}/${model}, in ${saved.configPath}.`));
3183
+ if (json) return out({ saved: true, configPath: saved.configPath, test: test.ok ? "pass" : test.refused ? "refused" : "inconclusive", reason: test.reason, trustJudgment });
3184
+ console.log(c.green(`✓ The judge is ${agent}/${model}, in ${saved.configPath}. ${trustJudgment}`));
3107
3185
  if (dominantBuilderVendor === null) {
3108
3186
  try {
3109
3187
  const roles = loadArmy({ projectDir: repoDir }).army.roles;
@@ -3141,7 +3219,111 @@ function cmdMcp() {
3141
3219
  child.on("exit", (code, signal) => { if (signal) process.kill(process.pid, signal); else process.exit(code ?? 1); });
3142
3220
  }
3143
3221
 
3144
- 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) };
3222
+ async function cmdAcceptance() {
3223
+ if (argv[1] === "fill") {
3224
+ const ids = [];
3225
+ for (let i = 2; i < argv.length; i++) {
3226
+ if (argv[i] === "--repo") { i++; continue; }
3227
+ if (["--json", "--dry-run"].includes(argv[i])) continue;
3228
+ if (argv[i].startsWith("--")) throw new Error(`Unknown acceptance option: ${argv[i]}`);
3229
+ ids.push(argv[i]);
3230
+ }
3231
+ if (ids.length !== 1) throw new Error("Usage: nomarmy acceptance fill <run-id|job-id> [--dry-run] [--json]");
3232
+ const proposals = gatherAcceptanceProposals({ id: ids[0], repoDir, jobsRoot: jobsRootDir(), runsRoot: path.join(agentStateRoot(), "runs") });
3233
+ const result = await fillAcceptance({ repoDir, proposals, dryRun: flag("dry-run") });
3234
+ if (json) out(result);
3235
+ else if (!result.criteria.length) console.log("No new acceptance proofs.");
3236
+ else for (const item of result.criteria) {
3237
+ console.log(`${item.criterion}: ${result.dryRun ? "would add" : "added"} ${item.added.length} proof(s) in ${item.file}; status ${item.status}`);
3238
+ for (const ref of item.added) console.log(` ${ref.file}: ${ref.test}`);
3239
+ }
3240
+ if (!json && result.verificationError) console.log(result.verificationError);
3241
+ return;
3242
+ }
3243
+ if (argv[1] !== "check") throw new Error("Usage: nomarmy acceptance check [file...] [--json] [--strict]");
3244
+ const files = [];
3245
+ for (let i = 2; i < argv.length; i++) {
3246
+ if (argv[i] === "--repo") { i++; continue; }
3247
+ if (["--json", "--strict"].includes(argv[i])) continue;
3248
+ if (argv[i].startsWith("--")) throw new Error(`Unknown acceptance option: ${argv[i]}`);
3249
+ files.push(argv[i]);
3250
+ }
3251
+ const contracts = files.length ? files.map((file) => loadContract(path.resolve(repoDir, file))) : loadContracts(repoDir);
3252
+ const results = contracts.map((contract) => ({
3253
+ file: contractDisplayPath(repoDir, contract.file),
3254
+ feature: contract.feature,
3255
+ criteria: checkContract(contract, { repoDir }),
3256
+ }));
3257
+ const totals = { met: 0, broken: 0, missing: 0, unproven: 0, retired: 0 };
3258
+ for (const result of results) for (const criterion of result.criteria) totals[criterion.status]++;
3259
+ if (json) out({ contracts: results, totals });
3260
+ else {
3261
+ results.forEach((result, index) => {
3262
+ console.log(c.bold(`${result.file}: ${result.feature}`));
3263
+ const idWidth = Math.max(0, ...result.criteria.map((criterion) => criterion.id.length));
3264
+ result.criteria.forEach((criterion, criterionIndex) => {
3265
+ const failed = ["broken", "missing"].includes(criterion.status);
3266
+ const label = failed ? c.red(criterion.status.toUpperCase())
3267
+ : criterion.status === "met" ? c.green("met") : c.yellow(criterion.status);
3268
+ const detail = criterion.failures.map((failure) => failure.command ?? `${failure.file}: ${failure.test}`).join("; ")
3269
+ || (criterion.status === "unproven" ? criterion.note ?? contracts[index].criteria[criterionIndex].note ?? "" : "");
3270
+ const manual = criterion.status === "met" && criterion.manual?.length
3271
+ ? ` (manual: ${criterion.manual.map((ref) => `${ref.checked_by}, ${ref.date}`).join("; ")})` : "";
3272
+ const skipped = criterion.notApplicable?.length ? ` (${criterion.notApplicable.length} proofs not run on ${process.platform})` : "";
3273
+ console.log(`${criterion.id.padEnd(idWidth)} ${label}${manual}${skipped}${detail ? ` ${detail}` : ""}`);
3274
+ });
3275
+ });
3276
+ console.log(`Total: ${Object.entries(totals).map(([status, count]) => `${count} ${status}`).join(", ")}`);
3277
+ }
3278
+ if (totals.broken || totals.missing || (flag("strict") && totals.unproven)) process.exitCode = 1;
3279
+ }
3280
+
3281
+ async function cmdTrust() {
3282
+ let result;
3283
+ if (argv[1] === "ack") {
3284
+ if (flag("accept") === flag("reject")) throw new Error("Choose exactly one of --accept or --reject");
3285
+ result = acknowledgeTrust({ jobsRoot: jobsRootDir(), stateDir: agentStateRoot(), operatorDir: repoDir, jobId: argv[2], decision: flag("accept") ? "accept" : "reject", reason: value("reason", "") });
3286
+ } else if (argv[1] === "map") {
3287
+ result = value("from")
3288
+ ? writeTrustProposal(repoDir, parseYaml(fs.readFileSync(path.resolve(repoDir, value("from")), "utf8")), { scoutJobId: value("scout-job") ?? null })
3289
+ : writeTrustScoutBrief({ operatorDir: repoDir, roles: loadArmy({ projectDir: repoDir }).army.roles });
3290
+ } else if (argv[1] === "review") {
3291
+ const learning = { stateDir: agentStateRoot(), operatorDir: repoDir };
3292
+ collectReviewEvidence({ ...learning, records: loadJobRecords(jobsRootDir()) });
3293
+ const stored = loadTrustProposal(repoDir);
3294
+ const suggestions = trustSuggestions(learning);
3295
+ const proposed = [...stored, ...suggestions.map(s => s.entry)];
3296
+ const origins = [...trustProposalOrigins(repoDir, stored), ...suggestions.map(s => ({ writer: "trust learning", status: "evidence-backed", evidence: s.evidence }))];
3297
+ if (json && !flag("accept-all") && !value("decisions")) return out({ status: "pending", entries: proposed, origins });
3298
+ let decisions = value("decisions") ? JSON.parse(value("decisions")) : undefined;
3299
+ if (!flag("accept-all") && decisions === undefined) {
3300
+ if (!input.isTTY) throw new Error("Use --accept-all or --decisions '<JSON array>' for scripted review; --json alone only lists proposals.");
3301
+ decisions = [];
3302
+ const rl = createInterface({ input, output });
3303
+ try {
3304
+ for (const [index, entry] of proposed.entries()) {
3305
+ const origin = origins[index];
3306
+ console.log(`Origin: ${origin.scoutJobId ?? "unknown"} (${origin.writer ?? origin.status})`);
3307
+ if (origin.evidence) console.log(`${origin.evidence.summary}; jobs: ${origin.evidence.jobIds.join(", ")}`);
3308
+ console.log(`${entry.symbol} (${entry.file}:${entry.line}), ${entry.category}: ${entry.reason}`);
3309
+ let action;
3310
+ do { action = (await rl.question("Accept, drop or edit? [a/d/e] ")).trim().toLowerCase(); } while (!["a", "d", "e"].includes(action));
3311
+ if (action !== "e") { decisions.push({ action: action === "a" ? "accept" : "drop" }); continue; }
3312
+ const edited = {};
3313
+ for (const key of ["symbol", "file", "category", "reason"]) edited[key] = (await rl.question(`${key} [${entry[key]}]: `)).trim() || entry[key];
3314
+ decisions.push({ action: "edit", entry: edited });
3315
+ }
3316
+ } finally { rl.close(); }
3317
+ }
3318
+ result = reviewTrustLearning({ ...learning, decisions, acceptAll: flag("accept-all"), expected: proposed });
3319
+ } else throw new Error("Usage: nomarmy trust map [--from scout.yml --scout-job job-id] | trust ack <job-id> --accept|--reject [--reason text] | trust review [--accept-all | --decisions '<JSON array>'] [--json]");
3320
+ if (json) out(result);
3321
+ else if (result.status === "acknowledged") console.log(`${result.jobId}: human review acknowledged (${result.ack.decision}) by ${result.ack.who} at ${result.ack.when}${result.ack.reason ? `: ${result.ack.reason}` : ""}`);
3322
+ else if (result.status === "awaiting-scout") console.log(`Scout brief: ${result.brief}. General must dispatch the read-only review scout, then import its cited YAML with nomarmy trust map --from scout.yml --scout-job job-id. Nothing is active yet.`);
3323
+ else console.log(`${result.status}: ${result.map ?? result.proposal} (${result.entries.length} entries)`);
3324
+ }
3325
+
3326
+ const commands = { trust: cmdTrust, acceptance: cmdAcceptance, 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) };
3145
3327
  // doctor command
3146
3328
  async function cmdDoctor() {
3147
3329
  if (windowsFrontEnd()) {
@@ -3150,7 +3332,10 @@ async function cmdDoctor() {
3150
3332
  }
3151
3333
  // Import lazily to avoid circular dependencies
3152
3334
  const { runDoctor } = await import("../lib/doctor.mjs");
3153
- const vendors = configuredSubscriptionVendors(loadAgentsOrExit().agents);
3335
+ const agents = loadAgentsOrExit().agents;
3336
+ const vendors = configuredSubscriptionVendors(agents);
3337
+ let armySummary = null;
3338
+ try { armySummary = describeArmy(loadArmy({ projectDir: repoDir }), { agents }); } catch { /* other doctor checks still run */ }
3154
3339
  let checks;
3155
3340
  if (flag("fix")) {
3156
3341
  const repaired = await repairOpenclaw({
@@ -3166,7 +3351,8 @@ async function cmdDoctor() {
3166
3351
  } else {
3167
3352
  checks = verifyOpenclaw({ command: openclawCmd(), vendors });
3168
3353
  }
3169
- await runDoctor({ json, exit: true, env: installEnv(), additionalChecks: checks });
3354
+ await runDoctor({ json, exit: true, env: installEnv(), additionalChecks: checks,
3355
+ runtime: { agents, armySummary, openclawCmd: openclawCmd() } });
3170
3356
  }
3171
3357
  commands.doctor = cmdDoctor;
3172
3358
  if (windowsFrontEnd() && windowsPlan(argv) === "FORWARD") {