diff --git a/AGENTS.md b/AGENTS.md index 02ee24be8..552283032 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,26 +93,41 @@ faculties), plus its `tests/`: `configSystemTwo(respond)` wires it, `useSystemTwo({ endpoints })` seeds the endpoint map - `system-one/` — TypeSafe/OpenRouter Decisions; `configSystemOne` + - `useSystemOne({ endpoint })`, with 429/529 retry + `useSystemOne({ endpoint })`, with 429/529 retry; `threads.ts` — the + admission judgment threads (the BP-native blocking judge over the Decisions + lane; the composition mounts it when systemOne is wired) and the supervision + threads (the runtime circuit breaker — the counting supervisor, its + block-then-judge verdict, and its recovery; `bProgram({ supervision })` + mounts the pack with systemOne when the host names watched types; a + root-mounted supervisor's block is global — one space's runaway loop halts + the watched type everywhere — while a space-stamped supervisor set + confines, expressible but not built) - `shell/` — bun-direct script execution — `run` op TS scripts via `bun run -`, - `shell` op Bun Shell commands through the wrapper; temp-file payloads over - ~100KB, deleted on every exit + `shell` op Bun Shell commands through the wrapper; `rpc` op generic remote + JSON-RPC (the remote-mcp layering is the threads, not the op); + temp-file payloads over ~100KB, deleted on every exit - `store/` — durable space-scoped persistence -- `mcp/` — remote MCP connections/sessions/auth; `keychain-oauth-provider.ts` - is the MCP OAuth `BunKeychain` over `Bun.secrets` plus the issuer-binding v2 - provider (faculty-only: nothing outside `mcp/` imports it) +- `security/` — the cross-cutting credential/policy faculty: + `keychain-oauth-provider.ts` is the issuer-bound OAuth `BunKeychain` over + `Bun.secrets` (SDK-free plain types in `security/types.ts`); the faculty + vends `credential_request` → `credential_result` (broker env-data first, + keychain floor second) — consumers are shell (remote MCP), system-two, ATProto - `frontier/` — the in-process embed — imported and driven by the composition; standalone spawns are a compatibility entry Each faculty owns its event types + input boundary; results echo the request `space`; op runners errors-as-data. **`src/tools/`** — deleted (fleet 0): the ICL conversion retired the CLI tool -fleet. mcp-client is the mcp faculty (`src/faculties/mcp/faculty.ts`); +fleet. Remote MCP is remote-mcp threads over the shell faculty's +generic `rpc` op (`src/faculties/shell/remote-mcp.threads.ts` — the retired +`mcp` faculty's replacement; the official SDK dependency is gone); skill/plugin operations are the shell faculty's threads -(`src/faculties/shell/threads.ts`) + recipes + store, taught by -`skills/skill-conventions/`. +(`src/faculties/shell/threads.ts` + `src/faculties/shell/plugin-threads.threads.ts` — +the plugin-thread proposal path: a host proposal → worker import → engine-ThreadSchema +validation → one `add_thread` candidate per validated export) + recipes + store, +taught by `skills/skill-conventions/`. **`src/faculties.ts`** — the faculties public surface (package export `./faculties`): the `Faculty` union, the wire types + JSON schemas/validators (`faculties.types.ts`), the override thread -threads (`shellThreads`, `mcpThreads`), their schemas/types, `useFaculty`, and the +threads (`shellThreads`), their schemas/types, `useFaculty`, and the System One/Two config surface (`configSystemOne`/`useSystemOne`, `configSystemTwo`/`useSystemTwo`) — what a `config.ts` imports to compose. (`facultiesThreads`, the default root threads, is internal.) The runtime composition itself is `src/cli/b-program.ts`. @@ -137,11 +152,29 @@ fleet (0 tools); turn/config commands land here as the composition rulings build JSON-RPC IPC host lives here too: `b-program.ts` (the runtime composition, `bProgram`), `json-rpc.ts` (the line codec), `serve.ts` (the `serve` entry — ingress messages → triggers, `ui_*` selections → client notifications, redacted -traces out), `load-config.ts` (`/config.ts`), and `trace-consumer.ts`. +traces out), `load-config.ts` (`/config.ts`), `trace-consumer.ts`, and +`plugin-thread-registry.ts` (the plugin-thread admission registry under `` — +host-local, keyed (plugin, file, content hash, space): `bProgram` mounts admitted +snapshots at boot, decided keys never re-adjudicate), and `ui-threads.ts` (the +`ui_*` producer threads — the view-generation policy: the standing design.md +scan → store tenant + artifact compile + render gate, plus the per-trigger +pipeline factory `uiPipelineThreads` (six once-threads minted by b-program's +pump on each `render` ingress — scale preflight, generation, `ui_render`, +the scoped `ui_style` serving seam, every correlation id per-trigger); mounted with shell + store + systemTwo, +composition territory — no process, not a faculty), and `ui-capture.ts` (the +autoresearch loop's capture side — the in-process lineage-keyed raw run +consumer + the frontier replay builder; the socket host wires its file sink +under `/captures`). **`src/utils/`** — shared pure utilities. **`src/faculties//threads.ts`** — faculty threads: `shell/threads.ts` (the ICL threads — skill/plugin scans, catalog/manifest schema gates, links dispatchers -+ stored recipes) and `mcp/threads.ts` (the auth replay spine). Threads ship with ++ stored recipes), `shell/rpc-auth.threads.ts` (the credential vend-and-replay +spine), `shell/remote-mcp.threads.ts` (the MCP layering over the rpc op), +`shell/plugin-threads.threads.ts` (the plugin-thread proposal path — dispatcher, +import join, candidate carry, add_thread dispatch), and +`system-one/threads.ts` (the admission judgment threads + the supervision +threads — the runtime circuit breaker, its judgment, and its recovery). +Threads ship with their faculty; `bProgram` mounts the faculty's threads when the faculty and its required faculties are on — except `faculties.threads.ts`, the composition's **root guard threads**, always mounted regardless of the allow-list. The diff --git a/bin/behavioral.ts b/bin/behavioral.ts index 0af6a594a..2ba6b4aca 100755 --- a/bin/behavioral.ts +++ b/bin/behavioral.ts @@ -21,6 +21,15 @@ export const runCli = makeCliRouter({ await init(args) }, }, + // The bare command is attach-or-start: attach to a running instance over + // /instance.sock, or start the foreground instance. --dev is a + // start-time flag: it configures the instance THIS process starts; an + // attaching process cannot flip a running instance. Lazy: --help and + // the subcommands must not load the composition graph until invoked. + default: async (args: string[]) => { + const { attachOrStart } = await import('../src/cli/attach-or-start.ts') + await attachOrStart({ dev: args.includes('--dev') }) + }, }) await runCli(Bun.argv) diff --git a/bun.lock b/bun.lock index 3a540fe36..44cf05564 100644 --- a/bun.lock +++ b/bun.lock @@ -11,8 +11,6 @@ "@biomejs/biome": "2.5.12", "@commitlint/cli": "21.2.2", "@commitlint/config-conventional": "21.2.2", - "@modelcontextprotocol/client": "^2.0.0", - "@modelcontextprotocol/server": "^2.0.0", "@types/bun": "^1.4.1", "@webref/css": "^8.7.4", "commitlint": "21.2.2", @@ -101,12 +99,6 @@ "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], - "@modelcontextprotocol/client": ["@modelcontextprotocol/client@2.0.0", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "jose": "^6.1.3", "pkce-challenge": "^5.0.0", "zod": "^4.2.0" } }, "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw=="], - - "@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.0.0", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA=="], - - "@modelcontextprotocol/server": ["@modelcontextprotocol/server@2.0.0", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0", "zod": "^4.2.0" } }, "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw=="], - "@simple-libs/child-process-utils": ["@simple-libs/child-process-utils@2.0.0", "", { "dependencies": { "@simple-libs/stream-utils": "^2.0.0" } }, "sha512-dvNoRKLijXnD0XoJAz94pbNuB5GQgDr55UhpSPhffDkTT0Cmcqh9jSCOtwfT2d4H6MI9E7c4SgtMuJXZ6F3c6A=="], "@simple-libs/stream-utils": ["@simple-libs/stream-utils@2.0.0", "", {}, "sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ=="], @@ -227,8 +219,6 @@ "cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@6.3.0", "", { "dependencies": { "jiti": "2.6.1" }, "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=9", "typescript": ">=5" } }, "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], "cz-conventional-changelog": ["cz-conventional-changelog@3.3.0", "", { "dependencies": { "chalk": "^2.4.1", "commitizen": "^4.0.3", "conventional-commit-types": "^3.0.0", "lodash.map": "^4.5.1", "longest": "^2.0.1", "word-wrap": "^1.0.3" }, "optionalDependencies": { "@commitlint/load": ">6.1.1" } }, "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw=="], @@ -253,10 +243,6 @@ "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - - "eventsource-parser": ["eventsource-parser@3.1.1", "", {}, "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ=="], - "expand-tilde": ["expand-tilde@2.0.2", "", { "dependencies": { "homedir-polyfill": "^1.0.1" } }, "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -337,8 +323,6 @@ "joi": ["joi@18.2.9", "", { "dependencies": { "@hapi/address": "^5.1.1", "@hapi/formula": "^3.0.2", "@hapi/hoek": "^11.0.7", "@hapi/pinpoint": "^2.0.1", "@hapi/tlds": "^1.1.1", "@hapi/topo": "^6.0.2", "@standard-schema/spec": "^1.1.0" } }, "sha512-2mD929bUVKUhOLQQEVhlf6EZ0Mlo0DeRb5MO7cViR9AXLtBauuccEtB1py9Ocxpo/P7ucnh442iY/iOwrh3IQw=="], - "jose": ["jose@6.2.12", "", {}, "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw=="], - "jq-wasm": ["jq-wasm@3.0.0-jq-1.8.2", "", {}, "sha512-jgWSEBJSd0lYR4Q5Fw8333MxQS5jCRI+g9KwAGL7yK1spwzTJy8C5uOS08wbpKE0Gz8oQYLlRpNLE/W70Ksp2g=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -393,14 +377,10 @@ "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], - "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], - "prettier": ["prettier@3.9.9", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -423,10 +403,6 @@ "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "sort-scripts": ["sort-scripts@1.0.1", "", {}, "sha512-58eys3wXg05rI51Gg/90Uvc0id0aboGLSzHm4nFvuD0MofSg/y8cyJ7ZqYuZ1eyj6AA8XwFTGaXA+6tApsMv4w=="], @@ -467,7 +443,7 @@ "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], @@ -489,8 +465,6 @@ "global-prefix/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - "global-prefix/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], - "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "inquirer/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], diff --git a/package.json b/package.json index ca659898c..51a173507 100644 --- a/package.json +++ b/package.json @@ -73,8 +73,6 @@ "@biomejs/biome": "2.5.12", "@commitlint/cli": "21.2.2", "@commitlint/config-conventional": "21.2.2", - "@modelcontextprotocol/client": "^2.0.0", - "@modelcontextprotocol/server": "^2.0.0", "@types/bun": "^1.4.1", "@webref/css": "^8.7.4", "commitlint": "21.2.2", diff --git a/skills/behavioral-tools/SKILL.md b/skills/behavioral-tools/SKILL.md deleted file mode 100644 index d44ebc330..000000000 --- a/skills/behavioral-tools/SKILL.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: behavioral-tools -description: Remote MCP operations for the behavioral agent via the mcp faculty — the mcp_request/mcp_request_result/mcp_cancel event wire (seven ops, typed authorization_required results, the auth replay spine) plus the auth/broker binding rules. The CLI tool fleet is retired: skills and plugins run through the skill-conventions skill (threads + recipes + store), git and raw shell belong to the shell faculty, HTML validation belongs to the controller floors + the classifier story , TypeScript LSP is a future faculty (TS 7.1 stable API). -license: ISC -compatibility: Requires bun and the behavioral CLI -allowed-tools: Bash ---- - -# Behavioral Tools - -Reference for the behavioral agent's compiled operator surfaces. As of the -ICL conversion, **every compiled surface is a faculty or the shell** — -the CLI tool fleet is retired: - -- **Remote MCP** is the mcp faculty: requests ride the behavioral - event wire (`mcp_request` / `mcp_request_result` / `mcp_cancel`), seven - ops, typed `authorization_required` results with request echo, auth bound - at the faculty's module scope (broker env-data + keychain floor — per-call - credentials are retired). See - [references/mcp-client.md](references/mcp-client.md). -- **Skills and plugins** (discovery, reading, frontmatter validation, link - extraction/validation) run through threads + the shell faculty (`bun run -`) - + the store — taught by the **skill-conventions** skill. -- **Git and raw shell** belong to the shell faculty (`shell_request`, bun-direct — - `run` op TS scripts, `shell` op Bun Shell commands). -- **HTML validation** belongs to the controller floors and the classifier - story (`src/controller/css.schemas.ts` doubles as classifier context — the - gate design is a local working doc, `.prompts/html-classifier-gate.md`). - -## Module references - -- [mcp-client](references/mcp-client.md) — the mcp faculty: wire, - ops, typed results, auth binding, the replay spine, composing. diff --git a/skills/behavioral-tools/references/mcp-client.md b/skills/behavioral-tools/references/mcp-client.md deleted file mode 100644 index ff5409ea6..000000000 --- a/skills/behavioral-tools/references/mcp-client.md +++ /dev/null @@ -1,66 +0,0 @@ -# mcp-client — the remote MCP faculty - -Remote MCP server operations are a spawned **faculty**, not CLI fleet tools: -`src/faculties/mcp/faculty.ts` (a spawned Bun process) holds the connections, -and the engine speaks to it over the behavioral event wire. The -`src/faculties/mcp/threads.ts` thread spine orchestrates cross-turn auth -replay. - -## The wire - -| Event | Detail | Direction | -|-------|--------|-----------| -| `mcp_request` | `{ id, op, input }` | program → faculty | -| `mcp_request_result` | `{ id, result }` | faculty → program | -| `mcp_cancel` | `{ id }` | program → faculty | - -The seven ops (`detail.op`): `discover`, `list-tools`, `call-tool`, -`list-prompts`, `get-prompt`, `list-resources`, `read-resource`. Each op -input carries the server `url` plus the op's own fields (`tool`/`args` for -call-tool, `name` for get-prompt, `uri` for read-resource) and an optional -`timeoutMs` wall-clock deadline for the whole call. - -## The result envelope - -`detail` is the uniform faculty result envelope — errors-as-data, never a -throw (`postResult` in `src/faculties/mcp/faculty.ts`): - -```json -{ "id": "…", "ok": true, "result": { "started": …, "output": … } } -{ "id": "…", "ok": false, "error": { "code": "…", "started": … } } -``` - -- `ok: true` — `result.output` carries the remote MCP data (loose; consumers - gate with their own `detailSchema`). -- `error.code = "authorization_required"` — the call hit a 401; `message` - holds the reason and `request` echoes `{ op, input }` (the replay spine's - capture payload). -- `error.code = "timeout" / "canceled"` — the two stop doors: the input - `timeoutMs` (default 30s) or an `mcp_cancel` mid-flight. -- `error.code = "error"` — invalid op input (the message names the AJV - errors) or a failed call/connection. - -## Auth - -Per-call input credentials are **retired** — the wire carries the server -URL only. Auth binds at the faculty's module scope: broker env-data -(`MCP_BROKER_URL` + `MCP_BROKER_BOOT_SECRET`, seeded by the spawning host) -with the OS-keychain floor beneath it. Neither yields a token → the call -goes unauthenticated → the server's 401 → typed `authorization_required`. - -## The auth replay spine (threads) - -An `authorization_required` result is captured in the store (`mcp-calls`, -keyed by call id, value = the echoed request), surfaced to the host as -`mcp_authorization_required { id, reason }` (the shell's "authorize X" -prompt), and after the host re-enters `mcp_authorization_granted { id }`, -the captured request is replayed and the capture deleted. Successful calls -never touch the store. - -## Composing - -Threads request `mcp_request` events like any other faculty; the composition -spawns it by default (`['bun', 'run', 'mcp/faculty.ts']` over stdio -lines — same wire, one JSON event per line). Schema-reflect the op inputs via -`src/faculties/mcp/types.ts` (`MCP_*_OP_INPUT_SCHEMA`) when model-facing -context is needed. diff --git a/skills/behavioral/SKILL.md b/skills/behavioral/SKILL.md index a296b0f94..da5b0f68f 100644 --- a/skills/behavioral/SKILL.md +++ b/skills/behavioral/SKILL.md @@ -2,11 +2,10 @@ name: behavioral description: > Behavioral-programming runtime and UI layer — b-threads, triggers, - listeners, the controller/custom-element protocol, SSR via stateless html - tools, frontier analysis, behavioral eval capture, and the design-system - spec. Use when creating, reading, updating, or deleting code where - @behavioral/sh is a declared dependency or where the work is in the - behavioral repo itself. + listeners, the super-step model, the faculties event wire, the controller + `ui_*` protocol, frontier analysis, and behavioral eval capture. Use when + creating, reading, updating, or deleting code where @behavioral/sh is a + declared dependency or where the work is in the behavioral repo itself. license: ISC compatibility: Requires bun and the behavioral CLI allowed-tools: Bash Read @@ -25,15 +24,12 @@ Use this skill when the task involves the behavioral **runtime** or **UI layer** and you're working in a project where `@behavioral/sh` is a declared dependency or in the behavioral repo itself. Specifically: -- Wiring **behavioral programs** — b-threads, triggers, `useTrace` - listeners, the super-step model, deadlock/livelock analysis. -- Building **custom elements** via the controller protocol, or - **server-side rendering** via the stateless html tools. +- Wiring **behavioral programs** — b-threads, triggers, + `useTrace` listeners, the super-step model, deadlock/livelock analysis. +- Building **custom elements** wired by the controller `ui_*` protocol + (browser Controller). - Capturing or grading **agent runs** (eval) — trace primitives and divergence analysis. -- Designing the **design-system spec** — a DESIGN.md derivative re-grounded - on Structural IA, CSS custom properties, and `@scope`/`:host()`/`::part()` - modes. The `behavioral` CLI is the entry (`bin/behavioral.ts`). The tools-fleet dispatcher is retired with the ICL conversion; run `behavioral --help` for @@ -46,17 +42,17 @@ only when the task calls for it. | When the task involves… | Read | |-------------------------|------| -| Behavioral programs — b-threads, `useAddThread`/`trigger`/`useTrace`, the super-step model, the action-channel pattern | [`references/behavioral.md`](./references/behavioral.md) | +| Behavioral programs — b-threads, `addThread`/`trigger`/`useTrace`/`step`, the super-step model, the action-channel pattern | [`references/behavioral.md`](./references/behavioral.md) | | Deadlock/livelock verification — frontier analysis over the closed state graph | [`references/frontier-analysis.md`](./references/frontier-analysis.md) | -| UI layer — the browser Controller protocol (`render`/`attrs`/`scale_check`, `ui_event`/`snapshot`/`error`/`success`/`scale_check_result`/`form_submit`) and the stateless SSR html tools | [`references/controller.md`](./references/controller.md) | +| UI layer — the browser Controller over the `ui_*` wire (`ui_render`/`ui_attrs`/`ui_scale_check`, `ui_event`/`ui_snapshot`/`ui_error`/`ui_success`/`ui_scale_check_result`/`ui_form_submit`) | [`references/controller.md`](./references/controller.md) | | Capturing/grading an agent run — eval trace primitives, divergence analysis | [`references/eval.md`](./references/eval.md) | -| Design-system spec — DESIGN.md derivative, Structural IA, custom properties, `@scope`/`:host()`/`::part()`, scale + affordances/feedback (in-progress consensus surface) | [`references/design-spec.md`](./references/design-spec.md) | -**Companion skills:** remote MCP operations are the **mcp faculty** -(`mcp_request` wire — see `behavioral-tools/references/mcp-client.md`); the -skill/plugin domain conventions (store tenants, scan recipes, `links_request`, -ICL composition) are the **skill-conventions** skill. This skill owns the -concepts; those skills own the operator contracts. +**Companion skills:** remote MCP operations are the **remote-mcp threads +over the shell faculty's generic `rpc` op +(`src/faculties/shell/remote-mcp.threads.ts`); the +skill/plugin domain conventions (store tenants, scan recipes, +`links_request`, ICL composition) are the **skill-conventions** skill. This +skill owns the concepts; those skills own the operator contracts. ## Repo conventions diff --git a/skills/behavioral/assets/DESIGN.md b/skills/behavioral/assets/DESIGN.md new file mode 100644 index 000000000..b1ed3d880 --- /dev/null +++ b/skills/behavioral/assets/DESIGN.md @@ -0,0 +1,354 @@ +--- +version: alpha +name: Behavioral (behavioral.sh) +description: > + Dual-theme design system and technical specification for behavioral.sh: + a sovereign agent harness and execution observer framework providing + multi-agent orchestration, telemetry inspection, memory visualization, and + autonomous node supervision. Basic theming — values only; component + styling emerges from generation, not component tokens. +omitted: + - section: components + reason: "basic theming — component styling emerges from generation (the ui_* producer threads derive structure from Structural IA; these tokens carry values only)" +brand: + name: behavioral.sh + product: behavioral.sh + domain: behavioral.sh + system_classification: "Sovereign Agent Harness & Execution Observer" + logo_mark: "B{ }" + logo_mark_description: "Capital letter B followed by curly brackets with explicit double spacing between brackets" + typography_font: "Ropa Sans" + font_specimen: "https://fonts.google.com/specimen/Ropa+Sans" + font_google_url: "https://fonts.googleapis.com/css2?family=Ropa+Sans:ital@0;1&display=swap" + weight_rule: "unweighted / regular rhythm (400 weight throughout, no aggressive heavy bolding); italics supported for annotations and agent prompts" + brand_anchor_gradient: "linear-gradient(135deg, #E2BAE0 0%, #FFC6CE 100%)" + brand_anchor_tokens: "Primary Lavender (#E2BAE0) → Tertiary Rose (#FFC6CE)" + aesthetic: "Technical, disciplined, notebook-inspired with muted violet and rose accents" +surfaces_texture: + pattern: "dotted-notebook" + dot_spacing: "24px" + dot_size: "1.5px" + implementation: | + background-color: light-dark(#FCF8FC, #110D11); + background-image: radial-gradient(color-mix(in srgb, light-dark(#755576, #E2BAE0) 12%, transparent) 1.5px, transparent 1.5px); + background-size: 24px 24px; + note: > + Dual-mode values use CSS light-dark(); derived values (the dot tint) use + color-mix() over the primary token rather than hardcoded rgba — theme + edits propagate to the texture. +typography: + display-lg: + fontFamily: "'Ropa Sans', sans-serif" + fontSize: "57px" + lineHeight: "64px" + letterSpacing: "-0.25px" + fontWeight: "400" + usage: "Runtime hero metrics, primary console display numbers" + display-md: + fontFamily: "'Ropa Sans', sans-serif" + fontSize: "45px" + lineHeight: "52px" + letterSpacing: "0px" + fontWeight: "400" + usage: "Telemetry counters, large status displays" + headline-lg: + fontFamily: "'Ropa Sans', sans-serif" + fontSize: "32px" + lineHeight: "40px" + letterSpacing: "0px" + fontWeight: "400" + usage: "Harness workspace titles, primary dashboard headings" + headline-md: + fontFamily: "'Ropa Sans', sans-serif" + fontSize: "28px" + lineHeight: "36px" + letterSpacing: "0px" + fontWeight: "400" + usage: "Inspector panels, section titles, modal headers" + headline-sm: + fontFamily: "'Ropa Sans', sans-serif" + fontSize: "24px" + lineHeight: "32px" + letterSpacing: "0px" + fontWeight: "400" + usage: "Agent cluster card titles, telemetry group headers" + title-lg: + fontFamily: "'Ropa Sans', sans-serif" + fontSize: "22px" + lineHeight: "28px" + letterSpacing: "0px" + fontWeight: "400" + usage: "Omnibar URI text, top navigation titles" + title-md: + fontFamily: "'Ropa Sans', sans-serif" + fontSize: "16px" + lineHeight: "24px" + letterSpacing: "0.15px" + fontWeight: "400" + usage: "Agent node status, session descriptors, table headers" + title-sm: + fontFamily: "'Ropa Sans', sans-serif" + fontSize: "14px" + lineHeight: "20px" + letterSpacing: "0.1px" + fontWeight: "400" + usage: "Subheadings, configuration keys, panel labels" + body-lg: + fontFamily: "'Ropa Sans', sans-serif" + fontSize: "16px" + lineHeight: "24px" + letterSpacing: "0.5px" + fontWeight: "400" + usage: "Agent trace logs, long-form execution telemetry" + body-md: + fontFamily: "'Ropa Sans', sans-serif" + fontSize: "14px" + lineHeight: "20px" + letterSpacing: "0.25px" + fontWeight: "400" + usage: "Standard console outputs, event logs, task descriptions" + body-sm: + fontFamily: "'Ropa Sans', sans-serif" + fontSize: "12px" + lineHeight: "16px" + letterSpacing: "0.4px" + fontWeight: "400" + usage: "Timestamps, metadata, memory addresses, node hashes" + label-lg: + fontFamily: "'Ropa Sans', sans-serif" + fontSize: "14px" + lineHeight: "20px" + letterSpacing: "0.1px" + fontWeight: "400" + usage: "Primary CTA buttons, interactive triggers" + label-md: + fontFamily: "'Ropa Sans', sans-serif" + fontSize: "12px" + lineHeight: "16px" + letterSpacing: "0.5px" + fontWeight: "400" + usage: "Filter chips, active state pills, mode toggles" + label-sm: + fontFamily: "'Ropa Sans', sans-serif" + fontSize: "11px" + lineHeight: "16px" + letterSpacing: "0.5px" + fontWeight: "400" + usage: "Telemetry badges, pulse indicators, status tags" +colors: + background: "light-dark(#FCF8FC, #110D11)" + surface: "light-dark(#FCF8FC, #110D11)" + surface-dim: "light-dark(#DED7DD, #110D11)" + surface-bright: "light-dark(#FFFFFF, #322A31)" + surface-container-lowest: "light-dark(#FFFFFF, #000000)" + surface-container-low: "light-dark(#F7F1F6, #171217)" + surface-container: "light-dark(#F2EDF0, #1E181E)" + surface-container-high: "light-dark(#EEE7EB, #241D24)" + surface-container-highest: "light-dark(#EAE1E6, #2B232B)" + on-surface: "light-dark(#1E151A, #F0E1EC)" + on-surface-variant: "light-dark(#564353, #B4A7B2)" + outline: "light-dark(#9D8B9B, #7D727C)" + outline-variant: "light-dark(#D5C1D2, #4E454E)" + primary: "light-dark(#755576, #E2BAE0)" + on-primary: "light-dark(#FFFFFF, #422644)" + primary-container: "light-dark(#F7CEF5, #674868)" + on-primary-container: "light-dark(#2B112E, #FFD7FD)" + secondary: "light-dark(#534152, #D7BFD5)" + on-secondary: "light-dark(#FFFFFF, #4C3B4C)" + secondary-container: "light-dark(#F4DBF1, #473647)" + on-secondary-container: "light-dark(#251726, #F4DBF1)" + tertiary: "light-dark(#86505A, #FFC6CE)" + on-tertiary: "light-dark(#FFFFFF, #4F222C)" + tertiary-container: "light-dark(#FFCFD5, #86505A)" + on-tertiary-container: "light-dark(#350E17, #FFCFD5)" + error: "light-dark(#BA1A1A, #FFB4AB)" + on-error: "light-dark(#FFFFFF, #690005)" + error-container: "light-dark(#FFDAD6, #93000A)" + on-error-container: "light-dark(#410002, #FFDAD6)" +rounded: + sm: "4px" + md: "8px" + default: "12px" + lg: "16px" + xl: "24px" + full: "9999px" +spacing: + xs: "4px" + sm: "8px" + md: "16px" + lg: "24px" + xl: "32px" + xxl: "48px" + gutter: "16px" + margin: "20px" +accessibility: + wcag_status: "WCAG 2.1 AA & AAA Compliant (audited pairs below; light-dark() preserves the audited values per mode)" + audited_contrast_pairs: + dark_primary_text: "15.2:1 (AAA Pass)" + dark_secondary_text: "8.1:1 (AAA Pass)" + dark_primary_action: "6.2:1 (AA / AAA Large Pass)" + dark_tertiary_accent: "7.4:1 (AAA Pass)" + light_primary_text: "16.1:1 (AAA Pass)" + light_secondary_text: "7.9:1 (AAA Pass)" + light_primary_action: "5.8:1 (AA Pass)" + light_tertiary_accent: "6.5:1 (AA Pass)" +--- + +# behavioral.sh — Design System Specification (DESIGN.md) + +> **Source of Truth:** This specification defines the visual identity, typography, dual-theme surface tokens, dotted-notebook background texture, and accessibility benchmarks for **behavioral.sh**. Basic theming: the frontmatter tokens are the normative values; component styling emerges from generation, not from this file. + +--- + +## 1. Brand Identity & Overview + +| Attribute | Specification | +|---|---| +| **Product & Domain** | `behavioral.sh` | +| **System Classification** | Sovereign Agent Harness & Execution Observer | +| **Primary Typography** | **Ropa Sans** (`font-family: 'Ropa Sans', sans-serif`) | +| **Font Specimen** | [Google Fonts: Ropa Sans](https://fonts.google.com/specimen/Ropa+Sans) | +| **Typographic Mark** | `B{ }` (Explicit double-space within brackets in Ropa Sans) | +| **Brand Anchor Gradient** | `#E2BAE0 → #FFC6CE` (Primary Lavender to Tertiary Rose) | +| **Surface Texture** | **Dotted Notebook Grid** (24px grid spacing, 1.5px dots) | +| **Default Corner Radius** | 12px (`rounded.default`) | + +--- + +## 2. Dotted Notebook Surfaces + +Both dark console and light workspace surfaces feature a disciplined 24px mathematical dot grid, evoking engineering notebooks and telemetry coordinate spaces. + +### 2.1 Dual-Mode Surface Texture + +```css +background-color: light-dark(#FCF8FC, #110D11); +background-image: radial-gradient(color-mix(in srgb, light-dark(#755576, #E2BAE0) 12%, transparent) 1.5px, transparent 1.5px); +background-size: 24px 24px; +``` + +The dot tint derives from the primary token via `color-mix()`, so a theme edit propagates to the texture with no separate maintenance. + +--- + +## 3. Typography: Ropa Sans + +**Ropa Sans** is the single source of typographic truth across all screens. To preserve technical discipline, typography is maintained in an **unweighted / 400 regular rhythm** without aggressive bolding; italics are supported for annotations and agent prompts. + +- **Google Font Import:** + ```html + + + + ``` + +- **Universal Application Rule:** + ```css + body, button, input, select, textarea, h1, h2, h3, h4, h5, h6, p, span, div, a, label, code, pre { + font-family: 'Ropa Sans', sans-serif !important; + } + ``` + +### Type Scale (Ropa Sans) + +| Token | Size | Weight | Line Height | Tracking | Usage | +|---|---|---|---|---|---| +| `display-lg` | 57px | 400 | 64px | -0.25px | Hero telemetry metrics, primary display figures | +| `display-md` | 45px | 400 | 52px | 0px | Secondary counters, harness cycle displays | +| `headline-lg` | 32px | 400 | 40px | 0px | Workspace & harness view headers | +| `headline-md` | 28px | 400 | 36px | 0px | Inspector panels, modal headers | +| `headline-sm` | 24px | 400 | 32px | 0px | Agent node cluster card headers | +| `title-lg` | 22px | 400 | 28px | 0px | Omnibar route text, main navigation tabs | +| `title-md` | 16px | 400 | 24px | 0.15px | Node status headers, agent labels | +| `title-sm` | 14px | 400 | 20px | 0.1px | Section subheadings, telemetry keys | +| `body-lg` | 16px | 400 | 24px | 0.5px | Agent memory logs, execution traces | +| `body-md` | 14px | 400 | 20px | 0.25px | Default console logs, task descriptions | +| `body-sm` | 12px | 400 | 16px | 0.4px | Timestamps, metadata, node hashes | +| `label-lg` | 14px | 400 | 20px | 0.1px | Primary interactive buttons, CTA elements | +| `label-md` | 12px | 400 | 16px | 0.5px | Filter chips, state pills, action triggers | +| `label-sm` | 11px | 400 | 16px | 0.5px | Telemetry badges, pulse indicators | + +--- + +## 4. Dual-Theme Surface Architecture & Contrast Compliance + +### 4.1 Dark Mode (Primary Console) + +| Token | Hex | Role & Mapping | Contrast Ratio | +|---|---|---|---| +| `surface` | `#110D11` | Primary console background | 15.2:1 against text | +| `surface-dim` | `#110D11` | Dimmed surface variant | — | +| `surface-bright` | `#322A31` | Brightened surface variant | — | +| `surface-container-lowest` | `#000000` | Lowest elevation layer | — | +| `surface-container-low` | `#171217` | Inset panels, statusbars | — | +| `surface-container` | `#1E181E` | Node cards, memory clusters | — | +| `surface-container-high` | `#241D24` | Elevated telemetry modules | — | +| `surface-container-highest` | `#2B232B` | Hover states, active layers | — | +| `primary` | `#E2BAE0` | Primary brand accent & active states | 6.2:1 on container | +| `on-primary` | `#422644` | High-contrast text on primary fill | 6.2:1 (AA / AAA Large) | +| `primary-container` | `#674868` | Secondary interactive fills | — | +| `on-primary-container` | `#FFD7FD` | Text on primary containers | — | +| `secondary` | `#D7BFD5` | Muted secondary accent | — | +| `on-secondary` | `#4C3B4C` | Text on secondary fill | — | +| `secondary-container` | `#473647` | Secondary container fills | — | +| `on-secondary-container` | `#F4DBF1` | Text on secondary containers | — | +| `tertiary` | `#FFC6CE` | Soft rose highlights, live AI spark | 7.4:1 on container | +| `on-tertiary` | `#4F222C` | Text on tertiary fill | — | +| `tertiary-container` | `#86505A` | Tertiary container fills | — | +| `on-tertiary-container` | `#FFCFD5` | Text on tertiary containers | — | +| `error` | `#FFB4AB` | Error accent | — | +| `on-error` | `#690005` | Text on error fill | — | +| `error-container` | `#93000A` | Error container fills | — | +| `on-error-container` | `#FFDAD6` | Text on error containers | — | +| `on-surface` | `#F0E1EC` | Primary console readable text | 15.2:1 (AAA Pass) | +| `on-surface-variant` | `#B4A7B2` | Subdued telemetry labels, timestamps | 8.1:1 (AAA Pass) | +| `outline` | `#7D727C` | Architectural borders | 4.8:1 (AA UI Pass) | +| `outline-variant` | `#4E454E` | Inset dividers & grid rules | — | + +### 4.2 Light Mode (Mirrored Workspace) + +| Token | Hex | Role & Mapping | Contrast Ratio | +|---|---|---|---| +| `surface` | `#FCF8FC` | Primary workspace canvas | 16.1:1 against text | +| `surface-dim` | `#DED7DD` | Dimmed surface variant | — | +| `surface-bright` | `#FFFFFF` | Brightened surface variant | — | +| `surface-container-lowest` | `#FFFFFF` | Lowest elevation layer | — | +| `surface-container-low` | `#F7F1F6` | Lowered canvas regions | — | +| `surface-container` | `#F2EDF0` | Node cards, container modules | — | +| `surface-container-high` | `#EEE7EB` | Elevated inspector panels, omnibar | — | +| `surface-container-highest` | `#EAE1E6` | High-contrast active borders | — | +| `primary` | `#755576` | Solid primary buttons & brand text | 5.8:1 (AA Pass) | +| `on-primary` | `#FFFFFF` | Text on solid primary actions | 5.8:1 (AA Pass) | +| `primary-container` | `#F7CEF5` | Muted chip and highlight backgrounds | — | +| `on-primary-container` | `#2B112E` | Text on primary containers | — | +| `secondary` | `#534152` | Muted secondary accent | — | +| `on-secondary` | `#FFFFFF` | Text on secondary fill | — | +| `secondary-container` | `#F4DBF1` | Secondary container fills | — | +| `on-secondary-container` | `#251726` | Text on secondary containers | — | +| `tertiary` | `#86505A` | Deep rose accent text and badges | 6.5:1 (AA Pass) | +| `on-tertiary` | `#FFFFFF` | Text on tertiary fill | — | +| `tertiary-container` | `#FFCFD5` | Tertiary container fills | — | +| `on-tertiary-container` | `#350E17` | Text on tertiary containers | — | +| `error` | `#BA1A1A` | Error accent | — | +| `on-error` | `#FFFFFF` | Text on error fill | — | +| `error-container` | `#FFDAD6` | Error container fills | — | +| `on-error-container` | `#410002` | Text on error containers | — | +| `on-surface` | `#1E151A` | Primary legible dark text | 16.1:1 (AAA Pass) | +| `on-surface-variant` | `#564353` | Secondary labels & property names | 7.9:1 (AAA Pass) | +| `outline` | `#9D8B9B` | Borders & card outlines | 4.6:1 (AA UI Pass) | +| `outline-variant` | `#D5C1D2` | Inset dividers & table borders | — | + +### 4.3 Dual-Mode & Derived Values + +Frontmatter color tokens carry dual-mode values via CSS [`light-dark()`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/light-dark) — one token, one custom property, the browser resolves the mode. Derived adjustments (hover tints, texture dots, translucent variants) use [`color-mix()`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/color-mix) over base tokens, never hardcoded rgba — a theme edit propagates to every derived value. + +### 4.4 Scale & Spacing Tokens + +| Group | Tokens | +|---|---| +| `rounded` | `sm` 4px · `md` 8px · `default` 12px · `lg` 16px · `xl` 24px · `full` 9999px | +| `spacing` | `xs` 4px · `sm` 8px · `md` 16px · `lg` 24px · `xl` 32px · `xxl` 48px · `gutter` 16px · `margin` 20px | + +--- + +*behavioral.sh · Ropa Sans & Dotted Notebook Specification v3.0.0* diff --git a/skills/behavioral/references/behavioral.md b/skills/behavioral/references/behavioral.md index 9c6157ac0..e526b3300 100644 --- a/skills/behavioral/references/behavioral.md +++ b/skills/behavioral/references/behavioral.md @@ -8,35 +8,41 @@ those matching any `block` are filtered out, the highest-priority remaining candidate is selected, threads waiting/requesting/interrupted by it are resumed, and the next step runs. If no unblocked candidate exists the program halts until an event arrives via `trigger` (the external admission -surface and the contentless internal kick). +surface and one super-step). ## Public surface The engine lives in-repo at `src/behavioral/behavioral.ts` (with types in `behavioral.types.ts`, constants in `behavioral.constants.ts`, utils in -`behavioral.utils.ts`). It is **not** a public package export — there is no -root `@behavioral/sh` export (the package exports only `./tools`, -`./controller`, `./utils`). Import it from its source path: +`behavioral.utils.ts`). It is **not** one of the package's public exports — +`@behavioral/sh` exports `.` (the `defineConfig` config helper, via +`src/main.ts`), `./faculties` (the faculty wire and config surface), +`./controller`, and `./utils`; none re-export the engine. Import it from its +source path: ```ts import { behavioral } from '../../behavioral/behavioral.ts' import type { + AddThread, BPEvent, Disconnect, Thread, Trace, - UseAddThread, UseTrace, } from '../../behavioral/behavioral.types.ts' ``` -`behavioral()` returns a frozen API object with **three hooks** — no +`behavioral()` returns a frozen API object with **five members** — no `useAddHandler`, no `sendTrace`, no generic type parameter: ```ts -const { useAddThread, trigger, useTrace } = behavioral({ instanceId?: string }) +const { addThread, trigger, useTrace, step, instanceId } = behavioral({ sessionId?: string }) ``` +The optional `sessionId` is host-supplied session identity stamped on every +trace alongside the self-minted `instanceId` (the host layer owns session +identity policy); absent it defaults to the `instanceId`. + Threads are JSON objects: `{ label: string, rules: Idioms[], once?: true }`. Each idiom is one sync point with `request` (propose an event), `waitFor` (block until an event), `block` (forbid an event), `interrupt` (terminate the @@ -44,20 +50,21 @@ thread on an event), and/or `transform` (match, hand off to external reshaping, re-enter via a `target` event). `detailSchema` on listeners is JSON Schema (draft 2020-12), compiled at registration. -## The three hooks +## The API surface -`const { useAddThread, trigger, useTrace } = behavioral()` +`const { addThread, trigger, useTrace, step, instanceId } = behavioral()` -| Hook | Signature | Use when | -|------|-----------|----------| -| `useAddThread(space?)` | `(args: Thread) => void` | Register a b-thread (`{ label, rules, once? }`). Optional `space` stamps all the thread's idioms. Inert — does not start a super-step. | +| Member | Signature | Use when | +|--------|-----------|----------| +| `addThread(args)` | `(args: Thread) => void` | Register a b-thread (`{ label, rules, once?, space? }`). The thread's optional `space` field stamps all its idioms (applied at registration). Inert — does not start a super-step. | | `trigger(event)` | `(event: BPEvent) => void` | Inject an external event (the event carries `space`; absent = root). Triggered candidates carry `ingress: true`, have highest priority (0), and can be blocked. Initiates a super-step. | -| `useTrace(listener)` | `Disconnect` | Observe internal state traces emitted after each event selection. Does not affect execution. | +| `useTrace(listener)` | `(listener) => Disconnect` | Observe internal state traces emitted after each event selection. Does not affect execution. | +| `step()` | `() => void` | Pump one super-step. The internal re-entry primitive — `addThread` alone is inert, so every re-entry pairs the thread addition with a `step()` pump. | +| `instanceId` | `string` | The per-process identity the engine self-mints and stamps on every trace. Exposed so a host can hand the identity to its clients without sniffing the trace wire (silent on an idle instance). | -### `useAddThread` — registering threads +### `addThread` — registering threads ```ts -const addThread = useAddThread() addThread({ label: 'producer', rules: [{ request: { type: 'task' } }], @@ -71,9 +78,10 @@ addThread({ ``` A thread is an object with `label`, `rules` (an array of `Idioms` sync points), -and optional `once`. Without `once`, the thread loops its `rules` indefinitely; -with `once: true`, it runs through the rules once and completes. The `label` -identifies the thread in traces. Invalid thread arguments (failing +and optional `once` and `space`. Without `once`, the thread loops its `rules` +indefinitely; with `once: true`, it runs through the rules once and completes. +The `label` identifies the thread in traces; the `space` stamps its idioms +(absent = root — see Space matching below). Invalid thread arguments (failing `ThreadSchema`, or an un-compilable `detailSchema`) are surfaced as an `add_thread_error` trace, not a throw — the thread simply isn't added. @@ -90,15 +98,16 @@ priority 0, stamped `ingress: true`. They are subject to `block` like any request. An event that fails `BPEvent` validation is rejected at the ingress boundary and surfaced as a `trigger_error` trace (not a throw), echoing the attempted `space` when present. `trigger` is how external systems (UI, -network, timers) drive the program — and also how internal re-entry starts a -super-step (the contentless kick; see the action channel). +network, timers) drive the program — it is external admission plus one +super-step, nothing else. Internal re-entry never uses `trigger`: it adds a +request thread and pumps a super-step with `step()` (see the action channel). #### The channel invariant A selected event carries `ingress: true` **iff** it was admitted externally -via `trigger`; everything internal arrives as a thread request added through -`useAddThread` (the dispatch bridge, the transform daemon, -`threads.registered`). Listeners opt into a channel with the optional +via `trigger`; everything internal (satellite results, transform +targets, admitted threads) arrives as a thread request added through +`addThread`. Listeners opt into a channel with the optional `ingressMatch` field: | listener field | value | matches | @@ -122,13 +131,31 @@ idioms — `waitFor`, `block`, `interrupt`, `transform` — share this one match seam, so both flags apply uniformly. `block` with `ingressMatch: true` is how backpressure on external events is expressed. +#### Space matching + +Space scoping is **root authority** — visibility flows UP only. A thread's +optional `space` stamp (absent = root) applies to all its idioms: + +| listener | candidate event | matches? | +|----------|-----------------|----------| +| root (unstamped) | any space | yes — a root listener sees every space | +| space `s1` | `s1` | yes | +| space `s1` | root or a sibling space | no — a stamped listener never escapes its space | + +A root thread can wait on, block, interrupt, and transform candidates in +every space; a space-stamped thread is confined to its own — a thread +governing several spaces is admitted per space explicitly, each mount +stamped. Requests are emissions, not observations: a thread's request bids +only in its own space (root requests bid in root). The `transform` target +re-enters stamped with the **source event's** space — a root transformer's +output stays in the space it observed. + ### `useTrace` — observation and the action channel ```ts const disconnect = useTrace((msg: Trace) => { - // msg is the engine's closed Trace union — narrow by `kind`: - // 'pending_bids' | 'frontier' | 'selection' | 'deadlock' - // 'trigger_error' | 'add_thread_error' | 'interrupt' | 'transform' + // msg is the engine's closed Trace union — narrow by `kind` + // (12 kinds; the full table below). }) ``` @@ -136,33 +163,27 @@ const disconnect = useTrace((msg: Trace) => { may be sync or async (`void | Promise`); **the engine never awaits it.** Each listener return value is absorbed by `Promise.resolve(...)` with a rejection handler attached, so a rejecting promise never breaks the -super-step. A listener that throws synchronously is caught and logged via -`console.error('[behavioral] trace listener ...')` — listener failures are -**log-only**, never published as traces. +super-step. A listener failure (a sync throw or a rejecting promise) is +caught and logged via `console.error('[behavioral] trace listener ...')` — +listener failures are **log-only**, never published as traces, and the catch +is per-consumer (one failing listener cannot suppress the others). #### The action-channel pattern (replaces `useAddHandler`) There is no `useAddHandler` hook. Side effects — tool dispatch, I/O, model calls — are performed by `useTrace` listeners that observe `selection` traces and act outside the super-step, then **re-enter the engine as a -thread**. The kernel's dispatch bridge is the canonical implementation -(`src/kernel/kernel.ts`): +thread**. The canonical host is the composition (`src/cli/b-program.ts` — +the runtime composition `bProgram`): its pump subscribes `useTrace`, routes +selected events to their faculty lanes, and re-enters results through one +seam: ```ts -// The action channel: fire on selection, do async I/O, re-enter via a -// once-thread + kick. -const disconnect = useTrace((msg) => { - if (msg.kind !== 'selection') return - void bridge(msg.selected.type) // async I/O outside the super-step -}) - -// Re-entry is deferred past the current super-step so the bridge never -// re-enters the engine synchronously from inside a listener. -const reenter = (event: BPEvent): void => { - queueMicrotask(() => { - addThread(createReentryThread(event)) // label: `reentry:`; once: true - trigger({ type: KICK_EVENT_TYPE, space }) // contentless kick - }) +// The in-process re-entry law: addThread alone is inert — every re-entry +// pumps one super-step. +const addThreads = (threads: Thread[]): void => { + for (const thread of threads) addThread(thread) + step() } ``` @@ -170,20 +191,18 @@ The contract: - The listener filters on `msg.kind === 'selection'` and reads `msg.selected.type` to decide what to do. -- Async work happens **after** the listener returns — the engine continues the - super-step without waiting. +- Async work happens **after** the listener returns — the engine never awaits + a listener, so the super-step continues without waiting. - Results re-enter by **adding a `once` thread that `request`s the event**, - then firing the contentless `KICK_EVENT_TYPE` kick. `useAddThread` is inert, - so the kick is what starts the super-step. The kick is priority 0, carries - no detail, and nothing may listen for/wait on/block/transform it — an - external actor triggering it is harmless by construction. The re-entry - event therefore arrives as a **request-origin** candidate (`ingress` - absent), which is what lets `ingressMatch: false` listeners match internal - results while `ingressMatch: true` listeners stay external-only. -- Both calls are deferred with `queueMicrotask` so the action channel never - re-enters the engine synchronously from inside a `sendTrace` listener call. -- Tool/I/O failures return as **data** (`isError: true` on the output) and - drive a `turn.end` or recovery trigger — they never throw into the space. + then pumping one super-step (the re-entry law above). The composition's + `useFaculty` pump funnels every satellite's result threads through the same + seam, and engine-internal code (the transform executor) calls the internal + `step()` directly. The re-entry event therefore arrives as a + **request-origin** candidate (`ingress` absent), which is what lets + `ingressMatch: false` listeners match internal results while + `ingressMatch: true` listeners stay external-only. +- Faculty/I/O failures return as **data** on the result event and re-enter + through the same seam — they never throw into the space. - A listener throw is `console.error`'d and swallowed — it cannot corrupt the program. @@ -208,7 +227,7 @@ addThread({ transform: [{ type: 'order', // match this selected event detailSchema: { ... }, // optional JSON Schema guard - query: '.order', // applied to selected.detail (e.g. a jq expression) + query: '.order', // applied to selected.detail (a jq expression) target: 'ship', // re-enter the engine with this event type }], }], @@ -218,51 +237,62 @@ addThread({ Shape (`TransformListener` in `behavioral.types.ts`): a `BPListener` plus `query` (string) and `target` (string). When a matching event is selected, the engine emits a `transform` trace carrying `transformers: { query, target, -thread }[]` **immediately before** the `selection` trace, then resumes the -thread (a transform match wakes the thread like a `waitFor` match). The -engine does **no I/O** — it only publishes the contract. External code reads -the `transform` trace, evaluates each `query` over `selected.detail`, and -re-enters by adding a `once` thread that `request`s `{ type: target, detail }` -(one per target when fanning out) and firing the contentless kick — never via -`trigger`, so the target stays request-origin. - -This is a two-phase loop: **prime** (the `transform` trace carries the -contracts) then **execute** (the immediately following `selection` trace -carries the payload). The reference implementation is -`src/behavioral/tests/transform.spec.ts`. Honest caveat: today only that test -loop consumes the trace — the kernel-side consumer is not yet wired, so there -is no production host applying `query` → `target` yet. The trace contract is -stable; the host is what's missing. +thread, space? }[]` **immediately before** the `selection` trace, then resumes +the thread (a transform match wakes the thread like a `waitFor` match) and +applies the contract itself: each `query` is evaluated over `selected.detail` +by the engine's internal jq subprocess (`src/behavioral/jq.worker.ts`, driven +by the `evaluateTransform` bridge in `behavioral.utils.ts`), and the result +re-enters as a `once` thread requesting `{ type: target, detail: result.value }` +stamped with the **source event's** space (a stamped contract's space equals +the event's — stamped confinement; a root transformer's output stays in the +space it observed) — the target stays request-origin. The +engine does no arbitrary I/O; its only external dependency is the jq binary. + +Failures are errors-as-data: a contract that fails (jq error, no detail, +empty or non-object output) never fires its target — the failure surfaces as +a `transform_error` trace carrying the `transformer` and a machine-readable +`reason`. + +The contract test is `src/behavioral/tests/transform.spec.ts`, and the +production consumer is real: the remote-mcp threads +(`src/faculties/shell/remote-mcp.threads.ts`) drives its entire +discover/tools/call pipeline with transforms over the shell faculty's `rpc` +op. ## The trace union -`Trace` is a closed discriminated union (narrow by `kind`). The kinds: +`Trace` is a closed discriminated union (narrow by `kind`) — the 12 kinds of +`TRACE_MESSAGE_KINDS` (`src/behavioral/behavioral.constants.ts`): | `kind` | Carries | When | |--------|---------|------| +| `step` | `step`, `ingress?` | A super-step began; `ingress: true` marks an externally initiated step | | `pending_bids` | `step`, `threads` (serialized pending set) | Before event selection each step | | `frontier` | `step`, `status`, `candidates`, `enabled` | After computing the frontier | | `selection` | `step`, `selected` (the chosen candidate) | When an event is selected | +| `idle` | `step` | No candidates at all — the program is quiescent (not deadlocked); the settle signal | | `deadlock` | `step` | Candidates exist but all are blocked | +| `thread_added` | `thread` (the full validated `Thread`) | `addThread` registered a thread — the provision record; replay = `thread_added` payloads + ingress events in order | | `interrupt` | `selected`, `threadLabel`, `step` | A thread was terminated by an interrupt | -| `transform` | `step`, `transformers` | A transform listener matched; external code applies `query` → `target` | -| `add_thread_error` | `error` (AJV errors), `space?` | `useAddThread` rejected invalid args / un-compilable `detailSchema` | +| `transform` | `step`, `transformers` | A transform listener matched; the engine applies `query` → `target` in-engine | +| `transform_error` | `step`, `transformer`, `reason`, `stderr?`, `exitCode?` | A transform contract failed (jq error, no detail, empty or non-object output); the target never fires | +| `add_thread_error` | `error` (AJV errors), `space?` | `addThread` rejected invalid args / un-compilable `detailSchema` | | `trigger_error` | `error` (AJV errors), `space?` | `trigger` rejected an invalid `BPEvent` | -The two error kinds are the engine's only failure surfaces, and both are +The three error kinds are the engine's failure surfaces, and all are **traces, not throws** — invalid input is reported as data and the program keeps running. There is no `feedback_error` trace. ## A common wiring mistake to avoid -Forgetting to start the super-step after adding threads. `useAddThread` +Forgetting to start the super-step after adding threads. `addThread` registers a thread but does **not** start a super-step on its own; the program pauses until an event enters via `trigger`. A common symptom: threads are added, nothing happens. For an external event, trigger it; for internal re-entry (a result or a transform target), add the `once` request thread and -fire the contentless kick. This inertness is deliberate: pure-requesting -programs (tic-tac-toe, water) do not self-start at registration, so quiescence -is preserved until someone admits an event. +pump a super-step with `step()`. This inertness is deliberate: +pure-requesting programs (tic-tac-toe, water) do not self-start at +registration, so quiescence is preserved until someone admits an event. The second common mistake: expecting side effects to fire on `trigger`. The action channel fires on **selected** events — a triggered event that is @@ -274,5 +304,5 @@ event wasn't filtered out. - [Frontier analysis](./frontier-analysis.md) — deadlock/livelock verification over the closed state graph of a behavioral program. -- [Controller](./controller.md) — the browser-side message applier and the - stateless SSR html tools (one UI-layer reference). +- [Controller](./controller.md) — the browser-side message applier over the + `ui_*` wire (the UI-layer reference). diff --git a/skills/behavioral/references/controller.md b/skills/behavioral/references/controller.md index 5b28ebece..c1457ef06 100644 --- a/skills/behavioral/references/controller.md +++ b/skills/behavioral/references/controller.md @@ -1,19 +1,15 @@ -# UI Layer — Controller and html tools +# UI Layer — the Controller Reference for an agent assisting an engineer in wiring up the UI layer of a -behavioral app. There are two surfaces, both driven by a -[behavioral program](./behavioral.md)'s `selection` listeners (the action -channel): +behavioral app. The live surface is the browser **`Controller`** — it applies +`ui_*` wire messages to a **live DOM** over a WebSocket (or an injected +`Transport`). It is driven by a [behavioral program](./behavioral.md)'s +`selection` listeners (the action channel). -- **Browser `Controller`** — applies `render`/`attrs` (plus - `dispatch_custom_event`/`navigate`/`scale_check`) to a **live DOM** over a - WebSocket. -- **Stateless html tools** — apply `render`/`attrs` to an **HTML string** in - memory, in a Bun process (SSR). - -There is **no `Renderer` class** — SSR is stateless html-in / html-out tools. -The two surfaces share the same `render`/`attrs` vocabulary; the substrate -(live DOM vs string) is the variable. +The compiled SSR html tools are **retired** (the ICL conversion removed the +fleet). There is **no `Renderer` class** and no server-side html surface — the +`ui_*` vocabulary's server side is emitted by the agent's behavioral program, +and its browser side is the Controller below. ## Browser Controller @@ -34,6 +30,7 @@ new Controller({ onPageSwap, // page swap callback onPageHide, // pagehide callback onPageShow, // pageshow callback + transport, // optional injected Transport (default: built-in WebSocket carrier) }) ``` @@ -41,34 +38,47 @@ new Controller({ This is the load-bearing concept: a behavioral page is **push-based**, not pull-based. The controller does not fetch state and render client-side; it -opens a WebSocket to its serving agent and applies server-pushed messages: +opens a carrier to its serving agent and applies server-pushed `ui_*` +messages: -| Server → browser (`CONTROLLER_INCOMING_MESSAGE_TYPES`) | What the Controller does | -|----------------------------------------------------------|---------------------------| -| `render` | Apply HTML to `[b-target]` elements per the `swap` mode | -| `attrs` | Set/remove attributes on `[b-target]` elements | -| `dispatch_custom_event` | Fire a `CustomEvent` on the target | -| `navigate` | Navigate the page (URL change) | -| `scale_check` | Resolve the effective `b-scale` for a target and reply with `scale_check_result` | +| Agent → browser (`CONTROLLER_INCOMING_MESSAGE_TYPES`) | What the Controller does | +|-------------------------------------------------------|--------------------------| +| `ui_render` | Apply HTML to `[b-target]` elements per the `swap` mode | +| `ui_attrs` | Set/remove attributes on `[b-target]` elements | +| `ui_dispatch_custom_event` | Fire a `CustomEvent` on the target | +| `ui_navigate` | Navigate the page (URL change) | +| `ui_scale_check` | Resolve the effective `b-scale` for a target and reply with `ui_scale_check_result` | +| `ui_style` | Apply scoped CSS (`@scope` block, verbatim) to the target's subtree — one idempotent style element per target | -User interactions and page lifecycle emit messages back to the agent: +User interactions and page lifecycle emit `ui_*` messages back to the agent: | Browser → agent (`CONTROLLER_OUTGOING_MESSAGE_TYPES`) | When | -|---------------------------------------------------------|------| +|-------------------------------------------------------|------| | `ui_event` | A `b-trigger` declaration fired (DOM event → BP event with `getAttributes` detail) | -| `snapshot` | A page lifecycle event (`pagereveal`/`pageswap`/`pagehide`/`pageshow`) — serialized HTML via `getHTML({ serializableShadowRoots: true })` | -| `success` | A server message was applied successfully (carries the request `id`) | -| `error` | A message handler threw (carries `name`, `error`, `stack`, `id`) | -| `scale_check_result` | Reply to a `scale_check` message (carries `effectiveScale`) | -| `form_submit` | A `b-form` form POST completed | +| `ui_snapshot` | A page lifecycle event (`pagereveal`/`pageswap`/`pagehide`/`pageshow`) — serialized HTML via `getHTML({ serializableShadowRoots: true })` | +| `ui_success` | A server message was applied successfully (carries the request `id`) | +| `ui_error` | A message handler threw (carries `name`, `error`, `stack`, `id`) | +| `ui_scale_check_result` | Reply to a `ui_scale_check` message (carries `effectiveScale`) | +| `ui_form_submit` | A `b-form` form POST completed | The agent — running a behavioral program — is the source of truth for what the page shows; the Controller is the DOM applier. -## Server-side html (the floors + classifier story) +The kind names are `keyMirror` constants in +`src/controller/controller.constants.ts` +(`CONTROLLER_INCOMING_MESSAGE_TYPES` / `CONTROLLER_OUTGOING_MESSAGE_TYPES`). + +### The schema home + +`CONTROLLER_DETAIL_SCHEMAS` (`src/controller/controller.schemas.ts`) maps +every `ui_*` kind to its AJV detail schema — the guard/reflection home. The +host threads import it (`validateControllerDetail`) to gate controller +messages at the composition boundary. The browser bundle **never** carries +the compiled validators — it ships only the deterministic floors below. -The compiled SSR html tools are **retired** (HTMLRewriter was Bun-only, dead -in both target hosts; the ICL conversion removed the fleet). What survives: +### The floors + classifier story + +What keeps the wire safe across every host: - **Deterministic floors** — `isInvalidTrigger` / `detectXssVectors` (`src/controller/controller.utils.ts`): hardcoded invariants (empty @@ -77,15 +87,144 @@ in both target hosts; the ICL conversion removed the fleet). What survives: - **Schemas as data** — `src/controller/html.schemas.ts` + `css.schemas.ts`: pure JSON-schema data (the classifier's context, not compiled validators). - **The classifier ceiling** — the System One/Jev gate story: - - — probabilistic admission over the schema context, with the floors as the + probabilistic admission over the schema context, with the floors as the deterministic backstop. Probabilistic gates never own security invariants. -## When to use which +## The ui_* producer threads + +Nothing above emits `ui_*` on the agent side by itself — the view-generation +policy is composition threads: `src/cli/ui-threads.ts`, mounted by `bProgram` +when shell + store + systemTwo are all on (absent systemTwo there is no +generation lane and the threads don't mount). The shape is a dispatcher + +per-trigger pipelines: a STANDING set (the boot design scan, the tenant + +artifact compile, the render gate) plus, on each `render` ingress, one MINTED +pipeline — `uiPipelineThreads({ id, detail })`, six once-threads added by the +composition's host leg (b-program's pump, the admission-path precedent) — so +concurrent triggers interleave without dropping and every correlation id is +per-trigger (`-scale`/`-tenant`/`-gen`/`-render`/`-style`, label +`ui/pipeline:/`). The vertical is the same — ingress `ui_event` → +scale preflight → generation → `ui_render` — refined by the autoresearch +loop, not by argument. + +### The design tenant (DESIGN.md → store) + +At boot a scan recipe (the shell faculty's `run` op, the SKILL.md +fence-slicing + `YAML.parse` contract) reads the USER'S `/DESIGN.md` — +[Google's DESIGN.md format](https://github.com/google-labs-code/design.md): +YAML frontmatter token groups plus `##` prose sections — and lands it in the +store as the `design` collection's `context` value: +`{ tokens, sections, warnings }` (warnings-as-data). **The no-lock contract**: +the shipped asset (`skills/behavioral/assets/DESIGN.md`) is an init-copied +seed only — the runtime never reads the asset, never re-syncs it; a user who +edits, replaces, or deletes their home file fully controls (or removes) their +design context. The design lane is an optional input, never a gate: with no +tenant (or a null-tokens tenant) generation proceeds plain and still produces +a conforming `ui_render`. + +Consumption is lenient per the format's consumer table: unknown frontmatter +groups and section headings ride verbatim; the spec-named groups +(`colors`/`typography`/`rounded`/`spacing`) validate by shape with a bad +group dropping to a warning; a duplicate `##` section heading rejects the +file (tokens and sections null, the rejection riding the warnings). A missing +`DESIGN.md` is not an error — no tenant, no warnings. + +### The scale preflight + +A `render` trigger (the b-trigger convention — a `ui_event` whose inner BPEvent +has type `render`, optionally carrying `detail.target`) drives the pipeline's +scale legs: the minted set requests `ui_scale_check` under its per-trigger id +and the scale-join once-thread composes the generation request (`generate`, a +thread-owned event — never a `ui_*` wire message) when the correlated +`ui_scale_check_result` re-enters. The join is the echoed `id` (the controller +wire carries no ctx); a result with a foreign id joins nothing. The effective +scale and target are stamped into the generation request's `ctx` — +host-supplied, never model-facing — and the TRIGGER's own detail rides as the +request's `request` field: the user's content, model-facing by right (it +composes the systemTwo user message). + +Without a browser attached the reply never arrives and the hold stands — +correct first-pass behavior: no browser, no scale fact, no generation. The +hold is per-trigger and visible in the frontier (`pending_bids` traces show +the scale-join once-thread parked on its transform listener); a second +trigger mints a SECOND pipeline rather than superseding the first. + +### The generation lane → `ui_render` + +With the preflight passed, the pipeline's generation legs request a systemTwo +response composing the render. The design tenant is an optional input, never +a gate — with a tenant: the token **vocabulary** (the flattened `--design-*` +custom property names, never literal values) rides model-facing and the prose +sections ride as system context (Consumption/F's two lanes); with NO tenant +(user deleted their `DESIGN.md`, or never had one) generation proceeds plain +— structural output, no token context, no artifact — and still produces a +conforming `ui_render`. The scale fact and target ride the request's `ctx` +(host-supplied, never model-facing; the store and systemTwo wires echo `ctx` +verbatim on results — the join lane the pipeline state round-trips through). +The user message composes from the trigger's own detail (`View request: …`) +— the view the user actually asked for, not a host-supplied fixed string. +The generation endpoint is a config seam: `bProgram({ ui: { provider, +modelId } })` names the provider label and model id the pipelines compose +with (unset = the `default`/`gpt-5.1` conventions; a named provider must +carry an entry in the systemTwo endpoint map). + +The model composes only the **html fragment**; the id, target, and swap are +host-stamped (the model is never trusted with the envelope). The composed +detail must validate against `CONTROLLER_DETAIL_SCHEMAS`'s `ui_render` schema +**before the thread requests it** (validate-before-request — the catalog +pattern; the root guard is the backstop, not the only gate). A non-conforming +reply is held as data — the draft is selected and visible in traces, never +emitted. + +The custom-properties artifact: a tenant-bearing scan also compiles the +tokens to a stylesheet (`--design-: ;` — `light-dark()` +values pass through verbatim) stored as the `design` collection's `artifact` +value, compiled from the TENANT only, never from the shipped asset. Generated +html references the properties, not literals. + +The artifact's serving seam is the `ui_style` egress message: a token-bearing +pipeline composes the SAME declarations wrapped in an `@scope` block whose +scope root is the render target's `b-target` selector — +`@scope ([b-target=…]) { :scope { --design-…: …; } }` — so the properties +live on the target and its subtree inherits them, without leaking to the +page (scoping proximity; `:scope` carries the declarations). The browser +applies the css VERBATIM into one `data-b-style`-keyed style element per +target (idempotent replace, never stack). Baseline 2026 — Chrome/Edge 118+, +Firefox 146+, Safari 26.4; an older engine drops the block silently, the +same plain degradation as no tenant. Emitted BEFORE the `ui_render` when a +token-bearing tenant exists; plain (no tenant, no tokens) emits nothing. The +css is jq-deterministic from validated tenant tokens — no model in the loop, +no standing gate (the render gate exists for MODEL output). MINIMAL: the +`=` selector match only (match variants ride a named need); the store +artifact remains the durable record for other consumers. + +### The autoresearch loop + +The initial thread set is a **first hypothesis refined by measurement, not +argument**. The capture lane: an in-process RAW `useTrace` consumer +(`src/cli/ui-capture.ts`, mounted by the socket host — the TUI/start path) +writes each ui-pipeline run to `/captures/ui-runs.jsonl` as +`{ pipeline, startedAt, threads, reentries, messages }`. Runs are LINEAGE-keyed +— keyed by the minted pipeline id parsed from thread labels, correlation ids, +and ctx lineage, never by a time window — so interleaved pipelines attribute +correctly and unrelated faculty traffic stays out of the runs; the standing +Thread set and the position-tagged once-thread re-entries (the minted legs +included — the pump's subscriber-order warp, the mint arriving before the +ingress trace, is handled by lazy binding) ride `thread_added` for free. A run +closes only at its `ui_render` terminus; a held run stays open (the quiescence +flush of incomplete runs is a named later iteration). The replay pass is +`frontier_request { op: 'replay' }` over a captured run (`uiReplayRequest`) — +the divergence view: replay the full run for the end state, or pass a +message-count prefix (e.g. up to just before the browser's scale reply) to +re-derive the hold — where requests blocked and what the frontier looked +like. The graders are consumer-authored; the loop wires the capture and the +replay, nothing more. + +## Wiring guidance - **Wiring a multi-page app**: one `Controller` per page, constructed in the - page's `` async module. The WebSocket URL is derived from the page's - origin (`location.href.replace(/^http/, 'ws')`). + page's `` async module. The default carrier derives the WebSocket URL + from the page's origin (`location.href.replace(/^http/, 'ws')`); pass + `transport` to inject a different one. - **Binding interactive elements**: declare `b-trigger` and `b-form` attributes in the DOM; the Controller wires them to emit `ui_event` messages on user interaction. No manual `addEventListener` in your code. @@ -94,34 +233,30 @@ in both target hosts; the ICL conversion removed the fleet). What survives: sockets, timers) on unload and bfcache freeze; the Controller does **not** force-close the socket on `pagehide` so a queued snapshot can flush during teardown. -- **SSR / pre-render**: a behavioral-program `selection` listener calls the - html tools directly to produce an HTML string for an initial page load or - snapshot — see [html](../../behavioral-tools/references/html.md) for the tool - surface. -- **Scale pre-flight**: the agent sends `scale_check` (browser) or calls - `html-scale-check` (SSR) to learn the effective `b-scale` a render target - lives in before generating content. +- **Scale pre-flight**: the agent sends `ui_scale_check` before generating + content to learn the effective `b-scale` a render target lives in; the + Controller replies with `ui_scale_check_result` carrying `effectiveScale`. ## A common wiring mistake to avoid Calling `Controller` methods directly to mutate the DOM. The Controller is a -**message applier**, not a DOM API — `render`/`attrs`/`dispatch_custom_event`/ -`navigate` arrive as server-pushed messages and are dispatched internally, -not called by your code. If you find yourself reaching for a Controller method -to change the page, the correct path is to emit a `ui_event` (via a -`b-trigger`/`b-form` declaration) and let the agent's behavioral program -respond with a server-pushed `render`. The DOM is downstream of the agent, -not the other way around. +**message applier**, not a DOM API — `ui_render`/`ui_attrs`/ +`ui_dispatch_custom_event`/`ui_navigate` arrive as server-pushed messages and +are dispatched internally, not called by your code. If you find yourself +reaching for a Controller method to change the page, the correct path is to +emit a `ui_event` (via a `b-trigger`/`b-form` declaration) and let the +agent's behavioral program respond with a server-pushed `ui_render`. The DOM +is downstream of the agent, not the other way around. The second common mistake: expecting the WebSocket to be manually managed. -The Controller handles connect, retry (with bounded backoff on codes 1006/ -1012/1013, max 3 retries), and message queuing during disconnect internally. -Do not wrap it in your own reconnection logic — that duplicates the built-in -faculty and races with the Controller's own retry. +The Controller handles connect, retry (bounded backoff on close codes +1006/1012/1013 — max 3 attempts, jittered exponential delay capped at +`UI_CORE_MAX_RETRIES` in `controller.constants.ts`), and message queuing +during disconnect (the queue flushes on reconnect) internally. Do not wrap it +in your own reconnection logic — that races with the Controller's built-in +retry. ## See also - [behavioral](./behavioral.md) — the runtime whose `selection` listeners - drive both surfaces (the action channel). -- [html](../../behavioral-tools/references/html.md) — the SSR tool surface: - I/O contracts, dispatch examples, gotchas. + drive the agent side of the `ui_*` wire (the action channel). \ No newline at end of file diff --git a/skills/behavioral/references/design-spec.md b/skills/behavioral/references/design-spec.md deleted file mode 100644 index e610dcf30..000000000 --- a/skills/behavioral/references/design-spec.md +++ /dev/null @@ -1,569 +0,0 @@ -# Design spec (consensus surface) - -Reference for an agent assisting in the design of behavioral's design-system -specification — a derivative of [DESIGN.md] re-grounded on Rachel Jaffe's -[Structural IA] to move beyond atomic visual styling into a full structural -+ expressive system an agent can use to build interfaces. This document is -the **editable consensus surface** for an in-progress wayfinding effort, not -the finished spec. It captures decisions locked so far, the substrate facts -gathered from the codebase and MDN, and the open frontier still to grill. -Edit it freely as exploration advances; when the way is clear, the hand-off -spec is written from it. - -> **Status: in progress.** Locks are marked **Locked**; unresolved -> questions live under [Open frontier](#open-frontier) and must not be -> treated as decided. Nothing here is normative yet — this is the shared -> map, not the territory. - -## Destination - -**Locked** — the effort's destination is a **hand-off spec**: a new -DESIGN.md-derivative format spec for the behavioral framework, handed off for -implementation and iteration. The map ends when every decision needed to -*write* that spec is made; the prose itself gets written *after* the map, by -whoever does the work. The destination fixes scope, so it was settled -first. - -## Two-phase usage model - -**Locked** — the spec is a **format template**, not a design system itself. -It is used in two phases by two different audiences: - -| Phase | Who | Reads | Produces | Needs | -|-------|-----|-------|----------|------| -| **A — authoring** | Agent + user | The **format spec** | A **project DESIGN.md** | Reasoning guidance: what concepts to elicit, how to choose patterns/affordances/feedback, what sections to fill | -| **B — building** | Agent | The **project DESIGN.md** | **Actual UI** (HTML + behavioral threads) | Declarative inventory: exact `--*` tokens, declared `affordances:` / `feedback:` / `patterns:` maps, prose explaining intent | - -The format spec therefore contains: **mechanism** (how `b-scale`, `@scope`, -DSD, custom properties work — stable, shared) + **starter vocabulary** -(default `affordances:` / `feedback:` / `patterns:` maps — shared defaults a -project overrides/extends) + **section templates** (what each body section -must contain — the project fills in) + **reasoning guidance** (woven into -section descriptions, teaching the Phase A agent how to elicit and choose). - -A project DESIGN.md is the **instance**: specific tokens, selected/customized -affordances, feedback states, and patterns, filled-in prose. The format -spec's frontmatter defaults are overridable; the project's frontmatter is the -conformance contract a Phase B agent generates against. - -## What lives where (the layer separation) - -**Locked** — the spec separates three layers, each with a different -relationship to HTML and to the framework: - -| Layer | What it declares | Appears in HTML? | Example | -|-------|-----------------|------------------|---------| -| **Structural** (`b-scale`, `patterns:`) | What a node *is* and what may nest inside it; the structural shape of a region | **Yes** — `b-scale` governs DOM nesting, so it appears in markup | `b-scale="s3"` (a block); a `Stream` pattern | -| **Functional** (`affordances:`, `feedback:`) | Named *interaction intents* and *loop response states* — the vocabulary reusable thread objects compose from | **No** — affordances and feedback are properties of *behavioral logic* (thread objects), not of HTML elements; the HTML is downstream of the thread, styled by the token bundles the vocabulary declares | `affordances: { danger, primary, secondary }`; `feedback: { error, confirmation, pending, success }` | -| **Expressive** (`--*` tokens, carrier model) | The *visual values* — CSS custom properties, resolved per scope via inheritance + `@scope` / DSD | Yes — as CSS custom properties, but the *names* come from the vocabulary, not from a mode attribute | `'--color-primary': "#1A1C1E"`; a `danger` affordance's token-bundle override | - -The key move: **affordances and feedback states are vocabulary for reusable -thread objects (behavioral structure), not HTML attributes.** A "danger -affordance" is not ` - -``` - -### Worked example — self-contained template (DSD) - -```html - - -``` - -`:host([b-scale="…"])` makes the host's light-DOM structural attribute drive -styling inside the shadow; inherited `--density-base` from a light ancestor -pierces in unless `:host()` redefines it. - -## Structural patterns - -**Locked (mechanism); placement open (vocabulary)** — the spec declares a -`patterns:` frontmatter map. Each pattern has the four Structural-IA -attributes: - -| Attribute | Meaning | In frontmatter | -|-----------|---------|----------------| -| **Content** | What activities/interactions take place; the *goal* for the user | Prose string | -| **Structure** | How information is organized; innate mechanics | Prose string | -| **Boundary** | What information shares in/out; permissions (prose contract, not a CSS mechanism) | Prose string | -| **Scale** | Which `b-scale` value this pattern occupies | Enum: `s1`–`s6` | - -### Starter vocabulary (default, overridable) - -The format spec ships a default `patterns:` map drawn from Structural IA: - -- **Blocks (S3):** Pools, Streams, Feeds, Collections, Walls, Threads -- **Platform structures (S7):** Strict Hierarchy, Nested Pools, Nested - Channels, Hypertext, Daisy, Multi-Dimensional Hierarchy - -A project DESIGN.md **overrides/extends** this: it selects which patterns it -uses, fills in project-specific Content/Boundary prose, and may declare its -own domain patterns. The project's `patterns:` frontmatter is what a Phase -B agent composes from. - -## Relationship to the original DESIGN.md - -The original DESIGN.md is purely the **expression layer** (Alexander: visual -atoms). This derivative adds a **structure layer** (Wurman/Jaffe: functional -units) and a **functional vocabulary layer** (affordances/feedback for -behavioral threads), connected by density-from-scale. The agent designs -top-down: function → structure → expression. - -| Original DESIGN.md | Effect | Status | -|---|---|---| -| **Design Tokens (frontmatter)** — grouped dot-notation, `{ref}` syntax, `components:` map | Transformed: flat `--*`→value map + `affordances:` / `feedback:` / `patterns:` maps; `var()` replaces `{ref}`; **no `components:` token block** | Transformed (kept) | -| **Overview** | Reframed for Behavioral/HTML-first + two-phase usage + functional flow | Keep | -| **Colors** | Prose; tokens live as `--color-*` in frontmatter | Keep | -| **Typography** | Keep; `font-size`/`line-height` couple to scale-implied density via `calc`/`em` | Keep | -| **Layout** | Reframe: density (from scale) × relationship-multipliers + regions, not grid + T-shirt scale | Keep (reframed) | -| **Elevation & Depth** | Prose; optional `--elevation-*` | Keep | -| **Shapes** | `--radius-*` custom properties | Keep | -| **Components** — token map with variant keys (`button-primary`, `button-primary-hover`) | **Eliminated.** No variant keys, no `components:` block. Components are `.html` templates styled with `--*` + `@scope`/`:host()`/`::part()`; their file format is an agent concern. | Eliminate | -| **Modes** | **Dropped.** Replaced by `affordances:` + `feedback:` functional vocabulary (for thread objects, not HTML attributes). | Drop / replace | -| **Structural Scale & Patterns** | *(NEW)* — `b-scale` (S1–S6 + `rel`), nesting constraint, density-from-scale, `patterns:` frontmatter map (overridable defaults) | Add | -| **Functional Vocabulary** | *(NEW)* — `affordances:` + `feedback:` maps, reusable thread objects, functional flow reasoning (substrate-neutral) | Add | -| **Do's and Don'ts** | Keep; scale + affordance/feedback-specific guidance | Keep | - -Net: the spec now has three layers — **structural** (`b-scale` + patterns, -appears in HTML), **functional** (affordances + feedback, vocabulary for -thread objects, not in HTML), and **expressive** (`--*` tokens + carrier -model). `b-scale` is the only spec attribute in HTML. `p-mode` and -`p-density` are gone. The original's `components:` token map is eliminated -(the anti-pattern of variant-per-intent token proliferation that structural -patterns + functional vocabulary dissolve). - -## Substrate facts (gathered from the codebase) - -These were looked up rather than grilled — they are facts about how Behavioral -actually works, not decisions. - -| Surface | What it does | Relevance | -|---------|--------------|-----------| -| **Controller floors + schemas-as-data** (`src/controller/`) | `isInvalidTrigger`/`detectXssVectors` as hardcoded invariants; `html.schemas.ts`/`css.schemas.ts` as pure schema data (classifier context, not compiled validators); . | Deterministic floor + probabilistic ceiling — the retired html tools' validation story, decomposed. | -| **Controller** (browser, `src/controller/controller.ts`) | WebSocket-push-driven; binds `b-trigger`/`b-form` in light DOM; applies `render`/`attrs`/`dispatch_custom_event`/`navigate`/`scale_check`. Swaps fragments via `