Optional PREVIEW console for Claude Code that observes Skill Heaven summons and never changes them: a status entry, a Lens band above the prompt, and a /heaven…

An optional console for Skill Heaven in Claude Code. It shows what a summon returned, whether the skill body actually entered context, and what you are trusting. It observes; it never changes a summon, refuses a tool call, or runs one on its own. Skill Heaven works exactly the same without it.
Status: preview. It is built on Claude Code's early-access Mods API (function hooks, first available in 2.1.293), which may change between releases. It has passed
claude plugin validateand the engine's own test kit on both the terminal and desktop surfaces, and a live probe in a logged-in terminal session on 2.1.294 (below). How it paints in the desktop app has not been probed — treat the desktop look as unconfirmed.
| Surface | What you see |
|---|---|
| Status entry | ◇ entropy ‹‹ [NATIVE] ›› · 2 skills beside your own status line. It is appended; your statusLine setting is never touched. |
| Lens band | Empty by default. After a summon it shows the result in two lines and whether the skill body was read (card returned · body not read or in context · body read by main agent). Inspect opens the pane, Dismiss hides it. It also hides on your next prompt. |
/lens <intent> | Previews which skill would be summoned, with preview: true. Nothing is materialized; the summon engine still logs the query in its own session directory. The preview and its detail appear in the band, not in the conversation. For a single candidate the band offers Summon, which only fills your prompt with /skill-heaven:summon <name> — you press Enter. If a summon tool ignores preview and materializes a skill, the band shows that real summon and the count includes it. |
/heaven | A pane with four sections: Session (receipts, each field labelled observed, reported, inferred or unknown), Scope (what Skill Heaven can see, what is active, rung controls that pre-fill commands), Flow (agents the host reported and what each summoned) and Trust (what this plugin reads and writes). |
Selecting a rung (/skill-heaven:skill-heaven low, /skill-heaven:skill-hell high, /skill-heaven:skill-ultra, /skill-heaven:skill-zero) changes the bracketed reading in the status entry. That reading is a preference you expressed and the console saw; Skill Heaven does not enforce it. /skill-ultra reads [ULTRA]; the Ultra controller is provisioned, not built. The compact status entry shows only [ULTRA] and the skill count — controller unavailable is spelled out in full mode, in the Scope section and in the pane.
Add the marketplace once, then install:
/plugin marketplace add gaia-research/gaia-skill-heaven
/plugin install skill-heaven-console@gaia-skill-heaven
It needs the skill-heaven plugin (or the claude-zero launcher) to have anything to observe; without a summon tool the pane says summon tool: not connected.
A mod installed this way is a terminal action: a local session started from the desktop app loads a user-scope install, but the install command itself is typed in a terminal session.
/skill-* commands you submit, and Read and Agent tool calls — only to observe them./lens preview.)/lens calls the bundled summon tool, which fetches the skill source./lens and /heaven is a row the model can see. It is fixed text and carries nothing a skill source, a tool error or a refusal supplied; all detail stays in the band and the pane.~/.claude. Buttons pre-fill a command for you to submit. All text that came from a skill source or a model is sanitized before it is drawn.status (Settings, or pluginConfigs in settings.json): off, compact (default) or full. off removes the status entry; the band and pane stay available.
/plugin disable skill-heaven-console
/plugin uninstall skill-heaven-console@gaia-skill-heaven
The status entry, band and pane disappear. Skill Heaven is unchanged.
PR #187, macOS, a logged-in session in a visible Herdr pane, both plugins loaded from the checkout with --plugin-dir and a session-only --settings that supplied a user statusLine:
statusLine; neither replaces the other. The host draws it as ⚠ skill-heaven-console: ◇ entropy ‹‹ [NATIVE] ›› · 0 skills; the ⚠ <plugin>: prefix is the host's./lens shows the band and leaves the count at 0 skills. /skill-heaven:summon takes it to 1 skill. A Read of the materialized SKILL.md moves the band from card returned · body not read to in context · body read by main agent.tool.call and $.tool.call as { ref, result, text }, with result the server's JSON text as a string. structuredContent appears nowhere; the console reads the JSON text.$.tool.list() names deferred MCP tools (the summon tool is behind ToolSearch and is listed), but only once MCP has connected: at session.start it lists no MCP tool at all, so /lens re-checks for a few seconds before saying "not connected".tool.call and turn.complete inside a subagent carry its agentId, and the Agent tool's result carries the same id. Flow attributes the subagent's summons to it.command.run fires before prompt.submit, and prompt.submit.text is the raw typed text (/skill-heaven:skill-hell high), not the expanded body. A subagent's hand-back arrives as prompt.submit with origin.kind: "peer"; the console ignores it./skill-zero or /skill-hell high resolves to the plugin's portable skill and the host says it "can only be invoked by Claude". Every console pre-fill uses /skill-heaven:<surface>.~/.claude/settings.json, settings.local.json and installed_plugins.json were byte-identical. claude plugin validate resolves the console's calls to $.state, $.ui.*, $.prompt.fill, $.tool.list/call (the /lens preview) and $.command.register: no file system, settings or process calls.Still unconfirmed:
node scripts/build-status.mjs # regenerate hooks/status-model.mjs from packages/status
claude plugin validate plugins/skill-heaven-console
claude plugin test plugins/skill-heaven-console
hooks/status-model.mjs is generated and committed; CI checks it against a fresh build. Do not edit it.
hooks/register.tsx 443 lines1// Skill Heaven console (preview) — a Claude Code mod.
2//
3// It projects the one status model (status-model.mjs) into the desktop and the
4// terminal: a status entry, the Lens band above the prompt, and the /heaven
5// pane. It OBSERVES summons; it never rewrites, refuses or issues one. Every
6// tool.call hook below calls `next`, records what came back, and returns the
7// result untouched. A button pre-fills a command for the person to submit; the
8// UI is not an authority channel. Nothing is written to disk or to settings.
9//
10// The engine reads this file's source: every helper that is handed `$` is a
11// function declared at the top level of this file.
12
13import { atom, read, update } from 'claude-code'
14import type { EngineInterface, PluginOptions, Register, RenderSurface } from 'claude-code'
15
16import {
17 eventFromSummonResult,
18 renderStatusSegments,
19 sanitizeDisplay,
20 selectionFromCommand,
21 toPlain,
22} from './status-model.mjs'
23import type { StatusMode, SummonEvent } from './status-model.mjs'
24import { PANE_ID } from './meta.ts'
25import {
26 LAUNCHER_SUMMON_TOOL,
27 MARKETPLACE_SUMMON_TOOL,
28 SUMMON_TOOL,
29 agentCallLabel,
30 agentLabelFor,
31 agentReturned,
32 dropAgentCall,
33 finishAgentCall,
34 fromData,
35 initialState,
36 noteAgentId,
37 qualifyCommand,
38 recordEvent,
39 recordRead,
40 recordSelection,
41 startAgentCall,
42 structuredOf,
43 toData,
44} from './model.ts'
45import type { ConsoleSection, ConsoleState } from './model.ts'
46import { renderBand, renderPane } from './views.tsx'
47import type { BandActions, PaneActions } from './views.tsx'
48
49// The shape tag: a reload whose code names another tag starts from a clean state
50// instead of reading a value an older layout wrote.
51const stateAtom = atom({ plugin: 'skill-heaven-console', key: 'state' } as const, toData(initialState()), {
52 shape: 'skill-heaven-console/state@2',
53})
54
55const SECTIONS: readonly ConsoleSection[] = ['session', 'scope', 'flow', 'trust']
56
57// True while this console's own /lens preview is in flight, so the summon
58// observer does not record the same call twice.
59let lensInFlight = false
60
61function modeOf(options: PluginOptions): StatusMode {
62 const v = options.status
63 return v === 'off' || v === 'full' ? v : 'compact'
64}
65
66const isoOf = (ms: number): string => new Date(ms).toISOString()
67
68/** Re-set the status entry from state. `off` removes it; it never touches statusLine. */
69function paintStatus($: EngineInterface, mode: StatusMode, state: ConsoleState): void {
70 $.ui.status(mode === 'off' ? undefined : toPlain(renderStatusSegments(state.status, mode)))
71}
72
73async function load($: EngineInterface): Promise<ConsoleState> {
74 return fromData(await read($, stateAtom))
75}
76
77/** Change state, then re-set the status entry. */
78async function change($: EngineInterface, mode: StatusMode, fn: (s: ConsoleState) => ConsoleState): Promise<ConsoleState> {
79 const next = fromData(await update($, stateAtom, (data) => toData(fn(fromData(data)))))
80 paintStatus($, mode, next)
81 return next
82}
83
84async function openPane($: EngineInterface): Promise<void> {
85 await $.ui.open({ id: PANE_ID, title: 'Skill Heaven' })
86}
87
88/** Fire and forget, but never leave a rejection unhandled. */
89function later(work: Promise<unknown>): void {
90 work.catch(() => {})
91}
92
93/** Pre-fill the prompt without wiping a draft; say so when the box could not take it.
94 * Skill Heaven commands are filled in the spelling the session accepts. */
95async function fillPrompt($: EngineInterface, command: string): Promise<void> {
96 const text = qualifyCommand(command)
97 try {
98 const filled = await $.prompt.fill({ text, mode: 'insert' })
99 if (!filled.isFilled) $.ui.toast(`Type this: ${text}`)
100 } catch {
101 $.ui.toast(`Type this: ${text}`)
102 }
103}
104
105async function copyText($: EngineInterface, text: string, surface: RenderSurface): Promise<void> {
106 try {
107 const copied = await $.ui.copy({ text, surface })
108 if (!copied.isCopied) $.ui.toast(`Copy this: ${text}`)
109 } catch {
110 $.ui.toast(`Copy this: ${text}`)
111 }
112}
113
114type Preview = { event: SummonEvent; reached: boolean } | { notConnected: true }
115
116/**
117 * Ask the summon tool what it would summon, with `preview: true`.
118 *
119 * A tool is absent only when it is not in the session's tool list. A call to a
120 * tool the list names that is rejected was aborted or failed: that is an error
121 * event, and says nothing about whether the tool is connected.
122 */
123/** MCP servers connect in the background after session.start: observed on
124 * 2.1.294, `$.tool.list()` names no MCP tool at session.start and a /lens typed in
125 * the first seconds found no summon tool. An absent tool is checked again, a few
126 * times, before the band says "not connected". */
127const CONNECT_CHECKS = 4
128const CONNECT_WAIT_MS = 1500
129
130async function previewSummon($: EngineInterface, query: string): Promise<Preview> {
131 for (let check = 1; ; check++) {
132 const outcome = await previewOnce($, query)
133 if (!('notConnected' in outcome) || check >= CONNECT_CHECKS) return outcome
134 try {
135 await $.clock.sleep(CONNECT_WAIT_MS)
136 } catch {
137 return outcome
138 }
139 }
140}
141
142async function previewOnce($: EngineInterface, query: string): Promise<Preview> {
143 const listed: string[] = []
144 try {
145 for (const t of await $.tool.list()) if (SUMMON_TOOL.test(t.name)) listed.push(t.name)
146 } catch {
147 // listing is a convenience; the two known spellings are tried next
148 }
149 const names = [...listed]
150 for (const known of [MARKETPLACE_SUMMON_TOOL, LAUNCHER_SUMMON_TOOL]) if (!names.includes(known)) names.push(known)
151 const at = isoOf(await $.clock.now())
152 const failed = (text: string): Preview => ({
153 event: eventFromSummonResult(undefined, { at, agent: null, preview: true, query }, { isError: true, text }),
154 reached: false,
155 })
156 for (const name of names) {
157 let result
158 try {
159 result = await $.tool.call({
160 tool: name as `mcp__${string}__${string}`,
161 query,
162 surface: 'any',
163 preview: true,
164 })
165 } catch {
166 if (listed.includes(name)) return failed('preview aborted or failed')
167 continue // not in this session
168 }
169 if (result.deny !== undefined) return failed(`preview refused: ${result.deny}`)
170 const failure = result.isError === true ? ({ isError: true, text: result.text ?? null } as const) : undefined
171 return {
172 event: eventFromSummonResult(failure ? undefined : structuredOf(result), { at, agent: null, preview: true, query }, failure),
173 reached: true,
174 }
175 }
176 return { notConnected: true }
177}
178
179/** The one-line result of /lens. The model reads it, so it is fixed text: nothing a
180 * skill source, a tool error or a refusal supplied ever goes in it. The detail is in the band. */
181function lensResultText(event: SummonEvent): string {
182 if (event.kind === 'summoned' && event.delta > 0) {
183 return 'Lens: this summon tool ignored preview; a skill was materialized. See the band.'
184 }
185 if (event.kind === 'previewed') return 'Lens preview shown in the band. Nothing was summoned.'
186 if (event.kind === 'no-match') return 'Lens: no match. Nothing was summoned.'
187 return 'Lens: the preview failed; see the band.'
188}
189
190export const register: Register = (on, options) => {
191 const mode = modeOf(options)
192
193 /* ----------------------------------------------------------------------- *
194 * Session
195 * ----------------------------------------------------------------------- */
196
197 on('session.start', async ($, e, next) => {
198 await $.command.register({ name: 'heaven', description: 'Open the Skill Heaven console', argumentHint: '[session|scope|flow|trust]' })
199 await $.command.register({
200 name: 'lens',
201 description: 'Preview which skill would be summoned — nothing is materialized',
202 argumentHint: '<intent>',
203 })
204 let hostVersion: string | null = null
205 try {
206 hostVersion = (await $.session.version()).version
207 } catch {
208 hostVersion = null
209 }
210 await change($, mode, (s) => ({ ...s, hostVersion }))
211 return next(e)
212 })
213
214 // A /clear ends the conversation without a new session.start: what the console
215 // counted belongs to the conversation that is gone.
216 on('session.end', async ($, e, next) => {
217 if (e.reason === 'clear') {
218 try {
219 await change($, mode, (s) => ({ ...initialState(), hostVersion: s.hostVersion }))
220 } catch {
221 // leave the old state rather than break the session
222 }
223 }
224 return next(e)
225 })
226
227 /* ----------------------------------------------------------------------- *
228 * Commands
229 * ----------------------------------------------------------------------- */
230
231 on('command.run', { command: 'heaven' }, async ($, e) => {
232 const arg = e.args.trim().toLowerCase()
233 const section = SECTIONS.find((s) => s === arg)
234 if (section) await change($, mode, (s) => ({ ...s, section }))
235 await openPane($)
236 // The model reads this row: fixed text only.
237 return { text: 'Skill Heaven console opened.' }
238 })
239
240 on('command.run', { command: 'lens' }, async ($, e) => {
241 const query = sanitizeDisplay(e.args, 200)
242 if (query === '') return { text: 'Usage: /lens <intent>. Nothing was summoned.' }
243 await change($, mode, (s) => ({ ...s, band: { kind: 'looking', query } }))
244 lensInFlight = true
245 try {
246 let outcome: Preview
247 try {
248 outcome = await previewSummon($, query)
249 } finally {
250 lensInFlight = false
251 }
252 if ('notConnected' in outcome) {
253 await change($, mode, (s) => ({
254 ...s,
255 band: { kind: 'not-connected' },
256 status: { ...s.status, summonTool: 'not-connected' },
257 }))
258 return { text: 'Lens: the summon tool is not connected.' }
259 }
260 const { event, reached } = outcome
261 await change($, mode, (s) => recordEvent(s, event, null, 'lens', reached))
262 return { text: lensResultText(event) }
263 } finally {
264 // however it ended, the in-flight band does not stay up
265 try {
266 await change($, mode, (s) => (s.band !== null && s.band.kind === 'looking' ? { ...s, band: null } : s))
267 } catch {
268 // nothing more to do
269 }
270 }
271 })
272
273 /* ----------------------------------------------------------------------- *
274 * Observing (never changing) tool calls
275 * ----------------------------------------------------------------------- */
276
277 // Agents: the host reports an agent id on calls made inside a subagent, and the
278 // Agent tool starts one. Both only add rows to Flow.
279 on('tool.call', async ($, e, next) => {
280 const args = e as unknown as Record<string, unknown>
281 const isAgentCall = String(e.tool) === 'Agent' || String(e.tool) === 'Task'
282 const key = typeof e.tool_use_id === 'string' ? e.tool_use_id : ''
283 const background = args.run_in_background !== false
284 try {
285 const reported = typeof e.agentId === 'string' ? e.agentId : null
286 if (reported !== null) {
287 const seen = await load($)
288 if (!seen.agentIdsSeen || !seen.agents.some((a) => a.id === reported)) {
289 await change($, mode, (s) => noteAgentId(s, reported))
290 }
291 }
292 if (isAgentCall && key) {
293 const label = agentCallLabel(args.subagent_type, args.description)
294 await change($, mode, (s) => startAgentCall(s, key, label, background))
295 }
296 } catch {
297 // observing must never get in the way of the call
298 }
299 let r
300 try {
301 r = await next(e)
302 } catch (error) {
303 // aborted: the call started nothing
304 if (isAgentCall && key) later(change($, mode, (s) => dropAgentCall(s, key)))
305 throw error
306 }
307 try {
308 if (isAgentCall && key) {
309 if (r.deny !== undefined || r.isError === true) {
310 // refused or failed: no agent was started, so no row is left
311 await change($, mode, (s) => dropAgentCall(s, key))
312 } else {
313 const record = (r as { result?: unknown }).result
314 const rec = typeof record === 'object' && record !== null ? (record as Record<string, unknown>) : {}
315 const id = typeof rec.agentId === 'string' ? rec.agentId : typeof rec.agent_id === 'string' ? rec.agent_id : null
316 await change($, mode, (s) => finishAgentCall(s, key, id, background))
317 }
318 }
319 } catch {
320 // see above
321 }
322 return r
323 }).catch(($, e, next) => next(e))
324
325 // Summons: record what came back. The result is returned exactly as received.
326 on('tool.call', { tool: /^mcp__(?:plugin_skill-heaven_)?skill-summon__summon$/ }, async ($, e, next) => {
327 const r = await next(e)
328 try {
329 const args = e as unknown as Record<string, unknown>
330 const preview = args.preview === true
331 if (r.deny === undefined && !(lensInFlight && preview)) {
332 const at = isoOf(await $.clock.now())
333 const agent = typeof e.agentId === 'string' ? e.agentId : null
334 const failure = r.isError === true ? ({ isError: true, text: r.text ?? null } as const) : undefined
335 const event = eventFromSummonResult(
336 failure ? undefined : structuredOf(r),
337 { at, agent, preview, query: typeof args.query === 'string' ? args.query : undefined },
338 failure,
339 )
340 await change($, mode, (s) => recordEvent(s, event, agent, 'tool'))
341 }
342 } catch {
343 // see above
344 }
345 return r
346 }).catch(($, e, next) => next(e))
347
348 // Body reads: a Read of a summoned skill's materialized SKILL.md is what puts
349 // it in context. The result is returned exactly as received.
350 on('tool.call', { tool: 'Read' }, async ($, e, next) => {
351 const r = await next(e)
352 try {
353 const args = e as unknown as Record<string, unknown>
354 // a partial read (an offset or a limit) is not the body read
355 const isWhole = (args.offset === undefined || args.offset === null) && (args.limit === undefined || args.limit === null)
356 if (isWhole && r.deny === undefined && r.isError !== true && typeof e.file_path === 'string') {
357 const path = e.file_path
358 const current = await load($)
359 const reader = agentLabelFor(current, typeof e.agentId === 'string' ? e.agentId : null)
360 if (recordRead(current, path, reader) !== current) {
361 await change($, mode, (s) => recordRead(s, path, reader))
362 }
363 }
364 } catch {
365 // see above
366 }
367 return r
368 }).catch(($, e, next) => next(e))
369
370 on('turn.complete', async ($, e, next) => {
371 try {
372 if (typeof e.agentId === 'string') {
373 const id = e.agentId
374 await change($, mode, (s) => agentReturned(s, id))
375 }
376 } catch {
377 // see above
378 }
379 return next(e)
380 })
381
382 // Rung selection: the person typed /skill-heaven, /skill-hell, /skill-ultra or
383 // /skill-zero. The console reads it; the prompt goes on unchanged.
384 on('prompt.submit', async ($, e, next) => {
385 // A subagent's hand-back arrives as a prompt too (origin `peer`, observed on
386 // 2.1.294): it is not the person's next prompt and selects no rung.
387 if ((e as { origin?: { kind?: string } }).origin?.kind === 'peer') return next(e)
388 try {
389 const reading = selectionFromCommand(e.text)
390 await change($, mode, (s) => {
391 const seen = reading ? recordSelection(s, reading, e.text.trim()) : s
392 // The band hides on the next prompt (a /lens in flight keeps its own).
393 return seen.band !== null && seen.band.kind !== 'looking' ? { ...seen, band: null } : seen
394 })
395 } catch {
396 // see above
397 }
398 return next(e)
399 }).catch(($, e, next) => next(e))
400
401 /* ----------------------------------------------------------------------- *
402 * Drawing
403 * ----------------------------------------------------------------------- */
404
405 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
406 if (e.props.hasSurvey) return next(e)
407 const state = await load($)
408 if (state.band === null) return next(e)
409 const actions: BandActions = {
410 inspect: (id) => {
411 later(change($, mode, (s) => ({ ...s, section: 'session', openEntry: id })).then(() => openPane($)))
412 },
413 dismiss: () => {
414 later(change($, mode, (s) => ({ ...s, band: null })))
415 },
416 summon: (name) => {
417 later(fillPrompt($, `/summon ${name}`))
418 },
419 }
420 const tree = renderBand($.ui.resolve(e), state, actions, e.props.bodyColumns)
421 return tree ?? next(e)
422 })
423
424 on('ui.render', { component: 'Pane', requestId: 'skill-heaven' }, async ($, e) => {
425 const state = await load($)
426 const actions: PaneActions = {
427 section: (section) => {
428 later(change($, mode, (s) => ({ ...s, section })))
429 },
430 toggle: (id) => {
431 later(change($, mode, (s) => ({ ...s, openEntry: s.openEntry === id ? null : id })))
432 },
433 fill: (text) => {
434 later(fillPrompt($, text))
435 },
436 copy: (text) => {
437 later(copyText($, text, e.surface))
438 },
439 }
440 return renderPane($.ui.resolve(e), state, actions, e.props.bodyColumns)
441 })
442}
443hooks/status-model.mjs 828 lines1// GENERATED by scripts/build-status.mjs from packages/status/src — do not edit.
2// One status model for every projection (docs/CONTROL-PLANE.md).
3// packages/status/src/model.ts
4var RUNGS = ["zero", "low", "med", "high", "xhigh", "max", "ultra"];
5var RUNG_BAND = Object.freeze({
6 zero: "zero",
7 low: "heaven",
8 med: "heaven",
9 high: "hell",
10 xhigh: "hell",
11 max: "hell",
12 ultra: "ultra"
13});
14function bandOf(rung) {
15 return RUNG_BAND[rung];
16}
17function readingRung(reading) {
18 if (reading.kind === "selected") return reading.rung;
19 if (reading.kind === "boot" && reading.posture === "product-floor") return "zero";
20 return null;
21}
22function isRung(value) {
23 return typeof value === "string" && RUNGS.includes(value);
24}
25function emptyStatus() {
26 return {
27 reading: { kind: "unknown" },
28 skills: null,
29 summons: null,
30 lastArrival: null,
31 controller: { kind: "not-selected" },
32 summonTool: "unknown"
33 };
34}
35
36// packages/status/src/sanitize.ts
37var ANSI = /\u001b(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007\u001b]*(?:\u0007|\u001b\\)|[@-Z\\-_])/g;
38var CONTROLS = /[\u0000-\u001f\u007f-\u009f]/g;
39var BIDI = /[--]/g;
40var RESERVED = /* @__PURE__ */ new Map([
41 ["\u25C7", "*"],
42 ["\u25C6", "*"],
43 ["\u2039", "<"],
44 ["\u203A", ">"],
45 ["\xD7", "x"]
46]);
47var DEFAULT_FIELD_LIMIT = 48;
48function sanitizeDisplay(value, limit = DEFAULT_FIELD_LIMIT) {
49 if (value === null || value === void 0) return "";
50 let text = String(value).replace(ANSI, "").replace(BIDI, "").replace(CONTROLS, " ");
51 let out = "";
52 for (const ch of text) out += RESERVED.get(ch) ?? ch;
53 text = out.replace(/\s+/g, " ").trim();
54 const chars = Array.from(text);
55 if (limit > 0 && chars.length > limit) return chars.slice(0, Math.max(1, limit - 1)).join("") + "\u2026";
56 return text;
57}
58function cellWidth(text) {
59 let width = 0;
60 for (const ch of text) {
61 const code = ch.codePointAt(0) ?? 0;
62 width += isWide(code) ? 2 : 1;
63 }
64 return width;
65}
66function isWide(code) {
67 return code >= 4352 && code <= 4447 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65103 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 127744 && code <= 129791 || code >= 131072 && code <= 262141;
68}
69
70// packages/status/src/tokens.ts
71var ROLE_COLORS = Object.freeze({
72 umbrella: { hex: "#a58ae0", ansi256: 141, ansi16: 35, cssVar: "--sh-violet" },
73 ultra: { hex: "#d9b25c", ansi256: 179, ansi16: 33, cssVar: "--sh-ultra" },
74 zero: { hex: "#5fc2d6", ansi256: 80, ansi16: 36, cssVar: "--sh-zero" },
75 heaven: { hex: "#6f96d8", ansi256: 68, ansi16: 34, cssVar: "--sh-heaven" },
76 hell: { hex: "#e094c8", ansi256: 175, ansi16: 95, cssVar: "--sh-hell" },
77 ink: { hex: "#eeebe6", ansi256: 255, ansi16: 39, cssVar: "--sh-bone" },
78 dim: { hex: "#9a9691", ansi256: 246, ansi16: 90, cssVar: "--sh-dim" },
79 arbor: { hex: "#55c878", ansi256: 77, ansi16: 32, cssVar: "--sh-arbor" },
80 stop: { hex: "#c81e1e", ansi256: 160, ansi16: 31, cssVar: "--sh-stop" },
81 amber: { hex: "#e0b45c", ansi256: 179, ansi16: 33, cssVar: "--sh-amber" }
82});
83var GROUND_HEX = "#1b1a1c";
84
85// packages/status/src/render.ts
86var SEP = { text: " \xB7 ", role: "dim" };
87var seg = (text, role) => ({ text, role });
88function readingToken(reading) {
89 switch (reading.kind) {
90 case "selected":
91 return reading.rung.toUpperCase();
92 case "boot":
93 return reading.posture === "product-floor" ? "ZERO" : reading.posture === "curated" ? "CURATED" : "NATIVE";
94 case "native":
95 return "NATIVE";
96 case "unknown":
97 return "?";
98 }
99}
100function readingRole(reading) {
101 const rung = readingRung(reading);
102 if (!rung) return "ink";
103 const band = RUNG_BAND[rung];
104 return band === "zero" ? "zero" : band === "heaven" ? "heaven" : band === "hell" ? "hell" : "ultra";
105}
106function isUltra(status) {
107 return readingRung(status.reading) === "ultra";
108}
109function skillsText(n, short) {
110 if (n === null) return short ? "?" : "? skills";
111 if (short) return String(n);
112 return `${n} ${n === 1 ? "skill" : "skills"}`;
113}
114function readingSegments(status) {
115 return [
116 seg("\u2039\u2039", "heaven"),
117 seg(" ", "dim"),
118 seg(`[${readingToken(status.reading)}]`, readingRole(status.reading)),
119 seg(" ", "dim"),
120 seg("\u203A\u203A", "hell")
121 ];
122}
123function headSegments(status, withWord) {
124 const ultra = isUltra(status);
125 const out = [];
126 if (status.fixture || status.controller.kind === "fixture") out.push(seg("FIXTURE", "amber"), seg(" ", "dim"));
127 out.push(seg(ultra ? "\u25C6" : "\u25C7", ultra ? "ultra" : "umbrella"));
128 if (withWord) out.push(seg(" entropy", "umbrella"));
129 out.push(seg(" ", "dim"), ...readingSegments(status));
130 return out;
131}
132function controllerSegments(controller, opts = { progress: true }) {
133 switch (controller.kind) {
134 case "not-selected":
135 return [];
136 case "unavailable":
137 return [SEP, seg("controller unavailable", "dim")];
138 case "reported":
139 case "fixture": {
140 const out = [SEP, seg(sanitizeDisplay(controller.state, 24).toUpperCase(), "ultra")];
141 if (controller.effective) {
142 const band = RUNG_BAND[controller.effective];
143 const role = band === "heaven" ? "heaven" : "hell";
144 out.push(SEP, seg("now ", "dim"), seg(controller.effective.toUpperCase(), role), seg(band === "heaven" ? " \u2039" : " \u203A", role));
145 }
146 if (opts.progress && controller.progress) {
147 out.push(SEP, seg(`${controller.progress.completed}/${controller.progress.total}`, "ink"));
148 }
149 return out;
150 }
151 }
152}
153function statusLevels(status, mode) {
154 if (mode === "off") return [];
155 const ultra = isUltra(status);
156 const controller = ultra ? status.controller : { kind: "not-selected" };
157 const skills = [SEP, seg(skillsText(status.skills, false), "ink")];
158 const summons = status.summons === null ? [] : [seg(" / ", "dim"), seg(`${status.summons} ${status.summons === 1 ? "summon" : "summons"}`, "dim")];
159 const arrival = status.lastArrival ? [SEP, seg(`+${sanitizeDisplay(status.lastArrival, 32)}`, "ink")] : [];
160 const full = [
161 [...headSegments(status, true), ...skills, ...summons, ...controllerSegments(controller), ...arrival],
162 [...headSegments(status, true), ...skills, ...summons, ...controllerSegments(controller)],
163 [...headSegments(status, true), ...skills, ...controllerSegments(controller)],
164 [...headSegments(status, true), ...skills, ...controllerSegments(controller, { progress: false })]
165 ];
166 const compact = [
167 [...headSegments(status, true), ...skills],
168 [...headSegments(status, false), SEP, seg(skillsText(status.skills, true), "ink")],
169 [...headSegments(status, false)]
170 ];
171 const bare = [];
172 if (status.fixture || status.controller.kind === "fixture") bare.push(seg("FIXTURE", "amber"), seg(" ", "dim"));
173 bare.push(seg(`[${readingToken(status.reading)}]`, readingRole(status.reading)));
174 return mode === "full" ? [...full, ...compact, bare] : [...compact, bare];
175}
176function segmentsWidth(segments) {
177 return segments.reduce((sum, s) => sum + cellWidth(s.text), 0);
178}
179function renderStatusSegments(status, mode, width) {
180 const levels = statusLevels(status, mode);
181 if (levels.length === 0) return [];
182 if (width === void 0 || !Number.isFinite(width)) return levels[0];
183 return levels.find((level) => segmentsWidth(level) <= width) ?? levels[levels.length - 1];
184}
185function directionGlyph(direction) {
186 if (direction === "converge") return seg("\u2039", "heaven");
187 if (direction === "explore") return seg("\u203A", "hell");
188 return seg("\u25C7", "umbrella");
189}
190function formatScore(value) {
191 if (value === null || !Number.isFinite(value)) return null;
192 const clamped = Math.max(0, Math.min(1, value));
193 return clamped >= 1 ? "1.00" : clamped.toFixed(2).replace(/^0/, "");
194}
195function formatMs(ms) {
196 if (ms === null || !Number.isFinite(ms) || ms < 0) return null;
197 return ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(2)}s`;
198}
199var CLOSE_CALL_MARGIN = 0.1;
200function receiptDetail(skill, delta) {
201 const parts = [];
202 if (skill.matchKind !== "unknown") parts.push(skill.matchKind);
203 const score = formatScore(skill.score);
204 if (score) parts.push(score);
205 const margin = formatScore(skill.margin);
206 if (margin) parts.push(`\u0394 ${margin}`);
207 if (skill.cache !== "unknown") parts.push(skill.cache);
208 const ms = formatMs(skill.ms);
209 if (ms) parts.push(ms);
210 if (delta !== null) parts.push(`+${delta}`);
211 return [seg(" ", "dim"), seg(parts.join(" \xB7 ") || "no ranking disclosure", "dim")];
212}
213function eventLines(event) {
214 const lines = eventLinesInner(event);
215 if (event.evidence === "fixture" && lines[0]) lines[0] = [seg("FIXTURE", "amber"), seg(" ", "dim"), ...lines[0]];
216 return lines;
217}
218function eventLinesInner(event) {
219 switch (event.kind) {
220 case "summoned": {
221 const [first, ...rest] = event.skills;
222 const line1 = [directionGlyph(event.direction), seg(" summoned ", "dim")];
223 line1.push(seg(first ? sanitizeDisplay(first.name, 40) : "nothing", "ink"));
224 if (rest.length > 0) line1.push(seg(` +${rest.length} more`, "dim"));
225 const line2 = first ? receiptDetail(first, event.delta) : [seg(" ", "dim"), seg("no skill in result", "dim")];
226 if (event.sourceHealth.kind === "stale") line2.push(SEP, seg("? index stale", "amber"));
227 return [line1, line2];
228 }
229 case "previewed": {
230 const [first] = event.skills;
231 const line1 = [seg("\u25C7", "umbrella"), seg(" lens ", "dim")];
232 if (event.skills.length === 1 && first) line1.push(seg(sanitizeDisplay(first.name, 40), "ink"));
233 else line1.push(seg(`${event.skills.length} candidates`, "ink"));
234 const parts = [seg(" ", "dim"), seg("nothing materialized", "dim")];
235 if (first && event.skills.length > 1) parts.push(SEP, seg(`top ${sanitizeDisplay(first.name, 32)}`, "dim"));
236 if (first?.margin !== null && first?.margin !== void 0 && first.margin < CLOSE_CALL_MARGIN && event.skills.length > 1) {
237 parts.push(SEP, seg(`\u0394 ${formatScore(first.margin)} close call (retrieval)`, "dim"));
238 }
239 return [line1, parts];
240 }
241 case "no-match": {
242 const detail = event.considered === null ? "0 admitted" : `${event.considered} considered \xB7 0 admitted`;
243 const reason = event.reason ? ` \xB7 ${sanitizeDisplay(event.reason, 60)}` : "";
244 const tail = event.preview ? " \xB7 nothing materialized" : "";
245 return [
246 [seg("\u25C7", "umbrella"), seg(event.preview ? " lens " : " summon ", "dim"), seg("\xD7", "ink"), seg(" no match", "ink")],
247 [seg(" ", "dim"), seg(detail + reason + tail, "dim")]
248 ];
249 }
250 case "unavailable":
251 return [
252 [seg("\u25C7", "umbrella"), seg(" summon ", "dim"), seg("?", "amber"), seg(" unavailable", "ink")],
253 [seg(" ", "dim"), seg(sanitizeDisplay(event.reason, 72), "dim")]
254 ];
255 case "error":
256 return [
257 [seg("\u25C7", "umbrella"), seg(" summon ", "dim"), seg("!", "stop"), seg(" failed", "ink")],
258 [seg(" ", "dim"), seg(sanitizeDisplay(event.reason, 72), "dim")]
259 ];
260 }
261}
262function noticeLines(notice) {
263 if (notice.kind === "looking") {
264 return [
265 [seg("\u25C7", "umbrella"), seg(" lens ", "dim"), seg("looking\u2026", "ink")],
266 [seg(" ", "dim"), seg(`${sanitizeDisplay(notice.query, 48)} \xB7 nothing materialized`, "dim")]
267 ];
268 }
269 return [
270 [seg("\u25C7", "umbrella"), seg(" summon ", "dim"), seg("?", "amber"), seg(" not connected", "ink")],
271 [seg(" ", "dim"), seg("the summon tool is not available in this session", "dim")]
272 ];
273}
274function describeReading(reading) {
275 switch (reading.kind) {
276 case "selected":
277 return `${reading.rung}, the rung you selected (not enforced)`;
278 case "boot":
279 return reading.posture === "product-floor" ? "zero, the Skill Zero floor set at launch" : reading.posture === "curated" ? "curated, set at launch" : "native, your harness's own skills";
280 case "native":
281 return "native, your harness's own skills";
282 case "unknown":
283 return "unknown";
284 }
285}
286function describeStatus(status) {
287 const parts = [`Skill entropy reading: ${describeReading(status.reading)}.`];
288 parts.push(
289 status.skills === null ? "Skills in this session: unknown." : `${status.skills} ${status.skills === 1 ? "skill" : "skills"} materialized in this session.`
290 );
291 if (isUltra(status)) {
292 const c = status.controller;
293 if (c.kind === "unavailable") parts.push("Ultra controller: unavailable; nothing is choosing for you yet.");
294 else if (c.kind === "reported" || c.kind === "fixture") {
295 parts.push(`Ultra controller: ${sanitizeDisplay(c.state, 24).toLowerCase()}${c.effective ? `, working at ${c.effective}` : ""}.`);
296 }
297 }
298 if (status.fixture || status.controller.kind === "fixture") parts.unshift("Fixture, design state only.");
299 return parts.join(" ");
300}
301function describeEvent(event) {
302 const text = describeEventInner(event);
303 return event.evidence === "fixture" ? `Fixture, design state only. ${text}` : text;
304}
305function describeEventInner(event) {
306 const how = event.direction === "converge" ? "converging" : event.direction === "explore" ? "exploring" : event.direction === "manual" ? "by hand" : "";
307 switch (event.kind) {
308 case "summoned": {
309 const names = event.skills.map((s) => sanitizeDisplay(s.name, 40)).join(", ") || "nothing";
310 return `Summoned ${names}${how ? `, ${how}` : ""}. A card was returned; ${event.delta} added to the session.`;
311 }
312 case "previewed":
313 return `Lens preview: ${event.skills.length} candidate${event.skills.length === 1 ? "" : "s"}; nothing materialized.`;
314 case "no-match":
315 return event.preview ? "Lens preview found no match. Nothing was materialized." : "Summon found no match. Nothing was added to the session.";
316 case "unavailable":
317 return `Summon unavailable: ${sanitizeDisplay(event.reason, 72)}.`;
318 case "error":
319 return `Summon failed: ${sanitizeDisplay(event.reason, 72)}.`;
320 }
321}
322function resolveColorDepth(env, isTTY = true) {
323 if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return "none";
324 if (!isTTY && env.FORCE_COLOR === void 0) return "none";
325 const term = env.TERM ?? "";
326 if (term === "dumb") return "none";
327 const colorterm = (env.COLORTERM ?? "").toLowerCase();
328 if (colorterm === "truecolor" || colorterm === "24bit") return "truecolor";
329 if (term.includes("256")) return "256";
330 return "16";
331}
332function sgr(role, depth) {
333 const c = ROLE_COLORS[role];
334 if (depth === "truecolor") {
335 const n = parseInt(c.hex.slice(1), 16);
336 return `\x1B[38;2;${n >> 16 & 255};${n >> 8 & 255};${n & 255}m`;
337 }
338 if (depth === "256") return `\x1B[38;5;${c.ansi256}m`;
339 return `\x1B[${c.ansi16}m`;
340}
341function paintAnsi(segments, depth) {
342 if (depth === "none") return toPlain(segments);
343 return segments.map((s) => s.role === "dim" && s.text.trim() === "" ? s.text : `${sgr(s.role, depth)}${s.text}\x1B[39m`).join("");
344}
345function toPlain(segments) {
346 return segments.map((s) => s.text).join("");
347}
348
349// packages/status/src/adapters.ts
350var isRec = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
351var str = (v) => typeof v === "string" && v.length > 0 ? v : null;
352var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
353function readingFromProfileManifest(manifest) {
354 if (!isRec(manifest) || manifest.schema !== "claude-zero/profile@1") return { kind: "unknown" };
355 const posture = manifest.posture;
356 if (posture === "product-floor" || posture === "curated" || posture === "native") {
357 return { kind: "boot", posture, source: "launcher-manifest" };
358 }
359 return { kind: "unknown" };
360}
361var BAND_COMMANDS = [
362 { command: "skill-zero", rungs: ["zero"], fallback: "zero" },
363 { command: "skill-heaven", rungs: ["low", "med"], fallback: "low" },
364 { command: "skill-hell", rungs: ["high", "xhigh", "max"], fallback: "high" },
365 { command: "skill-ultra", rungs: ["ultra"], fallback: "ultra" }
366];
367function selectionFromCommand(text) {
368 const match = /^\s*\/(?:[a-z0-9-]+:)?(skill-zero|skill-heaven|skill-hell|skill-ultra)(?:\s+(\S+))?\s*$/i.exec(text);
369 if (!match) return null;
370 const entry = BAND_COMMANDS.find((b) => b.command === match[1].toLowerCase());
371 const arg = match[2]?.toLowerCase();
372 if (!arg) return { kind: "selected", rung: entry.fallback, source: "observed-command" };
373 if (entry.command === "skill-zero") return arg === "all" ? { kind: "selected", rung: "zero", source: "observed-command" } : null;
374 if (isRung(arg) && entry.rungs.includes(arg)) return { kind: "selected", rung: arg, source: "observed-command" };
375 return null;
376}
377function withReading(status, reading) {
378 const ultra = reading.kind === "selected" && reading.rung === "ultra";
379 const boot = status.boot ?? (status.reading.kind === "selected" || status.reading.kind === "unknown" ? void 0 : status.reading);
380 const controller = ultra ? status.controller.kind === "reported" ? status.controller : { kind: "unavailable" } : { kind: "not-selected" };
381 return { ...status, reading, controller, ...boot ? { boot } : {} };
382}
383function skillsFromSessionManifest(manifest) {
384 if (!isRec(manifest) || !Array.isArray(manifest.skills)) return { skills: null, last: null };
385 const entries = manifest.skills.filter((s) => isRec(s) && typeof s.id === "string");
386 if (entries.length !== manifest.skills.length) return { skills: null, last: null };
387 const last = entries.length > 0 ? entries[entries.length - 1] : null;
388 return { skills: entries.length, last: last ? str(last.name) ?? str(last.id) : null };
389}
390function directionFromSurface(surface) {
391 if (surface === "any") return "manual";
392 if (surface === "heaven") return "converge";
393 if (surface === "hell") return "explore";
394 return "unspecified";
395}
396function laneOf(skill) {
397 if (skill.invocation === "human") return "human-led";
398 if (skill.invocation === "model") return "model-led";
399 if (skill.invocation === "any") return "unspecified";
400 return "unknown";
401}
402function receiptFrom(skill, stage, agent) {
403 const retrieval = isRec(skill.retrieval) ? skill.retrieval : {};
404 const matchKind = retrieval.matchKind === "exact" || retrieval.matchKind === "ranked" ? retrieval.matchKind : "unknown";
405 const cacheRaw = skill.cacheState ?? skill.cache;
406 const totalSeconds = num(skill.totalSeconds);
407 const installability = isRec(skill.installability) ? str(skill.installability.state) ?? "unknown" : "unknown";
408 return {
409 id: str(skill.id) ?? "unknown",
410 name: str(skill.name) ?? str(skill.id) ?? "unknown",
411 stage,
412 matchKind,
413 score: num(retrieval.score) ?? num(skill.score),
414 margin: num(retrieval.margin) ?? num(skill.margin),
415 cache: cacheRaw === "cold" || cacheRaw === "warm" ? cacheRaw : "unknown",
416 ms: totalSeconds === null ? null : Math.round(totalSeconds * 1e3),
417 lane: laneOf(skill),
418 source: str(skill.source),
419 repoUrl: str(skill.repoUrl),
420 ref: str(skill.branch),
421 sourceUrl: str(skill.sourceUrl),
422 subpath: str(skill.subpath),
423 sha256: str(skill.sha256),
424 path: str(skill.path),
425 installability,
426 agent
427 };
428}
429function sourceHealthOf(result) {
430 const ranking = isRec(result.ranking) ? result.ranking : null;
431 if (!ranking || typeof ranking.stale !== "boolean") return { kind: "unknown" };
432 const age = num(ranking.indexAgeDays);
433 const indexAgeDays = age === null ? null : Math.floor(age);
434 return ranking.stale ? { kind: "stale", indexAgeDays } : { kind: "fresh", indexAgeDays };
435}
436function arborOf(result, summoned) {
437 const arbor = isRec(result.arbor) ? result.arbor : null;
438 if (!arbor) return "unknown";
439 if (arbor.publicationState === "unavailable" || arbor.publicationState === "unreadable") return "unavailable";
440 if (arbor.publicationState !== "loaded") return "unknown";
441 return summoned.some((s) => isRec(s.arbor) && s.arbor.join === "content-pinned") ? "governed-record" : "no-record";
442}
443var NO_MATCH_REASON = {
444 no_candidates: "no candidates in the source",
445 below_floor: "nothing cleared the relevance floor",
446 all_filtered: "every candidate was outside this lane"
447};
448function eventFromSummonResult(structured, call = {}, failure) {
449 const at = call.at ?? null;
450 const agent = call.agent ?? null;
451 if (failure?.isError) {
452 const text = failure.text ?? "the summon tool reported an error";
453 const unavailable = /unresolvable|unreachable|ENOTFOUND|EAI_AGAIN|fetch failed|network|offline/i.test(text);
454 return {
455 kind: unavailable ? "unavailable" : "error",
456 direction: "unspecified",
457 query: call.query ?? "",
458 preview: call.preview === true,
459 reason: text,
460 at,
461 evidence: "observed"
462 };
463 }
464 if (!isRec(structured)) {
465 return {
466 kind: "error",
467 direction: "unspecified",
468 query: call.query ?? "",
469 preview: call.preview === true,
470 reason: "the summon result had no structured content",
471 at,
472 evidence: "observed"
473 };
474 }
475 const direction = directionFromSurface(structured.surface);
476 const query = str(structured.query) ?? call.query ?? "";
477 const sourceHealth = sourceHealthOf(structured);
478 if (structured.noMatch !== null && structured.noMatch !== void 0) {
479 const noMatch = isRec(structured.noMatch) ? structured.noMatch : {};
480 const reason = typeof noMatch.reason === "string" ? NO_MATCH_REASON[noMatch.reason] ?? null : null;
481 return { kind: "no-match", direction, query, preview: call.preview === true, considered: null, reason, at, evidence: "reported", sourceHealth };
482 }
483 const summoned = Array.isArray(structured.summoned) ? structured.summoned.filter(isRec) : [];
484 const previewed = Array.isArray(structured.previewed) ? structured.previewed.filter(isRec) : [];
485 if (summoned.length === 0 && (previewed.length > 0 || call.preview === true)) {
486 return {
487 kind: "previewed",
488 direction,
489 query,
490 skills: previewed.map((s) => receiptFrom(s, "previewed", agent)),
491 delta: 0,
492 preview: true,
493 at,
494 evidence: "reported",
495 sourceHealth
496 };
497 }
498 const skipped = Array.isArray(structured.skipped) ? structured.skipped : [];
499 if (summoned.length === 0 && skipped.length > 0) {
500 const first = skipped[0];
501 const why = isRec(first) ? str(first.why) ?? str(first.reason) ?? str(first.error) : str(first);
502 return {
503 kind: "error",
504 direction,
505 query,
506 preview: call.preview === true,
507 reason: why ? `nothing materialized: ${why}` : "nothing materialized: every candidate was skipped",
508 at,
509 evidence: "reported"
510 };
511 }
512 const composition = isRec(structured.composition) && structured.composition.mode === "relevance-only" ? "relevance-only" : "unknown";
513 return {
514 kind: "summoned",
515 direction,
516 query,
517 skills: summoned.map((s) => receiptFrom(s, "materialized", agent)),
518 delta: summoned.length,
519 preview: false,
520 at,
521 evidence: "reported",
522 composition,
523 arbor: arborOf(structured, summoned),
524 sourceHealth
525 };
526}
527function reduceStatus(status, event) {
528 if (event.kind === "previewed" || event.kind !== "summoned" && event.preview) return status;
529 const summons = status.summons === null ? null : status.summons + 1;
530 if (event.kind !== "summoned") return { ...status, summons };
531 const skills = status.skills === null ? null : status.skills + event.delta;
532 const last = event.skills[event.skills.length - 1];
533 return { ...status, skills, summons, lastArrival: last ? last.name : status.lastArrival };
534}
535function markRead(event, readPath) {
536 if (event.kind !== "summoned") return event;
537 const norm = readPath.replace(/\\/g, "/");
538 let changed = false;
539 const skills = event.skills.map((s) => {
540 if (!s.path || s.stage === "in-context") return s;
541 let root = s.path.replace(/\\/g, "/");
542 while (root.endsWith("/")) root = root.slice(0, -1);
543 if (norm === `${root}/SKILL.md`) {
544 changed = true;
545 return { ...s, stage: "in-context" };
546 }
547 return s;
548 });
549 return changed ? { ...event, skills } : event;
550}
551var STEERING_DECISIONS = {
552 hold: "HOLD",
553 explore: "EXPLORE",
554 recover: "RECOVER",
555 reopen: "REOPEN",
556 checkpoint: "CHECKPOINT",
557 close: "CLOSE",
558 stop: "STOP"
559};
560function controllerFromSteeringEntry(entry) {
561 if (!isRec(entry) || typeof entry.decision !== "string") return null;
562 const state = STEERING_DECISIONS[entry.decision];
563 if (!state) return null;
564 const to = isRec(entry.to) ? entry.to : null;
565 const rung = to && isRung(to.rung) && to.rung !== "zero" && to.rung !== "ultra" ? to.rung : void 0;
566 const from = isRec(entry.from) && typeof entry.from.rung === "string" ? entry.from.rung : null;
567 const explanation = str(entry.explanation);
568 return {
569 kind: "reported",
570 source: "steering-trace",
571 state,
572 ...rung ? { effective: rung } : {},
573 ...from && explanation && entry.changed === true ? { transition: { from: from.toUpperCase(), to: state, reason: explanation } } : {}
574 };
575}
576var PROPOSED_CONTROLLER_SCHEMA = "skill-heaven/controller-status@0";
577function controllerFromCampaignStatus(value) {
578 if (!isRec(value) || value.schema !== PROPOSED_CONTROLLER_SCHEMA) return null;
579 const allowed = /* @__PURE__ */ new Set(["schema", "state", "effective", "progress", "transition"]);
580 if (Object.keys(value).some((k) => !allowed.has(k))) return null;
581 if (typeof value.state !== "string" || !/^[A-Z][A-Z0-9 ·_-]{0,31}$/.test(value.state)) return null;
582 const out = { kind: "reported", source: "campaign", state: value.state };
583 if (value.effective !== void 0) {
584 if (!isRung(value.effective) || value.effective === "zero" || value.effective === "ultra") return null;
585 out.effective = value.effective;
586 }
587 if (value.progress !== void 0) {
588 const p = value.progress;
589 if (!isRec(p) || !Number.isInteger(p.completed) || !Number.isInteger(p.total)) return null;
590 const completed = p.completed;
591 const total = p.total;
592 if (completed < 0 || total < 1 || completed > total) return null;
593 out.progress = { completed, total };
594 }
595 if (value.transition !== void 0) {
596 const t = value.transition;
597 if (!isRec(t) || typeof t.from !== "string" || typeof t.to !== "string" || typeof t.reason !== "string") return null;
598 out.transition = { from: t.from, to: t.to, reason: t.reason };
599 }
600 return out;
601}
602
603// packages/status/src/compat.ts
604var AGENT_PLUGIN_INSTALL = {
605 posix: "curl -fsSL https://gaia-research.github.io/gaia-skill-heaven/install-agent-plugin.sh | sh",
606 windows: "irm https://gaia-research.github.io/gaia-skill-heaven/install-agent-plugin.ps1 | iex",
607 root: "$HOME/.local/share/gaia-skill-heaven-agent-plugin",
608 marketplace: "$HOME/.local/share/gaia-skill-heaven-agent-plugin/marketplace",
609 plugin: "$HOME/.local/share/gaia-skill-heaven-agent-plugin/marketplace/plugins/skill-heaven",
610 uninstall: "$HOME/.local/share/gaia-skill-heaven-agent-plugin/uninstall.sh"
611};
612var LAUNCHER_INSTALL = {
613 posix: "curl -fsSL https://gaia-research.github.io/gaia-skill-heaven/install.sh | sh",
614 windows: "irm https://gaia-research.github.io/gaia-skill-heaven/install.ps1 | iex",
615 uninstall: "$HOME/.local/share/gaia-skill-heaven/uninstall.sh"
616};
617var REPO = "https://github.com/gaia-research/gaia-skill-heaven/blob/main";
618var HARNESS_PATHS = [
619 {
620 id: "claude",
621 name: "Claude Code",
622 bin: "claude",
623 chip: "verified",
624 probedVersion: "2.1.288",
625 evidence: "Fresh-install receipt, 76/76 checks, GitHub main @ fdfd94e.",
626 evidenceHref: `${REPO}/docs/RELEASE-ACCEPTANCE.md`,
627 needsInstaller: false,
628 commands: [
629 "/plugin marketplace add gaia-research/gaia-skill-heaven",
630 "/plugin install skill-heaven@gaia-skill-heaven"
631 ],
632 inHarness: true,
633 blocked: null,
634 update: "claude plugin marketplace update gaia-skill-heaven && claude plugin update skill-heaven@gaia-skill-heaven",
635 remove: ["claude plugin uninstall skill-heaven@gaia-skill-heaven"],
636 statusIntegration: "APPEND",
637 statusNote: "The optional console (preview) adds a status entry beside yours \u2014 it never replaces your statusLine. Probed live in the terminal on 2.1.294; its desktop paint is not probed.",
638 launcher: "claude-zero",
639 // 2.1.294 refuses a bare /summon: it resolves to the portable skill, which is user-invocable: false.
640 firstRun: "/skill-heaven:summon <what you need>"
641 },
642 {
643 id: "codex",
644 name: "Codex",
645 bin: "codex",
646 chip: "compatible",
647 probedVersion: "0.161.0",
648 evidence: "Live smoke on 0.161.0: plugin added, codex mcp list resolves skill-summon, a summon call completed. Headless codex exec asks approval for MCP calls. Surfaces last probed on 0.146.0.",
649 evidenceHref: `${REPO}/plugins/skill-heaven/PROBE.md`,
650 needsInstaller: true,
651 commands: [
652 `codex plugin marketplace add "${AGENT_PLUGIN_INSTALL.marketplace}"`,
653 "codex plugin add skill-heaven@gaia-skill-heaven"
654 ],
655 inHarness: false,
656 blocked: null,
657 update: "Re-run the installer, then reinstall in Codex \u2014 Codex keeps its own cached copy.",
658 remove: ["codex plugin remove skill-heaven@gaia-skill-heaven", AGENT_PLUGIN_INSTALL.uninstall],
659 statusIntegration: "UNSUPPORTED",
660 statusNote: "Codex has no public status contribution API. Summon receipts still print in the transcript.",
661 launcher: "codex-zero",
662 firstRun: "/summon <what you need>"
663 },
664 {
665 id: "pi",
666 name: "Pi",
667 bin: "pi",
668 chip: "compatible",
669 probedVersion: "1.0.4",
670 evidence: "Live probe on 1.0.4: pi install, five commands, summon materialized a skill, the four surfaces expanded. The adapter keeps its own summon tool rather than Pi's MCP runtime, which an MCP extension can replace.",
671 evidenceHref: `${REPO}/plugins/skill-heaven/dev.skill-heaven.pi/PROBE.md`,
672 needsInstaller: true,
673 commands: [`pi install "${AGENT_PLUGIN_INSTALL.plugin}" --approve`],
674 inHarness: false,
675 blocked: null,
676 update: "Re-run the installer, then pi update.",
677 remove: [`pi remove "${AGENT_PLUGIN_INSTALL.plugin}"`, AGENT_PLUGIN_INSTALL.uninstall],
678 statusIntegration: "NATIVE SLOT",
679 statusNote: "Pi's status API takes the canonical entropy line (probed on 1.0.4, appended under its own key); the plugin does not draw it yet. The pi-zero extension draws a widget.",
680 launcher: "pi-zero",
681 firstRun: "/summon <what you need>"
682 },
683 {
684 id: "grok",
685 name: "Grok",
686 bin: "grok",
687 chip: "compatible",
688 probedVersion: "1.0.5",
689 evidence: "Live probe on 1.0.5. On 1.0.46, grok plugin install and details find the skills, commands and MCP server (static: no signed-in session to run a summon).",
690 evidenceHref: `${REPO}/plugins/skill-heaven/PROBE.md`,
691 needsInstaller: true,
692 commands: [`grok plugin install "${AGENT_PLUGIN_INSTALL.plugin}" --trust`],
693 inHarness: false,
694 blocked: null,
695 update: "Re-run the installer, then grok plugin update.",
696 remove: ["grok plugin uninstall skill-heaven", AGENT_PLUGIN_INSTALL.uninstall],
697 statusIntegration: "UNSUPPORTED",
698 statusNote: "A command-backed status line is possible on Grok but not built.",
699 launcher: "grok-zero",
700 firstRun: "/summon <what you need>"
701 },
702 {
703 id: "hermes",
704 name: "Hermes",
705 bin: "hermes",
706 chip: "compatible",
707 probedVersion: "0.20.0",
708 evidence: "Live probe, repeated on 0.20.0 (2026-10-08): plugin installed and enabled, a summon call completed.",
709 evidenceHref: `${REPO}/plugins/skill-heaven/PROBE.md`,
710 needsInstaller: true,
711 commands: [`hermes plugins install "file://${AGENT_PLUGIN_INSTALL.plugin}" --enable`],
712 inHarness: false,
713 blocked: null,
714 update: "Re-run the installer, then reinstall in Hermes. Remove the Hermes registration with Hermes's own plugin manager.",
715 // Hermes's own removal command was not verified on the machine that wrote
716 // this table (its `--help` could not run), so none is printed.
717 remove: [AGENT_PLUGIN_INSTALL.uninstall],
718 statusIntegration: "UNSUPPORTED",
719 statusNote: "Hermes has no stable public status contribution. Receipts print in the transcript.",
720 launcher: "hermes-zero",
721 firstRun: "/summon <what you need>"
722 },
723 {
724 id: "agy",
725 name: "Antigravity",
726 bin: "agy",
727 chip: "compatible",
728 probedVersion: "1.3.1",
729 evidence: "Live probe on 1.3.1 with your real HOME: agy plugin install loads the skills, commands and the summon server; /skill-heaven:summon and the four surfaces ran.",
730 evidenceHref: `${REPO}/packages/agy-zero/PROBE.md`,
731 needsInstaller: true,
732 commands: [`agy plugin install "${AGENT_PLUGIN_INSTALL.plugin}"`],
733 inHarness: false,
734 blocked: null,
735 update: "Re-run the installer, then agy plugin uninstall skill-heaven and install it again \u2014 Antigravity keeps its own copy.",
736 remove: ["agy plugin uninstall skill-heaven", AGENT_PLUGIN_INSTALL.uninstall],
737 statusIntegration: "UNSUPPORTED",
738 statusNote: "A stacked status command is the target; not built.",
739 launcher: "agy-zero",
740 // Antigravity namespaces plugin skills: its / menu lists /skill-heaven:summon, and a bare /summon is not a command.
741 firstRun: "/skill-heaven:summon <what you need>"
742 },
743 {
744 id: "other",
745 name: "Another Agent Plugins client",
746 bin: null,
747 chip: "unverified",
748 probedVersion: null,
749 evidence: "The package follows Agent Plugins 1.0.0. Your client owns registration; it has not been probed here.",
750 evidenceHref: "https://agent-plugins.org/specification",
751 needsInstaller: true,
752 commands: [],
753 inHarness: false,
754 blocked: `Point your client's own plugin install at ${AGENT_PLUGIN_INSTALL.plugin}. There is no universal registration command.`,
755 update: "Re-run the installer; your client may keep its own cached copy.",
756 remove: [AGENT_PLUGIN_INSTALL.uninstall],
757 statusIntegration: "UNSUPPORTED",
758 statusNote: "Depends on your client.",
759 launcher: null,
760 firstRun: "/summon <what you need>, or the spelling your client lists for the skill-heaven plugin"
761 }
762];
763var CHIP_LABEL = {
764 verified: "Verified",
765 compatible: "Compatible",
766 partial: "Partial",
767 unverified: "Unverified",
768 "needs-local-probe": "Needs local probe"
769};
770var CHIP_MEANING = {
771 verified: "A fresh install was verified end to end on the pinned version.",
772 compatible: "A live probe passed on the pinned version. Yours may differ.",
773 partial: "Part of the plugin loads; something is proven missing.",
774 unverified: "Not probed. The package is portable; this client is untested.",
775 "needs-local-probe": "Could not be established without a logged-in session."
776};
777function harnessById(id) {
778 const found = HARNESS_PATHS.find((h) => h.id === id);
779 if (!found) throw new Error(`unknown harness ${id}`);
780 return found;
781}
782export {
783 AGENT_PLUGIN_INSTALL,
784 CHIP_LABEL,
785 CHIP_MEANING,
786 CLOSE_CALL_MARGIN,
787 DEFAULT_FIELD_LIMIT,
788 GROUND_HEX,
789 HARNESS_PATHS,
790 LAUNCHER_INSTALL,
791 PROPOSED_CONTROLLER_SCHEMA,
792 ROLE_COLORS,
793 RUNGS,
794 RUNG_BAND,
795 bandOf,
796 cellWidth,
797 controllerFromCampaignStatus,
798 controllerFromSteeringEntry,
799 controllerSegments,
800 describeEvent,
801 describeReading,
802 describeStatus,
803 directionFromSurface,
804 emptyStatus,
805 eventFromSummonResult,
806 eventLines,
807 formatMs,
808 formatScore,
809 harnessById,
810 isRung,
811 markRead,
812 noticeLines,
813 paintAnsi,
814 readingFromProfileManifest,
815 readingRung,
816 readingToken,
817 reduceStatus,
818 renderStatusSegments,
819 resolveColorDepth,
820 sanitizeDisplay,
821 segmentsWidth,
822 selectionFromCommand,
823 skillsFromSessionManifest,
824 statusLevels,
825 toPlain,
826 withReading
827};
828hooks/meta.ts 16 lines1// What the Trust section states about the installed code. A mod cannot read its
2// own manifest, so these are written here and a repository test holds each to
3// the version in its plugin.json.
4
5/** plugins/skill-heaven/.claude-plugin/plugin.json `version`. */
6export const SKILL_HEAVEN_VERSION = '0.1.2'
7
8/** plugins/skill-heaven-console/.claude-plugin/plugin.json `version`. */
9export const CONSOLE_VERSION = '0.1.0'
10
11/** The pane's id, the status values' plugin name. */
12export const PANE_ID = 'skill-heaven'
13
14/** What a logged-in probe established about this console (PR #187), for Trust. */
15export const CONSOLE_PROBE = 'the console was probed live in the terminal on 2.1.294; desktop paint is not probed'
16hooks/model.ts 347 lines1// The console's pure logic: how observed facts fold into session state.
2//
3// No `$`, no engine, no I/O here. Every function takes a ConsoleState and
4// returns the next one; the hooks in register.tsx do the observing and the
5// engine does the keeping. The meaning of every field comes from the one status
6// model (status-model.mjs), never from this file.
7
8import {
9 emptyStatus,
10 markRead,
11 reduceStatus,
12 sanitizeDisplay,
13 withReading,
14} from './status-model.mjs'
15import type { EntropyReading, SkillHeavenStatus, SkillReceipt, SummonEvent } from './status-model.mjs'
16import type { ConsoleAgent, ConsoleBand, ConsoleEntryData, ConsoleSection, ConsoleStateData } from '../types'
17
18export type { ConsoleAgent, ConsoleBand, ConsoleSection }
19
20/** The state contract (types/index.d.ts) keeps `status` and `event` opaque because
21 * the engine wants that file self-contained; these are their real types, and
22 * `fromData` / `toData` are the only place the two meet. */
23export type ConsoleEntry = Omit<ConsoleEntryData, 'event'> & { event: SummonEvent }
24export type ConsoleState = Omit<ConsoleStateData, 'status' | 'entries'> & {
25 status: SkillHeavenStatus
26 entries: ConsoleEntry[]
27}
28export const fromData = (data: ConsoleStateData): ConsoleState => data as unknown as ConsoleState
29export const toData = (state: ConsoleState): ConsoleStateData => state as unknown as ConsoleStateData
30
31export const MAX_ENTRIES = 50
32/** Agents kept in Flow, newest last. */
33export const MAX_AGENTS = 100
34/** Skills stored per entry; the rest are counted in `omitted`. */
35export const MAX_SKILLS_PER_ENTRY = 20
36
37/** The summon tool, however the session spells it:
38 * `mcp__plugin_skill-heaven_skill-summon__summon` (marketplace install) or
39 * `mcp__skill-summon__summon` (launcher). Anchored at both ends: a server that
40 * merely ends in "skill-summon" (`mcp__evil-skill-summon__summon`) is not it.
41 * register.tsx spells the same pattern as a literal in its tool.call matcher;
42 * a repository test holds the two equal. */
43export const SUMMON_TOOL = /^mcp__(?:plugin_skill-heaven_)?skill-summon__summon$/
44export const MARKETPLACE_SUMMON_TOOL = 'mcp__plugin_skill-heaven_skill-summon__summon'
45export const LAUNCHER_SUMMON_TOOL = 'mcp__skill-summon__summon'
46
47/** The console watches the whole session, so zero is honest at the start. */
48export function initialState(): ConsoleState {
49 return {
50 status: {
51 ...emptyStatus(),
52 reading: { kind: 'native', source: 'no-launcher' },
53 skills: 0,
54 summons: 0,
55 summonTool: 'unknown',
56 },
57 entries: [],
58 band: null,
59 agents: [],
60 agentIdsSeen: false,
61 section: 'session',
62 openEntry: null,
63 selectedFrom: null,
64 hostVersion: null,
65 seq: 0,
66 }
67}
68
69/* ------------------------------------------------------------------------- *
70 * Reading a tool result
71 * ------------------------------------------------------------------------- */
72
73type Rec = Record<string, unknown>
74const isRec = (v: unknown): v is Rec => typeof v === 'object' && v !== null && !Array.isArray(v)
75
76const looksLikeSummon = (v: unknown): v is Rec =>
77 isRec(v) && ('summoned' in v || 'previewed' in v || 'noMatch' in v)
78
79/**
80 * The summon tool's `structuredContent`, wherever the host put it.
81 *
82 * Observed on Claude Code 2.1.294 (PR #187, a `tool.call` hook and the console's
83 * own `$.tool.call`): an MCP tool's result is `{ ref, result, text }` where
84 * `result` is a STRING — the server's JSON text block — and `text` is the same
85 * text; `structuredContent` appears nowhere. The object forms stay as
86 * fallbacks for a host that does pass it through.
87 */
88export function structuredOf(r: unknown): unknown {
89 if (!isRec(r)) return undefined
90 const result = r.result
91 if (isRec(result)) {
92 if (looksLikeSummon(result.structuredContent)) return result.structuredContent
93 if (looksLikeSummon(result)) return result
94 }
95 if (looksLikeSummon(r.structuredContent)) return r.structuredContent
96 const texts: string[] = []
97 if (typeof r.text === 'string') texts.push(r.text)
98 if (typeof result === 'string' && result !== r.text) texts.push(result)
99 if (isRec(result) && Array.isArray(result.content)) {
100 for (const block of result.content) {
101 if (isRec(block) && typeof block.text === 'string') texts.push(block.text)
102 }
103 }
104 for (const text of texts) {
105 try {
106 const parsed: unknown = JSON.parse(text)
107 if (looksLikeSummon(parsed)) return parsed
108 } catch {
109 // not all JSON: the object may be followed by resource-link text
110 }
111 const embedded = firstJsonObject(text)
112 if (looksLikeSummon(embedded)) return embedded
113 }
114 return undefined
115}
116
117/** The first balanced `{ ... }` in `text` that parses as JSON. Strings are skipped
118 * over so a brace inside one does not unbalance the scan. */
119export function firstJsonObject(text: string): unknown {
120 for (let start = text.indexOf('{'); start !== -1; start = text.indexOf('{', start + 1)) {
121 let depth = 0
122 let inString = false
123 for (let i = start; i < text.length; i++) {
124 const ch = text[i]
125 if (inString) {
126 if (ch === '\\') i++
127 else if (ch === '"') inString = false
128 } else if (ch === '"') inString = true
129 else if (ch === '{') depth++
130 else if (ch === '}' && --depth === 0) {
131 try {
132 return JSON.parse(text.slice(start, i + 1))
133 } catch {
134 break // not JSON from here; try the next opening brace
135 }
136 }
137 }
138 }
139 return undefined
140}
141
142/* ------------------------------------------------------------------------- *
143 * Agents (Flow)
144 * ------------------------------------------------------------------------- */
145
146const shortId = (id: string) => sanitizeDisplay(id, 12)
147
148/** A label for an agent the host named only by id. */
149export function agentLabelFor(state: ConsoleState, id: string | null | undefined): string {
150 if (id === null || id === undefined) return 'main agent'
151 const row = state.agents.find((a) => a.id === id)
152 return row ? row.label : `agent ${shortId(id)}`
153}
154
155/** subagent type + the call's own description, both sanitized (they are the model's words). */
156export function agentCallLabel(subagentType: unknown, description: unknown): string {
157 const type = typeof subagentType === 'string' && subagentType ? sanitizeDisplay(subagentType, 24) : 'agent'
158 const desc = typeof description === 'string' && description ? sanitizeDisplay(description, 48) : ''
159 return desc ? `${type} "${desc}"` : type
160}
161
162/** The host reported an agent id on a call: remember that it did, and the agent. */
163export function noteAgentId(state: ConsoleState, id: string): ConsoleState {
164 if (state.agents.some((a) => a.id === id)) return state.agentIdsSeen ? state : { ...state, agentIdsSeen: true }
165 // An Agent call that has not been given its id yet and is still open: leave it for finishAgentCall.
166 const agents: ConsoleAgent[] = [
167 ...state.agents,
168 { key: id, id, label: `agent ${shortId(id)}`, state: 'running' as const },
169 ].slice(-MAX_AGENTS)
170 return { ...state, agentIdsSeen: true, agents }
171}
172
173export function startAgentCall(state: ConsoleState, key: string, label: string, background: boolean): ConsoleState {
174 if (state.agents.some((a) => a.key === key)) return state
175 return {
176 ...state,
177 agents: [...state.agents, { key, id: null, label, state: background ? ('started' as const) : ('running' as const) }].slice(-MAX_AGENTS),
178 }
179}
180
181/** The Agent call was refused or failed: it started nothing, so it leaves no row. */
182export function dropAgentCall(state: ConsoleState, key: string): ConsoleState {
183 return state.agents.some((a) => a.key === key) ? { ...state, agents: state.agents.filter((a) => a.key !== key) } : state
184}
185
186/** The Agent call's `next` resolved. The result may carry the new agent's id. */
187export function finishAgentCall(state: ConsoleState, key: string, agentId: string | null, background: boolean): ConsoleState {
188 // The agent may have shown up by id (a stub row) before its call resolved: keep one, labelled row.
189 const agents = state.agents
190 .filter((a) => agentId === null || a.key !== agentId || a.key === key)
191 .map((a) =>
192 a.key === key ? { ...a, id: agentId ?? a.id, state: background ? ('started' as const) : ('returned' as const) } : a,
193 )
194 return { ...state, agents, agentIdsSeen: state.agentIdsSeen || agentId !== null }
195}
196
197/** The agent's own loop finished (its `turn.complete`). */
198export function agentReturned(state: ConsoleState, agentId: string): ConsoleState {
199 if (!state.agents.some((a) => a.id === agentId && a.state !== 'returned')) return state
200 return { ...state, agents: state.agents.map((a) => (a.id === agentId ? { ...a, state: 'returned' as const } : a)) }
201}
202
203/* ------------------------------------------------------------------------- *
204 * Events
205 * ------------------------------------------------------------------------- */
206
207/** Keep a bounded number of skills per entry; the rest are counted, not stored. */
208function bounded(event: SummonEvent): { event: SummonEvent; omitted: number } {
209 if ((event.kind === 'summoned' || event.kind === 'previewed') && event.skills.length > MAX_SKILLS_PER_ENTRY) {
210 return { event: { ...event, skills: event.skills.slice(0, MAX_SKILLS_PER_ENTRY) }, omitted: event.skills.length - MAX_SKILLS_PER_ENTRY }
211 }
212 return { event, omitted: 0 }
213}
214
215/**
216 * Fold one observed summon-tool call into state. `reached` is false when the
217 * call never got an answer from the tool (a /lens preview that was aborted or
218 * refused): that says nothing about whether the tool is connected.
219 */
220export function recordEvent(
221 state: ConsoleState,
222 event: SummonEvent,
223 agent: string | null,
224 via: ConsoleEntry['via'],
225 reached = true,
226): ConsoleState {
227 const id = state.seq + 1
228 const kept = bounded(event)
229 const entry: ConsoleEntry = {
230 id,
231 event: kept.event,
232 omitted: kept.omitted,
233 agent,
234 via,
235 readBy: 'skills' in kept.event ? kept.event.skills.map(() => null) : [],
236 }
237 const entries = [...state.entries, entry].slice(-MAX_ENTRIES)
238 const withAgent = agent !== null ? noteAgentId(state, agent) : state
239 const status = reduceStatus(state.status, event)
240 return {
241 ...withAgent,
242 status: reached ? { ...status, summonTool: 'connected' } : status,
243 entries,
244 band: { kind: 'event', id },
245 seq: id,
246 }
247}
248
249/** A Read of a path: any summoned skill whose materialized SKILL.md it is, is now in context. */
250export function recordRead(state: ConsoleState, path: string, reader: string): ConsoleState {
251 let changed = false
252 const entries = state.entries.map((entry) => {
253 const next = markRead(entry.event, path)
254 if (next === entry.event) return entry
255 changed = true
256 const readBy = [...entry.readBy]
257 if (next.kind === 'summoned' && entry.event.kind === 'summoned') {
258 const before = entry.event.skills
259 next.skills.forEach((skill, i) => {
260 if (skill.stage === 'in-context' && before[i]?.stage !== 'in-context') readBy[i] = reader
261 })
262 }
263 return { ...entry, event: next, readBy }
264 })
265 return changed ? { ...state, entries } : state
266}
267
268export function recordSelection(state: ConsoleState, reading: EntropyReading, typed: string): ConsoleState {
269 return { ...withStatusReading(state, reading), selectedFrom: sanitizeDisplay(typed, 40) }
270}
271
272function withStatusReading(state: ConsoleState, reading: EntropyReading): ConsoleState {
273 return { ...state, status: withReading(state.status, reading) }
274}
275
276export function entryById(state: ConsoleState, id: number): ConsoleEntry | undefined {
277 return state.entries.find((e) => e.id === id)
278}
279
280/* ------------------------------------------------------------------------- *
281 * What the person sees about an entry
282 * ------------------------------------------------------------------------- */
283
284export interface StageText {
285 text: string
286 /** True when the claim is derived by this UI (no read seen), not observed. */
287 inferred: boolean
288}
289
290/** §2.3 — the card versus context distinction, in the console's own words. */
291export function stageText(entry: ConsoleEntry): StageText {
292 const event = entry.event
293 if (event.kind !== 'summoned') return { text: 'nothing materialized', inferred: false }
294 const skills = event.skills
295 const inContext = skills.filter((s) => s.stage === 'in-context')
296 if (skills.length > 0 && inContext.length === skills.length) {
297 const readers = Array.from(new Set(skills.map((_, i) => entry.readBy[i] ?? 'an agent')))
298 return { text: `in context · body read by ${readers.join(', ')}`, inferred: false }
299 }
300 if (inContext.length > 0) {
301 return { text: `${inContext.length} of ${skills.length} in context · the rest: card returned · body not read`, inferred: false }
302 }
303 return { text: 'card returned · body not read', inferred: true }
304}
305
306/** A name that may be put in the prompt after the summon command: plain words
307 * only. Tree skills carry display names with spaces ("Frontend Code Review",
308 * observed live), which are safe and are what the engine matches exactly. */
309export function summonableName(skill: SkillReceipt | undefined): string | null {
310 if (!skill) return null
311 const name = sanitizeDisplay(skill.name, 64)
312 return /^[A-Za-z0-9][A-Za-z0-9._:/-]*(?: [A-Za-z0-9._:/-]+)*$/.test(name) ? name : null
313}
314
315/** The five Skill Heaven surfaces a pre-fill may name. */
316const SURFACE_COMMANDS = new Set(['summon', 'skill-zero', 'skill-heaven', 'skill-hell', 'skill-ultra'])
317
318/**
319 * The spelling of a Skill Heaven command that this session accepts, for a
320 * pre-fill. Observed on Claude Code 2.1.294: a bare `/skill-zero` (or
321 * `/skill-hell high`) resolves to the plugin's portable skill, which is
322 * `user-invocable: false`, and the host refuses it ("can only be invoked by
323 * Claude"); the plugin-qualified `/skill-heaven:skill-zero`, the spelling the
324 * `/` menu shows, reaches the command. Always the one plugin's own name: a
325 * look-alike `other:summon` is never filled.
326 */
327export function qualifyCommand(text: string): string {
328 const m = /^\/([a-z-]+)(\s.*)?$/.exec(text)
329 if (!m || !SURFACE_COMMANDS.has(m[1]!)) return text
330 return `/skill-heaven:${m[1]!}${m[2] ?? ''}`
331}
332
333/** §5.2: Summon is offered for a /lens preview with exactly one candidate. */
334export function summonSuggestion(entry: ConsoleEntry): string | null {
335 const event = entry.event
336 if (event.kind !== 'previewed' || event.skills.length !== 1) return null
337 return summonableName(event.skills[0])
338}
339
340export function timeLabel(at: string | null): string {
341 if (!at) return ''
342 const d = new Date(at)
343 if (Number.isNaN(d.getTime())) return ''
344 const two = (n: number) => String(n).padStart(2, '0')
345 return `${two(d.getHours())}:${two(d.getMinutes())}:${two(d.getSeconds())}`
346}
347hooks/views.tsx 557 lines1// Drawing: the Lens band and the /heaven pane, as trees of the surface's own
2// elements. Pure functions of state — no `$`, no writes. A press is handed back
3// to the hook that drew the tree through the `actions` objects below, so this
4// file cannot change anything by itself.
5//
6// Every string that came from the network or from a model (skill names, queries,
7// sources, reasons, agent labels) reaches the screen through the status model's
8// sanitizer or its renderers. Colour is never the only carrier of meaning: each
9// state keeps its glyph and its word.
10
11import type { Elements, RenderElement } from 'claude-code'
12
13import {
14 CHIP_LABEL,
15 HARNESS_PATHS,
16 ROLE_COLORS,
17 eventLines,
18 formatMs,
19 formatScore,
20 noticeLines,
21 readingRung,
22 readingToken,
23 renderStatusSegments,
24 sanitizeDisplay,
25} from './status-model.mjs'
26import type { EvidenceClass, Role, Segment, SkillReceipt } from './status-model.mjs'
27import { CONSOLE_PROBE, CONSOLE_VERSION, SKILL_HEAVEN_VERSION } from './meta.ts'
28import { agentLabelFor, entryById, qualifyCommand, stageText, summonSuggestion, timeLabel } from './model.ts'
29import type { ConsoleAgent, ConsoleEntry, ConsoleSection, ConsoleState } from './model.ts'
30
31/** The elements every surface that draws a band or a pane has in common. */
32export type UI = Pick<Elements['desktop'], 'Box' | 'Text' | 'Button'>
33
34export interface BandActions {
35 inspect: (id: number) => void
36 dismiss: () => void
37 /** Pre-fills `/summon <name>` into the prompt. The person submits it. */
38 summon: (name: string) => void
39}
40
41export interface PaneActions {
42 section: (section: ConsoleSection) => void
43 toggle: (id: number) => void
44 /** Pre-fills a command into the prompt. The person submits it. */
45 fill: (text: string) => void
46 copy: (text: string) => void
47}
48
49/* ------------------------------------------------------------------------- *
50 * Colour: roles from the one palette. `ink` is the theme's own text colour,
51 * `dim` is the theme's dim; every other role is its palette hex.
52 * ------------------------------------------------------------------------- */
53
54type StyleProps = { color?: string; dimColor?: boolean; italic?: boolean; bold?: boolean }
55
56function styleOf(role: Role): StyleProps {
57 if (role === 'ink') return {}
58 if (role === 'dim') return { dimColor: true }
59 return { color: ROLE_COLORS[role].hex }
60}
61
62function runs(ui: UI, segments: readonly Segment[], extra: StyleProps = {}): RenderElement {
63 const { Text } = ui
64 return (
65 <Text>
66 {segments.map((s) => (
67 <Text {...styleOf(s.role)} {...extra}>
68 {s.text}
69 </Text>
70 ))}
71 </Text>
72 )
73}
74
75/* ------------------------------------------------------------------------- *
76 * The Lens band — empty by default (the hook returns next(e) for null).
77 * ------------------------------------------------------------------------- */
78
79export function renderBand(ui: UI, state: ConsoleState, actions: BandActions, columns: number): RenderElement | null {
80 const { Box, Text, Button } = ui
81 const band = state.band
82 if (band === null) return null
83 const width = Math.max(20, columns)
84
85 if (band.kind === 'looking' || band.kind === 'not-connected') {
86 const [first, second] = noticeLines(band.kind === 'looking' ? { kind: 'looking', query: band.query } : { kind: 'not-connected' })
87 return (
88 <Box flexDirection="column" width={width}>
89 {first ? runs(ui, first) : null}
90 {second ? runs(ui, second) : null}
91 <Button key="dismiss" label="Dismiss" role="dismiss" onPress={() => actions.dismiss()} />
92 </Box>
93 )
94 }
95
96 const entry = entryById(state, band.id)
97 if (!entry) return null
98 const [line1, line2] = eventLines(entry.event)
99 const stage = stageText(entry)
100 const name = summonSuggestion(entry)
101 return (
102 <Box flexDirection="column" width={width}>
103 {line1 ? runs(ui, line1) : null}
104 {line2 ? runs(ui, line2) : null}
105 {/* a preview's second line already says "nothing materialized" (seen twice live on 2.1.294) */}
106 {entry.event.kind === 'previewed' || (entry.event.kind === 'no-match' && entry.event.preview) ? null : (
107 <Text key="stage" dimColor italic={stage.inferred}>
108 {' '}
109 {stage.text}
110 {stage.inferred ? ' (inferred)' : ''}
111 </Text>
112 )}
113 <Box flexDirection="row" gap={1}>
114 {name !== null ? <Button key="summon" label="Summon" variant="primary" onPress={() => actions.summon(name)} /> : null}
115 <Button key="inspect" label="Inspect" onPress={() => actions.inspect(entry.id)} />
116 <Button key="dismiss" label="Dismiss" role="dismiss" onPress={() => actions.dismiss()} />
117 </Box>
118 </Box>
119 )
120}
121
122/* ------------------------------------------------------------------------- *
123 * The /heaven pane
124 * ------------------------------------------------------------------------- */
125
126const SECTIONS: ReadonlyArray<{ id: ConsoleSection; label: string; hotkey: string }> = [
127 { id: 'session', label: 'Session', hotkey: '1' },
128 { id: 'scope', label: 'Scope', hotkey: '2' },
129 { id: 'flow', label: 'Flow', hotkey: '3' },
130 { id: 'trust', label: 'Trust', hotkey: '4' },
131]
132
133export function renderPane(ui: UI, state: ConsoleState, actions: PaneActions, columns: number): RenderElement {
134 const { Box, Text, Button } = ui
135 const width = Math.max(24, columns)
136 return (
137 <Box flexDirection="column" width={width} gap={1}>
138 <Box flexDirection="column">
139 <Text>
140 <Text bold>Skill Heaven</Text>
141 <Text color={ROLE_COLORS.amber.hex}>{' PREVIEW'}</Text>
142 <Text dimColor>{' console · observes, never changes'}</Text>
143 </Text>
144 {runs(ui, renderStatusSegments(state.status, 'full'))}
145 </Box>
146 <Box flexDirection="row" gap={1} flexWrap="wrap">
147 {SECTIONS.map((s) => (
148 <Button
149 key={`section-${s.id}`}
150 label={s.label}
151 hotkey={s.hotkey}
152 variant={state.section === s.id ? 'primary' : 'secondary'}
153 onPress={() => actions.section(s.id)}
154 />
155 ))}
156 </Box>
157 {state.section === 'session' ? sessionSection(ui, state, actions) : null}
158 {state.section === 'scope' ? scopeSection(ui, state, actions) : null}
159 {state.section === 'flow' ? flowSection(ui, state) : null}
160 {state.section === 'trust' ? trustSection(ui, state) : null}
161 </Box>
162 )
163}
164
165/* -- evidence-labelled rows ------------------------------------------------ */
166
167interface Field {
168 name: string
169 value: string | null
170 evidence: EvidenceClass
171}
172
173const FIELD_WIDTH = 16
174
175function fieldRow(ui: UI, f: Field, key: string): RenderElement {
176 const { Box, Text } = ui
177 const known = f.value !== null && f.value !== '' && f.evidence !== 'unknown'
178 const label = f.evidence === 'inferred' ? 'inferred' : f.evidence === 'unknown' || !known ? 'unknown' : f.evidence
179 return (
180 <Box key={key} flexDirection="row" flexWrap="wrap">
181 <Text dimColor>{f.name.padEnd(FIELD_WIDTH)}</Text>
182 <Text italic={f.evidence === 'inferred'}>{known ? f.value : '—'}</Text>
183 <Text dimColor>{` ${label}`}</Text>
184 </Box>
185 )
186}
187
188const DIRECTION_WORD: Record<string, string> = {
189 manual: 'any (explicit /summon)',
190 converge: 'heaven (converge)',
191 explore: 'hell (explore)',
192 unspecified: 'unspecified',
193}
194
195const LANE_WORD: Record<string, string> = {
196 'human-led': 'human-led (Skill Heaven lane)',
197 'model-led': 'model-led (Skill Hell lane)',
198 unspecified: 'unspecified (the source did not classify it)',
199}
200
201function healthText(entry: ConsoleEntry): string | null {
202 const event = entry.event
203 if (!('sourceHealth' in event)) return null
204 const h = event.sourceHealth
205 if (h.kind === 'unknown') return null
206 const age = h.indexAgeDays === null ? '' : ` · index ${h.indexAgeDays} days old`
207 return h.kind === 'stale' ? `? stale${age} · ranking still ran` : `fresh${age}`
208}
209
210function skillFields(skill: SkillReceipt, index: number, entry: ConsoleEntry): Field[] {
211 const stage: Field =
212 skill.stage === 'in-context'
213 ? { name: 'what entered', value: `body read (in context) by ${entry.readBy[index] ?? 'an agent'}`, evidence: 'observed' }
214 : skill.stage === 'materialized'
215 ? { name: 'what entered', value: 'card returned · body not read', evidence: 'inferred' }
216 : skill.stage === 'previewed'
217 ? { name: 'what entered', value: 'previewed · nothing materialized', evidence: 'reported' }
218 : { name: 'what entered', value: 'materialized · read not observed', evidence: 'inferred' }
219 const rank: string[] = []
220 if (skill.matchKind !== 'unknown') rank.push(skill.matchKind)
221 const score = formatScore(skill.score)
222 if (score) rank.push(score)
223 const margin = formatScore(skill.margin)
224 if (margin) rank.push(`Δ ${margin} (margin is a retrieval diagnostic)`)
225 const from: string[] = []
226 if (skill.source) from.push(sanitizeDisplay(skill.source, 48))
227 if (skill.repoUrl) from.push(sanitizeDisplay(skill.repoUrl, 64) + (skill.ref ? `@${sanitizeDisplay(skill.ref, 24)}` : ''))
228 if (skill.subpath) from.push(sanitizeDisplay(skill.subpath, 48))
229 if (skill.sha256) from.push(`sha256 ${sanitizeDisplay(skill.sha256, 12)}…`)
230 const cache: string[] = []
231 if (skill.cache !== 'unknown') cache.push(skill.cache)
232 const ms = formatMs(skill.ms)
233 if (ms) cache.push(ms)
234 const installability =
235 skill.installability === 'unknown' ? null : sanitizeDisplay(skill.installability, 40)
236 return [
237 stage,
238 { name: 'ranking', value: rank.length ? rank.join(' · ') : null, evidence: rank.length ? 'reported' : 'unknown' },
239 { name: 'from', value: from.length ? from.join(' · ') : null, evidence: from.length ? 'reported' : 'unknown' },
240 {
241 name: 'lane',
242 value: LANE_WORD[skill.lane] ?? null,
243 evidence: LANE_WORD[skill.lane] ? 'reported' : 'unknown',
244 },
245 {
246 name: 'installability',
247 value: installability,
248 evidence: installability ? 'reported' : 'unknown',
249 },
250 { name: 'cache', value: cache.length ? cache.join(' · ') : null, evidence: cache.length ? 'reported' : 'unknown' },
251 { name: 'on disk', value: skill.path ? sanitizeDisplay(skill.path, 96) : null, evidence: skill.path ? 'reported' : 'unknown' },
252 ]
253}
254
255function receiptFields(entry: ConsoleEntry, state: ConsoleState): Field[][] {
256 const event = entry.event
257 const common: Field[] = []
258 const query = 'query' in event && event.query ? `query "${sanitizeDisplay(event.query, 80)}" · surface ${DIRECTION_WORD[event.direction] ?? 'unspecified'}` : null
259 const health = healthText(entry)
260 const agent: Field = {
261 name: 'agent',
262 value: agentLabelFor(state, entry.agent),
263 evidence: 'observed',
264 }
265 if (event.kind === 'summoned' || event.kind === 'previewed') {
266 const groups: Field[][] = []
267 common.push({ name: 'why', value: query, evidence: query ? 'reported' : 'unknown' })
268 if (event.kind === 'summoned') {
269 const comp =
270 event.composition === 'relevance-only'
271 ? `relevance-only · Arbor: ${event.arbor === 'governed-record' ? 'governed record' : event.arbor === 'no-record' ? 'no governed record' : event.arbor}`
272 : null
273 common.push({ name: 'composition', value: comp, evidence: comp ? 'reported' : 'unknown' })
274 }
275 common.push({ name: 'source health', value: health, evidence: health ? 'reported' : 'unknown' }, agent)
276 groups.push(common)
277 event.skills.slice(0, 3).forEach((skill, i) =>
278 groups.push([{ name: 'skill', value: sanitizeDisplay(skill.name, 48), evidence: 'reported' }, ...skillFields(skill, i, entry)]),
279 )
280 const unshown = event.skills.length - 3 + entry.omitted
281 if (unshown > 0) groups.push([{ name: 'more', value: `+${unshown} more not shown here`, evidence: 'observed' }])
282 return groups
283 }
284 if (event.kind === 'no-match') {
285 return [
286 [
287 { name: 'what entered', value: 'nothing materialized', evidence: 'reported' },
288 { name: 'why', value: query, evidence: query ? 'reported' : 'unknown' },
289 {
290 name: 'refusal',
291 value:
292 (event.considered === null ? '0 admitted' : `${event.considered} considered · 0 admitted`) +
293 (event.reason ? ` · ${sanitizeDisplay(event.reason, 60)}` : ''),
294 evidence: 'reported',
295 },
296 { name: 'source health', value: health, evidence: health ? 'reported' : 'unknown' },
297 agent,
298 ],
299 ]
300 }
301 return [
302 [
303 { name: 'what entered', value: 'nothing materialized', evidence: 'reported' },
304 { name: 'reason', value: sanitizeDisplay(event.reason, 120), evidence: 'observed' },
305 agent,
306 ],
307 ]
308}
309
310/* -- Session ----------------------------------------------------------------- */
311
312function sessionSection(ui: UI, state: ConsoleState, actions: PaneActions): RenderElement {
313 const { Box, Text, Button } = ui
314 const rows = [...state.entries].reverse()
315 return (
316 <Box flexDirection="column" gap={1}>
317 {state.status.summonTool === 'not-connected' ? (
318 <Text>
319 <Text color={ROLE_COLORS.amber.hex}>{'? '}</Text>
320 <Text>{'summon tool: not connected'}</Text>
321 <Text dimColor>{` /summon and /lens cannot reach the Skill Heaven plugin. ${HARNESS_PATHS.find((h) => h.id === 'claude')?.commands[1] ?? ''}`}</Text>
322 </Text>
323 ) : null}
324 {rows.length === 0 ? (
325 <Text>
326 {'Nothing summoned yet. Try '}
327 <Text bold>{`${qualifyCommand('/summon')} <need>`}</Text>
328 {' or '}
329 <Text bold>{'/lens <need>'}</Text>
330 {'.'}
331 </Text>
332 ) : null}
333 {rows.map((entry) => {
334 const lines = eventLines(entry.event)
335 const open = state.openEntry === entry.id
336 const meta = [timeLabel(entry.event.at), agentLabelFor(state, entry.agent), entry.via === 'lens' ? '/lens' : ''].filter(Boolean).join(' · ')
337 return (
338 <Box key={`entry-${entry.id}`} flexDirection="column">
339 {lines[0] ? runs(ui, [...lines[0], { text: meta ? ` ${meta}` : '', role: 'dim' }]) : null}
340 {lines[1] ? runs(ui, lines[1]) : null}
341 <Button
342 key={`toggle-${entry.id}`}
343 label={open ? 'Hide receipt' : 'Receipt'}
344 plain
345 dimColor
346 onPress={() => actions.toggle(entry.id)}
347 />
348 {open
349 ? (
350 <Box flexDirection="column" marginLeft={2} gap={1}>
351 {receiptFields(entry, state).map((group, gi) => (
352 <Box key={`group-${gi}`} flexDirection="column">
353 {group.map((f, fi) => fieldRow(ui, f, `f-${gi}-${fi}`))}
354 </Box>
355 ))}
356 </Box>
357 )
358 : null}
359 </Box>
360 )
361 })}
362 <Text dimColor>
363 {'Evidence: observed = this console saw it · reported = the summon engine said so · inferred = derived here · unknown = no source.'}
364 </Text>
365 </Box>
366 )
367}
368
369/* -- Scope ------------------------------------------------------------------- */
370
371export const ULTRA_COPY =
372 'Skill Ultra · provisioned. Controller unavailable — no controller is choosing direction or depth yet. Ultra is the rung you selected; each summon is still judged per use. The long-horizon controller is tracked in #126 and is not yet empirically validated.'
373
374function latestSource(state: ConsoleState): { source: string | null; health: string | null; unavailable: boolean } {
375 const last = state.entries[state.entries.length - 1]
376 const unavailable = last?.event.kind === 'unavailable'
377 for (let i = state.entries.length - 1; i >= 0; i--) {
378 const e = state.entries[i]!
379 const event = e.event
380 if ('skills' in event) {
381 const withSource = event.skills.find((s) => s.source)
382 if (withSource?.source) return { source: sanitizeDisplay(withSource.source, 56), health: healthText(e), unavailable }
383 }
384 }
385 return { source: null, health: last ? healthText(last) : null, unavailable }
386}
387
388function scopeSection(ui: UI, state: ConsoleState, actions: PaneActions): RenderElement {
389 const { Box, Text, Button } = ui
390 const { source, health, unavailable } = latestSource(state)
391 const reading = state.status.reading
392 const boot = state.status.boot ?? (reading.kind === 'selected' ? ({ kind: 'native', source: 'no-launcher' } as const) : reading)
393 const rung = reading.kind === 'selected' ? reading.rung : null
394 const ultra = readingRung(reading) === 'ultra'
395 const skills = state.status.skills
396 const fields: Field[] = [
397 {
398 name: 'can see',
399 value: unavailable
400 ? `skill source ${source ?? ''} · unreachable`.replace(' ', ' ')
401 : source
402 ? `skill source ${source}${health ? ` · ${health}` : ''}`
403 : null,
404 evidence: source || unavailable ? 'reported' : 'unknown',
405 },
406 {
407 name: 'active',
408 value:
409 skills === null
410 ? null
411 : `${skills} ${skills === 1 ? 'skill' : 'skills'} materialized this session (temporary — gone when the session ends)`,
412 evidence: skills === null ? 'unknown' : 'observed',
413 },
414 {
415 name: 'inherited',
416 value:
417 boot.kind === 'native' || boot.kind === 'unknown'
418 ? `boot reading ${readingToken(boot)} — no launcher observed; this console cannot see how the session was started`
419 : `boot reading ${readingToken(boot)} — set at launch`,
420 evidence: boot.kind === 'native' || boot.kind === 'unknown' ? 'inferred' : 'reported',
421 },
422 {
423 name: 'selected',
424 value:
425 rung === null
426 ? 'none — no rung selected in this session'
427 : ultra
428 ? 'rung ULTRA · controller unavailable'
429 : `rung ${rung.toUpperCase()} — observed from ${state.selectedFrom ?? 'a rung command'} · not enforced`,
430 evidence: rung === null ? 'unknown' : 'observed',
431 },
432 { name: 'allowed', value: '/summon by hand: yes · zero cut: temporary (default)', evidence: 'reported' },
433 { name: 'keep small', value: '/skill-zero cuts temporary skills · claude-zero --level zero starts clean', evidence: 'reported' },
434 ]
435 return (
436 <Box flexDirection="column" gap={1}>
437 <Box flexDirection="column">{fields.map((f, i) => fieldRow(ui, f, `scope-${i}`))}</Box>
438 {rung !== null && !ultra ? (
439 <Text dimColor>{`You selected ${rung.toUpperCase()}. Skill Heaven does not enforce a rung; each summon is still judged per use.`}</Text>
440 ) : null}
441 {ultra ? <Text color={ROLE_COLORS.ultra.hex}>{ULTRA_COPY}</Text> : null}
442 <Box flexDirection="column">
443 <Text bold>Choose a rung</Text>
444 <Text dimColor>These fill your prompt. You press Enter; nothing runs until you do.</Text>
445 <Box flexDirection="row" gap={1} flexWrap="wrap">
446 <Button key="fill-heaven" label={qualifyCommand('/skill-heaven low')} onPress={() => actions.fill('/skill-heaven low')} />
447 <Button key="fill-hell" label={qualifyCommand('/skill-hell high')} onPress={() => actions.fill('/skill-hell high')} />
448 <Button key="fill-ultra" label={qualifyCommand('/skill-ultra')} onPress={() => actions.fill('/skill-ultra')} />
449 </Box>
450 <Text dimColor>{`Other rungs: ${qualifyCommand('/skill-heaven med')} · ${qualifyCommand('/skill-hell xhigh')} · ${qualifyCommand('/skill-hell max')}`}</Text>
451 </Box>
452 <Box flexDirection="column">
453 <Text bold>Keep context small</Text>
454 <Box flexDirection="row" gap={1} flexWrap="wrap">
455 <Button key="fill-zero" label={qualifyCommand('/skill-zero')} onPress={() => actions.fill('/skill-zero')} />
456 <Button key="fill-zero-all" label={qualifyCommand('/skill-zero all')} onPress={() => actions.fill('/skill-zero all')} />
457 <Button key="copy-clean" label="Copy: claude-zero --level zero" onPress={() => actions.copy('claude-zero --level zero')} />
458 </Box>
459 <Text dimColor>Start clean is run in a terminal before a session: it is copied, not run.</Text>
460 </Box>
461 </Box>
462 )
463}
464
465/* -- Flow -------------------------------------------------------------------- */
466
467const MAX_AGENT_ROWS = 12
468
469function agentLine(state: ConsoleState, agent: ConsoleAgent): string {
470 const mine = state.entries.filter((e) => e.agent !== null && e.agent === agent.id && e.event.kind === 'summoned')
471 const skills = mine.reduce((n, e) => n + (e.event.kind === 'summoned' ? e.event.delta : 0), 0)
472 const word = agent.state === 'started' ? 'started in background' : agent.state
473 return `${word} · ${mine.length === 0 ? 'no skills' : `${mine.length} ${mine.length === 1 ? 'summon' : 'summons'}`}${skills > 0 && skills !== mine.length ? ` (${skills} skills)` : ''}`
474}
475
476function flowSection(ui: UI, state: ConsoleState): RenderElement {
477 const { Box, Text } = ui
478 const mainSummons = state.entries.filter((e) => e.agent === null && e.event.kind === 'summoned')
479 const mainInContext = mainSummons.reduce(
480 (n, e) => n + (e.event.kind === 'summoned' ? e.event.skills.filter((s) => s.stage === 'in-context').length : 0),
481 0,
482 )
483 const shown = state.agents.slice(0, MAX_AGENT_ROWS)
484 const more = state.agents.length - shown.length
485 return (
486 <Box flexDirection="column" gap={1}>
487 <Box flexDirection="column">
488 <Text>
489 <Text bold>main</Text>
490 <Text dimColor>{` ${mainSummons.length} ${mainSummons.length === 1 ? 'summon' : 'summons'} · ${mainInContext} in context`}</Text>
491 </Text>
492 {shown.map((agent, i) => {
493 const last = i === shown.length - 1 && more <= 0
494 return (
495 <Text>
496 <Text dimColor>{last ? '└─ ' : '├─ '}</Text>
497 <Text>{sanitizeDisplay(agent.label, 64)}</Text>
498 <Text dimColor>{` ${agentLine(state, agent)}`}</Text>
499 </Text>
500 )
501 })}
502 {more > 0 ? (
503 <Text dimColor>{`└─ +${more} more`}</Text>
504 ) : null}
505 </Box>
506 {!state.agentIdsSeen ? (
507 <Text dimColor>
508 {'This host did not report agent ids. Summons are attributed to main.'}
509 </Text>
510 ) : null}
511 {state.entries.length >= 50 ? <Text dimColor>Counts cover the last 50 events.</Text> : null}
512 <Text dimColor>Read-only. Only agents the host reported appear.</Text>
513 </Box>
514 )
515}
516
517/* -- Trust ------------------------------------------------------------------- */
518
519function trustSection(ui: UI, state: ConsoleState): RenderElement {
520 const { Box, Text } = ui
521 const claude = HARNESS_PATHS.find((h) => h.id === 'claude')!
522 const mcp = state.status.summonTool
523 const mcpText =
524 mcp === 'connected' ? 'connected (a summon result was seen)' : mcp === 'not-connected' ? 'not connected' : 'unknown until the first summon or /lens'
525 const row = (name: string, value: string, key: string) => (
526 <Box key={key} flexDirection="row" flexWrap="wrap">
527 <Text dimColor>{name.padEnd(FIELD_WIDTH)}</Text>
528 <Text>{value}</Text>
529 </Box>
530 )
531 return (
532 <Box flexDirection="column" gap={1}>
533 <Box flexDirection="column">
534 {row('skill-heaven', `built for ${SKILL_HEAVEN_VERSION} · gaia-research/gaia-skill-heaven · bundles one MCP server (skill-summon)`, 't1')}
535 {row('console (preview)', `${CONSOLE_VERSION} · same repository · runs inside Claude Code as local code`, 't2')}
536 </Box>
537 <Box flexDirection="column" marginLeft={2}>
538 {row('reads', 'summon tool results · your /skill-* commands · Read/Agent tool calls (to observe, never to change)', 't3')}
539 {row('writes', 'nothing to disk · session-only $.state', 't4')}
540 {row('command text', '/lens and /heaven print a one-line result that the model can read. It is fixed text and carries nothing a skill source supplied.', 't4b')}
541 {row('network', "none of its own; /lens calls the bundled summon tool, which fetches the skill source", 't5')}
542 {row('disable', '/plugin disable skill-heaven-console — the status entry, band and pane disappear; Skill Heaven is unchanged', 't6')}
543 </Box>
544 <Box flexDirection="column">
545 {row('summon tool', `MCP: ${mcpText}`, 't7')}
546 {row(
547 'harness',
548 `Claude Code ${state.hostVersion ? sanitizeDisplay(state.hostVersion, 24) : '(version not reported)'} · ${CHIP_LABEL[claude.chip]} for the plugin at ${claude.probedVersion ?? 'unknown'}; ${CONSOLE_PROBE}`,
549 't8',
550 )}
551 {row('evidence', claude.evidence, 't9')}
552 </Box>
553 <Text dimColor>This lists what the code can do. It does not rate it.</Text>
554 </Box>
555 )
556}
557types/index.d.ts 72 lines1// The console's session state contract: the one `$.state` value its drawings read.
2//
3// The engine requires this file to be self-contained (no import), so the status
4// model's own types cannot be named here. `status` and each entry's `event` are
5// therefore opaque plain data (`ConsoleData`) at this seam; hooks/model.ts gives
6// them their real types — SkillHeavenStatus and SummonEvent from the one status
7// model — in exactly one place, and nothing else casts.
8
9/** Plain, JSON-shaped data. */
10export type ConsoleData = string | number | boolean | null | ConsoleData[] | { [key: string]: ConsoleData }
11
12export type ConsoleSection = 'session' | 'scope' | 'flow' | 'trust'
13
14/** One summon-tool call the console observed (a real summon, or a /lens preview). */
15export type ConsoleEntryData = {
16 id: number
17 /** A SummonEvent (status model). */
18 event: ConsoleData
19 /** Which agent the host reported for the call; null = the main conversation. */
20 agent: string | null
21 /** How the console came to know: a tool call it watched, or its own /lens preview. */
22 via: 'tool' | 'lens'
23 /** One slot per stored skill: who read its materialized SKILL.md (a label, never a path), or null. */
24 readBy: (string | null)[]
25 /** Skills the result named beyond the bounded number stored here. */
26 omitted: number
27}
28
29/** What the Lens band shows. null = nothing (the default). */
30export type ConsoleBand =
31 | { kind: 'event'; id: number }
32 | { kind: 'looking'; query: string }
33 | { kind: 'not-connected' }
34 | null
35
36/** An agent the host reported (an `agentId` on a tool call, or an Agent call). */
37export type ConsoleAgent = {
38 /** The Agent call's tool_use_id until the host names the agent, then its agentId. */
39 key: string
40 /** The host's agent id, when it has been reported. */
41 id: string | null
42 /** Sanitized: subagent type and the call's own description. */
43 label: string
44 /** running = a foreground call is open; started = launched in the background, end not observed; returned = observed to finish. */
45 state: 'running' | 'started' | 'returned'
46}
47
48export type ConsoleStateData = {
49 /** A SkillHeavenStatus (status model). */
50 status: ConsoleData
51 /** Newest last, at most 50. */
52 entries: ConsoleEntryData[]
53 band: ConsoleBand
54 agents: ConsoleAgent[]
55 /** True once the host reported any agent id this session. */
56 agentIdsSeen: boolean
57 section: ConsoleSection
58 /** The receipt row that is expanded in the Session section. */
59 openEntry: number | null
60 /** The rung command the person typed, sanitized, for the Scope section. */
61 selectedFrom: string | null
62 /** Claude Code's version string, when the host reported it. */
63 hostVersion: string | null
64 seq: number
65}
66
67declare module 'claude-code' {
68 interface PluginState {
69 'skill-heaven-console': { state: Shaped<ConsoleStateData> }
70 }
71}
72