nubos-pilot 1.3.0 → 1.3.2
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/CHANGELOG.md +10 -0
- package/bin/np-tools/_commands.cjs +2 -0
- package/bin/np-tools/_elision-proxy-entry.cjs +13 -0
- package/bin/np-tools/doctor.cjs +25 -3
- package/bin/np-tools/elision-bench.cjs +67 -0
- package/bin/np-tools/elision-get.cjs +48 -0
- package/bin/np-tools/elision-get.test.cjs +66 -0
- package/bin/np-tools/loop-run-round.cjs +25 -11
- package/bin/np-tools/plan-milestone.cjs +1 -0
- package/bin/np-tools/research-phase.cjs +1 -1
- package/bin/np-tools/resume-work.cjs +9 -0
- package/bin/np-tools/resume-work.test.cjs +21 -1
- package/bin/np-tools/spawn-headless.cjs +62 -9
- package/lib/cache-align.cjs +78 -0
- package/lib/cache-align.test.cjs +69 -0
- package/lib/checkpoint-reconcile.cjs +42 -0
- package/lib/checkpoint-reconcile.test.cjs +106 -0
- package/lib/compress.cjs +495 -0
- package/lib/compress.test.cjs +267 -0
- package/lib/config-defaults.cjs +39 -0
- package/lib/config-schema.cjs +40 -4
- package/lib/elision-bench.cjs +409 -0
- package/lib/elision-bench.test.cjs +89 -0
- package/lib/elision-proxy.cjs +158 -0
- package/lib/elision-proxy.test.cjs +243 -0
- package/lib/elision.cjs +163 -0
- package/lib/elision.test.cjs +143 -0
- package/lib/git.cjs +4 -2
- package/lib/nubosloop.cjs +1 -1
- package/lib/output-steering.cjs +68 -0
- package/lib/output-steering.test.cjs +74 -0
- package/lib/researcher-swarm.cjs +14 -3
- package/lib/runtime/agent-loop.cjs +36 -6
- package/lib/runtime/agent-loop.test.cjs +105 -0
- package/lib/runtime/dispatch.cjs +6 -6
- package/lib/runtime/dispatch.test.cjs +17 -3
- package/lib/runtime/providers/openai-compat.cjs +2 -1
- package/lib/runtime/providers/openai-compat.test.cjs +9 -0
- package/lib/runtime/tools/index.cjs +33 -1
- package/lib/runtime/tools/index.test.cjs +24 -0
- package/lib/schemas/data/elision-entry.v1.json +16 -0
- package/lib/token-cost.cjs +46 -0
- package/lib/token-cost.test.cjs +42 -0
- package/np-tools.cjs +2 -0
- package/package.json +1 -1
- package/workflows/execute-phase.md +10 -2
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
|
|
6
|
+
const ca = require('./cache-align.cjs');
|
|
7
|
+
|
|
8
|
+
test('CA-1: detectVolatile flags UUID/ISO-date/JWT/hex in the system prompt', () => {
|
|
9
|
+
const sys = 'Run id 550e8400-e29b-41d4-a716-446655440000 at 2026-06-23T10:00:00Z '
|
|
10
|
+
+ 'token eyJhbGc.eyJzdWI.sig hash d41d8cd98f00b204e9800998ecf8427e';
|
|
11
|
+
const kinds = ca.detectVolatile(sys).map((f) => f.kind).sort();
|
|
12
|
+
assert.deepEqual(kinds, ['hex_hash', 'iso_datetime', 'jwt', 'uuid']);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('CA-2: detectVolatile reads an array-form system prompt and is clean when stable', () => {
|
|
16
|
+
assert.deepEqual(ca.detectVolatile([{ type: 'text', text: 'You are a stable agent.' }]), []);
|
|
17
|
+
assert.deepEqual(ca.detectVolatile('plain stable instructions'), []);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('CA-3: normalizeTools sorts tools by name and recursively sorts schema keys; arrays keep order', () => {
|
|
21
|
+
const tools = [
|
|
22
|
+
{ name: 'zeta', description: 'z', input_schema: { type: 'object', required: ['b', 'a'], properties: { y: {}, x: {} } } },
|
|
23
|
+
{ name: 'alpha', description: 'a', input_schema: { properties: { n: {} }, type: 'object' } },
|
|
24
|
+
];
|
|
25
|
+
const out = ca.normalizeTools(tools);
|
|
26
|
+
assert.deepEqual(out.map((t) => t.name), ['alpha', 'zeta']);
|
|
27
|
+
assert.deepEqual(Object.keys(out[1]), ['description', 'input_schema', 'name']);
|
|
28
|
+
assert.deepEqual(Object.keys(out[1].input_schema.properties), ['x', 'y']);
|
|
29
|
+
assert.deepEqual(out[1].input_schema.required, ['b', 'a']);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('CA-4: alignAnthropicBody adds exactly one ephemeral breakpoint on the last tool and is idempotent', () => {
|
|
33
|
+
const body = { system: 'sys', tools: [{ name: 'b' }, { name: 'a' }], messages: [] };
|
|
34
|
+
const r1 = ca.alignAnthropicBody(body);
|
|
35
|
+
assert.equal(r1.applied, true);
|
|
36
|
+
assert.deepEqual(r1.body.tools.map((t) => t.name), ['a', 'b']);
|
|
37
|
+
assert.equal(r1.body.tools[0].cache_control, undefined);
|
|
38
|
+
assert.deepEqual(r1.body.tools[1].cache_control, { type: 'ephemeral' });
|
|
39
|
+
const r2 = ca.alignAnthropicBody(r1.body);
|
|
40
|
+
assert.equal(r2.applied, false, 'second pass is a no-op — breakpoint already present');
|
|
41
|
+
assert.equal(JSON.stringify(r2.body), JSON.stringify(r1.body), 'byte-identical second pass');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('CA-4b: normalizeTools is byte-deterministic regardless of input order (codepoint, not locale)', () => {
|
|
45
|
+
const a = [{ name: 'get_A' }, { name: 'get-a' }, { name: 'get_a' }, { name: 'tool10' }, { name: 'tool2' }];
|
|
46
|
+
const b = a.slice().reverse();
|
|
47
|
+
assert.equal(JSON.stringify(ca.normalizeTools(a)), JSON.stringify(ca.normalizeTools(b)), 'order-independent, stable bytes');
|
|
48
|
+
assert.deepEqual(ca.normalizeTools(a).map((t) => t.name), ['get-a', 'get_A', 'get_a', 'tool10', 'tool2']);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('CA-4c: duplicate/degenerate names still order deterministically via body tiebreak', () => {
|
|
52
|
+
const x = [{ name: 'x', id: 2 }, { name: 'x', id: 1 }];
|
|
53
|
+
assert.equal(JSON.stringify(ca.normalizeTools(x)), JSON.stringify(ca.normalizeTools(x.slice().reverse())));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('CA-5: customer-placement-wins — a caller-set cache_control is never disturbed', () => {
|
|
57
|
+
const body = { tools: [{ name: 'z', cache_control: { type: 'ephemeral' } }, { name: 'a' }], messages: [] };
|
|
58
|
+
const r = ca.alignAnthropicBody(body);
|
|
59
|
+
assert.equal(r.applied, false);
|
|
60
|
+
assert.deepEqual(r.body.tools.map((t) => t.name), ['z', 'a'], 'tool order untouched when caller manages caching');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('CA-6: no tools → no normalization, detection still runs', () => {
|
|
64
|
+
const body = { system: 'id 550e8400-e29b-41d4-a716-446655440000', messages: [] };
|
|
65
|
+
const r = ca.alignAnthropicBody(body);
|
|
66
|
+
assert.equal(r.applied, false);
|
|
67
|
+
assert.equal(r.body, body);
|
|
68
|
+
assert.deepEqual(r.findings.map((f) => f.kind), ['uuid']);
|
|
69
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const { listCheckpoints, deleteCheckpoint } = require('./checkpoint.cjs');
|
|
3
|
+
const { findCommitByTaskId } = require('./git.cjs');
|
|
4
|
+
const { TASK_ID_RE } = require('./ids.cjs');
|
|
5
|
+
|
|
6
|
+
function committedSha(taskId, cwd) {
|
|
7
|
+
try {
|
|
8
|
+
return findCommitByTaskId(taskId, cwd);
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function reconcileCommittedCheckpoints(cwd = process.cwd(), opts = {}) {
|
|
15
|
+
const exclude = opts.exclude || null;
|
|
16
|
+
const pruned = [];
|
|
17
|
+
const remaining = [];
|
|
18
|
+
for (const file of listCheckpoints(cwd)) {
|
|
19
|
+
const taskId = path.basename(file, '.json');
|
|
20
|
+
if (!TASK_ID_RE.test(taskId)) {
|
|
21
|
+
remaining.push(taskId);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (exclude && taskId === exclude) {
|
|
25
|
+
remaining.push(taskId);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const sha = committedSha(taskId, cwd);
|
|
29
|
+
if (sha) {
|
|
30
|
+
deleteCheckpoint(taskId, cwd);
|
|
31
|
+
pruned.push({ task_id: taskId, sha });
|
|
32
|
+
} else {
|
|
33
|
+
remaining.push(taskId);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { pruned, remaining };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
committedSha,
|
|
41
|
+
reconcileCommittedCheckpoints,
|
|
42
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const { test, after } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const { execFileSync } = require('node:child_process');
|
|
7
|
+
|
|
8
|
+
const { startTask, listCheckpoints } = require('./checkpoint.cjs');
|
|
9
|
+
const { reconcileCommittedCheckpoints, committedSha } = require('./checkpoint-reconcile.cjs');
|
|
10
|
+
|
|
11
|
+
const _repos = [];
|
|
12
|
+
|
|
13
|
+
function makeRepo() {
|
|
14
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-cr-'));
|
|
15
|
+
execFileSync('git', ['init', '-q', '-b', 'main', root], { stdio: 'pipe' });
|
|
16
|
+
execFileSync('git', ['-C', root, 'config', 'user.email', 'test@nubos.local']);
|
|
17
|
+
execFileSync('git', ['-C', root, 'config', 'user.name', 'nubos-test']);
|
|
18
|
+
execFileSync('git', ['-C', root, 'commit', '--allow-empty', '-q', '-m', 'chore: init'], { stdio: 'pipe' });
|
|
19
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
|
|
20
|
+
fs.writeFileSync(path.join(root, '.nubos-pilot', 'STATE.md'), `---
|
|
21
|
+
schema_version: 2
|
|
22
|
+
milestone: m1
|
|
23
|
+
milestone_name: m1
|
|
24
|
+
current_phase: null
|
|
25
|
+
current_plan: null
|
|
26
|
+
current_task: null
|
|
27
|
+
last_updated: "2026-04-15T00:00:00Z"
|
|
28
|
+
progress:
|
|
29
|
+
total_phases: 0
|
|
30
|
+
completed_phases: 0
|
|
31
|
+
total_plans: 0
|
|
32
|
+
completed_plans: 0
|
|
33
|
+
percent: 0
|
|
34
|
+
session:
|
|
35
|
+
stopped_at: null
|
|
36
|
+
resume_file: null
|
|
37
|
+
last_activity: null
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
# State
|
|
41
|
+
`, 'utf-8');
|
|
42
|
+
_repos.push(root);
|
|
43
|
+
return root;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function commitFor(root, taskId) {
|
|
47
|
+
execFileSync('git', ['-C', root, 'commit', '--allow-empty', '-q', '-m', `task(${taskId}): demo`], { stdio: 'pipe' });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
after(() => {
|
|
51
|
+
while (_repos.length) {
|
|
52
|
+
const r = _repos.pop();
|
|
53
|
+
try { fs.rmSync(r, { recursive: true, force: true }); } catch {}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('CR-1: prunes a checkpoint whose task already has a commit', () => {
|
|
58
|
+
const root = makeRepo();
|
|
59
|
+
startTask({ id: 'M013-S005-T0002', phase: 6, plan: '06-01', wave: 1 }, root);
|
|
60
|
+
commitFor(root, 'M013-S005-T0002');
|
|
61
|
+
|
|
62
|
+
const { pruned, remaining } = reconcileCommittedCheckpoints(root);
|
|
63
|
+
assert.equal(pruned.length, 1);
|
|
64
|
+
assert.equal(pruned[0].task_id, 'M013-S005-T0002');
|
|
65
|
+
assert.match(pruned[0].sha, /^[0-9a-f]{40}$/);
|
|
66
|
+
assert.deepEqual(remaining, []);
|
|
67
|
+
assert.deepEqual(listCheckpoints(root), []);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('CR-2: keeps a checkpoint with no matching commit (genuine orphan)', () => {
|
|
71
|
+
const root = makeRepo();
|
|
72
|
+
startTask({ id: 'M013-S005-T0003', phase: 6, plan: '06-01', wave: 1 }, root);
|
|
73
|
+
|
|
74
|
+
const { pruned, remaining } = reconcileCommittedCheckpoints(root);
|
|
75
|
+
assert.deepEqual(pruned, []);
|
|
76
|
+
assert.deepEqual(remaining, ['M013-S005-T0003']);
|
|
77
|
+
assert.equal(listCheckpoints(root).length, 1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('CR-3: excludes the active current_task even when committed', () => {
|
|
81
|
+
const root = makeRepo();
|
|
82
|
+
startTask({ id: 'M013-S005-T0004', phase: 6, plan: '06-01', wave: 1 }, root);
|
|
83
|
+
commitFor(root, 'M013-S005-T0004');
|
|
84
|
+
|
|
85
|
+
const { pruned, remaining } = reconcileCommittedCheckpoints(root, { exclude: 'M013-S005-T0004' });
|
|
86
|
+
assert.deepEqual(pruned, []);
|
|
87
|
+
assert.deepEqual(remaining, ['M013-S005-T0004']);
|
|
88
|
+
assert.equal(listCheckpoints(root).length, 1);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('CR-4: prunes committed tombstone but keeps uncommitted sibling', () => {
|
|
92
|
+
const root = makeRepo();
|
|
93
|
+
startTask({ id: 'M013-S005-T0002', phase: 6, plan: '06-01', wave: 1 }, root);
|
|
94
|
+
startTask({ id: 'M013-S005-T0003', phase: 6, plan: '06-01', wave: 1 }, root);
|
|
95
|
+
commitFor(root, 'M013-S005-T0002');
|
|
96
|
+
|
|
97
|
+
const { pruned, remaining } = reconcileCommittedCheckpoints(root);
|
|
98
|
+
assert.deepEqual(pruned.map((p) => p.task_id), ['M013-S005-T0002']);
|
|
99
|
+
assert.deepEqual(remaining, ['M013-S005-T0003']);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('CR-5: committedSha returns null outside a git repo (no throw)', () => {
|
|
103
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-cr-nogit-'));
|
|
104
|
+
_repos.push(root);
|
|
105
|
+
assert.equal(committedSha('M013-S005-T0002', root), null);
|
|
106
|
+
});
|