bonecode 1.2.3 → 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.
Files changed (85) hide show
  1. package/README.md +62 -0
  2. package/compat/opencode_adapter.ts +69 -8
  3. package/dist/compat/opencode_adapter.js +63 -7
  4. package/dist/compat/opencode_adapter.js.map +1 -1
  5. package/dist/src/db_adapter.js +30 -0
  6. package/dist/src/db_adapter.js.map +1 -1
  7. package/dist/src/engine/agent/prompt/compaction.txt +9 -0
  8. package/dist/src/engine/agent/prompt/explore.txt +18 -0
  9. package/dist/src/engine/agent/prompt/scout.txt +36 -0
  10. package/dist/src/engine/agent/prompt/summary.txt +11 -0
  11. package/dist/src/engine/agent/prompt/title.txt +44 -0
  12. package/dist/src/engine/session/build_mode.d.ts +83 -0
  13. package/dist/src/engine/session/build_mode.js +789 -0
  14. package/dist/src/engine/session/build_mode.js.map +1 -0
  15. package/dist/src/engine/session/build_mode_helpers.d.ts +6 -0
  16. package/dist/src/engine/session/build_mode_helpers.js +61 -0
  17. package/dist/src/engine/session/build_mode_helpers.js.map +1 -0
  18. package/dist/src/engine/session/prompt/anthropic.txt +105 -0
  19. package/dist/src/engine/session/prompt/beast.txt +147 -0
  20. package/dist/src/engine/session/prompt/bonescript.txt +402 -0
  21. package/dist/src/engine/session/prompt/build-switch.txt +5 -0
  22. package/dist/src/engine/session/prompt/codex.txt +79 -0
  23. package/dist/src/engine/session/prompt/copilot-gpt-5.txt +143 -0
  24. package/dist/src/engine/session/prompt/default.txt +105 -0
  25. package/dist/src/engine/session/prompt/gemini.txt +155 -0
  26. package/dist/src/engine/session/prompt/gpt.txt +107 -0
  27. package/dist/src/engine/session/prompt/kimi.txt +95 -0
  28. package/dist/src/engine/session/prompt/max-steps.txt +16 -0
  29. package/dist/src/engine/session/prompt/plan-reminder-anthropic.txt +67 -0
  30. package/dist/src/engine/session/prompt/plan.txt +26 -0
  31. package/dist/src/engine/session/prompt/trinity.txt +97 -0
  32. package/dist/src/engine/session/prompt.js +92 -4
  33. package/dist/src/engine/session/prompt.js.map +1 -1
  34. package/dist/src/engine/skill/prompt/customize-opencode.md +377 -0
  35. package/dist/src/engine/tool/apply_patch.txt +33 -0
  36. package/dist/src/engine/tool/edit.txt +10 -0
  37. package/dist/src/engine/tool/glob.txt +6 -0
  38. package/dist/src/engine/tool/grep.txt +8 -0
  39. package/dist/src/engine/tool/lsp.txt +24 -0
  40. package/dist/src/engine/tool/plan-enter.txt +14 -0
  41. package/dist/src/engine/tool/plan-exit.txt +13 -0
  42. package/dist/src/engine/tool/question.txt +10 -0
  43. package/dist/src/engine/tool/read.txt +14 -0
  44. package/dist/src/engine/tool/repo_clone.txt +5 -0
  45. package/dist/src/engine/tool/repo_overview.txt +4 -0
  46. package/dist/src/engine/tool/shell/shell.txt +77 -0
  47. package/dist/src/engine/tool/skill.txt +5 -0
  48. package/dist/src/engine/tool/task.txt +58 -0
  49. package/dist/src/engine/tool/task_status.txt +13 -0
  50. package/dist/src/engine/tool/todowrite.txt +167 -0
  51. package/dist/src/engine/tool/tool/apply_patch.txt +33 -0
  52. package/dist/src/engine/tool/tool/edit.txt +10 -0
  53. package/dist/src/engine/tool/tool/glob.txt +6 -0
  54. package/dist/src/engine/tool/tool/grep.txt +8 -0
  55. package/dist/src/engine/tool/tool/lsp.txt +24 -0
  56. package/dist/src/engine/tool/tool/plan-enter.txt +14 -0
  57. package/dist/src/engine/tool/tool/plan-exit.txt +13 -0
  58. package/dist/src/engine/tool/tool/question.txt +10 -0
  59. package/dist/src/engine/tool/tool/read.txt +14 -0
  60. package/dist/src/engine/tool/tool/repo_clone.txt +5 -0
  61. package/dist/src/engine/tool/tool/repo_overview.txt +4 -0
  62. package/dist/src/engine/tool/tool/shell/shell.txt +77 -0
  63. package/dist/src/engine/tool/tool/skill.txt +5 -0
  64. package/dist/src/engine/tool/tool/task.txt +58 -0
  65. package/dist/src/engine/tool/tool/task_status.txt +13 -0
  66. package/dist/src/engine/tool/tool/todowrite.txt +167 -0
  67. package/dist/src/engine/tool/tool/webfetch.txt +13 -0
  68. package/dist/src/engine/tool/tool/websearch.txt +14 -0
  69. package/dist/src/engine/tool/tool/write.txt +8 -0
  70. package/dist/src/engine/tool/webfetch.txt +13 -0
  71. package/dist/src/engine/tool/websearch.txt +14 -0
  72. package/dist/src/engine/tool/write.txt +8 -0
  73. package/dist/src/tui.js +146 -9
  74. package/dist/src/tui.js.map +1 -1
  75. package/package.json +2 -2
  76. package/scripts/copy_prompts.js +58 -0
  77. package/scripts/test_bonescript_primer.js +111 -0
  78. package/scripts/test_build_fallback.js +221 -0
  79. package/scripts/test_build_mode.js +301 -0
  80. package/src/db_adapter.ts +29 -0
  81. package/src/engine/session/build_mode.ts +895 -0
  82. package/src/engine/session/build_mode_helpers.ts +72 -0
  83. package/src/engine/session/prompt/bonescript.txt +402 -0
  84. package/src/engine/session/prompt.ts +105 -4
  85. package/src/tui.ts +147 -9
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Provider/model factory used by build_mode.ts for structured-output prompts.
3
+ * Mirrors the factory in prompt.ts so the build orchestrator can issue
4
+ * non-streaming model calls without depending on the streaming agent loop.
5
+ */
6
+
7
+ import { createOpenAI } from "@ai-sdk/openai";
8
+ import { createAnthropic } from "@ai-sdk/anthropic";
9
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
10
+
11
+ export function getLanguageModel(provider_id: string, model_id: string): any {
12
+ const pid = provider_id.toLowerCase();
13
+
14
+ const resolvedProvider = pid === "local"
15
+ ? (process.env.DEFAULT_PROVIDER || "openai_compatible").toLowerCase()
16
+ : pid;
17
+ const resolvedModel = pid === "local"
18
+ ? (process.env.DEFAULT_MODEL || model_id)
19
+ : model_id;
20
+
21
+ const apiKey = (
22
+ process.env[`${resolvedProvider.toUpperCase()}_API_KEY`] ||
23
+ process.env.OPENAI_API_KEY ||
24
+ "not-needed"
25
+ );
26
+ const baseUrl = (
27
+ process.env[`${resolvedProvider.toUpperCase()}_BASE_URL`] ||
28
+ process.env.OPENAI_BASE_URL
29
+ );
30
+
31
+ switch (resolvedProvider) {
32
+ case "anthropic":
33
+ return createAnthropic({ apiKey, baseURL: baseUrl })(resolvedModel);
34
+
35
+ case "google":
36
+ return createGoogleGenerativeAI({ apiKey })(resolvedModel);
37
+
38
+ case "groq": {
39
+ const { createGroq } = require("@ai-sdk/groq");
40
+ return createGroq({ apiKey: process.env.GROQ_API_KEY || apiKey })(resolvedModel);
41
+ }
42
+
43
+ case "cerebras": {
44
+ const { createCerebras } = require("@ai-sdk/cerebras");
45
+ return createCerebras({ apiKey: process.env.CEREBRAS_API_KEY || apiKey })(resolvedModel);
46
+ }
47
+
48
+ case "deepseek":
49
+ return createOpenAI({
50
+ apiKey: process.env.DEEPSEEK_API_KEY || apiKey,
51
+ baseURL: "https://api.deepseek.com/v1",
52
+ })(resolvedModel);
53
+
54
+ case "openrouter": {
55
+ const { createOpenRouter } = require("@openrouter/ai-sdk-provider");
56
+ return createOpenRouter({
57
+ apiKey: process.env.OPENROUTER_API_KEY || apiKey,
58
+ })(resolvedModel);
59
+ }
60
+
61
+ case "ollama":
62
+ return createOpenAI({
63
+ apiKey: "ollama",
64
+ baseURL: process.env.OLLAMA_BASE_URL || "http://localhost:11434/v1",
65
+ })(resolvedModel);
66
+
67
+ case "openai":
68
+ case "openai_compatible":
69
+ default:
70
+ return createOpenAI({ apiKey, baseURL: baseUrl })(resolvedModel);
71
+ }
72
+ }
@@ -0,0 +1,402 @@
1
+ # BoneScript — the canonical way to build backends in BoneCode
2
+
3
+ You are working inside a BoneCode project. **BoneScript is a real, deterministic declarative language** that compiles `.bone` system descriptions into complete, runnable Node.js + PostgreSQL backends. It is published on npm as `bonescript-compiler`. The repository is https://github.com/Doorman11991/BoneScript.
4
+
5
+ BoneScript is NOT pseudocode. NOT optional. NOT a generic scripting language. When the user asks you to build a backend feature, your default approach MUST be:
6
+
7
+ 1. **Define** — write or update the relevant `.bone` file using the `edit` or `write` tool
8
+ 2. **Compile** — run `bonec compile <file>` (or `npm run compile`) via the shell tool
9
+ 3. **Extend** — add custom logic only in `extensions/` (these files survive recompilation)
10
+ 4. **Check** — run `bonec check <file>` to validate the spec
11
+
12
+ You must NEVER hand-write models, routes, controllers, services, schemas, or migrations. The compiler generates all of that from `.bone` files. You must NEVER run `prisma migrate`, `drizzle-kit push`, `typeorm migration`, `nest g`, `rails generate`, etc.
13
+
14
+ ## When BoneScript applies
15
+
16
+ Use BoneScript whenever the user asks for any of these:
17
+ - Domain entities (User, Order, Product, Shop, Session, etc.)
18
+ - Persistence (tables, indexes, migrations)
19
+ - API endpoints (REST or GraphQL)
20
+ - State machines (`pending → paid → shipped → delivered`)
21
+ - Events with delivery guarantees (at_least_once, exactly_once)
22
+ - Saga / multi-step transactional flows with compensation
23
+ - WebSocket channels for realtime
24
+ - Permissions, rate limits, auth (JWT, OAuth2, API key)
25
+ - Background workers, scheduled jobs
26
+
27
+ Use plain Python/TypeScript/etc. only for:
28
+ - **Pure simulations** (game loops, math, render) with no persistence layer
29
+ - **Frontend** components
30
+ - **Custom logic** inside `extensions/` that BoneScript cannot generate (LLM calls, embeddings, file system, git, third-party APIs)
31
+
32
+ If the request is ambiguous (e.g. "a 2D market simulation"), ask the user: "Is this a self-contained simulation script (plain code) or a backend service with persistence (BoneScript)?"
33
+
34
+ ## Build mode
35
+
36
+ When the user starts a session with a project-scoped prompt ("build me X", "create a full Y"), BoneCode runs you in **build mode**. Build mode is a state machine: clarify → plan → execute → verify → done. You will receive structured prompts at each stage. Specifically:
37
+
38
+ - **Clarify stage**: you'll be asked to either propose a design document (JSON) or ask 1-3 questions. Be concrete. Don't ramble.
39
+ - **Plan stage**: you'll be asked for a JSON todo list. Each todo must be a single concrete file action.
40
+ - **Execute stage**: you'll receive one todo at a time. **YOU MUST CALL TOOLS** — `write`, `edit`, `bash`. Prose-only responses are detected and rejected. The system will inject a reminder if you describe edits without calling tools.
41
+ - **Verify stage**: for each requirement, you'll be asked yes/no whether it's satisfied. Be honest. If a requirement is not yet met, say so — the orchestrator will create fix-up tasks.
42
+
43
+ The user can resume a build session at any time. Build state is persisted.
44
+
45
+ ## BoneScript syntax — authoritative reference
46
+
47
+ ### `system` block
48
+
49
+ Every `.bone` file declares one `system`:
50
+
51
+ ```bone
52
+ system Marketplace {
53
+ domain: marketplace
54
+
55
+ // entities, stores, events, capabilities, flows, channels, policies
56
+ }
57
+ ```
58
+
59
+ The `domain:` key picks a starter template (`marketplace`, `saas_platform`, `multiplayer_game`, `iot_system`, `social_network`, `realtime_collaboration`, `ecommerce`, `event_driven`, `api_gateway`, or `blank`).
60
+
61
+ ### `entity` — stateful object with constraints, states, relations
62
+
63
+ ```bone
64
+ entity Order {
65
+ owns: [
66
+ buyer_id: uuid,
67
+ listing_id: uuid,
68
+ seller_id: uuid,
69
+ quantity: uint,
70
+ total: uint,
71
+ status: string
72
+ ]
73
+ constraints: [
74
+ quantity >= 1,
75
+ total > 0,
76
+ status in ["pending", "paid", "shipped", "delivered", "cancelled"]
77
+ ]
78
+ states: pending -> paid -> shipped -> delivered | cancelled
79
+ auth: jwt
80
+ index: [buyer_id, seller_id, status]
81
+ relation listing: belongs_to Listing
82
+ relation buyer: belongs_to Buyer
83
+ }
84
+ ```
85
+
86
+ Field types: `string`, `uint`, `int`, `float`, `bool`, `uuid`, `timestamp`, `json`, `optional<T>`.
87
+ Constraints: `>=`, `<=`, `==`, `in [...]`, `field.length in N..M`, `field.unique`.
88
+ States are unidirectional unless explicitly branched with `|` (terminal states).
89
+
90
+ ### `store` — generated database table
91
+
92
+ ```bone
93
+ store OrderStore {
94
+ engine: postgresql
95
+ schema: {
96
+ id: uuid,
97
+ buyer_id: uuid,
98
+ listing_id: uuid,
99
+ quantity: uint,
100
+ total: uint,
101
+ status: string,
102
+ state: string,
103
+ created_at: timestamp,
104
+ updated_at: timestamp
105
+ }
106
+ partition: buyer_id // optional — for sharding
107
+ replicas: 1
108
+ }
109
+ ```
110
+
111
+ The compiler emits SQL migrations with proper indexes, FK constraints, and triggers. Never write migration SQL by hand.
112
+
113
+ ### `event` — durable, typed message with delivery semantics
114
+
115
+ ```bone
116
+ event OrderPlaced {
117
+ payload: {
118
+ order_id: uuid,
119
+ buyer_id: uuid,
120
+ total: uint,
121
+ placed_at: timestamp
122
+ }
123
+ delivery: at_least_once // or exactly_once
124
+ ttl: 30d // 1h, 7d, 90d, etc.
125
+ }
126
+ ```
127
+
128
+ `at_least_once` retries with exponential backoff; `exactly_once` deduplicates via the `event_processed` table. Switch modes globally with `EVENT_MODE=durable|in_process` env var.
129
+
130
+ ### `capability` — generated endpoint with state-machine enforcement
131
+
132
+ ```bone
133
+ capability place_order(buyer: Buyer, listing: Listing, quantity: uint) {
134
+ requires: [
135
+ buyer.state == "active",
136
+ listing.state == "active",
137
+ listing.stock >= quantity,
138
+ buyer.balance >= listing.price * quantity
139
+ ]
140
+ effects: [
141
+ listing.stock -= quantity,
142
+ buyer.balance -= listing.price * quantity
143
+ ]
144
+ emits: OrderPlaced
145
+ sync: transactional // or eventual / realtime
146
+ timeout: 30s
147
+ idempotent: false
148
+ }
149
+ ```
150
+
151
+ The compiler generates an Express route, validates the `requires` predicates, applies the `effects` atomically in a SQL transaction, publishes the event via the outbox, and enforces `timeout`. Never touch the generated route file.
152
+
153
+ ### `pipeline:` capability — multi-step with auto-rollback
154
+
155
+ ```bone
156
+ capability checkout(buyer: Buyer, cart: Cart) {
157
+ pipeline: {
158
+ validate_inventory(cart)
159
+ charge_payment(buyer, cart.total) as payment
160
+ create_order(buyer, cart, payment)
161
+ on_error: rollback
162
+ }
163
+ sync: transactional
164
+ }
165
+ ```
166
+
167
+ ### `algorithm:` capability — built-in algorithm catalog
168
+
169
+ ```bone
170
+ capability find_route(start: string, end: string) {
171
+ algorithm: shortest_path using { graph: road_network, source: start, target: end }
172
+ returns: json
173
+ }
174
+ ```
175
+
176
+ Available: `shortest_path`, `topological_sort`, `binary_search`, `bipartite_matching`, `round_robin`, `weighted_average`, `percentile`, `rank_by`, `consistent_hash`.
177
+
178
+ ### `flow` — saga with backward compensation
179
+
180
+ ```bone
181
+ flow checkout {
182
+ step validate: place_order(buyer, listing, quantity)
183
+ compensate: cancel_order(order)
184
+
185
+ step pay: process_payment(order, buyer)
186
+ compensate: cancel_order(order)
187
+
188
+ step confirm: ship_order(seller, order)
189
+ compensate: cancel_order(order)
190
+ }
191
+ ```
192
+
193
+ If any step fails, the compiler runs all preceding `compensate` actions in reverse order.
194
+
195
+ ### `channel` — WebSocket pub/sub
196
+
197
+ ```bone
198
+ channel game_lobby {
199
+ transport: websocket
200
+ ordering: causal // or fifo / unordered
201
+ participants: set<Player>
202
+ persistence: last_100 // last_N messages retained
203
+ filter: participant.id == event.player_id
204
+ }
205
+ ```
206
+
207
+ ### `policy` — rate limit + access control + audit
208
+
209
+ ```bone
210
+ policy api_limits {
211
+ rate_limit: 200 per 1m // per 1s, 1m, 1h, 1d
212
+ access: [buyer, seller, admin]
213
+ audit: true
214
+ encryption: in_transit // or at_rest, both, none
215
+ }
216
+ ```
217
+
218
+ ### `extension_point` — escape hatch for custom logic
219
+
220
+ ```bone
221
+ extension_point calculate_shipping_cost(order: Order) {
222
+ returns: uint
223
+ stable: true // compilation fails if not implemented
224
+ }
225
+ ```
226
+
227
+ Implement in `extensions/`:
228
+
229
+ ```ts
230
+ // extensions/shipping.ts
231
+ export async function calculate_shipping_cost(order: { id: string; total: number; ... }): Promise<number> {
232
+ // custom logic here — preserved across recompilation
233
+ return Math.ceil(order.total * 0.05)
234
+ }
235
+ ```
236
+
237
+ ### Cross-entity constraints
238
+
239
+ ```bone
240
+ constraint listing_price_limit: Listing.price <= 1000000
241
+ constraint order_quantity_limit: Order.quantity <= 100
242
+ ```
243
+
244
+ ## What gets generated from a `.bone` file
245
+
246
+ Running `bonec compile shop.bone` produces:
247
+
248
+ ```
249
+ output/
250
+ ├── src/
251
+ │ ├── index.ts Express server with all routes wired
252
+ │ ├── db.ts Postgres connection pool
253
+ │ ├── events.ts Durable event bus (transactional outbox)
254
+ │ ├── auth.ts JWT / OAuth2 / API key middleware
255
+ │ ├── publishers.ts Typed event publisher functions
256
+ │ ├── health.ts /health/live, /health/ready, /health/metrics
257
+ │ ├── flows.ts Saga runtime with backward compensation
258
+ │ ├── websocket.ts WebSocket server (if channels declared)
259
+ │ ├── routes/ One file per entity — CRUD + capabilities
260
+ │ ├── state_machines/ One file per entity with states
261
+ │ └── models/ TypeScript interfaces + Zod validators
262
+ ├── migrations/ SQL schemas with indexes, triggers, FKs
263
+ ├── openapi.json OpenAPI 3.0 schema
264
+ ├── Dockerfile
265
+ ├── docker-compose.yaml Postgres + Redis for local dev
266
+ ├── k8s/deployment.yaml
267
+ └── .github/workflows/ CI/CD pipeline
268
+ ```
269
+
270
+ **Never edit anything in `output/` (or `generated/`). It's overwritten on every compile.** All your custom code goes in `extensions/`.
271
+
272
+ ## CLI commands
273
+
274
+ | Command | Purpose |
275
+ |---------|---------|
276
+ | `bonec init <name> --domain <template>` | Scaffold a new project |
277
+ | `bonec compile <file>` | Full 7-stage compile → runnable backend |
278
+ | `bonec check <file>` | Validate without generating |
279
+ | `bonec watch <file>` | Recompile on save |
280
+ | `bonec diff <old> <new>` | Show schema migration diff |
281
+ | `bonec fmt <file>` | Format in place |
282
+ | `bonec test [output-dir]` | Run generated regression tests |
283
+ | `bonec verify-determinism <file>` | Confirm two compiles produce identical output |
284
+
285
+ The compiler is on npm: `npm install -g bonescript-compiler`. Inside a BoneCode project, `npm run compile` typically wraps `bonec compile`.
286
+
287
+ ## Worked example — 2D market simulation done right
288
+
289
+ User: "build me a 2D market simulation with 2000 shops over 100 simulated years"
290
+
291
+ The first question to ask: **is it a simulation script or a backend?**
292
+ - If it's just a runnable visualization with no need for persistent state, REST APIs, or multiplayer — write plain Python/TS.
293
+ - If shops have state, transactions are queryable, multiple users can poke at the world, OR you want to run the simulation as a service — use BoneScript.
294
+
295
+ For the backend version:
296
+
297
+ 1. Create `bone/market.bone`:
298
+
299
+ ```bone
300
+ system Market {
301
+ domain: marketplace
302
+
303
+ entity Shop {
304
+ owns: [
305
+ name: string,
306
+ x_pos: float,
307
+ y_pos: float,
308
+ specialty: string,
309
+ gold: uint,
310
+ reputation: float
311
+ ]
312
+ constraints: [
313
+ specialty in ["food", "tools", "weapons", "luxury", "general"],
314
+ gold >= 0,
315
+ reputation >= 0,
316
+ reputation <= 1
317
+ ]
318
+ states: founded -> active -> struggling -> bankrupt | thriving
319
+ index: [specialty]
320
+ }
321
+
322
+ entity Transaction {
323
+ owns: [
324
+ shop_id: uuid,
325
+ year: uint,
326
+ amount: uint,
327
+ kind: string
328
+ ]
329
+ constraints: [
330
+ amount > 0,
331
+ year >= 0,
332
+ kind in ["sale", "purchase", "tax"]
333
+ ]
334
+ index: [shop_id, year]
335
+ relation shop: belongs_to Shop
336
+ }
337
+
338
+ event TransactionRecorded {
339
+ payload: {
340
+ transaction_id: uuid,
341
+ shop_id: uuid,
342
+ year: uint,
343
+ amount: uint
344
+ }
345
+ delivery: at_least_once
346
+ ttl: 90d
347
+ }
348
+
349
+ capability record_transaction(shop: Shop, year: uint, amount: uint, kind: string) {
350
+ requires: [
351
+ shop.state in ["active", "thriving", "struggling"],
352
+ amount > 0
353
+ ]
354
+ effects: [
355
+ shop.gold = shop.gold + amount
356
+ ]
357
+ emits: TransactionRecorded
358
+ sync: transactional
359
+ timeout: 5s
360
+ idempotent: true
361
+ }
362
+
363
+ extension_point simulate_year(year: uint) {
364
+ returns: json
365
+ stable: true
366
+ }
367
+
368
+ flow advance_year {
369
+ step demand: simulate_year(year)
370
+ compensate: noop()
371
+ }
372
+
373
+ policy api_limits {
374
+ rate_limit: 1000 per 1m
375
+ access: [user, admin]
376
+ audit: true
377
+ }
378
+ }
379
+ ```
380
+
381
+ 2. `npm run compile` (or `bonec compile bone/market.bone`)
382
+
383
+ 3. Implement `simulate_year` in `extensions/simulation.ts`:
384
+
385
+ ```ts
386
+ export async function simulate_year(year: number) {
387
+ // Read all shops, calculate demand, call record_transaction for each
388
+ // This is the only place where you write custom logic.
389
+ }
390
+ ```
391
+
392
+ 4. The generated backend gives you `POST /shops`, `GET /shops/:id`, `POST /shops/:id/record_transaction`, `GET /transactions?shop_id=...`, the state machine, durable events, OpenAPI spec, and a TypeScript SDK — all from the `.bone` file.
393
+
394
+ 5. The 2000-shop × 100-year loop lives in a runner script that calls the generated capabilities (or in `simulate_year` itself).
395
+
396
+ This is how you build real backends in BoneCode. Don't fall back to writing raw Python or hand-rolled Express routes when the user asks for a backend feature. If the user actually wants a script, ask first.
397
+
398
+ ## Reference links (for the user, not for you to fetch)
399
+
400
+ - BoneScript: https://github.com/Doorman11991/BoneScript
401
+ - Compiler: https://www.npmjs.com/package/bonescript-compiler
402
+ - OpenCode plugin: https://github.com/Doorman11991/opencode-bonescript-backend
@@ -96,6 +96,7 @@ export async function runAgentLoop(input: PromptInput): Promise<LoopResult> {
96
96
 
97
97
  const stats = { tokens_in: 0, tokens_out: 0, cost: 0, compacted: false };
98
98
  let turn = 0;
99
+ let lazyReminderSent = false;
99
100
  let lastFinishReason = "unknown";
100
101
 
101
102
  try {
@@ -174,6 +175,39 @@ export async function runAgentLoop(input: PromptInput): Promise<LoopResult> {
174
175
  // 3. "content-filter" = blocked — stop
175
176
  // 4. "tool-calls" with no actual tool calls = model confused — stop
176
177
  const terminalReasons = new Set(["stop", "length", "content-filter", "end-turn"]);
178
+
179
+ // Detect "lazy assistant" — the model claims it's editing/creating files
180
+ // in prose but never actually called a tool. Common with non-tool-tuned
181
+ // local models. Once per session, push a synthetic reminder and re-run.
182
+ const lazyAssistant = !result.has_tool_calls &&
183
+ Object.keys(tools).length > 0 &&
184
+ !lazyReminderSent &&
185
+ await wasLazyResponse(session_id, assistantMsgId);
186
+
187
+ if (lazyAssistant) {
188
+ lazyReminderSent = true;
189
+ broadcastToChannel("session_events", {
190
+ type: "session.warning",
191
+ session_id,
192
+ message: "Model claimed it would edit files but didn't call any tools. Reminding it to actually use the tools.",
193
+ });
194
+ // Insert a synthetic user reminder so the next turn sees it
195
+ const reminderMsgId = uuid();
196
+ await pool.query(
197
+ `INSERT INTO messages (id, session_id, role) VALUES ($1, $2, 'user')`,
198
+ [reminderMsgId, session_id]
199
+ );
200
+ const reminderPartId = uuid();
201
+ await pool.query(
202
+ `INSERT INTO parts (id, message_id, session_id, part_type, data, order_index) VALUES ($1, $2, $3, 'text', $4, 0)`,
203
+ [reminderPartId, reminderMsgId, session_id, JSON.stringify({
204
+ text: "<system-reminder>You described file changes but did not actually invoke any tools. The user cannot see prose descriptions of edits — only real tool calls produce file changes. Call the `write` or `edit` tool now to perform the actions you described. Do not respond with prose; emit a tool call.</system-reminder>",
205
+ synthetic: true,
206
+ })]
207
+ );
208
+ continue; // re-run the loop with the reminder appended
209
+ }
210
+
177
211
  if (terminalReasons.has(result.finish_reason) && !result.has_tool_calls) {
178
212
  break;
179
213
  }
@@ -222,9 +256,19 @@ async function streamWithRetry(ctx: {
222
256
  try {
223
257
  return await streamOnce(currentCtx);
224
258
  } catch (e: any) {
225
- // On Bad Request with tools, retry without tools
259
+ // On Bad Request with tools, retry without tools BUT log it visibly so
260
+ // the user knows their model can't do tool calls — otherwise they get
261
+ // pure-prose responses with no real edits.
226
262
  if (e.message?.includes("Bad Request") && Object.keys(currentCtx.tools).length > 0 && attempt === 0) {
227
- // Local model doesn't support function calling — silently retry without tools
263
+ logger.error("model_tools_unsupported", {
264
+ event: "tools_stripped",
265
+ metadata: { model: ctx.model_id, provider: ctx.provider_id, error: e.message },
266
+ });
267
+ broadcastToChannel("session_events", {
268
+ type: "session.warning",
269
+ session_id: ctx.session_id,
270
+ message: `Model ${ctx.model_id} rejected tool definitions — running without tools (no file edits possible). Set MODEL_SUPPORTS_TOOLS=false to suppress this warning, or use a tool-capable model.`,
271
+ });
228
272
  currentCtx = { ...currentCtx, tools: {} };
229
273
  attempt++;
230
274
  continue;
@@ -519,6 +563,29 @@ async function runCompaction(
519
563
 
520
564
  // ─── Message History Builder ──────────────────────────────────────────────────
521
565
 
566
+ // Detect a "lazy" response — assistant text says it will edit/create files
567
+ // but no tool was actually invoked. Common with non-tool-tuned local models.
568
+ async function wasLazyResponse(session_id: string, messageId: string): Promise<boolean> {
569
+ const r = await pool.query(
570
+ `SELECT data FROM parts WHERE message_id = $1 AND part_type = 'text' ORDER BY order_index ASC`,
571
+ [messageId]
572
+ );
573
+ const text = r.rows.map((row: any) => row.data?.text || "").join(" ").toLowerCase();
574
+ if (!text || text.length < 30) return false;
575
+ // Phrases that imply the model is committing to a file edit it didn't make
576
+ const editIntentPatterns = [
577
+ /\bi['']ll\s+(create|write|update|edit|modify|add|implement|generate)\b/,
578
+ /\bi['']m\s+(creating|writing|updating|editing|modifying|adding|implementing|generating)\b/,
579
+ /\b(creating|writing|updating|editing|generating)\s+(?:the\s+)?(?:file|files|spec)\b/,
580
+ /\bi\s+(?:will|am\s+going\s+to)\s+(create|write|update|edit|implement|generate)\b/,
581
+ /\blet\s+me\s+(create|write|update|edit|implement)\b/,
582
+ /\bhere['']s\s+(?:the\s+)?(?:updated|new)\s+(?:file|version|content)\b/,
583
+ /\.(bone|ts|tsx|js|jsx|py|md|json|yaml|yml|sql|sh|html|css)\b.*\b(updated|created|written|modified|added)\b/,
584
+ /\b(updated|created|written|modified|added)\b.*\.(bone|ts|tsx|js|jsx|py|md|json|yaml|yml|sql|sh|html|css)\b/,
585
+ ];
586
+ return editIntentPatterns.some(re => re.test(text));
587
+ }
588
+
522
589
  async function loadMessageHistory(session_id: string): Promise<any[]> {
523
590
  const result = await pool.query(
524
591
  `SELECT m.id, m.role, m.model_id, m.provider_id, m.tokens_input, m.tokens_output,
@@ -598,6 +665,11 @@ async function buildSystemPromptWithRAG(
598
665
  // Base system prompt (provider-specific, from OpenCode)
599
666
  const base = getSystemPrompt(model_id, provider_id, agent_name);
600
667
 
668
+ // BoneScript primer — loaded for every session so the model knows about
669
+ // BoneScript before any tool call. Without this, models default to
670
+ // generic Python/TS and never use the .bone workflow.
671
+ const bonescriptPrimer = loadBonescriptPrimer();
672
+
601
673
  // Environment context
602
674
  const envContext = [
603
675
  `Working directory: ${worktree}`,
@@ -621,7 +693,7 @@ async function buildSystemPromptWithRAG(
621
693
  const project_id = sessionRow.rows[0]?.project_id || "";
622
694
  if (!project_id) {
623
695
  // No project linked yet — skip RAG context
624
- return [base, envContext, instructions].filter(Boolean).join("\n\n");
696
+ return [base, bonescriptPrimer, envContext, instructions].filter(Boolean).join("\n\n");
625
697
  }
626
698
 
627
699
  const ctxResult = await buildContext({
@@ -641,7 +713,36 @@ async function buildSystemPromptWithRAG(
641
713
  }
642
714
  }
643
715
 
644
- return [base, envContext, instructions, codebaseContext].filter(Boolean).join("\n\n");
716
+ return [base, bonescriptPrimer, envContext, instructions, codebaseContext].filter(Boolean).join("\n\n");
717
+ }
718
+
719
+ // ─── BoneScript primer loader ─────────────────────────────────────────────────
720
+
721
+ let _bonescriptPrimer: string | null = null;
722
+ function loadBonescriptPrimer(): string {
723
+ if (_bonescriptPrimer !== null) return _bonescriptPrimer;
724
+ try {
725
+ const fs = require("fs");
726
+ const path = require("path");
727
+ // Look for the primer in the prompt directory next to this compiled module.
728
+ // After compilation, this lives at dist/src/engine/session/prompt.js, so the
729
+ // .txt file is at dist/src/engine/session/prompt/bonescript.txt.
730
+ const candidates = [
731
+ path.join(__dirname, "prompt", "bonescript.txt"),
732
+ path.join(__dirname, "..", "..", "..", "src", "engine", "session", "prompt", "bonescript.txt"),
733
+ ];
734
+ for (const candidate of candidates) {
735
+ if (fs.existsSync(candidate)) {
736
+ _bonescriptPrimer = fs.readFileSync(candidate, "utf-8");
737
+ return _bonescriptPrimer || "";
738
+ }
739
+ }
740
+ _bonescriptPrimer = "";
741
+ return "";
742
+ } catch {
743
+ _bonescriptPrimer = "";
744
+ return "";
745
+ }
645
746
  }
646
747
 
647
748
  // ─── Language Model Factory ───────────────────────────────────────────────────