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.
- package/gui/babavoss-web.js +4 -1
- package/gui/{chunk-9gmmm54v.js → chunk-qgzmajan.js} +619 -574
- package/gui/gui.js +1538 -277
- package/gui/theme.css +2007 -1128
- package/index.ts +1 -1
- package/package.json +5 -5
- package/src/{promptware → agents}/compile.ts +23 -31
- package/src/{promptware → agents}/define.ts +2 -2
- package/src/{promptware → agents}/disk.ts +7 -4
- package/src/{promptware → agents}/sync.ts +2 -2
- package/src/{promptware → agents}/system.ts +17 -17
- package/src/baba/check.ts +3 -3
- package/src/baba/config.ts +5 -2
- package/src/baba/init.ts +13 -18
- package/src/baba/node.ts +2 -2
- package/src/baba/project.ts +2 -2
- package/src/baba/requirements.ts +39 -0
- package/src/baba/setup.ts +30 -0
- package/src/baba/worker.ts +13 -5
- package/src/build/mdx.ts +4 -4
- package/src/build/project.ts +6 -1
- package/src/build/views.ts +3 -3
- package/src/desktop/desktop.css +35 -39
- package/src/desktop/index.ts +9 -7
- package/src/desktop/keys.ts +153 -0
- package/src/desktop/view.tsx +174 -130
- package/src/door/core.ts +2 -2
- package/src/ecs/baba.ts +27 -17
- package/src/generated/build.ts +1 -1
- package/src/gui/gui.tsx +21 -31
- package/src/gui/index.ts +6 -2
- package/src/gui/{promptware.tsx → instructions.tsx} +12 -12
- package/src/gui/levels.tsx +241 -59
- package/src/gui/lockup.ts +11 -0
- package/src/gui/settings.tsx +241 -0
- package/src/gui/setup.tsx +77 -0
- package/src/gui/theme.css +205 -58
- package/src/gui/theme.ts +9 -5
- package/src/gui/voss-settings.ts +48 -0
- package/src/gui/wizard.tsx +17 -40
- package/src/guide/add-a-desktop.mdx +7 -7
- package/src/guide/index.ts +4 -4
- package/src/guide/write-a-system.mdx +3 -3
- package/src/guide/{write-promptware.mdx → write-agentic-instructions.mdx} +17 -24
- package/src/http/server.ts +140 -17
- package/src/{prompt → instructions}/evals.ts +1 -1
- package/src/{prompt → instructions}/index.ts +9 -8
- package/src/{prompt → instructions}/jsx-runtime.ts +3 -3
- package/src/mcp/main.ts +3 -3
- package/src/mcp/tools.ts +4 -4
- package/src/runtime/harness.ts +2 -2
- package/src/server/edge.ts +31 -3
- package/src/server/main.ts +2 -1
- package/src/server/messages.ts +47 -7
- package/src/shell/run.ts +18 -18
- package/src/spec/index.ts +1 -1
- package/src/web/core.tsx +2 -2
- package/src/web/index.tsx +1 -1
- package/src/desktop/bob.ts +0 -76
- /package/src/{promptware → agents}/markdown.d.ts +0 -0
- /package/src/{prompt → instructions}/jsx-dev-runtime.ts +0 -0
- /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 `
|
|
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,
|
|
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, `
|
|
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
|
|
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;
|
|
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
|
|
97
|
-
- The shell inherits voss's theme tokens;
|
|
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.
|
package/src/guide/index.ts
CHANGED
|
@@ -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
|
|
4
|
-
import type { Prompt } from "../
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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-
|
|
2
|
-
description: "Write or change what a baba tells agents: its context, skills and hooks,
|
|
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
|
|
5
|
+
# Write agentic instructions
|
|
6
6
|
|
|
7
|
-
|
|
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,
|
|
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
|
|
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.
|
|
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/
|
|
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
|
-
```
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
+
## Rules
|
|
61
58
|
|
|
62
|
-
|
|
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
|
|
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
|
|
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: `
|
|
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
|
|
package/src/http/server.ts
CHANGED
|
@@ -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
|
-
|
|
200
|
+
pending = waiting;
|
|
201
|
+
return { projects: listing(), pending: pendingListing(), errors };
|
|
156
202
|
};
|
|
157
|
-
|
|
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
|
-
|
|
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.
|
|
247
|
-
if (!
|
|
248
|
-
|
|
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.
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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(`
|
|
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
|
|
2
|
-
// the DOM. A file says `/** @jsxImportSource babavoss/
|
|
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
|
|
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: [],
|
|
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.
|
|
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.
|
|
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
|
|
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 (`
|
|
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.
|
|
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.
|
|
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];
|