@llblab/pi-telegram 0.11.1 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -73,7 +73,7 @@
73
73
  - Telegram delivery strips top-level HTML comments from preview/final text; column-zero top-level `<!-- telegram_voice ... -->` and `<!-- telegram_button ... -->` blocks are special outbound comments handled after `agent_end` without requiring agent-side transport tool calls, while comments inside code, quotes, lists, or indented examples stay literal
74
74
  - `telegram_voice` and `telegram_button` are not π tools; keep prompts/docs explicit that agents should author markup while voice synthesis provider extensions own TTS/OGG conversion, and pi-telegram owns button routing plus Telegram delivery
75
75
  - Voice reply policy and prompt context are owned by pi-telegram's `telegram.json` `voice.replyMode`: missing/invalid config behaves as `manual` but does not add a `[voice]` prompt-context block; only an explicit valid `voice.replyMode` renders context. Render a single voice field as `[voice] reply mode: manual|mirror|always`, and render multiple fields as a `[voice]` list; place voice context after `[outputs]` when handler output exists, otherwise after `[attachments]`; provider prompt contributions are optional provider-specific additions, not the default policy channel
76
- - Optional `telegram.json` `time` may add `[time] YYYY-MM-DD HH:mm:ss <timezone>` to Telegram-originated prompts for wall-clock context. It is off by default, uses `time.injectionMode` values `off|always|interval`, stores `time.interval` in milliseconds, uses the system timezone, and should render last after `[attachments]`, `[outputs]`, and `[voice]` sections. The Settings row `🕒 Time injection: off|always|interval` controls `time.injectionMode` only.
76
+ - Optional `telegram.json` `time` may add `[time] YYYY-MM-DD HH:mm:ss <timezone>` to Telegram-originated prompts for wall-clock context. It is hidden by default, uses `time.injectionMode` values `hidden|always|interval`, stores `time.interval` in milliseconds, uses the system timezone, and should render last after `[attachments]`, `[outputs]`, and `[voice]` sections. The Settings row `🕒 Time injection: hidden|always|interval` controls `time.injectionMode` only.
77
77
  - Voice reply mode Settings UI standard: the top-level Settings row is `👄 Voice reply: hidden|manual|mirror|always`; `hidden` is the true default and means no valid `voice.replyMode` is persisted, behavior is manual, and no voice policy is added to prompt context; explicit `manual` behaves the same operationally but renders reply-mode context. The submenu title is `👄 Voice reply mode:`; choice buttons use lowercase labels with a model-style active dot (`🟢 hidden`, `🟢 mirror`) rather than per-mode emoji; the explanatory submenu body uses compact HTML-code bullets such as `<code>-</code> <code>hidden</code> (default): ...`. Preserve this wording/icons unless the operator explicitly asks to redesign it
78
78
  - Outbound voice delivery is one fallback pipeline: configured `outboundHandlers` with `type: "voice"` run first in `telegram.json` order, then programmatic voice handlers, then registered voice synthesis providers as zero-config progressive fallbacks; provider extensions must not override operator-configured handlers
79
79
  - `telegram_voice` text is arbitrary TTS-target text: use body form for multiline text, `<!-- telegram_voice text="Short summary" -->` for explicit one-line text, or `<!-- telegram_voice: Short summary -->` for one-line text with no attributes
@@ -130,6 +130,7 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
130
130
  - When fork identity changes, keep `README.md`, package metadata, and docs aligned so the published package does not point back at stale upstream coordinates
131
131
  - README positioning should emphasize `/start` as the primary Telegram operator menu and keep reaction shortcuts secondary. Reactions are useful queue affordances, but menu controls are the core CLI-to-Telegram bridge.
132
132
  - Document configuration knobs without UI in the root README when they affect bootstrap, networking, transport limits, or prompt context; currently this includes token env bootstrap, Node env proxy mode, inbound/outbound size limits, and `time`.
133
+ - Keep extension-local standards self-contained: shared patterns such as command templates may evolve independently in multiple extensions, but pi-telegram docs/changelog should describe the standard without naming sibling extension implementations as dependencies or authorities.
133
134
  - Work only inside this repository during development tasks; updating the installed Pi extension checkout is a separate manual operator step, not part of normal in-repo implementation work
134
135
 
135
136
  ## 8. Integration Protocols
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.11.2: Queue Continuation, Compaction Safety, And Settings Polish
6
+
7
+ - `[Time Context]` Renamed the disabled time injection mode from `off` to `hidden` in Settings and config defaults, matching voice reply mode semantics where no prompt-context line is injected. Legacy `off` callbacks/config values are still treated as hidden.
8
+ - `[Settings UI]` Settings detail headings now show the current value immediately after the bold label in monospace style, making each submenu title double as the active setting summary.
9
+ - `[Entrypoint]` Removed root-level API re-exports from `index.ts`; public extension APIs remain available from their owning `lib/*` modules while the root file stays a default-only composition root.
10
+ - `[Continue Queue]` `/continue` now enqueues a standalone priority prompt without preserving already queued prompts as history. Impact: queued prompts stay iterative queue items instead of being folded into one combined `continue` prompt.
11
+ - `[Auto Compaction]` Observes native `session_before_compact` / `session_compact` events and blocks queued Telegram prompt dispatch while compaction is running, then resumes the queue after compaction settles. Impact: queued Telegram turns no longer race Pi auto-compaction and trigger `Cannot read properties of undefined (reading 'signal')` after compaction.
12
+ - `[Command Templates]` Updated the command-template helper library and regressions with typed placeholders, array-index placeholder resolution, repeat fanout from array length, unbounded default timeout semantics, and trusted-command warnings.
13
+ - `[Docs]` Updated the command-template standard with typed args, array placeholders, failure propagation, recover cleanup, and trust-boundary guidance.
14
+ - `[Docs]` Decomposed oversized architecture documentation blocks into focused sections for runtime ownership, queue validation, application menu shape, outbound actions, and interactive controls. Impact: the architecture entry point is easier to scan without changing runtime behavior.
15
+ - `[README]` Split queue reaction shortcuts into priority and removal lists, added `pi-xai-voice` as a companion extension, and kept the time-injection row label compact while preserving the full detail-heading label. Impact: operator-facing docs match the current Telegram menu language.
16
+ - `[Context]` Recorded the convention that extension-local standards should stay self-contained and avoid naming sibling extension implementations as authorities. Impact: shared standards can evolve independently without creating false extension dependencies.
17
+
5
18
  ## 0.11.1: Time Context And Settings Polish
6
19
 
7
20
  - `[Time Context]` Added optional `telegram.json` `time` prompt context for Telegram-originated turns. `time.injectionMode` values are `off`, `always`, and per-chat `interval`; `time.interval` is stored in milliseconds and timezone comes from the system. The `[time]` line renders last after attachments, handler outputs, and voice context, and Settings exposes a `🕒 Time` mode selector.
@@ -138,7 +151,7 @@
138
151
  ## 0.9.4: Temp Dir And Command Template Hotfix
139
152
 
140
153
  - `[Telegram Temp Dir]` Default Telegram API temp files now respect `PI_CODING_AGENT_DIR`, falling back to `~/.pi/agent` when the env var is unset. Impact: sandboxed or relocated agent dirs no longer force Telegram downloads through the default home-directory path.
141
- - `[Command Templates]` Synced the local Command Template Standard with `pi-auto-tools@0.5.5`: command-template nodes now document `mode`, `label`, `delay`, `repeat`, parallel fanout semantics, zero-based repeat placeholders, padding, and limited arithmetic expressions such as `{_(index+1)}`. Impact: inbound/outbound Telegram handler docs and helpers share the current portable automation contract.
154
+ - `[Command Templates]` Updated the local Command Template Standard: command-template nodes now document `mode`, `label`, `delay`, `repeat`, parallel fanout semantics, zero-based repeat placeholders, padding, and limited arithmetic expressions such as `{_(index+1)}`. Impact: inbound/outbound Telegram handler docs and helpers share the current portable automation contract without depending on another extension's documentation.
142
155
  - `[Queue Menu]` Empty queue refresh clicks now rotate through compact alternate empty-state headings while preserving the default first-open `⌛ Queue is empty.` state, and the Refresh button now stays directly under Back for both empty and populated queue lists. Impact: manual queue polling feels alive and the primary refresh control stays in a stable location without changing queue semantics.
143
156
  - `[Package]` Bumped package metadata to `0.9.4` and kept the lockfile in sync.
144
157
 
@@ -190,7 +203,7 @@
190
203
  - `[Configuration Docs]` Documented the configuration philosophy that rich visual/TUI setup stays minimal for now while agents can read README/docs and update `telegram.json` for advanced workflows. Impact: configuration guidance matches the extension's agent-assisted operator model without adding premature TUI surfaces.
191
204
  - `[Outbound Docs]` Tightened voice-handler critical-step wording around transform → TTS → conversion pipelines and handler-level fallbacks. Impact: docs now match translated voice pipelines without implying provider-specific TTS fallbacks.
192
205
  - `[Package]` Bumped package metadata to `0.8.1` and kept the lockfile in sync.
193
- - `[Command Template Docs]` Synchronized `docs/command-templates.md` bit-for-bit with the current portable standard shared by `pi-auto-tools`. Impact: the documented standard now includes retry, fail-open composition, critical-step abort semantics, and the 30s default timeout in the same wording across both extensions.
206
+ - `[Command Template Docs]` Updated `docs/command-templates.md` to the current portable standard. Impact: the documented standard now includes retry, fail-open composition, critical-step abort semantics, and the 30s default timeout without requiring cross-extension references.
194
207
  - `[Lock Docs]` Synchronized `docs/locks.md` bit-for-bit with the extension-neutral Locks Standard shared by `pi-wakeup`. Impact: singleton ownership documentation no longer carries project-specific examples that prevent exact reuse across extensions.
195
208
 
196
209
  ## 0.8.0: Handler Bus
package/README.md CHANGED
@@ -120,7 +120,10 @@ The inline application menu is the primary operator surface. It exposes status,
120
120
 
121
121
  Messages sent while π is busy enter the prompt queue and are processed in order. Control actions and model-switch continuation turns use higher-priority lanes so operational commands can resume before normal prompts.
122
122
 
123
- The menu is the primary way to inspect and mutate the queue. Reactions are an extra shortcut when Telegram delivers `message_reaction` updates for the chat: `👍`, `⚡️`, `❤️`, `🕊`, and `🔥` promote waiting work; `👎`, `👻`, `💔`, `💩`, and `🗑` remove it. The same rules apply to text, voice, files, images, and media groups.
123
+ The menu is the primary way to inspect and mutate the queue. Reactions are an extra shortcut when Telegram delivers `message_reaction` updates for the chat. The same rules apply to text, voice, files, images, and media groups:
124
+
125
+ - Priority shortcuts: `👍`, `⚡️`, `❤️`, `🕊`, and `🔥` promote waiting work.
126
+ - Removal shortcuts: `👎`, `👻`, `💔`, `💩`, and `🗑` remove waiting work from the queue.
124
127
 
125
128
  ### Streaming and Telegram HTML rendering
126
129
 
@@ -203,17 +206,21 @@ import { registerTelegramVoiceSynthesisProvider } from "@llblab/pi-telegram/lib/
203
206
  import { recordTelegramRuntimeEvent } from "@llblab/pi-telegram/lib/outbound-handlers.ts";
204
207
 
205
208
  // Return path only (backward compatible)
206
- const dispose = registerTelegramVoiceSynthesisProvider(async (text, { lang, rate }) => {
207
- const path = await myTTS(text, { language: lang });
208
- return path; // must be .ogg or .opus
209
- });
209
+ const dispose = registerTelegramVoiceSynthesisProvider(
210
+ async (text, { lang, rate }) => {
211
+ const path = await myTTS(text, { language: lang });
212
+ return path; // must be .ogg or .opus
213
+ },
214
+ );
210
215
 
211
216
  // Return path + transcript caption
212
- const dispose2 = registerTelegramVoiceSynthesisProvider(async (text, { lang, rate }) => {
213
- const rewritten = rewriteWithSpeechTags(text); // internal TTS optimization
214
- const path = await myTTS(rewritten, { language: lang });
215
- return { audioPath: path, transcriptText: text };
216
- });
217
+ const dispose2 = registerTelegramVoiceSynthesisProvider(
218
+ async (text, { lang, rate }) => {
219
+ const rewritten = rewriteWithSpeechTags(text); // internal TTS optimization
220
+ const path = await myTTS(rewritten, { language: lang });
221
+ return { audioPath: path, transcriptText: text };
222
+ },
223
+ );
217
224
 
218
225
  // Surface diagnostics in /telegram-status
219
226
  recordTelegramRuntimeEvent("xai-voice", new Error("TTS complete"), {
@@ -240,7 +247,7 @@ Import `registerTelegramSection()` from `@llblab/pi-telegram/lib/extension-secti
240
247
 
241
248
  ### Time context
242
249
 
243
- `telegram.json` can opt into a compact `[time]` line in Telegram-originated prompts so π has a wall-clock reference for requests such as "today", "now", or scheduling. It is off by default and uses the system timezone; the mode can also be changed from Settings → `🕒 Time`.
250
+ `telegram.json` can opt into a compact `[time]` line in Telegram-originated prompts so π has a wall-clock reference for requests such as "today", "now", or scheduling. It is hidden by default and uses the system timezone; the mode can also be changed from Settings → `🕒 Time injection`.
244
251
 
245
252
  ```json
246
253
  {
@@ -251,7 +258,7 @@ Import `registerTelegramSection()` from `@llblab/pi-telegram/lib/extension-secti
251
258
  }
252
259
  ```
253
260
 
254
- Modes are `off`, `always`, and `interval`. `interval` is measured in milliseconds and rate-limits the time line per chat in memory, so back-to-back messages do not repeatedly spend context on the same timestamp. When present, `[time]` is the final prompt-context section after attachments, handler outputs, and voice policy.
261
+ Modes are `hidden`, `always`, and `interval`. `hidden` means no time line is added to prompt context. `interval` is measured in milliseconds and rate-limits the time line per chat in memory, so back-to-back messages do not repeatedly spend context on the same timestamp. When present, `[time]` is the final prompt-context section after attachments, handler outputs, and voice policy.
255
262
 
256
263
  ## Docs
257
264
 
@@ -285,6 +292,12 @@ Third-party extensions that integrate with `pi-telegram`:
285
292
  pi install npm:pi-telegram-tool-status
286
293
  ```
287
294
 
295
+ - [`pi-xai-voice`](https://github.com/luxus/pi-xai-voice) — xAI voice companion for Telegram voice replies through the `pi-telegram` voice provider API.
296
+
297
+ ```bash
298
+ pi install npm:pi-xai-voice
299
+ ```
300
+
288
301
  ## License
289
302
 
290
303
  MIT
@@ -32,7 +32,7 @@ Current runtime areas use these ownership boundaries:
32
32
  - `queue`: queue item contracts, lane admission/order, stores, mutations, dispatch readiness/runtime, prompt/control enqueueing, and session/agent/tool lifecycle sequencing.
33
33
  - `runtime`: session-local coordination primitives: counters, lifecycle flags, setup guard, abort handler, typing-loop timers, prompt-dispatch flags, and agent-end reset binding.
34
34
  - `model` / `menu-model` / `menu-thinking` / `menu-status` / `menu` / `menu-queue` / `menu-settings` / `commands`: model identity/thinking levels, scoped model resolution, in-flight switching, model/thinking/status/queue/settings menu UI, inline application callback composition, slash commands, and bot command registration.
35
- - Future `extension-sections`: structured external Telegram menu sections registered by ordinary pi extensions; owns section registry, compact section callback tokens, section render/callback dispatch, safe section runtime ports, and diagnostics.
35
+ - `extension-sections`: structured external Telegram menu sections registered by ordinary pi extensions; owns section registry, compact section callback tokens, section render/callback dispatch, safe section runtime ports, and diagnostics.
36
36
  - `keyboard`: shared Telegram inline-keyboard reply-markup structure; feature domains own callback semantics and button construction.
37
37
  - `preview` / `replies` / `rendering`: preview lifecycle/transports, final reply delivery and reply parameters, Telegram HTML Markdown rendering, chunking, and stable-preview snapshots.
38
38
  - `outbound-handlers`: outbound text transformation, assistant-authored outbound comments, generated reply artifacts, inline-keyboard callbacks, and post-`agent_end` outbound action delivery.
@@ -44,7 +44,7 @@ Current runtime areas use these ownership boundaries:
44
44
  Boundary invariants:
45
45
 
46
46
  - Constants and state types live with their owning domains; do not reintroduce shared buckets such as `lib/constants.ts` or `lib/types.ts`
47
- - Shared Telegram inline-keyboard structure belongs to `keyboard`; application-control labels, callback data, and callback behavior stay in `menu`/`menu-model`/`menu-thinking`/`menu-status`/`menu-queue`; future external section labels, callbacks, and dispatch stay in `extension-sections`; core queue mechanics stay in `queue`
47
+ - Shared Telegram inline-keyboard structure belongs to `keyboard`; application-control labels, callback data, and callback behavior stay in `menu`/`menu-model`/`menu-thinking`/`menu-status`/`menu-queue`; external section labels, callbacks, and dispatch stay in `extension-sections`; core queue mechanics stay in `queue`
48
48
  - Domain helpers use narrow structural projections when that avoids importing concrete wire DTOs or broader runtime objects unnecessarily
49
49
  - Preview appearance stays in `rendering`; preview transport/lifecycle stays in `preview`
50
50
  - Direct `node:*` file-operation imports stay in owning domains, not in `index.ts`
@@ -64,7 +64,19 @@ Because `ctx.ui.input()` only exposes placeholder text, the bridge uses `ctx.ui.
64
64
 
65
65
  ## Runtime Ownership
66
66
 
67
- Telegram bot configuration stays in `~/.pi/agent/telegram.json`; singleton runtime ownership lives separately in `~/.pi/agent/locks.json` under `@llblab/pi-telegram`. `/telegram-connect` acquires or moves that lock before polling starts, and `/telegram-disconnect` stops polling and releases it. Session start may read the existing lock and resume polling when the lock already points at the current `pid`/`cwd`; after a full π process restart, it may also replace a stale lock from the same `cwd` and resume polling automatically. Session start does not create new ownership from an inactive lock, a live external lock, or a stale lock from another directory. Session replacement suspends polling and ownership watchers without releasing the lock, allowing the next session-start hook in the same `pid`/`cwd` to resume from the existing explicit ownership. When a live external owner exists, `/telegram-connect` asks whether to move singleton ownership to the current π instance. Active owners poll the lock while running through a snapshotted ownership context, so long-lived timers do not touch stale π contexts after `/new`; they stop local polling when `locks.json` no longer points at their own `pid`/`cwd`, without deleting the new owner lock. Deleting `locks.json` resets runtime ownership without deleting Telegram configuration.
67
+ Telegram bot configuration stays in `~/.pi/agent/telegram.json`. Singleton runtime ownership lives separately in `~/.pi/agent/locks.json` under `@llblab/pi-telegram`.
68
+
69
+ Ownership lifecycle:
70
+
71
+ - `/telegram-connect` acquires or moves the singleton lock before polling starts.
72
+ - `/telegram-disconnect` stops polling and releases the lock.
73
+ - Session start resumes polling when the existing lock already points at the current `pid`/`cwd`.
74
+ - After a full π process restart, session start may replace a stale lock from the same `cwd` and resume polling automatically.
75
+ - Session start does not create new ownership from an inactive lock, a live external lock, or a stale lock from another directory.
76
+ - Session replacement suspends polling and ownership watchers without releasing the lock, allowing the next session-start hook in the same `pid`/`cwd` to resume from explicit ownership.
77
+ - When a live external owner exists, `/telegram-connect` asks whether to move singleton ownership to the current π instance.
78
+
79
+ Active owners poll the lock through a snapshotted ownership context. Long-lived timers therefore avoid stale π contexts after `/new`; they stop local polling when `locks.json` no longer points at their own `pid`/`cwd`, without deleting the new owner lock. Deleting `locks.json` resets runtime ownership without deleting Telegram configuration.
68
80
 
69
81
  ## Message And Queue Flow
70
82
 
@@ -101,7 +113,19 @@ Admission contract:
101
113
  - Priority prompt queue: a waiting prompt promoted by `👍`, `⚡️`, `❤️`, `🕊`, or `🔥` uses `kind: prompt`, `queueLane: priority`, and dispatches at rank `1`.
102
114
  - Default prompt queue: normal Telegram text/media turns use `kind: prompt`, `queueLane: default`, and dispatch at rank `2`.
103
115
 
104
- The command action itself carries its execution mode, and the queue domain exposes lane contracts for admission mode, dispatch rank, and allowed item kinds. Queue append and planning paths validate lane admission so a malformed control/default or other invalid lane pairing fails predictably instead of silently changing priority. This lets synthetic control actions and Telegram prompts share one stable ordering model while still rendering distinctly in status output. In the π status bar, busy labels distinguish `active`, `dispatching`, `queued`, `tool running`, `model`, and `compacting`; priority prompts and priority control items are marked with `⚡`. If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, the status remains yellow `active` instead of degrading to green `connected`.
116
+ The command action itself carries its execution mode. The queue domain exposes lane contracts for admission mode, dispatch rank, and allowed item kinds.
117
+
118
+ Queue validation rules:
119
+
120
+ - Queue append and planning paths validate lane admission.
121
+ - Malformed control/default or other invalid lane pairings fail predictably instead of silently changing priority.
122
+ - Synthetic control actions and Telegram prompts share one stable ordering model while still rendering distinctly in status output.
123
+
124
+ Status rendering rules:
125
+
126
+ - Busy labels distinguish `active`, `dispatching`, `queued`, `tool running`, `model`, and `compacting`.
127
+ - Priority prompts and priority control items are marked with `⚡`.
128
+ - If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, the status remains yellow `active` instead of degrading to green `connected`.
105
129
 
106
130
  A dispatched prompt remains in the queue until `agent_start` consumes it. That keeps the active Telegram turn bound correctly for previews, attachments, abort handling, and final reply delivery.
107
131
 
@@ -113,9 +137,49 @@ Dispatch is gated by:
113
137
  - `ctx.isIdle()` being true
114
138
  - `ctx.hasPendingMessages()` being false
115
139
 
116
- This prevents queue races around rapid follow-ups, `/compact`, and mixed local plus Telegram activity. Post-agent-end dispatch retries are scheduled through a session-bound deferred dispatcher that activates on session start, cancels timers on session shutdown, and skips callbacks from older generations before they touch `ExtensionContext`. Telegram `/start` and hidden compatibility shortcuts `/status`, `/model`, `/thinking`, `/queue`, and `/settings` execute immediately; the dispatch controller still serializes any deferred control items so a queued control action must settle before the next queued action can dispatch.
140
+ These gates prevent queue races around rapid follow-ups, `/compact`, and mixed local plus Telegram activity.
141
+
142
+ Post-agent-end dispatch retries use a session-bound deferred dispatcher:
143
+
144
+ - It activates on session start.
145
+ - It cancels timers on session shutdown.
146
+ - It skips callbacks from older generations before they touch `ExtensionContext`.
147
+
148
+ Telegram `/start` and hidden compatibility shortcuts `/status`, `/model`, `/thinking`, `/queue`, and `/settings` execute immediately. The dispatch controller still serializes deferred control items so a queued control action must settle before the next queued action can dispatch.
117
149
 
118
- `/start` opens the main application menu: visible command help, compact command-only prompt-template rows when π exposes Telegram-compatible prompt-template names, status rows (`Status`, `Usage`, `Cost`, `Context`), and top-level buttons for model, thinking, and queue sections. The `Status` row reports `compacting` while a Telegram `/compact` run is active, and the bridge sends Telegram's native `typing` chat action as a keepalive for the same compaction window. The Queue button includes the current queued-item count. Hidden compatibility shortcuts `/help`, `/status`, `/model`, `/thinking`, and `/queue` jump directly to their corresponding menu screens, while `/settings` opens the hidden settings menu for bridge toggles such as proactive push, voice reply mode, and `time.injectionMode`. Settings options open detail submenus; checkbox-like settings use Back plus green/black/yellow `on` and `off` controls instead of mutating directly from the list. Command emoji come from the `commands` domain map so visible command descriptions and matching menu buttons share one fixed adornment source. Prompt-template commands use a fixed `🧩` marker, map π template names to Telegram-safe aliases such as `fix-tests` → `/fix_tests`, stay visible only inside the `/start` menu, and expand before queueing because `ExtensionAPI.sendUserMessage()` intentionally bypasses π prompt-template expansion for extension-originated messages. Every submenu starts with a top Back row so navigation stays anchored near the original user message above the inline keyboard; model-menu pagination controls sit near the top, tapping the pagination indicator opens a compact page picker headed by `<b>Choose a page:</b>`, and tapping a model opens a detail submenu with Back, ☑️ Activate/🟢 Active selection, and yellow/black-marked Scoped/All membership tabs. `menu-model` owns model-menu state, scoped model pages, model detail rendering, scoped-list persistence planning, and model-menu rendering while `model` owns core model identity/switching semantics. `menu-thinking` owns thinking-menu text, reply markup, callback handling, and message rendering. `menu-status` owns status-menu payloads, status callback handling, and status-message rendering. `menu-queue` owns queue-menu UI only: queue items are rendered under a compact `<b>Queue:</b>` heading, top-to-bottom in dispatch order, numbered, and marked with `⚡` for priority prompts or `📎` for prompts with attachments. An empty queue renders bold message text with the bottom-filled `⌛` hourglass plus the top Main menu button, while non-empty queue states keep the running `⏳` hourglass. Selecting an item opens a submenu that displays the queue item number above the full queued prompt text with Back, side-by-side Priority/Normal tabs, and Cancel. If a callback targets an item that has already left the queue, the menu refreshes the list instead of applying a stale mutation.
150
+ ### Application Menu Shape
151
+
152
+ `/start` opens the main application menu. It contains visible command help, compact command-only prompt-template rows when π exposes Telegram-compatible prompt-template names, status rows (`Status`, `Usage`, `Cost`, `Context`), and top-level buttons for model, thinking, and queue sections.
153
+
154
+ Menu rules:
155
+
156
+ - The `Status` row reports `compacting` while a Telegram `/compact` run is active, and the bridge sends Telegram's native `typing` chat action as a keepalive for the same compaction window.
157
+ - The Queue button includes the current queued-item count.
158
+ - Hidden compatibility shortcuts `/help`, `/status`, `/model`, `/thinking`, and `/queue` jump directly to their corresponding menu screens.
159
+ - `/settings` opens the hidden settings menu for bridge toggles such as proactive push, voice reply mode, and `time.injectionMode`.
160
+ - Settings options open detail submenus. Boolean settings use Back plus green/black/yellow `on` and `off` controls; list-like settings such as time injection use explicit mode names like `hidden`, `always`, and `interval`.
161
+ - Command emoji come from the `commands` domain map so visible command descriptions and matching menu buttons share one fixed adornment source.
162
+ - Prompt-template commands use a fixed `🧩` marker, map π template names to Telegram-safe aliases such as `fix-tests` → `/fix_tests`, stay visible only inside the `/start` menu, and expand before queueing because `ExtensionAPI.sendUserMessage()` bypasses π prompt-template expansion for extension-originated messages.
163
+
164
+ Navigation and ownership:
165
+
166
+ - Every submenu starts with a top Back row so navigation stays anchored near the original user message above the inline keyboard.
167
+ - Model-menu pagination controls sit near the top; tapping the pagination indicator opens a compact page picker headed by `<b>Choose a page:</b>`.
168
+ - Tapping a model opens a detail submenu with Back, ☑️ Activate/🟢 Active selection, and yellow/black-marked Scoped/All membership tabs.
169
+ - `model` owns core model identity/switching semantics.
170
+ - `menu-model` owns model-menu state, scoped model pages, model detail rendering, scoped-list persistence planning, and model-menu rendering.
171
+ - `menu-thinking` owns thinking-menu text, reply markup, callback handling, and message rendering.
172
+ - `menu-status` owns status-menu payloads, status callback handling, and status-message rendering.
173
+ - `menu-queue` owns queue-menu UI only.
174
+
175
+ Queue menu rendering:
176
+
177
+ - Queue items render under a compact `<b>Queue:</b>` heading, top-to-bottom in dispatch order.
178
+ - Items are numbered and marked with `⚡` for priority prompts or `📎` for prompts with attachments.
179
+ - An empty queue renders bold message text with the bottom-filled `⌛` hourglass plus the top Main menu button.
180
+ - Non-empty queue states keep the running `⏳` hourglass.
181
+ - Selecting an item opens a submenu with the queue item number, full queued prompt text, Back, side-by-side Priority/Normal tabs, and Cancel.
182
+ - If a callback targets an item that has already left the queue, the menu refreshes the list instead of applying a stale mutation.
119
183
 
120
184
  ### Abort Behavior
121
185
 
@@ -154,32 +218,74 @@ Preferred order:
154
218
 
155
219
  Draft streaming can remain as a plain-text fallback path, but rich Telegram previews are driven through editable messages and stable-block snapshot selection.
156
220
 
157
- Telegram prompt responses use explicit delivery context to attach outbound text, rich previews, errors, attachment notices, and uploads as Telegram replies to the source prompt when possible. Reply metadata is opt-in per delivery path, uses `reply_parameters` with `allow_sending_without_reply: true`, and is applied only to the first chunk of split long responses; continuation chunks are sent as normal adjacent messages. Media-group turns reply to the turn's representative `replyToMessageId`, not to every source message in the group. Long text split coalescing is intentionally conservative: only human text messages at or above the 3600-character near-limit threshold open the short debounce window, immediate same-chat/user contiguous text tails join that prompt, and commands, bot messages, captions, media groups, and normal short follow-ups bypass the coalescer.
221
+ ### Response Context
222
+
223
+ Telegram prompt responses use explicit delivery context to attach outbound text, rich previews, errors, attachment notices, and uploads as Telegram replies to the source prompt when possible.
224
+
225
+ Reply metadata rules:
226
+
227
+ - Reply metadata is opt-in per delivery path.
228
+ - It uses `reply_parameters` with `allow_sending_without_reply: true`.
229
+ - It is applied only to the first chunk of split long responses; continuation chunks are sent as normal adjacent messages.
230
+ - Media-group turns reply to the turn's representative `replyToMessageId`, not to every source message in the group.
158
231
 
159
- Outbound files are sent only after the active Telegram turn completes, must be staged through the `telegram_attach` tool, are staged atomically per tool call, are checked against a default 50 MiB limit configurable through `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES` or `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES`, and use file-backed multipart blobs so large sends do not require preloading whole files into memory.
232
+ Long text split coalescing is intentionally conservative. Only human text messages at or above the 3600-character near-limit threshold open the short debounce window. Immediate same-chat/user contiguous text tails join that prompt; commands, bot messages, captions, media groups, and normal short follow-ups bypass the coalescer.
160
233
 
161
- Assistant-authored outbound actions use final-message markup instead of agent tool calls. Preview updates strip closed top-level HTML comments and currently open/partial top-level comment starts before rendering, so users do not see transient metadata even when streaming flushes happen after only `<`, `<!`, or `<!--`. On `agent_end`, the bridge removes top-level comments from the Markdown text reply, but treats column-zero top-level `<!-- telegram_voice ... -->` and `<!-- telegram_button ... -->` blocks specially before delivery; comments inside fenced code, quotes, lists, or indented examples stay literal, including fenced blocks with Markdown-valid indented closing fences. Voice uses a single fallback pipeline: configured `outboundHandlers` with `type: "voice"`, then programmatic `voice` handlers, then registered synthesis providers from `lib/voice.ts`. The bridge extracts body text, `text="..."`, or colon shorthand, asks the pipeline for an `.ogg`/`.opus` artifact, validates native voice format, and uploads the generated file via Telegram `sendVoice`; when delivery fails, the queue runtime records diagnostics and falls back to the planned text reply when no text was already delivered. Synthesis providers own TTS, speech rewriting, transcript choice, and format conversion. Button blocks are built in: each `telegram_button` block becomes one inline-keyboard button on the final text, and callback clicks enqueue the configured prompt text as a normal Telegram prompt turn; the `telegram_button: Label` shorthand uses the same text for label and prompt, `prompt="..."` supports explicit one-line prompts, and body-form buttons use the body as the prompt. Unknown callback data that does not match pi-telegram-owned prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, future `section:`) is forwarded to π as `[callback] <data>` after built-in handlers decline it, giving layered extensions a simple namespaced button channel without separate polling; layered callback payloads should follow the [Callback Namespace Standard](./callback-namespaces.md). Future structured menu integrations should use the [Telegram Extension Sections Standard](./extension-sections.md) instead of hand-rolled fallback callbacks. When proactive push is enabled, successful local non-Telegram final replies are sent to the paired chat. Local prompt text is not sent because the bot does not own or mirror terminal user messages. This keeps terminal-originated results visible in Telegram without changing Telegram-originated turn delivery.
234
+ ### Outbound Files
162
235
 
163
- This keeps technical Markdown, code, tables, formulas, and numbered lists in the text channel when appropriate while allowing TTS-friendly voice messages and tappable continuations without invoking `telegram_attach` or extra transport tools. Telegram prompt guidance targets about 37 visible cells for tables, dense list items, and compact text blocks because emoji and other wide glyphs make raw character counts misleading on mobile screens.
236
+ Outbound files are sent only after the active Telegram turn completes. They must be staged through the `telegram_attach` tool, are staged atomically per tool call, and are checked against a default 50 MiB limit configurable through `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES` or `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES`. Delivery uses file-backed multipart blobs so large sends do not require preloading whole files into memory.
237
+
238
+ ### Assistant-Authored Actions
239
+
240
+ Assistant-authored outbound actions use final-message markup instead of agent tool calls. Preview updates strip closed top-level HTML comments and currently open/partial top-level comment starts before rendering, so users do not see transient metadata even when streaming flushes happen after only `<`, `<!`, or `<!--`.
241
+
242
+ On `agent_end`, the bridge removes top-level comments from the Markdown text reply, but treats these column-zero top-level blocks specially before delivery:
243
+
244
+ - `<!-- telegram_voice ... -->`
245
+ - `<!-- telegram_button ... -->`
246
+
247
+ Comments inside fenced code, quotes, lists, or indented examples stay literal, including fenced blocks with Markdown-valid indented closing fences.
248
+
249
+ Voice delivery uses one fallback pipeline:
250
+
251
+ 1. Configured `outboundHandlers` with `type: "voice"`
252
+ 2. Programmatic `voice` handlers
253
+ 3. Registered synthesis providers from `lib/voice.ts`
254
+
255
+ The bridge extracts body text, `text="..."`, or colon shorthand, asks the pipeline for an `.ogg`/`.opus` artifact, validates native voice format, and uploads the generated file via Telegram `sendVoice`. When delivery fails, the queue runtime records diagnostics and falls back to the planned text reply when no text was already delivered. Synthesis providers own TTS, speech rewriting, transcript choice, and format conversion.
256
+
257
+ Button blocks are built in. Each `telegram_button` block becomes one inline-keyboard button on the final text, and callback clicks enqueue the configured prompt text as a normal Telegram prompt turn. The `telegram_button: Label` shorthand uses the same text for label and prompt, `prompt="..."` supports explicit one-line prompts, and body-form buttons use the body as the prompt.
258
+
259
+ Unknown callback data that does not match pi-telegram-owned prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `section:`) is forwarded to π as `[callback] <data>` after built-in handlers decline it. Layered callback payloads should follow the [Callback Namespace Standard](./callback-namespaces.md). Structured menu integrations should use the [Telegram Extension Sections Standard](./extension-sections.md) instead of hand-rolled fallback callbacks.
260
+
261
+ ### Proactive Push And Mobile Guidance
262
+
263
+ When proactive push is enabled, successful local non-Telegram final replies are sent to the paired chat. Local prompt text is not sent because the bot does not own or mirror terminal user messages. This keeps terminal-originated results visible in Telegram without changing Telegram-originated turn delivery.
264
+
265
+ Technical Markdown, code, tables, formulas, and numbered lists stay in the text channel when appropriate while TTS-friendly voice messages and tappable continuations do not require `telegram_attach` or extra transport tools. Telegram prompt guidance targets about 37 visible cells for tables, dense list items, and compact text blocks because emoji and other wide glyphs make raw character counts misleading on mobile screens.
164
266
 
165
267
  ## Interactive Controls
166
268
 
167
- The bridge exposes Telegram-side session controls in addition to regular chat forwarding.
168
-
169
- Current operator controls include:
170
-
171
- - `/start` for the main application menu: command help, prompt-template commands, model, usage, cost, context visibility, and inline controls, executed immediately from Telegram even while generation is active
172
- - Inline application-menu buttons for model, thinking, and queue controls, applying idle selections immediately while still respecting busy-run restart rules; model-menu inputs are cached briefly and stored inline-menu states are pruned by TTL/LRU so old keyboards expire predictably
173
- - Hidden `/model` and `/thinking` shortcuts for opening the model and thinking sections directly while keeping settings out of the visible bot command menu
174
- - `/compact` for Telegram-triggered π session compaction when the bridge is idle
175
- - `/queue` for opening the queue section of the inline application menu; the same section is reachable from the status/main menu and supports top-anchored Back navigation, Priority/Normal tabs, and cancellation
176
- - `/next` for dispatching the next queued turn, aborting the active run first when π is busy
177
- - `/continue` for enqueueing a Telegram-owned `continue` prompt, without aborting the current turn or forcing the next queued item
178
- - `/abort` for aborting the active Telegram-owned run while preserving queued items for manual continuation
179
- - `/stop` for aborting the active Telegram-owned run and clearing waiting Telegram queue items
180
- - `/telegram-status` for π-side diagnostics as grouped line-by-line sections separated by blank lines: connection, polling, execution, queue, and the recent redacted runtime/API event ring. These sections include polling state, last update id, active turn source ids, pending dispatch, compaction state, active tool count, pending model-switch state, total queue depth, and queue-lane counts. The event ring records transport/API, polling/update, prompt-dispatch, control-action, typing, compaction, setup, session-lifecycle, and attachment queue/delivery failures; benign unchanged edit responses and unsupported empty draft-clear attempts are filtered out so expected preview transport noise does not obscure real failures
181
- - `/telegram-settings` for π-side bridge settings; it currently exposes proactive push as a local toggle backed by the same `telegram.json` flag as the hidden Telegram `/settings` menu
182
- - Queue reactions apply to waiting text, voice, file, image, and media-group turns by matching the turn's source Telegram message ids: `👍`, `⚡️`, `❤️`, `🕊`, and `🔥` promote waiting prompts, while `👎`, `👻`, `💔`, `💩`, and `🗑` remove waiting turns because ordinary Telegram DM message deletions are not exposed through the Bot API polling path this bridge uses
269
+ The bridge exposes Telegram-side session controls in addition to regular chat forwarding. Menu layout details live in [Application Menu Shape](#application-menu-shape); this section summarizes the command/control surface.
270
+
271
+ Telegram chat controls:
272
+
273
+ - `/start`: opens the main application menu and runs immediately even while generation is active.
274
+ - `/model`, `/thinking`, `/queue`: hidden shortcuts for opening the matching menu sections directly.
275
+ - `/compact`: triggers π session compaction when the bridge is idle.
276
+ - `/next`: dispatches the next queued turn, aborting the active run first when π is busy.
277
+ - `/continue`: enqueues a Telegram-owned priority `continue` prompt without aborting the current turn.
278
+ - `/abort`: aborts the active Telegram-owned run while preserving queued items for manual continuation.
279
+ - `/stop`: aborts the active Telegram-owned run and clears waiting Telegram queue items.
280
+
281
+ Pi-side diagnostics and settings:
282
+
283
+ - `/telegram-status`: renders grouped diagnostics for connection, polling, execution, queue, and the recent redacted runtime/API event ring.
284
+ - `/telegram-settings`: exposes π-side bridge settings; currently this includes proactive push backed by the same `telegram.json` flag as the hidden Telegram `/settings` menu.
285
+
286
+ Queue reactions are shortcut controls for waiting text, voice, file, image, and media-group turns. Matching uses the turn's source Telegram message ids. `👍`, `⚡️`, `❤️`, `🕊`, and `🔥` promote waiting prompts; `👎`, `👻`, `💔`, `💩`, and `🗑` remove waiting turns because ordinary Telegram DM message deletions are not exposed through the Bot API polling path this bridge uses.
287
+
288
+ The `/telegram-status` event ring records transport/API, polling/update, prompt-dispatch, control-action, typing, compaction, setup, session-lifecycle, and attachment queue/delivery failures. Benign unchanged edit responses and unsupported empty draft-clear attempts are filtered out so expected preview transport noise does not obscure real failures.
183
289
 
184
290
  ## In-Flight Model Switching
185
291
 
@@ -4,12 +4,14 @@ Command templates are the portable integration format for deterministic local au
4
4
 
5
5
  **Meta-contract:** transportable (bit-for-bit identical across projects), high-density (zero fluff), constant (evolve by crystallizing, not speculating), optimal minimum (add only when it hurts).
6
6
 
7
- **Scope:** portable synchronous command execution format — shell-free exec, composition/pipes, timeout (30s default), delay-before-start, retry, critical-step branching, output artifact selection, and handler-level fallback. Single JSON standard; no platform lock-in.
7
+ **Scope:** portable synchronous command execution format — shell-free exec, composition/pipes, optional timeout, delay-before-start, bounded retry, failure propagation, recover cleanup, output artifact selection, and handler-level fallback. Single JSON standard; no platform lock-in.
8
8
 
9
9
  ---
10
10
 
11
11
  Extensions may choose their own config files, selectors, placeholder sources, and examples, but should preserve this core contract.
12
12
 
13
+ Layer boundary: command templates own only the synchronous execution graph. Recipe imports, import-reference expressions, recipe lookup, `async: true`, run ids, state dirs, FIFO controls, and outbox events are host/recipe/async-run configuration layers, not portable command-template syntax.
14
+
13
15
  ## Shape
14
16
 
15
17
  A command template is either a command-line string or an ordered array of command-template leaves:
@@ -32,13 +34,15 @@ Common object fields:
32
34
 
33
35
  - `label`: Optional human label for diagnostics and parallel branch reports.
34
36
  - `mode`: Optional execution mode for array templates. Default is `"sequence"`; `"parallel"` runs children concurrently.
35
- - `args`: Optional placeholder-name declarations only. Never stores defaults.
37
+ - `args`: Optional placeholder declarations. Untyped names remain valid; compact typed forms such as `file:path`, `timeout:int`, `speed:number`, `dry_run:bool`, `prompts:array`, and `mode:enum(check,fix)` are valid when the host supports typed tool schemas. Defaults belong in `defaults` or inline placeholder defaults; hosts may normalize interactive shorthand such as `timeout:int=60000` before persistence.
36
38
  - `defaults`: Placeholder default values by name.
37
- - `timeout`: Optional execution timeout in milliseconds. Default is `30000`. Long-running agent calls should set this explicitly.
39
+ - `timeout`: Optional execution timeout in milliseconds. Omit it, or set `0`, to leave the command unbounded. Set an explicit positive timeout when a tool must fail closed instead of waiting indefinitely.
38
40
  - `delay`: Optional wait in milliseconds before starting this node. Default is no delay.
39
41
  - `output`: Optional result selector. Default is `"stdout"`; runtime values such as `"ogg"` are valid.
40
42
  - `retry`: Optional max attempts including the first. Default is `1`.
41
- - `critical`: Optional boolean. When `true`, failure aborts the root composition.
43
+ - `critical`: Optional boolean. Backward-compatible alias for `failure: "root"`.
44
+ - `failure`: Optional failure propagation scope: `continue`, `branch`, or `root`. Default is `continue`.
45
+ - `recover`: Optional command template run between failed retry attempts. Recovery output is ignored; recovery failure stops retries.
42
46
  - `template`: Required command string or ordered composition array.
43
47
 
44
48
  For object form, write `template` last. Read the node flags first, then the executable content. Storage paths, labels, selectors, descriptions, and registry-specific metadata belong to each extension's local schema.
@@ -63,8 +67,9 @@ Supported forms:
63
67
  | ---------------- | ------------------------------------------------ |
64
68
  | `{name}` | Required value from runtime values or `defaults` |
65
69
  | `{name=default}` | Inline default when no value is provided |
70
+ | `{items[index]}` | Array item selected by literal or repeat index |
66
71
 
67
- Resolution order is runtime values → `defaults` → inline default → error.
72
+ Resolution order is runtime values → `defaults` → inline default → error. Default values that are themselves a single placeholder, such as `{prompt}` resolving to `{prompts[index]}`, are resolved recursively with a small depth guard. A repeat node may set `repeat` to `{items.length}` when an array arg should determine fanout width.
68
73
 
69
74
  ```json
70
75
  {
@@ -80,6 +85,8 @@ With runtime values `{ "text": "hello" }`, argv is:
80
85
 
81
86
  Use `defaults` for visible configuration data; use inline defaults for compact local literals. Prefer flag-style examples such as `/path/to/tool --file {file} --lang {lang=ru}` for readability, but positional forms such as `/path/to/tool {file} {lang=ru}` are valid when the invoked script defines that CLI contract.
82
87
 
88
+ Typed declarations annotate the public tool interface, not the shell command. They may live in `args` or inline placeholders such as `{timeout:int=60000}` and `{mode:enum(check,fix)=check}`. Use metadata-first authoring (`args` plus `defaults`) when long templates should stay visually short; use inline-first authoring when one self-contained `template` property is clearer. They do not sandbox or reinterpret the executable; they only let the host generate narrower input schemas and normalize runtime values before placeholder substitution. Untyped `args` and untyped placeholders continue to work unchanged.
89
+
83
90
  ## Quoting
84
91
 
85
92
  Placeholder values are not shell-escaped because no shell is used. A value containing spaces remains one argv item when it replaces one split word:
@@ -122,12 +129,12 @@ Composition rules:
122
129
 
123
130
  - Execute leaves in order when `mode` is omitted or set to `"sequence"`
124
131
  - Execute child templates concurrently when `mode` is set to `"parallel"`
125
- - Parallel composition uses soft-quorum semantics by default: failed non-critical children are reported but do not abort siblings or the next sequence step
126
- - Non-critical failures are recorded and execution continues, while `critical: true` failures abort the root composition
132
+ - Parallel composition uses soft-quorum semantics by default: failed children are reported as degraded branches unless failure propagation escalates
133
+ - Non-critical failures are recorded and execution continues, while `failure: "branch"` stops the current branch and `failure: "root"` aborts the root composition
127
134
  - Treat the whole composition as one handler for selector matching and fallback
128
135
  - Top-level `args` and `defaults` apply to every leaf unless the leaf defines private values
129
136
  - Leaf `args` replace inherited `args`; leaf `defaults` merge over inherited defaults; `timeout` and `output` are not inherited into leaves
130
- - Default `30000` (30s) timeout applies automatically; configure `timeout` only for exceptional long-running commands
137
+ - Timeout is disabled by default; configure a positive `timeout` for bounded commands that should fail closed
131
138
  - Each sequence leaf receives the previous leaf's stdout on stdin by default, while the final leaf stdout remains the default composition result
132
139
  - Each parallel child receives the same stdin, and child stdout values are joined in stable array order before flowing to the next sequence leaf
133
140
  - Parallel branch joins include branch label and status, and tool details include branch metadata plus coverage summary
@@ -231,38 +238,76 @@ Legacy local schemas may accept `pipe` as an alias, but the portable standard is
231
238
 
232
239
  By default, composition continues on failure: the failed step is logged and the next step executes. This is analogous to `make -k` — the user sees all failures at once and decides what to fix.
233
240
 
234
- ## Critical Steps
241
+ ## Failure Propagation
242
+
243
+ By default, failed steps use `failure: "continue"`: record the failure, clear stdout for that step, and continue the current sequence. This preserves the fail-open profile.
244
+
245
+ Use `failure` when a node should stop more aggressively:
235
246
 
236
- Set `critical: true` on any leaf to abort the entire root composition on failure. One `critical` leaf can halt the whole pipeline.
247
+ - `"continue"`: record the failure and continue the current sequence.
248
+ - `"branch"`: stop the current sequence/subtree and return a failed branch to the nearest parent. In a parallel node, sibling branches keep running and the join becomes degraded. At the root, branch failure is still a tool failure.
249
+ - `"root"`: abort the outermost composition.
237
250
 
238
251
  ```json
239
252
  {
253
+ "mode": "parallel",
240
254
  "template": [
241
- { "template": "cargo build" },
242
- { "template": "cargo fmt --check" },
243
- { "critical": true, "template": "cargo test" }
255
+ {
256
+ "label": "agent-a",
257
+ "failure": "branch",
258
+ "template": [
259
+ "agent-a-work {scope}",
260
+ "agent-a-validate {scope}",
261
+ "agent-a-push {scope}"
262
+ ]
263
+ },
264
+ {
265
+ "label": "agent-b",
266
+ "failure": "branch",
267
+ "template": [
268
+ "agent-b-work {scope}",
269
+ "agent-b-validate {scope}",
270
+ "agent-b-push {scope}"
271
+ ]
272
+ }
244
273
  ]
245
274
  }
246
275
  ```
247
276
 
248
- `build` / `fmt` failures are logged, execution continues. `test` failure aborts the root composition immediately.
277
+ If `agent-a-validate` fails, `agent-a-push` is skipped, `agent-b` can still finish, and the parallel join reports degraded branch coverage.
249
278
 
250
- A `critical` leaf in a nested composition still aborts the outermost root `template: [...]`. There is no per-branch scoping in the current standard.
279
+ `critical: true` remains a backward-compatible alias for `failure: "root"`. Prefer `failure` for new templates because it names the propagation scope directly.
251
280
 
252
281
  ## Retry
253
282
 
254
- Set `retry: N` on a leaf to attempt execution up to `N` times (including the first). Retries happen immediately on non-zero exit. The first successful attempt stops the retry loop.
283
+ Set `retry: N` to attempt execution up to `N` times including the first. The first successful attempt stops the retry loop.
284
+
285
+ On leaf commands, retry repeats that command. On sequence or parallel nodes, retry repeats the whole node. A retried group only retries when the group returns a failure, so validator checkpoints normally pair group retry with `failure: "branch"` or `failure: "root"`.
255
286
 
256
287
  ```json
257
288
  {
258
- "template": [
259
- { "retry": 3, "template": "npm install" },
260
- { "retry": 2, "critical": true, "template": "npm test" }
261
- ]
289
+ "failure": "branch",
290
+ "retry": 3,
291
+ "template": ["implement {scope}", "npm test", "git diff --check"]
292
+ }
293
+ ```
294
+
295
+ Here the whole group runs again when a validator fails. Without `failure: "branch"`, the failed validator would be logged and the group would continue by default.
296
+
297
+ ## Recover
298
+
299
+ Set `recover` on a retried node to run cleanup after a failed attempt and before the next attempt. `recover` is another command template: it can be a string command, sequence, or mode tree. Its output is ignored and the next retry receives the original stdin.
300
+
301
+ ```json
302
+ {
303
+ "failure": "branch",
304
+ "retry": 3,
305
+ "recover": "git -C {work_dir} reset --hard HEAD",
306
+ "template": ["pi -p --tools read,edit,bash {scope_file}", "npm test"]
262
307
  }
263
308
  ```
264
309
 
265
- `npm install` is retried up to 3 times. `npm test` is retried up to 2 times; if all attempts fail, the critical step aborts the pipeline.
310
+ `recover` is not a fallback success path. It is cleanup between attempts. Practical uses include resetting a worktree, removing temp files, clearing generated output, releasing a local lock, or stopping a helper process before trying the node again. If recovery fails, retries stop and the recovery failure is returned. Recovery uses fail-closed semantics by default; set an explicit `failure` inside a recover template only when a softer cleanup failure is intentional.
266
311
 
267
312
  ## Delay
268
313
 
@@ -290,12 +335,24 @@ string → leaf command
290
335
  string[] → sequential composition
291
336
  { template } → leaf command object
292
337
  { mode, template } → sequence or parallel subtree
293
- { mode, args, defaults, delay, retry, critical, output, template } → full node
338
+ { mode, args, defaults, delay, retry, failure, recover, output, template } → full node
294
339
  ```
295
340
 
296
- Start with a string. Add composition when needed. Add `mode: "parallel"` when independent work can run concurrently. Add delay when launch pacing matters. Add retry when flaky. Add critical when safety matters. Same contract, growing capability, no dead weight.
341
+ Start with a string. Add composition when needed. Add `mode: "parallel"` when independent work can run concurrently. Add delay when launch pacing matters. Add retry when flaky. Add `failure` when propagation scope matters. Add `recover` when a retried node needs cleanup before another attempt. Same contract, growing capability, no dead weight.
342
+
343
+ `mode: "parallel"` is the synchronous fanout shape. Saved JSON recipes and detached lifecycle concerns such as logs, cancellation, and durable state belong to host-specific recipe/async-run standards, not to command templates.
344
+
345
+ ## Trust Boundary
346
+
347
+ Command templates avoid shell interpolation by splitting the template into argv first and substituting placeholders per arg. A placeholder value containing spaces remains one argv value, not a shell fragment.
348
+
349
+ This is not a sandbox. The executable still runs with the same user permissions as the host agent. Shells, interpreter eval modes, destructive filesystem commands, and local scripts remain trusted code. Examples that deserve extra operator attention:
350
+
351
+ - `bash`, `sh`, `zsh`, or `fish`, especially with `-c`.
352
+ - `node -e`, `python -c`, `ruby -e`, `perl -e`, or similar eval modes.
353
+ - `rm`, `mv`, `cp`, or `rsync` over broad paths or placeholder-derived paths.
297
354
 
298
- `mode: "parallel"` is the synchronous fanout shape. Detached lifecycle, logs, cancellation, and durable state belong to host-specific async job or runtime-envelope standards, not to command templates.
355
+ Hosts may surface lightweight warnings for these obvious high-risk shapes. Warnings should inform review without blocking existing tools, because many trusted local wrappers intentionally use shells or filesystem mutation.
299
356
 
300
357
  ## Tool Boundary
301
358