ultra-builder-pro-cli 0.6.0 → 0.8.1
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/.ultra-template/changes/active/.gitkeep +1 -0
- package/.ultra-template/changes/archive/.gitkeep +1 -0
- package/CHANGELOG.md +90 -11
- package/README.md +39 -19
- package/adapters/_shared/handbook.cjs +8 -7
- package/adapters/_shared/provenance.cjs +286 -0
- package/adapters/_shared/runtime-assets.cjs +2 -0
- package/adapters/_shared/tests/handbook.test.cjs +4 -1
- package/adapters/_shared/tests/provenance.test.cjs +133 -0
- package/adapters/_shared/tests/runtime-assets.test.cjs +2 -0
- package/adapters/claude.js +33 -0
- package/adapters/codex.js +87 -0
- package/adapters/opencode.js +147 -20
- package/adapters/tests/codex.test.cjs +42 -3
- package/adapters/tests/opencode.test.cjs +16 -9
- package/agents/debugger.md +12 -0
- package/bin/install.js +63 -27
- package/bin/orchestrator.js +1 -26
- package/commands/ultra-change.md +48 -0
- package/commands/ultra-deliver.md +14 -7
- package/commands/ultra-dev.md +6 -3
- package/commands/ultra-doctor.md +36 -0
- package/commands/ultra-init.md +4 -3
- package/commands/ultra-status.md +7 -5
- package/commands/ultra-test.md +2 -1
- package/docs/AGENT-CONTEXT.md +31 -23
- package/docs/ARCHITECTURE.md +80 -17
- package/docs/PLAN.zh-CN.md +37 -29
- package/docs/ROADMAP.md +14 -14
- package/docs/RUNTIME-COMPAT-MATRIX.md +19 -12
- package/hooks/README.md +14 -9
- package/hooks/active_task_context.py +22 -15
- package/hooks/health_check.py +136 -38
- package/hooks/workflow_checkpoint.py +15 -1
- package/hooks/workflow_context.py +120 -30
- package/hooks/workflow_resume.py +96 -7
- package/mcp-server/lib/change-workflow.cjs +625 -0
- package/mcp-server/lib/change-workflow.test.cjs +346 -0
- package/mcp-server/lib/doctor.cjs +112 -0
- package/mcp-server/lib/doctor.test.cjs +86 -0
- package/mcp-server/lib/init-project.cjs +32 -6
- package/mcp-server/lib/runtime-state.cjs +222 -0
- package/mcp-server/lib/runtime-state.test.cjs +92 -0
- package/mcp-server/lib/state-db.cjs +31 -8
- package/mcp-server/lib/state-ops.cjs +17 -3
- package/mcp-server/server.cjs +102 -10
- package/mcp-server/tests/db-ops.test.cjs +5 -1
- package/mcp-server/tests/init-project.test.cjs +45 -0
- package/mcp-server/tests/server-contract.test.cjs +123 -1
- package/mcp-server/tests/state-db.test.cjs +44 -7
- package/mcp-server/tests/state-ops.test.cjs +19 -0
- package/orchestrator/daemon.cjs +39 -0
- package/orchestrator/tests/daemon.test.cjs +50 -0
- package/package.json +3 -3
- package/settings.json +1 -2
- package/skills/ultra-change/SKILL.md +149 -0
- package/skills/ultra-deliver/SKILL.md +83 -15
- package/skills/ultra-dev/SKILL.md +32 -12
- package/skills/ultra-doctor/SKILL.md +97 -0
- package/skills/ultra-init/SKILL.md +8 -5
- package/skills/ultra-review/SKILL.md +16 -0
- package/skills/ultra-status/SKILL.md +30 -7
- package/skills/ultra-test/SKILL.md +14 -2
- package/spec/cli-protocol.md +12 -1
- package/spec/mcp-tools.yaml +337 -1
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +117 -0
- package/ultra-tools/cli.cjs +3 -0
- package/ultra-tools/commands/system.cjs +57 -0
- package/ultra-tools/system-command.test.cjs +53 -0
- package/adapters/_shared/rtk-detect.cjs +0 -86
- package/adapters/_shared/tests/rtk-detect.test.cjs +0 -116
- package/docs/LEGACY-HERMES.md +0 -1099
- package/orchestrator/code-graph-watcher.cjs +0 -121
- package/orchestrator/tests/code-graph-watcher.test.cjs +0 -183
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Phase 6.4 — code-review-graph live watcher.
|
|
4
|
-
//
|
|
5
|
-
// fs-watch the project's source tree, debounce bursts of editor saves, and
|
|
6
|
-
// hand batches off to an onBatch callback. Callers wire the callback to
|
|
7
|
-
// whatever keeps the graph fresh (e.g. the external code-review-graph MCP
|
|
8
|
-
// server's build_or_update_graph_tool). Keeping the glue in callers means
|
|
9
|
-
// this module has no MCP-client dependency and is trivial to unit-test.
|
|
10
|
-
//
|
|
11
|
-
// PLAN D24: real-time feedback beats startup rebuilds; agents learn about
|
|
12
|
-
// impact radius immediately after save, not after the next phase boot.
|
|
13
|
-
|
|
14
|
-
const chokidar = require('chokidar');
|
|
15
|
-
|
|
16
|
-
const DEFAULT_INCLUDE_EXT = new Set([
|
|
17
|
-
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
|
|
18
|
-
'.py', '.go', '.rs', '.java', '.rb', '.php',
|
|
19
|
-
'.c', '.cc', '.cpp', '.h', '.hpp',
|
|
20
|
-
'.swift', '.kt', '.scala',
|
|
21
|
-
]);
|
|
22
|
-
|
|
23
|
-
const DEFAULT_EXCLUDE_GLOBS = Object.freeze([
|
|
24
|
-
'**/node_modules/**',
|
|
25
|
-
'**/.git/**',
|
|
26
|
-
'**/.ultra/**',
|
|
27
|
-
'**/.code-review-graph/**',
|
|
28
|
-
'**/dist/**',
|
|
29
|
-
'**/build/**',
|
|
30
|
-
'**/coverage/**',
|
|
31
|
-
'**/.next/**',
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
|
-
function hasCodeExt(p, includeExt) {
|
|
35
|
-
const idx = p.lastIndexOf('.');
|
|
36
|
-
if (idx < 0) return false;
|
|
37
|
-
return includeExt.has(p.slice(idx).toLowerCase());
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function startWatcher({
|
|
41
|
-
repoRoot,
|
|
42
|
-
debounceMs = 500,
|
|
43
|
-
includeExt = DEFAULT_INCLUDE_EXT,
|
|
44
|
-
excludeGlobs = DEFAULT_EXCLUDE_GLOBS,
|
|
45
|
-
largeBatchThreshold = 50,
|
|
46
|
-
onBatch,
|
|
47
|
-
} = {}) {
|
|
48
|
-
if (!repoRoot) throw new Error('startWatcher: repoRoot required');
|
|
49
|
-
if (typeof onBatch !== 'function') throw new Error('startWatcher: onBatch required');
|
|
50
|
-
|
|
51
|
-
let stopped = false;
|
|
52
|
-
let ready = false;
|
|
53
|
-
let timer = null;
|
|
54
|
-
// kind → latest kind; path → {kind, ts} so rapid edit+add collapses cleanly.
|
|
55
|
-
const pending = new Map();
|
|
56
|
-
|
|
57
|
-
const w = chokidar.watch(repoRoot, {
|
|
58
|
-
ignored: excludeGlobs,
|
|
59
|
-
ignoreInitial: true,
|
|
60
|
-
persistent: true,
|
|
61
|
-
awaitWriteFinish: { stabilityThreshold: 20, pollInterval: 10 },
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
const readyPromise = new Promise((resolve) => {
|
|
65
|
-
w.once('ready', () => { ready = true; resolve(); });
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
function record(kind, p) {
|
|
69
|
-
// Drop events fired during chokidar's initial scan — ignoreInitial=true
|
|
70
|
-
// mostly handles this but awaitWriteFinish can still surface add events
|
|
71
|
-
// from files that existed when the watcher started.
|
|
72
|
-
if (stopped || !ready) return;
|
|
73
|
-
if (!hasCodeExt(p, includeExt)) return;
|
|
74
|
-
pending.set(p, { kind, ts: Date.now() });
|
|
75
|
-
schedule();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function schedule() {
|
|
79
|
-
if (timer) return;
|
|
80
|
-
timer = setTimeout(flush, debounceMs);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function flush() {
|
|
84
|
-
timer = null;
|
|
85
|
-
if (pending.size === 0) return;
|
|
86
|
-
const changes = Array.from(pending.entries()).map(([p, meta]) => ({ path: p, kind: meta.kind, ts: meta.ts }));
|
|
87
|
-
pending.clear();
|
|
88
|
-
const batch = {
|
|
89
|
-
changes,
|
|
90
|
-
large_batch: changes.length >= largeBatchThreshold,
|
|
91
|
-
ts: new Date().toISOString(),
|
|
92
|
-
};
|
|
93
|
-
try { onBatch(batch); }
|
|
94
|
-
catch (err) { process.stderr.write(`code-graph-watcher onBatch error: ${err.message}\n`); }
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
w.on('add', (p) => record('add', p));
|
|
98
|
-
w.on('change', (p) => record('change', p));
|
|
99
|
-
w.on('unlink', (p) => record('unlink', p));
|
|
100
|
-
w.on('error', (err) => process.stderr.write(`watcher error: ${err.message}\n`));
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
stop() {
|
|
104
|
-
if (stopped) return;
|
|
105
|
-
stopped = true;
|
|
106
|
-
if (timer) clearTimeout(timer);
|
|
107
|
-
timer = null;
|
|
108
|
-
w.close().catch(() => { /* ignore close errors */ });
|
|
109
|
-
},
|
|
110
|
-
ready() { return readyPromise; },
|
|
111
|
-
get running() { return !stopped; },
|
|
112
|
-
get pendingCount() { return pending.size; },
|
|
113
|
-
flushNow: flush,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
module.exports = {
|
|
118
|
-
startWatcher,
|
|
119
|
-
DEFAULT_INCLUDE_EXT,
|
|
120
|
-
DEFAULT_EXCLUDE_GLOBS,
|
|
121
|
-
};
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Phase 6.4 — code-graph watcher:
|
|
4
|
-
// • fs-watch project code paths, debounce bursts, deliver batches via
|
|
5
|
-
// onBatch callback. The watcher itself does not talk to the external
|
|
6
|
-
// code-review-graph MCP server — that glue lives in callers (daemon /
|
|
7
|
-
// CLI / smoke scripts) so the watcher stays testable with just an
|
|
8
|
-
// onBatch spy.
|
|
9
|
-
|
|
10
|
-
const { test } = require('node:test');
|
|
11
|
-
const assert = require('node:assert/strict');
|
|
12
|
-
const fs = require('node:fs');
|
|
13
|
-
const os = require('node:os');
|
|
14
|
-
const path = require('node:path');
|
|
15
|
-
|
|
16
|
-
const watcher = require('../code-graph-watcher.cjs');
|
|
17
|
-
|
|
18
|
-
function mkTree(layout = {}) {
|
|
19
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-watch-'));
|
|
20
|
-
for (const [rel, content] of Object.entries(layout)) {
|
|
21
|
-
const file = path.join(root, rel);
|
|
22
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
23
|
-
fs.writeFileSync(file, content);
|
|
24
|
-
}
|
|
25
|
-
return root;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function cleanupTree(root) {
|
|
29
|
-
try { fs.rmSync(root, { recursive: true, force: true }); } catch (_) { /* ignore */ }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function waitForBatches(batches, count, timeoutMs = 1500) {
|
|
33
|
-
return new Promise((resolve, reject) => {
|
|
34
|
-
const start = Date.now();
|
|
35
|
-
const timer = setInterval(() => {
|
|
36
|
-
if (batches.length >= count) { clearInterval(timer); resolve(); return; }
|
|
37
|
-
if (Date.now() - start > timeoutMs) { clearInterval(timer); reject(new Error(`timed out waiting for ${count} batches, got ${batches.length}`)); }
|
|
38
|
-
}, 20);
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function waitStable(handle) {
|
|
43
|
-
// Block on chokidar's 'ready' so initial add events from pre-existing
|
|
44
|
-
// files don't leak into test assertions.
|
|
45
|
-
await handle.ready();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
test('touch a watched file → onBatch fires with that path after debounce', async () => {
|
|
49
|
-
const root = mkTree({ 'src/a.js': '// initial\n' });
|
|
50
|
-
const batches = [];
|
|
51
|
-
const handle = watcher.startWatcher({
|
|
52
|
-
repoRoot: root,
|
|
53
|
-
debounceMs: 80,
|
|
54
|
-
onBatch: (b) => batches.push(b),
|
|
55
|
-
});
|
|
56
|
-
try {
|
|
57
|
-
await waitStable(handle);
|
|
58
|
-
fs.writeFileSync(path.join(root, 'src/a.js'), '// changed\n');
|
|
59
|
-
await waitForBatches(batches, 1);
|
|
60
|
-
assert.equal(batches.length, 1);
|
|
61
|
-
assert.ok(batches[0].changes.some((c) => c.path.endsWith('src/a.js')));
|
|
62
|
-
} finally {
|
|
63
|
-
handle.stop();
|
|
64
|
-
cleanupTree(root);
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('debounce merges bursts: 3 writes within window → 1 batch', async () => {
|
|
69
|
-
const root = mkTree({ 'src/a.js': '0', 'src/b.js': '0', 'src/c.js': '0' });
|
|
70
|
-
const batches = [];
|
|
71
|
-
const handle = watcher.startWatcher({
|
|
72
|
-
repoRoot: root,
|
|
73
|
-
debounceMs: 100,
|
|
74
|
-
onBatch: (b) => batches.push(b),
|
|
75
|
-
});
|
|
76
|
-
try {
|
|
77
|
-
await waitStable(handle);
|
|
78
|
-
fs.writeFileSync(path.join(root, 'src/a.js'), '1');
|
|
79
|
-
fs.writeFileSync(path.join(root, 'src/b.js'), '1');
|
|
80
|
-
fs.writeFileSync(path.join(root, 'src/c.js'), '1');
|
|
81
|
-
await waitForBatches(batches, 1);
|
|
82
|
-
// Give a bit extra time in case a 2nd batch is coming (it shouldn't).
|
|
83
|
-
await new Promise((r) => setTimeout(r, 150));
|
|
84
|
-
assert.equal(batches.length, 1, `expected 1 merged batch, got ${batches.length}`);
|
|
85
|
-
const paths = new Set(batches[0].changes.map((c) => path.basename(c.path)));
|
|
86
|
-
assert.ok(paths.has('a.js') && paths.has('b.js') && paths.has('c.js'));
|
|
87
|
-
} finally {
|
|
88
|
-
handle.stop();
|
|
89
|
-
cleanupTree(root);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test('excludePaths: node_modules changes are ignored', async () => {
|
|
94
|
-
const root = mkTree({
|
|
95
|
-
'src/a.js': '0',
|
|
96
|
-
'node_modules/lib/x.js': '0',
|
|
97
|
-
});
|
|
98
|
-
const batches = [];
|
|
99
|
-
const handle = watcher.startWatcher({
|
|
100
|
-
repoRoot: root,
|
|
101
|
-
debounceMs: 80,
|
|
102
|
-
onBatch: (b) => batches.push(b),
|
|
103
|
-
});
|
|
104
|
-
try {
|
|
105
|
-
await waitStable(handle);
|
|
106
|
-
fs.writeFileSync(path.join(root, 'node_modules/lib/x.js'), '1');
|
|
107
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
108
|
-
// If only node_modules was changed, no batch should fire.
|
|
109
|
-
assert.equal(batches.length, 0, 'node_modules write should be ignored');
|
|
110
|
-
// Sanity: a real source change still fires.
|
|
111
|
-
fs.writeFileSync(path.join(root, 'src/a.js'), '1');
|
|
112
|
-
await waitForBatches(batches, 1);
|
|
113
|
-
assert.equal(batches.length, 1);
|
|
114
|
-
assert.ok(batches[0].changes.every((c) => !c.path.includes('node_modules')));
|
|
115
|
-
} finally {
|
|
116
|
-
handle.stop();
|
|
117
|
-
cleanupTree(root);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test('stop(): subsequent writes do not deliver', async () => {
|
|
122
|
-
const root = mkTree({ 'src/a.js': '0' });
|
|
123
|
-
const batches = [];
|
|
124
|
-
const handle = watcher.startWatcher({
|
|
125
|
-
repoRoot: root,
|
|
126
|
-
debounceMs: 80,
|
|
127
|
-
onBatch: (b) => batches.push(b),
|
|
128
|
-
});
|
|
129
|
-
try {
|
|
130
|
-
await waitStable(handle);
|
|
131
|
-
handle.stop();
|
|
132
|
-
fs.writeFileSync(path.join(root, 'src/a.js'), '1');
|
|
133
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
134
|
-
assert.equal(batches.length, 0);
|
|
135
|
-
} finally {
|
|
136
|
-
cleanupTree(root);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test('large_batch flag: >threshold changes in one burst', async () => {
|
|
141
|
-
const files = {};
|
|
142
|
-
for (let i = 0; i < 12; i++) files[`src/f${i}.js`] = '0';
|
|
143
|
-
const root = mkTree(files);
|
|
144
|
-
const batches = [];
|
|
145
|
-
const handle = watcher.startWatcher({
|
|
146
|
-
repoRoot: root,
|
|
147
|
-
debounceMs: 120,
|
|
148
|
-
largeBatchThreshold: 10, // lower for test speed
|
|
149
|
-
onBatch: (b) => batches.push(b),
|
|
150
|
-
});
|
|
151
|
-
try {
|
|
152
|
-
await waitStable(handle);
|
|
153
|
-
for (let i = 0; i < 12; i++) fs.writeFileSync(path.join(root, `src/f${i}.js`), '1');
|
|
154
|
-
await waitForBatches(batches, 1);
|
|
155
|
-
await new Promise((r) => setTimeout(r, 150));
|
|
156
|
-
assert.equal(batches.length, 1);
|
|
157
|
-
assert.equal(batches[0].changes.length, 12);
|
|
158
|
-
assert.equal(batches[0].large_batch, true);
|
|
159
|
-
} finally {
|
|
160
|
-
handle.stop();
|
|
161
|
-
cleanupTree(root);
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test('non-code files ignored (e.g. .log, .tmp)', async () => {
|
|
166
|
-
const root = mkTree({ 'src/a.js': '0', 'run.log': '0', 'tmp.tmp': '0' });
|
|
167
|
-
const batches = [];
|
|
168
|
-
const handle = watcher.startWatcher({
|
|
169
|
-
repoRoot: root,
|
|
170
|
-
debounceMs: 80,
|
|
171
|
-
onBatch: (b) => batches.push(b),
|
|
172
|
-
});
|
|
173
|
-
try {
|
|
174
|
-
await waitStable(handle);
|
|
175
|
-
fs.writeFileSync(path.join(root, 'run.log'), '1');
|
|
176
|
-
fs.writeFileSync(path.join(root, 'tmp.tmp'), '1');
|
|
177
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
178
|
-
assert.equal(batches.length, 0);
|
|
179
|
-
} finally {
|
|
180
|
-
handle.stop();
|
|
181
|
-
cleanupTree(root);
|
|
182
|
-
}
|
|
183
|
-
});
|