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.
Files changed (74) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +25 -0
  3. package/README.md +484 -0
  4. package/bin/nomarmy.mjs +2248 -0
  5. package/config/agents.yml.example +63 -0
  6. package/config/common.env +31 -0
  7. package/config/profiles/bedrock-cheap.env +26 -0
  8. package/config/profiles/bedrock.env +28 -0
  9. package/config/profiles/cpu-linux.env +8 -0
  10. package/config/profiles/dgx-spark.env +12 -0
  11. package/config/profiles/macbook-pro.env +9 -0
  12. package/config/profiles/nvidia-linux.env +9 -0
  13. package/docker/Dockerfile +15 -0
  14. package/docker/Dockerfile.go +29 -0
  15. package/docker/Dockerfile.rust +19 -0
  16. package/e2e.sh +153 -0
  17. package/install.sh +125 -0
  18. package/lib/agents.mjs +285 -0
  19. package/lib/army.mjs +400 -0
  20. package/lib/budget.mjs +368 -0
  21. package/lib/claude-transcript.mjs +150 -0
  22. package/lib/config.mjs +193 -0
  23. package/lib/connect.mjs +409 -0
  24. package/lib/coordinator-instructions.mjs +23 -0
  25. package/lib/decompose.mjs +389 -0
  26. package/lib/dispatch-config.mjs +164 -0
  27. package/lib/dispatch-schema.mjs +280 -0
  28. package/lib/doctor.mjs +443 -0
  29. package/lib/evidence.mjs +679 -0
  30. package/lib/gguf.mjs +589 -0
  31. package/lib/hardware.mjs +476 -0
  32. package/lib/health.mjs +278 -0
  33. package/lib/model-catalog.mjs +71 -0
  34. package/lib/notifier-app.mjs +95 -0
  35. package/lib/notify.mjs +66 -0
  36. package/lib/openclaw-config.mjs +65 -0
  37. package/lib/openclaw-errors.mjs +40 -0
  38. package/lib/propose.mjs +110 -0
  39. package/lib/prune.mjs +77 -0
  40. package/lib/repo-query.mjs +267 -0
  41. package/lib/runs.mjs +150 -0
  42. package/lib/sabotage.mjs +128 -0
  43. package/lib/sandbox-images.mjs +434 -0
  44. package/lib/scan.mjs +1538 -0
  45. package/lib/schema.mjs +288 -0
  46. package/lib/scout.mjs +544 -0
  47. package/lib/sizing.mjs +1322 -0
  48. package/lib/slots.mjs +112 -0
  49. package/lib/statusline.mjs +126 -0
  50. package/lib/subscription-config.mjs +68 -0
  51. package/lib/subscription-setup.mjs +217 -0
  52. package/lib/transcript.mjs +195 -0
  53. package/lib/verify.mjs +700 -0
  54. package/mcp/server.mjs +4206 -0
  55. package/notifier/icon.swift +34 -0
  56. package/notifier/main.swift +52 -0
  57. package/notifier/nomarmy-icon.png +0 -0
  58. package/package.json +67 -0
  59. package/playbooks/feature.md +43 -0
  60. package/policies/coder.md +49 -0
  61. package/policies/orchestrator.md +35 -0
  62. package/policies/reviewer.md +35 -0
  63. package/policies/scout.md +65 -0
  64. package/scripts/configure-openclaw.sh +96 -0
  65. package/scripts/configure-orchestrator.sh +84 -0
  66. package/scripts/install-llama-cpp.sh +16 -0
  67. package/scripts/lib.sh +198 -0
  68. package/scripts/select-model.mjs +96 -0
  69. package/scripts/select-model.sh +4 -0
  70. package/scripts/setup-sandbox.sh +38 -0
  71. package/scripts/start-inference.sh +46 -0
  72. package/scripts/stop-inference.sh +5 -0
  73. package/scripts/uninstall.sh +6 -0
  74. package/scripts/verify-install.sh +68 -0
package/lib/schema.mjs ADDED
@@ -0,0 +1,288 @@
1
+ // nomArmy `.nomarmy.yml` schema (v1.3).
2
+ //
3
+ // This module owns the *shape* of a repository's environment contract and
4
+ // nothing else: no filesystem access, no YAML. `lib/config.mjs` is the only
5
+ // place that touches either, so the parser stays swappable.
6
+ //
7
+ // Design rules the rest of the system depends on:
8
+ // * Every object is strict. A field belonging to another service source is a
9
+ // configuration error, never a silently ignored key.
10
+ // * `shared` and `remote` services are elevated: validation accepts them, but
11
+ // the caller is handed a structured list so it can require explicit policy
12
+ // approval before the job runs. See `collectElevated`.
13
+
14
+ import { z } from "zod";
15
+ import { armySchema } from "./army.mjs";
16
+
17
+ export const SERVICE_SOURCES = Object.freeze([
18
+ "compose",
19
+ "image",
20
+ "process",
21
+ "shared",
22
+ "remote",
23
+ ]);
24
+
25
+ /** Sources that reach outside the job's own sandbox and need policy approval. */
26
+ export const ELEVATED_SERVICE_SOURCES = Object.freeze(["shared", "remote"]);
27
+
28
+ export const ENVIRONMENT_LEVELS = Object.freeze([
29
+ "none",
30
+ "basic",
31
+ "integration",
32
+ "e2e",
33
+ ]);
34
+
35
+ export const RETENTION_ACTIONS = Object.freeze(["destroy", "logs", "retain"]);
36
+
37
+ export const DEFAULT_RETENTION = Object.freeze({
38
+ success: "destroy",
39
+ failure: "logs",
40
+ debug: "retain",
41
+ });
42
+
43
+ /** The field each service source owns, keyed by source. */
44
+ export const SERVICE_FIELDS = Object.freeze({
45
+ compose: "service",
46
+ image: "image",
47
+ process: "command",
48
+ shared: "endpoint",
49
+ remote: "endpoint_env",
50
+ });
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // primitives
54
+ // ---------------------------------------------------------------------------
55
+
56
+ // Error messages omit the field name on purpose: `formatIssues` always prefixes
57
+ // the dotted path, so repeating it reads as stutter.
58
+ const requiredString = () =>
59
+ z
60
+ .string({ required_error: "is required", invalid_type_error: "must be a string" })
61
+ .refine((value) => value.trim().length > 0, { message: "must not be empty" });
62
+
63
+ const enumOf = (values) =>
64
+ z.enum(values, {
65
+ errorMap: () => ({ message: `must be one of ${values.join(", ")}` }),
66
+ });
67
+
68
+ // A plain hostname: no scheme, no path, no port, no whitespace.
69
+ const HOSTNAME_RE =
70
+ /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*$/;
71
+
72
+ const SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
73
+
74
+ /**
75
+ * Why a value is not a usable bare hostname, or null when it is fine.
76
+ * Ordered so the most specific explanation wins (a URL trips scheme, not port).
77
+ * @param {unknown} value
78
+ * @returns {string|null}
79
+ */
80
+ export function hostnameProblem(value) {
81
+ if (typeof value !== "string") return "must be a string hostname";
82
+ if (value.length === 0) return "must not be empty";
83
+ if (/\s/.test(value)) return `must not contain whitespace (got "${value}")`;
84
+ if (SCHEME_RE.test(value)) return `must not include a URI scheme (got "${value}")`;
85
+ if (value.includes("/")) return `must not include a path or slash (got "${value}")`;
86
+ if (value.includes(":")) return `must not include a port (got "${value}")`;
87
+ if (value.length > 253) return "must be at most 253 characters";
88
+ if (!HOSTNAME_RE.test(value)) return `must be a plain hostname (got "${value}")`;
89
+ return null;
90
+ }
91
+
92
+ const hostnameSchema = z
93
+ .string({ invalid_type_error: "must be a string hostname" })
94
+ .superRefine((value, ctx) => {
95
+ const problem = hostnameProblem(value);
96
+ if (problem) ctx.addIssue({ code: z.ZodIssueCode.custom, message: problem });
97
+ });
98
+
99
+ // Service names become DNS names on the job network, so keep them boring.
100
+ const serviceNameSchema = z
101
+ .string()
102
+ .regex(
103
+ /^[A-Za-z0-9][A-Za-z0-9_.-]*$/,
104
+ "service name must start alphanumeric and use only letters, digits, hyphen, underscore or dot",
105
+ );
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // services (discriminated on `source`, every branch strict)
109
+ // ---------------------------------------------------------------------------
110
+
111
+ export const composeServiceSchema = z
112
+ .object({
113
+ source: z.literal("compose"),
114
+ service: requiredString(),
115
+ })
116
+ .strict();
117
+
118
+ export const imageServiceSchema = z
119
+ .object({
120
+ source: z.literal("image"),
121
+ image: requiredString(),
122
+ })
123
+ .strict();
124
+
125
+ export const processServiceSchema = z
126
+ .object({
127
+ source: z.literal("process"),
128
+ command: requiredString(),
129
+ })
130
+ .strict();
131
+
132
+ export const sharedServiceSchema = z
133
+ .object({
134
+ source: z.literal("shared"),
135
+ endpoint: requiredString(),
136
+ })
137
+ .strict();
138
+
139
+ export const remoteServiceSchema = z
140
+ .object({
141
+ source: z.literal("remote"),
142
+ endpoint_env: requiredString(),
143
+ })
144
+ .strict();
145
+
146
+ export const serviceSchema = z.discriminatedUnion("source", [
147
+ composeServiceSchema,
148
+ imageServiceSchema,
149
+ processServiceSchema,
150
+ sharedServiceSchema,
151
+ remoteServiceSchema,
152
+ ]);
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // environment
156
+ // ---------------------------------------------------------------------------
157
+
158
+ // Which requirements file(s) a Python repo's sandbox image should install --
159
+ // see lib/sandbox-images.mjs's pythonRequirementsFor(). Paths are relative to
160
+ // the repository root; this schema only validates shape, never touches the
161
+ // filesystem (no "does this file exist" check belongs here).
162
+ export const pythonEnvironmentSchema = z
163
+ .object({
164
+ requirements: z
165
+ .array(requiredString(), { invalid_type_error: "must be an array of strings" })
166
+ .min(1, "must list at least one requirements file"),
167
+ })
168
+ .strict();
169
+
170
+ export const environmentSchema = z
171
+ .object({
172
+ compose: z.object({ file: requiredString() }).strict().optional(),
173
+ services: z.record(serviceNameSchema, serviceSchema).optional(),
174
+ application: z
175
+ .object({
176
+ command: requiredString(),
177
+ healthcheck: requiredString().optional(),
178
+ })
179
+ .strict()
180
+ .optional(),
181
+ browser: z.object({ base_url: requiredString() }).strict().optional(),
182
+ allowed_hosts: z.array(hostnameSchema).optional(),
183
+ python: pythonEnvironmentSchema.optional(),
184
+ })
185
+ .strict();
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // verification
189
+ // ---------------------------------------------------------------------------
190
+
191
+ export const verificationProfileSchema = z
192
+ .object({
193
+ environment: enumOf(ENVIRONMENT_LEVELS).default("none"),
194
+ commands: z
195
+ .array(requiredString(), {
196
+ required_error: "is required",
197
+ invalid_type_error: "must be an array of strings",
198
+ })
199
+ .min(1, "must list at least one command"),
200
+ })
201
+ .strict();
202
+
203
+ export const verificationSchema = z.record(
204
+ z.string().min(1, "verification profile name must not be empty"),
205
+ verificationProfileSchema,
206
+ );
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // retention
210
+ // ---------------------------------------------------------------------------
211
+
212
+ export const environmentRetentionSchema = z
213
+ .object({
214
+ success: enumOf(RETENTION_ACTIONS).default(DEFAULT_RETENTION.success),
215
+ failure: enumOf(RETENTION_ACTIONS).default(DEFAULT_RETENTION.failure),
216
+ debug: enumOf(RETENTION_ACTIONS).default(DEFAULT_RETENTION.debug),
217
+ })
218
+ .strict()
219
+ .default({});
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // root
223
+ // ---------------------------------------------------------------------------
224
+
225
+ export const configSchema = z
226
+ .object({
227
+ environment: environmentSchema.optional(),
228
+ verification: verificationSchema.optional(),
229
+ environment_retention: environmentRetentionSchema,
230
+ // Who the General calls for what in this repo. Owned by lib/army.mjs,
231
+ // which merges it with the global and .nomarmy.local.yml layers.
232
+ army: armySchema.optional(),
233
+ })
234
+ .strict();
235
+
236
+ // ---------------------------------------------------------------------------
237
+ // helpers
238
+ // ---------------------------------------------------------------------------
239
+
240
+ /** A fresh, empty elevated-services record. */
241
+ export function emptyElevated() {
242
+ return { shared: [], remote: [] };
243
+ }
244
+
245
+ /**
246
+ * Structured list of every service that reaches outside the job sandbox.
247
+ * Declaration order is preserved so the list reads like the file.
248
+ * @param {object|null|undefined} config validated config
249
+ * @returns {{ shared: string[], remote: string[] }}
250
+ */
251
+ export function collectElevated(config) {
252
+ const elevated = emptyElevated();
253
+ const services = config && config.environment && config.environment.services;
254
+ if (!services) return elevated;
255
+ for (const [name, definition] of Object.entries(services)) {
256
+ if (!definition) continue;
257
+ if (definition.source === "shared") elevated.shared.push(name);
258
+ else if (definition.source === "remote") elevated.remote.push(name);
259
+ }
260
+ return elevated;
261
+ }
262
+
263
+ /**
264
+ * Turn a ZodError into readable `path: message` lines. Never leak a raw dump.
265
+ * @param {import("zod").ZodError} error
266
+ * @returns {string[]}
267
+ */
268
+ export function formatIssues(error) {
269
+ const lines = [];
270
+ for (const issue of error.issues) {
271
+ const where = issue.path.length > 0 ? issue.path.join(".") : "config";
272
+ if (issue.code === "unrecognized_keys") {
273
+ const keys = (issue.keys || []).map((key) => `"${key}"`).join(", ");
274
+ lines.push(`${where}: unexpected field(s) ${keys}`);
275
+ continue;
276
+ }
277
+ if (issue.code === "invalid_union_discriminator") {
278
+ lines.push(`${where}: must be one of ${SERVICE_SOURCES.join(", ")}`);
279
+ continue;
280
+ }
281
+ if (issue.code === "invalid_type" && issue.received === "undefined") {
282
+ lines.push(`${where}: is required`);
283
+ continue;
284
+ }
285
+ lines.push(`${where}: ${issue.message}`);
286
+ }
287
+ return [...new Set(lines)];
288
+ }