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.
- package/bin/nomarmy.mjs +220 -34
- package/lib/acceptance-fill.mjs +180 -0
- package/lib/acceptance-impact.mjs +215 -0
- package/lib/acceptance-result.mjs +21 -0
- package/lib/acceptance.mjs +166 -0
- package/lib/admission.mjs +84 -15
- package/lib/army.mjs +8 -6
- package/lib/codex-link.mjs +42 -0
- package/lib/connect.mjs +9 -5
- package/lib/continue-from.mjs +29 -12
- package/lib/coordinator-instructions.mjs +4 -0
- package/lib/diff-checks.mjs +9 -3
- package/lib/doctor.mjs +7 -2
- package/lib/execute.mjs +187 -32
- package/lib/git-record.mjs +9 -2
- package/lib/health.mjs +97 -24
- package/lib/install-freshness.mjs +2 -2
- package/lib/jev-checks.mjs +13 -0
- package/lib/job-format.mjs +3 -1
- package/lib/judge.mjs +26 -5
- package/lib/line-diff.mjs +46 -0
- package/lib/mutation.mjs +27 -13
- package/lib/openclaw-run.mjs +21 -7
- package/lib/openclaw-runtime-health.mjs +56 -0
- package/lib/process.mjs +30 -4
- package/lib/repo-query.mjs +4 -4
- package/lib/sandbox-images.mjs +31 -4
- package/lib/schema.mjs +28 -0
- package/lib/server-context.mjs +3 -2
- package/lib/share.mjs +53 -2
- package/lib/stats.mjs +11 -0
- package/lib/statusline.mjs +2 -2
- package/lib/subscription-setup.mjs +13 -0
- package/lib/trust-checks.mjs +269 -0
- package/lib/trust-files.mjs +123 -0
- package/lib/trust-judgment.mjs +157 -0
- package/lib/trust-learning.mjs +153 -0
- package/lib/trust-map.mjs +149 -0
- package/lib/trust-reach.mjs +220 -0
- package/lib/trust.mjs +227 -0
- package/lib/usage-limits.mjs +224 -21
- package/lib/validators.mjs +7 -0
- package/lib/verification-flow.mjs +25 -18
- package/lib/verification-network.mjs +2 -2
- package/lib/verify.mjs +46 -3
- package/lib/worktree-pointer.mjs +246 -0
- package/lib/worktree-write.mjs +128 -0
- package/mcp/server.mjs +43 -19
- package/package.json +1 -1
- package/playbooks/feature.md +5 -5
- 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 {
|
|
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)
|
|
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
|
|
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
|
|
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).
|
|
1307
|
-
*
|
|
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 =
|
|
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
|
-
//
|
|
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
|
|
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")
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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") {
|