babavoss 0.0.1 → 0.0.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/NOTICE +7 -0
- package/bin/voss.ts +48 -0
- package/gui/babavoss-web.js +234 -0
- package/gui/chunk-5hpp1ypv.js +11710 -0
- package/gui/chunk-9rq662fd.js +8627 -0
- package/gui/chunk-k3cm8j6r.js +433 -0
- package/gui/chunk-pk09y98y.js +50 -0
- package/gui/chunk-smz02qa6.js +186 -0
- package/gui/chunk-wwqypxre.js +49 -0
- package/gui/gui.js +2015 -0
- package/gui/react-compiler-runtime.js +39 -0
- package/gui/react-dom-client.js +21 -0
- package/gui/react-dom.js +44 -0
- package/gui/react-jsx-runtime.js +19 -0
- package/gui/react.js +105 -0
- package/gui/theme.css +3053 -0
- package/index.ts +15 -0
- package/package.json +50 -4
- package/src/baba/check.ts +90 -0
- package/src/baba/config.ts +261 -0
- package/src/baba/find.ts +12 -0
- package/src/baba/init.ts +176 -0
- package/src/baba/node.ts +483 -0
- package/src/baba/project.ts +63 -0
- package/src/baba/registry.ts +35 -0
- package/src/baba/worker.ts +55 -0
- package/src/bench/index.ts +6 -0
- package/src/bench/measure.ts +132 -0
- package/src/bench/scenarios.ts +136 -0
- package/src/build/builder.ts +74 -0
- package/src/build/failure.ts +78 -0
- package/src/build/guard.ts +85 -0
- package/src/build/mdx-register.ts +3 -0
- package/src/build/mdx.ts +38 -0
- package/src/build/project.ts +38 -0
- package/src/build/views.ts +146 -0
- package/src/builder/main.ts +29 -0
- package/src/desktop/bob.ts +76 -0
- package/src/desktop/desktop.css +111 -0
- package/src/desktop/icons.ts +50 -0
- package/src/desktop/index.ts +323 -0
- package/src/desktop/routes.ts +98 -0
- package/src/desktop/view.tsx +673 -0
- package/src/door/core.ts +384 -0
- package/src/ecs/baba.ts +431 -0
- package/src/ecs/codec.ts +334 -0
- package/src/ecs/handles.ts +91 -0
- package/src/ecs/replica.ts +150 -0
- package/src/ecs/runtime.ts +603 -0
- package/src/ecs/scheduler.ts +75 -0
- package/src/ecs/snapshot.ts +102 -0
- package/src/ecs/state.ts +759 -0
- package/src/ecs/system.ts +256 -0
- package/src/ecs/table.ts +420 -0
- package/src/ecs/testbed.ts +97 -0
- package/src/exec/host.ts +177 -0
- package/src/exec/main.ts +98 -0
- package/src/exec/watch.ts +7 -0
- package/src/exec/wire.ts +29 -0
- package/src/generated/build.ts +4 -0
- package/src/gui/css.d.ts +1 -0
- package/src/gui/gui.tsx +245 -0
- package/src/gui/index.ts +51 -0
- package/src/gui/inspector.tsx +47 -0
- package/src/gui/levels.tsx +73 -0
- package/src/gui/promptware.tsx +68 -0
- package/src/gui/runner.tsx +118 -0
- package/src/gui/theme.css +498 -0
- package/src/gui/theme.ts +25 -0
- package/src/gui/wizard.tsx +227 -0
- package/src/guide/add-a-desktop.mdx +100 -0
- package/src/guide/compose-an-interface.mdx +84 -0
- package/src/guide/index.ts +13 -0
- package/src/guide/reach-outside.mdx +112 -0
- package/src/guide/spec-a-system.mdx +93 -0
- package/src/guide/systems-together.mdx +72 -0
- package/src/guide/write-a-system.mdx +183 -0
- package/src/guide/write-promptware.mdx +90 -0
- package/src/http/server.ts +310 -0
- package/src/kernel/build.ts +21 -0
- package/src/kernel/builder.ts +105 -0
- package/src/kernel/children.ts +117 -0
- package/src/kernel/context.ts +90 -0
- package/src/kernel/lock.ts +46 -0
- package/src/kernel/names.ts +14 -0
- package/src/kernel/schema.ts +130 -0
- package/src/kernel/where.ts +12 -0
- package/src/kit/index.ts +232 -0
- package/src/maker/system.ts +213 -0
- package/src/mcp/daemon.ts +61 -0
- package/src/mcp/main.ts +208 -0
- package/src/mcp/rpc.ts +64 -0
- package/src/mcp/tools.ts +125 -0
- package/src/prompt/evals.ts +42 -0
- package/src/prompt/index.ts +242 -0
- package/src/prompt/jsx-dev-runtime.ts +1 -0
- package/src/prompt/jsx-runtime.ts +49 -0
- package/src/prompt/mdx.d.ts +1 -0
- package/src/promptware/compile.ts +183 -0
- package/src/promptware/define.ts +16 -0
- package/src/promptware/disk.ts +72 -0
- package/src/promptware/markdown.d.ts +6 -0
- package/src/promptware/sync.ts +437 -0
- package/src/promptware/system.ts +215 -0
- package/src/runtime/bridge.ts +85 -0
- package/src/runtime/connect.ts +54 -0
- package/src/runtime/env.ts +35 -0
- package/src/runtime/harness.ts +80 -0
- package/src/runtime/main.ts +119 -0
- package/src/runtime/worker.ts +33 -0
- package/src/server/edge.ts +332 -0
- package/src/server/main.ts +45 -0
- package/src/server/messages.ts +97 -0
- package/src/server/protocol.ts +37 -0
- package/src/services/args.ts +45 -0
- package/src/services/exec.ts +69 -0
- package/src/services/fs.ts +139 -0
- package/src/services/http.ts +30 -0
- package/src/services/index.ts +113 -0
- package/src/services/secrets.ts +18 -0
- package/src/shell/address.ts +21 -0
- package/src/shell/args.ts +219 -0
- package/src/shell/client.ts +107 -0
- package/src/shell/codes.ts +26 -0
- package/src/shell/positional.ts +20 -0
- package/src/shell/run.ts +470 -0
- package/src/shell/service.ts +167 -0
- package/src/shell/state.ts +204 -0
- package/src/spec/adapters.ts +72 -0
- package/src/spec/diff.ts +26 -0
- package/src/spec/files.ts +17 -0
- package/src/spec/index.ts +155 -0
- package/src/spec/run.ts +97 -0
- package/src/spec/take.ts +54 -0
- package/src/test/index.ts +8 -0
- package/src/test/prove.ts +56 -0
- package/src/test/records.ts +23 -0
- package/src/test/specs.ts +56 -0
- package/src/test/steps.ts +100 -0
- package/src/test/voss-dir.ts +17 -0
- package/src/transport/messages.ts +110 -0
- package/src/transport/transport.ts +62 -0
- package/src/wall/probe.ts +67 -0
- package/src/wall/profile.ts +103 -0
- package/src/wall/spawn.ts +59 -0
- package/src/web/app.tsx +53 -0
- package/src/web/core.tsx +140 -0
- package/src/web/form.ts +155 -0
- package/src/web/hooks.ts +135 -0
- package/src/web/index.tsx +17 -0
- package/src/web/list.ts +19 -0
- package/src/web/maker.tsx +766 -0
- package/src/web/objects.tsx +213 -0
- package/src/web/socket.ts +84 -0
- package/src/web/state.tsx +69 -0
- package/src/web/store.ts +221 -0
- package/src/web/ui.tsx +135 -0
- package/README.md +0 -5
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export const prompt = { kind: "skill", name: "spec-a-system", description: "Read before testing a system: its spec in systems/NAME/spec.ts beside it (the system and what it reads), the adapters that play its outside, quiet effects, setup, params, scenarios and their script, saved scenarios, the one runner .baba/spec.test.ts under bun test, and the Maker that runs the same specs live." };
|
|
2
|
+
|
|
3
|
+
# Spec a system
|
|
4
|
+
|
|
5
|
+
There is one way to test a system: its spec, `systems/NAME/spec.ts`, beside its `index.ts`. The spec is the system and every system it reads, and what those read, seeded by a setup, with its outside played by adapters, and its scenarios: what must hold, as a script of steps and checks. The whole baba has one too, `.baba/spec.ts`, named `baba`. One program runs in two places: headless under `bun test`, which writes the verdicts, and live in the Maker, where a person watches every round and may act. `voss baba add-system NAME` writes the first spec beside the system.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// .baba/systems/digest/spec.ts
|
|
9
|
+
import { spec, ok, fail, after, sequence } from "babavoss/spec";
|
|
10
|
+
import digest from "./index.ts";
|
|
11
|
+
import pagesSpec from "../pages/spec.ts";
|
|
12
|
+
|
|
13
|
+
export default spec(digest, {
|
|
14
|
+
summary: "the day's pages, mailed once a day", // the intent, as the person said it
|
|
15
|
+
also: [pagesSpec], // digest reads pages: its adapters, setup and params come along
|
|
16
|
+
tick: 200, // rounds on their own every 200 ms, so the state moves while a person watches
|
|
17
|
+
now: 1_000, // the first time; `seed` the random seed, 1 by default
|
|
18
|
+
params: { to: "ada@example.test" }, // knobs with defaults, for the setup and every scenario
|
|
19
|
+
setup: ({ tick }) => [tick({ after: 400 })], // an empty state to the initial one, after the setups of `also`
|
|
20
|
+
adapters: [
|
|
21
|
+
[digest.mail, sequence([fail("smtp: down"), after(100, ok({ id: "m1" }))])], // the first job fails, the next lands 100 ms later
|
|
22
|
+
],
|
|
23
|
+
scenarios: [
|
|
24
|
+
{
|
|
25
|
+
name: "a digest is mailed after one failure",
|
|
26
|
+
intent: "the first send fails, the retry lands, and the digest counts one sent",
|
|
27
|
+
script: ({ fire, ticks, check }, p) => [
|
|
28
|
+
check("the page from the setup is fetched", (t) => t.read("digest", {}).pages, 1),
|
|
29
|
+
fire("digest-send", { to: p.to as string }),
|
|
30
|
+
ticks(3, 1_000),
|
|
31
|
+
check("one sent", (t) => t.read("digest", {}).sent, 1),
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## The spec
|
|
39
|
+
|
|
40
|
+
- `spec(system, { summary, also, adapters, setup, params, now, seed, tick, scenarios })`: a definition, not a run, the module's default export. Every field is optional; `spec(pond)` is a spec with no scenarios yet. The spec is named like the system's directory; its baba is the system and its reads, so `t.read`, `fire` and a handle in a check type-check against exactly those systems.
|
|
41
|
+
- `spec(baba, { … })` in `.baba/spec.ts`: the whole baba's spec, named `baba`, for what only the systems together show.
|
|
42
|
+
- `also` takes the specs of systems this one reads: their adapters, setups and params come along, so their outside is played here too. The spec's own adapters win for the same effect; its setup runs after theirs; its params lay over theirs.
|
|
43
|
+
- `tick` is the baba's own by default; `null` leaves the rounds to the script.
|
|
44
|
+
|
|
45
|
+
## The outside
|
|
46
|
+
|
|
47
|
+
An adapter plays an effect, by the effect itself: `[pond.weather, after(300, ok({ temp: 20 }))]`.
|
|
48
|
+
|
|
49
|
+
- Requests: `ok(value)`, `fail(error)`, `wait()`, `after(ms, a)`, `sequence([…])` (successive jobs in turn, the last for every job after), `match(where, a, otherwise)`, `model(initial, (state, args) => [state, answer])` for an outside with memory, `custom((args, ctx) => answer)`, `recorded(answer)`.
|
|
50
|
+
- Sources: `listing(items)` (or a function of the args and the adapter's `ctx.state`) with optional `events` and `relist`, and `events([{ at, event }])` for a stream.
|
|
51
|
+
- An adapter's state is the spec's: seeded with it, reset with it, shown in the Maker. Keep state in `model` or `ctx.state`, never in a closure.
|
|
52
|
+
- An effect the spec plays no adapter for is **quiet**: its job is never started, so it waits, pending, like an outside that has not answered yet. A script answers it by hand: `done(effect, result, where?)`, `failed(effect, error, where?)`, `list(source, items)`, `watch(source, events)`; `where` picks the jobs by their args, or `{ entity }`; every job of the effect without it. A quiet effect is nothing faked: a quiet mirror keeps what was listed by hand. Waiting jobs are in `t.jobs` (`t.pending` stays empty in a spec). A verdict names the quiet effects a scenario's jobs asked of: what an adapter would have to play.
|
|
53
|
+
|
|
54
|
+
## Scenarios
|
|
55
|
+
|
|
56
|
+
A scenario is `{ name, intent, params?, script }`, run from the spec's setup. `script(words, params)` answers the lines, in the testbed's words:
|
|
57
|
+
|
|
58
|
+
- `given(label, parts | (t) => …)`: a precondition the contract cannot express; parts by component name spawn one entity, `given("one fish", { fish: { x: 100, y: 100, hunger: 0 } })`.
|
|
59
|
+
- `fire(action, args)`: an action, now; `answer(0, "entity")` is an earlier fire's answer in a later fire's args.
|
|
60
|
+
- `tick({ after | at })`, `ticks(count, every)`: rounds on the spec's clock.
|
|
61
|
+
- `done`, `failed`, `list`, `watch`: the outside answering, landing at the next tick.
|
|
62
|
+
- `check(label, (t) => read, want?)`: deep-equal to `want`, or true. A failed check records and the run goes on; a step that throws stops it.
|
|
63
|
+
|
|
64
|
+
Everything runs on the spec's own clock: an adapter's delay, a retry, a schedule and the baba's tick are due moments on it. Headless the clock jumps; live it follows real time at the speed chosen. The state lands the same either way; `w.random()` is seeded, so a run repeats.
|
|
65
|
+
|
|
66
|
+
A scenario in a file of its own joins the spec beside it: `systems/NAME/scenarios/NAME.ts` (or `.baba/scenarios/` for the whole baba), `export default scenario({ name, intent, script })` from `babavoss/spec`. Its testbed is untyped; one written in the spec is typed by its spec. The Maker saves a take there (`spec-save`).
|
|
67
|
+
|
|
68
|
+
## Running
|
|
69
|
+
|
|
70
|
+
- `.baba/spec.test.ts` is the one runner, and `voss baba check` fails without it: `import { proveAll } from "babavoss/test"; await proveAll(import.meta.dir);`. It finds every `systems/*/spec.ts`, `.baba/spec.ts` and the saved scenarios; nothing is listed by hand.
|
|
71
|
+
- Each scenario is one bun test named `SPEC: SCENARIO`. From `.baba/`: `bun test ./spec.test.ts` runs every spec, `bun test ./spec.test.ts -t "^pond: "` one spec, `-t "^pond: a fish goes for the food$"` one scenario.
|
|
72
|
+
- Verdicts land in `.baba/.voss/spec/SPEC/verdicts.json`, written as the run goes: a scenario a finished run did not reach is unknown, never green.
|
|
73
|
+
- `voss baba spec` lists every spec with its verdicts and whether code or spec changed since; `voss baba spec-run '{"spec":"pond"}'` reruns one headless (no spec: every one).
|
|
74
|
+
- A system's other `*.test.ts` beside it (a pure helper), or an app's in `.baba/apps/` (its markup), is plain bun test; the system's behaviour is proved in its spec.
|
|
75
|
+
|
|
76
|
+
## The Maker
|
|
77
|
+
|
|
78
|
+
The Maker app shows the same specs live: its page imports them all, `import specs from "babavoss:specs"`, and the baba's apps, `<Maker specs={specs} apps={apps} />`. A spec open is drawn by one of those apps, one whose `systems` the spec holds, or by the desktop for a spec that has it; the person picks the app, and the pick is kept per spec (see `compose-an-interface`). A person opens a spec, plays, and your edits reach it live; the state carries across them, reseeding only when the fit dropped something the new code no longer declares. `voss baba spec-read '{"spec":"pond","query":"take"}'` then `voss baba follow N` answers what they did, noted and expected; `voss baba spec-step '{"spec":"pond","steps":[{"fire":"feed","args":{"x":1}},{"tick":300},{"read":"pond"}]}'` acts in that very spec and answers what changed. A run launched on the spec as seeded is canonical; after a hand moved it, a failed check is a warning. The truth is always the run from the setup.
|
|
79
|
+
|
|
80
|
+
## The loop
|
|
81
|
+
|
|
82
|
+
1. **Intent.** The person's sentence, as they said it: the spec's `summary`, later a scenario's `intent`.
|
|
83
|
+
2. **Spec.** `systems/NAME/spec.ts`: the setup, adapters for what the sentence needs, `also` for the systems it reads; the rest stays quiet.
|
|
84
|
+
3. **Look.** The person opens it in the Maker and plays; read their take.
|
|
85
|
+
4. **Scenario.** The sentence as a script, in the spec or saved beside it; `bun test` or `spec-run`, and read the verdicts and the quiet effects.
|
|
86
|
+
|
|
87
|
+
## Never
|
|
88
|
+
|
|
89
|
+
- Reach the real outside from a spec: an adapter or a script answers every effect.
|
|
90
|
+
- Keep an adapter's state in a closure: use `model` or `ctx.state`, so it resets with the spec.
|
|
91
|
+
- Put a spec anywhere but beside its system, or list specs by hand: the convention finds them.
|
|
92
|
+
- Write under `.baba/.voss/spec/`: the runner does.
|
|
93
|
+
- Reset the person's state for them: say what changed and let them.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const prompt = { kind: "skill", name: "systems-together", description: "Make babavoss systems use each other without calling, messaging or modelling one another: reads, asks (accept), answers, chains on one entity, workflows. Read before making one system depend on another." };
|
|
2
|
+
|
|
3
|
+
# Systems together
|
|
4
|
+
|
|
5
|
+
Systems share one state and nothing else. There are no calls, no events, no messages between systems. A system names the systems it reads in `reads`; it reads their components there, and uses one by putting an **ask** on an entity; the provider answers it beside the ask. The provider never knows who asked.
|
|
6
|
+
|
|
7
|
+
## The provider: say what you do for others
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { system, effect, accept, s, not } from "babavoss";
|
|
11
|
+
|
|
12
|
+
export default system({
|
|
13
|
+
name: "images",
|
|
14
|
+
model: {
|
|
15
|
+
ask: accept({ prompt: s.string(), size: s.optional(s.string()) }), // a reader may add it, once; only images replaces or removes it
|
|
16
|
+
image: { url: s.string() }, // the answer
|
|
17
|
+
failed: { error: s.string() },
|
|
18
|
+
},
|
|
19
|
+
effects: { generate: effect({ args: s.object({ prompt: s.string() }), result: s.object({ url: s.string() }), run: async ({ prompt }, ctx) => { /* ctx.http.fetch(...) */ } }) },
|
|
20
|
+
}, (images, p) => [
|
|
21
|
+
// Asked and not yet answered: a job. The answer lands beside the ask, and the ask goes.
|
|
22
|
+
p.on(images.generate, {
|
|
23
|
+
for: (w) => w.query(images.ask, not(images.image, images.failed)).map((r) => [r.entity, { prompt: r.ask.prompt }] as const),
|
|
24
|
+
done: (w, e, r) => { w.add(e!, images.image, r); w.remove(e!, images.ask); },
|
|
25
|
+
failed: (w, e, error) => { w.add(e!, images.failed, { error }); w.remove(e!, images.ask); },
|
|
26
|
+
}),
|
|
27
|
+
]);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
A provider is a reconciler: open asks become jobs; answers land beside the ask; the ask is removed when answered. Policy is in `for`: yield one ask at a time to serialize, yield only `approved` ones to wait for a person, yield as many as a quota allows.
|
|
31
|
+
|
|
32
|
+
## The asker: name the provider in `reads`, put the ask on your own entity, read the answer there
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import images from "../images/index.ts";
|
|
36
|
+
|
|
37
|
+
export default system({
|
|
38
|
+
name: "album",
|
|
39
|
+
model: { frame: { prompt: s.string() }, captioned: { url: s.string() } },
|
|
40
|
+
reads: [images], // without this, images' handles do not type-check in album's pieces
|
|
41
|
+
}, (album, p) => [
|
|
42
|
+
p.step("want", (w) => { for (const r of w.query(added(album.frame))) w.add(r.entity, images.ask, { prompt: r.frame.prompt }); }),
|
|
43
|
+
p.step("got", (w) => { for (const r of w.query(album.frame, added(images.image))) w.add(r.entity, album.captioned, { url: r.image.url }); }),
|
|
44
|
+
]);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- The entity is the correlation: no ids, no callbacks. Put the ask on an entity you made, beside your own component.
|
|
48
|
+
- You cannot change or remove an ask once added. To withdraw it, despawn your entity: you made it, so you may, whatever it carries, and every job hanging off it is cancelled.
|
|
49
|
+
- `reads` is your only dependency: you reach the provider's handles on the imported system, `images.ask`, and the state your pieces get reads its components and adds its asks, nothing more of it; a handle of a system you did not name does not type-check. The provider never reads you: two systems never read each other. When the provider needs something of you, the backward need is an ask: it declares an `accept` carrying what it needs, and you add it.
|
|
50
|
+
- Order: systems run in the order the baba lists them in `baba({ systems })`. A later system sees an earlier system's writes this round; an earlier one sees them next round. Design for a one-round lag; never for same-round.
|
|
51
|
+
|
|
52
|
+
## Chains and workflows
|
|
53
|
+
|
|
54
|
+
A pipeline of systems is components arriving on one entity in order. There is no workflow engine: the entity is the workflow, and `state N` shows every step as what it carries.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// the release system, reads: [tests, build, deploy]: three steps, one entity per release
|
|
58
|
+
}, (releases, p) => [
|
|
59
|
+
p.step("test", (w) => { for (const r of w.query(added(releases.release))) w.add(r.entity, tests.suite, { name: "all" }); }),
|
|
60
|
+
p.step("pack", (w) => { for (const r of w.query(releases.release, added(tests.outcome))) if (r.outcome.failed === 0) w.add(r.entity, build.bundle, { entry: "src/index.ts" }); }),
|
|
61
|
+
p.step("launch", (w) => { for (const r of w.query(releases.release, added(build.artifact))) w.add(r.entity, deploy.ship, { path: r.artifact.path }); }),
|
|
62
|
+
]);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Branching is a condition in a step, parallel steps are two asks in one round and a join is a query for both answers, cancellation is despawning the entity. A plan composed at runtime, by an agent, is the same shape as data: a `plan` component with steps, and a runner system that adds the next ask when the previous answer lands.
|
|
66
|
+
|
|
67
|
+
## Do not
|
|
68
|
+
|
|
69
|
+
- Do not model another system inside yours and sync it with effects: its data is already in the state, name it in `reads` and query it.
|
|
70
|
+
- Do not write another system's components to ask for something: declare an `accept` in the provider's `model`, or put your own component on the shared entity and let the provider react to it, if the provider agreed to: it names you in its `reads`.
|
|
71
|
+
- Do not fire an action from inside a round: an action is a transaction of the outside; inside, write the state the action would have written.
|
|
72
|
+
- Do not mirror another system of the same baba. Mirror another baba: a source over its query through `ctx.http`.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
export const prompt = { kind: "skill", name: "write-a-system", description: "Write or change a babavoss system: declare its model, effects and reads, and make its pieces in one expression (steps in the order they run, effect bindings, actions and queries with their contract beside the body). Read before touching any file under .baba/systems/." };
|
|
2
|
+
|
|
3
|
+
# Write a system
|
|
4
|
+
|
|
5
|
+
A system is a unit of logic over one shared state. It declares what it owns and supplies pure functions over the state. It keeps nothing of its own between rounds: everything it knows is in the state. A system directory holds `index.ts` (the system) and `spec.ts` (its spec: the system and what it reads, its outside played and its scenarios; see `spec-a-system`), and nothing to look at: a view of it is an app in `.baba/apps/` (see `compose-an-interface`). The spec's intent comes first. `voss baba add-system NAME` scaffolds the system and its spec.
|
|
6
|
+
|
|
7
|
+
A system is one expression: its declaration, data, and the function that makes its pieces from it and answers the list of them.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { system, accept, resource, s, not, added, changed, removed, select, type StateOf, type ReaderOf } from "babavoss";
|
|
11
|
+
import tasks from "../tasks/index.ts";
|
|
12
|
+
|
|
13
|
+
export default system({
|
|
14
|
+
name: "pond",
|
|
15
|
+
model: { // components, asks and resources, together
|
|
16
|
+
fish: { x: s.number(), y: s.number(), hunger: s.number() }, // per entity
|
|
17
|
+
target: { food: s.entity("food") }, // a reference is an entity id
|
|
18
|
+
food: { x: s.number(), y: s.number(), at: s.integer() },
|
|
19
|
+
tagged: s.tag(), // a component with no value
|
|
20
|
+
named: { name: s.key(s.string()) }, // one key field: w.find(pond.named, "nemo")
|
|
21
|
+
meal: accept({ size: s.integer() }), // an ask: a system that reads pond may add it
|
|
22
|
+
fed: resource(s.integer(), 0), // once per baba, always present
|
|
23
|
+
},
|
|
24
|
+
effects: { /* the outside, as jobs: see reach-outside */ },
|
|
25
|
+
reads: [tasks], // the systems this one reads, or asks of
|
|
26
|
+
}, (pond, p) => {
|
|
27
|
+
// Helpers and selects first, above the list: they close over the system's handles.
|
|
28
|
+
const hungry = select(pond.fish, not(pond.target)).where((r) => r.fish.hunger > 10);
|
|
29
|
+
return [
|
|
30
|
+
p.step("swim", (w) => { /* every round, in list order */ }),
|
|
31
|
+
p.on(pond.fetch, { /* where an effect runs, how its answers land: see reach-outside */ }),
|
|
32
|
+
p.action("feed", { /* the contract, then run: (w, args) => … */ }),
|
|
33
|
+
p.query("pond", { /* the contract, then read: (w, args) => … */ }),
|
|
34
|
+
];
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The first parameter is the system itself, named like it: a handle is always `system.handle`, `pond.fish`, everywhere, inside its pieces and out, in apps, specs and tests. A read system's handles come from the imported system, `tasks.task`, and type-check in a piece only because it is in `reads`. No `$`, no destructuring needed; a local `const { fish } = pond;` inside a long step is allowed, sparingly. `p` makes the pieces; everything the system does is a piece made with `p` and returned in the one list:
|
|
39
|
+
|
|
40
|
+
- A piece made and not listed throws at load, and so does one listed twice, two steps of one name, two actions or two queries of one name, or another system's piece.
|
|
41
|
+
- A piece made only when a condition holds: `flag && p.step("trace", …)`; `false`, `null` and `undefined` in the list are skipped.
|
|
42
|
+
- `export default system(...)` when the module exports nothing else; `const pond = system(...)` and `export default pond` when it also exports helpers or constants that need the system.
|
|
43
|
+
- A system object holds its handles by name and nothing else: no prototype, and it never stringifies. Never `String(pond)` or `` `${pond}` ``; its name is `nameOf(pond)`. The one name not allowed as a handle is `entity`.
|
|
44
|
+
|
|
45
|
+
## Objects first
|
|
46
|
+
|
|
47
|
+
Before steps and before any app, decide what the state is made of. An **object** is a component with a key, `s.key(…)`, plus whatever rides on the same entity: the issue is `issue` keyed by number with `thread` beside it; the worktree is `worktree` keyed by path with `work` and `outcome` beside it. A component spawned on its own entity, a window, a run, an opening, is an object known by its entity. Design in this order, OOUX's: the objects, then their relationships, entity fields `s.entity("x")` and what rides together, then their calls to action, the actions that take the object, then their attributes, the fields. The Maker reads all four off the manifest and shows every object with its instances under its **model** lens, so a system is designed and watched before it has an app. A system written for agents alone never needs one.
|
|
48
|
+
|
|
49
|
+
## Rules the state enforces
|
|
50
|
+
|
|
51
|
+
- The state a piece gets accepts only declared handles. It writes what the system owns, and asks of what it reads; it reads what it owns and what it reads; it runs its own effects. Anything else does not type-check, a handle imported directly included. A query reads only.
|
|
52
|
+
- To read another system, name it in `reads`: `reads: [tasks]`, then `w.query(tasks.task)` in a piece. You import the system for its handles, never for its functions.
|
|
53
|
+
- Reads go one way: two systems never read each other. When the one read also needs something back, the backward need is an ask: the reader adds the other's `accept` on an entity it made (see `systems-together`).
|
|
54
|
+
- A value is immutable data. `w.add(e, pond.fish, value)` replaces the whole value; `w.update(e, pond.fish, (f) => ({ ...f, x: 1 }))` makes the new one from the old. Never mutate what a query gave you.
|
|
55
|
+
- A component kept by a source (`mirror`) is written only by the outside, never by your steps. An ask (`accept`) is added by anyone who reads its system, once, and answered by its owner. See `systems-together`.
|
|
56
|
+
- You despawn an entity you made, whatever it carries; any other only if it carries nothing but your components.
|
|
57
|
+
- Names are flat across the baba: two systems cannot both declare `file`. Pick words specific to the system.
|
|
58
|
+
|
|
59
|
+
## The round
|
|
60
|
+
|
|
61
|
+
The runtime moves the state in rounds, each synchronous: the inbox first, where the actions fired and the raw writes run in order; then each system in declared order lands its sources' batches and its effects' answers, runs its steps, and is asked for its jobs; then the jobs start. What a system writes in a round is a function of the state before it and the inbox, nothing else. A round happens when there is a reason, an action, an answer, a batch, a schedule, the baba's `tick`; nothing runs while nothing is due. Values are checked where they enter from outside, action args, raw writes, job results, a state carried into new code; your own writes are trusted, and an audit at the round's end checks what changed.
|
|
62
|
+
|
|
63
|
+
## Steps
|
|
64
|
+
|
|
65
|
+
Pure, synchronous, run every round in the order they appear in the list. Order is the design: put `seek` above `swim` above `eat`, and say why in a comment above each.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
}, (pond, p) => {
|
|
69
|
+
const hungry = select(pond.fish, not(pond.target)).where((r) => r.fish.hunger > 10); // once, above the list
|
|
70
|
+
return [
|
|
71
|
+
// Hungry fish go for the first food. Seek before swim: a fish turns toward food before it moves.
|
|
72
|
+
p.step("seek", (w) => {
|
|
73
|
+
const f = w.query(pond.food)[0];
|
|
74
|
+
if (f) w.each(hungry, (e) => w.add(e, pond.target, { food: f.entity }));
|
|
75
|
+
}),
|
|
76
|
+
// Every round, for everything: time passes for all.
|
|
77
|
+
p.step("swim", (w) => {
|
|
78
|
+
const dt = w.elapsed / 1000;
|
|
79
|
+
w.each([pond.fish], (e, f) => w.add(e, pond.fish, { ...f, x: f.x + dt }));
|
|
80
|
+
}),
|
|
81
|
+
// Only for what changed since this system last ran: nothing on a quiet round.
|
|
82
|
+
p.step("eat", (w) => w.each([changed(pond.fish), pond.target], (e, f, t) => { /* ... */ })),
|
|
83
|
+
p.step("react", (w) => { for (const r of w.query(added(pond.food))) w.add(r.entity, pond.tagged); }),
|
|
84
|
+
p.step("clean", (w) => { for (const r of w.query(removed(pond.food))) { /* r.food is the last value it had */ } }),
|
|
85
|
+
];
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- Each step is `p.step("name", (w) => …)`, with a comment above it saying what it does. Its name is how logs name it: "pond swim threw".
|
|
90
|
+
- `w.query(a, not(b))` answers rows `{ entity, a }` in spawn order. `w.each([a, not(b)], (e, a) => …)` visits them without making rows: use it in steps.
|
|
91
|
+
- `added(c)`, `changed(c)`, `removed(c)` measure from the moment this system's steps last ran, the same for every step of it. `changed` includes `added`. A removal includes a despawn.
|
|
92
|
+
- `select(pond.fish, not(pond.target)).where(…)` made once, above the list, keeps its set live; pass it to `w.each` or `w.query`.
|
|
93
|
+
- `w.now` is the round's time (ms since the epoch), `w.elapsed` the time since the last round, `w.round` the round. Do not keep a clock resource.
|
|
94
|
+
- `w.count(c)`, `w.has(e, c)`, `w.get(e, c)`, `w.get(resource)`, `w.set(resource, v)`, `w.update(resource, fn)`, `w.find(c, key)`, `w.spawn(c, v)`, `w.remove(e, c)`, `w.despawn(e)`.
|
|
95
|
+
- Each step is guarded on its own: a throw is logged, the next step runs, the state is not undone. Steps throw only for bugs.
|
|
96
|
+
- `w.random()` is the baba's own seeded generator: use it, never `Math.random`, so a round can be reproduced and a scenario repeats.
|
|
97
|
+
|
|
98
|
+
## Helpers
|
|
99
|
+
|
|
100
|
+
A function shared by pieces takes the state as the system sees it: `StateOf<typeof pond>` when steps, bindings or actions share it, `ReaderOf<typeof pond>` when queries share it too. Write it above the list, where `pond` is in scope.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
}, (pond, p) => {
|
|
104
|
+
const spawnFish = (w: StateOf<typeof pond>, x: number) => w.spawn(pond.fish, { x, y: 0, hunger: 0 });
|
|
105
|
+
const feedOne = (w: StateOf<typeof pond>) => w.update(pond.fed, (n) => n + 1);
|
|
106
|
+
const hungriest = (w: ReaderOf<typeof pond>) => w.query(pond.fish).sort((a, b) => b.fish.hunger - a.fish.hunger)[0];
|
|
107
|
+
return [ /* … */ ];
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A helper typed by `StateOf` refuses what the system may not touch, as a step would. `Act<typeof pond>` and `Read<typeof pond>` are its actions' and queries' args and results, as types; `HandlesOf<typeof pond>` its handles.
|
|
112
|
+
|
|
113
|
+
## A big system: a declaration and parts
|
|
114
|
+
|
|
115
|
+
When one file grows too long, split it so each helper sits beside the pieces that use it. `decl.ts` holds the declaration, checked at import; a part is pieces typed by it, in a file of its own; `index.ts` lists them all, the part's where its steps run.
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
// systems/trunk/decl.ts
|
|
119
|
+
import { declare, s } from "babavoss";
|
|
120
|
+
export default declare({ name: "trunk", model: { stage: { name: s.key(s.string()), up: s.boolean() } }, effects: { /* … */ }, reads: [] });
|
|
121
|
+
|
|
122
|
+
// systems/trunk/stages.ts
|
|
123
|
+
import { part, type StateOf } from "babavoss";
|
|
124
|
+
import decl from "./decl.ts";
|
|
125
|
+
const isUp = (w: StateOf<typeof decl>, name: string) => { /* … */ }; // a declaration types a helper too
|
|
126
|
+
export const stages = part(decl, (trunk, p) => [
|
|
127
|
+
p.step("stage", (w) => { /* … */ }),
|
|
128
|
+
p.action("stage-start", { /* … */ }),
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
// systems/trunk/index.ts
|
|
132
|
+
import { system } from "babavoss";
|
|
133
|
+
import decl from "./decl.ts";
|
|
134
|
+
import { stages } from "./stages.ts";
|
|
135
|
+
export default system(decl, (trunk, p) => [
|
|
136
|
+
...stages(trunk, p),
|
|
137
|
+
p.query("trunk", { /* … */ }),
|
|
138
|
+
]);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Split only where it reads better; a system of one file is the default. Keep the imports one way: `decl.ts` may import the helpers its effects run, so a part cannot live in that helpers' file, or the two import each other. Put a part in a file of its own, and helpers that pieces of two parts share in `index.ts` or a third file.
|
|
142
|
+
|
|
143
|
+
## Actions and queries: the contract beside the body
|
|
144
|
+
|
|
145
|
+
An action is `p.action("name", { … })`, a query `p.query("name", { … })`: the public name first, then the contract, summary, args, positional and result, then the body, `run` for an action, `read` for a query, typed by it: an arg the contract does not have, or an answer its `result` refuses, is a type error. An action with no args omits `args` and runs `(w) => …`; an action without a `result` answers nothing and need not return. An action runs inside the round as this system, all-or-nothing: a throw or a bad result undoes every write it made. A query reads between rounds and changes nothing. Both are flat and unique across the baba and reach the CLI, MCP, the GUI and the apps unchanged.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
p.action("feed", {
|
|
149
|
+
summary: "drop food into the pond", // one line, imperative
|
|
150
|
+
args: { x: s.optional(s.number()), y: s.optional(s.number()) },
|
|
151
|
+
positional: ["x"], // for people at the shell
|
|
152
|
+
result: s.object({ entity: s.entity("food") }), // an entity made goes back as its id
|
|
153
|
+
run: (w, { x, y }) => ({ entity: w.spawn(pond.food, { x: x ?? 0, y: y ?? 0, at: w.now }) }),
|
|
154
|
+
}),
|
|
155
|
+
p.query("pond", {
|
|
156
|
+
summary: "every fish",
|
|
157
|
+
result: s.array(s.object({ entity: s.entity("fish"), x: s.number() })),
|
|
158
|
+
read: (w) => w.query(pond.fish).map((r) => ({ entity: r.entity, x: r.fish.x })),
|
|
159
|
+
}),
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- Return data, not conventions: the caller follows an entity with `state` or `follow`.
|
|
163
|
+
- An arg `s.entity("food")` must carry `food`, and a caller may give the component's key in place of the id. A result's `s.entity()` is how an entity you made goes back.
|
|
164
|
+
- A fire may carry an idempotency key (`--key K` at the shell, `key` by MCP): the baba remembers the answer for a day, so a retry answers the same without running again. Make actions safe to repeat anyway.
|
|
165
|
+
- `w.transaction(fn)` gives any code what an action has: a throw undoes every write inside it.
|
|
166
|
+
- Validation is at the boundary: args and results are checked; your writes inside are trusted, so write what the schema says.
|
|
167
|
+
- Reserved names, not for actions or queries: start, stop, status, state, follow, run, raw, secret, help, mcp, check, install, add-system, serve, promptware, init, babas, service, gui.
|
|
168
|
+
|
|
169
|
+
## The baba
|
|
170
|
+
|
|
171
|
+
`.baba/index.ts` lists every system, in the order their steps run: `baba({ systems: [tasks, pond], apps, promptware, harness, tick })`. A system that reads another goes after it, so it sees this round's writes.
|
|
172
|
+
|
|
173
|
+
## Schemas
|
|
174
|
+
|
|
175
|
+
`s.string() number() integer() boolean() literal(v) enum([...]) tag() entity(component?) key(inner) secret() unknown() void() array(inner) nullable(inner) optional(inner) object({...})`. A bare object in `model` is `s.object`. `s.key` marks the one key field; `s.entity("food")` says which component the id carries; `s.secret()` is redacted from every snapshot that leaves the runtime.
|
|
176
|
+
|
|
177
|
+
## Checklist before you are done
|
|
178
|
+
|
|
179
|
+
1. Every component and resource the system writes is in its `model`; every other system it reads or asks of is in its `reads`, and none of those reads it back.
|
|
180
|
+
2. Steps are ordered on purpose; the hot ones use `w.each`; the reactive ones use change terms.
|
|
181
|
+
3. Every piece is made with this system's `p` and listed once; actions answer data; queries change nothing; names are flat and not reserved.
|
|
182
|
+
4. The baba's promptware says in a line what the system is for (see `write-promptware`); its `spec.ts` beside it proves it, a scenario for each thing it must do (see `spec-a-system`); an app in `.baba/apps/` that names it in `systems` shows it, live and in the Maker (see `compose-an-interface`).
|
|
183
|
+
5. `bun test` (the runner `.baba/spec.test.ts` proves every spec) and the type-check are green; the running baba reloads on save and `voss baba` shows the new contract.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export const prompt = { kind: "skill", name: "write-promptware",
|
|
2
|
+
description: "Write or change what a baba tells agents: its context, skills and hooks, as code under .baba/promptware/. Use before writing any promptware, in any baba, including skills like this one.",
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
# Write promptware
|
|
6
|
+
|
|
7
|
+
A promptware file an agent can act on at first read: one trigger, steps in order, one example, claims it can check, rules it cannot break.
|
|
8
|
+
|
|
9
|
+
## When
|
|
10
|
+
|
|
11
|
+
- A baba gets a capability and agents must know how to use it.
|
|
12
|
+
- An agent did something wrong that a sentence would have prevented.
|
|
13
|
+
- The owner has a taste about how a thing is done, or a convention is repeated in chat.
|
|
14
|
+
|
|
15
|
+
## Do
|
|
16
|
+
|
|
17
|
+
1. Pick the kind. `context`: the baba's section of the context file, read by every session; what the project is, how it is worked on, its rules; one per baba, in `promptware/index.ts`. `skill`: a file read when its description fits; how to do one thing; one `.mdx` per skill beside it. `hook`: a harness moment handed to an action; what the harness may not do.
|
|
18
|
+
2. Write the description as the trigger: `Use when …`, the situations, the files, the words an agent would be thinking. The description decides whether the skill is read; the body decides whether it works.
|
|
19
|
+
3. Write Markdown directly in an MDX file. Export a `prompt` metadata object with `kind: "skill"`, name, description, and optional uses/eval; for context use `kind: "context"`. An `eval` is the owner's golden interpretation, run on demand from the Trials app to score readers of the text; it gates nothing. Changing a skill, leave its eval alone unless the owner asks. Import contract components and embed them as JSX. MDX expressions are JavaScript; keep typed helpers in imported .ts files. Existing TSX and `md` templates remain supported.
|
|
20
|
+
4. Reference the contract, never restate it: `<Call of="feed" />` for a call with its example and summary, `<Calls of="pond" />` for a system's calls, `<Schema of>` for args, `<Usage of>` for the shell's spelling, `<See skills={[…]} />` for the skills to read next. A name that does not exist refuses to load.
|
|
21
|
+
5. List the file in `promptware/index.ts`, which `baba({ systems, promptware, harness })` declares; `harness` says which harnesses get the files and Claude Code's permissions.
|
|
22
|
+
6. Read the output: `CLAUDE.md` and `.claude/skills/NAME/SKILL.md` after the sync. Cut every sentence that does not change what an agent does.
|
|
23
|
+
|
|
24
|
+
## Example
|
|
25
|
+
|
|
26
|
+
```mdx
|
|
27
|
+
import { Call, See } from "babavoss/prompt";
|
|
28
|
+
|
|
29
|
+
export const prompt = {
|
|
30
|
+
kind: "skill",
|
|
31
|
+
name: "feed-the-pond",
|
|
32
|
+
description: "Use when asked to feed, stock or watch the fish.",
|
|
33
|
+
uses: ["feed", "pond"],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
Feed the fish, then inspect the pond.
|
|
37
|
+
|
|
38
|
+
## Do
|
|
39
|
+
|
|
40
|
+
<Call of="feed" />
|
|
41
|
+
|
|
42
|
+
Follow the returned entity, or read <Call of="pond" />.
|
|
43
|
+
|
|
44
|
+
## Never
|
|
45
|
+
|
|
46
|
+
Do not add or remove fish with raw writes.
|
|
47
|
+
|
|
48
|
+
<See skills={["write-a-system"]} />
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// .baba/promptware/index.ts
|
|
53
|
+
import { context, hook, md } from "babavoss/prompt";
|
|
54
|
+
import feedThePond from "./feed-the-pond.mdx";
|
|
55
|
+
|
|
56
|
+
export default [
|
|
57
|
+
context(md\`
|
|
58
|
+
The demo: an example baba. It shows what a baba is; it is not a product.
|
|
59
|
+
|
|
60
|
+
## Rules
|
|
61
|
+
|
|
62
|
+
- Keep each system small and proved by its scenarios.
|
|
63
|
+
\`),
|
|
64
|
+
feedThePond,
|
|
65
|
+
hook({ on: "PreToolUse", match: "Bash", action: "guard-raw" }),
|
|
66
|
+
];
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Check
|
|
70
|
+
|
|
71
|
+
- The description starts with `Use when` and names a situation, not a topic.
|
|
72
|
+
- Every step names the exact thing to type, open or call.
|
|
73
|
+
- The example runs as written; nothing in it is elided.
|
|
74
|
+
- No sentence restates the contract, the README or another skill: it references them.
|
|
75
|
+
- The rendered file is shorter than the draft.
|
|
76
|
+
|
|
77
|
+
## Never
|
|
78
|
+
|
|
79
|
+
- Read the state in promptware: it is static, rendered from the manifest once per generation.
|
|
80
|
+
- Explain why in a step; put reasons in the context if they are needed at all.
|
|
81
|
+
- Write `simply`, `just`, `easily`, `should`, or a synonym for a term that has one name: system, app, spec, step, state, round, action, query, effect, source, mirror, context, skill, hook, baba, voss.
|
|
82
|
+
- Edit `CLAUDE.md`, `AGENTS.md` or a `SKILL.md`: they are outputs.
|
|
83
|
+
|
|
84
|
+
## Hooks
|
|
85
|
+
|
|
86
|
+
A hook hands a harness moment to an action: `hook({ on, match?, action, strict? })` with `on` one of PreToolUse, PostToolUse, UserPromptSubmit, Stop, SubagentStop, SessionStart, PreCompact, Notification, and `match` a tool-name regex for the two tool events. The action takes `hookArgs(EVENT)` and answers `hookResult()`: `{ decision: "block", reason }` stops the tool call and the agent reads the reason; `{ context }` is told to the agent; `{}` lets it through. Voss compiles it into `.claude/settings.json` as `voss baba ACTION @- --hook EVENT`; a baba that is down lets through with a note, `strict: true` blocks. Prove a hook with a scenario: `fire` the action with an event, `check` the decision.
|
|
87
|
+
|
|
88
|
+
## Voice
|
|
89
|
+
|
|
90
|
+
Imperative, present, one fact per sentence, the name of the thing instead of a pronoun. Code first, then the sentence it needs. Numbers, paths and names exact. In MDX, use ordinary Markdown backticks and fenced code. Literal braces and angle brackets belong in code spans or must be escaped outside code.
|