@raidiant/notifai 0.1.8 → 0.2.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/dist/send.js CHANGED
@@ -1,4 +1,4 @@
1
- import { CLI_SOUNDS, COLLAPSE_KEY_MAX_BYTES, INTERRUPTION_LEVELS, NOTIFICATION_SCHEMA_VERSION, NOTIFICATION_KINDS, PLATFORMS, } from '@raidiant/notifai-protocol';
1
+ import { CLI_SOUNDS, COLLAPSE_KEY_MAX_BYTES, INTERRUPTION_LEVELS, NOTIFICATION_SCHEMA_VERSION, QUESTION_TEXT_MAX_LENGTH, NOTIFICATION_KINDS, PLATFORMS, } from '@raidiant/notifai-protocol';
2
2
  /** Merge flags over resolved config into a Notification Request draft. */
3
3
  export function buildDraft(config, flags) {
4
4
  if (!flags.title || !flags.body)
@@ -79,6 +79,21 @@ export function buildDraft(config, flags) {
79
79
  error: CHOICE_USAGE,
80
80
  };
81
81
  }
82
+ if (flags.replyMulti && choices === null && flags.questions === undefined) {
83
+ return { ok: false, error: '--reply-multi needs answers to select between; add --reply-choice.' };
84
+ }
85
+ // The banner body doubles as the question text on a single-question send, so
86
+ // it inherits the question's readable-in-full bound; context beyond that
87
+ // belongs in --detail, which never rides the banner.
88
+ if (flags.reply &&
89
+ flags.questions === undefined &&
90
+ flags.body.length > QUESTION_TEXT_MAX_LENGTH) {
91
+ return {
92
+ ok: false,
93
+ error: `A question must be readable where it is answered: keep it within ${QUESTION_TEXT_MAX_LENGTH} ` +
94
+ 'characters and put the longer context in --detail.',
95
+ };
96
+ }
82
97
  const draft = {
83
98
  schema_version: NOTIFICATION_SCHEMA_VERSION,
84
99
  ...(flags.event !== undefined ? { event: flags.event } : {}),
@@ -103,7 +118,14 @@ export function buildDraft(config, flags) {
103
118
  ? {
104
119
  reply: {
105
120
  expires_in_seconds: flags.replyWindow ?? 3600,
106
- ...(choices !== null ? { kind: 'choice', choices } : {}),
121
+ questions: flags.questions ?? [
122
+ {
123
+ id: 'q1',
124
+ text: flags.body,
125
+ ...(choices !== null ? { choices } : {}),
126
+ ...(flags.replyMulti ? { multi: true } : {}),
127
+ },
128
+ ],
107
129
  },
108
130
  }
109
131
  : {}),
@@ -152,23 +174,18 @@ export function receiptExitCode(receipt) {
152
174
  return receipt.overall === 'provider_rejected_all' ? 1 : 0;
153
175
  }
154
176
  /**
155
- * `"Staging,Production"` -> `[{id:'staging',label:'Staging'}, ...]`.
177
+ * One flag occurrence, one answer: `--choice Staging --choice Production` ->
178
+ * `[{id:'staging',label:'Staging'}, ...]`.
156
179
  *
157
- * A single occurrence splits on commas, which keeps the common one-liner
158
- * short. Repeating the flag is the escape hatch, and the only way to write a
159
- * label that itself contains a comma: passing `"Yes, ship it,No"` as one value
160
- * silently yields three choices, and nothing downstream can detect that the
161
- * agent meant two.
180
+ * Commas are ordinary characters. The old single-value comma split produced
181
+ * buttons the agent never intended ("Nothing, I'll review" became two answers)
182
+ * and nothing downstream could detect it, so the delimiter form is gone
183
+ * rather than guarded: a label is a label.
162
184
  */
163
185
  export function parseChoices(value) {
164
- if (value === undefined)
165
- return null;
166
- const raw = Array.isArray(value) ? value : [value];
167
- if (raw.length === 0)
186
+ if (value === undefined || value.length === 0)
168
187
  return null;
169
- const labels = (raw.length === 1 ? (raw[0] ?? '').split(',') : raw)
170
- .map((part) => part.trim())
171
- .filter((part) => part.length > 0);
188
+ const labels = value.map((part) => part.trim()).filter((part) => part.length > 0);
172
189
  if (labels.length < 2 || labels.length > 6)
173
190
  return 'invalid';
174
191
  const choices = labels.map((label) => ({ id: slugify(label), label }));
@@ -179,36 +196,9 @@ export function parseChoices(value) {
179
196
  return choices;
180
197
  }
181
198
  /** Message shared by every surface that accepts choice labels. */
182
- export const CHOICE_USAGE = 'Choices must be 2-6 non-empty labels, unique once slugified. One value splits on ' +
183
- 'commas ("Staging,Production"); repeat the flag when a label contains a comma.';
184
- /**
185
- * Catches the split that looked like it worked.
186
- *
187
- * `--choice "macOS parity,Long-form detail,Nothing, I'll review"` produced four
188
- * buttons rather than three, because the third label contained a comma. Nothing
189
- * downstream can detect it — the labels are all well-formed — and the CLI
190
- * cheerfully echoed the wrong list back, so the user saw buttons the agent
191
- * never intended.
192
- *
193
- * The tell is a comma followed by a space. A delimiter list is written
194
- * "Yes,No"; prose is written "Nothing, I'll review". That is a heuristic and it
195
- * will occasionally be wrong, which is exactly why this warns and names the
196
- * escape hatch rather than rejecting: the split may well have been intended.
197
- */
198
- export function ambiguousChoiceSplit(value) {
199
- if (value === undefined || Array.isArray(value))
200
- return null;
201
- if (!/,\s/.test(value))
202
- return null;
203
- const parts = value
204
- .split(',')
205
- .map((part) => part.trim())
206
- .filter((part) => part.length > 0);
207
- return (`Heads up: "${value}" was split on commas into ${parts.length} answers ` +
208
- `(${parts.join(' / ')}). If one of those labels was meant to contain a comma, ` +
209
- 'pass --choice once per answer instead.');
210
- }
211
- function slugify(label) {
199
+ export const CHOICE_USAGE = 'Offer 2-6 answers, one --choice flag per answer, unique once slugified — ' +
200
+ 'e.g. --choice "Yes, ship it" --choice "Not yet".';
201
+ export function slugify(label) {
212
202
  return label
213
203
  .toLowerCase()
214
204
  .replace(/[^a-z0-9]+/g, '-')
package/dist/send.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"send.js","sourceRoot":"","sources":["../src/send.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"send.js","sourceRoot":"","sources":["../src/send.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,sBAAsB,EACtB,mBAAmB,EACnB,2BAA2B,EAC3B,wBAAwB,EAKxB,kBAAkB,EAClB,SAAS,GAIV,MAAM,4BAA4B,CAAA;AAsDnC,0EAA0E;AAC1E,MAAM,UAAU,UAAU,CAAC,MAAiB,EAAE,KAAgB;IAC5D,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,uCAAuC,EAAE,CAAA;IACrG,MAAM,QAAQ,GAAG,eAAe,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IAChD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,qBAAqB,KAAK,CAAC,QAAQ,WAAW,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAA;IACtG,CAAC;IAED,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IACjG,MAAM,OAAO,GACX,SAAS,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;IAEnG,MAAM,aAAa,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,QAAQ,CAAC,CAAA;IACzE,MAAM,OAAO,GAAG,aAAa,CAAC,aAA2C,CAAC,CAAA;IAC1E,MAAM,eAAe,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAA;IAC1F,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,eAAe,IAAI,OAAO,EAAE,KAAK,CAAA;IAC9D,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,KAAiB,CAAC,EAAE,CAAC;QACrF,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,kBAAkB,KAAK,kBAAkB,UAAU,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;SACtG,CAAA;IACH,CAAC;IACD,MAAM,eAAe,GACnB,MAAM,CAAC,kBAAkB,CAAC,MAAM,KAAK,SAAS;QAC5C,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,MAAM,CAAC,kBAAkB,CAAC,KAAK,CAAA;IACrC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,eAAe,IAAI,OAAO,EAAE,KAAK,CAAA;IAC9D,IACE,KAAK,KAAK,IAAI;QACd,KAAK,KAAK,SAAS;QACnB,CAAC,mBAAmB,CAAC,QAAQ,CAAC,KAA6C,CAAC,EAC5E,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,+BAA+B,KAAK,IAAI,EAAE,CAAA;IACvE,CAAC;IAED,MAAM,OAAO,GAAgC,EAAE,CAAA;IAC/C,IAAI,KAAK,KAAK,MAAM;QAAE,OAAO,CAAC,KAAK,GAAG,IAAI,CAAA;SACrC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/C,OAAO,CAAC,KAAK,GAAG,KAAqD,CAAA;IACvE,CAAC;IACD,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS;QAAE,OAAO,CAAC,SAAS,GAAG,KAAK,CAAC,QAAQ,CAAA;IACpE,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QAC1C,OAAO,CAAC,kBAAkB,GAAG,KAA6C,CAAA;IAC5E,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QACvB,MAAM,IAAI,GAA2B,EAAE,CAAA;QACvC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;YAC9B,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;YAC5B,IAAI,EAAE,IAAI,CAAC;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,kCAAkC,IAAI,IAAI,EAAE,CAAA;YACpF,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;QAC9C,CAAC;QACD,OAAO,CAAC,WAAW,GAAG,IAAI,CAAA;IAC5B,CAAC;IAED,IACE,KAAK,CAAC,IAAI,KAAK,SAAS;QACxB,CAAC,kBAAkB,CAAC,QAAQ,CAAC,KAAK,CAAC,IAA2C,CAAC,EAC/E,CAAC;QACD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,iBAAiB,KAAK,CAAC,IAAI,kBAAkB,kBAAkB,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;SAChH,CAAA;IACH,CAAC;IAED,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,IAAI,MAAM,CAAC,WAAW,CAAC,KAAK,CAAA;IACjD,MAAM,QAAQ,GAAG,KAAK,CAAC,WAAW,IAAI,MAAM,CAAC,YAAY,CAAC,KAAK,CAAA;IAC/D,IACE,QAAQ,KAAK,IAAI;QACjB,QAAQ,KAAK,SAAS;QACtB,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,sBAAsB,EAC5D,CAAC;QACD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,kCAAkC,sBAAsB,eAAe;SAC/E,CAAA;IACH,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAA;IACrD,wEAAwE;IACxE,iBAAiB;IACjB,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAA;IAE/D,4EAA4E;IAC5E,4EAA4E;IAC5E,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;IAC/C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,YAAY;SACpB,CAAA;IACH,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,IAAI,OAAO,KAAK,IAAI,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QAC1E,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,oEAAoE,EAAE,CAAA;IACnG,CAAC;IACD,6EAA6E;IAC7E,yEAAyE;IACzE,qDAAqD;IACrD,IACE,KAAK,CAAC,KAAK;QACX,KAAK,CAAC,SAAS,KAAK,SAAS;QAC7B,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,wBAAwB,EAC5C,CAAC;QACD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EACH,oEAAoE,wBAAwB,GAAG;gBAC/F,oDAAoD;SACvD,CAAA;IACH,CAAC;IAED,MAAM,KAAK,GAAuB;QAChC,cAAc,EAAE,2BAA2B;QAC3C,GAAG,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS;YAC1B,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAA2C,EAAE;YAC7D,CAAC,CAAC,EAAE,CAAC;QACP,GAAG,CAAC,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxE,GAAG,CAAC,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjE,GAAG,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/D,YAAY,EAAE;YACZ,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,GAAG,CAAC,KAAK,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrE,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE;gBAC1D,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE;gBAC1B,CAAC,CAAC,EAAE,CAAC;YACP,GAAG,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC3E;QACD,OAAO;QACP,QAAQ,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,YAAY,EAAE,QAAQ,EAAE;QACtD,GAAG,CAAC,KAAK,CAAC,KAAK;YACb,CAAC,CAAC;gBACE,KAAK,EAAE;oBACL,kBAAkB,EAAE,KAAK,CAAC,WAAW,IAAI,IAAI;oBAC7C,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI;wBAC5B;4BACE,EAAE,EAAE,IAAI;4BACR,IAAI,EAAE,KAAK,CAAC,IAAI;4BAChB,GAAG,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;4BACxC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;yBAC7C;qBACF;iBACF;aACF;YACH,CAAC,CAAC,EAAE,CAAC;QACP,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC;YACjC,CAAC,CAAC,KAAK,CAAC,QAAQ,KAAK,SAAS;gBAC5B,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;gBAChD,CAAC,CAAC,QAAQ,KAAK,KAAK;oBAClB,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE;oBAChC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YACtC,CAAC,CAAC,EAAE,CAAC;KACR,CAAA;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAA;AACtC,CAAC;AAED,MAAM,aAAa,GAAG;IACpB,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE;IAC3C,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE;IACzC,QAAQ,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE;CACzC,CAAA;AAEV,SAAS,eAAe,CAAC,KAAyB;IAChD,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IACrC,OAAQ,SAA+B,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAE,KAAkB,CAAC,CAAC,CAAC,IAAI,CAAA;AACtF,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,aAAa,CAAC,OAA0B;IACtD,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,MAAM,OAAO,GACX,OAAO,CAAC,OAAO,KAAK,uBAAuB;QACzC,CAAC,CAAC,gDAAgD;QAClD,CAAC,CAAC,OAAO,CAAC,OAAO,KAAK,2BAA2B;YAC/C,CAAC,CAAC,2BAA2B;YAC7B,CAAC,CAAC,OAAO,CAAC,OAAO,KAAK,uBAAuB;gBAC3C,CAAC,CAAC,2BAA2B;gBAC7B,CAAC,CAAC,iBAAiB,CAAA;IAC3B,KAAK,CAAC,IAAI,CAAC,WAAW,OAAO,CAAC,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,KAAK,OAAO,EAAE,CAAC,CAAA;IAC/F,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;QACnC,MAAM,MAAM,GAAG,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC,EAAE,CAAA;QACjE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,KAAK,GAAG,MAAM,EAAE,CAAC,CAAA;IACvD,CAAC;IACD,KAAK,CAAC,IAAI,CACR,6DAA6D,OAAO,CAAC,UAAU,iCAAiC,CACjH,CAAA;IACD,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,QAAQ;QAAE,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAA;IACjF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,eAAe,CAAC,OAA0B;IACxD,OAAO,OAAO,CAAC,OAAO,KAAK,uBAAuB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAC5D,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAC1B,KAA2B;IAE3B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAC1D,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IACjF,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IAC5D,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;IACtE,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;QAAE,OAAO,SAAS,CAAA;IAChE,IAAI,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,MAAM;QAAE,OAAO,SAAS,CAAA;IACzF,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,kEAAkE;AAClE,MAAM,CAAC,MAAM,YAAY,GACvB,2EAA2E;IAC3E,kDAAkD,CAAA;AAEpD,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,OAAO,KAAK;SACT,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;AACjB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raidiant/notifai",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Send native phone notifications from agents and local programs",
6
6
  "bin": {
@@ -11,9 +11,14 @@
11
11
  "src",
12
12
  "tsconfig.json"
13
13
  ],
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json",
16
+ "typecheck": "tsc -p tsconfig.json --noEmit",
17
+ "test": "vitest run"
18
+ },
14
19
  "dependencies": {
15
20
  "@clack/prompts": "^1.7.0",
16
- "@raidiant/notifai-protocol": "0.1.2",
21
+ "@raidiant/notifai-protocol": "0.2.0",
17
22
  "commander": "^14.0.0",
18
23
  "smol-toml": "^1.4.0"
19
24
  },
@@ -47,10 +52,5 @@
47
52
  "ai",
48
53
  "claude",
49
54
  "codex"
50
- ],
51
- "scripts": {
52
- "build": "tsc -p tsconfig.json",
53
- "typecheck": "tsc -p tsconfig.json --noEmit",
54
- "test": "vitest run"
55
- }
56
- }
55
+ ]
56
+ }
@@ -14,6 +14,7 @@ import { ApiCallError, NetworkError, type ApiClient } from './client.js'
14
14
  import {
15
15
  askCommand,
16
16
  accessStatusCommand,
17
+ buildQuestions,
17
18
  assessReadiness,
18
19
  capabilitiesCommand,
19
20
  configSetCommand,
@@ -379,7 +380,10 @@ describe('command contracts', () => {
379
380
  replyTimeout: 30,
380
381
  }),
381
382
  ).toBe(EXIT.ok)
382
- expect(submitted?.draft.reply).toEqual({ expires_in_seconds: 3_600 })
383
+ expect(submitted?.draft.reply).toEqual({
384
+ expires_in_seconds: 3_600,
385
+ questions: [{ id: 'q1', text: 'Deploy?' }],
386
+ })
383
387
  })
384
388
 
385
389
  it('rejects --reply-timeout 0, the other spelling of nobody waiting', async () => {
@@ -1147,7 +1151,7 @@ describe('init', () => {
1147
1151
  source: 'Raidiant-io/notifai',
1148
1152
  sourceType: 'github',
1149
1153
  sourceUrl: 'https://github.com/Raidiant-io/notifai.git',
1150
- ref: 'v0.1.8',
1154
+ ref: 'v0.2.0',
1151
1155
  }
1152
1156
  }
1153
1157
 
@@ -1250,7 +1254,7 @@ describe('init', () => {
1250
1254
  })
1251
1255
 
1252
1256
  it('pins the skill installer to the tagged public release syntax', () => {
1253
- expect(SKILLS_SOURCE).toBe('Raidiant-io/notifai#v0.1.8')
1257
+ expect(SKILLS_SOURCE).toBe('Raidiant-io/notifai#v0.2.0')
1254
1258
  expect(SKILLS_SOURCE).not.toContain('@v')
1255
1259
  })
1256
1260
 
@@ -1273,7 +1277,7 @@ describe('init', () => {
1273
1277
  source: 'Raidiant-io/notifai',
1274
1278
  sourceType: 'github',
1275
1279
  sourceUrl: 'https://github.com/Raidiant-io/notifai.git',
1276
- ref: 'v0.1.8',
1280
+ ref: 'v0.2.0',
1277
1281
  },
1278
1282
  ],
1279
1283
  }),
@@ -2173,7 +2177,7 @@ describe('asking before the hooks have ever run', () => {
2173
2177
  source: 'Raidiant-io/notifai',
2174
2178
  sourceType: 'github',
2175
2179
  sourceUrl: 'https://github.com/Raidiant-io/notifai.git',
2176
- ref: 'v0.1.8',
2180
+ ref: 'v0.2.0',
2177
2181
  },
2178
2182
  ]
2179
2183
  : [],
@@ -2486,7 +2490,97 @@ describe('a server behind this CLI', () => {
2486
2490
  })
2487
2491
  })
2488
2492
 
2489
- /** First-reply-wins is the right default, silently is the wrong way. */
2493
+ describe('question sets', () => {
2494
+ it('maps --reply-multi into the single question', async () => {
2495
+ const io = new CapturedIo()
2496
+ let submitted: SubmitNotificationRequestT | undefined
2497
+ const client = {
2498
+ submit: async (body: SubmitNotificationRequestT) => {
2499
+ submitted = body
2500
+ return receipt
2501
+ },
2502
+ replies: async () => replyResponse([reply]),
2503
+ } as unknown as ApiClient
2504
+
2505
+ expect(
2506
+ await sendCommand(makeDeps(io, client), {
2507
+ title: 'Question',
2508
+ body: 'Which fronts?',
2509
+ reply: true,
2510
+ replyTimeout: 30,
2511
+ replyChoice: ['CLI', 'Server', 'Apps'],
2512
+ replyMulti: true,
2513
+ }),
2514
+ ).toBe(EXIT.ok)
2515
+ expect(submitted?.draft.reply?.questions?.[0]).toMatchObject({
2516
+ text: 'Which fronts?',
2517
+ multi: true,
2518
+ })
2519
+ expect(submitted?.draft.reply?.questions?.[0]?.choices).toHaveLength(3)
2520
+ })
2521
+
2522
+ it('rejects a question body too long for the answering surface', async () => {
2523
+ const io = new CapturedIo()
2524
+ let submitCalls = 0
2525
+ const client = {
2526
+ submit: async () => {
2527
+ submitCalls += 1
2528
+ return receipt
2529
+ },
2530
+ } as unknown as ApiClient
2531
+
2532
+ expect(
2533
+ await sendCommand(makeDeps(io, client), {
2534
+ title: 'Question',
2535
+ body: 'x'.repeat(501),
2536
+ reply: true,
2537
+ replyTimeout: 30,
2538
+ }),
2539
+ ).toBe(EXIT.usage)
2540
+ expect(submitCalls).toBe(0)
2541
+ expect(io.errLines.join(' ')).toContain('--detail')
2542
+ })
2543
+
2544
+ it('generates unique question ids when texts collide', () => {
2545
+ const built = buildQuestions(
2546
+ {
2547
+ form: JSON.stringify({
2548
+ questions: [{ text: 'Ready?' }, { text: 'Ready?' }],
2549
+ }),
2550
+ },
2551
+ undefined,
2552
+ )
2553
+ expect(built).toMatchObject({ ok: true })
2554
+ if (built.ok) {
2555
+ expect(built.questions.map((question) => question.id)).toEqual(['ready', 'q2'])
2556
+ }
2557
+ })
2558
+
2559
+ it('rejects forms outside the documented shape', () => {
2560
+ expect(buildQuestions({ form: 'not json' }, undefined)).toMatchObject({ ok: false })
2561
+ expect(
2562
+ buildQuestions({ form: JSON.stringify({ questions: [] }) }, undefined),
2563
+ ).toMatchObject({ ok: false })
2564
+ expect(
2565
+ buildQuestions(
2566
+ { form: JSON.stringify({ questions: Array.from({ length: 5 }, (_, i) => ({ text: `Q${i}?` })) }) },
2567
+ undefined,
2568
+ ),
2569
+ ).toMatchObject({ ok: false, error: expect.stringContaining('1-4') })
2570
+ expect(
2571
+ buildQuestions(
2572
+ { form: JSON.stringify({ questions: [{ text: 'Pick?', multi: true }] }) },
2573
+ undefined,
2574
+ ),
2575
+ ).toMatchObject({ ok: false, error: expect.stringContaining('--choice') })
2576
+ // --form replaces the flag surface; mixing them is a usage error.
2577
+ expect(
2578
+ buildQuestions({ form: JSON.stringify({ questions: [{ text: 'Q?' }] }) }, 'Also a question?'),
2579
+ ).toMatchObject({ ok: false })
2580
+ })
2581
+ })
2582
+
2583
+ /** Latest-reply-wins, and never silently: a correction is named as one. */
2490
2584
  describe('a second device that disagrees', () => {
2491
2585
  function view(overrides: Partial<ReplyView>): ReplyView {
2492
2586
  return {
@@ -2496,27 +2590,28 @@ describe('a second device that disagrees', () => {
2496
2590
  device_id: 'dev',
2497
2591
  device_name: 'iPhone',
2498
2592
  text: 'Yes',
2499
- choice_id: null,
2593
+ answers: [],
2594
+ source: null,
2500
2595
  created_at: new Date().toISOString(),
2501
2596
  ...overrides,
2502
2597
  }
2503
2598
  }
2504
2599
 
2505
- it('says which answer counted and which was discarded', () => {
2600
+ it('says the latest answer counted and which were superseded', () => {
2506
2601
  const said = contradictingAnswer([
2507
2602
  view({ seq: 1, device_name: 'iPhone', text: 'Yes' }),
2508
2603
  view({ seq: 2, device_name: 'FurankuMac', text: 'No' }),
2509
2604
  ])
2510
- expect(said).toContain('"Yes" from iPhone')
2511
- expect(said).toContain('FurankuMac')
2512
- expect(said).toMatch(/arrived first/)
2605
+ expect(said).toContain('"No" from FurankuMac')
2606
+ expect(said).toContain('iPhone')
2607
+ expect(said).toMatch(/corrects an earlier one/)
2513
2608
  })
2514
2609
 
2515
2610
  it('is silent when the second answer agrees', () => {
2516
2611
  expect(
2517
2612
  contradictingAnswer([
2518
- view({ seq: 1, device_name: 'iPhone', text: 'Ship it', choice_id: 'ship' }),
2519
- view({ seq: 2, device_name: 'FurankuMac', text: 'Ship it', choice_id: 'ship' }),
2613
+ view({ seq: 1, device_name: 'iPhone', text: 'Ship it', answers: [{ question_id: 'q1', choice_ids: ['ship'], text: null }] }),
2614
+ view({ seq: 2, device_name: 'FurankuMac', text: 'Ship it', answers: [{ question_id: 'q1', choice_ids: ['ship'], text: null }] }),
2520
2615
  ]),
2521
2616
  ).toBeNull()
2522
2617
  })
package/src/commands.ts CHANGED
@@ -6,12 +6,15 @@ import path from 'node:path'
6
6
  import { parse as parseToml, stringify as stringifyToml } from 'smol-toml'
7
7
  import {
8
8
  CAPABILITIES_V1,
9
+ QUESTION_TEXT_MAX_LENGTH,
10
+ REPLY_MAX_QUESTIONS,
9
11
  REPLY_MAX_WINDOW_SECONDS,
10
12
  validateDraft,
11
13
  type EvidenceSnapshot,
12
14
  type AccountAccessResponse,
13
15
  type ListRepliesResponse,
14
16
  type Platform,
17
+ type QuestionT,
15
18
  type ReplyView,
16
19
  type RoutableDevice,
17
20
  type SubmissionReceipt,
@@ -82,11 +85,11 @@ import {
82
85
  } from './opencode-plugin.js'
83
86
  import {
84
87
  CHOICE_USAGE,
85
- ambiguousChoiceSplit,
86
88
  buildDraft,
87
89
  formatReceipt,
88
90
  parseChoices,
89
91
  receiptExitCode,
92
+ slugify,
90
93
  type SendFlags,
91
94
  } from './send.js'
92
95
  import type { NativeSkill, NativeSkills, SkillScope } from './native-skills.js'
@@ -770,28 +773,27 @@ function printReplies(deps: CommandDeps, replies: ReplyView[]): void {
770
773
  }
771
774
 
772
775
  /**
773
- * The other half of first-reply-wins.
776
+ * The correction note for a wait that saw more than one answer.
774
777
  *
775
- * A blocking send returns the first non-empty batch and acts on it, so a later
776
- * answer that disagrees is silently ignored. That is the right default — an
777
- * agent wants one answer, not a quorum — but silently is the wrong way to do
778
- * it. The server now retires the question on the other devices the moment one
779
- * answers, which makes this rare; it is still reachable, because a device can
780
- * answer between the first reply landing and its retirement arriving.
778
+ * A later reply that conflicts with an earlier one is a correction, so the
779
+ * latest answer is the one that counts. The server retires the question on
780
+ * the other devices the moment one answers, which makes this rare; it is
781
+ * still reachable, because a device can answer between the first reply
782
+ * landing and its retirement arriving.
781
783
  *
782
- * Ordered by seq, so `replies[0]` is the one that won.
784
+ * Ordered by seq, so `replies.at(-1)` is the answer to act on.
783
785
  */
784
786
  export function contradictingAnswer(replies: ReplyView[]): string | null {
785
- const winner = replies[0]
787
+ const winner = replies.at(-1)
786
788
  if (winner === undefined || replies.length < 2) return null
787
- const disagreeing = replies
788
- .slice(1)
789
- .filter((reply) => (reply.choice_id ?? reply.text) !== (winner.choice_id ?? winner.text))
790
- if (disagreeing.length === 0) return null
791
- const names = [...new Set(disagreeing.map((reply) => reply.device_name))].join(', ')
789
+ const key = (reply: ReplyView) =>
790
+ reply.answers.length > 0 ? JSON.stringify(reply.answers) : reply.text
791
+ const corrected = replies.slice(0, -1).filter((reply) => key(reply) !== key(winner))
792
+ if (corrected.length === 0) return null
793
+ const names = [...new Set(corrected.map((reply) => reply.device_name))].join(', ')
792
794
  return (
793
- `note: "${winner.text}" from ${winner.device_name} is the answer that counts — it arrived first. ` +
794
- `${names} answered differently afterwards and that answer was not used.`
795
+ `note: "${winner.text}" from ${winner.device_name} is the answer that counts — a later ` +
796
+ `answer corrects an earlier one. Earlier differing answers from ${names} were superseded.`
795
797
  )
796
798
  }
797
799
 
@@ -1096,17 +1098,119 @@ function rejectedPaths(details: unknown): string[] {
1096
1098
  }
1097
1099
 
1098
1100
  export interface AskFlags {
1099
- choice?: string | string[]
1101
+ choice?: string[]
1102
+ /** The single question is multi-select: several answers may be chosen. */
1103
+ multi?: boolean
1104
+ /** Long-form markdown context; shown in the app, never on the banner. */
1105
+ detail?: string
1106
+ /** Raw JSON for a multi-question form; replaces the positional question. */
1107
+ form?: string
1100
1108
  session?: string
1101
1109
  }
1102
1110
 
1111
+ /** The `--form` document: what an agent writes to ask several things at once. */
1112
+ interface AskFormQuestion {
1113
+ text: string
1114
+ choices?: string[]
1115
+ multi?: boolean
1116
+ }
1117
+
1118
+ /**
1119
+ * Turn ask input into the question set that will ride the push. Everything is
1120
+ * validated here, at registration, because the push happens inside a hook
1121
+ * where a rejection becomes a stderr note the agent never reads.
1122
+ */
1123
+ export function buildQuestions(
1124
+ flags: AskFlags,
1125
+ question: string | undefined,
1126
+ ): { ok: true; questions: QuestionT[]; detail?: string } | { ok: false; error: string } {
1127
+ if (flags.form !== undefined) {
1128
+ if (question !== undefined || flags.choice?.length || flags.multi) {
1129
+ return { ok: false, error: '--form replaces the positional question, --choice, and --multi.' }
1130
+ }
1131
+ let parsed: unknown
1132
+ try {
1133
+ parsed = JSON.parse(flags.form)
1134
+ } catch {
1135
+ return { ok: false, error: '--form must be JSON: {"questions": [{"text", "choices"?, "multi"?}], "detail"?}.' }
1136
+ }
1137
+ if (typeof parsed !== 'object' || parsed === null || !Array.isArray((parsed as { questions?: unknown }).questions)) {
1138
+ return { ok: false, error: '--form needs a "questions" array (1-4 entries).' }
1139
+ }
1140
+ const form = parsed as { questions: unknown[]; detail?: unknown }
1141
+ if (form.questions.length < 1 || form.questions.length > REPLY_MAX_QUESTIONS) {
1142
+ return { ok: false, error: `A form asks 1-${REPLY_MAX_QUESTIONS} questions; this one has ${form.questions.length}.` }
1143
+ }
1144
+ if (form.detail !== undefined && typeof form.detail !== 'string') {
1145
+ return { ok: false, error: '"detail" must be a markdown string.' }
1146
+ }
1147
+ const questions: QuestionT[] = []
1148
+ const usedIds = new Set<string>()
1149
+ for (const [index, entry] of form.questions.entries()) {
1150
+ if (typeof entry !== 'object' || entry === null || typeof (entry as AskFormQuestion).text !== 'string') {
1151
+ return { ok: false, error: `Question ${index + 1} needs a "text" string.` }
1152
+ }
1153
+ const spec = entry as AskFormQuestion
1154
+ const built = buildOneQuestion(spec.text, spec.choices, spec.multi === true, index, usedIds)
1155
+ if ('error' in built) return { ok: false, error: `Question ${index + 1}: ${built.error}` }
1156
+ questions.push(built.question)
1157
+ }
1158
+ return { ok: true, questions, ...(form.detail !== undefined ? { detail: form.detail } : {}) }
1159
+ }
1160
+
1161
+ if (question === undefined || question.trim() === '') {
1162
+ return { ok: false, error: 'The question cannot be empty.' }
1163
+ }
1164
+ const built = buildOneQuestion(question, flags.choice, flags.multi === true, 0, new Set())
1165
+ if ('error' in built) return { ok: false, error: built.error }
1166
+ return {
1167
+ ok: true,
1168
+ questions: [built.question],
1169
+ ...(flags.detail !== undefined && flags.detail.trim() !== '' ? { detail: flags.detail } : {}),
1170
+ }
1171
+ }
1172
+
1173
+ function buildOneQuestion(
1174
+ text: string,
1175
+ choiceLabels: string[] | undefined,
1176
+ multi: boolean,
1177
+ index: number,
1178
+ usedIds: Set<string>,
1179
+ ): { question: QuestionT } | { error: string } {
1180
+ const trimmed = text.trim()
1181
+ if (trimmed === '') return { error: 'the question text cannot be empty.' }
1182
+ if (trimmed.length > QUESTION_TEXT_MAX_LENGTH) {
1183
+ return {
1184
+ error:
1185
+ `a question must be readable where it is answered: keep it within ` +
1186
+ `${QUESTION_TEXT_MAX_LENGTH} characters and put the longer context in detail.`,
1187
+ }
1188
+ }
1189
+ const choices = parseChoices(choiceLabels)
1190
+ if (choices === 'invalid') return { error: CHOICE_USAGE }
1191
+ if (multi && choices === null) {
1192
+ return { error: '--multi needs answers to select between; add --choice.' }
1193
+ }
1194
+ let id = slugify(trimmed)
1195
+ if (id === '' || usedIds.has(id)) id = `q${index + 1}`
1196
+ usedIds.add(id)
1197
+ return {
1198
+ question: {
1199
+ id,
1200
+ text: trimmed,
1201
+ ...(choices !== null ? { choices } : {}),
1202
+ ...(multi ? { multi: true } : {}),
1203
+ },
1204
+ }
1205
+ }
1206
+
1103
1207
  /**
1104
1208
  * Registers a question for turn-end routing under the user's presence config.
1105
1209
  * Returns immediately so the agent can ask in prose and end its turn. With the
1106
1210
  * default presence gate, recent keyboard or mouse activity keeps it local;
1107
1211
  * `require_idle = false` intentionally permits a push while the user is active.
1108
1212
  */
1109
- export function askCommand(deps: CommandDeps, question: string, flags: AskFlags): number {
1213
+ export function askCommand(deps: CommandDeps, question: string | undefined, flags: AskFlags): number {
1110
1214
  // An agent calling this gets no hook payload. Harness-native environment
1111
1215
  // markers identify the active owner, while the UserPromptSubmit hook leaves
1112
1216
  // a pointer keyed on the project directory with the hook's canonical id.
@@ -1155,16 +1259,12 @@ export function askCommand(deps: CommandDeps, question: string, flags: AskFlags)
1155
1259
  for (const line of diagnoseMissingSession(deps)) deps.io.err(line)
1156
1260
  return EXIT.usage
1157
1261
  }
1158
- if (question.trim() === '') {
1159
- deps.io.err('The question cannot be empty.')
1160
- return EXIT.usage
1161
- }
1162
1262
  // Validate here, not at push time. The push happens inside a hook, where a
1163
1263
  // rejection becomes a stderr note the agent never reads — so a malformed
1164
- // choice set would look like it registered fine and then silently never ask.
1165
- const choices = parseChoices(flags.choice)
1166
- if (choices === 'invalid') {
1167
- deps.io.err(CHOICE_USAGE)
1264
+ // question set would look like it registered fine and then silently never ask.
1265
+ const built = buildQuestions(flags, question)
1266
+ if (!built.ok) {
1267
+ deps.io.err(built.error)
1168
1268
  return EXIT.usage
1169
1269
  }
1170
1270
  try {
@@ -1172,8 +1272,9 @@ export function askCommand(deps: CommandDeps, question: string, flags: AskFlags)
1172
1272
  sessionId,
1173
1273
  deps.env,
1174
1274
  {
1175
- question: question.trim(),
1176
- ...(choices !== null ? { choices: choices.map((choice) => choice.label) } : {}),
1275
+ question: built.questions[0]!.text,
1276
+ questions: built.questions,
1277
+ ...(built.detail !== undefined ? { detail: built.detail } : {}),
1177
1278
  },
1178
1279
  (deps.now ?? Date.now)(),
1179
1280
  )
@@ -1181,14 +1282,20 @@ export function askCommand(deps: CommandDeps, question: string, flags: AskFlags)
1181
1282
  deps.io.err(`Could not register the question: ${err instanceof Error ? err.message : String(err)}`)
1182
1283
  return EXIT.failed
1183
1284
  }
1184
- if (choices !== null) {
1185
- deps.io.out(`Answers offered: ${choices.map((choice) => choice.label).join(' / ')}`)
1285
+ for (const [index, entry] of built.questions.entries()) {
1286
+ const prefix = built.questions.length > 1 ? `${index + 1}. ` : ''
1287
+ if (entry.choices !== undefined) {
1288
+ const kind = entry.multi === true ? 'answers offered (several may be chosen)' : 'answers offered'
1289
+ deps.io.out(`${prefix}${entry.text} — ${kind}: ${entry.choices.map((choice) => choice.label).join(' / ')}`)
1290
+ } else if (built.questions.length > 1) {
1291
+ deps.io.out(`${prefix}${entry.text} — free text`)
1292
+ }
1186
1293
  }
1187
- // On stdout, not stderr: the agent reads the former and this is only useful
1188
- // if it is read while there is still time to fix the question.
1189
- const ambiguous = ambiguousChoiceSplit(flags.choice)
1190
- if (ambiguous !== null) deps.io.out(ambiguous)
1191
- deps.io.out('Question registered. Ask it in the conversation as usual and end your turn.')
1294
+ deps.io.out(
1295
+ built.questions.length > 1
1296
+ ? `${built.questions.length} questions registered as one form. Ask them in the conversation as usual and end your turn.`
1297
+ : 'Question registered. Ask it in the conversation as usual and end your turn.',
1298
+ )
1192
1299
  return EXIT.ok
1193
1300
  }
1194
1301
 
@@ -1729,7 +1836,7 @@ export async function configSetCommand(
1729
1836
  * 1.5.x, `owner/repo@name` selects a skill; a Git ref belongs after `#`.
1730
1837
  * Keep this immutable and public because the command is printed to users.
1731
1838
  */
1732
- export const SKILLS_SOURCE = 'Raidiant-io/notifai#v0.1.8'
1839
+ export const SKILLS_SOURCE = 'Raidiant-io/notifai#v0.2.0'
1733
1840
 
1734
1841
  function skillSourceParts(): { source: string; ref: string } | null {
1735
1842
  const match = /^([^#]+)#(.+)$/.exec(SKILLS_SOURCE)