nomarmy 0.1.0-alpha.0
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/LICENSE +202 -0
- package/NOTICE +25 -0
- package/README.md +484 -0
- package/bin/nomarmy.mjs +2248 -0
- package/config/agents.yml.example +63 -0
- package/config/common.env +31 -0
- package/config/profiles/bedrock-cheap.env +26 -0
- package/config/profiles/bedrock.env +28 -0
- package/config/profiles/cpu-linux.env +8 -0
- package/config/profiles/dgx-spark.env +12 -0
- package/config/profiles/macbook-pro.env +9 -0
- package/config/profiles/nvidia-linux.env +9 -0
- package/docker/Dockerfile +15 -0
- package/docker/Dockerfile.go +29 -0
- package/docker/Dockerfile.rust +19 -0
- package/e2e.sh +153 -0
- package/install.sh +125 -0
- package/lib/agents.mjs +285 -0
- package/lib/army.mjs +400 -0
- package/lib/budget.mjs +368 -0
- package/lib/claude-transcript.mjs +150 -0
- package/lib/config.mjs +193 -0
- package/lib/connect.mjs +409 -0
- package/lib/coordinator-instructions.mjs +23 -0
- package/lib/decompose.mjs +389 -0
- package/lib/dispatch-config.mjs +164 -0
- package/lib/dispatch-schema.mjs +280 -0
- package/lib/doctor.mjs +443 -0
- package/lib/evidence.mjs +679 -0
- package/lib/gguf.mjs +589 -0
- package/lib/hardware.mjs +476 -0
- package/lib/health.mjs +278 -0
- package/lib/model-catalog.mjs +71 -0
- package/lib/notifier-app.mjs +95 -0
- package/lib/notify.mjs +66 -0
- package/lib/openclaw-config.mjs +65 -0
- package/lib/openclaw-errors.mjs +40 -0
- package/lib/propose.mjs +110 -0
- package/lib/prune.mjs +77 -0
- package/lib/repo-query.mjs +267 -0
- package/lib/runs.mjs +150 -0
- package/lib/sabotage.mjs +128 -0
- package/lib/sandbox-images.mjs +434 -0
- package/lib/scan.mjs +1538 -0
- package/lib/schema.mjs +288 -0
- package/lib/scout.mjs +544 -0
- package/lib/sizing.mjs +1322 -0
- package/lib/slots.mjs +112 -0
- package/lib/statusline.mjs +126 -0
- package/lib/subscription-config.mjs +68 -0
- package/lib/subscription-setup.mjs +217 -0
- package/lib/transcript.mjs +195 -0
- package/lib/verify.mjs +700 -0
- package/mcp/server.mjs +4206 -0
- package/notifier/icon.swift +34 -0
- package/notifier/main.swift +52 -0
- package/notifier/nomarmy-icon.png +0 -0
- package/package.json +67 -0
- package/playbooks/feature.md +43 -0
- package/policies/coder.md +49 -0
- package/policies/orchestrator.md +35 -0
- package/policies/reviewer.md +35 -0
- package/policies/scout.md +65 -0
- package/scripts/configure-openclaw.sh +96 -0
- package/scripts/configure-orchestrator.sh +84 -0
- package/scripts/install-llama-cpp.sh +16 -0
- package/scripts/lib.sh +198 -0
- package/scripts/select-model.mjs +96 -0
- package/scripts/select-model.sh +4 -0
- package/scripts/setup-sandbox.sh +38 -0
- package/scripts/start-inference.sh +46 -0
- package/scripts/stop-inference.sh +5 -0
- package/scripts/uninstall.sh +6 -0
- package/scripts/verify-install.sh +68 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// What did the worker actually read, and was it worth it?
|
|
2
|
+
//
|
|
3
|
+
// OpenClaw keeps its session transcript in a sqlite database under the state
|
|
4
|
+
// directory nomArmy now gives it. From that transcript two things can be
|
|
5
|
+
// derived that the model's own report cannot be trusted to state:
|
|
6
|
+
//
|
|
7
|
+
// * which tools it called and which files it read, and
|
|
8
|
+
// * how much repository content came back through those tool results.
|
|
9
|
+
//
|
|
10
|
+
// The second number is the one this project exists for. It is roughly what
|
|
11
|
+
// the frontier coordinator would have carried in its own context to do the
|
|
12
|
+
// same reading. Compared with the size of the verified report it receives
|
|
13
|
+
// instead, it says whether a scout displaced frontier context or added to it.
|
|
14
|
+
// Estimates are labeled as such and use the same 4-chars-per-token rule as
|
|
15
|
+
// the budgets; the point is the sign and the order of magnitude.
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { CALIBRATED } from "./budget.mjs";
|
|
19
|
+
|
|
20
|
+
/** Locate OpenClaw's transcript database under a state directory. */
|
|
21
|
+
export function findTranscriptDb(stateDir) {
|
|
22
|
+
const stack = [stateDir];
|
|
23
|
+
while (stack.length) {
|
|
24
|
+
const dir = stack.pop();
|
|
25
|
+
let entries;
|
|
26
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
27
|
+
for (const e of entries) {
|
|
28
|
+
const p = path.join(dir, e.name);
|
|
29
|
+
if (e.isDirectory()) stack.push(p);
|
|
30
|
+
else if (e.isFile() && e.name === "openclaw-agent.sqlite") return p;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reduce raw transcript events to what the coordinator cares about. Pure:
|
|
38
|
+
* takes the parsed `event_json` objects in order.
|
|
39
|
+
*/
|
|
40
|
+
/** Tools whose results are repository content the coordinator would otherwise have read itself. */
|
|
41
|
+
export const REPO_READ_TOOLS = Object.freeze(["read", "cat", "view", "open", "ls", "glob", "grep", "search", "exec", "bash", "shell"]);
|
|
42
|
+
|
|
43
|
+
export function summarizeTranscriptEvents(events) {
|
|
44
|
+
const out = { modelCalls: 0, toolCalls: [], toolResultChars: 0, repoReadChars: 0, harnessChars: 0, assistantChars: 0, filesRead: [], commands: [],
|
|
45
|
+
// The most recent tool result's own text, overwritten as later ones
|
|
46
|
+
// arrive -- makeAbandonedBackgroundProcessTick reads this to tell "the
|
|
47
|
+
// worker's last known state was a backgrounded process handle" from
|
|
48
|
+
// anything else, without re-deriving it from raw events itself.
|
|
49
|
+
lastToolResultText: null,
|
|
50
|
+
// The final assistant message's text, which is the worker's report.
|
|
51
|
+
// Kept so a run whose work finished but whose exit failed (OpenClaw's
|
|
52
|
+
// own cleanup erroring after stopReason=stop, seen live with Codex)
|
|
53
|
+
// can still be salvaged instead of discarded.
|
|
54
|
+
lastAssistantText: null,
|
|
55
|
+
// Summed over assistant messages that carry it; null when none did.
|
|
56
|
+
usage: null };
|
|
57
|
+
const pending = []; // tool calls awaiting their result, in order
|
|
58
|
+
for (const e of events ?? []) {
|
|
59
|
+
if (e?.type !== "message") continue;
|
|
60
|
+
const m = e.message ?? {};
|
|
61
|
+
const content = Array.isArray(m.content) ? m.content : [];
|
|
62
|
+
if (m.role === "assistant") {
|
|
63
|
+
out.modelCalls++;
|
|
64
|
+
if (m.usage && typeof m.usage === "object") {
|
|
65
|
+
const u = (out.usage ??= { input: 0, output: 0, cacheRead: 0 });
|
|
66
|
+
for (const k of ["input", "output", "cacheRead"]) if (Number.isFinite(m.usage[k])) u[k] += m.usage[k];
|
|
67
|
+
}
|
|
68
|
+
const texts = content.filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text);
|
|
69
|
+
if (texts.length) out.lastAssistantText = texts.join("\n");
|
|
70
|
+
for (const c of content) {
|
|
71
|
+
if (c.type === "toolCall" || c.type === "tool_call" || c.type === "tool_use") {
|
|
72
|
+
const input = c.input ?? c.arguments ?? c.args ?? {};
|
|
73
|
+
const tool = c.name ?? null;
|
|
74
|
+
const call = { tool, path: typeof input.path === "string" ? input.path : typeof input.file === "string" ? input.file : null,
|
|
75
|
+
command: typeof input.command === "string" ? input.command : null, resultChars: 0,
|
|
76
|
+
repoRead: REPO_READ_TOOLS.includes(String(tool ?? "").toLowerCase()) };
|
|
77
|
+
out.toolCalls.push(call);
|
|
78
|
+
pending.push(call);
|
|
79
|
+
if (call.path && /^(read|cat|view|open)$/i.test(tool ?? "")) out.filesRead.push(call.path);
|
|
80
|
+
if (call.command) out.commands.push(call.command);
|
|
81
|
+
} else if (c.type === "text" && typeof c.text === "string") {
|
|
82
|
+
out.assistantChars += c.text.length;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} else if (m.role === "toolResult" || m.role === "tool") {
|
|
86
|
+
let chars = 0, text = "";
|
|
87
|
+
for (const c of content) {
|
|
88
|
+
if (typeof c.text === "string") { chars += c.text.length; text += c.text; }
|
|
89
|
+
else if (typeof c.content === "string") { chars += c.content.length; text += c.content; }
|
|
90
|
+
}
|
|
91
|
+
out.toolResultChars += chars;
|
|
92
|
+
out.lastToolResultText = text;
|
|
93
|
+
// Results arrive in call order. A result for tool_search, sessions_* or
|
|
94
|
+
// any other harness tool is the agent framework talking to itself, not
|
|
95
|
+
// repository content, and must not be counted as displaced reading.
|
|
96
|
+
const call = pending.shift();
|
|
97
|
+
if (call) { call.resultChars = chars; if (call.repoRead) out.repoReadChars += chars; else out.harnessChars += chars; }
|
|
98
|
+
else out.harnessChars += chars;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
out.filesRead = [...new Set(out.filesRead)];
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Read and summarize the transcript. Never throws: a missing database, a
|
|
107
|
+
* Node without `node:sqlite`, or a locked file yields `available:false` with
|
|
108
|
+
* the reason, and the caller records that instead of a fabricated number.
|
|
109
|
+
*/
|
|
110
|
+
export async function readOpenClawTranscript(stateDir) {
|
|
111
|
+
const dbPath = stateDir ? cachedTranscriptDb(stateDir) : null;
|
|
112
|
+
if (!dbPath) return { available: false, reason: "no transcript database under the state directory", dbPath: null };
|
|
113
|
+
let DatabaseSync;
|
|
114
|
+
try { ({ DatabaseSync } = await import("node:sqlite")); }
|
|
115
|
+
catch { return { available: false, reason: "node:sqlite is not available in this Node version", dbPath }; }
|
|
116
|
+
try {
|
|
117
|
+
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
118
|
+
try {
|
|
119
|
+
const rows = db.prepare("select event_json from transcript_events order by seq, rowid").all();
|
|
120
|
+
const events = [];
|
|
121
|
+
for (const r of rows) { try { events.push(JSON.parse(r.event_json)); } catch { /* skip a torn row */ } }
|
|
122
|
+
return { available: true, reason: null, dbPath, events: events.length, ...summarizeTranscriptEvents(events) };
|
|
123
|
+
} finally { db.close(); }
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return { available: false, reason: `could not read transcript: ${error.message}`, dbPath };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// The transcript database never moves once created, and finding it walks
|
|
130
|
+
// the whole state tree (for a Codex job, Codex's entire harness home), so
|
|
131
|
+
// the found path is cached per state directory.
|
|
132
|
+
const dbPathCache = new Map();
|
|
133
|
+
function cachedTranscriptDb(stateDir) {
|
|
134
|
+
const hit = dbPathCache.get(stateDir);
|
|
135
|
+
if (hit && fs.existsSync(hit)) return hit;
|
|
136
|
+
const found = findTranscriptDb(stateDir);
|
|
137
|
+
if (found) dbPathCache.set(stateDir, found);
|
|
138
|
+
return found;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* A cheap read of part of the transcript, for watchers that run every few
|
|
143
|
+
* seconds while a job is live. The full read (readOpenClawTranscript)
|
|
144
|
+
* parsed every event synchronously on the server's only thread, and the
|
|
145
|
+
* heartbeat and idle watcher each did it every tick -- on a long Codex job
|
|
146
|
+
* that froze the server long enough for a status call to hang for 35
|
|
147
|
+
* minutes (a real Senti run).
|
|
148
|
+
*
|
|
149
|
+
* `limit`: the last N events (insertion order). `sinceEvent`: every event
|
|
150
|
+
* after the first N, i.e. only what a call that started at event N wrote.
|
|
151
|
+
* `events` is always the total count.
|
|
152
|
+
*/
|
|
153
|
+
export async function readOpenClawTranscriptTail(stateDir, { limit = 40, sinceEvent = null } = {}) {
|
|
154
|
+
const dbPath = stateDir ? cachedTranscriptDb(stateDir) : null;
|
|
155
|
+
if (!dbPath) return { available: false, reason: "no transcript database under the state directory", dbPath: null, events: 0 };
|
|
156
|
+
let DatabaseSync;
|
|
157
|
+
try { ({ DatabaseSync } = await import("node:sqlite")); }
|
|
158
|
+
catch { return { available: false, reason: "node:sqlite is not available in this Node version", dbPath, events: 0 }; }
|
|
159
|
+
try {
|
|
160
|
+
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
161
|
+
try {
|
|
162
|
+
const total = Number(db.prepare("select count(*) as n from transcript_events").get()?.n ?? 0);
|
|
163
|
+
const rows = sinceEvent !== null
|
|
164
|
+
? db.prepare("select event_json from transcript_events order by rowid limit -1 offset ?").all(sinceEvent)
|
|
165
|
+
: limit > 0 ? db.prepare("select event_json from transcript_events order by rowid desc limit ?").all(limit).reverse() : [];
|
|
166
|
+
const events = [];
|
|
167
|
+
for (const r of rows) { try { events.push(JSON.parse(r.event_json)); } catch { /* skip a torn row */ } }
|
|
168
|
+
return { available: true, reason: null, dbPath, events: total, ...summarizeTranscriptEvents(events) };
|
|
169
|
+
} finally { db.close(); }
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return { available: false, reason: `could not read transcript: ${error.message}`, dbPath, events: 0 };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The number this project is for. `readChars` is repository content the
|
|
177
|
+
* worker pulled through tool results; `deliveredChars` is what the coordinator
|
|
178
|
+
* receives instead (the rendered report plus its record).
|
|
179
|
+
*/
|
|
180
|
+
export function estimateDisplacement({ readChars, deliveredChars, charsPerToken = CALIBRATED.charsPerToken }) {
|
|
181
|
+
const read = Number.isFinite(readChars) ? Math.round(readChars / charsPerToken) : null;
|
|
182
|
+
const delivered = Number.isFinite(deliveredChars) ? Math.round(deliveredChars / charsPerToken) : null;
|
|
183
|
+
if (read === null || delivered === null) {
|
|
184
|
+
return { frontier_read_tokens_est: read, delivered_tokens_est: delivered, displaced_tokens_est: null, ratio: null,
|
|
185
|
+
verdict: "unknown", note: "transcript unavailable; displacement cannot be estimated" };
|
|
186
|
+
}
|
|
187
|
+
const displaced = read - delivered;
|
|
188
|
+
const ratio = delivered > 0 ? +(read / delivered).toFixed(2) : null;
|
|
189
|
+
let verdict, note;
|
|
190
|
+
if (displaced <= 0) { verdict = "negative"; note = "the report is at least as large as what the scout read; asking the coordinator to read it directly would have cost less context"; }
|
|
191
|
+
else if (ratio !== null && ratio < 2) { verdict = "marginal"; note = "less than a 2x reduction; a point lookup the coordinator could have done itself"; }
|
|
192
|
+
else if (ratio !== null) { verdict = "positive"; note = `about ${ratio}x less coordinator context than reading the same material directly (estimate)`; }
|
|
193
|
+
else { verdict = "positive"; note = "the scout read content but the report delivered none; context was displaced"; }
|
|
194
|
+
return { frontier_read_tokens_est: read, delivered_tokens_est: delivered, displaced_tokens_est: displaced, ratio, verdict, note };
|
|
195
|
+
}
|