babavoss 0.12.6 → 0.13.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 (62) hide show
  1. package/gui/babavoss-web.js +4 -1
  2. package/gui/{chunk-9gmmm54v.js → chunk-qgzmajan.js} +619 -574
  3. package/gui/gui.js +1538 -277
  4. package/gui/theme.css +2007 -1128
  5. package/index.ts +1 -1
  6. package/package.json +5 -5
  7. package/src/{promptware → agents}/compile.ts +23 -31
  8. package/src/{promptware → agents}/define.ts +2 -2
  9. package/src/{promptware → agents}/disk.ts +7 -4
  10. package/src/{promptware → agents}/sync.ts +2 -2
  11. package/src/{promptware → agents}/system.ts +17 -17
  12. package/src/baba/check.ts +3 -3
  13. package/src/baba/config.ts +5 -2
  14. package/src/baba/init.ts +13 -18
  15. package/src/baba/node.ts +2 -2
  16. package/src/baba/project.ts +2 -2
  17. package/src/baba/requirements.ts +39 -0
  18. package/src/baba/setup.ts +30 -0
  19. package/src/baba/worker.ts +13 -5
  20. package/src/build/mdx.ts +4 -4
  21. package/src/build/project.ts +6 -1
  22. package/src/build/views.ts +3 -3
  23. package/src/desktop/desktop.css +35 -39
  24. package/src/desktop/index.ts +9 -7
  25. package/src/desktop/keys.ts +153 -0
  26. package/src/desktop/view.tsx +174 -130
  27. package/src/door/core.ts +2 -2
  28. package/src/ecs/baba.ts +27 -17
  29. package/src/generated/build.ts +1 -1
  30. package/src/gui/gui.tsx +21 -31
  31. package/src/gui/index.ts +6 -2
  32. package/src/gui/{promptware.tsx → instructions.tsx} +12 -12
  33. package/src/gui/levels.tsx +241 -59
  34. package/src/gui/lockup.ts +11 -0
  35. package/src/gui/settings.tsx +241 -0
  36. package/src/gui/setup.tsx +77 -0
  37. package/src/gui/theme.css +205 -58
  38. package/src/gui/theme.ts +9 -5
  39. package/src/gui/voss-settings.ts +48 -0
  40. package/src/gui/wizard.tsx +17 -40
  41. package/src/guide/add-a-desktop.mdx +7 -7
  42. package/src/guide/index.ts +4 -4
  43. package/src/guide/write-a-system.mdx +3 -3
  44. package/src/guide/{write-promptware.mdx → write-agentic-instructions.mdx} +17 -24
  45. package/src/http/server.ts +140 -17
  46. package/src/{prompt → instructions}/evals.ts +1 -1
  47. package/src/{prompt → instructions}/index.ts +9 -8
  48. package/src/{prompt → instructions}/jsx-runtime.ts +3 -3
  49. package/src/mcp/main.ts +3 -3
  50. package/src/mcp/tools.ts +4 -4
  51. package/src/runtime/harness.ts +2 -2
  52. package/src/server/edge.ts +31 -3
  53. package/src/server/main.ts +2 -1
  54. package/src/server/messages.ts +47 -7
  55. package/src/shell/run.ts +18 -18
  56. package/src/spec/index.ts +1 -1
  57. package/src/web/core.tsx +2 -2
  58. package/src/web/index.tsx +1 -1
  59. package/src/desktop/bob.ts +0 -76
  60. /package/src/{promptware → agents}/markdown.d.ts +0 -0
  61. /package/src/{prompt → instructions}/jsx-dev-runtime.ts +0 -0
  62. /package/src/{prompt → instructions}/mdx.d.ts +0 -0
@@ -2,9 +2,9 @@ export const prompt = { kind: "skill", name: "add-a-desktop", description: "Give
2
2
 
3
3
  # Add a desktop
4
4
 
5
- Every baba runs voss's shell: the desktop system, which voss adds to every baba as it adds `maker` and `promptware`, over the apps the baba declares in `baba({ apps })`. The interface gives each app its component; the page is voss's. Three nouns and one verb: a **app** is a kind the baba registers; a **window** is one instance of it in a session, at a path; the **session** shows one window, or nothing. The verb is `desktop-open`: it brings the window at that path to the front, else the app's current window, else makes one; asked for a `fresh` window, an app declared `many` gets another, any other app gives the one it has. The launcher, a link in an app, the page's address, the CLI and another system's ask all go through it.
5
+ Every baba runs voss's shell: the desktop system, which voss adds to every baba as it adds `maker` and `agents`, over the apps the baba declares in `baba({ apps })`. The interface gives each app its component; the page is voss's. Three nouns and one verb: a **app** is a kind the baba registers; a **window** is one instance of it in a session, at a path; the **session** shows one window, or nothing. The verb is `desktop-open`: it brings the window at that path to the front, else the app's current window, else makes one; asked for a `fresh` window, an app declared `many` gets another, any other app gives the one it has. The launcher, a link in an app, the page's address, the CLI and another system's ask all go through it.
6
6
 
7
- The page is a bar over one app. At the bar's far left the mark, the two eyes, opens the launcher; then a pill per open window, its icon at the left and the one in front filled; pointing at a pill turns its icon into the × that closes it, and a middle click closes it too, as it closes a browser tab; closing the one in front brings the pill to its left forward, else the one to its right. The launcher is the page's, not the state's: it shows while its search has focus, and while nothing is in front, since there is nothing else to show. The mark or Cmd/Ctrl K gives the search focus; a click on the launcher's empty space keeps it; choosing a row, Escape or the mark again takes it, and the launcher goes. It covers the page and changes nothing beneath: the bar's row becomes the search, and below it one column of rows, a palette: the icon, beginning where the search begins, then the label; what is open first, then every app under its group. One dot under the mark says which row is chosen, by the keys or the pointer, and glides between them. Typing ranks both by the words, a word at the start of a name first, and drops the groups. An app's row opens a fresh window of it, or brings the one it has to the front; a window's row brings it to the front, and pointing at it turns its icon into its ×. A browser tab shows one window at a time: a pill on the bar brings an open one to the front, the mark then a row opens another. Below the apps, under Sessions, the tab's other sessions and New session. A page inside a baba is that baba only; the last row, Leave, and the `voss` link before the title on the bar go back to the picker, voss's root page, which has no state: it lists the babas voss knows, one inside another under its parent, and New baba, the wizard that makes a baba in a folder or remembers one already there.
7
+ The page is a bar of tabs over one app, nothing else on it. At the bar's far left the mark, the two eyes and the baba's name, is the launcher's tab, a pill like the others, which never closes: pointing at it turns its eyes into the way out, which leaves the baba for voss's picker and keeps the session to come back to; then a pill per open window, its icon at the left and the one in front filled; pointing at a pill turns its icon into the × that closes it, and a middle click closes it too, as it closes a browser tab; closing the one in front brings the pill to its left forward, else the one to its right. The launcher is the page's, not the state's, and shows two ways. The mark chooses the launcher's tab, a place: it takes the app's place, the mark filled, at the baba's own address, `/NAME/`, so Back returns to the app it was chosen over; it is also all there is while nothing is in front. Cmd/Ctrl P opens it quick, over the app in front, which stays its tab and shows dimmed behind, the address untouched; Escape, Cmd/Ctrl P again or a press around it goes back to the app as it was, focus included. Choosing a row or a pill on the bar leaves either; Escape in the tab goes back to the app it was chosen over; Cmd/Ctrl or a middle click on the mark opens the launcher in a new browser tab. Either way it changes nothing beneath, the pills staying on the bar: a palette in the middle of the page, the search, then one column of rows, each the icon then the label, the chosen one filled: every app under its group; what is open is on the bar, not here. Typing ranks them by the words, a word at the start of a name first, and drops the groups. An app's row opens a fresh window of it, or brings the one it has to the front. A browser tab shows one window at a time: a pill on the bar, or Cmd/Ctrl J and K, the tab to the left and to the right, the mark's among them, bring an open one to the front; the launcher opens another. Below the apps, under Sessions, the tab's other sessions and New session. A page inside a baba is that baba only; the launcher's last row, Leave, goes back to the picker, voss's root page, which has no state: the Baba Voss lockup and voss's version over the babas voss knows, one inside another under its parent. A folder is added by its path, typed with completion or browsed, and forgotten with its ×, its files kept, with a moment to undo. voss requires some properties of every baba, for now its name; a folder whose baba lacks one, or that has no baba yet, is listed to be set up and does not run, and opening it opens the setup wizard at `/?setup=FOLDER`, a card of steps, one per property missing, then the one that writes them, making its `.baba` when there is none; then it opens as any other. A folder with a baba that has them all just joins the list.
8
8
 
9
9
  ```ts
10
10
  // .baba/index.ts: the apps, as data, in launcher order
@@ -30,7 +30,7 @@ import { Tasks } from "./apps/tasks.tsx";
30
30
  export default compose<typeof baba>({ apps: { tasks: Tasks } });
31
31
  ```
32
32
 
33
- An app missing from `compose`, or a component for an app the baba does not declare, is a type error. After the baba's apps come voss's own, under Voss: `maker` the specs run live, `state` the entities and resources, `contract` the actions and queries as forms, `promptware` the know-how, `settings` the theme. A baba never declares them; its interface may give one a component of its own to draw it otherwise, `compose<typeof baba>({ apps: { …, promptware: Mine } })`. A baba that declares no apps has one, `view`, which draws its interface whole. A baba whose interface fails to build or load still has its shell: its apps say why, voss's work. The old addresses, `/NAME/run`, `/inspect`, `/proofs`, open the app they became, unless the baba has an app by that name. The old form, `compose({ shell })`, still loads for one release, in the `view` app; a baba cannot declare apps and keep a desktop system of its own.
33
+ An app missing from `compose`, or a component for an app the baba does not declare, is a type error. After the baba's apps come voss's own, under Voss: `maker` the specs run live, `state` the entities and resources, `contract` the actions and queries as forms, `agentic instructions` the know-how, `settings` the theme. A baba never declares them; its interface may give one a component of its own to draw it otherwise, `compose<typeof baba>({ apps: { …, agentic instructions: Mine } })`. A baba that declares no apps has one, `view`, which draws its interface whole. A baba whose interface fails to build or load still has its shell: its apps say why, voss's work. The old addresses, `/NAME/run`, `/inspect`, `/proofs`, open the app they became, unless the baba has an app by that name. The old form, `compose({ shell })`, still loads for one release, in the `view` app; a baba cannot declare apps and keep a desktop system of its own.
34
34
 
35
35
  An app's component receives `AppProps`: `{ session, window, path, route, params, active, memory, navigate, open, setTitle, remember }`. It never names an action: `navigate(path)` moves its own window, `open(app, path)` opens any app by the one rule, `setTitle(title)` names its window after what it shows, the pill's tooltip, `remember(key, value)` keeps a filter, a selection or a fold on its window, in the state, and `memory` reads it back. Windows stay mounted when hidden. A path begins with one `/`.
36
36
 
@@ -56,7 +56,7 @@ An app's `systems` names the systems it shows, so the Maker offers it as a view
56
56
  - `desktopWindow`, one per instance, in the order opened: its session, its app, its path, its title, its `memory`.
57
57
  - `desktopSession`, one per session: the selected window and when it was last touched. A browser tab owns one: on load it takes the address's `?session=KEY`, else the one the tab had (its `sessionStorage`), else enters a new one, which the system names after a tree, `oak`, `ash`, `elm`. A link with `?session=KEY` joins a session, and two tabs on one key move together; the launcher lists the other sessions, to go to one or end it, and New session. A session with nothing open goes after a day untouched. The launcher and what is typed into it are the page's.
58
58
  - A call that names no session reaches the current session, the one last touched: a tab touches its session on load and on focus, so `voss baba desktop-open '{"app":"tasks","path":"/task/7"}'` lands in the tab last used. Name one to reach another.
59
- - `desktopSettings`, one resource: the theme, the same on every page.
59
+ - `desktopSettings`, one resource: the baba's own theme, which its frames in the Maker show; voss's pages wear the person's, from voss's settings.
60
60
  - Its contract, the same at the CLI, by MCP and in the page: `desktop-open`, `desktop-select`, `desktop-close`, `desktop-title`, `desktop-remember`, `desktop-enter`, `desktop-leave`, `desktop-settings` and the query `desktop-state`, which also lists every session. Read them with `voss baba`.
61
61
 
62
62
  ## Opening an app from a system
@@ -91,10 +91,10 @@ The ask takes `{ app, path?, fresh?, session? }`; an unknown app, a bad path or
91
91
 
92
92
  ## Rules
93
93
 
94
- - The address records the window in front, `/NAME/APP/PATH`, with Back and Forward; opening the launcher from the page leaves no entry; pass `address={false}` for a desktop embedded in another page.
94
+ - The address records the window in front, `/NAME/APP/PATH`, with Back and Forward; the launcher's tab is an entry, `/NAME/`, and the quick launcher none; pass `address={false}` for a desktop embedded in another page.
95
95
  - A baba's name is the first segment of its address, so `api`, `assets`, `view` and `ws`, the page's own, are not names a baba can take.
96
- - Cmd/Ctrl K opens the launcher; Cmd/Ctrl W or the front pill's × closes the window in front; Escape clears the search, then leaves the launcher; arrows move down the rows, Enter opens. The theme is `desktop-settings`, with no control in the launcher yet.
97
- - The shell inherits voss's theme tokens; the session's theme applies inside the desktop.
96
+ - Cmd/Ctrl P opens the quick launcher, the mark the launcher's tab; Cmd/Ctrl J and K go to the tab on the left and on the right; the front pill's ×, or a middle click on a pill, closes its window, and Close tab, which has no key until one is given it, since Cmd/Ctrl W closes the browser tab or the app around the page; Escape clears the search, then leaves the launcher; arrows move down the rows, Enter opens. These are defaults, and the keys are the person's, not a baba's: voss keeps them in its own settings, `settings.json` in its root, with the theme every page of every baba wears; `POST /api/settings` with `keys`, `{"launcher": "Mod+Shift+L"}`, remaps one, `Mod` being Cmd on a Mac and Ctrl elsewhere, `null` gives the default back, and a binding two commands would share is refused; the launcher's arrows, Enter and Escape are fixed. voss's Settings app shows them under two levels: the baba's first, Profile at `/`, its name, which renamed moves its address and opens it again there; then voss's, Appearance at `/appearance`, Keyboard at `/keyboard` and About at `/about`, which voss runs, its runtime and the paths it keeps. `desktop-settings` sets a baba's own theme, which only its frames in the Maker show.
97
+ - The shell inherits voss's theme tokens; voss's settings choose light, dark or the system's for every page.
98
98
  - Reload keeps compatible desktop state and drops apps no longer declared; stopping the baba ends it with the rest of the state.
99
99
  - Prove an app's navigation like any system: a scenario that fires `desktop-open` and checks `desktop-state`.
100
100
  - The old spellings, `surfaces` and `SurfaceProps`, still work for one release; `dock` in the options is accepted and ignored.
@@ -1,13 +1,13 @@
1
1
  // voss's own know-how for agents, given to every baba after its own: how to
2
2
  // write, prove and compose systems, how to draw the page as apps and give it
3
- // a desktop, and how to write promptware.
4
- import type { Prompt } from "../prompt/index.ts";
3
+ // a desktop, and how to write agentic instructions.
4
+ import type { Prompt } from "../instructions/index.ts";
5
5
  import writeASystem from "./write-a-system.mdx";
6
6
  import reachOutside from "./reach-outside.mdx";
7
7
  import systemsTogether from "./systems-together.mdx";
8
8
  import composeAnInterface from "./compose-an-interface.mdx";
9
9
  import addADesktop from "./add-a-desktop.mdx";
10
- import writePromptware from "./write-promptware.mdx";
10
+ import writeInstructions from "./write-agentic-instructions.mdx";
11
11
  import specASystem from "./spec-a-system.mdx";
12
12
 
13
- export const guide: readonly Prompt[] = [writeASystem, reachOutside, systemsTogether, specASystem, composeAnInterface, addADesktop, writePromptware];
13
+ export const guide: readonly Prompt[] = [writeASystem, reachOutside, systemsTogether, specASystem, composeAnInterface, addADesktop, writeInstructions];
@@ -164,11 +164,11 @@ p.query("pond", {
164
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
165
  - `w.transaction(fn)` gives any code what an action has: a throw undoes every write inside it.
166
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.
167
+ - Reserved names, not for actions or queries: start, stop, status, state, follow, run, raw, secret, help, mcp, check, install, add-system, serve, agents, init, babas, service, gui.
168
168
 
169
169
  ## The baba
170
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.
171
+ `.baba/index.ts` lists every system, in the order their steps run: `baba({ systems: [tasks, pond], apps, harness, tick })`. A system that reads another goes after it, so it sees this round's writes.
172
172
 
173
173
  ## Schemas
174
174
 
@@ -179,5 +179,5 @@ p.query("pond", {
179
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
180
  2. Steps are ordered on purpose; the hot ones use `w.each`; the reactive ones use change terms.
181
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`).
182
+ 4. The baba's context, an `.mdx` under `.baba/agents/`, says in a line what the system is for (see `write-agentic-instructions`); 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
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.
@@ -1,10 +1,10 @@
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.",
1
+ export const prompt = { kind: "skill", name: "write-agentic-instructions",
2
+ description: "Write or change what a baba tells agents: its context, skills and hooks, each an .mdx file under .baba/agents/. Use before writing any agentic instructions, in any baba, including skills like this one.",
3
3
  };
4
4
 
5
- # Write promptware
5
+ # Write agentic instructions
6
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.
7
+ An instruction file an agent can act on at first read: one trigger, steps in order, one example, claims it can check, rules it cannot break. The agentic instructions of a baba are the `.mdx` files under `.baba/agents/`; voss reads every one of them, in name order, and there is no list to keep.
8
8
 
9
9
  ## When
10
10
 
@@ -14,17 +14,17 @@ A promptware file an agent can act on at first read: one trigger, steps in order
14
14
 
15
15
  ## Do
16
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.
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, `export const prompt = { kind: "context" }`. `skill`: a file read when its description fits; how to do one thing; `export const prompt = { kind: "skill", name, description, uses }`, one `.mdx` per skill. A hook, a harness moment handed to an action, is `hooks: [{ on, match, action }]` on the context's `prompt`.
18
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.
19
+ 3. Write Markdown in the `.mdx` file; `import { Call } from "babavoss/instructions"` for the tags below. Export the `prompt` metadata object with `kind: "skill"`, name, description, and optional uses/eval; for the context, `kind: "context"` with optional hooks/eval. 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
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.
21
+ 5. Save it under `.baba/agents/`: voss finds it there. `baba({ harness })` says which harnesses get the files and Claude Code's permissions.
22
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
23
 
24
24
  ## Example
25
25
 
26
26
  ```mdx
27
- import { Call, See } from "babavoss/prompt";
27
+ import { Call, See } from "babavoss/instructions";
28
28
 
29
29
  export const prompt = {
30
30
  kind: "skill",
@@ -48,22 +48,15 @@ Do not add or remove fish with raw writes.
48
48
  <See skills={["write-a-system"]} />
49
49
  ```
50
50
 
51
- ```ts
52
- // .baba/promptware/index.ts
53
- import { context, hook, md } from "babavoss/prompt";
54
- import feedThePond from "./feed-the-pond.mdx";
51
+ ```mdx
52
+ {/* .baba/agents/context.mdx */}
53
+ export const prompt = { kind: "context", hooks: [{ on: "PreToolUse", match: "Bash", action: "guard-raw" }] };
55
54
 
56
- export default [
57
- context(md\`
58
- The demo: an example baba. It shows what a baba is; it is not a product.
55
+ The demo: an example baba. It shows what a baba is; it is not a product.
59
56
 
60
- ## Rules
57
+ ## Rules
61
58
 
62
- - Keep each system small and proved by its scenarios.
63
- \`),
64
- feedThePond,
65
- hook({ on: "PreToolUse", match: "Bash", action: "guard-raw" }),
66
- ];
59
+ - Keep each system small and proved by its scenarios.
67
60
  ```
68
61
 
69
62
  ## Check
@@ -76,15 +69,15 @@ export default [
76
69
 
77
70
  ## Never
78
71
 
79
- - Read the state in promptware: it is static, rendered from the manifest once per generation.
72
+ - Read the state in agentic instructions: it is static, rendered from the manifest once per generation.
80
73
  - Explain why in a step; put reasons in the context if they are needed at all.
81
74
  - 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`, a `SKILL.md` or `.claude/settings.local.json`: they are outputs. Git keeps none of them: voss writes its lines of the project's `.gitignore`, and a checkout makes its own with `voss baba promptware sync`.
75
+ - Edit `CLAUDE.md`, `AGENTS.md`, a `SKILL.md` or `.claude/settings.local.json`: they are outputs. Git keeps none of them: voss writes its lines of the project's `.gitignore`, and a checkout makes its own with `voss baba agents sync`.
83
76
  - Commit an output. A hand-written `.claude/settings.json` beside the made `settings.local.json` is the place for what the repository itself tells Claude Code, a `SessionStart` hook that runs the sync, say.
84
77
 
85
78
  ## Hooks
86
79
 
87
- 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.local.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.
80
+ A hook hands a harness moment to an action: `{ on, match?, action, strict? }` in the context's `hooks`, 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.local.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.
88
81
 
89
82
  ## Voice
90
83
 
@@ -7,6 +7,10 @@ import { mkdir, writeFile, chmod } from "node:fs/promises";
7
7
  import { dirname } from "node:path";
8
8
  import { readFileSync, unlinkSync } from "node:fs";
9
9
  import { openProject, exists } from "../baba/project.ts";
10
+ import { requirementError, requirements, type RequirementValues } from "../baba/requirements.ts";
11
+ import { setupOf, writeRequirements } from "../baba/setup.ts";
12
+ import { themes } from "../desktop/index.ts";
13
+ import { applyKeyChanges } from "../desktop/keys.ts";
10
14
  import { init } from "../baba/init.ts";
11
15
  import { homedir } from "node:os";
12
16
  import { readdir } from "node:fs/promises";
@@ -15,12 +19,13 @@ import type { BuildOutput } from "../build/builder.ts";
15
19
  import type { Registry } from "../baba/registry.ts";
16
20
  import { page, buildGui } from "../gui/index.ts";
17
21
  import type { Transport } from "../transport/transport.ts";
18
- import type { ViewIndex, Asset, Failure, FromServer, ProjectEntry, ToServer, Refreshed, Browsed } from "../server/messages.ts";
22
+ import type { ViewIndex, Asset, Failure, FromServer, ProjectEntry, PendingEntry, ToServer, Refreshed, Browsed, VossSettings } from "../server/messages.ts";
19
23
  import { connectFor, builderFor, serverSpawn } from "../kernel/children.ts";
20
24
  import { dev, framework } from "../kernel/where.ts";
21
25
  import { takeLock, LockError } from "../kernel/lock.ts";
22
26
  import { thisBuild, buildOf, buildName, tellApart, type Build } from "../kernel/build.ts";
23
- import type { Voss } from "../server/messages.ts";
27
+ import type { About, Voss } from "../server/messages.ts";
28
+ import { agentPath, logPath, unitPath } from "../shell/service.ts";
24
29
  import { stat } from "node:fs/promises";
25
30
 
26
31
  export type ServerTransport = Transport<ToServer, FromServer>;
@@ -101,6 +106,35 @@ function failure(err: unknown): Failure {
101
106
  return { ok: false, code: err instanceof NodeError ? err.code : "failed", message: err instanceof Error ? err.message : String(err) };
102
107
  }
103
108
 
109
+ /** voss's settings, the person's, as ROOT/settings.json keeps them: what does not read, or no longer fits, is left out and the default stands. */
110
+ async function readSettings(path: string, log: (l: string) => void): Promise<VossSettings> {
111
+ const out: VossSettings = { theme: "system" };
112
+ try {
113
+ const f = Bun.file(path);
114
+ if (!(await f.exists())) return out;
115
+ const j = await f.json() as { theme?: unknown; keys?: unknown };
116
+ if (themes.includes(j.theme as never)) out.theme = j.theme as VossSettings["theme"];
117
+ if (j.keys && typeof j.keys === "object") {
118
+ try { const keys = applyKeyChanges(undefined, j.keys as Record<string, string>); if (Object.keys(keys).length) out.keys = keys; }
119
+ catch (err) { log(`${path}: keys left out: ${err instanceof Error ? err.message : String(err)}`); }
120
+ }
121
+ } catch (err) { log(`${path}: ${err instanceof Error ? err.message : String(err)}; the defaults stand`); }
122
+ return out;
123
+ }
124
+
125
+ /** Where this voss is and what it runs on, as the settings' About page shows it. */
126
+ async function aboutOf(root: string): Promise<About> {
127
+ const home = homedir();
128
+ const file = process.platform === "darwin" ? agentPath(home) : process.platform === "linux" ? unitPath(home) : null;
129
+ return {
130
+ started: new Date().toISOString(),
131
+ runtime: { bun: Bun.version, platform: process.platform, arch: process.arch, executable: process.execPath },
132
+ framework, cli: process.argv[1] ?? "",
133
+ root, registry: join(root, "projects.json"), token: join(root, "http-token"),
134
+ service: file && await exists(file) ? { file, log: logPath(home) } : null,
135
+ };
136
+ }
137
+
104
138
  /** Where a root's daemon says it listens: DIR/address, written on listen and removed on stop. */
105
139
  export const addressPath = (rootDir: string) => join(rootDir, "address");
106
140
  export interface Address { hostname: string; port: number; pid: number }
@@ -124,10 +158,14 @@ export async function serve(reg: Registry, hostname: string, port: number, log =
124
158
  }
125
159
 
126
160
  // The registry is the list of what runs: a refresh opens what it gained and retires what it lost, one at a time.
161
+ // A folder that lacks something voss requires is kept and not run, to be set up; one that has it all and fails is kept with its error.
127
162
  let refreshing: Promise<unknown> = Promise.resolve();
163
+ let pending: Omit<PendingEntry, "parent">[] = [];
128
164
  const reread = async (): Promise<Refreshed> => {
129
165
  const dirs = (await reg.list()).map((d) => resolve(d));
130
166
  const errors: Refreshed["errors"] = [];
167
+ // Gathered here and kept whole at the end: a page that asks meanwhile gets the last list, never a half one.
168
+ const waiting: typeof pending = [];
131
169
  for (const [name, n] of nodes) {
132
170
  if (dirs.includes(n.project.dir)) continue;
133
171
  nodes.delete(name);
@@ -137,6 +175,12 @@ export async function serve(reg: Registry, hostname: string, port: number, log =
137
175
  for (const dir of dirs) {
138
176
  if ([...nodes.values()].some((n) => n.project.dir === dir)) continue;
139
177
  try {
178
+ if (!(await exists(dir))) throw new Error(`${dir} is gone`);
179
+ const setup = await setupOf(dir);
180
+ if (setup.missing.length) { waiting.push({ dir, baba: setup.baba, missing: setup.missing, error: null }); continue; }
181
+ // A name another running baba has is as good as none: the folder waits for one of its own, which its wizard asks for.
182
+ const clash = setup.values.name !== undefined ? nodes.get(setup.values.name) : undefined;
183
+ if (clash && clash.project.dir !== dir) { log(`${dir}: its name ${setup.values.name} is ${clash.project.dir}'s; it waits for another`); waiting.push({ dir, baba: true, missing: ["name"], error: null }); continue; }
140
184
  if (voss.stale) throw new Error(`not opened: voss ${buildName(voss.installed!)} is installed and this one is stale; restart to update`);
141
185
  const p = await openProject(dir);
142
186
  const other = nodes.get(p.name);
@@ -150,16 +194,20 @@ export async function serve(reg: Registry, hostname: string, port: number, log =
150
194
  const error = err instanceof Error ? err.message : String(err);
151
195
  log(`${dir}: ${error}`);
152
196
  errors.push({ dir, error });
197
+ waiting.push({ dir, baba: await exists(join(dir, ".baba")), missing: [], error });
153
198
  }
154
199
  }
155
- return { projects: listing(), errors };
200
+ pending = waiting;
201
+ return { projects: listing(), pending: pendingListing(), errors };
156
202
  };
157
- const refresh = (): Promise<Refreshed> => { const r = refreshing.then(reread); refreshing = r.catch(() => {}); return r; };
203
+ /** Runs after whatever changes the running babas is already running, and before the next: refreshes, and the rename that retires a baba to open it again. */
204
+ const queued = <T,>(fn: () => Promise<T>): Promise<T> => { const r = refreshing.then(fn); refreshing = r.catch(() => {}); return r; };
205
+ const refresh = (): Promise<Refreshed> => queued(reread);
158
206
  const stopNodes = async () => { await refreshing; await Promise.all([...nodes.values()].map((n) => n.stop())); };
159
207
 
160
208
  // Which voss this is. A newer package installed over it makes this one stale: it serves what it has,
161
209
  // loads no change and opens no project, until the restart brings the new one up.
162
- const voss: Voss = { ...thisBuild, pid: process.pid, kept: options.kept ?? false, stale: false, installed: null };
210
+ const voss: Voss = { ...thisBuild, pid: process.pid, kept: options.kept ?? false, stale: false, installed: null, about: await aboutOf(reg.dir) };
163
211
  const stale = (b: Build) => {
164
212
  voss.stale = true;
165
213
  voss.installed = { version: b.version, sha: b.sha, at: b.at };
@@ -198,6 +246,9 @@ export async function serve(reg: Registry, hostname: string, port: number, log =
198
246
  if (!built) { built = join(reg.dir, "gui"); await buildGui(built); }
199
247
  const assets = await readTree(built);
200
248
  const token = await pageToken(reg.dir, log);
249
+ const settingsPath = join(reg.dir, "settings.json");
250
+ let settings = await readSettings(settingsPath, log);
251
+ let settingsTurn: Promise<void> = Promise.resolve();
201
252
  function listing(): ProjectEntry[] {
202
253
  const all = [...nodes.values()];
203
254
  const parentOf = (n: Node) => all
@@ -205,6 +256,29 @@ export async function serve(reg: Registry, hostname: string, port: number, log =
205
256
  .sort((a, b) => b.project.dir.length - a.project.dir.length)[0]?.name ?? null;
206
257
  return all.map((n) => ({ name: n.name, title: n.project.title, dir: n.project.dir, parent: parentOf(n) }));
207
258
  }
259
+ /** The folders remembered and not run, each under the nearest running baba that holds it. */
260
+ function pendingListing(): PendingEntry[] {
261
+ const all = [...nodes.values()];
262
+ return pending.map((x) => ({ ...x, parent: all.filter((o) => x.dir.startsWith(o.project.dir + "/")).sort((a, b) => b.project.dir.length - a.project.dir.length)[0]?.name ?? null }));
263
+ }
264
+ /** Sets a folder's required properties: refused when one will not do, its .baba made first when there is none. `fresh` asks for a new .baba and refuses one already there. */
265
+ async function setUp(dir: string, values: RequirementValues, title: string | undefined, fresh: boolean): Promise<void> {
266
+ const now = await setupOf(dir);
267
+ if (fresh && now.baba) throw new NodeError("failed", `${dir} already has a .baba`);
268
+ const taken = [...nodes.values()].filter((n) => n.project.dir !== dir).map((n) => n.name);
269
+ // Each value as sent, else, for one the folder lacks, the suggestion, not the value that made it lacking; else the one it has.
270
+ const merged = Object.fromEntries(requirements.map((r) => [r.key, values[r.key] ?? (now.missing.includes(r.key) ? now.suggested[r.key] : now.values[r.key])])) as RequirementValues;
271
+ // What is written is what is checked: every requirement the folder lacks, and every one asked for, each as it will be written.
272
+ const set = requirements.map((r) => r.key).filter((k) => now.missing.includes(k) || values[k] !== undefined);
273
+ for (const k of set) {
274
+ const why = requirementError(k, merged[k] ?? "", taken);
275
+ if (why) throw new NodeError("args", `${k}: ${why}`);
276
+ }
277
+ if (!now.baba) {
278
+ if (!options.init) throw new NodeError("failed", "this voss cannot make a baba; `voss init` does");
279
+ await init(dir, { name: merged.name, title, framework: options.init.framework, extra: options.init.extra, install: options.init.install });
280
+ } else if (set.length) await writeRequirements(dir, Object.fromEntries(set.map((k) => [k, merged[k]])));
281
+ }
208
282
  const pagesOf = () => {
209
283
  const pages: Record<string, string> = { "": withToken(page(null), token) };
210
284
  for (const name of nodes.keys()) pages[name] = withToken(page(name), token);
@@ -232,7 +306,7 @@ export async function serve(reg: Registry, hostname: string, port: number, log =
232
306
  if (m.type === "refresh") {
233
307
  try {
234
308
  const value = await refresh();
235
- child?.send({ type: "projects", projects: value.projects, pages: pagesOf() });
309
+ child?.send({ type: "projects", projects: value.projects, pending: value.pending, pages: pagesOf() });
236
310
  return { type: "refresh-answer", id: m.id, ok: true, value };
237
311
  } catch (err) { return { type: "refresh-answer", id: m.id, ...failure(err) }; }
238
312
  }
@@ -240,20 +314,69 @@ export async function serve(reg: Registry, hostname: string, port: number, log =
240
314
  try { return { type: "browse-answer", id: m.id, ok: true, value: await browse(m.path || homedir(), await reg.list()) }; }
241
315
  catch (err) { return { type: "browse-answer", id: m.id, ...failure(err) }; }
242
316
  }
243
- if (m.type === "add") {
317
+ // voss's settings: changed whole, written, and the edge told; a theme not known, a key that is not one or that two commands would share, is refused.
318
+ if (m.type === "set-settings") {
319
+ // One change at a time, each over the last one written: two quick changes both stand.
320
+ const turn = settingsTurn;
321
+ let done!: () => void;
322
+ settingsTurn = new Promise((r) => { done = r; });
323
+ await turn;
324
+ try {
325
+ if (m.theme !== undefined && !themes.includes(m.theme)) throw new NodeError("args", `theme: one of ${themes.join(", ")}`);
326
+ let keys: VossSettings["keys"];
327
+ try { keys = m.keys ? applyKeyChanges(settings.keys, m.keys) : settings.keys; } catch (err) { throw new NodeError("args", err instanceof Error ? err.message : String(err)); }
328
+ const next: VossSettings = { theme: m.theme ?? settings.theme, ...(keys && Object.keys(keys).length ? { keys } : {}) };
329
+ await writeFile(settingsPath, JSON.stringify(next, null, 2) + "\n");
330
+ settings = next;
331
+ child?.send({ type: "settings", settings });
332
+ return { type: "settings-answer", id: m.id, ok: true, value: settings };
333
+ } catch (err) { return { type: "settings-answer", id: m.id, ...failure(err) }; }
334
+ finally { done(); }
335
+ }
336
+ // A rename writes the name into the baba's package.json and opens it again under it; its state is its folder's, and stays.
337
+ if (m.type === "rename") {
338
+ try {
339
+ // Retired in the queue that opens and retires babas, so no refresh under way reopens it while it stops; refused while voss is stale, which opens nothing.
340
+ const dir = await queued(async () => {
341
+ const n = nodes.get(m.name);
342
+ if (!n) throw new NodeError("nokey", `no baba ${m.name}`);
343
+ if (m.to === m.name) return n.project.dir;
344
+ if (voss.stale) throw new NodeError("failed", `not renamed: voss ${buildName(voss.installed!)} is installed and this one is stale; restart to update first`);
345
+ const why = requirementError("name", m.to, [...nodes.keys()].filter((k) => k !== m.name));
346
+ if (why) throw new NodeError("args", `name: ${why}`);
347
+ await writeRequirements(n.project.dir, { name: m.to });
348
+ nodes.delete(m.name);
349
+ await n.stop();
350
+ log(`${m.name}: renamed ${m.to}`);
351
+ return n.project.dir;
352
+ });
353
+ const value = await refresh();
354
+ child?.send({ type: "projects", projects: value.projects, pending: value.pending, pages: pagesOf() });
355
+ const failed = value.errors.find((e) => e.dir === dir);
356
+ if (failed) throw new NodeError("failed", failed.error);
357
+ return { type: "add-answer", id: m.id, ok: true, value: { ...value, name: value.projects.find((p) => p.dir === dir)?.name ?? null, dir } };
358
+ } catch (err) { return { type: "add-answer", id: m.id, ...failure(err) }; }
359
+ }
360
+ // Adding remembers a folder: its baba runs when it has what voss requires, else it waits to be set up. With `init`, its .baba is made first.
361
+ // Setting up writes the requirements, making the .baba when there is none; removing forgets the folder, and its files stay.
362
+ if (m.type === "add" || m.type === "setup" || m.type === "remove") {
244
363
  try {
245
364
  const dir = resolve(m.dir);
246
- if (m.init) {
247
- if (!options.init) throw new NodeError("failed", "this voss cannot make a baba; `voss init` does");
248
- await init(dir, { name: m.name, title: m.title, framework: options.init.framework, extra: options.init.extra, install: options.init.install });
365
+ if (m.type === "remove") {
366
+ if (!(await reg.remove(dir))) throw new NodeError("nokey", `voss does not remember ${dir}`);
367
+ } else {
368
+ const st = await stat(dir).catch(() => null);
369
+ if (!st?.isDirectory()) throw new NodeError("args", `${dir} is not a folder`);
370
+ if (m.type === "setup" || m.init) await setUp(dir, m.type === "setup" ? m.values : { ...(m.name ? { name: m.name } : {}) }, m.type === "add" ? m.title : undefined, m.type === "add");
371
+ await reg.add(dir);
249
372
  }
250
- const p = await openProject(dir);
251
- await reg.add(p.dir);
252
373
  const value = await refresh();
253
- child?.send({ type: "projects", projects: value.projects, pages: pagesOf() });
254
- const own = value.errors.find((e) => e.dir === p.dir);
255
- if (own) throw new NodeError("failed", own.error);
256
- return { type: "add-answer", id: m.id, ok: true, value: { ...value, name: p.name, dir: p.dir } };
374
+ child?.send({ type: "projects", projects: value.projects, pending: value.pending, pages: pagesOf() });
375
+ const own = value.projects.find((p) => p.dir === dir);
376
+ // Made or set up, it has to run: a failure is the answer's.
377
+ const failed = value.errors.find((e) => e.dir === dir);
378
+ if (m.type !== "remove" && (m.type === "setup" || m.init) && failed) throw new NodeError("failed", failed.error);
379
+ return { type: "add-answer", id: m.id, ok: true, value: { ...value, name: own?.name ?? null, dir } };
257
380
  } catch (err) { return { type: "add-answer", id: m.id, ...failure(err) }; }
258
381
  }
259
382
  const n = nodes.get(m.name);
@@ -289,7 +412,7 @@ export async function serve(reg: Registry, hostname: string, port: number, log =
289
412
  });
290
413
  child.onMessage((m) => {
291
414
  if (m.type === "ready") {
292
- child.send({ type: "serve", hostname, port, token, voss, projects: listing(), home: homedir(), pages: pagesOf(), assets });
415
+ child.send({ type: "serve", hostname, port, token, voss, projects: listing(), pending: pendingListing(), settings, home: homedir(), pages: pagesOf(), assets });
293
416
  } else if (m.type === "listening") {
294
417
  up = true;
295
418
  // The address goes next to the token, for the CLI to find the daemon by; it goes when the daemon does.
@@ -1,4 +1,4 @@
1
- // The golden interpretation of a piece of promptware: situations a fresh
1
+ // The golden interpretation of a piece of agentic instructions: situations a fresh
2
2
  // reader must decide from the text alone. Authored as givens, each with the
3
3
  // cases under it: when (the moment of decision) and then (the right move, a
4
4
  // few words). A situation the text never mentions, decided by what the text
@@ -1,4 +1,4 @@
1
- // babavoss/prompt: promptware as code. What a baba tells agents, written in
1
+ // babavoss/instructions: agentic instructions as code. What a baba tells agents, written in
2
2
  // MDX (or TSX) that renders to Markdown: `context(...)` for the baba's section of the
3
3
  // context file, `skill({ name, description }, ...)` for a skill file,
4
4
  // `hook(...)` for a moment of the harness; `md` for raw Markdown; `<Call
@@ -15,9 +15,9 @@ import { isElement, jsx, Fragment, type PromptNode, type PromptElement, type Pro
15
15
 
16
16
  export { Fragment, type PromptNode, type PromptElement, type PromptComponent, type JSX } from "./jsx-runtime.ts";
17
17
 
18
- /** What a baba declares under `promptware`. */
18
+ /** What a baba declares under `.baba/agents/`. */
19
19
  export type Prompt =
20
- | { kind: "context"; body: PromptNode; eval?: InterpretationSuite }
20
+ | { kind: "context"; body: PromptNode; eval?: InterpretationSuite; hooks?: HookDef[] }
21
21
  | { kind: "skill"; name: string; description: string; uses: string[]; body: PromptNode; eval?: InterpretationSuite }
22
22
  | { kind: "hook"; on: HookEvent; match: string | null; action: string; strict: boolean };
23
23
 
@@ -33,7 +33,8 @@ export type HookEvent = (typeof HOOK_EVENTS)[number];
33
33
  * PostToolUse to tool names, a regex. With `strict`, a baba that does not
34
34
  * answer blocks; otherwise it lets through.
35
35
  */
36
- export function hook(def: { on: HookEvent; match?: string; action: string; strict?: boolean }): Prompt {
36
+ export interface HookDef { on: HookEvent; match?: string; action: string; strict?: boolean }
37
+ export function hook(def: HookDef): Prompt {
37
38
  if (!HOOK_EVENTS.includes(def.on)) throw new Error(`hook: on is one of ${HOOK_EVENTS.join(", ")}, not ${JSON.stringify(def.on)}`);
38
39
  if (def.match !== undefined && def.on !== "PreToolUse" && def.on !== "PostToolUse") throw new Error(`hook on ${def.on}: match is for PreToolUse and PostToolUse`);
39
40
  return { kind: "hook", on: def.on, match: def.match ?? null, action: def.action, strict: def.strict === true };
@@ -72,7 +73,7 @@ export const hookResult = (): Schema<HookAnswer> => s.object({ decision: s.optio
72
73
  const WORD = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
73
74
 
74
75
  /** The baba's section of the context file, CLAUDE.md and AGENTS.md: what the project is and how it is worked on, in any Markdown; one per baba. Voss wraps it with the doors and the skills. */
75
- export const context = (body: PromptNode, options?: { eval?: InterpretationSuite }): Prompt => ({ kind: "context", body, ...(options?.eval ? { eval: options.eval } : {}) });
76
+ export const context = (body: PromptNode, options?: { eval?: InterpretationSuite; hooks?: HookDef[] }): Prompt => ({ kind: "context", body, ...(options?.eval ? { eval: options.eval } : {}), ...(options?.hooks?.length ? { hooks: options.hooks.map((h) => { hook(h); return h; }) } : {}) });
76
77
 
77
78
  /** A skill: a file an agent opens when the description fits what it is doing. `uses` names the calls it is about, checked against the contract. The body is any Markdown node. */
78
79
  export function skill(head: { name: string; description: string; uses?: string[]; eval?: InterpretationSuite }, body: PromptNode): Prompt {
@@ -82,11 +83,11 @@ export function skill(head: { name: string; description: string; uses?: string[]
82
83
  }
83
84
 
84
85
  /** MDX's metadata and lazy document component become the same prompt as TSX. */
85
- export function mdxDocument(meta: { kind: "context"; eval?: InterpretationSuite } | { kind: "skill"; name: string; description: string; uses?: string[]; eval?: InterpretationSuite }, Content: PromptComponent): Prompt {
86
+ export function mdxDocument(meta: { kind: "context"; eval?: InterpretationSuite; hooks?: HookDef[] } | { kind: "skill"; name: string; description: string; uses?: string[]; eval?: InterpretationSuite }, Content: PromptComponent): Prompt {
86
87
  const body = jsx(Content, {});
87
88
  if (meta?.kind === "context") return context(body, meta);
88
89
  if (meta?.kind === "skill") return skill(meta, body);
89
- throw new Error('MDX promptware needs export const prompt = { kind: "context" | "skill", ... }');
90
+ throw new Error('an MDX instruction file needs export const prompt = { kind: "context" | "skill", ... }');
90
91
  }
91
92
 
92
93
  /** Raw Markdown, verbatim, with the common indentation removed: for prose longer than a line. */
@@ -172,7 +173,7 @@ function block(node: PromptNode): string {
172
173
  case "table": return table(kids) + "\n";
173
174
  case "strong": case "em": case "del": case "img": case "input": case "code": case "b": case "i": case "a": case "br": return inline(node) + "\n\n";
174
175
  case "tr": case "th": case "td": return inline(kids);
175
- default: throw new Error(`promptware: no such tag <${String(type)}>`);
176
+ default: throw new Error(`instructions: no such tag <${String(type)}>`);
176
177
  }
177
178
  }
178
179
 
@@ -1,5 +1,5 @@
1
- // The JSX runtime for promptware: elements that render to Markdown, not to
2
- // the DOM. A file says `/** @jsxImportSource babavoss/prompt */` and writes
1
+ // The JSX runtime for agentic instructions: elements that render to Markdown, not to
2
+ // the DOM. A file says `/** @jsxImportSource babavoss/instructions */` and writes
3
3
  // `<p>`, `<ul>`, `<pre lang="ts">`, `<Call of="feed" />`; `render` turns the
4
4
  // tree into text. Pure, no React.
5
5
  export interface PromptElement { readonly $prompt: true; type: string | PromptComponent; props: Record<string, unknown> }
@@ -18,7 +18,7 @@ export const isElement = (v: unknown): v is PromptElement => typeof v === "objec
18
18
 
19
19
  type Children = { children?: PromptNode };
20
20
 
21
- // What TypeScript admits in a promptware file: the Markdown tags, each with the props it renders.
21
+ // What TypeScript admits in an instruction file: the Markdown tags, each with the props it renders.
22
22
  export namespace JSX {
23
23
  export type Element = PromptElement;
24
24
  /** What a tag may be: a Markdown tag, or a component answering any node, text and lists included, not only an element. */
package/src/mcp/main.ts CHANGED
@@ -17,7 +17,7 @@ import { toolsOf, callName, promptsOf, promptText, type Tools } from "./tools.ts
17
17
  import { BUDGET, DoorError, budget, errorOf, index, schemaOf, followEntity, watchQuery, stateOf, untilOf, byKey, isTruncated, type Outcome } from "../door/core.ts";
18
18
 
19
19
  const VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
20
- const empty: Manifest = { systems: [], promptware: { context: null, skills: {}, hooks: [] }, harness: defaultHarness, components: {}, resources: {}, effects: {}, actions: {}, queries: {}, tick: null };
20
+ const empty: Manifest = { systems: [], instructions: { context: null, skills: {}, hooks: [] }, harness: defaultHarness, components: {}, resources: {}, effects: {}, actions: {}, queries: {}, tick: null };
21
21
 
22
22
  const log = (l: string) => process.stderr.write(`voss mcp: ${l}\n`);
23
23
  const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
@@ -159,13 +159,13 @@ export async function runMcp(project: Project, rootDir: string): Promise<number>
159
159
  }
160
160
  case "resources/list": return {
161
161
  resources: [
162
- ...(m.promptware.context ? [{ uri: "baba://context", name: "context", description: "what the baba tells agents", mimeType: "text/markdown" }] : []),
162
+ ...(m.instructions.context ? [{ uri: "baba://context", name: "context", description: "what the baba tells agents", mimeType: "text/markdown" }] : []),
163
163
  ...[...outs.keys()].map((f) => ({ uri: `baba://out/${f}`, name: f, description: "an answer over the output budget", mimeType: "application/json" })),
164
164
  ],
165
165
  };
166
166
  case "resources/read": {
167
167
  const uri = String(p.uri ?? "");
168
- const context = uri === "baba://context" ? m.promptware.context : null;
168
+ const context = uri === "baba://context" ? m.instructions.context : null;
169
169
  if (context) return { contents: [{ uri, mimeType: "text/markdown", text: context }] };
170
170
  const out = uri.startsWith("baba://out/") ? outs.get(basename(uri.slice(11))) : undefined;
171
171
  if (out) return { contents: [{ uri, mimeType: "application/json", text: await readFile(out, "utf8") }] };
package/src/mcp/tools.ts CHANGED
@@ -16,12 +16,12 @@ export interface Target { kind: "action" | "query"; name: string; system: string
16
16
 
17
17
  export interface Tools { tools: Tool[]; targets: Map<string, Target>; generic: boolean }
18
18
 
19
- /** The promptware's threshold for describing the contract as `call`/`read`; the server itself is generic unless VOSS_MCP_TOOLS=1. */
19
+ /** The agentic instructions's threshold for describing the contract as `call`/`read`; the server itself is generic unless VOSS_MCP_TOOLS=1. */
20
20
 
21
21
  /** How an agent learns the contract: said in every tool that takes a call's name. */
22
22
  const LEARN = "`manifest` lists every call with an example; `manifest` with a name gives its schemas.";
23
23
 
24
- /** `<system>_<name>`; a name that already starts with its system's, as voss's own do (`promptware-sync`), is not prefixed twice: `promptware_sync`. */
24
+ /** `<system>_<name>`; a name that already starts with its system's, as voss's own do (`agents-sync`), is not prefixed twice: `agents_sync`. */
25
25
  export const toolName = (system: string, name: string) => name.startsWith(`${system}-`) ? `${system}_${name.slice(system.length + 1)}` : `${system}_${name}`;
26
26
 
27
27
  const isObj = (v: unknown): v is Obj => typeof v === "object" && v !== null && !Array.isArray(v);
@@ -107,12 +107,12 @@ export function callName(m: Manifest, n: string): { kind: "action" | "query"; na
107
107
 
108
108
  /** One prompt per skill, by its name. */
109
109
  export function promptsOf(m: Manifest): { name: string; description: string; skill: string }[] {
110
- return Object.entries(m.promptware.skills).map(([k, sk]) => ({ name: k, description: sk.description, skill: k }));
110
+ return Object.entries(m.instructions.skills).map(([k, sk]) => ({ name: k, description: sk.description, skill: k }));
111
111
  }
112
112
 
113
113
  /** A skill's prompt text: its body, then the tools it uses. */
114
114
  export function promptText(m: Manifest, skill: string, generic: boolean): string | null {
115
- const sk = m.promptware.skills[skill];
115
+ const sk = m.instructions.skills[skill];
116
116
  if (!sk) return null;
117
117
  const uses = (sk.uses ?? []).map((n) => {
118
118
  const c = m.actions[n] ?? m.queries[n];