@llblab/pi-telegram 0.36.0 → 0.36.1

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/BACKLOG.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  _This file owns unresolved project work only. Completed behavior belongs in `CHANGELOG.md`; durable contracts belong in `AGENTS.md` and `/docs`._
4
4
 
5
+ - [ ] `Automated release publication live proof`: Exercise the implemented tag-triggered release path on `v0.36.1` and verify reusable full validation, exact tag/version/lock/changelog preflight, npm Trusted Publisher provenance, idempotent package identity, public tarball contents, and one workflow-owned GitHub Release before declaring manual npm publication retired.
5
6
  - [ ] `Operator-confirmed pairing`: Replace first-contact automatic ownership with an explicit pending request that the trusted Pi interface can allow or reject before `allowedUserId` is persisted or any Telegram prompt executes; retain manual preconfiguration, bound pending-request lifetime and multiplicity, fail closed across session/transport replacement, and cover unauthorized-first-contact races.
6
- - [ ] `Unauthorized denial consistency`: Align unauthorized Guest Mode answers and direct private-message replies so both begin with the same denial emoji and apply equivalent explicit bold emphasis to their denial copy through each surface's supported renderer; centralize the copy/style owner and protect both paths with focused regressions without pretending callback popups support rich text.
7
7
  - [ ] `Environment-backed bot tokens`: Let each profile store an exact Pi-native `$ENV_VAR` or `${ENV_VAR}` reference in `telegram.json` instead of copying the resolved secret; preserve the originating alias when `/telegram-setup` prefills from a supported environment variable, resolve only at validation/activation boundaries, fail closed with a redacted named-variable diagnostic when unresolved, retain literal-token compatibility, and cover named profiles, reload, persistence, status, and secret-redaction paths.
8
8
  - [ ] [`Inference bypass Generative Apps`](./docs/generative-apps.md): Let the agent install and bind one managed JavaScript app owner for a generated-prompt prefix so deterministic controls can bypass model inference without creating another button grammar.
9
9
  - [ ] Harden the implemented `telegram_bind` installation/invocation kernel with removal operations, stronger cross-process replacement recovery, and bounded diagnostics while preserving lifecycle cancellation, worker-isolated methods, explicit staged replacement, canonical `<agent-dir>/genapps/<app>/<app>.mjs` identity, mandatory named `init`, no manifest/package metadata, non-symlink roots/sources, and fail-closed silent replacement.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.36.1: Adaptive Control Surfaces
8
+
9
+ - `Adaptive Button Literal`: Replaces binary JSON-or-CML parsing with one strict-JSON-first matrix grammar over a shared button AST. Named JSON objects and positional CML cells may coexist in one matrix or row, commas are optional only between completed elements, every compression step preserves semantics, and malformed comments reject atomically without registering partial callbacks.
10
+ - `Trusted Release Publication`: Makes tag releases depend on reusable full validation, exact tag/package/lock/changelog identity, npm Trusted Publisher provenance, idempotent existing-version checks, public package and tarball verification, and one workflow-owned GitHub Release path without npm tokens.
11
+ - `Consistent Access Denial`: Centralizes unauthorized copy so Guest Mode and private messages render `🚫 Access denied.` with equivalent explicit bold emphasis through HTML-capable surfaces, while callback popups use the same emoji and plain copy without pretending to support rich text.
12
+ - `Destructive Skip State`: Changes the active queue Skip selector from yellow to red and records the indicator in the UI registry, making deferred prompt removal visually distinct from Priority, Scoped, and other elevated but non-destructive states.
13
+ - `Worker Cancellation Fence`: Makes Generative App workers acknowledge child-process abort handling before parent termination, with a bounded forced fallback for synchronously blocked methods, preventing Linux CI load from orphaning a cancelled process long enough to commit its delayed side effect.
14
+ - `Capability App Routing`: Telegram interaction now prefers an already-advertised capability-owned Generative App for repeated controls before synthesizing one-shot prompt buttons, while the bridge remains generic and neither discovers capability-specific apps nor owns their state or identities.
15
+ - `Hybrid Surface Breadcrumbs`: Generated Control Surface and Generative Apps now cross-route over one logical button matrix and shared JSON/CML runtime notation: ephemeral agent composition stays separate from reusable scripts, while one app view may combine deterministic bound methods with ordinary model-mediated prompts and no third button Skill is introduced.
16
+
7
17
  ## 0.36.0: Generative Apps
8
18
 
9
19
  - `Generative App Kernel`: Adds `telegram_bind` for canonical `.mjs` install/replace/invoke with mandatory `init`, strict JSON, bounded output/processes, transactional snapshots, repaired partial-tail recovery, non-symlink root/app/module/source containment, and worker-isolated terminable methods. Lifecycle cancellation stops child processes and fences state commit; failed staging or `init` preserves the installed app.
package/README.md CHANGED
@@ -220,7 +220,7 @@ Voice notes, audio, images, PDFs, and other media can pass through configured in
220
220
 
221
221
  ### Buttons And Callbacks
222
222
 
223
- Assistant replies can include top-level hidden `telegram_button` comments using a JSON object, JSON matrix, Compact Matrix Literal (CML), or compact double-quoted attributes; `telegram_buttons` is a plural alias. In JSON and CML matrices, top-level cells become full-width rows while nested rows group one or more buttons horizontally without an artificial parser-level width cap; generated surfaces default to five columns and use six to eight only for short position-bearing labels. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}` with `primary`, `success`, or `danger`; the optional style requires an explicit prompt. It trims atom boundaries, preserves non-structural text literally, and decodes only `\|`, `\}`, and `\\`. Prefer one matrix 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.
223
+ Assistant replies can include top-level hidden `telegram_button` comments using a JSON object, adaptive JSON/CML matrix, positional Compact Matrix Literal (CML), or compact double-quoted attributes; `telegram_buttons` is a plural alias. One adaptive matrix may mix named JSON objects with positional CML cells, and commas between completed matrix or row elements are optional while JSON object internals remain strict. Top-level cells become full-width rows while nested rows group one or more buttons horizontally without an artificial parser-level width cap; generated surfaces default to five columns and use six to eight only for short position-bearing labels. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}` with `primary`, `success`, or `danger`; the optional style requires an explicit prompt. It trims atom boundaries, preserves non-structural text literally, and decodes only `\|`, `\}`, and `\\`. Prefer one matrix 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.
224
224
 
225
225
  ### Threaded Mode And Multi-Instance Bus
226
226
 
package/docs/README.md CHANGED
@@ -15,7 +15,7 @@ Living index of project documentation in `/docs`.
15
15
  - [command-templates.md](./command-templates.md) — Portable command-template standard core
16
16
  - [inbound.md](./inbound.md) — Local `pi-telegram` inbound text/media handler bus, programmatic inbound handlers, registered STT provider fallbacks, legacy `attachmentHandlers` compatibility, placeholders, and fallbacks
17
17
  - [outbound.md](./outbound.md) — Local `pi-telegram` outbound-handler config, text/voice/button behavior, single-artifact Rich results, voice synthesis provider fallback priority, artifact outputs, and callback routing
18
- - [compact-matrix-literal.md](./compact-matrix-literal.md) — Portable Compact Matrix Literal v1 standard for trimmed key-value cells, minimal escapes, bounded-depth rows, JSON coexistence, and renderer-owned width policy
18
+ - [compact-matrix-literal.md](./compact-matrix-literal.md) — Adaptive Button Literal / CML v3 standard for strict JSON objects, positional cells, optional element commas, mixed bounded-depth matrices, atomic parsing, and renderer-owned width policy
19
19
  - [generative-apps.md](./generative-apps.md) — Generative Apps runtime and wire contract for managed `.mjs` identity, inference-bypass bindings, persistent state timelines, bounded adapters, replacement, and lifecycle; agent operation lives in the bundled `generative-apps` Skill
20
20
  - [callback-namespaces.md](./callback-namespaces.md) — Shared Telegram `callback_data` namespace standard for layered extensions
21
21
  - [updates.md](./updates.md) — Update classification and runtime handler registry that lets layered extensions observe and consume Telegram updates without owning their own polling connection
@@ -344,7 +344,7 @@ Outbound files staged during an active Telegram turn are delivered after that tu
344
344
  Assistant-authored final-message actions use hidden top-level comments with colon-free markers; colon-prefixed payloads are rejected:
345
345
 
346
346
  - `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.
347
- - `telegram_button` accepts a JSON object, JSON matrix, Compact Matrix Literal, or double-quoted attributes; `telegram_buttons` aliases the same parser. Each top-level matrix cell creates one full-width row, while a nested row groups one or more buttons horizontally without an artificial parser-width cap; every callback enqueues its configured prompt text as a normal Telegram prompt turn. CML's portable grammar, trim/escape rules, JSON-first routing, and renderer-owned width policy are specified in [Compact Matrix Literal](./compact-matrix-literal.md).
347
+ - `telegram_button` accepts a JSON object, adaptive JSON/CML matrix, positional Compact Matrix Literal, or double-quoted attributes; `telegram_buttons` aliases the same parser. Named JSON objects and positional cells may coexist, with commas optional only between completed matrix or row elements. Each top-level cell creates one full-width row, while a nested row groups buttons horizontally without an artificial parser-width cap; every callback enqueues its configured prompt text as a normal Telegram prompt turn. The JSON-first grammar, positional trim/escape rules, atomic rejection, and renderer-owned width policy are specified in [Adaptive Button Literal](./compact-matrix-literal.md).
348
348
 
349
349
  Preview delivery strips top-level action comments before streaming draft Markdown. Comments inside code fences, quotes, lists, or indented examples stay literal.
350
350
 
@@ -1,94 +1,101 @@
1
- # Compact Matrix Literal
1
+ # Adaptive Button Literal
2
2
 
3
- > Status: Portable v2 standard implemented by the unreleased `pi-telegram` control-surface parser.
3
+ > Status: Portable CML v3 standard implemented by the unreleased `pi-telegram` control-surface parser.
4
4
 
5
- Compact Matrix Literal (CML) is a bounded-depth text format for ordered key-value cells arranged as singleton or compact rows. It optimizes repeated interactive controls where JSON field names, quotes, and commas dominate the payload.
5
+ Adaptive Button Literal is one bounded-depth matrix grammar over a shared button AST. It accepts strict JSON button objects, positional Compact Matrix Literal (CML) cells, or both in the same matrix and row. Commas between completed matrix or row elements are optional, so producers can progressively compress representation without changing runtime meaning.
6
6
 
7
- CML is transport-neutral. An embedding maps each decoded cell's `key`, `value`, and optional `variant` to its own domain. The `pi-telegram` profile maps them to button label, prompt, and selected style.
8
-
9
- ## Goals
7
+ ```text
8
+ full named JSON → comma-optional adjacency → mixed named/positional cells → compact CML
9
+ ```
10
10
 
11
- - Encode common key-value matrices with minimal punctuation.
12
- - Preserve ordered singleton and compact rows.
13
- - Preserve non-structural Unicode text literally.
14
- - Admit deterministic linear-time parsing without evaluation or recovery.
15
- - Remain deterministic beside an existing JSON form.
16
- - Fail closed on malformed or deeper structures.
11
+ The compact form is not JSON: `{label|prompt|variant}` assigns meaning by position. The formats share semantics and topology, not syntax.
17
12
 
18
- ## Non-Goals
13
+ ## Goals
19
14
 
20
- - Replacing JSON for arbitrary objects, multiline values, non-positional metadata, or extensible schemas.
21
- - Defining callback ownership, application state, rendering policy, or transport behavior.
22
- - Recovering partial intent from malformed input.
23
- - Defining one universal visual row-width limit for every renderer.
15
+ - Preserve one ordered button matrix across multiple representation densities.
16
+ - Let producers compress individual cells without converting the whole surface.
17
+ - Preserve valid strict JSON behavior unchanged.
18
+ - Admit deterministic linear-time parsing without evaluation or partial recovery.
19
+ - Keep rendering, callback ownership, and application state outside the notation.
24
20
 
25
21
  ## Data Model
26
22
 
27
- A decoded payload is an ordered non-empty list of non-empty rows:
23
+ Every accepted payload normalizes to a non-empty ordered list of non-empty rows:
28
24
 
29
25
  ```text
30
- Cell = { key: string, value: string, variant?: string }
26
+ Cell = { label?: string, prompt?: string, value?: string, selected_style?: string }
31
27
  Rows = Cell[][]
32
28
  ```
33
29
 
34
- A top-level cell normalizes to a singleton row. A nested row preserves its compact grouping.
30
+ A top-level cell becomes a singleton row. A nested row preserves horizontal grouping.
35
31
 
36
- A cell with one atom copies its key into its value:
32
+ These representations are semantically equivalent:
37
33
 
38
34
  ```text
39
- {7} == { key: "7", value: "7" }
35
+ [[{"label":"Pause","prompt":"music::pause"},{"value":"Next"}],{"value":"Status"}]
36
+ [[{"label":"Pause","prompt":"music::pause"}{"value":"Next"}]{"value":"Status"}]
37
+ [[{"label":"Pause","prompt":"music::pause"},{Next}],{Status}]
38
+ [[{Pause|music::pause}{Next}]{Status}]
40
39
  ```
41
40
 
42
- A cell with two atoms separates key and value with one unescaped vertical bar:
41
+ Named JSON objects and positional cells may coexist within the same horizontal row:
43
42
 
44
43
  ```text
45
- {🟥|2,5} == { key: "🟥", value: "2,5" }
44
+ [[{"label":"Open","prompt":"/tmp"}{Back|/}]]
46
45
  ```
47
46
 
48
- A cell with three atoms adds one optional positional variant. The variant is valid only when the explicit value atom is present:
47
+ ## Positional Cells
48
+
49
+ A one-atom cell copies its value into label and prompt through the existing button contract:
49
50
 
50
51
  ```text
51
- {Stop|music-player::stop|danger}
52
- == { key: "Stop", value: "music-player::stop", variant: "danger" }
52
+ {Next}
53
53
  ```
54
54
 
55
- ## Grammar
55
+ A two-atom cell separates label and prompt:
56
+
57
+ ```text
58
+ {Pause|music::pause}
59
+ ```
56
60
 
57
- The normative structural grammar is:
61
+ A three-atom cell adds the selected style:
58
62
 
59
63
  ```text
60
- payload := cell | matrix
61
- matrix := "[" ws element (ws element)* ws "]"
62
- element := cell | row
63
- row := "[" ws cell (ws cell)* ws "]"
64
- cell := "{" atom "}"
65
- | "{" atom "|" atom "}"
66
- | "{" atom "|" atom "|" atom "}"
67
- atom := atom-unit+
68
- atom-unit := ordinary | "\\|" | "\\}" | "\\\\"
69
- ws := *(SP | HTAB | CR | LF)
64
+ {Stop|music::stop|danger}
70
65
  ```
71
66
 
72
- `ordinary` is any printable Unicode scalar other than unescaped `|`, unescaped `}`, or `\`. No commas separate elements. A matrix and every nested row must contain at least one element. A row cannot contain another row.
67
+ The style atom is accepted only as `primary`, `success`, or `danger`.
73
68
 
74
- Examples:
69
+ ## Adaptive Grammar
70
+
71
+ The structural grammar is:
75
72
 
76
73
  ```text
77
- {Continue}
78
- {Open|/tmp}
79
- [{Up|/}[{Prev|page-1}{Next|page-3}]{etc|/etc}]
80
- [[{1}{2}{3}{4}{5}{6}{7}{8}]]
81
- {A \| B|C:\\Games\}}
82
- {Stop|music-player::stop|danger}
74
+ payload := json-object | matrix | positional-cell
75
+ matrix := "[" ws element (boundary element)* ws "]"
76
+ element := cell | row
77
+ row := "[" ws cell (boundary cell)* ws "]"
78
+ cell := json-object | positional-cell
79
+ boundary := ws [","] ws
80
+ positional-cell := "{" atom "}"
81
+ | "{" atom "|" atom "}"
82
+ | "{" atom "|" atom "|" atom "}"
83
+ atom := atom-unit+
84
+ atom-unit := ordinary | "\|" | "\}" | "\\"
85
+ ws := *(SP | HTAB | CR | LF)
83
86
  ```
84
87
 
85
- These normalize respectively to one copied singleton cell, one key-value singleton cell, a mixed singleton/compact matrix, one eight-cell row, `{ key: "A | B", value: "C:\\Games}" }`, and one key-value cell with the `danger` variant.
88
+ `boundary` occurs only after one complete element and before another. It may contain one comma or no comma. Element delimiters make empty adjacency unambiguous. Leading, repeated, and trailing commas are invalid.
89
+
90
+ A `json-object` is one complete strict JSON object. Its property commas, strings, escaping, nested values, and other internals remain ordinary strict JSON; comma optionality applies only between matrix or row elements.
91
+
92
+ Rows cannot contain rows. The grammar never recurses beyond one row inside the top-level matrix.
86
93
 
87
94
  ## Atoms, Whitespace, And Escapes
88
95
 
89
- Leading and trailing whitespace in each decoded key and value is trimmed. Internal ordinary spaces are preserved. CR, LF, HTAB, C0 controls, DEL, and C1 controls that remain inside an atom after trimming are invalid.
96
+ Leading and trailing whitespace in positional atoms is trimmed. Internal ordinary spaces are preserved. CR, LF, HTAB, C0 controls, DEL, and C1 controls remaining inside an atom after trimming are invalid.
90
97
 
91
- Only three escape sequences exist:
98
+ Only three positional-cell escapes exist:
92
99
 
93
100
  ```text
94
101
  \| → literal |
@@ -96,104 +103,74 @@ Only three escape sequences exist:
96
103
  \\ → literal \
97
104
  ```
98
105
 
99
- Unknown escapes and a trailing backslash are invalid. No character is silently dropped.
100
-
101
- Every other printable character is literal inside a cell, including:
102
-
103
- ```text
104
- { [ ] " : , / emoji and ordinary spaces
105
- ```
106
-
107
- An opening `{` has no structural meaning after a cell has begun. Square brackets are structural only outside a cell. One or two unescaped vertical bars select the two- or three-atom form; a third is invalid. Every selected atom must remain non-empty after trimming. When multiline text or non-positional metadata is needed, the producer uses the embedding's JSON or other full-fidelity form.
108
-
109
- ## Width Policy
110
-
111
- CML Core does not impose a visual row-width maximum. Width is a renderer and interaction-policy concern, not a property of the key-value matrix wire format.
112
-
113
- An embedding may enforce a documented host limit. The `pi-telegram` parser does not add an artificial per-row width cap because Telegram Bot API does not document one and existing top-level matrices already admit host-bounded action counts. Its bundled Generated Control Surface Skill owns UX policy: five columns are the proven default for short position-bearing labels, six to eight may be used only when labels remain compact and readable, and wider surfaces should normally be regrouped.
106
+ Every other printable character is literal inside a positional cell, including commas, colons, quotes, square brackets, emoji, and ordinary spaces. A comma inside `{label|prompt}` is data; a comma after the closing `}` is an optional element separator.
114
107
 
115
- ## Parsing Contract
108
+ ## Deterministic Parsing
116
109
 
117
110
  A conforming parser:
118
111
 
119
- 1. Consumes Unicode text without executing, interpolating, or evaluating it.
120
- 2. Parses exactly one `payload` and rejects trailing non-whitespace input.
121
- 3. Rejects empty atoms, empty matrices, empty rows, and nesting deeper than one row inside the top-level matrix.
122
- 4. Rejects missing, extra, crossed, or mismatched delimiters.
123
- 5. Accepts at most two unescaped vertical bars in a cell and rejects a third.
124
- 6. Decodes only `\|`, `\}`, and `\\`; unknown or trailing escapes fail.
125
- 7. Trims atom boundaries, then rejects empty values and remaining control characters.
126
- 8. Returns no partial rows or cells after any failure.
127
- 9. Runs in linear time over a host-bounded payload and does not recurse beyond the fixed grammar depth.
112
+ 1. Attempts strict JSON first for sources beginning with `{` or `[`. Successful JSON is validated only against the existing button matrix schema and never reinterpreted.
113
+ 2. If strict JSON parsing fails, parses the original source with the adaptive grammar.
114
+ 3. Tries one complete strict JSON object at each cell boundary before positional interpretation.
115
+ 4. Accepts at most one optional comma between completed matrix or row elements.
116
+ 5. Rejects leading, repeated, trailing, or property-level omitted commas.
117
+ 6. Rejects empty atoms, matrices, rows, and nesting deeper than one row.
118
+ 7. Decodes only `\|`, `\}`, and `\\` in positional cells.
119
+ 8. Consumes exactly one complete payload and rejects trailing content.
120
+ 9. Returns no partial rows or cells after any failure.
121
+ 10. Runs in linear time over a host-bounded payload with fixed grammar depth.
128
122
 
129
- Implementations may report diagnostics internally, but an invalid payload must not register or execute any action.
123
+ Malformed JSON-looking input receives no generic recovery. It is accepted only if it independently forms a complete valid adaptive literal.
130
124
 
131
- ## JSON Coexistence
125
+ ## Telegram Profile
132
126
 
133
- An embedding that already accepts JSON uses deterministic routing:
127
+ For `telegram_button` and the exact `telegram_buttons` alias:
134
128
 
135
- 1. Attempt strict JSON parsing first for payloads beginning with `{` or `[`.
136
- 2. If JSON parsing succeeds, validate only against the embedding's JSON schema. A JSON shape failure must not fall back to CML.
137
- 3. If JSON parsing fails, attempt CML from the original source.
138
- 4. Accept CML only after complete grammar and embedding validation.
129
+ - JSON `value` keeps its existing label/prompt fallback semantics.
130
+ - Positional `{value}` is equivalent to JSON `{"value":"value"}`.
131
+ - Positional `{label|prompt}` is equivalent to JSON `{"label":"label","prompt":"prompt"}`.
132
+ - Positional `{label|prompt|selected_style}` is equivalent to the corresponding three-field JSON object.
133
+ - Top-level cells become full-width rows.
134
+ - Nested rows become horizontal keyboard rows.
135
+ - Invalid payloads are stripped with their recognized action comment and register no callbacks.
139
136
 
140
- Valid JSON behavior therefore remains unchanged. A malformed JSON-looking source receives no tolerant recovery: it is accepted only when it independently forms a complete valid CML payload.
141
-
142
- ## `pi-telegram` Profile
143
-
144
- For `telegram_button` and its exact `telegram_buttons` alias:
145
-
146
- - `Cell.key` becomes the visible button label.
147
- - `Cell.value` becomes the queued prompt.
148
- - A top-level cell becomes one full-width inline-keyboard row.
149
- - A nested row becomes one horizontal row.
150
- - `{value}` is equivalent to JSON `{"value":"value"}`.
151
- - `{label|prompt}` is equivalent to JSON `{"label":"label","prompt":"prompt"}`.
152
- - `{label|prompt|selected_style}` is equivalent to JSON `{"label":"label","prompt":"prompt","selected_style":"selected_style"}`.
153
- - The third atom is accepted only when the prompt atom is present and its exact value is `primary`, `success`, or `danger`.
154
- - JSON and double-quoted attributes remain the full-fidelity forms for Generative App script output, multiline values, and other metadata.
155
- - Invalid CML is stripped with its enclosing recognized action comment and registers no callbacks, matching existing fail-closed action behavior.
156
-
157
- Example embedding:
137
+ Example:
158
138
 
159
139
  ```html
160
- <!-- telegram_button [{⬆️ Up|/}[{⬅️|page-1}{➡️|page-3}]{📁 etc|/etc}] -->
140
+ <!-- telegram_button [{⬆️ Up|/},[{⬅️|page-1}{➡️|page-3}],{"label":"📁 etc","prompt":"/etc"}] -->
161
141
  ```
162
142
 
163
- The enclosing HTML-comment transport still owns its own delimiter boundary; content containing the comment terminator cannot reach the CML parser and must use another supported delivery representation.
143
+ The enclosing HTML-comment transport owns its own delimiter boundary. Content containing the comment terminator must use another supported representation.
164
144
 
165
- ## Conformance Classes
145
+ ## Width Policy
146
+
147
+ The grammar imposes no visual row-width maximum. Renderer and interaction policy own width. The bundled Generated Control Surface Skill defaults to at most five short position-bearing controls per row, permits six to eight only when labels remain compact, and treats eight as the phone-width UX maximum.
166
148
 
167
- A conformance suite covers properties rather than incident-specific strings.
149
+ ## Conformance
168
150
 
169
- ### Accepted
151
+ Accepted classes include:
170
152
 
171
- - Singular copied, key-value, and key-value-variant cells.
172
- - Top-level singleton rows.
173
- - Nested rows at widths one, five, and eight.
174
- - Mixed singleton and compact rows.
175
- - Unicode, punctuation, brackets, quotes, commas, colons, and internal spaces.
176
- - Trimmed atom boundaries.
177
- - Each defined escape sequence.
178
- - Structural whitespace between tokens.
179
- - Semantic equivalence with supported JSON cell forms.
153
+ - Strict JSON objects and matrices.
154
+ - Positional singleton, two-atom, and styled cells.
155
+ - Matrices and rows with commas, without commas, or a mixture of boundaries.
156
+ - Named JSON and positional cells mixed in one matrix or row.
157
+ - Literal commas inside positional atoms and strict JSON strings.
158
+ - Unicode, defined escapes, structural whitespace, and rows at supported renderer widths.
159
+ - Semantic equivalence across every progressive-compression step.
180
160
 
181
- ### Rejected
161
+ Rejected classes include:
182
162
 
183
- - Empty payload, matrix, row, key, or value.
184
- - Deeper nesting.
185
- - Missing or mismatched delimiters.
186
- - A third unescaped separator, an empty positional atom, or a profile-invalid variant.
187
- - Unknown or trailing escapes.
188
- - Internal control characters.
189
- - Commas between cells.
190
- - Trailing garbage.
191
- - JSON that parses but fails the JSON action schema.
163
+ - Empty payloads, matrices, rows, labels, prompts, or style atoms.
164
+ - Leading, repeated, or trailing element commas.
165
+ - Missing commas between properties inside a JSON object.
166
+ - Deeper row nesting.
167
+ - Missing, crossed, or mismatched delimiters.
168
+ - A third positional separator, unknown style, unknown escape, or trailing backslash.
169
+ - Internal control characters and trailing garbage.
170
+ - Valid JSON that fails the existing JSON action schema.
192
171
 
193
172
  Every rejected case proves zero callback registration.
194
173
 
195
174
  ## Versioning
196
175
 
197
- This document defines CML v2. The three-atom cell is the explicit syntactic discriminator from v1: v1 parsers reject it atomically, while v2 parsers cannot reinterpret a valid v1 cell. Compatible embeddings may impose documented host-level byte, cell-count, variant-enum, or width limits without changing the core grammar, but must preserve bounded-depth and fail-closed semantics.
198
-
199
- Future versions must not assign new meaning to input rejected by a security or ownership boundary without an explicit syntactic discriminator. Non-positional metadata and deeper structures require a revised standard rather than permissive recovery.
176
+ This document defines CML v3. V3 extends the v2 positional grammar with strict JSON object cells, mixed representation, and optional element-boundary commas. It does not make JSON object internals permissive and does not add deeper structures. Future versions must preserve strict-JSON-first routing, bounded depth, atomic rejection, and an explicit discriminator for any new meaning at a security or ownership boundary.
package/docs/outbound.md CHANGED
@@ -164,7 +164,7 @@ I can continue.
164
164
 
165
165
  Rules:
166
166
 
167
- - `telegram_button` accepts a JSON object, JSON matrix, [Compact Matrix Literal](./compact-matrix-literal.md), or double-quoted HTML-like attributes; `telegram_buttons` is an exact plural alias. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}`; the optional third atom requires a prompt and accepts only `primary`, `success`, or `danger`. It trims every atom, preserves non-structural printable text, and decodes only `\|`, `\}`, and `\\`. Shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute forms are rejected.
167
+ - `telegram_button` accepts a JSON object, adaptive JSON/CML matrix, positional [Compact Matrix Literal](./compact-matrix-literal.md), or double-quoted HTML-like attributes; `telegram_buttons` is an exact plural alias. Named JSON objects and positional cells may coexist in one matrix or row, and commas are optional only between completed elements while JSON object internals remain strict. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}`; the optional third atom requires a prompt and accepts only `primary`, `success`, or `danger`. It trims every atom, preserves non-structural printable text, and decodes only `\|`, `\}`, and `\\`. Shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute forms are rejected.
168
168
  - A colon after either button marker is rejected so every payload form shares one unambiguous action marker.
169
169
  - 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.
170
170
  - 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.
@@ -66,7 +66,7 @@ This command surface is a mobile companion subset, not a raw terminal-command br
66
66
  - `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.
67
67
  - The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, Generative App operation, and profile-specific debugging guidance. The regular prompt routes applicable turns to that Skill. `telegram_attach`, `telegram_bind`, 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.
68
68
  - `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.
69
- - `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. One marker accepts a JSON object, JSON matrix, [Compact Matrix Literal](./compact-matrix-literal.md), or double-quoted attributes; `telegram_buttons` is an exact plural alias. Top-level matrix cells become full-width rows, while nested rows group one or more buttons horizontally without an artificial parser-width cap. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}`; the optional third atom requires a prompt and accepts only `primary`, `success`, or `danger`. It trims atom boundaries and supports only the minimal escapes `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Use JSON `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
+ - `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. One marker accepts a JSON object, adaptive JSON/CML matrix, positional [Compact Matrix Literal](./compact-matrix-literal.md), or double-quoted attributes; `telegram_buttons` is an exact plural alias. Named JSON objects and positional cells may coexist in one matrix or row, and commas are optional only between completed elements. Top-level cells become full-width rows, while nested rows group one or more buttons horizontally without an artificial parser-width cap. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}`; the optional third atom requires a prompt and accepts only `primary`, `success`, or `danger`. It trims atom boundaries and supports only the minimal escapes `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Use JSON `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.
70
70
 
71
71
  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.
72
72
 
package/docs/ui-style.md CHANGED
@@ -62,6 +62,7 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
62
62
  | --- | --- | --- | --- |
63
63
  | `🟢` | Current/active/enabled `On` | Current option in vertical lists, active state rows, active `On` toggle | One strong current marker per option list. |
64
64
  | `🟡` | Active `Off` or elevated/filter state | Active `Off` toggle, Priority/Scoped active tab | Yellow means intentionally not-normal or off/default-caution, not error. |
65
+ | `🔴` | Active destructive/deferred disposition | Active queue `Skip` selector | Red distinguishes a prompt that will be discarded at dispatch from reversible neutral or elevated state. |
65
66
  | `🟣` | Normal/default active tab | Normal priority tab, All/default scope tab, active page picker | Use for neutral active tabs. |
66
67
  | `⚫️` | Inactive placeholder | Inactive toggle values and inactive tabs | Keeps row width stable. |
67
68
  | `⬆️` | Navigate upward | `⬆️ Main menu`, `⬆️ Back` | Always first row in submenus. |
@@ -73,7 +74,7 @@ Queue reactions are shortcut controls for waiting turns. Preserve their semantic
73
74
  Queue item detail renders two independent selector rows:
74
75
 
75
76
  - `🟡 Priority` / `⚫️ Normal` or `⚫️ Priority` / `🟣 Normal` selects the lane.
76
- - `🟢 Keep` / `⚫️ Skip` or `⚫️ Keep` / `🟡 Skip` selects dispatch disposition.
77
+ - `🟢 Keep` / `⚫️ Skip` or `⚫️ Keep` / `🔴 Skip` selects dispatch disposition.
77
78
 
78
79
  The menu may clear internal Skip but cannot remove a reaction created by the user through Telegram's Bot API.
79
80
 
@@ -16,6 +16,7 @@ parentPort.on("message", (message) => {
16
16
  if (message?.type !== "abort") return;
17
17
  controller.abort();
18
18
  for (const child of runningChildren) child.kill();
19
+ parentPort.postMessage({ type: "abort-ack" });
19
20
  });
20
21
 
21
22
  function runBoundedProcess(input) {
@@ -490,7 +490,7 @@ async function executeGenerativeAppWorker(options: {
490
490
  if (settled || terminationError) return;
491
491
  terminationError = error;
492
492
  worker.postMessage({ type: "abort" });
493
- const forcedTermination = setTimeout(terminateAndFinish, 50);
493
+ const forcedTermination = setTimeout(terminateAndFinish, 250);
494
494
  forcedTermination.unref?.();
495
495
  };
496
496
  const abort = (): void => {
@@ -507,9 +507,14 @@ async function executeGenerativeAppWorker(options: {
507
507
  }, options.methodTimeoutMs);
508
508
  timeout.unref?.();
509
509
  options.execution?.signal.addEventListener("abort", abort, { once: true });
510
- worker.once("message", (message: { error?: string; ok?: boolean; result?: unknown }) => {
510
+ worker.on("message", (message: {
511
+ error?: string;
512
+ ok?: boolean;
513
+ result?: unknown;
514
+ type?: string;
515
+ }) => {
511
516
  if (terminationError) {
512
- terminateAndFinish();
517
+ if (message?.type === "abort-ack") terminateAndFinish();
513
518
  } else if (message?.ok === true) {
514
519
  finish(undefined, message.result);
515
520
  void worker.terminate();
package/lib/menu-queue.ts CHANGED
@@ -192,7 +192,7 @@ function buildTelegramQueueItemSubmenuReplyMarkup(
192
192
  callback_data: `queue:skip-set:${chatId}:${replyToMessageId}:keep`,
193
193
  },
194
194
  {
195
- text: isSkipped ? "🟡 Skip" : "⚫️ Skip",
195
+ text: isSkipped ? "🔴 Skip" : "⚫️ Skip",
196
196
  callback_data: `queue:skip-set:${chatId}:${replyToMessageId}:skip`,
197
197
  },
198
198
  ],
@@ -183,21 +183,20 @@ export function planTelegramButtonReply(
183
183
  parseTopLevelTelegramComment(comment, candidate),
184
184
  );
185
185
  if (!command) return comment.raw;
186
- const payloadRows = parseTelegramActionPayloadRows(comment, command) ?? [];
187
- for (const payloadRow of payloadRows) {
188
- const row = payloadRow.flatMap((payload) => {
189
- const action = parseTelegramButtonAction(payload);
190
- return action
191
- ? [{
192
- text: action.text,
193
- callback_data: deps.registerAction({
194
- ...action,
195
- ...(deps.binding ? { binding: deps.binding } : {}),
196
- }),
197
- }]
198
- : [];
199
- });
200
- if (row.length > 0) keyboard.push(row);
186
+ const payloadRows = parseTelegramActionPayloadRows(comment, command);
187
+ if (!payloadRows) return "";
188
+ const actionRows = payloadRows.map((payloadRow) =>
189
+ payloadRow.map(parseTelegramButtonAction),
190
+ );
191
+ if (actionRows.some((row) => row.some((action) => !action))) return "";
192
+ for (const actionRow of actionRows) {
193
+ keyboard.push(actionRow.map((action) => ({
194
+ text: action!.text,
195
+ callback_data: deps.registerAction({
196
+ ...action!,
197
+ ...(deps.binding ? { binding: deps.binding } : {}),
198
+ }),
199
+ })));
201
200
  }
202
201
  return "";
203
202
  });
@@ -207,7 +207,7 @@ export function parseTelegramActionPayload(
207
207
 
208
208
  const TELEGRAM_COMPACT_ACTION_CONTROL_PATTERN = /[\u0000-\u001f\u007f-\u009f]/u;
209
209
 
210
- function parseTelegramCompactActionPayloadRows(
210
+ function parseTelegramAdaptiveActionPayloadRows(
211
211
  source: string,
212
212
  ): Record<string, unknown>[][] | undefined {
213
213
  let offset = 0;
@@ -219,13 +219,20 @@ function parseTelegramCompactActionPayloadRows(
219
219
  const skipWhitespace = (): void => {
220
220
  while (isStructuralWhitespace(source[offset])) offset += 1;
221
221
  };
222
+ const consumeOptionalSeparator = (): boolean => {
223
+ skipWhitespace();
224
+ if (source[offset] !== ",") return true;
225
+ offset += 1;
226
+ skipWhitespace();
227
+ return source[offset] !== "," && source[offset] !== "]";
228
+ };
222
229
  const normalizeAtom = (value: string): string | undefined => {
223
230
  const normalized = value.trim();
224
231
  return normalized && !TELEGRAM_COMPACT_ACTION_CONTROL_PATTERN.test(normalized)
225
232
  ? normalized
226
233
  : undefined;
227
234
  };
228
- const parseCell = (): Record<string, unknown> | undefined => {
235
+ const parseCompactCell = (): Record<string, unknown> | undefined => {
229
236
  if (source[offset] !== "{") return undefined;
230
237
  offset += 1;
231
238
  const atomSources: string[][] = [[]];
@@ -267,6 +274,54 @@ function parseTelegramCompactActionPayloadRows(
267
274
  }
268
275
  return undefined;
269
276
  };
277
+ const parseJsonObjectCell = (): Record<string, unknown> | undefined => {
278
+ if (source[offset] !== "{") return undefined;
279
+ const start = offset;
280
+ const stack: string[] = [];
281
+ let inString = false;
282
+ let escaped = false;
283
+ for (let index = start; index < source.length; index += 1) {
284
+ const character = source[index]!;
285
+ if (inString) {
286
+ if (escaped) escaped = false;
287
+ else if (character === "\\") escaped = true;
288
+ else if (character === '"') inString = false;
289
+ continue;
290
+ }
291
+ if (character === '"') {
292
+ inString = true;
293
+ continue;
294
+ }
295
+ if (character === "{" || character === "[") {
296
+ stack.push(character);
297
+ continue;
298
+ }
299
+ if (character !== "}" && character !== "]") continue;
300
+ const opening = stack.pop();
301
+ if (
302
+ (character === "}" && opening !== "{") ||
303
+ (character === "]" && opening !== "[")
304
+ ) return undefined;
305
+ if (stack.length > 0) continue;
306
+ const candidate = source.slice(start, index + 1);
307
+ try {
308
+ const value: unknown = JSON.parse(candidate);
309
+ if (!isTelegramActionPayload(value)) return undefined;
310
+ offset = index + 1;
311
+ return value;
312
+ } catch {
313
+ return undefined;
314
+ }
315
+ }
316
+ return undefined;
317
+ };
318
+ const parseCell = (): Record<string, unknown> | undefined => {
319
+ const start = offset;
320
+ const jsonCell = parseJsonObjectCell();
321
+ if (jsonCell) return jsonCell;
322
+ offset = start;
323
+ return parseCompactCell();
324
+ };
270
325
  const parseRow = (): Record<string, unknown>[] | undefined => {
271
326
  if (source[offset] !== "[") return undefined;
272
327
  offset += 1;
@@ -281,6 +336,7 @@ function parseTelegramCompactActionPayloadRows(
281
336
  const cell = parseCell();
282
337
  if (!cell) return undefined;
283
338
  row.push(cell);
339
+ if (!consumeOptionalSeparator()) return undefined;
284
340
  }
285
341
  return undefined;
286
342
  };
@@ -299,15 +355,14 @@ function parseTelegramCompactActionPayloadRows(
299
355
  const cell = parseCell();
300
356
  if (!cell) return undefined;
301
357
  rows.push([cell]);
302
- continue;
303
- }
304
- if (character === "[") {
358
+ } else if (character === "[") {
305
359
  const row = parseRow();
306
360
  if (!row) return undefined;
307
361
  rows.push(row);
308
- continue;
362
+ } else {
363
+ return undefined;
309
364
  }
310
- return undefined;
365
+ if (!consumeOptionalSeparator()) return undefined;
311
366
  }
312
367
  return undefined;
313
368
  };
@@ -353,7 +408,7 @@ export function parseTelegramActionPayloadRows(
353
408
  }
354
409
  return rows;
355
410
  } catch {
356
- return parseTelegramCompactActionPayloadRows(payload.source);
411
+ return parseTelegramAdaptiveActionPayloadRows(payload.source);
357
412
  }
358
413
  }
359
414
  if (payload.hasBody) return undefined;
package/lib/updates.ts CHANGED
@@ -886,6 +886,19 @@ export type TelegramMessageOwnershipRecorder = (
886
886
  input: TelegramMessageOwnershipRecorderInput,
887
887
  ) => void;
888
888
 
889
+ interface TelegramUnauthorizedReplyOptions {
890
+ parseMode?: "HTML";
891
+ target?: { chatId: number; threadId?: number };
892
+ }
893
+
894
+ const TELEGRAM_UNAUTHORIZED_DENIAL_COPY = "Access denied.";
895
+
896
+ function formatTelegramUnauthorizedDenial(format: "plain" | "html"): string {
897
+ return format === "html"
898
+ ? `🚫 <b>${TELEGRAM_UNAUTHORIZED_DENIAL_COPY}</b>`
899
+ : `🚫 ${TELEGRAM_UNAUTHORIZED_DENIAL_COPY}`;
900
+ }
901
+
889
902
  export interface TelegramUpdateRuntimeDeps<
890
903
  TContext = unknown,
891
904
  TReactionUpdate extends TelegramMessageReactionUpdated =
@@ -927,7 +940,11 @@ export interface TelegramUpdateRuntimeDeps<
927
940
  callbackQueryId: string,
928
941
  text?: string,
929
942
  ) => Promise<void>;
930
- answerGuestQuery: (guestQueryId: string, text?: string) => Promise<void>;
943
+ answerGuestQuery: (
944
+ guestQueryId: string,
945
+ text?: string,
946
+ options?: Pick<TelegramUnauthorizedReplyOptions, "parseMode">,
947
+ ) => Promise<void>;
931
948
  handleAuthorizedTelegramCallbackQuery: (
932
949
  query: TCallbackQuery,
933
950
  ctx: TContext,
@@ -936,7 +953,7 @@ export interface TelegramUpdateRuntimeDeps<
936
953
  chatId: number,
937
954
  replyToMessageId: number,
938
955
  text: string,
939
- options?: { target?: { chatId: number; threadId?: number } },
956
+ options?: TelegramUnauthorizedReplyOptions,
940
957
  ) => Promise<number | undefined>;
941
958
  handleAuthorizedTelegramMessage: (
942
959
  message: TMessage,
@@ -996,7 +1013,11 @@ export interface TelegramUpdateRuntimeControllerDeps<
996
1013
  callbackQueryId: string,
997
1014
  text?: string,
998
1015
  ) => Promise<void>;
999
- answerGuestQuery: (guestQueryId: string, text?: string) => Promise<void>;
1016
+ answerGuestQuery: (
1017
+ guestQueryId: string,
1018
+ text?: string,
1019
+ options?: Pick<TelegramUnauthorizedReplyOptions, "parseMode">,
1020
+ ) => Promise<void>;
1000
1021
  handleAuthorizedTelegramCallbackQuery: (
1001
1022
  query: TCallbackQuery,
1002
1023
  ctx: TContext,
@@ -1005,7 +1026,7 @@ export interface TelegramUpdateRuntimeControllerDeps<
1005
1026
  chatId: number,
1006
1027
  replyToMessageId: number,
1007
1028
  text: string,
1008
- options?: { target?: { chatId: number; threadId?: number } },
1029
+ options?: TelegramUnauthorizedReplyOptions,
1009
1030
  ) => Promise<number | undefined>;
1010
1031
  handleAuthorizedTelegramMessage: (
1011
1032
  message: TMessage,
@@ -1449,7 +1470,7 @@ export async function executeTelegramUpdatePlan<
1449
1470
  assertExecutionCurrent();
1450
1471
  await deps.answerCallbackQuery(
1451
1472
  callbackQueryId,
1452
- "This bot is not authorized for your account.",
1473
+ formatTelegramUnauthorizedDenial("plain"),
1453
1474
  );
1454
1475
  }
1455
1476
  return;
@@ -1464,7 +1485,8 @@ export async function executeTelegramUpdatePlan<
1464
1485
  assertExecutionCurrent();
1465
1486
  await deps.answerGuestQuery(
1466
1487
  plan.guestMessage.guest_query_id,
1467
- "🚫 Access denied.",
1488
+ formatTelegramUnauthorizedDenial("html"),
1489
+ { parseMode: "HTML" },
1468
1490
  );
1469
1491
  return;
1470
1492
  }
@@ -1584,8 +1606,8 @@ export async function executeTelegramUpdatePlan<
1584
1606
  await deps.sendTextReply(
1585
1607
  replyTarget.chatId,
1586
1608
  replyTarget.messageId,
1587
- "This bot is not authorized for your account.",
1588
- { target: replyTarget },
1609
+ formatTelegramUnauthorizedDenial("html"),
1610
+ { parseMode: "HTML", target: replyTarget },
1589
1611
  );
1590
1612
  }
1591
1613
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.36.0",
3
+ "version": "0.36.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,6 +1,6 @@
1
1
  ---
2
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.
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; route reusable deterministic loops toward Generative Apps, omit decorative UI, and preserve domain ownership while fixed transport menus and callbacks remain with their runtime owners.
4
4
  ---
5
5
 
6
6
  # Generated Control Surface
@@ -25,7 +25,9 @@ The primitive belongs to the Surface plane: it projects State, exposes Agency ca
25
25
 
26
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, runtime-owned control, or installed Generative App stays with that subsystem and does not route here merely because it contains buttons.
27
27
 
28
- When a generated surface reveals a repeated stable interaction with bounded state and deterministic transitions, consider graduating it to the complementary `generative-apps` Skill. Keep one-off, interpretive, changing, and context-heavy interaction here; compile only when bypassing repeated model mediation has concrete latency, token, cost, reliability, or UX value.
28
+ This Skill and `generative-apps` share one logical button-matrix and `label + prompt` interaction model while retaining different execution owners. On Telegram, the bridge runtime owns the full JSON/CML action notation: this Skill emits an ephemeral surface from current context, while a Generative App returns reusable views through its script ABI. Shared rendering does not justify a third button Skill or move transport grammar into either interaction Skill.
29
+
30
+ When a generated surface reveals repeated stable interaction with bounded state and deterministic transitions, load the complementary `generative-apps` Skill and consider compiling that loop. A Generative App may retain ordinary model-mediated prompt buttons beside deterministic bound methods, so only the stable actions need bypass inference. Conversely, keep one-off, interpretive, changing, and context-heavy interaction here when a reusable script would add no concrete latency, token, cost, reliability, or UX value.
29
31
 
30
32
  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.
31
33
 
@@ -122,7 +124,7 @@ Re-check mutable targets immediately before execution. Access denial never autho
122
124
 
123
125
  ## Prompt Buttons
124
126
 
125
- Use the transport's canonical prompt-button syntax. For pi-telegram, one top-level `telegram_button` comment accepts one JSON object, double-quoted attributes, a JSON matrix, or Compact Matrix Literal (CML). CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}`; the optional third atom requires an explicit prompt and accepts only `primary`, `success`, or `danger`. It trims atom boundaries, preserves other printable text literally, and decodes only `\|`, `\}`, and `\\`. Prefer CML whenever the model authors the control and it can express the required surface; fall back to expanded JSON only for multiline prompts, non-positional metadata, or a concrete CML parse/render failure, never merely from implementation habit. Deterministic Generative App scripts may return ordinary JSON because their source payload does not consume model-output tokens; author and operate those adapters through the `generative-apps` Skill rather than growing a parallel app workflow here. A top-level cell becomes one full-width row, while a nested row groups one or more controls horizontally without a parser-level width cap. Prefer one layout comment for multiple controls instead of repeating the marker; `telegram_buttons` is a plural alias, not a different format.
127
+ Use the transport's canonical prompt-button syntax. For pi-telegram, one top-level `telegram_button` comment accepts one JSON object, double-quoted attributes, an adaptive JSON/CML matrix, or positional Compact Matrix Literal (CML). One matrix or row may mix named JSON objects with positional cells, and commas are optional only between completed elements while JSON object internals remain strict. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}`; the optional third atom requires an explicit prompt and accepts only `primary`, `success`, or `danger`. It trims atom boundaries, preserves other printable text literally, and decodes only `\|`, `\}`, and `\\`. Prefer CML whenever the model authors the control and it can express the required surface; fall back to expanded JSON only for multiline prompts, non-positional metadata, or a concrete CML parse/render failure, never merely from implementation habit. Deterministic Generative App scripts may return ordinary JSON because their source payload does not consume model-output tokens; author and operate those adapters through the `generative-apps` Skill rather than growing a parallel app workflow here. A top-level cell becomes one full-width row, while a nested row groups one or more controls horizontally without a parser-level width cap. Prefer one layout comment for multiple controls instead of repeating the marker; `telegram_buttons` is a plural alias, not a different format.
126
128
 
127
129
  ### Semantic Row Composition
128
130
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: generative-apps
3
- description: Design, author, review, install, replace, invoke, or debug reusable Generative Apps that combine generated button interfaces, deterministic bound methods, and optional model-mediated prompts. Use for standalone applications and bounded view/controller adapters rendered through pi-telegram.
3
+ description: Design, author, review, install, replace, invoke, or debug reusable Generative Apps that combine generated button interfaces, deterministic bound methods, and optional model-mediated prompts. Use for standalone applications and bounded view/controller adapters rendered through pi-telegram; prefer Generated Control Surface when no reusable deterministic program is earned.
4
4
  ---
5
5
 
6
6
  # Generative Apps
@@ -29,6 +29,10 @@ Generated Control Surface → current context → model → one ephemeral surfac
29
29
  Generative App → model → reusable program → many evolving surfaces
30
30
  ```
31
31
 
32
+ Both Skills use the same logical button matrix and `label + prompt` interaction model. The Telegram runtime owns its full JSON/CML notation and callback routing; this Skill owns reusable program judgment, while `generated-control-surface` owns ephemeral agent-authored composition. Shared rendering needs no third button Skill and does not collapse those responsibilities.
33
+
34
+ An app may mix deterministic `app::method` controls and ordinary prompts in one view. Compile only the stable transitions that benefit from inference bypass; keep explanation, interpretation, teaching, and adaptation on the model-mediated plane. When no reusable state or deterministic loop earns a script, load and use `generated-control-surface` instead.
35
+
32
36
  The `generated` / `generative` distinction is intentional. Do not rename `generated-control-surface` to a competing generative term.
33
37
 
34
38
  ## Ownership
@@ -46,7 +46,9 @@ 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.
49
+ Before compiling assistant-authored controls, inspect already-loaded capability guidance for an advertised maintained Generative App or view/controller adapter. When that owner-provided view exists and the current intent concerns repeated interaction, load and follow the bundled `generative-apps` Skill and prefer binding or invoking the existing app over synthesizing one-shot prompt buttons. This is a routing breadcrumb, not permission for the bridge to discover capability-specific apps, own their state, or hard-code their identities.
50
+
51
+ Otherwise, 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
52
 
51
53
  `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.
52
54
 
@@ -60,7 +62,7 @@ Button forms:
60
62
  <!-- telegram_button [{⬆️ Up|/}[{⬅️|page-1}{➡️|page-3}]{📁 etc|/etc}] -->
61
63
  ```
62
64
 
63
- - `telegram_button` accepts one JSON object, a JSON matrix, Compact Matrix Literal (CML), or double-quoted attributes; `telegram_buttons` is an exact plural alias. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}`; the optional third atom requires an explicit prompt and accepts only `primary`, `success`, or `danger`. It trims atom boundaries, preserves other printable text literally, and decodes only `\|`, `\}`, and `\\`. Prefer CML for model-authored controls whenever it can express the required surface; use expanded JSON only for multiline prompts, non-positional metadata, or a concrete parse/render failure fallback, never merely from implementation habit. Deterministic Generative App scripts may use ordinary JSON because their output does not spend model tokens. In a matrix, each top-level cell becomes a full-width row and each nested row groups one or more buttons horizontally without a parser-level width cap. Prefer one matrix comment for multiple buttons, normally keep generated rows at five columns or fewer, and use six through eight only for short position-bearing labels. Keep the complete action in one top-level comment and encode multiline content with JSON `\n`.
65
+ - `telegram_button` accepts one JSON object, an adaptive JSON/CML matrix, positional Compact Matrix Literal (CML), or double-quoted attributes; `telegram_buttons` is an exact plural alias. One matrix or row may mix named JSON objects with positional cells, and commas are optional only between completed elements while JSON object internals remain strict. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}`; the optional third atom requires an explicit prompt and accepts only `primary`, `success`, or `danger`. It trims atom boundaries, preserves other printable text literally, and decodes only `\|`, `\}`, and `\\`. Prefer CML for model-authored controls whenever it can express the required surface; use expanded JSON only for multiline prompts, non-positional metadata, or a concrete parse/render failure fallback, never merely from implementation habit. Deterministic Generative App scripts may use ordinary JSON because their output does not spend model tokens. In a matrix, each top-level cell becomes a full-width row and each nested row groups one or more buttons horizontally without a parser-level width cap. Prefer one matrix comment for multiple buttons, normally keep generated rows at five columns or fewer, and use six through eight only for short position-bearing labels. Keep the complete action in one top-level comment and encode multiline content with JSON `\n`.
64
66
  - Use `label` plus a self-contained `prompt`, or non-empty `value` when both are identical.
65
67
  - Optional `selected_style` is `primary` (default), `success`, or `danger`; style never suppresses prompt admission.
66
68
  - If button comments form the whole reply, the bridge supplies the standard choice heading.