bonecode 1.3.0 → 1.4.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/README.md +42 -0
- package/compat/opencode_adapter.ts +69 -8
- package/dist/compat/opencode_adapter.js +63 -7
- package/dist/compat/opencode_adapter.js.map +1 -1
- package/dist/src/db_adapter.js +30 -0
- package/dist/src/db_adapter.js.map +1 -1
- package/dist/src/engine/session/build_mode.d.ts +83 -0
- package/dist/src/engine/session/build_mode.js +789 -0
- package/dist/src/engine/session/build_mode.js.map +1 -0
- package/dist/src/engine/session/build_mode_helpers.d.ts +6 -0
- package/dist/src/engine/session/build_mode_helpers.js +61 -0
- package/dist/src/engine/session/build_mode_helpers.js.map +1 -0
- package/dist/src/engine/session/prompt/bonescript.txt +11 -0
- package/dist/src/engine/session/prompt.js +57 -2
- package/dist/src/engine/session/prompt.js.map +1 -1
- package/dist/src/tui.js +146 -9
- package/dist/src/tui.js.map +1 -1
- package/package.json +1 -1
- package/scripts/test_build_fallback.js +221 -0
- package/scripts/test_build_mode.js +301 -0
- package/src/db_adapter.ts +29 -0
- package/src/engine/session/build_mode.ts +895 -0
- package/src/engine/session/build_mode_helpers.ts +72 -0
- package/src/engine/session/prompt/bonescript.txt +11 -0
- package/src/engine/session/prompt.ts +69 -2
- package/src/tui.ts +147 -9
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for build_mode.ts pure logic:
|
|
4
|
+
* - parseJsonLoose handles fenced/unfenced/think-tagged/balanced JSON
|
|
5
|
+
* - isBuildPrompt identifies project-scoped vs ad-hoc prompts
|
|
6
|
+
* - state transitions are persistable / restorable
|
|
7
|
+
*
|
|
8
|
+
* Skips anything that requires the live language model (askJson, runBuildMode).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
|
|
15
|
+
const G = "\x1b[32m"; const R = "\x1b[31m"; const C = "\x1b[36m";
|
|
16
|
+
const B = "\x1b[1m"; const D = "\x1b[2m"; const N = "\x1b[0m";
|
|
17
|
+
|
|
18
|
+
let passed = 0;
|
|
19
|
+
let failed = 0;
|
|
20
|
+
const failures = [];
|
|
21
|
+
|
|
22
|
+
function ok(name, info = "") {
|
|
23
|
+
passed++;
|
|
24
|
+
console.log(` ${G}✓${N} ${name}${info ? ` ${D}${info}${N}` : ""}`);
|
|
25
|
+
}
|
|
26
|
+
function fail(name, msg) {
|
|
27
|
+
failed++;
|
|
28
|
+
failures.push(`${name}: ${msg}`);
|
|
29
|
+
console.log(` ${R}✗${N} ${name} ${R}${msg}${N}`);
|
|
30
|
+
}
|
|
31
|
+
function header(s) { console.log(`\n${C}${B}${s}${N}`); }
|
|
32
|
+
|
|
33
|
+
// Extract pure functions from build_mode.ts source (so we don't trigger imports
|
|
34
|
+
// that need a live DB pool / network)
|
|
35
|
+
const ROOT = path.resolve(__dirname, "..");
|
|
36
|
+
const src = fs.readFileSync(path.join(ROOT, "src", "engine", "session", "build_mode.ts"), "utf-8");
|
|
37
|
+
|
|
38
|
+
function extractFunc(name) {
|
|
39
|
+
// Match `function name(`, `export function name(`, or with generic params <T>
|
|
40
|
+
const re = new RegExp(`(?:export\\s+)?function ${name}(?:<[^>]+>)?\\s*\\(`);
|
|
41
|
+
const m = src.match(re);
|
|
42
|
+
if (!m) return null;
|
|
43
|
+
// Strip the leading "export " if present so the eval'd source defines a local function
|
|
44
|
+
const start = m.index + (m[0].startsWith("export") ? "export ".length : 0);
|
|
45
|
+
let depth = 0;
|
|
46
|
+
let inStr = false;
|
|
47
|
+
let strChar = "";
|
|
48
|
+
let inTpl = false;
|
|
49
|
+
let inLineComment = false;
|
|
50
|
+
let inBlockComment = false;
|
|
51
|
+
let escape = false;
|
|
52
|
+
let i = src.indexOf("{", start);
|
|
53
|
+
for (; i < src.length; i++) {
|
|
54
|
+
const ch = src[i];
|
|
55
|
+
const next = src[i + 1];
|
|
56
|
+
if (escape) { escape = false; continue; }
|
|
57
|
+
if (ch === "\\") { escape = true; continue; }
|
|
58
|
+
if (inLineComment) {
|
|
59
|
+
if (ch === "\n") inLineComment = false;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (inBlockComment) {
|
|
63
|
+
if (ch === "*" && next === "/") { inBlockComment = false; i++; }
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (inStr) {
|
|
67
|
+
if (ch === strChar) inStr = false;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (inTpl) {
|
|
71
|
+
if (ch === "`") inTpl = false;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (ch === "/" && next === "/") { inLineComment = true; i++; continue; }
|
|
75
|
+
if (ch === "/" && next === "*") { inBlockComment = true; i++; continue; }
|
|
76
|
+
if (ch === '"' || ch === "'") { inStr = true; strChar = ch; continue; }
|
|
77
|
+
if (ch === "`") { inTpl = true; continue; }
|
|
78
|
+
if (ch === "{") depth++;
|
|
79
|
+
else if (ch === "}") { depth--; if (depth === 0) return src.slice(start, i + 1); }
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const parseJsonLooseSrc = extractFunc("parseJsonLoose");
|
|
85
|
+
const extractBalancedSrc = extractFunc("extractBalanced");
|
|
86
|
+
const tryParseSrc = extractFunc("tryParse");
|
|
87
|
+
const isBuildPromptSrc = extractFunc("isBuildPrompt");
|
|
88
|
+
|
|
89
|
+
if (!parseJsonLooseSrc || !extractBalancedSrc || !tryParseSrc || !isBuildPromptSrc) {
|
|
90
|
+
console.error("Could not extract helpers from build_mode.ts");
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Strip TypeScript annotations for plain Node eval
|
|
95
|
+
function stripTs(s) {
|
|
96
|
+
return s
|
|
97
|
+
// Remove generic type parameters on function declarations: `function foo<T>(`
|
|
98
|
+
.replace(/function\s+(\w+)\s*<[^>]+>/g, "function $1")
|
|
99
|
+
// Remove generic type args at call sites: `foo<Bar>(...)` → `foo(...)`
|
|
100
|
+
// Only strip when followed by `(` so we don't break comparisons.
|
|
101
|
+
.replace(/(\b[a-zA-Z_$][\w$]*)<[^<>=]+>(?=\s*\()/g, "$1")
|
|
102
|
+
// Remove return-type annotations like `): T | null {` or `): string {`
|
|
103
|
+
.replace(/\)\s*:\s*[^=({}]+\s*\{/g, ") {")
|
|
104
|
+
// Remove parameter type annotations: `name: type`
|
|
105
|
+
.replace(/(\(|,\s*)([a-zA-Z_$][\w$]*)\s*:\s*[a-zA-Z_$][\w$<>|&,\s\[\]'"]*?(?=[,)])/g, "$1$2")
|
|
106
|
+
// Remove "as Type" assertions
|
|
107
|
+
.replace(/\bas\s+\w+(?:\s*\|\s*\w+)*/g, "")
|
|
108
|
+
// Drop "export " on declarations
|
|
109
|
+
.replace(/^export\s+/gm, "");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const sandboxSrc = `
|
|
113
|
+
${stripTs(extractBalancedSrc)}
|
|
114
|
+
${stripTs(tryParseSrc)}
|
|
115
|
+
${stripTs(parseJsonLooseSrc)}
|
|
116
|
+
${stripTs(isBuildPromptSrc)}
|
|
117
|
+
return { parseJsonLoose, extractBalanced, tryParse, isBuildPrompt };
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
let helpers;
|
|
121
|
+
try {
|
|
122
|
+
helpers = new Function(sandboxSrc)();
|
|
123
|
+
} catch (e) {
|
|
124
|
+
console.error("Failed to evaluate helpers:", e.message);
|
|
125
|
+
console.error(sandboxSrc.slice(0, 500));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { parseJsonLoose, isBuildPrompt } = helpers;
|
|
130
|
+
|
|
131
|
+
// ─── parseJsonLoose ───────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
header("[1] parseJsonLoose — JSON extraction");
|
|
134
|
+
|
|
135
|
+
(() => {
|
|
136
|
+
const r = parseJsonLoose('{"a": 1, "b": "two"}');
|
|
137
|
+
if (r && r.a === 1 && r.b === "two") ok("plain JSON object");
|
|
138
|
+
else fail("plain JSON", JSON.stringify(r));
|
|
139
|
+
})();
|
|
140
|
+
|
|
141
|
+
(() => {
|
|
142
|
+
const r = parseJsonLoose('```json\n{"x": 42}\n```');
|
|
143
|
+
if (r && r.x === 42) ok("fenced JSON");
|
|
144
|
+
else fail("fenced JSON", JSON.stringify(r));
|
|
145
|
+
})();
|
|
146
|
+
|
|
147
|
+
(() => {
|
|
148
|
+
const r = parseJsonLoose("```\n{\"x\": 42}\n```");
|
|
149
|
+
if (r && r.x === 42) ok("fenced JSON without language");
|
|
150
|
+
else fail("fenced no-lang", JSON.stringify(r));
|
|
151
|
+
})();
|
|
152
|
+
|
|
153
|
+
(() => {
|
|
154
|
+
const r = parseJsonLoose('Here is the answer: {"ok": true} done.');
|
|
155
|
+
if (r && r.ok === true) ok("JSON with surrounding prose");
|
|
156
|
+
else fail("prose+json", JSON.stringify(r));
|
|
157
|
+
})();
|
|
158
|
+
|
|
159
|
+
(() => {
|
|
160
|
+
const r = parseJsonLoose('<think>let me think</think>\n{"ready": true}');
|
|
161
|
+
if (r && r.ready === true) ok("JSON after <think> block");
|
|
162
|
+
else fail("think+json", JSON.stringify(r));
|
|
163
|
+
})();
|
|
164
|
+
|
|
165
|
+
(() => {
|
|
166
|
+
// Nested objects
|
|
167
|
+
const r = parseJsonLoose('{"design": {"goal": "build a thing", "requirements": ["a", "b"]}}');
|
|
168
|
+
if (r && r.design && r.design.goal === "build a thing" && r.design.requirements.length === 2) {
|
|
169
|
+
ok("nested object with array");
|
|
170
|
+
} else {
|
|
171
|
+
fail("nested object", JSON.stringify(r));
|
|
172
|
+
}
|
|
173
|
+
})();
|
|
174
|
+
|
|
175
|
+
(() => {
|
|
176
|
+
const r = parseJsonLoose("not json at all");
|
|
177
|
+
if (r === null) ok("non-JSON returns null");
|
|
178
|
+
else fail("non-JSON", JSON.stringify(r));
|
|
179
|
+
})();
|
|
180
|
+
|
|
181
|
+
(() => {
|
|
182
|
+
// String containing braces
|
|
183
|
+
const r = parseJsonLoose('{"q": "what about { this }?"}');
|
|
184
|
+
if (r && r.q === "what about { this }?") ok("string with braces inside");
|
|
185
|
+
else fail("string with braces", JSON.stringify(r));
|
|
186
|
+
})();
|
|
187
|
+
|
|
188
|
+
(() => {
|
|
189
|
+
// JSON array at top level
|
|
190
|
+
const r = parseJsonLoose("[1, 2, 3]");
|
|
191
|
+
if (Array.isArray(r) && r.length === 3) ok("top-level array");
|
|
192
|
+
else fail("array", JSON.stringify(r));
|
|
193
|
+
})();
|
|
194
|
+
|
|
195
|
+
(() => {
|
|
196
|
+
// Truncated JSON returns null
|
|
197
|
+
const r = parseJsonLoose('{"a": 1, "b":');
|
|
198
|
+
if (r === null) ok("truncated JSON returns null");
|
|
199
|
+
else fail("truncated", JSON.stringify(r));
|
|
200
|
+
})();
|
|
201
|
+
|
|
202
|
+
// ─── isBuildPrompt ────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
header("[2] isBuildPrompt — trigger detection");
|
|
205
|
+
|
|
206
|
+
const buildPrompts = [
|
|
207
|
+
"build me a 2D market simulation",
|
|
208
|
+
"create a full e-commerce backend",
|
|
209
|
+
"design and implement a chat application",
|
|
210
|
+
"make a complete authentication system",
|
|
211
|
+
"implement a full GraphQL API",
|
|
212
|
+
"build a backend for a todo app",
|
|
213
|
+
"create a marketplace from scratch",
|
|
214
|
+
"build me an end-to-end notification service",
|
|
215
|
+
];
|
|
216
|
+
for (const p of buildPrompts) {
|
|
217
|
+
if (isBuildPrompt(p)) ok(`"${p.slice(0, 50)}..."`, "→ build");
|
|
218
|
+
else fail(`build prompt missed`, p);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const adhocPrompts = [
|
|
222
|
+
"what does this function do",
|
|
223
|
+
"fix the typo in line 5",
|
|
224
|
+
"explain this error",
|
|
225
|
+
"rename foo to bar",
|
|
226
|
+
"show me the file",
|
|
227
|
+
"hi",
|
|
228
|
+
"how do I run this",
|
|
229
|
+
];
|
|
230
|
+
for (const p of adhocPrompts) {
|
|
231
|
+
if (!isBuildPrompt(p)) ok(`"${p}" → not build`);
|
|
232
|
+
else fail(`adhoc prompt over-matched`, p);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── State persistence shape ──────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
header("[3] BuildState shape — JSON round-trip");
|
|
238
|
+
|
|
239
|
+
(() => {
|
|
240
|
+
const state = {
|
|
241
|
+
stage: "execute",
|
|
242
|
+
original_prompt: "build a thing",
|
|
243
|
+
design: {
|
|
244
|
+
goal: "thing",
|
|
245
|
+
requirements: ["does X", "does Y"],
|
|
246
|
+
constraints: ["fast", "simple"],
|
|
247
|
+
artifacts: ["src/thing.ts"],
|
|
248
|
+
},
|
|
249
|
+
todos: [
|
|
250
|
+
{ id: "abc", title: "Write src/thing.ts", description: "create the thing", status: "completed", failure_count: 0, evidence: "1 tool call" },
|
|
251
|
+
{ id: "def", title: "Test it", description: "run tests", status: "pending", failure_count: 1 },
|
|
252
|
+
],
|
|
253
|
+
iteration: 5,
|
|
254
|
+
max_iterations: 30,
|
|
255
|
+
};
|
|
256
|
+
const round = JSON.parse(JSON.stringify(state));
|
|
257
|
+
const equal = JSON.stringify(round) === JSON.stringify(state);
|
|
258
|
+
if (equal) ok("BuildState round-trips through JSON");
|
|
259
|
+
else fail("round-trip", "objects differ");
|
|
260
|
+
})();
|
|
261
|
+
|
|
262
|
+
// ─── extractBalanced edge cases ───────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
header("[4] extractBalanced — bracket matching");
|
|
265
|
+
|
|
266
|
+
const { extractBalanced } = helpers;
|
|
267
|
+
|
|
268
|
+
(() => {
|
|
269
|
+
const r = extractBalanced("xx{a: 1}xx", 2, "{", "}");
|
|
270
|
+
if (r === "{a: 1}") ok("simple braces");
|
|
271
|
+
else fail("simple braces", `got "${r}"`);
|
|
272
|
+
})();
|
|
273
|
+
|
|
274
|
+
(() => {
|
|
275
|
+
const r = extractBalanced('{"q": "with { brace"}', 0, "{", "}");
|
|
276
|
+
if (r === '{"q": "with { brace"}') ok("braces inside string ignored");
|
|
277
|
+
else fail("string brace", `got "${r}"`);
|
|
278
|
+
})();
|
|
279
|
+
|
|
280
|
+
(() => {
|
|
281
|
+
const r = extractBalanced("{a:{b:{c:1}}}", 0, "{", "}");
|
|
282
|
+
if (r === "{a:{b:{c:1}}}") ok("nested braces");
|
|
283
|
+
else fail("nested", `got "${r}"`);
|
|
284
|
+
})();
|
|
285
|
+
|
|
286
|
+
(() => {
|
|
287
|
+
// Unbalanced — returns what we have
|
|
288
|
+
const r = extractBalanced("{a:1", 0, "{", "}");
|
|
289
|
+
if (r === "{a:1") ok("unbalanced returns partial");
|
|
290
|
+
else fail("unbalanced", `got "${r}"`);
|
|
291
|
+
})();
|
|
292
|
+
|
|
293
|
+
console.log();
|
|
294
|
+
if (failed === 0) {
|
|
295
|
+
console.log(`${G}${B}✓ All ${passed} tests passed${N}`);
|
|
296
|
+
process.exit(0);
|
|
297
|
+
} else {
|
|
298
|
+
console.log(`${R}${B}✗ ${failed} failed, ${passed} passed${N}`);
|
|
299
|
+
for (const f of failures) console.log(` ${R}- ${f}${N}`);
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
package/src/db_adapter.ts
CHANGED
|
@@ -164,6 +164,7 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
164
164
|
summary_deletions INTEGER,
|
|
165
165
|
share_url TEXT,
|
|
166
166
|
workspace_id TEXT,
|
|
167
|
+
build_state TEXT,
|
|
167
168
|
created_at TEXT DEFAULT (datetime('now')),
|
|
168
169
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
169
170
|
);
|
|
@@ -309,6 +310,7 @@ export async function initDatabase(silent = false): Promise<{
|
|
|
309
310
|
process.stderr.write(`[DB] Pool error: ${err.message}\n`);
|
|
310
311
|
}
|
|
311
312
|
});
|
|
313
|
+
await ensurePostgresColumns(pgPool);
|
|
312
314
|
return { pool: pgPool, mode: "postgres", pgAvailable: true };
|
|
313
315
|
}
|
|
314
316
|
log(`\x1b[33m[BoneCode] Postgres not reachable at ${dbUrl.replace(/:[^:@]+@/, ":***@")} — trying Docker...\x1b[0m`);
|
|
@@ -330,6 +332,7 @@ export async function initDatabase(silent = false): Promise<{
|
|
|
330
332
|
}
|
|
331
333
|
});
|
|
332
334
|
log(`\x1b[32m[BoneCode] PostgreSQL ready (Docker) — full RAG enabled\x1b[0m`);
|
|
335
|
+
await ensurePostgresColumns(pgPool);
|
|
333
336
|
return { pool: pgPool, mode: "postgres", pgAvailable: true };
|
|
334
337
|
}
|
|
335
338
|
}
|
|
@@ -348,7 +351,33 @@ export async function initDatabase(silent = false): Promise<{
|
|
|
348
351
|
const Database = require("better-sqlite3");
|
|
349
352
|
const db = new Database(dbPath);
|
|
350
353
|
db.exec(SQLITE_SCHEMA);
|
|
354
|
+
// Idempotent migrations for older databases — add new columns if missing.
|
|
355
|
+
// SQLite has no IF NOT EXISTS for columns, so we swallow the duplicate-column error.
|
|
356
|
+
const safeAlter = (sql: string) => { try { db.exec(sql); } catch { /* column exists */ } };
|
|
357
|
+
safeAlter(`ALTER TABLE sessions ADD COLUMN build_state TEXT`);
|
|
351
358
|
db.close();
|
|
352
359
|
|
|
353
360
|
return { pool: sqlitePool, mode: "sqlite", pgAvailable: false };
|
|
354
361
|
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
// ─── Postgres column migrations ───────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Idempotently add columns the BoneScript-generated schema doesn't include.
|
|
368
|
+
* Each ALTER is run with IF NOT EXISTS / pg_class probing so it's safe to run
|
|
369
|
+
* on every server startup.
|
|
370
|
+
*/
|
|
371
|
+
async function ensurePostgresColumns(pool: any): Promise<void> {
|
|
372
|
+
const migrations: Array<{ table: string; column: string; type: string }> = [
|
|
373
|
+
{ table: "sessions", column: "build_state", type: "JSONB" },
|
|
374
|
+
];
|
|
375
|
+
for (const m of migrations) {
|
|
376
|
+
try {
|
|
377
|
+
await pool.query(`ALTER TABLE ${m.table} ADD COLUMN IF NOT EXISTS ${m.column} ${m.type}`);
|
|
378
|
+
} catch {
|
|
379
|
+
// Either the table doesn't exist yet (first run) or the user has
|
|
380
|
+
// restricted permissions. Build mode falls back to permission_ruleset.build.
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|