superbee 0.0.1 → 0.1.1-pre.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +14 -0
- package/README.md +94 -7
- package/SKILL.md +453 -0
- package/dist/superbee.mjs +101701 -0
- package/package.json +38 -7
- package/references/recipes/claims/conventions/claim.md +39 -0
- package/references/recipes/claims/recipe.md +34 -0
- package/references/recipes/review-workflow/conventions/review-request.md +44 -0
- package/references/recipes/review-workflow/conventions/view.md +71 -0
- package/references/recipes/review-workflow/recipe.md +20 -0
- package/references/recipes/review-workflow/references/view-authoring-v0.md +324 -0
- package/references/recipes/review-workflow/views/review-workflow/reviews.html +27 -0
- package/references/recipes/review-workflow/views-registry/review-workflow-reviews.md +8 -0
- package/references/sample-bundle/concepts/index.md +4 -0
- package/references/sample-bundle/concepts/link-graph.md +25 -0
- package/references/sample-bundle/concepts/okf-alignment.md +26 -0
- package/references/sample-bundle/context-notes/cycle-okf-lite-vision.md +33 -0
- package/references/sample-bundle/context-notes/index.md +3 -0
- package/references/sample-bundle/index.md +8 -0
- package/references/sample-bundle/log.md +7 -0
- package/references/sample-bundle/references/index.md +3 -0
- package/references/sample-bundle/references/okf-spec.md +27 -0
- package/references/views/about.html +102 -0
- package/references/views/conventions/view.md +71 -0
- package/references/views/pulse.html +439 -0
- package/references/views/references/view-authoring-v0.md +324 -0
- package/references/views/roadmap.html +226 -0
- package/references/views/views-registry/about.md +12 -0
- package/references/views/views-registry/pulse.md +13 -0
- package/references/views/views-registry/roadmap.md +12 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Reference
|
|
3
|
+
title: Bundle View authoring — shared web and MCP contract
|
|
4
|
+
protocol: v0+v1
|
|
5
|
+
timestamp: "2026-07-22T00:00:00.000Z"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Bundle View authoring — shared web and MCP contract
|
|
9
|
+
|
|
10
|
+
Author **one durable View** for both local web and MCP hosts: a self-contained, responsive HTML
|
|
11
|
+
blob under `views/…` plus a `type: View` registry doc under `views-registry/…`. Both hosts launch
|
|
12
|
+
the same registry id, exact HTML bytes, access declaration, and bridge contract. The host chooses
|
|
13
|
+
the available size and may offer expansion; do not create separate inline, expanded, web, or MCP
|
|
14
|
+
implementations.
|
|
15
|
+
|
|
16
|
+
Use the bridge for bundle data and `render-document` for canonical Markdown presentation. Style
|
|
17
|
+
the returned inert fragment inside the View; do not ship another Markdown parser. The sections
|
|
18
|
+
below are the exact protocol contract and copy-paste client. This contract travels with portable
|
|
19
|
+
View-bearing recipes, so authoring does not depend on an agent-harness skill.
|
|
20
|
+
|
|
21
|
+
Legacy `Page` and `bridge` are retired authoring names. Use `type: View` and `access`; `superbee status`
|
|
22
|
+
reports legacy content that needs migration. Legacy wire names such as `open-page` remain stable.
|
|
23
|
+
|
|
24
|
+
## Trust model (exact-byte approval + no credential)
|
|
25
|
+
|
|
26
|
+
The `ui` server serves two privilege tiers on one loopback origin:
|
|
27
|
+
|
|
28
|
+
- **Data API** (`/v0/*`): reachable ONLY with the shell's per-run session token/cookie.
|
|
29
|
+
- **View bytes** (`/__page/<nonce>`): a view's static HTML, served for a short-lived **nonce**
|
|
30
|
+
the session-authed shell mints (`POST /__page/mint`) for that view's one blob key. The nonce
|
|
31
|
+
is not the session token, so it is rejected by every data route; the session token does not
|
|
32
|
+
open the page route to arbitrary keys.
|
|
33
|
+
|
|
34
|
+
The iframe is `sandbox="allow-scripts"` with **no** `allow-same-origin`, so the view runs at an
|
|
35
|
+
**opaque origin**. A strict per-view CSP (`connect-src 'none'`) blocks ordinary direct network APIs
|
|
36
|
+
such as fetch, XHR, WebSocket, and EventSource, while the View never receives the shell's credential
|
|
37
|
+
or a data endpoint. Those controls are defense-in-depth: a data-bearing View is executable code,
|
|
38
|
+
and approving its exact bytes and declared access is the decision to trust that code. Approve only
|
|
39
|
+
a View whose source or author you trust; changed bytes or expanded access ask again. The supported
|
|
40
|
+
bundle-data channel is `postMessage` to the shell, whose stable v0 bridge is read-only. A View that
|
|
41
|
+
declares `bundle-propose` may additionally ask trusted shell chrome to prepare one v1 scalar-field
|
|
42
|
+
action; only the human's shell-native Apply choice authorizes the CAS write.
|
|
43
|
+
|
|
44
|
+
## Message shapes
|
|
45
|
+
|
|
46
|
+
Every message carries `bridge: "v0"`. A request carries an `id`; the reply echoes it as
|
|
47
|
+
`"<type>:result"` (or `"error"`). The shell drops any message whose `event.source` is not the
|
|
48
|
+
view's own iframe; the view drops any message whose `event.source` is not `window.parent`.
|
|
49
|
+
|
|
50
|
+
### View → shell (requests)
|
|
51
|
+
|
|
52
|
+
| type | payload | reply `result` |
|
|
53
|
+
| ----------- | ---------------------------------------------------------- | ---------------------------------------------------------- |
|
|
54
|
+
| `hello` | — | `{ bundle: { root, name }, mode, protocol: "v0", grant }` |
|
|
55
|
+
| `query` | `{ params: { type?, prefix?, field?, open?, limit? } }` | `{ rows: DocHead[], count }` |
|
|
56
|
+
| `read` | `{ docId }` | `{ id, frontmatter, body }` |
|
|
57
|
+
| `render-document` | `{ docId }` | `{ document: { id, version }, html, bounded }` |
|
|
58
|
+
| `edges` | `{ params: { from?, to?, text? } }` | `{ edges: { from, to, text }[], count }` |
|
|
59
|
+
| `subscribe` | — | `{ ok: true }`, then a stream of `change` events |
|
|
60
|
+
| `open-page` | `{ pageId: "views-registry/…" }` | none; fire-and-forget shell navigation |
|
|
61
|
+
|
|
62
|
+
`hello.result.grant` is `"read"` for `bundle-read` and `"propose"` for `bundle-propose`.
|
|
63
|
+
|
|
64
|
+
`open-page` is the sole capability-independent action: `access: none`, `access: bundle-read`, and
|
|
65
|
+
`access: bundle-propose` Views may ask the shell to open another usable registered View. The shell
|
|
66
|
+
accepts only a conservative `views-registry/…` (or legacy-location `pages-registry/…`) concept
|
|
67
|
+
id, validates that it resolves to a `type: View` doc with a safe `views/…`
|
|
68
|
+
(or legacy-location `pages/…`) entry, and mounts the target normally with its own sandbox, nonce, and
|
|
69
|
+
bridge capability. It returns no target body, frontmatter, entry, HTML, or nonce. A failed
|
|
70
|
+
attempt can reveal that one caller-supplied registry id is not usable; this bounded existence
|
|
71
|
+
oracle is the only information exposed by navigation.
|
|
72
|
+
|
|
73
|
+
`DocHead` is `{ id, version, frontmatter }` — the same **head projection** `list` uses (full
|
|
74
|
+
frontmatter, never a body). `query` params:
|
|
75
|
+
|
|
76
|
+
`render-document` reads the canonical document and serializes its body with the shell's shared,
|
|
77
|
+
bounded Markdown renderer. The returned `html` is inert semantic markup: it contains no scripts,
|
|
78
|
+
event handlers, forms, controls, images, or navigable anchors. Internal concept links become
|
|
79
|
+
passive elements carrying `data-aslite-doc-id`; the View may delegate clicks on those markers to
|
|
80
|
+
its own selection logic and issue another `render-document` request. Insert only the unmodified
|
|
81
|
+
`html` returned by this request. The accompanying document `version` is the exact version rendered;
|
|
82
|
+
after a matching `change` event, refetch instead of treating old HTML as current. `bounded: true`
|
|
83
|
+
means renderer safety limits truncated or collapsed part of the input.
|
|
84
|
+
|
|
85
|
+
- `type` / `prefix` — server-side facets (a bundle-relative id prefix, a frontmatter `type`).
|
|
86
|
+
- `field` — a client-side `key=value` filter; comma-separated values are OR (`progress_status=todo,blocked`).
|
|
87
|
+
Scalar and array-valued fields use the same string-coerced membership rule as CLI `list`.
|
|
88
|
+
- `open` — drop terminal rows, derived from the BUNDLE'S OWN kind conventions exactly like
|
|
89
|
+
`list --open`: a row is dropped iff the convention governing its `type` declares the row's
|
|
90
|
+
current field value(s) terminal (`fields.terminal`, e.g. the Task kind's `done`/`canceled`).
|
|
91
|
+
A row with no governing kind is kept; a bundle where no kind declares a terminal set filters
|
|
92
|
+
nothing. (The shell loads the registry once per change from the server, which builds it with
|
|
93
|
+
core's `loadKinds` — one registry, no bridge-side schema.)
|
|
94
|
+
- `limit` — a positive number caps `rows`; `0` or absence is unlimited. `count` remains the total
|
|
95
|
+
matched after `field`/`open` filtering and before the cap, matching CLI `list`.
|
|
96
|
+
|
|
97
|
+
`edges` is the general graph query — the ONE primitive every edge-shaped question reduces to
|
|
98
|
+
(the same `queryEdges` atom `link list` is a CLI face over). `params`:
|
|
99
|
+
|
|
100
|
+
- `from` / `to` — each one exact nonblank concept id, a bundle-relative `prefix/` (trailing
|
|
101
|
+
slash), or an array of 1–32 such strings (union within the facet; giving both ANDs them). Omit
|
|
102
|
+
the property for "no restriction". Supplied empty/all-whitespace strings, empty arrays,
|
|
103
|
+
blank/non-string array entries, and arrays above 32 are invalid. Every string is preserved
|
|
104
|
+
byte-for-byte and may be at most 1,024 UTF-8 bytes; duplicate entries still count toward 32.
|
|
105
|
+
- `text` — one exact nonblank link-display string (never substring/regex), preserved byte-for-byte
|
|
106
|
+
and at most 1,024 UTF-8 bytes; an empty/all-whitespace value is invalid.
|
|
107
|
+
|
|
108
|
+
Backlinks are `edges({ to: docId })`; a container's contents are `edges({ from: itemId, text:
|
|
109
|
+
"contains" })` (or whatever link text a bundle's convention uses) — there is no separate
|
|
110
|
+
backlinks-only bridge call. A source linking to the same target twice with different text yields
|
|
111
|
+
two rows (no dedup), matching `queryEdges`'s own granularity.
|
|
112
|
+
|
|
113
|
+
### Shell → view (server-initiated)
|
|
114
|
+
|
|
115
|
+
| type | payload |
|
|
116
|
+
| -------- | --------------------------------------------------- |
|
|
117
|
+
| `change` | `{ event: { changes: [{ id, version }], removed: [id] } }` |
|
|
118
|
+
|
|
119
|
+
`change` is pushed only to a view that has `subscribe`d. It is a **delta signal** — refetch with
|
|
120
|
+
`query` against it rather than trusting it as a full state. Removed ids have been deleted.
|
|
121
|
+
|
|
122
|
+
For live data views, prefer `Bridge.watch(refresh)` over assembling the startup sequence yourself.
|
|
123
|
+
It subscribes before the first snapshot, passes an ordered batch of raw `change` event payloads to
|
|
124
|
+
each refresh, and never overlaps refresh calls. Events arriving during a refresh are coalesced into
|
|
125
|
+
one follow-up batch. A failed refresh does not poison later event-driven refreshes; `watch` does not
|
|
126
|
+
retry on a timer. Its returned Promise covers subscription plus the first refresh, so handle that
|
|
127
|
+
Promise to surface startup failures. Raw `subscribe` remains available when a view needs the
|
|
128
|
+
lower-level event stream.
|
|
129
|
+
|
|
130
|
+
**There are no mutation messages in v0.** Read-only is enforced *by construction*: the shell
|
|
131
|
+
defines no write/delete/update handler, so any such request returns an `error` reply.
|
|
132
|
+
|
|
133
|
+
### Trusted action bridge v1
|
|
134
|
+
|
|
135
|
+
`access: bundle-propose` includes the v0 read surface and adds two exact v1 requests:
|
|
136
|
+
|
|
137
|
+
- `{ bridge: "v1", type: "read-versioned", id, docId }` returns one canonical document and the
|
|
138
|
+
version from the same read.
|
|
139
|
+
- `{ bridge: "v1", type: "action.propose", requestId, action: { kind:
|
|
140
|
+
"document.set-field", docId, field, value, expectedVersion } }` proposes changing one declared
|
|
141
|
+
scalar field on an existing governed document.
|
|
142
|
+
|
|
143
|
+
The shell independently re-reads the View registry, exact HTML version, target document, and Kind;
|
|
144
|
+
shows canonical before/after values outside the iframe; and commits only after the human chooses
|
|
145
|
+
Apply. The approval token and immutable launch identity never enter the iframe. A stale target is a
|
|
146
|
+
visible conflict and is never retried behind the human's back. V1 is local `--dir` only and excludes
|
|
147
|
+
body writes, links, creation, deletion, remote writes, and persistent grants. Start the shell with
|
|
148
|
+
`superbee ui --actor <name>` (or set `SUPERBEE_ACTOR`; `AGENTSTATE_LITE_ACTOR` remains a
|
|
149
|
+
supported compatibility input) to enable proposals.
|
|
150
|
+
|
|
151
|
+
## Live updates
|
|
152
|
+
|
|
153
|
+
The shell (only) holds one `EventSource('/events')`. The server watches the bundle — `fs.watch`
|
|
154
|
+
in `--dir` mode, a poll in `--remote` mode — diffs content-addressed **version tokens**, and pushes
|
|
155
|
+
a change delta. The shell fans doc changes into subscribed views as `change` events, and
|
|
156
|
+
**hot-reloads** a view's iframe (with a fresh nonce) when the view's own HTML blob changes. (Remote
|
|
157
|
+
view-blob hot-reload is a labeled follow-up; live doc updates work in both modes.)
|
|
158
|
+
|
|
159
|
+
## `access` — the enforced data/content split
|
|
160
|
+
|
|
161
|
+
The registry doc's `access` field decides whether the shell will answer THIS view's bridge
|
|
162
|
+
requests at all — and the shell, not the view, is what enforces it:
|
|
163
|
+
|
|
164
|
+
- `access: bundle-read` — a **data view**. The shell answers `hello`/`query`/`read`/
|
|
165
|
+
`render-document`/`edges`/`subscribe` as described above.
|
|
166
|
+
- `access: bundle-propose` — an **interactive view**. It receives the same read surface and may
|
|
167
|
+
submit the narrow v1 proposal above. Each proposal still requires trusted-shell confirmation.
|
|
168
|
+
- `access: none` — a **content view**. The shell replies to every bundle-data request with a
|
|
169
|
+
`FORBIDDEN` error, before touching any bundle data. It may still use `open-page` navigation.
|
|
170
|
+
- `bridge` is the legacy spelling of this field, and it is no longer read: a doc declaring only
|
|
171
|
+
the legacy `bridge` field resolves to `access: none` (every bundle-data request is denied).
|
|
172
|
+
The repo's `migrate-legacy-view-names` script renames leftover legacy `bridge` fields to
|
|
173
|
+
`access` in place, and `superbee status` lists them under its `legacy_naming` finding.
|
|
174
|
+
Authoring uses `access`.
|
|
175
|
+
- The `View` convention declares `access` REQUIRED — every view is an intentional
|
|
176
|
+
classification, not a silent default. At runtime the shell still fails closed for a doc this
|
|
177
|
+
convention didn't govern (an external bundle, a hand-edited file that skipped the lint): absent,
|
|
178
|
+
malformed, or any other value is treated as `access: none`. A view only gets bundle access by
|
|
179
|
+
declaring exactly `bundle-read` or `bundle-propose`.
|
|
180
|
+
|
|
181
|
+
The launcher groups views by this same field: "Dashboards" for `bundle-read`, "Interactive" for
|
|
182
|
+
`bundle-propose`, and "Documents" for `none`.
|
|
183
|
+
|
|
184
|
+
## Authoring a view
|
|
185
|
+
|
|
186
|
+
Start from a working installed View when possible, then adapt it responsively for the space the
|
|
187
|
+
host provides. Keep data selection bounded and show empty, partial, over-limit, and unavailable
|
|
188
|
+
states.
|
|
189
|
+
|
|
190
|
+
```sh
|
|
191
|
+
superbee blobs --prefix views/
|
|
192
|
+
superbee pull --doc-key views/review-workflow/reviews.html --out my-view.html
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Keep HTML, CSS, and JavaScript self-contained with no external hosts. A data View embeds the bridge
|
|
196
|
+
client below. A content View (`access: none`) may use only `openPage`; bundle-data calls return
|
|
197
|
+
`FORBIDDEN`.
|
|
198
|
+
|
|
199
|
+
Install the HTML blob and its registry entry:
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
superbee promote my-view.html --doc-key views/my-view.html
|
|
203
|
+
superbee new "View" my-view \
|
|
204
|
+
--title "My view" \
|
|
205
|
+
--entry views/my-view.html \
|
|
206
|
+
--access bundle-read \
|
|
207
|
+
--description "A live view of this bundle."
|
|
208
|
+
superbee ui --open
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`new "View" my-view` applies the View Kind's declared `views-registry/` path. Use `access: none`
|
|
212
|
+
for a static report or diagram. Re-promoting the HTML updates the open View; the shell reloads it
|
|
213
|
+
with a fresh nonce. If the bundle does not yet declare the View Kind, install its View-bearing
|
|
214
|
+
recipe or promote the supplied `conventions/view.md` once before creating the registry entry.
|
|
215
|
+
|
|
216
|
+
Verify the same registered id in both surfaces: open it with `superbee ui`, then have an MCP-capable
|
|
217
|
+
desktop list and show that View. Confirm narrow and expanded layouts without changing the source.
|
|
218
|
+
|
|
219
|
+
The seed views here are working examples: `pulse.html`/`roadmap.html` are `access: bundle-read`
|
|
220
|
+
data views — `roadmap.html` is the one that exercises the `edges` request end-to-end (a live graph
|
|
221
|
+
view of Roadmap Items and the tasks each one `contains`) — and `about.html` is an `access: none`
|
|
222
|
+
content view (no bridge calls at all). `demo.sh` (repo only) wires all of this over a scratch copy
|
|
223
|
+
of this repo's own board.
|
|
224
|
+
|
|
225
|
+
## The bridge client (embedded copy)
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
(function () {
|
|
229
|
+
var PROTO = "v0", seq = 0, pending = {}, subs = [];
|
|
230
|
+
function send(type, extra) {
|
|
231
|
+
return new Promise(function (resolve, reject) {
|
|
232
|
+
var id = String(++seq);
|
|
233
|
+
pending[id] = { resolve: resolve, reject: reject };
|
|
234
|
+
var msg = { bridge: PROTO, id: id, type: type };
|
|
235
|
+
if (extra) for (var k in extra) msg[k] = extra[k];
|
|
236
|
+
parent.postMessage(msg, "*"); // parent origin is opaque to us; the shell validates by source
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
// A shell action, deliberately separate from send(): the source frame may unload immediately,
|
|
240
|
+
// so openPage is void/fire-and-forget and must not be awaited.
|
|
241
|
+
function openPage(pageId) {
|
|
242
|
+
parent.postMessage({ bridge: PROTO, type: "open-page", pageId: pageId }, "*");
|
|
243
|
+
}
|
|
244
|
+
function watch(refresh) {
|
|
245
|
+
if (typeof refresh !== "function") return Promise.reject(new TypeError("Bridge.watch requires a refresh function"));
|
|
246
|
+
var active = true, ready = false, running = false, queued = [];
|
|
247
|
+
function schedule(initial) {
|
|
248
|
+
running = true;
|
|
249
|
+
var batch = queued.splice(0);
|
|
250
|
+
return Promise.resolve().then(function () { return refresh(batch); }).then(function (value) {
|
|
251
|
+
running = false;
|
|
252
|
+
if (queued.length) void schedule(false);
|
|
253
|
+
return value;
|
|
254
|
+
}, function (err) {
|
|
255
|
+
running = false;
|
|
256
|
+
if (queued.length) void schedule(false);
|
|
257
|
+
if (initial) throw err;
|
|
258
|
+
console.error("Bridge.watch refresh failed", err);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function onChange(event) {
|
|
262
|
+
if (!active) return;
|
|
263
|
+
queued.push(event);
|
|
264
|
+
if (ready && !running) void schedule(false);
|
|
265
|
+
}
|
|
266
|
+
return window.Bridge.subscribe(onChange).then(function () {
|
|
267
|
+
ready = true;
|
|
268
|
+
return schedule(true);
|
|
269
|
+
}, function (err) {
|
|
270
|
+
active = false;
|
|
271
|
+
throw err;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
window.addEventListener("message", function (e) {
|
|
275
|
+
if (e.source !== window.parent) return; // only trust the shell
|
|
276
|
+
var m = e.data;
|
|
277
|
+
if (!m || m.bridge !== PROTO) return;
|
|
278
|
+
if (m.type === "change") { subs.forEach(function (cb) { cb(m.event); }); return; }
|
|
279
|
+
var p = pending[m.id];
|
|
280
|
+
if (!p) return;
|
|
281
|
+
delete pending[m.id];
|
|
282
|
+
if (m.type === "error") p.reject(new Error((m.error && m.error.message) || "bridge error"));
|
|
283
|
+
else p.resolve(m.result);
|
|
284
|
+
});
|
|
285
|
+
window.Bridge = {
|
|
286
|
+
hello: function () { return send("hello"); },
|
|
287
|
+
query: function (params) { return send("query", { params: params }); },
|
|
288
|
+
read: function (docId) { return send("read", { docId: docId }); },
|
|
289
|
+
renderDocument: function (docId) { return send("render-document", { docId: docId }); },
|
|
290
|
+
edges: function (params) { return send("edges", { params: params }); },
|
|
291
|
+
openPage: openPage,
|
|
292
|
+
subscribe: function (cb) { subs.push(cb); return send("subscribe"); },
|
|
293
|
+
watch: watch
|
|
294
|
+
};
|
|
295
|
+
})();
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Compose and style the trusted fragment inside the View; do not rewrite or concatenate its HTML:
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
async function showDocument(docId) {
|
|
302
|
+
var rendered = await Bridge.renderDocument(docId);
|
|
303
|
+
documentPanel.innerHTML = rendered.html;
|
|
304
|
+
}
|
|
305
|
+
documentPanel.addEventListener("click", function (event) {
|
|
306
|
+
var target = event.target instanceof Element ? event.target.closest("[data-aslite-doc-id]") : null;
|
|
307
|
+
if (target) void showDocument(target.getAttribute("data-aslite-doc-id"));
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
```css
|
|
312
|
+
.document-panel [data-aslite-rendered-document] { line-height: 1.6; }
|
|
313
|
+
.document-panel h1 { font: 600 1.5rem/1.2 system-ui; }
|
|
314
|
+
.document-panel [data-aslite-doc-id] { cursor: pointer; text-decoration: underline; }
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
A live view supplies only its domain snapshot and render work:
|
|
318
|
+
|
|
319
|
+
```js
|
|
320
|
+
Bridge.watch(async function (events) {
|
|
321
|
+
var result = await Bridge.query({ type: "Task" });
|
|
322
|
+
render(result.rows, events);
|
|
323
|
+
}).catch(showStartupError);
|
|
324
|
+
```
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Roadmap</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--paper:#F8F6F2; --white:#fff; --rule:#E0DDD8; --mist:#EDF0F3;
|
|
10
|
+
--ink:#0F1A24; --slate:#4A5D6E; --ghost:#9AAAB8;
|
|
11
|
+
--blue:#1B6A9E; --teal:#0A8A7A; --amber:#B8832A;
|
|
12
|
+
--radius:2px;
|
|
13
|
+
--serif: "Cormorant Garamond", "Iowan Old Style", Georgia, "Times New Roman", serif;
|
|
14
|
+
--sans: -apple-system, "Segoe UI", system-ui, sans-serif;
|
|
15
|
+
--mono: ui-monospace, "SF Mono", Menlo, monospace;
|
|
16
|
+
}
|
|
17
|
+
@media (prefers-color-scheme: dark) {
|
|
18
|
+
:root { --paper:#0B1926; --white:#0F2A3F; --rule:#26323d; --mist:#132636;
|
|
19
|
+
--ink:#F8F6F2; --slate:#9fb2c2; --ghost:#6f8494; --amber:#E0AA40; }
|
|
20
|
+
}
|
|
21
|
+
* { box-sizing:border-box; margin:0; }
|
|
22
|
+
body { background:var(--paper); color:var(--ink); font:15px/1.55 var(--sans); }
|
|
23
|
+
header { padding:22px 32px 14px; border-bottom:1px solid var(--rule); background:var(--white); }
|
|
24
|
+
header h1 { font-family:var(--serif); font-weight:500; font-size:30px; letter-spacing:.01em; display:flex; align-items:baseline; gap:12px; }
|
|
25
|
+
header h1 .chev { color:var(--blue); font-family:var(--sans); font-weight:700; }
|
|
26
|
+
header .sub { color:var(--slate); font-size:13px; margin-top:2px; }
|
|
27
|
+
header .live { float:right; font-size:11.5px; color:var(--teal); font-family:var(--mono); }
|
|
28
|
+
header .live .dot { display:inline-block; width:7px; height:7px; border-radius:50%; background:var(--teal); margin-right:5px; animation:pulse 1.4s infinite; }
|
|
29
|
+
@keyframes pulse { 50%{opacity:.3} }
|
|
30
|
+
main { max-width:900px; margin:0 auto; padding:26px 32px 80px; }
|
|
31
|
+
.group { margin-bottom:34px; }
|
|
32
|
+
.group > h2 { font-family:var(--sans); font-size:12px; font-weight:700; text-transform:uppercase; letter-spacing:.09em; color:var(--slate); display:flex; align-items:center; gap:9px; margin-bottom:4px; }
|
|
33
|
+
.group > h2 .swatch { width:10px; height:10px; border-radius:2px; }
|
|
34
|
+
.group > .note { font-size:12px; color:var(--ghost); margin-bottom:12px; }
|
|
35
|
+
.item { background:var(--white); border:1px solid var(--rule); border-radius:var(--radius); border-left:3px solid var(--rule); margin-bottom:10px; overflow:hidden; }
|
|
36
|
+
.item.active { border-left-color:var(--teal); }
|
|
37
|
+
.item.committed { border-left-color:var(--blue); }
|
|
38
|
+
.item.candidate { border-left-color:var(--amber); }
|
|
39
|
+
.item .head { display:flex; align-items:center; gap:12px; padding:13px 16px; cursor:pointer; }
|
|
40
|
+
.item .head:hover { background:var(--mist); }
|
|
41
|
+
.item .title { font-family:var(--serif); font-size:19px; font-weight:500; flex:1; line-height:1.2; }
|
|
42
|
+
.item .caret { color:var(--ghost); font-size:12px; transition:transform .15s; }
|
|
43
|
+
.item.open .caret { transform:rotate(90deg); }
|
|
44
|
+
.badge { font-family:var(--sans); font-size:10.5px; font-weight:600; padding:2px 8px; border-radius:999px; white-space:nowrap; letter-spacing:.02em; }
|
|
45
|
+
.badge.active { color:var(--teal); background:color-mix(in srgb,var(--teal) 13%,transparent); }
|
|
46
|
+
.badge.queued { color:var(--slate); background:var(--mist); }
|
|
47
|
+
.badge.candidate { color:var(--amber); background:color-mix(in srgb,var(--amber) 15%,transparent); }
|
|
48
|
+
.badge.done { color:var(--slate); background:var(--mist); }
|
|
49
|
+
.roll { display:flex; align-items:center; gap:9px; padding:0 16px 12px; }
|
|
50
|
+
.bar { flex:1; height:5px; background:var(--mist); border-radius:3px; overflow:hidden; display:flex; }
|
|
51
|
+
.bar i { height:100%; display:block; }
|
|
52
|
+
.bar i.done { background:var(--teal); } .bar i.prog { background:var(--amber); } .bar i.open { background:var(--rule); }
|
|
53
|
+
.roll .count { font-family:var(--mono); font-size:11.5px; color:var(--slate); white-space:nowrap; }
|
|
54
|
+
.roll .none { font-size:11.5px; color:var(--ghost); font-style:italic; }
|
|
55
|
+
.body { display:none; padding:2px 16px 14px; border-top:1px solid var(--rule); }
|
|
56
|
+
.item.open .body { display:block; }
|
|
57
|
+
.desc { font-size:13.5px; color:var(--slate); margin:12px 0; max-width:70ch; }
|
|
58
|
+
.tasks { display:flex; flex-direction:column; gap:1px; }
|
|
59
|
+
.task { display:flex; align-items:center; gap:10px; padding:6px 8px; border-radius:2px; }
|
|
60
|
+
.task:hover { background:var(--mist); }
|
|
61
|
+
.task .tdot { width:7px; height:7px; border-radius:50%; flex:none; }
|
|
62
|
+
.task .ttitle { flex:1; font-size:13.5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
63
|
+
.task .tid { font-family:var(--mono); font-size:11px; color:var(--ghost); }
|
|
64
|
+
.st-todo{color:var(--slate)} .st-in_progress{color:var(--amber)} .st-blocked,.st-canceled{color:#c0392b} .st-done{color:var(--teal)}
|
|
65
|
+
.d-todo{background:var(--ghost)} .d-in_progress{background:var(--amber)} .d-blocked,.d-canceled{background:#c0392b} .d-done{background:var(--teal)}
|
|
66
|
+
.item.fresh { animation:land 1.8s ease-out; }
|
|
67
|
+
@keyframes land { 0%{background:color-mix(in srgb,var(--amber) 18%,var(--white))} 100%{background:var(--white)} }
|
|
68
|
+
footer { text-align:center; color:var(--ghost); font-size:11.5px; padding:20px; font-family:var(--mono); }
|
|
69
|
+
.empty { color:var(--ghost); text-align:center; margin-top:16vh; font-family:var(--serif); font-size:20px; }
|
|
70
|
+
</style>
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<header>
|
|
74
|
+
<span class="live" id="live"><span class="dot"></span>live</span>
|
|
75
|
+
<h1><span class="chev">›</span> Roadmap</h1>
|
|
76
|
+
<div class="sub" id="sub">loading the graph…</div>
|
|
77
|
+
</header>
|
|
78
|
+
<main id="main"><div class="empty">reading the roadmap…</div></main>
|
|
79
|
+
<footer>rendered live from the bundle graph — items · contained tasks · candidate vs committed</footer>
|
|
80
|
+
<script>
|
|
81
|
+
(function () {
|
|
82
|
+
var PROTO="v0", seq=0, pending={}, subs=[];
|
|
83
|
+
function send(type, extra){ return new Promise(function(res,rej){ var id=String(++seq); pending[id]={res:res,rej:rej};
|
|
84
|
+
var m={bridge:PROTO,id:id,type:type}; if(extra) for(var k in extra) m[k]=extra[k]; parent.postMessage(m,"*"); }); }
|
|
85
|
+
function openPage(pageId){ parent.postMessage({bridge:PROTO,type:"open-page",pageId:pageId},"*"); }
|
|
86
|
+
function watch(refresh) {
|
|
87
|
+
if (typeof refresh !== "function") return Promise.reject(new TypeError("Bridge.watch requires a refresh function"));
|
|
88
|
+
var active = true, ready = false, running = false, queued = [];
|
|
89
|
+
function schedule(initial) {
|
|
90
|
+
running = true;
|
|
91
|
+
var batch = queued.splice(0);
|
|
92
|
+
return Promise.resolve().then(function () { return refresh(batch); }).then(function (value) {
|
|
93
|
+
running = false;
|
|
94
|
+
if (queued.length) void schedule(false);
|
|
95
|
+
return value;
|
|
96
|
+
}, function (err) {
|
|
97
|
+
running = false;
|
|
98
|
+
if (queued.length) void schedule(false);
|
|
99
|
+
if (initial) throw err;
|
|
100
|
+
console.error("Bridge.watch refresh failed", err);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function onChange(event) {
|
|
104
|
+
if (!active) return;
|
|
105
|
+
queued.push(event);
|
|
106
|
+
if (ready && !running) void schedule(false);
|
|
107
|
+
}
|
|
108
|
+
return window.Bridge.subscribe(onChange).then(function () {
|
|
109
|
+
ready = true;
|
|
110
|
+
return schedule(true);
|
|
111
|
+
}, function (err) {
|
|
112
|
+
active = false;
|
|
113
|
+
throw err;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
window.addEventListener("message",function(e){ if(e.source!==window.parent) return; var m=e.data; if(!m||m.bridge!==PROTO) return;
|
|
117
|
+
if(m.type==="change"){ subs.forEach(function(cb){cb(m.event)}); return; }
|
|
118
|
+
var p=pending[m.id]; if(!p) return; delete pending[m.id];
|
|
119
|
+
if(m.type==="error") p.rej(new Error((m.error&&m.error.message)||"bridge error")); else p.res(m.result); });
|
|
120
|
+
window.Bridge={ hello:function(){return send("hello")}, query:function(params){return send("query",{params:params})},
|
|
121
|
+
read:function(docId){return send("read",{docId:docId})}, edges:function(params){return send("edges",{params:params})},
|
|
122
|
+
openPage:openPage,
|
|
123
|
+
subscribe:function(cb){subs.push(cb); return send("subscribe")},
|
|
124
|
+
watch:watch };
|
|
125
|
+
})();
|
|
126
|
+
|
|
127
|
+
(function(){
|
|
128
|
+
"use strict";
|
|
129
|
+
var mainEl=document.getElementById("main"), subEl=document.getElementById("sub");
|
|
130
|
+
var openIds={}, freshIds=new Set();
|
|
131
|
+
function esc(s){return String(s==null?"":s).replace(/[&<>"]/g,function(c){return {"&":"&","<":"<",">":">",'"':"""}[c]})}
|
|
132
|
+
function fm(r){return r.frontmatter||{}}
|
|
133
|
+
function progress(f){return f.progress_status}
|
|
134
|
+
|
|
135
|
+
function classify(it){
|
|
136
|
+
var s=progress(fm(it));
|
|
137
|
+
if(s==="active") return "active";
|
|
138
|
+
if(s==="done"||s==="canceled") return "done";
|
|
139
|
+
// queued: committed if it has a sequence, candidate if not
|
|
140
|
+
return (fm(it).sequence!=null && String(fm(it).sequence).trim()!=="") ? "committed" : "candidate";
|
|
141
|
+
}
|
|
142
|
+
var GROUPS=[
|
|
143
|
+
{key:"active", label:"In motion", swatch:"var(--teal)", note:"actively being worked"},
|
|
144
|
+
{key:"committed",label:"Committed", swatch:"var(--blue)", note:"sequenced, not yet started"},
|
|
145
|
+
{key:"candidate",label:"Candidate", swatch:"var(--amber)",note:"recognized direction — not yet committed or sequenced (queued + no sequence). Awaiting a founders' decision."},
|
|
146
|
+
{key:"done", label:"Done", swatch:"var(--slate)",note:""}
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
function render(items, taskById, containsByItem){
|
|
150
|
+
if(!items.length){ mainEl.innerHTML='<div class="empty">no roadmap items yet</div>'; return; }
|
|
151
|
+
var byGroup={active:[],committed:[],candidate:[],done:[]};
|
|
152
|
+
items.forEach(function(it){ byGroup[classify(it)].push(it); });
|
|
153
|
+
// committed sorts by sequence
|
|
154
|
+
byGroup.committed.sort(function(a,b){ return (+fm(a).sequence||0)-(+fm(b).sequence||0); });
|
|
155
|
+
var html="";
|
|
156
|
+
GROUPS.forEach(function(g){
|
|
157
|
+
var list=byGroup[g.key]; if(!list.length) return;
|
|
158
|
+
html+='<div class="group"><h2><span class="swatch" style="background:'+g.swatch+'"></span>'+g.label+' · '+list.length+'</h2>';
|
|
159
|
+
if(g.note) html+='<div class="note">'+esc(g.note)+'</div>';
|
|
160
|
+
list.forEach(function(it){
|
|
161
|
+
var id=it.id, f=fm(it), open=openIds[id];
|
|
162
|
+
var contained=(containsByItem[id]||[]);
|
|
163
|
+
var tstats=contained.map(function(tid){ var t=taskById[tid]; return t?progress(fm(t)):null; });
|
|
164
|
+
var done=tstats.filter(function(s){return s==="done"||s==="canceled"}).length;
|
|
165
|
+
var prog=tstats.filter(function(s){return s==="in_progress"||s==="blocked"}).length;
|
|
166
|
+
var total=contained.length;
|
|
167
|
+
html+='<div class="item '+g.key+(open?" open":"")+(freshIds.has(id)?" fresh":"")+'" data-id="'+esc(id)+'">';
|
|
168
|
+
html+='<div class="head" data-toggle="'+esc(id)+'"><span class="caret">›</span>'+
|
|
169
|
+
'<span class="title">'+esc(f.title||id)+'</span>'+
|
|
170
|
+
'<span class="badge '+(g.key==="candidate"?"candidate":(progress(f)||"queued"))+'">'+(g.key==="candidate"?"candidate":esc(progress(f)||"queued"))+'</span></div>';
|
|
171
|
+
// rollup
|
|
172
|
+
html+='<div class="roll">';
|
|
173
|
+
if(total){ var wd=Math.round(done/total*100), wp=Math.round(prog/total*100);
|
|
174
|
+
html+='<div class="bar"><i class="done" style="width:'+wd+'%"></i><i class="prog" style="width:'+wp+'%"></i><i class="open" style="width:'+(100-wd-wp)+'%"></i></div>'+
|
|
175
|
+
'<span class="count">'+done+'/'+total+' done</span>';
|
|
176
|
+
} else { html+='<span class="none">no contained tasks yet</span>'; }
|
|
177
|
+
html+='</div>';
|
|
178
|
+
// body
|
|
179
|
+
html+='<div class="body"><div class="desc">'+esc(f.description||"—")+'</div><div class="tasks">';
|
|
180
|
+
contained.forEach(function(tid){ var t=taskById[tid]; var s=t?progress(fm(t)):"?";
|
|
181
|
+
html+='<div class="task"><span class="tdot d-'+esc(s)+'"></span><span class="ttitle">'+esc(t?fm(t).title:tid)+'</span>'+
|
|
182
|
+
'<span class="badge st-'+esc(s)+'" style="background:transparent">'+esc(s)+'</span><span class="tid">'+esc(tid)+'</span></div>';
|
|
183
|
+
});
|
|
184
|
+
if(!total) html+='<div class="task"><span class="none">— no <code>contains</code> edges into this item</span></div>';
|
|
185
|
+
html+='</div></div></div>';
|
|
186
|
+
});
|
|
187
|
+
html+='</div>';
|
|
188
|
+
});
|
|
189
|
+
mainEl.innerHTML=html;
|
|
190
|
+
mainEl.querySelectorAll("[data-toggle]").forEach(function(el){
|
|
191
|
+
el.addEventListener("click",function(){ var id=el.getAttribute("data-toggle"); openIds[id]=!openIds[id];
|
|
192
|
+
el.closest(".item").classList.toggle("open"); });
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function load(freshChanged){
|
|
197
|
+
return Promise.all([
|
|
198
|
+
Bridge.query({type:"Roadmap Item"}),
|
|
199
|
+
Bridge.query({type:"Task"}),
|
|
200
|
+
Bridge.edges({text:"contains"}) // <- the NEW edges bridge: all containment edges in one call
|
|
201
|
+
]).then(function(r){
|
|
202
|
+
var items=r[0].rows||[], tasks=r[1].rows||[], edges=r[2].edges||[];
|
|
203
|
+
var taskById={}; tasks.forEach(function(t){ taskById[t.id]=t; });
|
|
204
|
+
var containsByItem={};
|
|
205
|
+
edges.forEach(function(e){ if(e.from.indexOf("roadmap-items/")===0 && e.to.indexOf("tasks/")===0){
|
|
206
|
+
(containsByItem[e.from]=containsByItem[e.from]||[]).push(e.to); } });
|
|
207
|
+
if(freshChanged){ freshChanged.forEach(function(id){freshIds.add(id)});
|
|
208
|
+
setTimeout(function(){ freshChanged.forEach(function(id){freshIds.delete(id)}); },1900); }
|
|
209
|
+
subEl.textContent=items.length+" roadmap items · "+edges.length+" contains-edges · "+tasks.length+" tasks — live graph";
|
|
210
|
+
render(items, taskById, containsByItem);
|
|
211
|
+
}).catch(function(err){ mainEl.innerHTML='<div class="empty">graph read failed: '+esc(err.message)+'</div>'; });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
Bridge.hello().then(function(info){
|
|
215
|
+
document.querySelector("header .chev").title=(info.bundle&&info.bundle.name)||"";
|
|
216
|
+
}).catch(function(err){ console.error("Bridge.hello failed",err); });
|
|
217
|
+
Bridge.watch(function(events){
|
|
218
|
+
var changed=[];
|
|
219
|
+
events.forEach(function(ev){ (ev.changes||[]).forEach(function(c){changed.push(c.id)}); });
|
|
220
|
+
return load(changed.length?changed:null);
|
|
221
|
+
}).catch(function(err){ mainEl.innerHTML='<div class="empty">bridge failed: '+esc(err.message)+'</div>'; });
|
|
222
|
+
setInterval(function(){ /* keep-live heartbeat noop */ },60000);
|
|
223
|
+
})();
|
|
224
|
+
</script>
|
|
225
|
+
</body>
|
|
226
|
+
</html>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: View
|
|
3
|
+
title: About this bundle
|
|
4
|
+
entry: views/about.html
|
|
5
|
+
description: A content view with no bundle-data access that demonstrates registered View navigation.
|
|
6
|
+
access: none
|
|
7
|
+
actor: mike/claude
|
|
8
|
+
timestamp: "2026-07-10T00:00:00.000Z"
|
|
9
|
+
---
|
|
10
|
+
A fixture for the `access: none` content-view path: arbitrary self-contained HTML the shell
|
|
11
|
+
frames like any bundle view. Bundle-data requests are denied; registered View navigation remains
|
|
12
|
+
available as a capability-independent shell action.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: View
|
|
3
|
+
title: Pulse — activity feed
|
|
4
|
+
entry: views/pulse.html
|
|
5
|
+
description: Live activity feed — every bundle doc, newest first, with type filters, search, and a markdown detail pane.
|
|
6
|
+
access: bundle-read
|
|
7
|
+
actor: mike/claude
|
|
8
|
+
timestamp: "2026-07-09T00:00:00.000Z"
|
|
9
|
+
---
|
|
10
|
+
A live feed of every document in the bundle, newest first, grouped by recency with type filter
|
|
11
|
+
chips and a search box. Selecting a row opens a detail pane that renders the doc's body as
|
|
12
|
+
markdown, including clickable relative doc links. New writes stream in over the bridge's `change`
|
|
13
|
+
events and land with a brief highlight — no reload.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: View
|
|
3
|
+
title: Roadmap
|
|
4
|
+
entry: views/roadmap.html
|
|
5
|
+
description: Live graph view — Roadmap Items grouped by status, each expandable to its contained tasks with a derived rollup bar.
|
|
6
|
+
access: bundle-read
|
|
7
|
+
actor: mike/claude
|
|
8
|
+
timestamp: "2026-07-09T00:00:01.000Z"
|
|
9
|
+
---
|
|
10
|
+
Roadmap Items grouped into In motion / Committed / Candidate / Done, each expandable to the tasks
|
|
11
|
+
it `contains` (fetched with one call to the bridge's `edges` request) with a derived done/total
|
|
12
|
+
rollup bar. Updates to either an item or one of its tasks stream in live, no reload.
|