babavoss 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/NOTICE +7 -0
  2. package/bin/voss.ts +48 -0
  3. package/gui/babavoss-web.js +234 -0
  4. package/gui/chunk-5hpp1ypv.js +11710 -0
  5. package/gui/chunk-9rq662fd.js +8627 -0
  6. package/gui/chunk-k3cm8j6r.js +433 -0
  7. package/gui/chunk-pk09y98y.js +50 -0
  8. package/gui/chunk-smz02qa6.js +186 -0
  9. package/gui/chunk-wwqypxre.js +49 -0
  10. package/gui/gui.js +2015 -0
  11. package/gui/react-compiler-runtime.js +39 -0
  12. package/gui/react-dom-client.js +21 -0
  13. package/gui/react-dom.js +44 -0
  14. package/gui/react-jsx-runtime.js +19 -0
  15. package/gui/react.js +105 -0
  16. package/gui/theme.css +3053 -0
  17. package/index.ts +15 -0
  18. package/package.json +50 -4
  19. package/src/baba/check.ts +90 -0
  20. package/src/baba/config.ts +261 -0
  21. package/src/baba/find.ts +12 -0
  22. package/src/baba/init.ts +176 -0
  23. package/src/baba/node.ts +483 -0
  24. package/src/baba/project.ts +63 -0
  25. package/src/baba/registry.ts +35 -0
  26. package/src/baba/worker.ts +55 -0
  27. package/src/bench/index.ts +6 -0
  28. package/src/bench/measure.ts +132 -0
  29. package/src/bench/scenarios.ts +136 -0
  30. package/src/build/builder.ts +74 -0
  31. package/src/build/failure.ts +78 -0
  32. package/src/build/guard.ts +85 -0
  33. package/src/build/mdx-register.ts +3 -0
  34. package/src/build/mdx.ts +38 -0
  35. package/src/build/project.ts +38 -0
  36. package/src/build/views.ts +146 -0
  37. package/src/builder/main.ts +29 -0
  38. package/src/desktop/bob.ts +76 -0
  39. package/src/desktop/desktop.css +111 -0
  40. package/src/desktop/icons.ts +50 -0
  41. package/src/desktop/index.ts +323 -0
  42. package/src/desktop/routes.ts +98 -0
  43. package/src/desktop/view.tsx +673 -0
  44. package/src/door/core.ts +384 -0
  45. package/src/ecs/baba.ts +431 -0
  46. package/src/ecs/codec.ts +334 -0
  47. package/src/ecs/handles.ts +91 -0
  48. package/src/ecs/replica.ts +150 -0
  49. package/src/ecs/runtime.ts +603 -0
  50. package/src/ecs/scheduler.ts +75 -0
  51. package/src/ecs/snapshot.ts +102 -0
  52. package/src/ecs/state.ts +759 -0
  53. package/src/ecs/system.ts +256 -0
  54. package/src/ecs/table.ts +420 -0
  55. package/src/ecs/testbed.ts +97 -0
  56. package/src/exec/host.ts +177 -0
  57. package/src/exec/main.ts +98 -0
  58. package/src/exec/watch.ts +7 -0
  59. package/src/exec/wire.ts +29 -0
  60. package/src/generated/build.ts +4 -0
  61. package/src/gui/css.d.ts +1 -0
  62. package/src/gui/gui.tsx +245 -0
  63. package/src/gui/index.ts +51 -0
  64. package/src/gui/inspector.tsx +47 -0
  65. package/src/gui/levels.tsx +73 -0
  66. package/src/gui/promptware.tsx +68 -0
  67. package/src/gui/runner.tsx +118 -0
  68. package/src/gui/theme.css +498 -0
  69. package/src/gui/theme.ts +25 -0
  70. package/src/gui/wizard.tsx +227 -0
  71. package/src/guide/add-a-desktop.mdx +100 -0
  72. package/src/guide/compose-an-interface.mdx +84 -0
  73. package/src/guide/index.ts +13 -0
  74. package/src/guide/reach-outside.mdx +112 -0
  75. package/src/guide/spec-a-system.mdx +93 -0
  76. package/src/guide/systems-together.mdx +72 -0
  77. package/src/guide/write-a-system.mdx +183 -0
  78. package/src/guide/write-promptware.mdx +90 -0
  79. package/src/http/server.ts +310 -0
  80. package/src/kernel/build.ts +21 -0
  81. package/src/kernel/builder.ts +105 -0
  82. package/src/kernel/children.ts +117 -0
  83. package/src/kernel/context.ts +90 -0
  84. package/src/kernel/lock.ts +46 -0
  85. package/src/kernel/names.ts +14 -0
  86. package/src/kernel/schema.ts +130 -0
  87. package/src/kernel/where.ts +12 -0
  88. package/src/kit/index.ts +232 -0
  89. package/src/maker/system.ts +213 -0
  90. package/src/mcp/daemon.ts +61 -0
  91. package/src/mcp/main.ts +208 -0
  92. package/src/mcp/rpc.ts +64 -0
  93. package/src/mcp/tools.ts +125 -0
  94. package/src/prompt/evals.ts +42 -0
  95. package/src/prompt/index.ts +242 -0
  96. package/src/prompt/jsx-dev-runtime.ts +1 -0
  97. package/src/prompt/jsx-runtime.ts +49 -0
  98. package/src/prompt/mdx.d.ts +1 -0
  99. package/src/promptware/compile.ts +183 -0
  100. package/src/promptware/define.ts +16 -0
  101. package/src/promptware/disk.ts +72 -0
  102. package/src/promptware/markdown.d.ts +6 -0
  103. package/src/promptware/sync.ts +437 -0
  104. package/src/promptware/system.ts +215 -0
  105. package/src/runtime/bridge.ts +85 -0
  106. package/src/runtime/connect.ts +54 -0
  107. package/src/runtime/env.ts +35 -0
  108. package/src/runtime/harness.ts +80 -0
  109. package/src/runtime/main.ts +119 -0
  110. package/src/runtime/worker.ts +33 -0
  111. package/src/server/edge.ts +332 -0
  112. package/src/server/main.ts +45 -0
  113. package/src/server/messages.ts +97 -0
  114. package/src/server/protocol.ts +37 -0
  115. package/src/services/args.ts +45 -0
  116. package/src/services/exec.ts +69 -0
  117. package/src/services/fs.ts +139 -0
  118. package/src/services/http.ts +30 -0
  119. package/src/services/index.ts +113 -0
  120. package/src/services/secrets.ts +18 -0
  121. package/src/shell/address.ts +21 -0
  122. package/src/shell/args.ts +219 -0
  123. package/src/shell/client.ts +107 -0
  124. package/src/shell/codes.ts +26 -0
  125. package/src/shell/positional.ts +20 -0
  126. package/src/shell/run.ts +470 -0
  127. package/src/shell/service.ts +167 -0
  128. package/src/shell/state.ts +204 -0
  129. package/src/spec/adapters.ts +72 -0
  130. package/src/spec/diff.ts +26 -0
  131. package/src/spec/files.ts +17 -0
  132. package/src/spec/index.ts +155 -0
  133. package/src/spec/run.ts +97 -0
  134. package/src/spec/take.ts +54 -0
  135. package/src/test/index.ts +8 -0
  136. package/src/test/prove.ts +56 -0
  137. package/src/test/records.ts +23 -0
  138. package/src/test/specs.ts +56 -0
  139. package/src/test/steps.ts +100 -0
  140. package/src/test/voss-dir.ts +17 -0
  141. package/src/transport/messages.ts +110 -0
  142. package/src/transport/transport.ts +62 -0
  143. package/src/wall/probe.ts +67 -0
  144. package/src/wall/profile.ts +103 -0
  145. package/src/wall/spawn.ts +59 -0
  146. package/src/web/app.tsx +53 -0
  147. package/src/web/core.tsx +140 -0
  148. package/src/web/form.ts +155 -0
  149. package/src/web/hooks.ts +135 -0
  150. package/src/web/index.tsx +17 -0
  151. package/src/web/list.ts +19 -0
  152. package/src/web/maker.tsx +766 -0
  153. package/src/web/objects.tsx +213 -0
  154. package/src/web/socket.ts +84 -0
  155. package/src/web/state.tsx +69 -0
  156. package/src/web/store.ts +221 -0
  157. package/src/web/ui.tsx +135 -0
  158. package/README.md +0 -5
@@ -0,0 +1,242 @@
1
+ // babavoss/prompt: promptware as code. What a baba tells agents, written in
2
+ // MDX (or TSX) that renders to Markdown: `context(...)` for the baba's section of the
3
+ // context file, `skill({ name, description }, ...)` for a skill file,
4
+ // `hook(...)` for a moment of the harness; `md` for raw Markdown; `<Call
5
+ // of="feed" />` and its kin for the parts that come from the contract, so a
6
+ // skill never says what the contract does not. Static: rendered once per
7
+ // generation, from the manifest alone.
8
+ import type { InterpretationSuite } from "./evals.ts";
9
+ export { interpretation, givensOf, type InterpretationSuite, type InterpretationQuestion, type InterpretationGiven, type InterpretationCase } from "./evals.ts";
10
+ import type { Manifest, Call as CallEntry } from "../ecs/baba.ts";
11
+ import { s, type Json, type Schema } from "../kernel/schema.ts";
12
+ import { exampleOf } from "../ecs/handles.ts";
13
+ import { usage } from "../shell/args.ts";
14
+ import { isElement, jsx, Fragment, type PromptNode, type PromptElement, type PromptComponent } from "./jsx-runtime.ts";
15
+
16
+ export { Fragment, type PromptNode, type PromptElement, type PromptComponent, type JSX } from "./jsx-runtime.ts";
17
+
18
+ /** What a baba declares under `promptware`. */
19
+ export type Prompt =
20
+ | { kind: "context"; body: PromptNode; eval?: InterpretationSuite }
21
+ | { kind: "skill"; name: string; description: string; uses: string[]; body: PromptNode; eval?: InterpretationSuite }
22
+ | { kind: "hook"; on: HookEvent; match: string | null; action: string; strict: boolean };
23
+
24
+ /** The moments a harness can hand to the baba: Claude Code's hook events. */
25
+ export const HOOK_EVENTS = ["PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop", "SubagentStop", "SessionStart", "PreCompact", "Notification"] as const;
26
+ export type HookEvent = (typeof HOOK_EVENTS)[number];
27
+
28
+ /**
29
+ * A hook: at `on`, the harness calls the baba's action `action` with the
30
+ * event as its args, `voss baba ACTION @- --hook EVENT`, and does what the
31
+ * answer says: `{ decision: "block", reason }` stops it, `{ context }` is
32
+ * told to the agent, `{}` lets it through. `match` narrows PreToolUse and
33
+ * PostToolUse to tool names, a regex. With `strict`, a baba that does not
34
+ * answer blocks; otherwise it lets through.
35
+ */
36
+ export function hook(def: { on: HookEvent; match?: string; action: string; strict?: boolean }): Prompt {
37
+ if (!HOOK_EVENTS.includes(def.on)) throw new Error(`hook: on is one of ${HOOK_EVENTS.join(", ")}, not ${JSON.stringify(def.on)}`);
38
+ if (def.match !== undefined && def.on !== "PreToolUse" && def.on !== "PostToolUse") throw new Error(`hook on ${def.on}: match is for PreToolUse and PostToolUse`);
39
+ return { kind: "hook", on: def.on, match: def.match ?? null, action: def.action, strict: def.strict === true };
40
+ }
41
+
42
+ /** A hook's event as Claude Code sends it: the fields any event may carry, typed; anything else let through. */
43
+ export interface HookArgs {
44
+ session_id?: string; transcript_path?: string; cwd?: string; hook_event_name?: string;
45
+ tool_name?: string; tool_input?: unknown; tool_response?: unknown;
46
+ prompt?: string; message?: string; trigger?: string; custom_instructions?: string; source?: string; stop_hook_active?: boolean;
47
+ [field: string]: unknown;
48
+ }
49
+ /** What a hook's action answers: block with a reason, or let through, with context for the agent when there is some. */
50
+ export interface HookAnswer { decision?: "allow" | "block"; reason?: string; context?: string }
51
+
52
+ /** The args a hook's action takes: the event as Claude Code sends it, the common fields typed, the rest let through. */
53
+ export function hookArgs(on: HookEvent): Schema<HookArgs> {
54
+ const common = { session_id: s.optional(s.string()), transcript_path: s.optional(s.string()), cwd: s.optional(s.string()), hook_event_name: s.optional(s.string()) };
55
+ const tool = { tool_name: s.optional(s.string()), tool_input: s.optional(s.unknown()) };
56
+ const shape = (() => {
57
+ switch (on) {
58
+ case "PreToolUse": return { ...common, ...tool };
59
+ case "PostToolUse": return { ...common, ...tool, tool_response: s.optional(s.unknown()) };
60
+ case "UserPromptSubmit": return { ...common, prompt: s.optional(s.string()) };
61
+ case "Notification": return { ...common, message: s.optional(s.string()) };
62
+ case "PreCompact": return { ...common, trigger: s.optional(s.string()), custom_instructions: s.optional(s.string()) };
63
+ case "SessionStart": return { ...common, source: s.optional(s.string()) };
64
+ default: return { ...common, stop_hook_active: s.optional(s.boolean()) };
65
+ }
66
+ })();
67
+ return s.object(shape, { open: true }) as unknown as Schema<HookArgs>;
68
+ }
69
+ /** What a hook's action answers: block with a reason, or let through, with context for the agent when there is some. */
70
+ export const hookResult = (): Schema<HookAnswer> => s.object({ decision: s.optional(s.enum("allow", "block")), reason: s.optional(s.string()), context: s.optional(s.string()) }) as Schema<HookAnswer>;
71
+
72
+ const WORD = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
73
+
74
+ /** 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
+
77
+ /** 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
+ export function skill(head: { name: string; description: string; uses?: string[]; eval?: InterpretationSuite }, body: PromptNode): Prompt {
79
+ if (!WORD.test(head.name)) throw new Error(`skill ${JSON.stringify(head.name)}: lowercase words joined by single dashes`);
80
+ if (!head.description.trim() || head.description.includes("\n") || head.description.length > 1024) throw new Error(`skill ${head.name}: a one-line description, at most 1024 characters`);
81
+ return { kind: "skill", name: head.name, description: head.description.trim(), uses: [...(head.uses ?? [])], body, ...(head.eval ? { eval: head.eval } : {}) };
82
+ }
83
+
84
+ /** 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
+ const body = jsx(Content, {});
87
+ if (meta?.kind === "context") return context(body, meta);
88
+ if (meta?.kind === "skill") return skill(meta, body);
89
+ throw new Error('MDX promptware needs export const prompt = { kind: "context" | "skill", ... }');
90
+ }
91
+
92
+ /** Raw Markdown, verbatim, with the common indentation removed: for prose longer than a line. */
93
+ export function md(strings: TemplateStringsArray, ...values: unknown[]): string {
94
+ // Raw, so a backslash before a backtick or a dollar is the author's escape; the bundler may have escaped a non-ASCII character as \uXXXX, which is put back.
95
+ const text = strings.raw.map((s, i) => s.replace(/\\u([0-9a-fA-F]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16))).replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\\\/g, "\\") + (i < values.length ? String(values[i]) : "")).join("");
96
+ return dedent(text);
97
+ }
98
+
99
+ function dedent(text: string): string {
100
+ const lines = text.replace(/^\n/, "").replace(/\n[ \t]*$/, "").split("\n");
101
+ const indents = lines.filter((l) => l.trim()).map((l) => /^[ \t]*/.exec(l)![0].length);
102
+ const cut = indents.length ? Math.min(...indents) : 0;
103
+ return lines.map((l) => l.slice(Math.min(cut, /^[ \t]*/.exec(l)![0].length))).join("\n").trim();
104
+ }
105
+
106
+ // ---- rendering --------------------------------------------------------------
107
+
108
+ /** What the contract components render from; `refs` collects the skills a `See` named, for the manifest to check. */
109
+ export interface Context { manifest: Manifest; program: string; refs?: string[] }
110
+
111
+ let current: Context | null = null;
112
+ const ctx = (what: string): Context => { if (!current) throw new Error(`<${what}> renders from the contract, and no manifest is in scope`); return current; };
113
+
114
+ /** The tree as Markdown, with `context` in scope for the contract components. */
115
+ export function render(node: PromptNode, context: Context | null = null): string {
116
+ const was = current;
117
+ current = context;
118
+ try { return block(node).trim(); } finally { current = was; }
119
+ }
120
+
121
+ const text = (v: unknown): string => (typeof v === "string" ? v : typeof v === "number" ? String(v) : "");
122
+
123
+ /** Inline content: text and inline tags joined as they come. */
124
+ function inline(node: PromptNode): string {
125
+ if (node === null || node === undefined || typeof node === "boolean") return "";
126
+ if (Array.isArray(node)) return node.map(inline).join("");
127
+ if (!isElement(node)) return text(node);
128
+ const { type, props } = node;
129
+ const kids = props.children as PromptNode;
130
+ if (typeof type === "function") return inline(type(props));
131
+ switch (type) {
132
+ case "code": {
133
+ const value = inline(kids);
134
+ const fence = "`".repeat(Math.max(1, ...Array.from(value.matchAll(/`+/g), m => m[0].length + 1)));
135
+ const pad = value.startsWith("`") || value.endsWith("`") ? " " : "";
136
+ return `${fence}${pad}${value}${pad}${fence}`;
137
+ }
138
+ case "strong": case "b": return `**${inline(kids)}**`;
139
+ case "em": case "i": return `*${inline(kids)}*`;
140
+ case "a": return `[${inline(kids)}](${String(props.href)})`;
141
+ case "del": return `~~${inline(kids)}~~`;
142
+ case "img": return `![${String(props.alt ?? "")}](${String(props.src)})`;
143
+ case "input": return props.type === "checkbox" ? `[${props.checked ? "x" : " "}] ` : "";
144
+ case "br": return " \n";
145
+ default: return block(node);
146
+ }
147
+ }
148
+
149
+ /** Block content: paragraphs, lists, code, tables, each followed by a blank line. */
150
+ function block(node: PromptNode): string {
151
+ if (node === null || node === undefined || typeof node === "boolean") return "";
152
+ if (Array.isArray(node)) return joinBlocks(node.map(block));
153
+ if (!isElement(node)) return text(node) + "\n\n";
154
+ const { type, props } = node;
155
+ const kids = props.children as PromptNode;
156
+ if (typeof type === "function") return block(type(props));
157
+ switch (type) {
158
+ case "h1": case "h2": case "h3": case "h4": case "h5": case "h6": return `${"#".repeat(Number(type.slice(1)))} ${inline(kids).trim()}\n\n`;
159
+ case "h": return `${"#".repeat(Number(props.level ?? 2))} ${inline(kids).trim()}\n\n`;
160
+ case "p": return `${inline(kids).trim()}\n\n`;
161
+ case "ul": case "ol": return items(kids, type === "ol") + "\n";
162
+ case "li": return `- ${inline(kids).trim()}\n`;
163
+ case "pre": {
164
+ const code = isElement(kids) && kids.type === "code" ? kids : null;
165
+ const content = code ? text(code.props.children).replace(/\n$/, "") : dedent(inline(kids));
166
+ const lang = code ? String(code.props.className ?? "").replace(/^language-/, "") : String(props.lang ?? "");
167
+ const fence = "`".repeat(Math.max(3, ...Array.from(content.matchAll(/`+/g), m => m[0].length + 1)));
168
+ return `${fence}${lang}\n${content}\n${fence}\n\n`;
169
+ }
170
+ case "blockquote": return block(kids).trim().split("\n").map((l) => `> ${l}`).join("\n") + "\n\n";
171
+ case "hr": return "---\n\n";
172
+ case "table": return table(kids) + "\n";
173
+ 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
+ case "tr": case "th": case "td": return inline(kids);
175
+ default: throw new Error(`promptware: no such tag <${String(type)}>`);
176
+ }
177
+ }
178
+
179
+ const joinBlocks = (parts: string[]): string => parts.filter((p) => p.trim()).map((p) => p.replace(/\n+$/, "")).join("\n\n") + "\n\n";
180
+
181
+ function items(kids: PromptNode, ordered: boolean): string {
182
+ const list = (Array.isArray(kids) ? kids : [kids]).flat().filter((k) => isElement(k) && k.type === "li") as PromptElement[];
183
+ return list.map((li, i) => {
184
+ // An item is a line: text and inline parts joined; a block inside it stands on its own lines, indented.
185
+ const body = inline(li.props.children as PromptNode).trim().split("\n").join("\n ");
186
+ return `${ordered ? `${i + 1}.` : "-"} ${body}`;
187
+ }).join("\n") + "\n";
188
+ }
189
+
190
+ function table(kids: PromptNode): string {
191
+ const flattened = (Array.isArray(kids) ? kids : [kids]).flat().flatMap(k => isElement(k) && ["thead", "tbody"].includes(String(k.type)) ? (Array.isArray(k.props.children) ? k.props.children : [k.props.children]) : [k]);
192
+ const rows = (Array.isArray(flattened) ? flattened : [flattened]).flat().filter((k) => isElement(k) && k.type === "tr") as PromptElement[];
193
+ const cellsOf = (tr: PromptElement) => ((Array.isArray(tr.props.children) ? tr.props.children : [tr.props.children]) as PromptNode[]).flat().filter(isElement) as PromptElement[];
194
+ const line = (tr: PromptElement) => `| ${cellsOf(tr).map((c) => inline(c.props.children as PromptNode).trim().replaceAll("|", "\\|")).join(" | ")} |`;
195
+ const out: string[] = [];
196
+ rows.forEach((tr, i) => {
197
+ out.push(line(tr));
198
+ if (i === 0) out.push(`| ${cellsOf(tr).map(() => "---").join(" | ")} |`);
199
+ });
200
+ return out.join("\n") + "\n";
201
+ }
202
+
203
+ // ---- the contract's components ----------------------------------------------
204
+
205
+ const quoted = (v: unknown) => `'${JSON.stringify(v).replaceAll("'", `'\\''`)}'`;
206
+ const entry = (name: string, what: string): { d: CallEntry; kind: "action" | "query" } => {
207
+ const c = ctx(what);
208
+ const d = c.manifest.actions[name] ?? c.manifest.queries[name];
209
+ if (!d) throw new Error(`<${what} of="${name}">: no action or query ${name}; the baba has: ${[...Object.keys(c.manifest.actions), ...Object.keys(c.manifest.queries)].join(", ")}`);
210
+ return { d, kind: name in c.manifest.actions ? "action" : "query" };
211
+ };
212
+
213
+ /** The CLI line of a call with a literal example of its args, then its summary: `voss baba feed '{"x":0}'`: drop food. */
214
+ export function Call({ of }: { of: string }): PromptNode {
215
+ const { d, kind } = entry(of, "Call");
216
+ return `\`${ctx("Call").program} ${of} ${quoted(exampleOf(d.args))}\`: ${d.summary || `${kind} of ${d.owner}`}`;
217
+ }
218
+ /** The usage line of a call as people write it: `voss baba feed <x> [--y]`. */
219
+ export function Usage({ of }: { of: string }): PromptNode {
220
+ const { d } = entry(of, "Usage");
221
+ return `\`${ctx("Usage").program} ${[of, ...usage(d.args, d.positional)].join(" ")}\``;
222
+ }
223
+ /** A call's args schema, and its result's, as JSON blocks. */
224
+ export function Schema({ of, result = false }: { of: string; result?: boolean }): PromptNode {
225
+ const { d } = entry(of, "Schema");
226
+ const show = (j: Json) => ({ $prompt: true, type: "pre", props: { lang: "json", children: JSON.stringify(j, null, 2) } } as PromptElement);
227
+ return [show(d.args), ...(result ? [show(d.result)] : [])];
228
+ }
229
+ /** The skills to read next: "See also: a, b." The names are checked to exist once every skill is rendered. */
230
+ export function See({ skills }: { skills: string[] }): PromptNode {
231
+ const c = ctx("See");
232
+ for (const name of skills) { if (!WORD.test(name)) throw new Error(`<See>: ${JSON.stringify(name)} is not a skill name`); c.refs?.push(name); }
233
+ return `See also: ${skills.join(", ")}.`;
234
+ }
235
+
236
+ /** Every call of a system, one `Call` line each, as a list. */
237
+ export function Calls({ of }: { of: string }): PromptNode {
238
+ const c = ctx("Calls");
239
+ const names = [...Object.entries(c.manifest.actions), ...Object.entries(c.manifest.queries)].filter(([, d]) => d.owner === of).map(([n]) => n);
240
+ if (!names.length) throw new Error(`<Calls of="${of}">: the system ${of} has no actions or queries`);
241
+ return { $prompt: true, type: "ul", props: { children: names.map((n) => ({ $prompt: true, type: "li", props: { children: Call({ of: n }) } })) } } as PromptElement;
242
+ }
@@ -0,0 +1 @@
1
+ export { jsx, jsxs, jsxDEV, Fragment, type JSX } from "./jsx-runtime.ts";
@@ -0,0 +1,49 @@
1
+ // The JSX runtime for promptware: elements that render to Markdown, not to
2
+ // the DOM. A file says `/** @jsxImportSource babavoss/prompt */` and writes
3
+ // `<p>`, `<ul>`, `<pre lang="ts">`, `<Call of="feed" />`; `render` turns the
4
+ // tree into text. Pure, no React.
5
+ export interface PromptElement { readonly $prompt: true; type: string | PromptComponent; props: Record<string, unknown> }
6
+ export type PromptNode = PromptElement | string | number | boolean | null | undefined | PromptNode[];
7
+ export type PromptComponent<P = any> = (props: P) => PromptNode;
8
+
9
+ export const Fragment: PromptComponent<{ children?: PromptNode }> = (p) => p.children ?? null;
10
+
11
+ export function jsx(type: string | PromptComponent, props: Record<string, unknown>, key?: unknown): PromptElement {
12
+ return { $prompt: true, type, props: key === undefined ? props : { ...props, key } };
13
+ }
14
+ export const jsxs = jsx;
15
+ export const jsxDEV = (type: string | PromptComponent, props: Record<string, unknown>, key?: unknown) => jsx(type, props, key);
16
+
17
+ export const isElement = (v: unknown): v is PromptElement => typeof v === "object" && v !== null && (v as PromptElement).$prompt === true;
18
+
19
+ type Children = { children?: PromptNode };
20
+
21
+ // What TypeScript admits in a promptware file: the Markdown tags, each with the props it renders.
22
+ export namespace JSX {
23
+ export type Element = PromptElement;
24
+ /** What a tag may be: a Markdown tag, or a component answering any node, text and lists included, not only an element. */
25
+ export type ElementType = keyof IntrinsicElements | PromptComponent;
26
+ export interface ElementChildrenAttribute { children: {} }
27
+ export interface IntrinsicElements {
28
+ /** A heading; `level` 1 to 4, 2 by default. */
29
+ h: Children & { level?: 1 | 2 | 3 | 4 };
30
+ p: Children;
31
+ ul: Children;
32
+ ol: Children;
33
+ li: Children;
34
+ /** Inline code. */
35
+ code: Children;
36
+ /** A code block; `lang` is the fence's language. */
37
+ pre: Children & { lang?: string };
38
+ b: Children;
39
+ i: Children;
40
+ a: Children & { href: string };
41
+ br: {};
42
+ hr: {};
43
+ blockquote: Children;
44
+ table: Children;
45
+ tr: Children;
46
+ th: Children;
47
+ td: Children;
48
+ }
49
+ }
@@ -0,0 +1 @@
1
+ declare module "*.mdx" { const prompt: import("./index.ts").Prompt; export default prompt; }
@@ -0,0 +1,183 @@
1
+ // Promptware to harness files, as text. Pure: the manifest, with the baba's
2
+ // context and skills rendered and its harness declaration, becomes what
3
+ // each output path, relative to the project, should hold.
4
+ //
5
+ // Claude Code CLAUDE.md .claude/skills/NAME/SKILL.md .claude/settings.json
6
+ // Codex AGENTS.md .agents/skills/NAME/SKILL.md
7
+ //
8
+ // Promptware is know-how; the contract teaches itself at the doors, the
9
+ // CLI's index and schemas and the MCP server's tools. The instruction file
10
+ // teaches the doors, true whatever the contract becomes; the table of calls
11
+ // is rendered only when the baba asks for it. The MCP registration is
12
+ // not promptware's: it is the machine's. The permissions, in a file the
13
+ // project shares with others, are a merge: voss owns only its entries.
14
+ import type { Json } from "../kernel/schema.ts";
15
+ import type { Manifest } from "../ecs/baba.ts";
16
+ import { usage } from "../shell/args.ts";
17
+ import { exampleOf } from "../ecs/handles.ts";
18
+ import { skillsOf, type SkillManifest } from "./define.ts";
19
+
20
+ /** The systems voss gives every baba: their calls are not the project's contract. */
21
+ export const VOSS = new Set(["promptware", "maker"]);
22
+
23
+ export type Harness = "claude" | "codex";
24
+ export const harnesses: Harness[] = ["claude", "codex"];
25
+
26
+ const files: Record<Harness, { instructions: string; skills: string }> = {
27
+ claude: { instructions: "CLAUDE.md", skills: ".claude/skills" },
28
+ codex: { instructions: "AGENTS.md", skills: ".agents/skills" },
29
+ };
30
+
31
+ /** Which harness an output path belongs to. */
32
+ export const harnessOf = (path: string): Harness => (path === "AGENTS.md" || path.startsWith(".agents/") || path.startsWith(".codex/") ? "codex" : "claude");
33
+
34
+ /** Where the promptware lives, relative to the project. */
35
+ export const SOURCE = ".baba/promptware";
36
+
37
+ // ---- merges ------------------------------------------------------------
38
+
39
+ /**
40
+ * A part of a file voss shares with others: an entry at a key of a JSON
41
+ * file, entries of a JSON list, or one table of a TOML file. sync.ts owns
42
+ * only the part, and holds the whole file when the part was edited by hand.
43
+ * The compiler asks only for the permissions' list now; the other kinds are
44
+ * what an older voss recorded, the MCP registration, and sync.ts still
45
+ * takes them out of the files once no longer wanted.
46
+ */
47
+ export type Merge =
48
+ | { kind: "json-key"; at: string[]; value: unknown }
49
+ | { kind: "json-list"; at: string[]; values: unknown[] }
50
+ | { kind: "json-lists"; lists: { at: string[]; values: unknown[] }[] }
51
+ | { kind: "toml-table"; table: string; body: string };
52
+
53
+ /** Output path to text, sorted by path. */
54
+ export type Outputs = Record<string, string>;
55
+ /** What the compiler wants: whole files, and the parts of shared ones. */
56
+ export interface Compiled { files: Outputs; merges: Record<string, Merge> }
57
+
58
+ // ---- the compiler --------------------------------------------------------
59
+
60
+ export interface CompileInput {
61
+ /** The words that reach the project, the first of every call: "voss baba". */
62
+ program: string;
63
+ /** Heads the instruction file; the program when absent. */
64
+ title?: string;
65
+ manifest: Manifest;
66
+ }
67
+
68
+
69
+ const byPath = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
70
+ const sorted = <T>(r: Record<string, T>) => Object.fromEntries(Object.entries(r).sort(([a], [b]) => byPath(a, b)));
71
+
72
+ /** Every output of every enabled harness. */
73
+ export function compile(input: CompileInput): Compiled {
74
+ const out: Outputs = {};
75
+ const merges: Record<string, Merge> = {};
76
+ const st = input.manifest.harness;
77
+ const systemSkills = skillsOf(input.manifest);
78
+ const put = (path: string, text: string) => {
79
+ if (path in out) throw new Error(`two skills render to ${path}`);
80
+ out[path] = text;
81
+ };
82
+ for (const h of harnesses) {
83
+ const hs = st[h];
84
+ if (!hs.enabled) continue;
85
+ out[files[h].instructions] = instructionFile(input);
86
+ for (const sk of systemSkills) {
87
+ put(`${files[h].skills}/${sk.name}/SKILL.md`, skillFile(input.program, sk, input.manifest));
88
+ }
89
+ // A golden interpretation stays in the manifest, for the trials its owner runs when they choose; it is not written
90
+ // beside the skill, so an agent reading the skill reads the skill, and nothing asks it to keep a suite in step.
91
+ if (h === "claude") {
92
+ // Voss's parts of the shared settings: the permissions, and one list per hook event, each entry by content beside anyone else's.
93
+ const lists: { at: string[]; values: unknown[] }[] = [];
94
+ const perms = st.claude.permissions;
95
+ if (perms.length) lists.push({ at: ["permissions", "allow"], values: [...new Set(perms)] });
96
+ const byEvent = new Map<string, unknown[]>();
97
+ for (const hk of input.manifest.promptware.hooks) {
98
+ const command = `${input.program} ${hk.action} @- --hook ${hk.on}${hk.strict ? " --strict=true" : ""}`;
99
+ const entry = { ...(hk.match ? { matcher: hk.match } : {}), hooks: [{ type: "command", command }] };
100
+ if (!byEvent.has(hk.on)) byEvent.set(hk.on, []);
101
+ byEvent.get(hk.on)!.push(entry);
102
+ }
103
+ for (const [on, values] of byEvent) lists.push({ at: ["hooks", on], values });
104
+ if (lists.length === 1 && lists[0]!.at[0] === "permissions") merges[".claude/settings.json"] = { kind: "json-list", at: lists[0]!.at, values: lists[0]!.values };
105
+ else if (lists.length) merges[".claude/settings.json"] = { kind: "json-lists", lists };
106
+ }
107
+ }
108
+ return { files: sorted(out), merges: sorted(merges) };
109
+ }
110
+
111
+ const system = (s: string) => s.replaceAll("|", "\\|").replaceAll("\n", " ");
112
+
113
+ /** JSON as one word of a POSIX shell: single-quoted. */
114
+ const quoted = (v: unknown) => `'${JSON.stringify(v).replaceAll("'", `'\\''`)}'`;
115
+
116
+ /** The CLI line of a call with a literal example of its args. */
117
+ const cliLine = (prog: string, name: string, args: Json) => `${prog} ${name} ${quoted(exampleOf(args))}`;
118
+
119
+ /** The shell around the baba's context: the header, the context, what a baba is and how to reach it at its doors, an example, the two systems voss adds, the skills by name. */
120
+ function instructionFile(input: CompileInput): string {
121
+ const prog = input.program;
122
+ const m = input.manifest;
123
+ const lines = [
124
+ `<!-- Generated by ${prog} from the baba's promptware. Do not edit: a hand edit is held and never overwritten. Edit the promptware; it syncs. -->`,
125
+ `# ${input.title ?? prog}`,
126
+ ];
127
+ const context = m.promptware.context?.trim();
128
+ if (context) lines.push("", context);
129
+ lines.push(
130
+ "", "## This baba", "",
131
+ "A baba is this project's own program: systems and apps over one state of entities and resources, moved by the actions its systems declare and read by their queries; the apps show it to people. Its code is under `.baba/`, and voss runs it.",
132
+ "",
133
+ `The contract is live: \`${prog}\` answers the index, every action and query with its system, kind, summary and an example, and \`${prog} NAME --schema\` one call's args and result schemas. Never rely on a copied list: a system added or changed shows up there within seconds.`,
134
+ "",
135
+ `- Call one with JSON: \`${prog} NAME '{"arg": value}'\` (flags exist for people). Every answer is one line of JSON; an error is one line on stderr, \`{"error":{"code","message","retry"}}\`, and the exit code says whose: 2 your arguments, 1 it failed, 3 time ran out, 4 gone.`,
136
+ `- An entity in an answer is a number: \`${prog} state N\` reads it, \`${prog} follow N [--until COMPONENT]\` waits on it.`,
137
+ `- \`${prog} run\` sends several calls in one turn: lines of \`{"NAME": args}\`, \`"$0.entity"\` to reference an earlier answer.`,
138
+ `- \`${prog} state\` reads the state; \`${prog} raw …\` writes it, only when no action does it. \`--help\` after any word explains it.`,
139
+ `- \`--key K\` on an action makes a retry safe: the baba remembers the answer for a day and answers it again without running. A secret is never on the command line: \`--token @-\` reads it from stdin. An answer over 20 000 characters goes to \`.baba/.voss/out/\` and the line says where.`,
140
+ "",
141
+ "If your harness has this baba's MCP server (`voss baba mcp`), its `manifest`, `call`, `read`, `follow`, `watch` and `state` tools are the same six operations: prefer them.",
142
+ "",
143
+ "Voss serves this baba from a service on loopback, `http://localhost:47802/` by default. `voss baba start` opens it there, installing the service if need be; `voss service status` gives the configured URL and whether it runs. Never guess another port: read the status.",
144
+ );
145
+ const first = Object.entries(m.actions).find(([, d]) => !VOSS.has(d.owner));
146
+ if (first) {
147
+ const [name, d] = first;
148
+ lines.push("", "For example, at the shell and by MCP:", "", "```", cliLine(prog, name, d.args), `call ${JSON.stringify({ name, args: exampleOf(d.args) })}`, "```");
149
+ }
150
+ const calls = Object.keys(m.actions).length + Object.keys(m.queries).length;
151
+ if (m.harness.table && calls) {
152
+ lines.push(
153
+ "",
154
+ "| Call | Kind | Of | What |",
155
+ "| --- | --- | --- | --- |",
156
+ ...Object.entries(m.actions).filter(([, d]) => !VOSS.has(d.owner)).map(([n, d]) => `| \`${prog} ${[n, ...usage(d.args, d.positional)].join(" ")}\` | action | ${d.owner} | ${system(d.summary)} |`),
157
+ ...Object.entries(m.queries).filter(([, q]) => !VOSS.has(q.owner)).map(([n, q]) => `| \`${prog} ${[n, ...usage(q.args, q.positional)].join(" ")}\` | query | ${q.owner} | ${system(q.summary)} |`),
158
+ );
159
+ }
160
+ lines.push("", "Voss adds two systems to this baba. `promptware` compiles what the baba tells agents, its context, skills and hooks written as code under `.baba/promptware/`, into each harness's files: `CLAUDE.md`, `AGENTS.md`, the skills, Claude Code's permissions and hooks. Those are generated: edit the promptware, never the outputs. `maker` keeps the specs, each system's `systems/NAME/spec.ts` beside it and the whole baba's `.baba/spec.ts`, each a seeded state with its scenarios and the adapters that play its outside, proved under `bun test` by `.baba/spec.test.ts`; `spec` lists them with their verdicts, `spec-run` runs a state's scenarios headless so the verdicts are remade, and the Maker app runs them live, where `spec-step` and `spec-read` reach the state a person has open.");
161
+ const skills = skillsOf(m).map((sk) => sk.name);
162
+ if (skills.length) lines.push("", `Skills: ${skills.join(", ")}.`);
163
+ return lines.join("\n") + "\n";
164
+ }
165
+
166
+ function yamlString(s: string): string {
167
+ const plain = /^[A-Za-z0-9(][^\n]*$/.test(s) && !/: |:$| #|^(true|false|null|yes|no|on|off|~)$/i.test(s);
168
+ return plain ? s : JSON.stringify(s);
169
+ }
170
+
171
+ /** A skill: frontmatter, the body, and the calls it uses, each as a CLI line with an example and its summary. */
172
+ function skillFile(prog: string, sk: SkillManifest, m: Manifest): string {
173
+ const lines = ["---", `name: ${sk.name}`, `description: ${yamlString(sk.description)}`, "---", "", sk.body.trim()];
174
+ if (sk.uses.length > 0) {
175
+ lines.push("", "## Calls", "");
176
+ for (const u of new Set(sk.uses)) {
177
+ const d = m.actions[u] ?? m.queries[u]!;
178
+ lines.push(`- \`${cliLine(prog, u, d.args)}\`: ${d.summary || `${u in m.actions ? "action" : "query"} of ${d.owner}`}`);
179
+ }
180
+ lines.push("", `Every answer is one line of JSON; \`${prog} NAME --schema\` gives a call's schemas.`);
181
+ }
182
+ return lines.join("\n") + "\n";
183
+ }
@@ -0,0 +1,16 @@
1
+ // What a baba tells agent harnesses, as data: its promptware rendered into
2
+ // the manifest; the kernel renders the files from it.
3
+ import type { Manifest } from "../ecs/baba.ts";
4
+
5
+ /** One skill as rendered: plain data, the actions and queries it uses by name. */
6
+ export interface SkillManifest { name: string; description: string; body: string; uses: string[] }
7
+
8
+ /** The skills of a manifest, by name, each checked to name actions and queries that exist. */
9
+ export function skillsOf(m: Manifest): SkillManifest[] {
10
+ const out: SkillManifest[] = [];
11
+ for (const [name, s] of Object.entries(m.promptware.skills).sort(([a], [b]) => a.localeCompare(b))) {
12
+ for (const u of s.uses ?? []) if (!(u in m.actions) && !(u in m.queries)) throw new Error(`skill ${name} uses ${u}, which is not an action or query of the baba`);
13
+ out.push({ name, description: s.description, body: s.body, uses: [...(s.uses ?? [])] });
14
+ }
15
+ return out;
16
+ }
@@ -0,0 +1,72 @@
1
+ // Promptware with the baba down: the shell reads the manifest the last load
2
+ // wrote, which carries the baba's promptware rendered and its harness, and
3
+ // runs the same compiler and the same plan the system runs.
4
+ import { join, dirname } from "node:path";
5
+ import { lstat, mkdir, rename, rm, rmdir } from "node:fs/promises";
6
+ import type { Manifest } from "../ecs/baba.ts";
7
+ import { compile, type CompileInput } from "./compile.ts";
8
+ import { plan, apply, report, type Files, type Plan, type Report, type Seen, type Status } from "./sync.ts";
9
+
10
+ /** The project's files through Bun: a symlink at a path, or at a directory below the root, holds it. */
11
+ export function diskFiles(root: string): Files {
12
+ return {
13
+ look: (path) => look(root, path),
14
+ write: (path, text) => writeAtomic(join(root, path), text),
15
+ remove: (path) => rm(join(root, path), { force: true }),
16
+ prune: (path) => rmdir(join(root, path)).catch(() => {}),
17
+ };
18
+ }
19
+
20
+ async function look(root: string, path: string): Promise<Seen> {
21
+ const parts = path.split("/");
22
+ for (let i = 1; i <= parts.length; i++) {
23
+ const sub = parts.slice(0, i).join("/");
24
+ let st;
25
+ try { st = await lstat(join(root, sub)); } catch { return { kind: "absent" }; }
26
+ if (st.isSymbolicLink()) return { kind: "blocked", reason: `symlink at ${sub}` };
27
+ if (i < parts.length && !st.isDirectory()) return { kind: "blocked", reason: `${sub} is not a directory` };
28
+ if (i === parts.length && !st.isFile()) return { kind: "blocked", reason: "not a regular file" };
29
+ }
30
+ return { kind: "file", text: await Bun.file(join(root, path)).text() };
31
+ }
32
+
33
+ async function writeAtomic(path: string, text: string): Promise<void> {
34
+ await mkdir(dirname(path), { recursive: true });
35
+ const tmp = join(dirname(path), `.promptware-${crypto.randomUUID()}`);
36
+ try {
37
+ await Bun.write(tmp, text);
38
+ await rename(tmp, path);
39
+ } catch (e) {
40
+ await rm(tmp, { force: true });
41
+ throw e;
42
+ }
43
+ }
44
+
45
+ /** What the compiler takes for the project at root. */
46
+ export function sources(_root: string, manifest: Manifest, title?: string): CompileInput {
47
+ return { program: "voss baba", ...(title !== undefined ? { title } : {}), manifest };
48
+ }
49
+
50
+ /** Compiles and plans against the project at root. Writes nothing. */
51
+ export async function planProject(root: string, input: CompileInput): Promise<Plan> {
52
+ return plan(diskFiles(root), compile(input), root);
53
+ }
54
+
55
+ /** Compiles and applies. */
56
+ export async function syncProject(root: string, input: CompileInput): Promise<Report> {
57
+ return apply(await planProject(root, input));
58
+ }
59
+
60
+ /** One output as compiled, whether or not it is held, with its status. */
61
+ export async function outputOf(root: string, input: CompileInput, path: string): Promise<{ path: string; status: Status; text: string }> {
62
+ const p = await planProject(root, input);
63
+ const text = p.wanted[path];
64
+ if (text === undefined) {
65
+ const e = p.entries.find((x) => x.path === path);
66
+ if (e) return { path, status: e.status, text: await Bun.file(join(root, path)).text().catch(() => "") };
67
+ throw new Error(`not an output of this project: ${path}; outputs: ${p.entries.map((x) => x.path).join(", ")}`);
68
+ }
69
+ return { path, status: p.entries.find((e) => e.path === path)!.status, text };
70
+ }
71
+
72
+ export { report };
@@ -0,0 +1,6 @@
1
+ // Markdown imported as text: `import guide from "./guide.md" with { type: "text" }`.
2
+ // Bun inlines the file; this tells the checker it is a string.
3
+ declare module "*.md" {
4
+ const text: string;
5
+ export default text;
6
+ }