nomarmy 0.1.0-alpha.21 → 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 (47) hide show
  1. package/bin/nomarmy.mjs +155 -16
  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 +33 -3
  7. package/lib/army.mjs +2 -2
  8. package/lib/codex-link.mjs +6 -1
  9. package/lib/connect.mjs +6 -2
  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 +3 -1
  14. package/lib/execute.mjs +187 -32
  15. package/lib/git-record.mjs +9 -2
  16. package/lib/health.mjs +25 -3
  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/process.mjs +30 -4
  25. package/lib/repo-query.mjs +4 -4
  26. package/lib/sandbox-images.mjs +31 -4
  27. package/lib/schema.mjs +28 -0
  28. package/lib/server-context.mjs +3 -2
  29. package/lib/share.mjs +53 -2
  30. package/lib/stats.mjs +11 -0
  31. package/lib/trust-checks.mjs +269 -0
  32. package/lib/trust-files.mjs +123 -0
  33. package/lib/trust-judgment.mjs +157 -0
  34. package/lib/trust-learning.mjs +153 -0
  35. package/lib/trust-map.mjs +149 -0
  36. package/lib/trust-reach.mjs +220 -0
  37. package/lib/trust.mjs +227 -0
  38. package/lib/validators.mjs +7 -0
  39. package/lib/verification-flow.mjs +25 -18
  40. package/lib/verification-network.mjs +2 -2
  41. package/lib/verify.mjs +46 -3
  42. package/lib/worktree-pointer.mjs +246 -0
  43. package/lib/worktree-write.mjs +128 -0
  44. package/mcp/server.mjs +28 -11
  45. package/package.json +1 -1
  46. package/playbooks/feature.md +5 -5
  47. package/scripts/trust-measure.mjs +139 -0
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ // Measure phase-2 trust against a corpus kept outside this repository.
3
+ // Usage: node scripts/trust-measure.mjs <corpus-dir> [--judgment off|jev|judge] [--json]
4
+ // <corpus-dir>/corpus.json is an array of { "kind": "defect" | "harmless",
5
+ // "diff": "diffs/<name>.diff", ... }. Optional free-form fields such as fix,
6
+ // introduced_by, what, subject and commit are echoed, never interpreted.
7
+ // Each diff is a git-show-style unified diff of one or more files. Only the
8
+ // hunk's removed/context lines form before and added/context lines form after;
9
+ // new and deleted files use an empty snapshot on the absent side.
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import os from 'node:os';
13
+ import { pathToFileURL } from 'node:url';
14
+ import { evaluateDiffTrust } from '../lib/trust-judgment.mjs';
15
+ import { jevSettings, judgeSettings, askJev } from '../lib/validators.mjs';
16
+ import { askJudge } from '../lib/judge.mjs';
17
+ import { loadAgents, agentProviderId, agentRunsToolsOnHost } from '../lib/agents.mjs';
18
+ import { globalConfigDir } from '../lib/army.mjs';
19
+
20
+ export function fileChangesFromDiff(diff) {
21
+ const changes = [];
22
+ let current = null, hunk = null;
23
+ const flush = () => {
24
+ if (!current) return;
25
+ if (!current.hunks) throw new Error(`No hunks for ${current.file}`);
26
+ if (hunk && (hunk.old !== 0 || hunk.new !== 0)) throw new Error(`Incomplete hunk for ${current.file}`);
27
+ changes.push({ file: current.file, before: current.before.join('\n'), after: current.after.join('\n') });
28
+ current = null;
29
+ };
30
+ for (const line of diff.split(/\r?\n/)) {
31
+ if (line.startsWith('diff --git ')) { flush(); current = { file: null, before: [], after: [], hunks: 0 }; hunk = null; continue; }
32
+ if (!current) continue;
33
+ if (line.startsWith('+++ ')) {
34
+ const name = line.slice(4);
35
+ if (name !== '/dev/null') current.file = name.startsWith('b/') ? name.slice(2) : name;
36
+ continue;
37
+ }
38
+ if (line.startsWith('--- ') && !current.file) {
39
+ const name = line.slice(4);
40
+ if (name !== '/dev/null') current.file = name.startsWith('a/') ? name.slice(2) : name;
41
+ continue;
42
+ }
43
+ const match = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
44
+ if (match) {
45
+ if (!current.file) throw new Error('Hunk has no file name');
46
+ if (hunk && (hunk.old !== 0 || hunk.new !== 0)) throw new Error(`Incomplete hunk for ${current.file}`);
47
+ const oldStart = Number(match[1]), newStart = Number(match[3]);
48
+ if (oldStart > 0 && current.before.length > oldStart - 1 || newStart > 0 && current.after.length > newStart - 1) throw new Error(`Overlapping hunks for ${current.file}`);
49
+ while (current.before.length < oldStart - 1) current.before.push('');
50
+ while (current.after.length < newStart - 1) current.after.push('');
51
+ hunk = { old: Number(match[2] ?? 1), new: Number(match[4] ?? 1) };
52
+ current.hunks++;
53
+ continue;
54
+ }
55
+ if (!hunk || (hunk.old === 0 && hunk.new === 0)) continue;
56
+ const prefix = line[0];
57
+ if (prefix === ' ' || prefix === '-') { current.before.push(line.slice(1)); hunk.old--; }
58
+ if (prefix === ' ' || prefix === '+') { current.after.push(line.slice(1)); hunk.new--; }
59
+ if (![' ', '-', '+', '\\'].includes(prefix) || hunk.old < 0 || hunk.new < 0) throw new Error(`Invalid hunk for ${current.file}`);
60
+ // Accept Git's no-newline marker; snapshots are assembled from hunk lines.
61
+ if (prefix === '\\' && line !== '\') throw new Error(`Invalid hunk marker for ${current.file}`);
62
+ }
63
+ if (hunk && (hunk.old !== 0 || hunk.new !== 0)) throw new Error(`Incomplete hunk for ${current.file}`);
64
+ flush();
65
+ if (!changes.length) throw new Error('Diff contains no file changes');
66
+ return changes;
67
+ }
68
+
69
+ function options(argv) {
70
+ let dir, judgment = 'off', json = false;
71
+ for (let i = 0; i < argv.length; i++) {
72
+ if (argv[i] === '--json') json = true;
73
+ else if (argv[i] === '--judgment' && i + 1 < argv.length) judgment = argv[++i];
74
+ else if (!argv[i].startsWith('-') && !dir) dir = argv[i];
75
+ else throw new Error(`Unexpected argument: ${argv[i]}`);
76
+ }
77
+ if (!dir || !['off', 'jev', 'judge'].includes(judgment)) throw new Error('Usage: node scripts/trust-measure.mjs <corpus-dir> [--judgment off|jev|judge] [--json]');
78
+ return { dir: path.resolve(dir), judgment, json };
79
+ }
80
+
81
+ function validatorFor(mode) {
82
+ if (mode === 'off') return { judgment: false };
83
+ if (mode === 'jev') {
84
+ const jev = jevSettings();
85
+ if (!jev) throw new Error('Skipped: Jev is not configured with a readable key.');
86
+ return { jev, askJev };
87
+ }
88
+ const configDir = globalConfigDir();
89
+ let agents;
90
+ try { agents = loadAgents(configDir).agents; } catch (error) { throw new Error(`Skipped: judge agents are not configured: ${error.message}`); }
91
+ const judge = judgeSettings({ configDir, agents, providerOf: agentProviderId, runsOnHost: agentRunsToolsOnHost });
92
+ if (!judge || judge.problem) throw new Error(`Skipped: judge is not configured: ${judge?.problem ?? 'no enabled judge'}`);
93
+ return { judge, askJudge, stateRoot: process.env.NOMARMY_AGENT_STATE || path.join(os.homedir(), '.local', 'share', 'nomarmy-local-agents') };
94
+ }
95
+
96
+ export async function measure(dir, mode = 'off', validator = validatorFor(mode)) {
97
+ const corpus = JSON.parse(fs.readFileSync(path.join(dir, 'corpus.json'), 'utf8'));
98
+ if (!Array.isArray(corpus)) throw new Error('corpus.json must be an array');
99
+ const items = [];
100
+ for (const [index, item] of corpus.entries()) {
101
+ if (!item || !['defect', 'harmless'].includes(item.kind) || typeof item.diff !== 'string') throw new Error(`Invalid corpus item ${index + 1}`);
102
+ const diffPath = path.resolve(dir, item.diff);
103
+ if (!diffPath.startsWith(`${dir}${path.sep}`)) throw new Error(`Diff path escapes corpus directory: ${item.diff}`);
104
+ const fileChanges = fileChangesFromDiff(fs.readFileSync(diffPath, 'utf8'));
105
+ const trust = await evaluateDiffTrust({ floor: { level: 'normal', reasons: [] }, fileChanges, ...validator });
106
+ const { kind, diff, ...metadata } = item;
107
+ items.push({ kind, label: path.basename(diff, path.extname(diff)), diff, ...metadata, judgment: trust.judgment, level: trust.level, findings: trust.checks.length, reason: trust.reasons[0]?.reason ?? null });
108
+ }
109
+ const count = (kind, caught) => items.filter(i => i.kind === kind && (i.level !== 'normal') === caught).length;
110
+ const defects = count('defect', true) + count('defect', false);
111
+ const harmless = count('harmless', true) + count('harmless', false);
112
+ return { items, judged: items.filter(item => item.judgment.status === 'available').length, confusion: {
113
+ defects: { caught: count('defect', true), missed: count('defect', false), catchRate: defects ? count('defect', true) / defects : null },
114
+ harmless: { normal: count('harmless', false), falsePositives: count('harmless', true), falsePositiveRate: harmless ? count('harmless', true) / harmless : null },
115
+ } };
116
+ }
117
+
118
+ export function measurementLine(item) {
119
+ const { kind, label, level, findings, reason, diff, judgment, ...metadata } = item;
120
+ const status = judgment.status === 'available' ? 'judged' : judgment.status === 'unavailable'
121
+ ? `unavailable: ${judgment.error}` : judgment.status;
122
+ return `${kind}\t${label}\t${level}\t${findings}\t${reason ?? 'none'}\tjudgment ${status}${Object.keys(metadata).length ? `\t${JSON.stringify(metadata)}` : ''}`;
123
+ }
124
+
125
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
126
+ try {
127
+ const { dir, judgment, json } = options(process.argv.slice(2));
128
+ const result = await measure(dir, judgment);
129
+ if (json) console.log(JSON.stringify(result, null, 2));
130
+ else {
131
+ for (const item of result.items) console.log(measurementLine(item));
132
+ console.log(`judged\t${result.judged}/${result.items.length}`);
133
+ const { defects, harmless } = result.confusion;
134
+ console.log('kind\tcaught/normal\tmissed/false positives\trate');
135
+ console.log(`defects\t${defects.caught}\t${defects.missed}\t${defects.catchRate === null ? 'n/a' : (100 * defects.catchRate).toFixed(1) + '%'}`);
136
+ console.log(`harmless\t${harmless.normal}\t${harmless.falsePositives}\t${harmless.falsePositiveRate === null ? 'n/a' : (100 * harmless.falsePositiveRate).toFixed(1) + '%'}`);
137
+ }
138
+ } catch (error) { console.error(error.message); process.exitCode = 1; }
139
+ }