pidge-cli 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.0 — 2026-06-28
4
+
5
+ The married vocabulary (perfis) — the CLI now speaks the SAME language as the server
6
+ (manifest v42) and the app: ONE list of 5 message types, with RESPONSE as a separate
7
+ axis. No scripts break — the old names keep working as aliases.
8
+
9
+ - **feat:** the typed sends are renamed to the canonical 5 — `pidge message` ·
10
+ `important` · `urgent` · `event` · `live` (message←fyi, important←report, urgent←alert;
11
+ event/live unchanged). `important` is the recommended default. The wire sends the new
12
+ `template_kind`. (perfis-S1)
13
+ - **feat (compat):** the OLD names still work as aliases — `pidge fyi`→message,
14
+ `report`→important, `alert`→urgent — mapped to the new type, with a one-line rename
15
+ note on stderr. Muscle-memory and existing scripts are untouched. (perfis-S1)
16
+ - **feat:** RESPONSE is now its own axis, composing on ANY type — `--actions`/
17
+ `--custom-action` (buttons) + the new **`--wait`** (block until the human answers,
18
+ then print `chosen_action` JSON; without it = fire-and-forget). This is the explicit
19
+ "send-and-go vs wait". (perfis-S2)
20
+ - **feat:** `pidge ask` is now the shortcut for `important --wait` (still REQUIRES a way
21
+ to answer; preserved behavior). There is no `ask` TYPE in the married catalog — asking
22
+ is a type + buttons + wait. (perfis-S2)
23
+ - **feat:** `pidge approval` — a new go/no-go RECIPE = `important` + Approve/Reject +
24
+ Face ID on Approve + `--wait`. Sent as `custom_actions` (only custom actions carry
25
+ `biometric`, and a custom id can't reuse a built-in like approve/reject — so the ids
26
+ are `grant`/`deny`). Pass your own `--actions`/`--custom-action` to override the
27
+ pair. (perfis-S2)
28
+ - **docs:** USAGE, per-command help and the generated `SKILL.md` rewritten around the two
29
+ axes (type + response) — mirrors the human's app, drops the dead fyi/report framing.
30
+ - **chore:** `KNOWN_MANIFEST_VERSION` 36 → 42 (the live server), silencing the news nag.
31
+
3
32
  ## 0.13.1 — 2026-06-26
4
33
 
5
34
  Polish from an agent E2E (2026-06-26). No breaking changes.
package/README.md CHANGED
@@ -11,6 +11,15 @@ then gets the answer as JSON — no webhook, no polling loop to write.
11
11
  > current spec (fields, profiles, guarantees). This CLI is a thin pipe over it — any
12
12
  > new server field works without a CLI update via `--param key=value`.
13
13
 
14
+ > **New in v0.14.0** — the **married vocabulary** (perfis): the CLI now speaks the same
15
+ > language as the server (manifest v42) and the app. **One list of 5 types** —
16
+ > `pidge message · important · urgent · event · live` (message←fyi, important←report,
17
+ > urgent←alert; old names still work as aliases). **RESPONSE is a separate axis**:
18
+ > `--actions`/`--custom-action` add buttons on ANY type, and **`--wait`** blocks until
19
+ > the human answers — the explicit *send-and-go vs wait*. `pidge ask` is now the shortcut
20
+ > for `important --wait`; new **`pidge approval`** = important + Approve/Reject + Face
21
+ > ID + wait. `important` is the recommended default.
22
+ >
14
23
  > **New in v0.12.0** — CLI bugs batch (all reported by an agent in real use): **`pidge
15
24
  > <sub> --help`** now shows that subcommand's own help (its flags), not the global dump
16
25
  > (#240); the **manifest-version nag is throttled to once / 24 h** (cached in
@@ -113,39 +122,73 @@ export PIDGE_TOKEN=hld_xxx # your channel's bearer key
113
122
  # (or skip the exports: the CLI reads ~/.config/pidge/env — KEY=VALUE — so the
114
123
  # key never has to appear in an agent's chat; explicit env vars win)
115
124
 
116
- # Send AND wait for the answer (the one an agent wants):
117
- npx pidge-cli ask \
118
- --title "Aprovar deploy?" --actions yes,no,reply --timeout 600
125
+ # Just inform fire-and-forget (clears when the human opens it):
126
+ npx pidge-cli message --title "Build green" --body "2m12s"
127
+
128
+ # A pendency they should resolve — the DEFAULT type ("waiting-for-you" card):
129
+ npx pidge-cli important --title "Review PR #42" --url https://github.com/…/pull/42
119
130
 
120
- # Urgent escalates to an AlarmKit alarm if the human doesn't answer in minutes:
131
+ # Send AND wait for the answer (the one an agent wants) = important + --wait:
121
132
  npx pidge-cli ask \
122
- --title "Posso rodar a migration?" --profile escalating --actions yes,no
133
+ --title "Approve deploy?" --actions yes,no,reply --timeout 600
134
+
135
+ # A go/no-go with Face ID — the approval RECIPE (= important + Approve/Reject + wait):
136
+ npx pidge-cli approval --title "Deploy to production?"
137
+
138
+ # Urgent — breaks through silent/Focus; --escalate forces an AlarmKit alarm:
139
+ npx pidge-cli urgent --title "Balance dropped below $5k" --escalate
123
140
 
124
141
  # A thing with a known time — push at T−lead + a lock-screen countdown to the event:
125
- npx pidge-cli notify \
126
- --title "Reunião com o time" --profile event --event-at "2026-06-10T15:00:00"
142
+ npx pidge-cli event \
143
+ --title "Team meeting" --event-at "2026-06-10T15:00:00"
127
144
 
128
145
  # A chart you generated — uploaded for you, shown on the banner + feed:
129
- npx pidge-cli notify --title "Gráfico pronto" --image ./chart.png
146
+ npx pidge-cli message --title "Chart ready" --image ./chart.png
130
147
 
131
148
  # A real artifact — the human previews it on the phone, shares it, saves to Files:
132
- npx pidge-cli notify --title "Relatório" --file ./relatorio.xlsx
149
+ npx pidge-cli important --title "Report" --file ./report.xlsx
133
150
  ```
134
151
 
135
- `ask` prints the chosen action as JSON to **stdout** and exits `0`:
152
+ `ask`/`approval` (and any `--wait` send) print the chosen action as JSON to
153
+ **stdout** and exit `0`:
136
154
 
137
155
  ```json
138
156
  { "kind": "acted", "action_id": "yes", "label": "Sim", "text": null,
139
157
  "at": "2026-06-08T18:19:51Z", "snooze_until": null }
140
158
  ```
141
159
 
160
+ ## Two axes: the TYPE + the RESPONSE
161
+
162
+ You pick **one type** (how much it may intrude — the human already configured how each
163
+ arrives), then ORTHOGONALLY decide the **response** (buttons? wait or not?).
164
+
165
+ **Axis 1 — type** (the married catalog of 5):
166
+
167
+ | Type | For | Clears when |
168
+ |---|---|---|
169
+ | `pidge message` | just inform, no action | the human OPENS it |
170
+ | `pidge important` ⭐ | a pendency they should resolve (the DEFAULT) | **Feito** |
171
+ | `pidge urgent` | wake them now (rare, real); `--escalate` = AlarmKit alarm | Feito (cuts the alarm) |
172
+ | `pidge event --event-at <ISO>` | a thing with a known time (countdown LA) | passed / Feito |
173
+ | `pidge live` | track something live (Live Activity) | you end it |
174
+
175
+ **Axis 2 — response** (composes on ANY type): `--actions yes,no` / `--custom-action`
176
+ add buttons (free text is always available); `--wait` blocks until the human answers
177
+ (else fire-and-forget — the answer arrives later in `pidge listen --all`). Two shortcuts
178
+ bundle both: **`pidge ask`** = `important --wait` (needs `--actions`); **`pidge approval`**
179
+ = `important` + Approve/Reject + Face ID + `--wait`.
180
+
181
+ > Old names still work as **aliases**: `fyi`→message, `report`→important, `alert`→urgent.
182
+
142
183
  ## Commands
143
184
 
144
185
  | Command | What it does |
145
186
  |---|---|
187
+ | `message` / `important` / `urgent` / `event` / `live` | The 5 message types (axis 1). Fire-and-forget by default; add `--actions`/`--wait` (axis 2) to ask for a reply. `important` is the recommended default. |
188
+ | `ask` | `important --wait` shortcut: send **and block** until the human answers; prints the chosen action JSON. Requires a way to answer (`--actions`/`--custom-action`/`--template`). |
189
+ | `approval` | Go/no-go RECIPE: `important` + Approve (Face ID) / Reject + `--wait`. Pass your own `--actions` to override the pair. |
146
190
  | `hello` | **v0.11.0 (#217):** your channel's **first-contact WOW** — send the onboarding handshake **and block** until the human confirms. The server narrates a 3-stage Live Activity on the lock screen (Conectando → toque para confirmar → Concluído ✓) so they *see* the agent→human→agent loop close. Run it as your **first** contact on a fresh channel. A thin `ask --template onboarding` wrapper with friendly default copy. |
147
- | `ask` | Send a notification **and block** until the human answers; prints the chosen action JSON. The default for agents. |
148
- | `notify` | Send only. Prints the raw 201 JSON; the `correlation_id` + warnings go to stderr. |
191
+ | `notify` | **Deprecated** — send without a type (the server picks the channel default). Prefer a typed send. Prints the raw 201 JSON; the `correlation_id` + warnings go to stderr. |
149
192
  | `wait <correlation_id>` | Block on an already-sent notification until it's answered. |
150
193
  | `cancel <correlation_id>` | Cancel a **still-scheduled** notification before it fires (idempotent; 409 once it reached the phone). |
151
194
  | `inbox` | What you sent: list, `--pending` slice, or `--summary` (counts + answer latency). |
@@ -188,11 +231,11 @@ WebSocket → ?wait= long-poll (capped 25 s server-side) → plain GETs ever
188
231
  --body TEXT the message shown on the banner
189
232
  --body-markdown MD rich body for the tap-through detail screen
190
233
  --subtitle TEXT
191
- --profile ID delivery profile — the HUMAN owns what each one does:
192
- default · event (needs --event-at; countdown Live Activity) ·
193
- escalating (alarm if unanswered minutes after delivery) ·
194
- the user's custom profiles. See the manifest's `profiles`.
195
- --event-at ISO8601 WHEN the thing happens (a FACT; required by profile event)
234
+ --profile ID low-level alias of the TYPE — the HUMAN owns what each does:
235
+ message · important · urgent · event (needs --event-at) ·
236
+ live · the user's custom profiles. Prefer the typed
237
+ subcommands above; an explicit --profile still wins.
238
+ --event-at ISO8601 WHEN the thing happens (a FACT; required by event)
196
239
  --lead-minutes N notify/start the countdown N min before event_at (5–240)
197
240
  --urgency LEVEL normal | persistent | alarm (low-level — prefer --profile)
198
241
  --image PATH_OR_URL image on the banner + feed: a local path is uploaded for you
@@ -207,13 +250,16 @@ WebSocket → ?wait= long-poll (capped 25 s server-side) → plain GETs ever
207
250
  '[{"id":"approve","label":"Aprovar agora"},{"id":"defer","label":"Depois"}]'
208
251
  --custom-action SPEC "id:label[:destructive][:confirm][:biometric][:terminal]"
209
252
  (repeatable — your own buttons; composes with --actions JSON)
253
+ --wait RESPONSE axis: block until the human answers (ANY type), then
254
+ print chosen_action JSON. Without it: fire-and-forget (the
255
+ answer arrives later in `pidge listen --all`). ask/approval imply it.
210
256
  --deliver-at ISO8601 schedule for later
211
257
  --reply-to URL also POST the answer to your webhook (HMAC-signed)
212
258
  --correlation-id ID idempotency + routing key (auto-generated if omitted)
213
259
  --collapse-key KEY replace/update a prior notification
214
260
  --param KEY=VALUE pass ANY raw /notify field (repeatable) — future server
215
261
  fields work day-one, no CLI update needed
216
- --timeout SECONDS ask: default 600 · wait: default 300
262
+ --timeout SECONDS how long --wait blocks (ask/approval: template suggestion ~3600 · wait: 300)
217
263
  --interval SECONDS FALLBACK poll cadence (default 30) — normally unused: WS or
218
264
  the server-held long-poll (?wait=25) make answers ~instant
219
265
  --realtime force the WebSocket (Node ≥22); --no-realtime = polling only
@@ -224,9 +270,10 @@ WebSocket → ?wait= long-poll (capped 25 s server-side) → plain GETs ever
224
270
  - **`ask` prints `correlation_id=<cid>` as its FIRST stderr line** (minted client-side
225
271
  when you don't pass one) — a killed `ask` always leaves the handle behind, so you
226
272
  can `pidge wait <cid>` instead of re-sending.
227
- - **stdout is always machine-readable.** `notify` → the raw 201 JSON; `ask`/`wait` →
228
- the `chosen_action` JSON. Everything human (warnings, the correlation_id, snooze
229
- notices, armed-escalation and policy-degrade narration) goes to **stderr**.
273
+ - **stdout is always machine-readable.** A fire-and-forget send → the raw 201 JSON; a
274
+ `--wait` send / `ask` / `approval` / `wait` the `chosen_action` JSON. Everything
275
+ human (warnings, the correlation_id, snooze notices, armed-escalation and
276
+ policy-degrade narration) goes to **stderr**.
230
277
  - **Exit codes:** `0` answered · `3` timed out (= *no answer yet*, NOT a failure —
231
278
  back off and retry later) · `4` timed out **without one healthy round-trip all
232
279
  session** (the CHANNEL looks broken — server/network — tell your human) ·
@@ -234,11 +281,11 @@ WebSocket → ?wait= long-poll (capped 25 s server-side) → plain GETs ever
234
281
  - **Responses are one-and-done.** Every answer closes the notification EXCEPT a
235
282
  **snooze** (or a reschedule that set a new time), which re-fires later. `ask`/`wait`
236
283
  keep polling through a snooze and print `snooze_until` so you can schedule a re-check.
237
- - **Profiles degrade, never reject.** An over-ceiling profile is delivered at the
238
- channel's allowed level — read `degraded`/`degrade_reason` in the 201 (narrated on
239
- stderr). That's the human's policy working; don't retry harder.
240
- - **`ask --profile tracking` is refused** — tracking is Live-Activity-only and never
241
- produces an answer.
284
+ - **Types degrade, never reject.** An over-ceiling type is delivered at the channel's
285
+ allowed level — read `degraded`/`degrade_reason` in the 201 (narrated on stderr).
286
+ That's the human's policy working; don't retry harder.
287
+ - **`--wait` / `ask` on `live` is refused** — `live` is status-only and never produces
288
+ an answer.
242
289
  - A genuine follow-up question is a **new** notification, never a second answer on
243
290
  the same one.
244
291
 
package/bin/pidge.js CHANGED
@@ -11,14 +11,25 @@
11
11
  // ~/.config/pidge/env — KEY=VALUE — is read instead, so the key can live
12
12
  // OUTSIDE the agent's chat/context entirely, #57)
13
13
  //
14
- // # send AND block until the human answers, then print the chosen action as JSON
15
- // pidge ask --title "Aprovar deploy?" --actions yes,no,reply --timeout 600
14
+ // TWO AXES (perfis, manifest v40+): (1) the TYPE one married list of 5 the
15
+ // human configured how to receive: message · important · urgent · event · live;
16
+ // (2) the RESPONSE — buttons (--actions/--custom-action) + send-and-go vs wait
17
+ // (--wait blocks until the human answers). Response composes onto ANY type.
16
18
  //
17
- // # urgent: escalates to an AlarmKit alarm if unanswered (see the manifest's profiles)
18
- // pidge ask --title "Posso rodar a migration?" --profile escalating --actions yes,no
19
+ // # just inform fire-and-forget (prints the raw 201)
20
+ // pidge message --title "Build green" --body "2m12s"
19
21
  //
20
- // # a thing with a known time: push at T−lead + a lock-screen countdown to the event
21
- // pidge notify --title "Reunião com o time" --profile event --event-at "2026-06-10T15:00:00"
22
+ // # a pendency the human should resolve (the DEFAULT type) + block on the answer
23
+ // pidge important --title "Approve deploy?" --actions yes,no,reply --wait
24
+ //
25
+ // # a go/no-go decision with Face ID — the approval RECIPE (= important + wait + gate)
26
+ // pidge approval --title "Deploy to production?"
27
+ //
28
+ // # urgent: breaks through silent/Focus, escalates to an AlarmKit alarm
29
+ // pidge urgent --title "Balance dropped below $5k" --escalate
30
+ //
31
+ // # a thing with a known time: push at T−lead + a lock-screen countdown
32
+ // pidge event --title "Team meeting" --event-at "2026-06-10T15:00:00"
22
33
  //
23
34
  // # block on an already-sent notification (by correlation_id)
24
35
  // pidge wait order-7 --timeout 300
@@ -119,6 +130,9 @@ const OPTIONS = {
119
130
  param: { type: 'string', multiple: true }, // key=value escape hatch → raw /notify field
120
131
  timeout: { type: 'string' },
121
132
  interval: { type: 'string' },
133
+ // perfis-S2 response axis: --wait blocks until the human answers (composes on
134
+ // ANY type — send-and-go vs wait). ask/approval imply it.
135
+ wait: { type: 'boolean' },
122
136
  // inbox flags (#83)
123
137
  pending: { type: 'boolean' },
124
138
  summary: { type: 'boolean' },
@@ -163,14 +177,20 @@ USAGE
163
177
  narrated LIVE on the lock screen by a 3-stage Live Activity
164
178
  (Conectando → toque para confirmar → Concluído ✓). send + wait
165
179
  in one — run it as your FIRST contact on a fresh channel.
166
- TYPED SENDS (#246 pick the one that matches your INTENT):
167
- pidge fyi [options] passive info, no action — log/registro (template_kind fyi)
168
- pidge report [options] a curated result the human will want to read now (report)
169
- pidge ask [options] a DECISION — send AND wait; needs --actions (prints chosen_action JSON)
170
- pidge event [options] a scheduled thing with a time — needs --event-at (event)
171
- pidge alert [options] an anomaly/error; --escalate forces an AlarmKit alarm (alert)
172
- pidge live [options] an in-flight task with incremental updates (live)
173
- pidge notify [options] DEPRECATED (0.13.x) send without a type (server falls back to fyi)
180
+ AXIS 1 — TYPE (the married list of 5; the human configured how each arrives):
181
+ pidge message [options] just inform, no action — clears when the human OPENS it
182
+ pidge important [options] ⭐DEFAULT a pendency the human should resolve ("waiting-for-you" card)
183
+ pidge urgent [options] breaks through silent/Focus; --escalate forces an AlarmKit alarm
184
+ pidge event [options] a scheduled thing — needs --event-at (countdown Live Activity)
185
+ pidge live [options] an in-flight task with incremental updates (Live Activity)
186
+ AXIS 2 RESPONSE (composes on ANY type above): --actions/--custom-action add
187
+ buttons; text reply is ALWAYS available; --wait blocks until the human answers
188
+ (send-and-go vs --wait). Two shortcuts bundle both axes:
189
+ pidge ask [options] = important + --wait; needs --actions (prints chosen_action JSON)
190
+ pidge approval [options] = important + Approve/Reject + Face ID + --wait (a go/no-go)
191
+ COMPAT aliases (old names still work → mapped to the new type):
192
+ pidge fyi→message · report→important · alert→urgent (event/live unchanged)
193
+ pidge notify [options] DEPRECATED — send without a type; prefer a TYPE above
174
194
  pidge wait <correlation_id> [options] block on an already-sent notification
175
195
  pidge cancel <correlation_id> cancel a still-scheduled notification (#56)
176
196
  pidge inbox [--pending|--summary|--all|--limit N] what you sent: list, pending slice, or counts+latency (#83)
@@ -220,10 +240,10 @@ OPTIONS (notify / ask)
220
240
  --template ID content/action pattern — WHAT you're asking: context (FYI,
221
241
  no buttons) · decision (yes/no/reply) · approval · reminder ·
222
242
  nudge · sensitive (gated, Face ID). Composes with --profile.
223
- --profile ID delivery profile (the HUMAN owns what it does): default ·
224
- event (needs --event-at; countdown Live Activity) ·
225
- escalating (alarm if unanswered minutes after delivery) ·
226
- the user's custom profiles. See the manifest's \`profiles\`.
243
+ --profile ID low-level alias of the TYPE axis (the HUMAN owns what it
244
+ does): message · important · urgent · event · live ·
245
+ the user's custom profiles. Prefer the typed subcommands
246
+ above; an explicit --profile still wins. See the manifest.
227
247
  --event-at ISO8601 WHEN the thing happens (a FACT; required by profile event)
228
248
  --lead-minutes N notify/start countdown N min before event_at (5–240)
229
249
  --urgency LEVEL normal | persistent | alarm (low-level — prefer --profile)
@@ -233,9 +253,13 @@ OPTIONS (notify / ask)
233
253
  shares and saves on the phone; uploaded automatically (≤25 MB)
234
254
  --url URL deep link the app opens when the user taps (PR, dashboard, log)
235
255
  --copy TEXT value offered as tap-to-copy on the detail (code, token)
236
- --actions LIST comma list: yes,no,approve,reject,accept,decline,later,
237
- done,snooze,reschedule,reply,mute
256
+ --actions LIST RESPONSE axis — comma list: yes,no,approve,reject,accept,
257
+ decline,later,done,snooze,reschedule,reply,mute (or a JSON
258
+ array of custom {id,label} objects). Composes on ANY type.
238
259
  --custom-action SPEC "id:label[:destructive][:confirm][:biometric][:terminal]" (repeatable)
260
+ --wait RESPONSE axis — block until the human answers (any type),
261
+ then print chosen_action JSON. Without it: fire-and-forget
262
+ (the answer arrives later in \`pidge listen --all\`). ask/approval imply it.
239
263
  --deliver-at ISO8601 schedule for later
240
264
  --reply-to URL also POST the answer to your webhook (HMAC-signed)
241
265
  --correlation-id ID idempotency + routing key (auto-generated if omitted)
@@ -247,7 +271,8 @@ OPTIONS (notify / ask)
247
271
  --collapse-key KEY replace/update a prior notification
248
272
  --param KEY=VALUE pass ANY raw /notify field (repeatable) — future server
249
273
  fields work without a CLI update; the manifest is the contract
250
- --timeout SECONDS ask: 600 · wait: 300
274
+ --timeout SECONDS how long --wait blocks (ask/approval: template's suggestion,
275
+ ~3600 for a decision · wait: 300) — explicit always wins
251
276
  --interval SECONDS FALLBACK poll cadence (default 30) — normally unused: WS or
252
277
  the server-held long-poll (?wait=25) make answers ~instant
253
278
 
@@ -262,17 +287,18 @@ ENV
262
287
  shared ~/.config/pidge/env (single-agent only).
263
288
 
264
289
  OUTPUT
265
- stdout is machine-readable (notify→201 JSON; ask/wait→chosen_action JSON);
266
- human notices go to stderr. Exit: 0 answered · 3 timed out (no answer yet,
267
- not a failure) · 4 timed out WITHOUT ONE healthy round-trip all session (the
268
- CHANNEL looks broken server/network not the human ignoring you: surface
269
- it instead of retrying blindly, #119) · 2 error · 1 usage.
270
-
271
- Responses are one-and-done EXCEPT snooze/reschedule (they re-fire); ask/wait keep
272
- polling through a snooze and print snooze_until. Follow-up = a NEW notification.
273
- An over-ceiling profile is delivered DEGRADED, never rejected read the 201's
274
- degraded/degrade_reason (narrated on stderr). profile "tracking" is Live-Activity-
275
- only: it never produces an answer, so \`ask\` refuses it.
290
+ stdout is machine-readable (a fire-and-forget sendthe raw 201 JSON; a --wait
291
+ send / ask / approval / wait→chosen_action JSON); human notices go to stderr.
292
+ Exit: 0 answered · 3 timed out (no answer yet, not a failure) · 4 timed out
293
+ WITHOUT ONE healthy round-trip all session (the CHANNEL looks broken
294
+ server/network not the human ignoring you: surface it instead of retrying
295
+ blindly, #119) · 2 error · 1 usage.
296
+
297
+ Responses are one-and-done EXCEPT snooze/reschedule (they re-fire); a --wait send
298
+ keeps polling through a snooze and prints snooze_until. Follow-up = a NEW
299
+ notification. An over-ceiling type is delivered DEGRADED, never rejected — read
300
+ the 201's degraded/degrade_reason (narrated on stderr). \`live\` is status-only:
301
+ it never produces an answer, so --wait/ask refuse it.
276
302
 
277
303
  Full spec (the contract — always current): GET $PIDGE_URL/api/v1/manifest`;
278
304
 
@@ -290,17 +316,18 @@ const OPTION_DOCS = {
290
316
  'body-markdown': '--body-markdown MD rich body for the tap-through detail screen',
291
317
  subtitle: '--subtitle TEXT a secondary line under the title',
292
318
  template: '--template ID content/action pattern: context · decision · approval · reminder · nudge · sensitive',
293
- profile: '--profile ID delivery profile (the human owns it): default · event · escalating · custom',
294
- 'event-at': '--event-at ISO8601 WHEN the thing happens (required by profile event)',
319
+ profile: '--profile ID low-level alias of the TYPE (the human owns it): message · important · urgent · event · live · custom',
320
+ 'event-at': '--event-at ISO8601 WHEN the thing happens (required by event)',
295
321
  'lead-minutes': '--lead-minutes N notify/countdown N min before event_at (5–240)',
296
- urgency: '--urgency LEVEL normal | persistent | alarm (low-level — prefer --profile)',
297
- escalate: '--escalate alert: force an AlarmKit alarm that breaks through silent/Focus',
322
+ urgency: '--urgency LEVEL normal | persistent | alarm (low-level — prefer the typed subcommand)',
323
+ escalate: '--escalate urgent: force an AlarmKit alarm that breaks through silent/Focus',
298
324
  image: '--image PATH_OR_URL banner+feed image: a local path is uploaded; an https URL is sent as-is',
299
325
  file: '--file PATH a real artifact (xlsx/pdf/csv…) uploaded for the human (≤25 MB)',
300
326
  url: '--url URL deep link the app opens on tap (PR, dashboard, log)',
301
327
  copy: '--copy TEXT tap-to-copy value on the detail screen',
302
- actions: '--actions LIST|JSON comma list from the catalog (yes,no,reply) OR a JSON array of {"id","label"} custom actions',
328
+ actions: '--actions LIST|JSON RESPONSE axis: comma list from the catalog (yes,no,reply) OR a JSON array of {"id","label"} custom actions — composes on ANY type',
303
329
  'custom-action': '--custom-action SPEC "id:label[:destructive][:confirm][:biometric][:terminal]" (repeatable)',
330
+ wait: '--wait RESPONSE axis: block until the human answers (any type), then print chosen_action JSON (ask/approval imply it)',
304
331
  'deliver-at': '--deliver-at ISO8601 schedule the send for later',
305
332
  'reply-to': '--reply-to URL also POST the answer to your webhook (HMAC-signed)',
306
333
  'correlation-id': '--correlation-id ID idempotency + routing key (auto-generated if omitted)',
@@ -308,7 +335,7 @@ const OPTION_DOCS = {
308
335
  after: '--after CID decision queue (#157): held until that notification is answered',
309
336
  'collapse-key': '--collapse-key KEY replace/update a prior notification',
310
337
  param: '--param KEY=VALUE pass ANY raw /notify field (repeatable) — the manifest is the contract',
311
- timeout: '--timeout SECONDS how long to block (ask: 600 · wait: 300 · listen: 600)',
338
+ timeout: '--timeout SECONDS how long --wait blocks (ask/approval: template suggestion ~3600 · wait: 300 · listen: 600)',
312
339
  interval: '--interval SECONDS FALLBACK poll cadence (default 30) — normally unused (WS/long-poll)',
313
340
  realtime: '--realtime force the realtime WebSocket (warn + fall back to polling if unavailable)',
314
341
  'no-realtime': '--no-realtime polling only (skip the WebSocket)',
@@ -330,11 +357,14 @@ const OPTION_DOCS = {
330
357
  window: '--window N reachability window in seconds (default 30)',
331
358
  'quiet-nag': '--quiet-nag silence the "server has new capabilities" nag for this run',
332
359
  };
333
- // Content flags shared by notify / ask / hello.
360
+ // Content flags shared by every send.
334
361
  const CONTENT_OPTS = ['title', 'body', 'body-markdown', 'subtitle', 'template', 'profile',
335
362
  'event-at', 'lead-minutes', 'urgency', 'image', 'file', 'url', 'copy', 'actions',
336
363
  'custom-action', 'deliver-at', 'reply-to', 'correlation-id', 'thread', 'after',
337
364
  'collapse-key', 'param'];
365
+ // Typed sends also carry the RESPONSE axis: --wait (block on the answer) + the
366
+ // blocking knobs. (`live` is status-only — it never answers, so it skips these.)
367
+ const SEND_OPTS = [...CONTENT_OPTS, 'wait', 'timeout', 'interval', 'realtime', 'no-realtime'];
338
368
 
339
369
  const HELP = {
340
370
  setup: {
@@ -359,47 +389,72 @@ const HELP = {
359
389
  body: 'A thin wrapper over `ask --template onboarding` with friendly default copy. Run it as your FIRST contact on a fresh channel.',
360
390
  opts: [...CONTENT_OPTS, 'timeout', 'interval', 'realtime', 'no-realtime'],
361
391
  },
362
- fyi: {
363
- summary: 'send passive info the human can read later no action (#246 type fyi profile Mensagem).',
364
- usage: 'pidge fyi --title TEXT [--body TEXT | --body-markdown MD] [--image PATH] [--url URL]',
365
- body: 'Fire-and-forget: stdout is the raw 201. Use it for logs, registros and neutral summaries if you need a DECISION use `pidge ask`.',
366
- opts: [...CONTENT_OPTS],
392
+ // AXIS 1 — the married catalog of 5 (perfis-S1/S2). The TYPE you pick IS how the
393
+ // human configured it to arrive. RESPONSE (--actions/--wait) composes on any of them.
394
+ message: {
395
+ summary: 'just inform passive info the human reads when they want; no action (clears when they OPEN it).',
396
+ usage: 'pidge message --title TEXT [--body TEXT | --body-markdown MD] [--image PATH] [--url URL]',
397
+ body: 'Fire-and-forget by default (stdout is the raw 201). Use it for logs, registros and neutral summaries. Need a decision? add --actions + --wait, or use `pidge important`/`pidge approval`. (Replaces the old `fyi`.)',
398
+ opts: [...SEND_OPTS],
367
399
  },
368
- report: {
369
- summary: 'send a curated result/digest the human will want to read now (#246 type report Relevante).',
370
- usage: 'pidge report --title TEXT [--body-markdown MD] [--image PATH] [--url URL]',
371
- body: 'Fire-and-forget, like fyi, but flagged as worth reading now (the feed gives it a highlighted hairline).',
372
- opts: [...CONTENT_OPTS],
400
+ important: {
401
+ summary: ' the DEFAULT — a pendency the human should resolve ("waiting-for-you" card; clears on Done).',
402
+ usage: 'pidge important --title TEXT [--actions yes,no,reply] [--wait] [--body-markdown MD]',
403
+ body: 'Fire-and-forget by default; add --actions/--custom-action for quick-tap buttons and --wait to block until the human answers (prints chosen_action JSON). The most-used type — on the fence between informing and asking, pick this. (Replaces the old `report`.)',
404
+ opts: [...SEND_OPTS],
373
405
  },
374
- ask: {
375
- summary: 'ask the human a yes/no/choice and block until they answer (#246 type ask Relevante + ação badge).',
376
- usage: 'pidge ask --title TEXT --actions yes,no,reply [--reply-to URL] [options]',
377
- body: 'Sends, then holds a WebSocket (or polls) until a TERMINAL answer. REQUIRES a way to answer --actions (catalog or JSON), --custom-action, or a --template that supplies them. A snooze/reschedule re-fires (ask keeps waiting, prints snooze_until). profile "tracking" is refused (it never produces an answer).',
378
- opts: [...CONTENT_OPTS, 'timeout', 'interval', 'realtime', 'no-realtime'],
406
+ urgent: {
407
+ summary: 'breaks through silent/Focus; --escalate forces an AlarmKit alarm. Use for the real and inadiável (<1/day).',
408
+ usage: 'pidge urgent --title TEXT [--escalate] [--actions yes,no] [--wait]',
409
+ body: 'A contract of trust: reserve it for what truly can\'t wait. --escalate asks for an AlarmKit alarm that rings through silent + Focus (the human\'s settings still decide). Once DELIVERED an urgent only stops when answered you can\'t abort it. (Replaces the old `alert`.)',
410
+ opts: [...SEND_OPTS, 'escalate'],
379
411
  },
380
412
  event: {
381
- summary: 'surface a scheduled thing with a known time — countdown Live Activity (#246 type event → Evento).',
413
+ summary: 'a scheduled thing with a known time — countdown Live Activity (needs --event-at).',
382
414
  usage: 'pidge event --title TEXT --event-at ISO8601 [--lead-minutes N] [--body-markdown MD]',
383
415
  body: 'REQUIRES --event-at (ISO8601, e.g. 2026-06-26T14:00-03:00 — no offset ⇒ the user\'s timezone). --lead-minutes (5–240) starts the countdown N min before.',
384
- opts: [...CONTENT_OPTS],
385
- },
386
- alert: {
387
- summary: 'flag an anomaly/error needing attention; --escalate forces an AlarmKit alarm (#246 type alert → Urgente).',
388
- usage: 'pidge alert --title TEXT [--body TEXT | --body-markdown MD] [--escalate]',
389
- body: 'Fire-and-forget. The channel\'s Urgente profile decides the modality; --escalate asks for an AlarmKit alarm that breaks through silent/Focus (the human\'s profile still has the final say).',
390
- opts: [...CONTENT_OPTS, 'escalate'],
416
+ opts: [...SEND_OPTS],
391
417
  },
392
418
  live: {
393
- summary: 'track an in-flight task (deploy/build/trip) with incremental updates (#246 type live Live Activity).',
419
+ summary: 'track an in-flight task (deploy/build/trip) with incremental updates (Live Activity). Status-only never answers.',
394
420
  usage: 'pidge live --title TEXT [--body TEXT] [--lead-minutes N]',
395
- body: 'Fire-and-forget. Records the live type; the LA-as-primitive is being built — today the send is delivered as a normal notification.',
421
+ body: 'Fire-and-forget. Records the live type; the LA-as-primitive is being built — today the send is delivered as a normal notification. Use judgement, not a recipe: show what the human WANTS to watch evolve.',
396
422
  opts: [...CONTENT_OPTS],
397
423
  },
424
+ // AXIS 2 — the two response shortcuts (bundle a type + buttons + --wait).
425
+ ask: {
426
+ summary: 'a DECISION — = important + --wait; needs --actions. Blocks until the human answers (prints chosen_action JSON).',
427
+ usage: 'pidge ask --title TEXT --actions yes,no,reply [--reply-to URL] [options]',
428
+ body: 'Shorthand for `important --wait` that REQUIRES a way to answer — --actions (catalog or JSON), --custom-action, or a --template that supplies them. Holds a WebSocket (or polls) until a TERMINAL answer; a snooze/reschedule re-fires (ask keeps waiting, prints snooze_until). `live` is refused (it never answers).',
429
+ opts: [...CONTENT_OPTS, 'timeout', 'interval', 'realtime', 'no-realtime'],
430
+ },
431
+ approval: {
432
+ summary: 'a go/no-go RECIPE — = important + Approve/Reject + Face ID on Approve + --wait.',
433
+ usage: 'pidge approval --title TEXT [--body-markdown MD] [options]',
434
+ body: 'The easy shortcut for an explicit approval: injects an Approve (Face-ID gated) / Reject pair and blocks on the answer. Pass your own --actions/--custom-action to override the default pair. A gated action is detail-screen only (the banner shows no quick buttons by design — gotcha #19).',
435
+ opts: [...CONTENT_OPTS, 'timeout', 'interval', 'realtime', 'no-realtime'],
436
+ },
437
+ // COMPAT aliases — old names map to the new type (kept so scripts don't break).
438
+ fyi: {
439
+ summary: 'COMPAT alias of `pidge message` (renamed in 0.14 — the married catalog). Still works; prefer `message`.',
440
+ usage: 'pidge fyi … (→ pidge message …)',
441
+ opts: [...SEND_OPTS],
442
+ },
443
+ report: {
444
+ summary: 'COMPAT alias of `pidge important` (renamed in 0.14). Still works; prefer `important`.',
445
+ usage: 'pidge report … (→ pidge important …)',
446
+ opts: [...SEND_OPTS],
447
+ },
448
+ alert: {
449
+ summary: 'COMPAT alias of `pidge urgent` (renamed in 0.14). Still works; prefer `urgent`.',
450
+ usage: 'pidge alert … (→ pidge urgent …)',
451
+ opts: [...SEND_OPTS, 'escalate'],
452
+ },
398
453
  notify: {
399
- summary: 'DEPRECATED (0.13.x) — send WITHOUT a type; the server falls back to fyi. Use a typed send instead.',
454
+ summary: 'DEPRECATED — send WITHOUT a type; the server falls back to its default. Use a typed send instead.',
400
455
  usage: 'pidge notify [options]',
401
- body: 'Kept for one minor for compat retro — it warns and still sends (template_kind defaults to fyi server-side; 0.14 will 422). Prefer `pidge fyi/report/ask/event/alert/live`.',
402
- opts: [...CONTENT_OPTS],
456
+ body: 'Kept for compat — it warns and still sends (no template_kind; the server picks the channel default). Prefer `pidge message/important/urgent/event/live` (or the `ask`/`approval` shortcuts).',
457
+ opts: [...SEND_OPTS],
403
458
  },
404
459
  wait: {
405
460
  summary: 'block on an already-sent notification until it is answered (prints chosen_action JSON).',
@@ -504,7 +559,7 @@ function fetchT(url, opts = {}, timeoutMs = 30000) {
504
559
  // The server advertises its manifest version on every response. When it's newer
505
560
  // than what this CLI shipped knowing, nudge on stderr — the agent re-reads the
506
561
  // manifest (whats_new) and learns the new capabilities without polling.
507
- const KNOWN_MANIFEST_VERSION = 36;
562
+ const KNOWN_MANIFEST_VERSION = 42;
508
563
  const NAG_TTL_MS = 24 * 60 * 60 * 1000; // #241: at most one nag per 24 h
509
564
  let newsWarned = false;
510
565
 
@@ -932,23 +987,80 @@ async function doNotify(extra = {}) {
932
987
  return { ok, info, raw };
933
988
  }
934
989
 
935
- // #246: the typed send subcommands (fyi/report/event/alert/live) share notify's
936
- // fire-and-forget shapestamp template_kind, POST, print the raw 201, exit
937
- // (0 ok / 2 failed). `ask` is the one type that send+waits (it needs a decision)
938
- // and so keeps its own case. `extra` carries alert's escalate:true.
939
- async function doTypedNotify(kind, extra = {}) {
940
- const { ok, info, raw } = await doNotify({ template_kind: kind, ...extra });
941
- console.log(raw);
942
- if (ok && info.correlation_id)
943
- console.error(`pidge: correlation_id=${info.correlation_id} (use: pidge wait ${info.correlation_id})`);
944
- process.exit(ok ? 0 : 2);
990
+ // The RESPONSE axis (perfis-S2): true when the send carries SOME way for the human
991
+ // to answer with a tap built-in actions, custom actions, or a content --template
992
+ // that supplies them. Free-text reply is ALWAYS available, so this is only about
993
+ // buttons. `ask` requires it; `approval` injects a default pair when it's absent.
994
+ const hasAnswerAffordance = () =>
995
+ v.actions !== undefined || (v['custom-action'] || []).length > 0 || v.template !== undefined;
996
+
997
+ // The `approval` recipe's default button pair (perfis-S2 follow-up). Sent as
998
+ // CUSTOM actions, NOT built-ins: only custom_actions can carry `biometric` (Face
999
+ // ID), and a custom id may NOT reuse a built-in id like approve/reject (the server
1000
+ // 422s "collides with a built-in") — so the ids are grant/deny. Face ID gates the
1001
+ // consequential "Approve"; "Reject" is the safe (destructive-styled) out. A gated
1002
+ // action is detail-screen only (no banner buttons — gotcha #19), by design.
1003
+ const APPROVAL_ACTIONS = [
1004
+ { id: 'grant', label: 'Approve', biometric: true, terminal: true },
1005
+ { id: 'deny', label: 'Reject', style: 'destructive', terminal: true },
1006
+ ];
1007
+
1008
+ // The married catalog of 5 (perfis-S1): one send, stamped with the canonical
1009
+ // `template_kind` (message/important/urgent/event/live). The RESPONSE axis is
1010
+ // orthogonal: with `wait:false` it's fire-and-forget (print the raw 201, exit);
1011
+ // with `wait:true` it mints a cid, sends, and BLOCKS until a terminal answer
1012
+ // (print chosen_action JSON). `requireAnswerable` gates `ask`. `extra` carries
1013
+ // raw fields (urgent's escalate:true, approval's injected custom_actions).
1014
+ async function doTypedSend(kind, { wait = false, extra = {}, requireAnswerable = false, label = kind } = {}) {
1015
+ if (!v.title) die('pidge: --title is required', 1);
1016
+ // `live` is status-only — it never produces an answer, so --wait would block the
1017
+ // full timeout believing the human is deciding. Refuse it (mirror the old ask guard).
1018
+ if (wait && (kind === 'live' || v.profile === 'tracking'))
1019
+ die(`pidge: \`${label}\`${kind === 'live' ? '' : ' --profile tracking'} can't --wait — ${kind === 'live' ? '`live` is' : 'tracking is'} status-only and never produces an answer (drop --wait, or ask with a real type)`, 1);
1020
+ if (requireAnswerable && !hasAnswerAffordance())
1021
+ die(`pidge: --actions required for ${label}. Use --actions yes,no (or approve,reject), --custom-action, or a --template that supplies them.`, 1);
1022
+
1023
+ if (!wait) {
1024
+ const { ok, info, raw } = await doNotify({ template_kind: kind, ...extra });
1025
+ console.log(raw);
1026
+ if (ok && info.correlation_id)
1027
+ console.error(`pidge: correlation_id=${info.correlation_id} (use: pidge wait ${info.correlation_id})`);
1028
+ process.exit(ok ? 0 : 2);
1029
+ }
1030
+
1031
+ // --wait: the cid is minted CLIENT-side when not given, and printed as the FIRST
1032
+ // stderr line (greppable) — a killed/crashed wait always leaves the handle behind,
1033
+ // so the agent can `pidge wait <cid>` instead of re-sending.
1034
+ const cid = v['correlation-id'] || crypto.randomUUID();
1035
+ v['correlation-id'] = cid;
1036
+ console.error(`pidge: correlation_id=${cid}`);
1037
+ const { ok, info } = await doNotify({ template_kind: kind, ...extra });
1038
+ if (!ok) process.exit(2);
1039
+ console.error(`pidge: sent (${info.registered_devices} device(s)) — waiting on ${cid}`);
1040
+ // #132: no --timeout ⇒ obey the template's suggestion from the 201 echo (human
1041
+ // decisions take 30-40 min; a 600 s default misreads them as silence). Explicit wins.
1042
+ let timeout = num(v.timeout, NaN);
1043
+ if (!Number.isFinite(timeout)) {
1044
+ if (info.suggested_ask_timeout) {
1045
+ timeout = info.suggested_ask_timeout;
1046
+ console.error(`pidge: timeout ${Math.round(timeout / 60)} min — suggested by template ${info.template || v.template} (override with --timeout)`);
1047
+ } else {
1048
+ timeout = 600;
1049
+ }
1050
+ }
1051
+ await waitForAnswer(cid, { timeout, interval: num(v.interval, 30) });
1052
+ }
1053
+
1054
+ // A compat alias (perfis-S1): the OLD type name still works, mapped to the new
1055
+ // canonical one — a one-line note points at the rename so muscle-memory migrates.
1056
+ function warnRenamed(oldName, newName) {
1057
+ console.error(`pidge: \`pidge ${oldName}\` was renamed → use \`pidge ${newName}\` (the married catalog of 5; the alias keeps working).`);
945
1058
  }
946
1059
 
947
- // #246: `pidge notify` / `pidge send` (no type) are deprecated for ONE minor
948
- // (0.13.x) they still send, and the server falls back to template_kind "fyi"
949
- // (soft-rollout). 0.14 will 422 a typeless send. The warning is local (stderr).
1060
+ // `pidge notify` / `pidge send` (no type) are deprecated they still send, and the
1061
+ // server falls back to the channel default. Prefer a typed send. Warning is local.
950
1062
  function warnDeprecatedSend(name) {
951
- console.error(`pidge: \`pidge ${name}\` is deprecated — use a TYPE instead: fyi · report · ask · event · alert · live (see \`pidge help\`). Server-side fallback to \`fyi\` continues in 0.13.x; will be removed in 0.14.`);
1063
+ console.error(`pidge: \`pidge ${name}\` is deprecated — use a TYPE instead: message · important · urgent · event · live (or the ask/approval shortcuts; see \`pidge help\`). It still sends (no template_kind the server picks the channel default).`);
952
1064
  }
953
1065
 
954
1066
  // Poll GET /notifications/:cid until a TERMINAL answer, print chosen_action JSON to
@@ -1567,7 +1679,7 @@ async function runSkillInstall() {
1567
1679
  const exits = (m.cli && m.cli.output) || '';
1568
1680
  const skill = `---
1569
1681
  name: pidge
1570
- description: Send rich, actionable iPhone notifications to your human and get their decision back (Pidge). Use when finishing long tasks (report), needing a decision/approval, sending FYIs with substance, or anything time-anchored. Also covers reading the human's replies/messages back.
1682
+ description: Send rich, actionable iPhone notifications to your human and get their decision back (Pidge). Pick a type (message/important/urgent/event/live) and, orthogonally, a response (buttons + send-and-go vs wait). Use when finishing long tasks, needing a decision/approval, sending updates with substance, or anything time-anchored. Also covers reading the human's replies/messages back.
1571
1683
  ---
1572
1684
 
1573
1685
  # Pidge — notify your human, get answers back
@@ -1576,23 +1688,43 @@ Generated from manifest v${m.manifest_version} of ${BASE} — re-run \`pidge ski
1576
1688
 
1577
1689
  All commands: \`npx pidge-cli …\` (Node ≥18; reads ~/.config/pidge/env — no token in context). Not set up? \`pidge doctor\` tells you; onboard with \`pidge setup --claim <code>\` (the human copies the code from the Pidge app).
1578
1690
 
1579
- ## Choose the right type (REQUIRED in 0.14+)
1580
-
1581
- Every send needs a type. Pick by intent:
1691
+ ## Two axes: the TYPE + the RESPONSE
1582
1692
 
1583
- | You want to... | Use | Example |
1584
- |---|---|---|
1585
- | Log something the human can read later, no action | \`pidge fyi\` | "Build completed in 2m12s" |
1586
- | Deliver a curated result/digest worth reading now | \`pidge report\` | "Daily standup summary" |
1587
- | Ask the human a yes/no/choice — block until they answer | \`pidge ask\` | "Approve deploy v3.2?" with \`--actions yes,no\` |
1588
- | Surface a scheduled thing (with time) | \`pidge event\` | "Sprint review 14h" with \`--event-at ...\` |
1589
- | Anomaly/error needing attention; add \`--escalate\` for AlarmKit | \`pidge alert\` | "API 503 errors spiked" |
1590
- | Track an in-flight task with incremental updates | \`pidge live\` | "Deploy v3.2 — building..." |
1693
+ You and your human speak the SAME language. You pick ONE **type** (how much it may
1694
+ intrude — the human already configured how each arrives); then, ORTHOGONALLY, you
1695
+ decide the **response** (buttons? wait or not?).
1591
1696
 
1592
- If unsure: \`fyi\` for passive info, \`ask\` if you need a decision. NEVER use \`pidge send\`
1593
- without a type — in 0.14 it'll 422. (In 0.13.x it warns locally + server falls back to fyi.)
1697
+ ### Axis 1 the type (one married list of 5)
1594
1698
 
1595
- Available CLI commands (typed sends): \`pidge fyi\` · \`pidge report\` · \`pidge ask\` · \`pidge event\` · \`pidge alert\` · \`pidge live\` (and \`pidge notify\`, deprecated). Run \`pidge <type> --help\` for each one's own flags.
1699
+ | You want to... | Use | The human sees / clears when |
1700
+ |---|---|---|
1701
+ | just inform, no action | \`pidge message\` | quiet banner; clears when they OPEN it |
1702
+ | a pendency they should resolve ⭐ DEFAULT | \`pidge important\` | "waiting-for-you" card; clears on **Done** |
1703
+ | a go/no-go DECISION (approve/choose) | \`pidge approval\` | Approve/Reject + **Face ID**; clears when they decide |
1704
+ | a thing with a known TIME | \`pidge event --event-at <ISO>\` | countdown + reminder; passed / Done |
1705
+ | TRACK something live | \`pidge live\` | Live Activity on the lock; you end it |
1706
+ | WAKE them now (rare, real) | \`pidge urgent\` | **alarm** through silent/Focus; Done cuts it |
1707
+
1708
+ ⭐ \`important\` is the default — on the fence between informing and asking, pick it.
1709
+ (Forget \`fyi\`/\`report\` — they're gone; every send is title + markdown, only the
1710
+ DELIVERY differs. The old names still work as aliases → message/important/urgent.)
1711
+
1712
+ ### Axis 2 — the response (composes on ANY type)
1713
+
1714
+ "Asking for a reply" is separate from the type — you don't need \`approval\` to get a button:
1715
+ - **Free text** → ALWAYS available; the human can write back on any notification.
1716
+ - **Buttons** → optional, any type: \`--actions yes,no\` (catalog) or \`--custom-action\` (e.g. \`confirm/postpone\`).
1717
+ - **Face ID** → \`:biometric\` locks a sensitive button (\`approval\` turns it on by default). A flag, not a type.
1718
+ - **send-and-go vs wait** — the choice that decides how YOU work:
1719
+ - **send-and-go** (fire and continue): the answer arrives later in \`pidge listen --all\`. For a turn-based agent.
1720
+ - **wait** (block until they tap): \`--wait\` (or \`pidge ask\`). For when you can't proceed without the decision.
1721
+ - \`approval\` is a RECIPE, not magic: = \`important\` + Approve/Reject + Face ID + \`--wait\`.
1722
+
1723
+ Need a TYPED reply (a time/value/name)? \`--actions reply\` ALONE — never yes/no+reply
1724
+ together (the human taps the easy button and you get a useless "Yes"). ONE question per send.
1725
+
1726
+ Available subcommands: \`pidge message · important · urgent · event · live\` (+ the
1727
+ \`ask\`/\`approval\` shortcuts; \`fyi/report/alert\` aliases; \`notify\` deprecated). Run \`pidge <type> --help\` for each one's flags.
1596
1728
 
1597
1729
  ## Pick the right send (decision table)
1598
1730
 
@@ -1672,12 +1804,21 @@ Each poll is one of your turns: pick up the message, do the work, \`pidge ack --
1672
1804
  await runSkillInstall();
1673
1805
  break;
1674
1806
  }
1675
- // #246: typed sendsfyi/report/event/alert/live stamp template_kind and
1676
- // fire-and-forget. ask is separate (it send+waits). notify/send are the
1677
- // deprecated typeless path (server falls back to fyi during the soft-rollout).
1678
- case 'fyi':
1679
- case 'report':
1680
- await doTypedNotify(command);
1807
+ // === AXIS 1the married catalog of 5 (perfis-S1/S2). Each stamps the
1808
+ // canonical template_kind. AXIS 2 (response) is orthogonal: --actions/
1809
+ // --custom-action add buttons, --wait blocks on the answer (else fire-and-
1810
+ // forget). notify/send = the deprecated typeless path; ask/approval = the
1811
+ // two shortcuts that bundle a type + response. ===
1812
+ case 'message':
1813
+ await doTypedSend('message', { wait: !!v.wait });
1814
+ break;
1815
+ case 'important':
1816
+ await doTypedSend('important', { wait: !!v.wait });
1817
+ break;
1818
+ case 'urgent':
1819
+ // --escalate ⇒ escalate:true (ask the Urgente profile for an AlarmKit alarm
1820
+ // that breaks through silent/Focus; the human's profile still decides).
1821
+ await doTypedSend('urgent', { wait: !!v.wait, extra: v.escalate ? { escalate: true } : {} });
1681
1822
  break;
1682
1823
  case 'event': {
1683
1824
  // event needs a TIME — validate locally (ISO8601) so the agent fails fast
@@ -1686,17 +1827,37 @@ Each poll is one of your turns: pick up the message, do the work, \`pidge ack --
1686
1827
  die('pidge: --event-at required for event. Use ISO8601: --event-at 2026-06-26T14:00-03:00', 1);
1687
1828
  if (Number.isNaN(Date.parse(v['event-at'])))
1688
1829
  die(`pidge: --event-at ${JSON.stringify(v['event-at'])} is not a valid ISO8601 datetime. Use e.g. --event-at 2026-06-26T14:00-03:00`, 1);
1689
- await doTypedNotify('event');
1830
+ await doTypedSend('event', { wait: !!v.wait });
1690
1831
  break;
1691
1832
  }
1833
+ case 'live':
1834
+ // status-only — pass --wait through so doTypedSend REFUSES it loudly (it
1835
+ // never produces an answer); without --wait it's fire-and-forget.
1836
+ await doTypedSend('live', { wait: !!v.wait });
1837
+ break;
1838
+ // --- compat aliases (perfis-S1): old type names → the new canonical 5. They
1839
+ // map to the new template_kind and still honor --wait/--actions, so scripts
1840
+ // and muscle-memory keep working; a one-line note points at the new name.
1841
+ case 'fyi':
1842
+ warnRenamed('fyi', 'message');
1843
+ await doTypedSend('message', { wait: !!v.wait, label: 'fyi' });
1844
+ break;
1845
+ case 'report':
1846
+ warnRenamed('report', 'important');
1847
+ await doTypedSend('important', { wait: !!v.wait, label: 'report' });
1848
+ break;
1692
1849
  case 'alert':
1693
- // --escalate ⇒ escalate:true (ask the channel's Urgente profile for an
1694
- // AlarmKit alarm that breaks through silent/Focus; the human's profile decides).
1695
- await doTypedNotify('alert', v.escalate ? { escalate: true } : {});
1850
+ warnRenamed('alert', 'urgent');
1851
+ await doTypedSend('urgent', { wait: !!v.wait, extra: v.escalate ? { escalate: true } : {}, label: 'alert' });
1696
1852
  break;
1697
- case 'live':
1698
- await doTypedNotify('live');
1853
+ // `approval` = the RECIPE (perfis-S2 follow-up): important + Approve/Reject
1854
+ // (Face ID on Approve) + --wait. A shortcut for an explicit go/no-go; the human
1855
+ // can override the pair with their own --actions/--custom-action.
1856
+ case 'approval': {
1857
+ const extra = hasAnswerAffordance() ? {} : { custom_actions: APPROVAL_ACTIONS };
1858
+ await doTypedSend('important', { wait: true, extra, label: 'approval' });
1699
1859
  break;
1860
+ }
1700
1861
  case 'notify':
1701
1862
  case 'send': {
1702
1863
  warnDeprecatedSend(command);
@@ -1733,40 +1894,11 @@ Each poll is one of your turns: pick up the message, do the work, \`pidge ack --
1733
1894
  break;
1734
1895
  }
1735
1896
  case 'ask': {
1736
- // Send, then block on the answer in one shot. stdout = ONLY chosen_action JSON.
1737
- // tracking is Live-Activity-only: it NEVER produces a chosen_action, so an ask
1738
- // would block the full timeout believing the human is deciding.
1739
- if (v.profile === 'tracking')
1740
- die('pidge: `ask --profile tracking` makes no sense — tracking never produces an answer (use the live_activities API; need a decision? send a real profile)', 1);
1741
- if (!v.title) die('pidge: --title is required', 1);
1742
- // #246: an ask DECLARES a decision — it must say HOW the human answers.
1743
- // --actions (catalog or JSON), --custom-action, or a --template that supplies
1744
- // them all satisfy it; none ⇒ a local error (the spec's "no hidden default").
1745
- if (v.actions === undefined && !(v['custom-action'] || []).length && v.template === undefined)
1746
- die('pidge: --actions required for ask. Use --actions yes,no,reply or a JSON array.', 1);
1747
- // The cid is minted CLIENT-side when not given, and printed as the FIRST
1748
- // stderr line (greppable) — a killed/crashed ask always leaves the handle
1749
- // behind, so the agent can `pidge wait <cid>` instead of re-sending.
1750
- const cid = v['correlation-id'] || crypto.randomUUID();
1751
- v['correlation-id'] = cid;
1752
- console.error(`pidge: correlation_id=${cid}`);
1753
- const { ok, info } = await doNotify({ template_kind: 'ask' });
1754
- if (!ok) process.exit(2);
1755
- console.error(`pidge: sent (${info.registered_devices} device(s)) — waiting on ${cid}`);
1756
- // #132: no --timeout ⇒ obey the template's suggestion from the 201 echo
1757
- // (human decisions take 30-40 min in the wild; a 600 s default misread
1758
- // them as silence). Explicit --timeout always wins.
1759
- let timeout = num(v.timeout, NaN);
1760
- if (!Number.isFinite(timeout)) {
1761
- if (info.suggested_ask_timeout) {
1762
- timeout = info.suggested_ask_timeout;
1763
- const mins = Math.round(timeout / 60);
1764
- console.error(`pidge: timeout ${mins} min — suggested by template ${info.template || v.template} (override with --timeout)`);
1765
- } else {
1766
- timeout = 600;
1767
- }
1768
- }
1769
- await waitForAnswer(cid, { timeout, interval: num(v.interval, 30) });
1897
+ // `ask` = the preserved shortcut: important + --wait + REQUIRES a way to
1898
+ // answer. There is no `ask` TYPE in the married catalog (manifest v40+) —
1899
+ // asking is "a type + buttons + wait". The legacy alias keeps working because
1900
+ // it always ships with buttons. `live`/tracking is refused (it never answers).
1901
+ await doTypedSend('important', { wait: true, requireAnswerable: true, label: 'ask' });
1770
1902
  break;
1771
1903
  }
1772
1904
  case 'wait': {
@@ -2052,8 +2184,8 @@ Each poll is one of your turns: pick up the message, do the work, \`pidge ack --
2052
2184
  break;
2053
2185
  }
2054
2186
  default:
2055
- // #246: name the bad command and point at the type catalog (a friendlier
2056
- // landing than dumping the whole USAGE on a typo).
2057
- die(`pidge: unknown subcommand '${command}'. Try: fyi · report · ask · event · alert · live · notify (deprecated). pidge --help`, 1);
2187
+ // Name the bad command and point at the married catalog + the two response
2188
+ // shortcuts (a friendlier landing than dumping the whole USAGE on a typo).
2189
+ die(`pidge: unknown subcommand '${command}'. Types: message · important · urgent · event · live (response: --actions/--wait, or the ask/approval shortcuts). pidge --help`, 1);
2058
2190
  }
2059
2191
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pidge-cli",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "Send rich, actionable iPhone notifications to a human and block until they answer. Built for AI agents.",
5
5
  "keywords": [
6
6
  "pidge",