본문으로 건너뛰기
cccskills
무료GitHub에 공개

surface

How a downstream app consumes the shared @kolu/surface stack (@kolu/surface · surface-app · surface-remote · surface-mcp) — declaring a typed reactive surface, serving it, consuming it (SolidJS hooks or a CLI), and mirroring a remote surface over ssh. Grounded in the real consumers: kolu, drishti, odu, and the TUIs. Load when wiring a surface server/client/mirror, or reaching for makeSession / a link / the `.use()` hooks. CHANGING the framework is gated separately — see "Changing the framework" below (a paired, CI-green drishti PR pinned to final kolu HEAD).

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

설치 방법 보기

포함된 파일(1)

  • SKILL.md17.1 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Using @kolu/surface (downstream consumer guide)

Declare a typed reactive surface once; the framework derives the Effect RPC group, wires the server, and binds the Solid client hooks. This is the consumer guide — changing the framework needs a paired drishti PR (see "Changing the framework" below).

Who uses it — match the closest consumer, don't hand-roll

ConsumerShapeNotable
kolu (client+server)one browser ⇄ one Node server, ONE wssingle-tier; two sibling surfaces; uses surface+surface-app only (not -remote)
drishti (srid/drishti)browser ⇄ Node ⇄ ssh agent mirrorthe canonical twin; 3 workspaces (common/agent/app)
odu (juspay/odu)CI runner: stdio lanes → unix-socket fan-in → CLI/MCPserve+consume+mirror over every transport at once; surface-mcp projection
kolu-cli (kolu ls/create/send/wait/…)one-shot CLI verbs, no browserthe primary CLI consumer: a PURE padi client — transport-blind {client,dispose}, unix-socket local, ssh remote (--host); no .use() hooks. padi-tui/kaval-tui are the same shape, retiring

The spine (real import paths)

  • Define — defineSurface({cells,collections,streams,events,procedures}) (@kolu/surface/define). Many surfaces over one transport: composeSurfaceContracts(map) + sibling clients, never merged. Several flat GROUPS onto one wire (a sibling bundle beside a keyed map, hand-written root procedures, an unprefixed root surface): mergeDisjointGroups({label: group}) — the one counted merge, which names the tag and both halves on a collision. Never a bare RpcGroup.merge (last-writer-wins, silent).
  • Serve — implementSurface(surface, deps) / implementSurfaces(map, base, perKeyDeps) (@kolu/surface/server) returns a supervised runtime { router, ctx, done, close }. router is the FINAL top-level router — serve it directly (no implement(surface.contract).router(...) re-wrap; a second finalize double-prefixes /surface/surface/… and 404s). Observe done (owned-fault channel) and own close at teardown. Serve on a SHARED caller-owned publisher via implementSurfaceOnPublisher / implementSurfacesOnPublisher. A ROOTED bundle whose sibling set MOVES while it is served is implementRootedSurfaces(core, base, coreDeps) — mount(key, surface, deps) returns a MountedSurface {key, surface, ctx, drop()} (the registration carries its own undo), group/handlers are LIVE reads (read them as a pair in one turn), composition is INCREMENTAL (survivors keep handler identity, stores and sources), and a drop refuses at a record an open connection already captured — in-flight subscriptions included (SurfaceSiblingDropped). Live-ness is a property of the ROOTED door because a rootless wire's reserved probes target the FIRST sibling, which a moving roster can drop.
  • Consume (SolidJS) — surfaceClient(surface, link) / surfaceClients(link, map) (@kolu/surface/solid) → client.cells.X.use({authority,initial,onError}), .collections.X.use({keys,onError}) then .byKey(id)?.() / .keys(), .streams.X.use(inputFn,{onError}) with .pending()/.error(), .events.X.use(inputFn,handler).
  • Consume (CLI/TUI) — no reactive hooks; raw awaited conn.client.surface.<verb>(…) + async-iterator iteration; a live board uses mirrorRemoteSurface(spec, client, {collections,streams}, {log}) (@kolu/surface/mirror) into plain callbacks.

Links (transport, swappable)

websocketLink(ws) (/links/websocket) · stdioLink (/links/stdio) · unixSocketLink({socketPath}) (/links/unix-socket) · directLink(router) (/links/direct, in-process identity, for tests). Serve side: serveOverStdio (/peer-server), serveOverUnixSocket (/unix-socket), and for browsers serveSurfaceApp (@kolu/surface-app/serve) — don't hand-roll a listener: it owns the whole order (origin gate → upgrade → stale-tab check → heartbeat enrolment → serve) plus the shell layers, the bind and the scope-registered teardown, and it reads the inbound frame cap from RPC_MAX_FRAME_BYTES so a consumer cannot undercut the wire. TLS (tls), an HTTP middleware, an app's own routes and a dist-less dev shell (clientDist is optional) are all options — kolu's own listener rides it. Reach past it to the granular acceptSurfaceSocket + serveSurfaceSocket only when the app picks WHICH runtime serves each upgrade — ?host= per-host dispatch (drishti), the one decision the accept seam leaves at the call site. CLIs keep ONE transport-blind Connection = {client, dispose} so every command is written once across local vs ssh. serveSurfaceApp and serveOverUnixSocket take an optional per-face expose (@kolu/surface/expose: exposeFace(surface, map) / exposeFaces for a bundle / exposeRootedFaces for a root beside a bundle, the same ExposeMap serveSurfaceAsMcp takes and the same classifyExpose grammar), so a verb can be reachable over the unix socket and not to browsers. WHICH faces take a map, which gate themselves, and what a key grants are stated once — in that module's header. Read it there rather than from memory.

Mirror a remote surface (drishti / pulam-web / odu)

  1. Dial the host — makeSession({connectOnce: sshConnector<contract>({host, binary, resolveDrvPath})}) (@kolu/surface-remote): long-lived, nix copys the agent closure, runs <bin> --stdio over ssh, reconnects. buildRemotePool fans out N hosts; one-shot CLIs use dialAgentOnce instead.
  2. Mirror inward — pumpRemoteSurface({source, session, makeSink, …}) (-remote) folds the remote agent's frames into a local implementSurface re-serve via a SurfaceSink (makeSink, @kolu/surface/mirror). The parent implements the same surface; a remotely-unobservable cell (e.g. connection state) is parent-authoritative.
  3. Re-serve — the local re-served surface (a FINAL implementSurface router) served on /rpc/ws, accepted via acceptSurfaceSocket (@kolu/surface-app/server). Browsers connect with connectSurface (@kolu/surface-app/solid), which bundles socket + websocketLink + surfaceClient + a default-on liveness heartbeat. connectSurfaces is the multi-surface twin, and its core: {surface, name} slot carries an UNPREFIXED root beside the siblings — typed client, health enrolment under name, and the reserved probes addressed to the root's bare surface/system/*. A rooted app has no reason to hand-assemble the seam. A SIBLING ROSTER CHANGE IS A REDIAL, not an update — Effect RPC bakes the RpcGroup into the client at openWireLink, so new tags need a new wire at both ends; conn.redial(surfaces) owns it, re-using this connection's own options, keeping the root, and dialling the replacement BEFORE releasing the old wire.

Gotchas (hard-won, all real)

  • Procedures call off the FULL link, not the scoped client — surfaceClients per-key .rpc is typed unknown; reach the root link for raw procedures.
  • Raw streaming — unenrolledStreamCall(client.X, input, {signal, onRetry}) (@kolu/surface/client) carries the reconnect (STREAM_RETRY) context; a bare client.X(…) silently loses it. There is no stream namespace (the streaming skill owns the full rule).
  • Consume streams fine-grained — value-bearing → .streams.use() (replace-each-frame); delta-accumulate → mirrorRemoteSurface / createSubscription+reduce. Never coarse-read-and-copy: same-shape frames coalesce and the view freezes.
  • Snapshot-then-deltas + fail-fast — a cell always opens with a snapshot; firstFrameOrThrow (@kolu/surface/first-frame) treats an empty stream as a link failure, never a silent empty.
  • Liveness is on by construction — framework-reserved surface.system.live; connectSurface / HostSession / createServerLifecycle default their watchdog to it (probeSurfaceLive). Don't nominate your own probe unless you mean to.
  • Version skew — gate on isContractVersionCompatible (major.minor), never a string ==.
  • Nix-baked deps — odu declares no @kolu/* in package.json; they're symlinked at Nix build (the bake-in-via-Nix convention). A bare pnpm install won't resolve them.

Reference

Runnable end-to-end: packages/surface/example/ (its mini-ci stdio example is odu's seed). Full API + rationale: each package's README.md. Match the closest consumer above; don't reinvent a primitive the table already shows how to use.

Changing the framework

The consumer guide above is for using the stack. Changing it has two gates and one docs requirement: the drishti pair, the ODU-IMPACT verdict, and the Reference-page update below.

Surface libraries are shared — mirror API changes to drishti

@kolu/surface and its sibling libraries (@kolu/surface-app, @kolu/surface-map, @kolu/surface-remote, and — since drishti's durable agent landed (UW3) — @kolu/surface-daemon and @kolu/surface-daemon-supervisor) are not kolu-internal: they are consumed by drishti as well as kolu. Drishti's daemon serves the frozen control-core fragment, rides daemonHome/daemonMain/frontDaemonOverStdio, converges via convergeAdmit/probeDaemonIdentityFrom, and imports ./upgrade-window.testlib — the daemon packages are production surface for it, gate file included. Drishti's client reads the host map through @kolu/surface-map: EntryState and the entries collection back its host dot, its fleet card, its tab chip and its readiness gate across a dozen files.

How this list is kept honest: it is the set of @kolu/* packages drishti IMPORTS FROM SOURCE, which is not the set it hydrates into node_modules. Re-derive it, never recall it — grep -rhoE '"@kolu/[a-z-]+' packages/*/src --include=*.ts --include=*.tsx | sort -u in a drishti checkout. Today that yields exactly the five above plus @kolu/solid-pwa-install (one import, TabStrip.tsx), while @kolu/log and @kolu/shell-quote are hydrated by Nix and imported by nothing — so they need no gate. solid-pwa-install is deliberately outside this rule (it is not part of the surface stack and its one consumer is a leaf hook); if it ever grows a second consumer or a contract-shaped API, it wants a gate of its own rather than a quiet addition to this one.

Any API-facing change to packages/surface/, packages/surface-app/, packages/surface-map/, packages/surface-remote/, packages/surface-daemon/, or packages/surface-daemon-supervisor/ — exported types, function signatures, the Effect RPC contract (the defineSurface spec, the wire tags an RpcGroup mints from it, a member's payload/success/error Schema), package exports, runtime behaviour a consumer depends on, or anything else that changes how a downstream consumer wires these packages — requires a corresponding PR to github.com/srid/drishti that updates drishti for the change and passes full CI.

  • Open the drishti PR before (or alongside) merging the kolu change, and link the two PRs to each other. Cut and keep the pair branch on CURRENT drishti master: git fetch origin and branch off origin/master at open — never a stale local master — and merge origin/master into the pair branch again at every repin, confirming GitHub reports it MERGEABLE before calling it ready. The recorded failure: a pair PR (drishti#107) cut off a stale base reached the human CONFLICTING — unmergeable at the exact moment it was needed.
  • The drishti PR is not optional and "done" is not "done" until drishti's CI is green against the new surface API.
  • The gate is only satisfied against the final kolu HEAD — re-validate it after any post-gate edit that touches the surface API. A drishti CI green pinned to a pre-gauntlet SHA is stale: the review gauntlet (lens/debate/police) and CI fix-commits routinely reshape the shared API after the gate first passed — e.g. a gauntlet fix dropping a parameter (pumpRemoteSurface's makeSink client arg) silently breaks drishti's consumer ((_client, {seq}) → ({seq})), which a gate validated at the earlier SHA never caught. Whenever the surface API moves after the gate was last green, bump the drishti PR's pin to the final kolu HEAD and re-confirm its CI is green there before calling it done.
  • Purely internal changes (private helpers, tests, comments, refactors with no observable API delta) do not need a drishti PR.
  • The recorded near-miss this list's completeness exists to prevent: juspay/kolu#2129 added an arm to @kolu/surface-map's client-facing EntryState — a change that fails drishti's build outright — while surface-map was absent from the package list above. The gate did not fire, and the pair PR (srid/drishti#134) was opened only because the author grepped drishti's tree by hand. A glob that is nearly right is worse than an obviously partial one: it reads as authoritative, so nobody re-derives it. Hence the grep above, and hence this note.

The third consumer — odu rides a loose pin, and pays with a verdict

odu also consumes @kolu/surface*, but deliberately loosely: it pins kolu by npins and absorbs changes at its own pin bumps — there is no per-PR odu pair (three-repo lockstep was considered and rejected; drishti is the tight twin because it tracks kolu master continuously). The recorded failure this section exists to prevent: a whole campaign of surface changes accumulated with zero odu-impact accounting, so nobody knew what the eventual bump would break until it was scheduled.

  • Every PR the drishti gate applies to also posts an ODU-IMPACT VERDICT in its body — one of breaks-at-bump / adoption-opportunity / none, each grounded by an actual grep of odu's tree at its pinned kolu SHA for consumption of the changed exports (a verdict without the grep is hollow).
  • Any non-none verdict is appended as a line item to the standing ledger — odu#43 — naming the kolu PR, the affected odu site, and the verdict. The ledger, not memory, is what the next pin bump drains.
  • A breaks-at-bump item makes the next bump schedulable work, not someday-work — the coordinator surfaces it; the bump PR's body checks off every drained line item.

Surface public-API changes must update the Reference docs in the same PR

The @kolu/surface* packages each have a Reference page in the four-quadrant Surface docs at website/src/content/surface/. Those pages are the austere, authoritative record of the public API — they are only trustworthy if they move with the code.

Any public-API change to one of these packages — an exported type or function signature, the Effect RPC contract (the defineSurface spec, the wire tags an RpcGroup mints from it, a member's payload/success/error Schema), package exports, a member added to or removed from defineSurface, or any runtime behaviour a documented consumer wires against — must update the matching Reference page in the same PR.

PackageReference page
@kolu/surfacewebsite/src/content/surface/ref-surface.mdx
@kolu/surface-appwebsite/src/content/surface/ref-surface-app.mdx
@kolu/surface-mcpwebsite/src/content/surface/ref-surface-mcp.mdx
@kolu/surface-cliwebsite/src/content/surface/ref-surface-cli.mdx
@kolu/surface-daemonwebsite/src/content/surface/ref-surface-daemon.mdx
@kolu/surface-daemon-supervisorwebsite/src/content/surface/ref-surface-supervisor.mdx
@kolu/surface-remotewebsite/src/content/surface/ref-surface-remote.mdx
@kolu/surface-mapwebsite/src/content/surface/ref-surface-map.mdx
  • Update the affected entries (names, signatures, tables, snippets) so the page describes the new surface, not the old one. If the change touches an invariant, update surface-daemon-invariants.mdx or the relevant Explanation page too.
  • "Done" is not done until the page matches the shipped API. This is the mirror, on the docs side, of the drishti-consumer gate above — the same "the record must not lie" discipline that the docs themselves are built on.
  • Purely internal changes (private helpers, tests, comments, refactors with no observable API delta) do not need a docs update.
  • A new export that a consumer is meant to use is an API change: add it to the Reference page. If the package's README changes shape, keep the ~15-line README (identity + install + one snippet + links into the quadrants) pointing at the Reference page rather than duplicating it.

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

blog-post

무료

Write a kolu blog post grounded in the real build history — mine the Claude Code session logs behind a feature for the actual story (for large efforts, fan out over the transcripts with an ultracode workflow), draft it in the author's voice, and wire it into the Astro site. Use when asked to write a blog post or engineering essay about something that was built, especially one that should read as a narrative of what actually happened rather than invented marketing copy.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

juspay/kolu822026년 10월 21일 업데이트

ci

무료

Run this repo's CI end-to-end — kolu's policy on top of the odu runner: banned opt-out flags, x86_64-linux pinning, pool upkeep, fail-fast surfacing, e2e timing evidence, and the green-gate. Triggers on "run CI", "drive CI", "re-run the pipeline", "close the red check", "warm the pool". Runner mechanics live in the `/odu` skill.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

juspay/kolu822026년 10월 21일 업데이트

Launch the kolu dev server on two RANDOM free ports so it never collides with the running production `kolu.service`, remember the ports for the rest of the session, and tear down only the dev instance. Load before running the app locally — for evidence capture, driving a live kolu with the chrome-devtools MCP, or any `just dev` you'd otherwise run by hand. Triggers on "run kolu locally", "launch the dev server", "boot kolu", "drive a live kolu", "start the app to screenshot it", or before pointing chrome-devtools at a local kolu.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

juspay/kolu822026년 10월 21일 업데이트

diataxis

무료

Classify, write, and audit documentation per Diátaxis (diataxis.fr) — the four-quadrant method (tutorials · how-to guides · reference · explanation). Encodes the compass as a decision procedure, each quadrant's contract as hard rules, and an executable mixed-mode audit. Project-agnostic; the per-repo doc-location mapping lives in a repo rule of the project's own (kolu: the Kolu overlay below). Use when writing any user-facing doc, structuring a docs site, or auditing existing docs for mixed modes.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

juspay/kolu822026년 10월 21일 업데이트

The Dock's agent-state folds — one fenced source of truth in @kolu/terminal-vocab, the load-bearing order≠colour split, and the one "is something happening" question. Use when editing agentProjection or the canvas dock.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

juspay/kolu822026년 10월 21일 업데이트

E2E test conventions — semantic selectors, arrange-steps that prove the process started, runtime markers that cannot match the shell echo, and absolute-path tool invocations. Use when writing or changing a .feature scenario under packages/tests.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

juspay/kolu822026년 10월 21일 업데이트

juspay의 스킬 모두 보기

이 스킬의 문제 신고