@llblab/pi-telegram 0.30.1 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -43,7 +43,7 @@ Keep each fact in one authoritative layer:
43
43
  - `/lib/*.ts`: Flat, cohesive runtime domains; package-private unless re-exported through `/api`.
44
44
  - `/tests/*.test.ts`: Domain-mirrored suites; `tests/integration.test.ts` owns cross-domain runtime flows.
45
45
  - `/skills/telegram-bridge`: Stable agent operating protocol for Telegram turns, delivery, actions, Threaded Mode, and diagnosis.
46
- - `/skills/control-surface`: Optional agent-generated cybernetic control surface over truthful domain evidence, workflows, and choices; it remains independent from the bridge skill and owns no parallel state.
46
+ - `/skills/generated-control-surface`: Optional state-derived, late-bound interface over truthful domain evidence, capabilities, workflows, and choices; it remains renderer-neutral, independent from the bridge skill, and owns no parallel state.
47
47
  - `/.agents/skills/telegram-bot`: Bot API lookup guidance and vendored `api.md`; keep the reference intact.
48
48
  - `/.agents/skills/domain-dag`: Repository architecture guidance and validator.
49
49
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  > Each release keeps at most 8 outcome records of at most 512 characters.
4
4
 
5
+ ## 0.32.0: Compact Button Arrays And Filesystem Surfaces
6
+
7
+ - `Button Arrays`: Added JSON-array payloads to the canonical colon-free `telegram_button` action so one hidden comment can declare many ordered button rows; `telegram_buttons` is an exact plural alias, while existing single-object and double-quoted attribute forms remain valid.
8
+ - `Filesystem Surface`: Made bare filesystem paths—including `/`—legitimate Generated Control Surface intents. Directory surfaces resolve fresh metadata, paginate large listings, preserve secret boundaries, and may use exact path-only button prompts with compact semantic file/folder labels.
9
+
10
+ ## 0.31.0: State-Derived Generated Control Surfaces
11
+
12
+ - `Generated Control Surface`: Renamed the optional `control-surface` Skill to `generated-control-surface`, aligned its identity with the architectural primitive, and made Telegram routing evaluate it proactively when controls can materially shorten likely feedback without waiting for an explicit button request; decorative UI still resolves to zero controls.
13
+ - `Late-Bound Interface`: Defined generated surfaces as `interface = f(state, capabilities, intent)`: ephemeral renderer projections that expose domain-owned state and agency under constitutional constraints without absorbing parallel state. Telegram remains the first renderer rather than the capability boundary.
14
+
5
15
  ## 0.30.1: Priority Button Queue Hotfix
6
16
 
7
17
  - `Priority Queue`: Assistant-authored button callbacks now enter the sorted priority lane ahead of already queued default prompts, aligning the visible lightning marker with actual dispatch order while preserving callback deduplication and FIFO ordering within each lane.
package/README.md CHANGED
@@ -192,7 +192,7 @@ Voice notes, audio, images, PDFs, and other media can pass through configured in
192
192
 
193
193
  ### Buttons And Callbacks
194
194
 
195
- Assistant replies can include top-level hidden `telegram_button` comments using either a JSON object or compact double-quoted attributes. Buttons use `label` plus `prompt`, or the compact `value` key when both are identical. The action marker is colon-free for both payload forms. The bridge strips the comments from visible text, renders inline buttons, and routes callbacks back into Pi as queued prompts or extension-owned callback actions. Button-only replies receive the standard `☑️ **Choose an option:**` heading as automatic visible fallback text. Once a generated prompt button is accepted, only that exact button switches to its optional `selected_style` (`primary` blue by default, `success` green, or `danger` red) without altering its agent-authored label or emoji; every style still queues the selected prompt.
195
+ Assistant replies can include top-level hidden `telegram_button` comments using a JSON object, a JSON array of button objects, or compact double-quoted attributes; `telegram_buttons` is a plural alias. Prefer one array comment for multiple buttons. Buttons use `label` plus `prompt`, or the compact `value` key when both are identical. The action marker is colon-free for every payload form. The bridge strips the comments from visible text, renders inline buttons, and routes callbacks back into Pi as queued prompts or extension-owned callback actions. Button-only replies receive the standard `☑️ **Choose an option:**` heading as automatic visible fallback text. Once a generated prompt button is accepted, only that exact button switches to its optional `selected_style` (`primary` blue by default, `success` green, or `danger` red) without altering its agent-authored label or emoji; every style still queues the selected prompt.
196
196
 
197
197
  ### Threaded Mode And Multi-Instance Bus
198
198
 
@@ -257,7 +257,7 @@ Durable inbound admission is a **process-crash recovery** guarantee. Atomic priv
257
257
 
258
258
  Telegram is a companion surface around a live Pi runtime, not a second runtime. It can compact the current session, but it cannot create, resume, fork, browse, or switch sessions until Pi exposes safe public extension APIs for those operations.
259
259
 
260
- A Telegram prompt is a normal model turn in the active Pi session and therefore inherits that session's active post-compaction context; the bridge does not make token cost proportional only to the new mobile message. The bundled `telegram-bridge` Skill owns agent operation and `control-surface` provides optional evidence-backed controls for workflows, stateful systems, navigation, supervision, and decisions—not only terminal operations. Disconnecting removes pi-telegram's delivery tools and transient routing guidance from later requests until direct ownership or follower registration returns, without changing other active Pi tools. Pi session JSONL contains model history; profile-scoped pi-telegram `logs*.jsonl` contains redacted operational events and is never model context.
260
+ A Telegram prompt is a normal model turn in the active Pi session and therefore inherits that session's active post-compaction context; the bridge does not make token cost proportional only to the new mobile message. The bundled `telegram-bridge` Skill owns agent operation and `generated-control-surface` proactively compiles optional evidence-backed, ephemeral controls from current state, available capabilities, and user intent when they materially shorten likely feedback for workflows, stateful systems, navigation, supervision, and decisions—not only terminal operations. Disconnecting removes pi-telegram's delivery tools and transient routing guidance from later requests until direct ownership or follower registration returns, without changing other active Pi tools. Pi session JSONL contains model history; profile-scoped pi-telegram `logs*.jsonl` contains redacted operational events and is never model context.
261
261
 
262
262
  ## Documentation Map
263
263
 
@@ -324,10 +324,10 @@ Final delivery attaches reply metadata only where requested. Reply parameters ap
324
324
 
325
325
  Outbound files staged during an active Telegram turn are delivered after that turn completes. They use `telegram_attach`, are checked atomically per tool call, and use configurable size limits before photo/document upload. When no Telegram turn is active, `telegram_attach` sends files immediately to the paired/default chat, an assigned follower thread, or an explicit `chat_id` plus optional `thread_id`; `telegram_message` provides direct local/TUI Markdown text delivery for explicit user requests and runs the same `telegram_button` markup planner so buttons attach to that text message. Direct local/TUI delivery is singleton-controlled: classic mode requires this Pi instance to own `/telegram-connect`, while Threaded Mode followers must be registered and route through the leader-owned transport. Already accepted active-turn reply/attachment delivery remains session-local.
326
326
 
327
- Assistant-authored final-message actions use hidden top-level comments. Both actions accept a JSON object or double-quoted HTML-like attributes after one colon-free action marker; colon-prefixed payloads are rejected:
327
+ Assistant-authored final-message actions use hidden top-level comments with colon-free markers; colon-prefixed payloads are rejected:
328
328
 
329
- - `telegram_voice` creates voice reply artifacts through configured outbound handlers, programmatic voice handlers, or registered synthesis providers.
330
- - `telegram_button` creates inline buttons whose callbacks enqueue the configured prompt text as a normal Telegram prompt turn.
329
+ - `telegram_voice` accepts a JSON object or double-quoted HTML-like attributes and creates voice reply artifacts through configured outbound handlers, programmatic voice handlers, or registered synthesis providers.
330
+ - `telegram_button` accepts a JSON object, a JSON array of button objects, or double-quoted attributes; `telegram_buttons` aliases the same parser. Each array object creates one inline button row whose callback enqueues the configured prompt text as a normal Telegram prompt turn.
331
331
 
332
332
  Preview delivery strips top-level action comments before streaming draft Markdown. Comments inside code fences, quotes, lists, or indented examples stay literal.
333
333
 
@@ -362,7 +362,7 @@ Complete intermediate assistant text blocks from Telegram-originated activity ar
362
362
 
363
363
  `assistant.activity` is an independent bridge-owned projection over normalized Activity events. Each process reloads the shared file-backed setting at `agent-start` before activity admission, so multi-instance mode cannot continue projecting a stale broader process-local selection. Omitted values resolve to `verbose`, while invalid values fail closed to `quiet`; `thinking` and `tools` select one technical class, while `verbose` enables both. Provider-exposed thinking uses persistent ordinary HTML containing only a standard expandable blockquote with a bounded redacted latest-text window and inline Markdown rendered as Telegram HTML. Completed executed tools use native Rich Messages: each closed `<Tool>: <status>` root details node renders snake-case names as title words while preserving an uppercase two- or three-letter repeated prefix per word, then the native disclosure chevron reveals an open-by-default `arguments` child plus closed retained `update N` and `result`/`error` child details with lowercase monospaced, marker-free summaries and JSON pre blocks; known-safe Rich rejections fall back to the previous HTML disclosure. The projection captures the exact target and transport stamp at activity admission, serializes updates, preserves tool-start order, closes coalescing across assistant/thinking boundaries, bounds retained text/update memory plus edit frames and message/tool size, disables previews and HTTP(S) auto-link recognition inside technical evidence, and never replays a possibly committed send. Session generations own independent queues, so replacement drops queued old work without waiting on an old call. Both proactive prose and active-turn final delivery wait for the admitted activity queue inside their extension-owned delivery tasks, preserving technical-before-semantic ordering without delaying Pi lifecycle completion. Settlement, replacement, disconnect, failure, or stale authority clears only local ownership; already-sent activity messages remain in chat.
364
364
 
365
- Telegram prompt guidance is context- and authority-aware. The package and source-checkout extension both contribute `telegram-bridge` plus the optional `control-surface` Skill through Pi resource discovery. The latter generates transient evidence-backed controls over domain-owned workflows, systems, navigation, supervision, and decisions without creating parallel application state. Only an exact direct owner or live registered follower exposes the two pi-telegram delivery tools, their active-tool metadata, and the compact routing suffix. Disconnect or authority loss removes those tool surfaces for subsequent requests without touching foreign tools; reconnect/recovery restores only the pi-telegram subset that was active before suspension, including across same-process reload. Telegram-originated turns route to the stable Skill contract and retain dynamic blocks such as `[voice] delivery: automatic voice`; the Skill and public documentation own syntax, target routing, Threaded Mode behavior, and diagnostics.
365
+ Telegram prompt guidance is context- and authority-aware. The package and source-checkout extension both contribute `telegram-bridge` plus the optional `generated-control-surface` Skill through Pi resource discovery. The latter treats `interface = f(state, capabilities, intent)` as a renderer-neutral primitive, compiling transient evidence-backed controls over domain-owned workflows, systems, navigation, supervision, and decisions without creating parallel application state. Only an exact direct owner or live registered follower exposes the two pi-telegram delivery tools, their active-tool metadata, and the compact routing suffix. Disconnect or authority loss removes those tool surfaces for subsequent requests without touching foreign tools; reconnect/recovery restores only the pi-telegram subset that was active before suspension, including across same-process reload. Telegram-originated turns route to the stable Skill contract and retain dynamic blocks such as `[voice] delivery: automatic voice`; the Skill and public documentation own syntax, target routing, Threaded Mode behavior, and diagnostics.
366
366
 
367
367
  ## In-Flight Model Switching
368
368
 
package/docs/outbound.md CHANGED
@@ -144,7 +144,7 @@ The bridge strips the comment from Telegram text. On `agent_end`, it maps each `
144
144
 
145
145
  ## Buttons Markup
146
146
 
147
- Assistant replies can include independent button actions in the same two payload forms:
147
+ Assistant replies can include one or many button actions through one colon-free marker:
148
148
 
149
149
  ```md
150
150
  I can continue.
@@ -154,15 +154,19 @@ I can continue.
154
154
  <!-- telegram_button label="Show risks" prompt="List the main risks first." selected_style="danger" -->
155
155
 
156
156
  <!-- telegram_button {"value":"Done"} -->
157
+
158
+ <!-- telegram_button [{"label":"📁 etc","prompt":"/etc"},{"label":"📁 home","prompt":"/home"}] -->
159
+
160
+ <!-- telegram_buttons [{"value":"Approve"},{"value":"Reject"}] -->
157
161
  ```
158
162
 
159
163
  Rules:
160
164
 
161
- - JSON objects and double-quoted HTML-like attributes are the only accepted payload forms; shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute forms are rejected.
162
- - A colon after `telegram_button` is rejected so both payload forms share one unambiguous action marker.
165
+ - `telegram_button` accepts a JSON object, a JSON array of button objects, or double-quoted HTML-like attributes; `telegram_buttons` is an exact plural alias. Shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute forms are rejected.
166
+ - A colon after either button marker is rejected so every payload form shares one unambiguous action marker.
163
167
  - Use `label` plus `prompt`, or the compact `value` key when both strings are identical. Explicit `label` or `prompt` takes precedence over its `value` fallback. Use JSON with `\n` escapes for multiline prompts.
164
168
  - The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it remains literal Markdown.
165
- - Use one comment per button; this mirrors HTML's singular element model and avoids a nested button DSL.
169
+ - Prefer one JSON array comment for multiple buttons; each object becomes one inline-keyboard row in source order. Repeated singular comments remain valid.
166
170
  - Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
167
171
  - After Telegram accepts a generated button callback as a queued prompt, the bridge changes that exact button to its configured selection style without changing agent-authored text or emoji. Set `selected_style` to `primary` (blue), `success` (green), or `danger` (red); omitted or invalid values fall back to `primary`. The style never suppresses queue admission. Other choices stay visually unchanged and remain available; the callback acknowledgement remains the fallback on clients that do not render button styles.
168
172
  - When generated button markup is the entire assistant reply, the bridge supplies the standard `☑️ **Choose an option:**` heading as visible message text so Telegram has a message to which it can attach the inline keyboard.
@@ -180,7 +184,7 @@ The extension injects prompt guidance by context:
180
184
  - For Telegram-originated turns, the compact note routes the agent to `telegram-bridge`, which owns voice/button/direct-delivery/Threaded Mode/formatting/debug guidance.
181
185
  - For Telegram-originated turns, write the full technical answer as normal Markdown.
182
186
  - Add `telegram_voice` with either a JSON object or double-quoted attributes when a Telegram-native voice message is useful. A companion summary is optional, no specific summary format is required.
183
- - Add `telegram_button` with either a JSON object or double-quoted attributes. Use `label` plus `prompt`, or `value` when they are identical; `selected_style` is optional. A button-only reply may omit parent text because the bridge supplies `☑️ **Choose an option:**` automatically.
187
+ - Add `telegram_button` with a JSON object, a JSON array of button objects, or double-quoted attributes; `telegram_buttons` is an alias. Prefer one array for multiple controls. Use `label` plus `prompt`, or `value` when they are identical; `selected_style` is optional. A button-only reply may omit parent text because the bridge supplies `☑️ **Choose an option:**` automatically.
184
188
  - For ordinary Telegram-turn replies, do not call transport tools for voice or buttons; the bridge owns delivery, while registered voice synthesis providers own TTS and OGG/Opus conversion. For explicit local/TUI direct sends, `telegram_message` may include top-level `telegram_button` comments in its Markdown text because those buttons are attached to that text message.
185
189
  - Prefer meaningful visible parent text when it adds context; for a button-only answer, rely on the bridge's automatic `☑️ **Choose an option:**` fallback rather than manufacturing duplicate text.
186
190
 
@@ -65,7 +65,7 @@ This command surface is a mobile companion subset, not a raw terminal-command br
65
65
  - `telegram_message(text, chat_id?, thread_id?)` sends a direct Telegram Markdown message when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. During an active Telegram turn, omitted targeting and an explicit target equal to that turn are rejected so the ordinary final-reply path remains the sole current-target response; an explicit different chat/thread target remains allowed for requested cross-target delivery. Outside active turns, paired/default local/TUI delivery remains unchanged. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
66
66
  - The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, and profile-specific debugging guidance. The regular prompt routes applicable turns to that Skill. `telegram_attach` and `telegram_message` remain registered but are model-active only while this instance owns direct transport or holds a live follower registration; disconnect/loss suppresses their schemas and prompt metadata, and recovery restores only the operator's previously active pi-telegram subset.
67
67
  - `telegram_voice` hidden comments request Telegram-native voice delivery through either a JSON object or double-quoted attributes. Equivalent `text` or `value` supplies the spoken payload; explicit `text` takes precedence.
68
- - `telegram_button` hidden comments create inline buttons whose taps enqueue prompts through the same two payload forms. Use `label` plus `prompt`, or `value` when both strings are identical. Both action names use one colon-free marker; colon-prefixed payloads are rejected. Use top-level column-zero comments outside code, quotes, lists, and indented examples; do not emit standalone button actions.
68
+ - `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. One marker accepts a JSON object, a JSON array whose objects become separate button rows, or double-quoted attributes; `telegram_buttons` is an exact plural alias. Prefer one array comment for multiple buttons. Use `label` plus `prompt`, or `value` when both strings are identical. Action markers are colon-free; colon-prefixed payloads are rejected. Use top-level column-zero comments outside code, quotes, lists, and indented examples; do not emit standalone button actions.
69
69
 
70
70
  Prompt guidance is context-aware: local/TUI prompts see only explicit direct-delivery guidance, while Telegram-originated turns receive the full action-comment syntax and phone-width output contract.
71
71
 
@@ -11,7 +11,7 @@ import type {
11
11
  TelegramInlineKeyboardMarkup,
12
12
  } from "./keyboard.ts";
13
13
  import {
14
- parseTelegramActionPayload,
14
+ parseTelegramActionPayloads,
15
15
  parseTopLevelTelegramComment,
16
16
  replaceTopLevelHtmlComments,
17
17
  } from "./outbound-markup.ts";
@@ -163,11 +163,14 @@ export function planTelegramButtonReply(
163
163
  ): TelegramButtonReplyPlan {
164
164
  const keyboard: TelegramOutboundButtonMarkup["inline_keyboard"] = [];
165
165
  const stripped = replaceTopLevelHtmlComments(markdown, (comment) => {
166
- const command = parseTopLevelTelegramComment(comment, "telegram_button");
166
+ const command = ["telegram_button", "telegram_buttons"].find((candidate) =>
167
+ parseTopLevelTelegramComment(comment, candidate),
168
+ );
167
169
  if (!command) return comment.raw;
168
- const payload = parseTelegramActionPayload(comment, "telegram_button");
169
- const action = payload ? parseTelegramButtonAction(payload) : undefined;
170
- if (action) {
170
+ const payloads = parseTelegramActionPayloads(comment, command) ?? [];
171
+ for (const payload of payloads) {
172
+ const action = parseTelegramButtonAction(payload);
173
+ if (!action) continue;
171
174
  keyboard.push([
172
175
  {
173
176
  text: action.text,
@@ -170,30 +170,61 @@ function parseCanonicalTelegramActionAttributes(
170
170
  return Object.keys(attributes).length > 0 ? attributes : undefined;
171
171
  }
172
172
 
173
- export function parseTelegramActionPayload(
173
+ function getTelegramActionPayloadSource(
174
174
  comment: TelegramTopLevelHtmlComment,
175
175
  command: string,
176
- ): Record<string, unknown> | undefined {
176
+ ): { source: string; hasBody: boolean } | undefined {
177
177
  const parsed = parseTopLevelTelegramComment(comment, command);
178
- if (!parsed) return undefined;
179
- if (parsed.head.trimStart().startsWith(":")) return undefined;
178
+ if (!parsed || parsed.head.trimStart().startsWith(":")) return undefined;
180
179
  const source = [parsed.head, parsed.body]
181
180
  .filter((part): part is string => part !== undefined)
182
181
  .join("\n")
183
182
  .trim();
184
- if (!source) return undefined;
185
- if (source.startsWith("{")) {
183
+ return source ? { source, hasBody: parsed.body !== undefined } : undefined;
184
+ }
185
+
186
+ function isTelegramActionPayload(value: unknown): value is Record<string, unknown> {
187
+ return value !== null && typeof value === "object" && !Array.isArray(value);
188
+ }
189
+
190
+ export function parseTelegramActionPayload(
191
+ comment: TelegramTopLevelHtmlComment,
192
+ command: string,
193
+ ): Record<string, unknown> | undefined {
194
+ const payload = getTelegramActionPayloadSource(comment, command);
195
+ if (!payload) return undefined;
196
+ if (payload.source.startsWith("{")) {
197
+ try {
198
+ const value: unknown = JSON.parse(payload.source);
199
+ return isTelegramActionPayload(value) ? value : undefined;
200
+ } catch {
201
+ return undefined;
202
+ }
203
+ }
204
+ if (payload.hasBody) return undefined;
205
+ return parseCanonicalTelegramActionAttributes(payload.source);
206
+ }
207
+
208
+ export function parseTelegramActionPayloads(
209
+ comment: TelegramTopLevelHtmlComment,
210
+ command: string,
211
+ ): Record<string, unknown>[] | undefined {
212
+ const payload = getTelegramActionPayloadSource(comment, command);
213
+ if (!payload) return undefined;
214
+ if (payload.source.startsWith("[") || payload.source.startsWith("{")) {
186
215
  try {
187
- const value: unknown = JSON.parse(source);
188
- return value !== null && typeof value === "object" && !Array.isArray(value)
189
- ? (value as Record<string, unknown>)
216
+ const value: unknown = JSON.parse(payload.source);
217
+ if (isTelegramActionPayload(value)) return [value];
218
+ return Array.isArray(value) && value.every(isTelegramActionPayload)
219
+ ? value
190
220
  : undefined;
191
221
  } catch {
192
222
  return undefined;
193
223
  }
194
224
  }
195
- if (parsed.body !== undefined) return undefined;
196
- return parseCanonicalTelegramActionAttributes(source);
225
+ if (payload.hasBody) return undefined;
226
+ const attributes = parseCanonicalTelegramActionAttributes(payload.source);
227
+ return attributes ? [attributes] : undefined;
197
228
  }
198
229
 
199
230
  export function normalizeMarkdownAfterVoiceExtraction(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.30.1",
3
+ "version": "0.32.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,24 +1,31 @@
1
1
  ---
2
- name: control-surface
3
- description: Generates contextual, evidence-backed prompt-button interfaces as transient control surfaces for workflows, tools, stateful systems, choices, navigation, and operator actions—not only terminals. Use when a user asks the agent to generate buttons or a contextual operating surface through Telegram or another prompt-button transport. Existing transport menus and callback interfaces remain with their runtime owners.
2
+ name: generated-control-surface
3
+ description: Proactively compiles current state, available capabilities, and user intent into contextual, evidence-backed, ephemeral prompt-button interfaces. Use on Telegram turns and other prompt-button transports whenever controls materially shorten likely feedback, without requiring an explicit user request, while omitting decorative UI and preserving domain ownership; fixed transport menus and callbacks remain with their runtime owners.
4
4
  ---
5
5
 
6
- # Control Surface
6
+ # Generated Control Surface
7
7
 
8
- Generate a temporary, truthful interface that turns the agent's current understanding and available capabilities into useful controls. The agent remains the interpreter, authority boundary, and interface generator; buttons are contextual prompts, not a second application, hidden daemon, or direct grant of capability.
8
+ Compile a temporary, truthful interface from current state, available capabilities, and user intent. The agent remains the interpreter and authority boundary; the generated surface remains an ephemeral renderer projection, not a second application, hidden daemon, state owner, or direct grant of capability.
9
9
 
10
- ## Concept
10
+ ## Primitive
11
11
 
12
12
  ```text
13
- User intent + current evidence → compact state projection → contextual controls
13
+ interface = f(state, capabilities, intent)
14
+
15
+ current evidence + available acts + user intent
16
+ → compact state projection → contextual controls
14
17
  → next prompt → authorized act → fresh projection
15
18
  ```
16
19
 
17
- Each response is one generated control surface. Reinspect mutable reality after actions and regenerate the surface instead of maintaining a parallel UI model.
20
+ This is late-bound UI rather than a fixed frontend for one backend. Each response is one generated control surface whose renderer may be Telegram, another prompt-button transport, or a numbered fallback. Reinspect mutable reality after actions and regenerate instead of maintaining a parallel UI model.
21
+
22
+ The primitive belongs to the Surface plane: it projects State, exposes Agency capabilities, and preserves Constitution constraints without absorbing any of those owners. Its reusable competence lives in this Skill so quality depends less on rediscovering interface policy in each turn.
18
23
 
19
24
  ## Scope
20
25
 
21
- Use this Skill only to synthesize an agent-generated prompt-button surface. Operating or modifying an existing Telegram bridge menu, callback interface, or runtime-owned control stays with that subsystem and does not route here merely because it contains buttons.
26
+ Use this Skill only to synthesize a state-derived prompt-button surface. Operating or modifying an existing Telegram bridge menu, callback interface, fixed frontend, or runtime-owned control stays with that subsystem and does not route here merely because it contains buttons.
27
+
28
+ On Telegram turns, evaluate this Skill proactively rather than waiting for the user to ask for buttons. Load and apply it when a likely next decision, approval, navigation step, inspection, or bounded action can be made materially easier through controls; its correct output may still contain zero buttons when the admission test fails.
22
29
 
23
30
  A control surface may expose:
24
31
 
@@ -109,10 +116,11 @@ Re-check mutable targets immediately before execution. Access denial never autho
109
116
 
110
117
  ## Prompt Buttons
111
118
 
112
- Use the transport's canonical prompt-button syntax. For pi-telegram, emit a top-level hidden comment:
119
+ Use the transport's canonical prompt-button syntax. For pi-telegram, one top-level `telegram_button` comment accepts either one JSON object, double-quoted attributes, or a JSON array whose objects become separate button rows. Prefer one array comment for multiple controls instead of repeating the marker; `telegram_buttons` is a plural alias, not a different format.
113
120
 
114
121
  ```html
115
122
  <!-- telegram_button {"label":"🔍 Inspect run","prompt":"Inspect Run run:example read-only, summarize its current status and latest material evidence, then regenerate relevant supervision controls."} -->
123
+ <!-- telegram_button [{"label":"📁 etc","prompt":"/etc"},{"label":"📁 home","prompt":"/home"}] -->
116
124
  ```
117
125
 
118
126
  Button prompts must:
@@ -134,7 +142,7 @@ Use normal console programs as the capability owner. Check exit status and stder
134
142
 
135
143
  ### Filesystem
136
144
 
137
- Resolve paths before listing. Directory navigation may show safe names and metadata without reading contents. Never preview credential stores, private keys, browser profiles, cookies, tokens, wallets, or other secret-bearing files. Use unambiguous paths in prompts and offer safe operations before mutations.
145
+ Treat a user prompt that is exactly a plausible filesystem path—including `/`—as legitimate intent to render that location through a generated filesystem surface. Resolve and freshly inspect it before display. For directories, show safe names and types without reading file contents, paginate large listings, and offer useful Up, Previous, Next, and Refresh controls. A filesystem entry button may use the exact target path as its entire prompt because this Skill defines path-only prompts as navigation intent; use the entry name plus a semantic folder/file emoji as its label. Never preview credential stores, private keys, browser profiles, cookies, tokens, wallets, or other secret-bearing files, and never raise privileges merely to enumerate a path.
138
146
 
139
147
  ### Workflows And Actor Runs
140
148
 
@@ -46,6 +46,8 @@ Use `telegram_attach` outside Telegram turns only when the user explicitly reque
46
46
 
47
47
  ## Assistant-Authored Actions
48
48
 
49
+ On Telegram turns, proactively load `generated-control-surface` when a likely next decision or action may benefit from prompt buttons; do not wait for an explicit button request, and accept zero controls when its admission rules reject decorative or low-value UI.
50
+
49
51
  `telegram_button` and `telegram_voice` are hidden top-level HTML comments, not tools. Emit them at column zero, outside lists, quotes, code blocks, and indentation.
50
52
 
51
53
  Button forms:
@@ -53,9 +55,11 @@ Button forms:
53
55
  ```html
54
56
  <!-- telegram_button {"label":"Continue","prompt":"Continue with the current plan."} -->
55
57
  <!-- telegram_button value="Continue" -->
58
+ <!-- telegram_button [{"label":"📁 etc","prompt":"/etc"},{"label":"📁 home","prompt":"/home"}] -->
59
+ <!-- telegram_buttons [{"value":"Approve"},{"value":"Reject"}] -->
56
60
  ```
57
61
 
58
- - Payloads accept JSON after an optional colon or double-quoted attributes; keep the complete action in one top-level comment and encode JSON line breaks as `\n`.
62
+ - `telegram_button` accepts one JSON object, a JSON array of button objects, or double-quoted attributes; `telegram_buttons` is an exact plural alias. Prefer one array comment for multiple buttons. Keep the complete action in one top-level comment and encode JSON line breaks as `\n`.
59
63
  - Use `label` plus a self-contained `prompt`, or non-empty `value` when both are identical.
60
64
  - Optional `selected_style` is `primary` (default), `success`, or `danger`; style never suppresses prompt admission.
61
65
  - If button comments form the whole reply, the bridge supplies the standard choice heading.