create-claude-cabinet 0.34.1 → 0.36.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.34.1",
3
+ "version": "0.36.0",
4
4
  "description": "Claude Cabinet — opinionated process scaffolding for Claude Code projects",
5
5
  "bin": {
6
6
  "create-claude-cabinet": "bin/create-claude-cabinet.js"
@@ -40,6 +40,13 @@ slash commands for CC maintenance.
40
40
 
41
41
  These hold for every SKILL.md regardless of type.
42
42
 
43
+ **Runtime output formatting lives in a sibling doc.** This doc covers
44
+ *write-time* authoring. For *runtime* user-facing output — when a running
45
+ skill should present choices via the native `AskUserQuestion` tool versus
46
+ prose versus a markdown table, plus the AskUserQuestion schema facts
47
+ authors must get right — see `skill-output-conventions.md`. Follow that
48
+ pointer whenever a skill prompts the user to make a decision.
49
+
43
50
  ### Body under 500 lines
44
51
 
45
52
  The SKILL.md **body** — content after the closing `---` of
@@ -0,0 +1,153 @@
1
+ # Skill Output Conventions
2
+
3
+ How skills format **user-facing output** and **interactive prompts** at
4
+ runtime. The companion to `skill-best-practices.md`.
5
+
6
+ ## 1. Purpose & Scope
7
+
8
+ `skill-best-practices.md` governs **write-time** authoring rules — file
9
+ structure, naming, frontmatter, length. This doc governs **runtime**
10
+ interaction: how a skill, while executing, presents choices and
11
+ information to the user. `output-contract.md` is a third, separate
12
+ concern — the JSON cabinet members emit during audits, not user-facing
13
+ output.
14
+
15
+ Stating the boundary keeps the three docs from drifting together: if a
16
+ rule is about *how a SKILL.md is written*, it belongs in
17
+ skill-best-practices; if it's about *what the running skill shows the
18
+ user*, it belongs here.
19
+
20
+ ## 2. When to Use AskUserQuestion
21
+
22
+ `AskUserQuestion` is the native interactive primitive — a structured
23
+ menu of 2-4 options the user picks from. Use it when **all** of these
24
+ hold:
25
+
26
+ - The choice is among **discrete options** (not free text).
27
+ - The answer **branches the skill's behavior** (it's a real decision).
28
+ - There are **2-4 mutually-exclusive choices** (or a small set if
29
+ multiSelect).
30
+ - It is **not an obvious-default confirm** — a yes/no where one answer
31
+ is plainly expected (see §5).
32
+ - The item count is **bounded and small** — not a variable-length loop
33
+ (see §4).
34
+
35
+ If any answer is "no," use prose (§5) or a table (§6) instead.
36
+
37
+ ## 3. Schema Facts Authors Must Get Right
38
+
39
+ Verified against the live schema. Get these wrong and the call fails or
40
+ behaves unexpectedly.
41
+
42
+ - **1-4 questions per call; 2-4 options per question.** Outside this
43
+ range fails validation.
44
+ - **`header` is REQUIRED** — a short chip/tag, **max 12 chars** (e.g.
45
+ `Branch`, `Verdict`, `Decision`). Omitting it fails validation. This
46
+ is the most common authoring mistake.
47
+ - **Cost is per-session, not per-call.** AskUserQuestion is a deferred
48
+ tool: the first use in a session loads its schema (one ToolSearch
49
+ round-trip, ~200 tokens); every call after that is free. Don't
50
+ under-use it to "save" a cost that's already paid.
51
+ - **`multiSelect` is required.** `false` for mutually-exclusive choices
52
+ (the norm — one answer). `true` only when the user may legitimately
53
+ pick more than one.
54
+ - **"Other" is auto-added** by the harness. Never add an "Other" /
55
+ "Something else" option manually — it duplicates.
56
+ - **No reliable preview/comparison field.** The official schema has no
57
+ dependable preview surface; don't build conventions on preview
58
+ behavior or expect side-by-side rendering.
59
+ - **Unavailable in Task-spawned subagents.** Agents launched via the
60
+ Task tool (execute-group worktree agents, Workflow agents,
61
+ `context:fork`) cannot call AskUserQuestion — they must use prose.
62
+ This warning outlives any current wiring: a skill that runs in the
63
+ main session today may be called from a subagent tomorrow.
64
+ - **Pre-specify each call site** in the SKILL.md prose — exact `header`,
65
+ `question`, and option labels/descriptions. In a long skill body the
66
+ model otherwise drifts to a prose question instead of emitting the
67
+ structured call. (This is an authoring practice — `skill-best-practices.md`
68
+ is the write-time home for it — surfaced here because it's what makes a
69
+ runtime AskUserQuestion call fire reliably.)
70
+
71
+ ## 4. Bounded-List Caveat
72
+
73
+ AskUserQuestion is for **decisions**, not **batch processing**. A
74
+ variable-length loop of homogeneous items — reviewing 12 copy edits,
75
+ approving 30 line items — should not become 30 sequential dialogs. Past
76
+ ~5 items, prefer a single batch path (present all, take one combined
77
+ response) over N prompts. The fatigue of N near-identical dialogs
78
+ outweighs the structure they add.
79
+
80
+ The test: are these **genuine, distinct decisions** (→ AskUserQuestion,
81
+ one per item up to a small cap) or **homogeneous review items** (→ batch
82
+ path)?
83
+
84
+ ## 5. When to Use Prose
85
+
86
+ Use plain conversational prose for:
87
+
88
+ - **Free-text gathering** — names, reasons, descriptions, anything not
89
+ a fixed menu.
90
+ - **Clarifications and reasons** — "why did you defer this?"
91
+ - **Obvious-default confirms** — a yes/no where one answer is plainly
92
+ expected and the other is rare. A structured menu over-formalizes it;
93
+ a prose "Want me to X? (I'll skip if not)" is lighter.
94
+
95
+ ## 6. When to Use Markdown Tables
96
+
97
+ Use a table for **rosters, status, or any data with ≥2 columns** —
98
+ where the value is in *comparing rows*, not *choosing one*. The `/menu`
99
+ skill (skills × description) and `/cabinet` (member × domain) are
100
+ canonical examples. A table presents; AskUserQuestion decides. Don't use
101
+ a table to offer a choice, and don't use AskUserQuestion to display a
102
+ list the user isn't picking from.
103
+
104
+ ## 7. Option-Block Format
105
+
106
+ When presenting options as prose (subagent context, or >4 options), the
107
+ canonical format is in `templates/skills/onboard/phases/options.md` —
108
+ name, "what it is," "good for," "trade-off" per option. Do not duplicate
109
+ that format here; reference it.
110
+
111
+ ## 8. Section Structure & Tone
112
+
113
+ `options.md` is also canonical for tone: **present, don't prescribe.**
114
+ "Here are your options" not "I recommend X." Ground choices in the
115
+ user's actual situation. This applies whether the choice is rendered via
116
+ AskUserQuestion or prose — the primitive changes, the posture doesn't.
117
+
118
+ ## 9. Calibration Examples
119
+
120
+ **Before/after — engagement decision items** (the Tier 1 conversion):
121
+
122
+ > Before: "Present the options conversationally and let the client
123
+ > choose."
124
+ > After: For each decision item, one AskUserQuestion call — `header:
125
+ > "Decision"`, `question:` the item's client-facing prompt, `options:`
126
+ > the item's own authored option list, `multiSelect: false`. One call
127
+ > per item; never batched.
128
+
129
+ Why: the items already carry discrete, consultant-authored options, and
130
+ this is the consultant↔client boundary where ambiguity is most expensive
131
+ and the client can't ask for real-time clarification.
132
+
133
+ **Before/after — triage-audit fallback verdict:**
134
+
135
+ > Before: a prose prompt listing "Fix / Defer / Reject / Question" and
136
+ > asking the user to type one.
137
+ > After: AskUserQuestion — `header: "Verdict"`, four options, one per
138
+ > finding, `multiSelect: false`.
139
+
140
+ Why: four discrete mutually-exclusive verdicts that branch behavior —
141
+ this scores well on all five §2 criteria.
142
+
143
+ **EXCLUDED — orient registry-orphan prompt** (do NOT convert):
144
+
145
+ > "Your old project 'deal-v1' seems to have been deleted — want me to
146
+ > remove it from the registry?"
147
+
148
+ This looks like a Remove/Keep choice but is an **obvious-default
149
+ confirm**: the path no longer exists, so removal is plainly expected and
150
+ "keep" is the rare exception. A structured two-option menu over-formalizes
151
+ a routine yes/no. Leave it prose. This is the boundary that stops
152
+ AskUserQuestion from being applied to every binary question in the
153
+ codebase.
@@ -187,6 +187,14 @@ One or more "why this matters" lines for the client.
187
187
  blocks dispatch until it is supplied (the internal `text` is never
188
188
  shown to a client verbatim).
189
189
 
190
+ **Literal `\n` normalization.** When notes are written via MCP tools
191
+ that JSON-encode the string, newlines sometimes arrive as literal
192
+ two-character `\n` sequences (backslash + n) instead of real newlines.
193
+ `parseClientCopy` normalizes these before parsing — titles won't contain
194
+ stray `\n` characters. Status suffixes like "— complete" or "— shipped"
195
+ are also stripped from titles since the bucket placement already
196
+ communicates status.
197
+
190
198
  The copy block may be human-written or generated-then-approved.
191
199
  `/engagement-sync` step 3.5 can auto-draft copy from structured action
192
200
  metadata (via `buildGenerationSource`), with the consultant reviewing
@@ -240,6 +248,9 @@ transport:
240
248
  type: email # the CHANNEL (email | mcp | file)
241
249
  consultant: "oren@example.com" # where client feedback is sent
242
250
 
251
+ engagement_notes: # persistent notes shown at top of every packet
252
+ - "All email logic is built and tested on staging — real delivery activates once Postmark is configured."
253
+
243
254
  sections: # the credential checklist = one section
244
255
  - key: go_live
245
256
  title: "Go-Live Credentials"
@@ -274,8 +285,10 @@ A **packet** is a per-recipient projection of engagement state. Fields:
274
285
  engagement, packet_id, recipient, role, generated_at,
275
286
  needs_you: [ { ref, title, why, kind, options } ],
276
287
  in_flight, shipped, delegated,
277
- billing, # present ONLY if roles[role].billing === true
278
- messages, new_since_last, generated_label
288
+ billing, # present ONLY if roles[role].billing === true
289
+ messages, new_since_last, generated_label,
290
+ engagement_notes, # persistent consultant-authored notes (from config)
291
+ checklist_summary # [ { type, count } ] when engagement.yaml has sections
279
292
  ```
280
293
 
281
294
  - The client **never** sees `action_fid` — items are referenced by an
@@ -306,13 +319,15 @@ The pure render engine (`engagement.mjs`, imports nothing — the adapter
306
319
  boundary) pins these behaviors so Phase 3/4 can build on them without
307
320
  reopening Phase 1:
308
321
 
309
- - **`renderPacket` returns `{ packet, refmap, notClientReady }`.**
322
+ - **`renderPacket` returns `{ packet, refmap, notClientReady, allDropped }`.**
310
323
  `packet` is the client projection, built without `action_fid`/`_refmap`
311
324
  ever added (leaking is impossible by construction, not by a strip step).
312
325
  `refmap` is the consultant-side `ref → action_fid` map. The caller writes
313
326
  `{ ...packet, _refmap: refmap }` to `<recipient>-sent.json` and delivers
314
327
  `packet` alone. `notClientReady` lists fids of client-visible actions
315
- dropped because they lacked a client-facing copy block.
328
+ dropped because they lacked a client-facing copy block. `allDropped` is
329
+ `true` when ALL visible items were dropped (likely a missing-notes-column
330
+ query bug) — the caller should halt dispatch, not send an empty packet.
316
331
  - **Not-client-ready items are dropped, never rendered with internal text.**
317
332
  A `client-visible` action with no `<!-- client-facing -->` block is
318
333
  excluded from the packet (and reported in `notClientReady`) — the engine
@@ -16,6 +16,9 @@
16
16
  const KNOWN_TAG_PREFIXES = ['audience', 'scope', 'assignee', 'needs'];
17
17
  const KNOWN_BARE_TAGS = ['client-visible'];
18
18
 
19
+ // Trailing status phrases that are redundant with bucket placement.
20
+ const STATUS_SUFFIX_RE = /\s*[—–-]\s*(?:complete|completed|shipped|done|planned|in progress|started|delivered)\s*(?:\(.*\))?\s*$/i;
21
+
19
22
  // pib-db status → plain English shown to clients (never the raw enum).
20
23
  const STATUS_LABELS = {
21
24
  open: 'Not started',
@@ -82,8 +85,9 @@ export function loadEngagement(config) {
82
85
 
83
86
  const transport = config.transport || { type: 'email', consultant: null };
84
87
  const sections = Array.isArray(config.sections) ? config.sections : [];
88
+ const engagement_notes = config.engagement_notes || null;
85
89
 
86
- return { engagement, billing, recipients, roles, transport, sections };
90
+ return { engagement, billing, recipients, roles, transport, sections, engagement_notes };
87
91
  }
88
92
 
89
93
  export function normalizeRecipient(r) {
@@ -171,8 +175,11 @@ export function parseClientCopy(notes) {
171
175
  const close = notes.indexOf('-->', bodyStart);
172
176
  if (close === -1) return null;
173
177
 
174
- const inner = notes.slice(bodyStart, close).trim();
175
- if (!inner) return null;
178
+ const raw = notes.slice(bodyStart, close).trim();
179
+ if (!raw) return null;
180
+ // Normalize literal two-char \n sequences (from double-escaping during
181
+ // JSON-encoded MCP writes) to real newlines before splitting.
182
+ const inner = raw.replace(/\\n/g, '\n');
176
183
  const lines = inner.split('\n').map(l => l.trim()).filter(Boolean);
177
184
  if (lines.length === 0) return null;
178
185
 
@@ -361,7 +368,8 @@ export function renderPacket(config, actions, events, billing, recipient, prevPa
361
368
  throw new Error(`renderPacket: stableRef collision — ${refmap[ref]} and ${a.fid} both hash to ${ref}`);
362
369
  }
363
370
  refmap[ref] = a.fid;
364
- const item = { ref, title: copy.client_title, why: copy.client_why };
371
+ const title = copy.client_title.replace(STATUS_SUFFIX_RE, '');
372
+ const item = { ref, title, why: copy.client_why };
365
373
 
366
374
  if (a.completed || a.status === 'done') {
367
375
  // Completed wins over needs: a done item is never shown as pending.
@@ -408,7 +416,8 @@ export function renderPacket(config, actions, events, billing, recipient, prevPa
408
416
  }
409
417
  // Construction-not-strip: explicit field listing only. Never spread from
410
418
  // project row. fid/project_fid/name/notes/tags are never assigned.
411
- const rollupItem = { title: copy.client_title, why: copy.client_why, status: rollupStatus, feedbackable: false };
419
+ const rollupTitle = copy.client_title.replace(STATUS_SUFFIX_RE, '');
420
+ const rollupItem = { title: rollupTitle, why: copy.client_why, status: rollupStatus, feedbackable: false };
412
421
  const parsedProjectTags = parseActionTags(p.tags);
413
422
  if (p.status === 'done' || (children.length > 0 && children.every(c => c.completed || c.status === 'done'))) {
414
423
  shipped.push(rollupItem);
@@ -419,6 +428,29 @@ export function renderPacket(config, actions, events, billing, recipient, prevPa
419
428
  }
420
429
  }
421
430
 
431
+ // Silent-empty signal: if there were visible actions but ALL of them landed
432
+ // in notClientReady, the caller likely used a query that omits the notes
433
+ // column (e.g. pib_list_actions instead of pib_query). Signal so the
434
+ // caller can halt dispatch instead of silently sending an empty packet.
435
+ const allDropped = visible.length > 0 && notClientReady.length === visible.length;
436
+
437
+ // Checklist summary: sections parsed from engagement.yaml but NOT rendered as
438
+ // bucket items (those are interactive via /engagement). Include a summary so
439
+ // the preview shows checklist coverage.
440
+ const sections = config.sections || [];
441
+ const checklistSummary = sections.length > 0
442
+ ? sections.map(s => {
443
+ const items = Array.isArray(s.items) ? s.items : [];
444
+ return { type: s.type || s.label || 'unknown', count: items.length };
445
+ })
446
+ : null;
447
+
448
+ // Engagement-wide notes: persistent messages from the consultant that appear
449
+ // at the top of every packet (e.g. caveats, disclaimers, contextual notes).
450
+ const engagementNotes = Array.isArray(config.engagement_notes) ? config.engagement_notes
451
+ : typeof config.engagement_notes === 'string' ? [config.engagement_notes]
452
+ : null;
453
+
422
454
  // Messages: engagement-level events (target_fid null) plus events whose
423
455
  // target action is still live. Events for soft-deleted / unknown actions
424
456
  // are dropped (defensive — even if the caller passed them through).
@@ -452,6 +484,9 @@ export function renderPacket(config, actions, events, billing, recipient, prevPa
452
484
  new_since_last,
453
485
  };
454
486
 
487
+ if (engagementNotes) packet.engagement_notes = engagementNotes;
488
+ if (checklistSummary) packet.checklist_summary = checklistSummary;
489
+
455
490
  // billing only for roles configured to receive it, and only when billing is
456
491
  // actually enabled (a config.billing with enabled:false must not produce a
457
492
  // "$0.00" section; a computed renderBilling block has no `enabled` field, so
@@ -465,7 +500,7 @@ export function renderPacket(config, actions, events, billing, recipient, prevPa
465
500
  // roles, defined role). Defense-in-depth so the dispatch path can't skip it.
466
501
  assertPacketInvariants(packet, recipient, roles);
467
502
 
468
- return { packet, refmap, notClientReady };
503
+ return { packet, refmap, notClientReady, allDropped };
469
504
  }
470
505
 
471
506
  function extractOptions(action) {
@@ -321,6 +321,20 @@ information-design's mock output. This should be sequential: designer
321
321
  produces mock, then usability critiques the interaction model using the
322
322
  mock as input."
323
323
 
324
+ **Good (output conventions — prose where AskUserQuestion fits):** "/triage-audit's
325
+ CLI-fallback path prompts the user to type one of Fix / Defer / Reject /
326
+ Question. These are four discrete, mutually-exclusive verdicts that branch
327
+ behavior — the canonical case for the native `AskUserQuestion` tool per
328
+ `skill-output-conventions.md` §2. Rendering them as a prose 'type one of…'
329
+ prompt is a guidance-layer drift. Flag as a finding pointing to that doc."
330
+ Do NOT raise this finding for: free-text gathering (names, reasons), open
331
+ clarifying questions, obvious-default confirms (e.g. orient's "remove the
332
+ deleted project from the registry?" — removal is plainly expected, so prose
333
+ is correct), or variable-length per-item loops of homogeneous items (those
334
+ prefer a batch path, not N dialogs). Match the finding to the doc's §2
335
+ checklist; if any checklist item fails, prose is the right call and there is
336
+ no finding.
337
+
324
338
  **Too narrow (belongs to another cabinet member):** "The deploy script has a
325
339
  race condition." That's technical-debt or architecture territory.
326
340
 
@@ -66,7 +66,14 @@ note before anything else:
66
66
  Always show the `generated_label` ("as of <date>") as a header so
67
67
  staleness is visible even under the threshold.
68
68
 
69
- ### 3. Messages first, then the work
69
+ ### 3. Engagement notes, messages, then work
70
+
71
+ If the packet carries `engagement_notes` (persistent consultant-authored
72
+ notes), render them first in a distinct block before any messages or items.
73
+
74
+ If the packet carries `checklist_summary`, show a single summary line
75
+ after engagement notes: "Plus N checklist items (X decisions, Y credentials,
76
+ ...) — these appear when you run `/engagement` interactively."
70
77
 
71
78
  Render the **messages** feed distinctly from action items — each with an
72
79
  explicit sender and timestamp, in order:
@@ -74,16 +81,23 @@ explicit sender and timestamp, in order:
74
81
  > **Oren · May 30** — "Quick note: the hosting migration is scheduled
75
82
  > for next week."
76
83
 
77
- Then render the work, **needs_you first**:
78
- - **Needs your attention** (`needs_you`) — lead with these.
79
- - **In progress** (`in_flight`).
80
- - **Done** (`shipped`).
84
+ Then render the work with **visual structure**:
85
+
86
+ - **Needs your attention (N)** (`needs_you`) — lead with these. Use a
87
+ clear heading with the count.
88
+ - **In progress (N)** (`in_flight`) — each item gets an arrow indicator.
89
+ - **Done (N)** (`shipped`) — each item gets a checkmark indicator.
81
90
  - **Handled for you** (`delegated`) — place LAST, collapsed to a one-line
82
91
  summary ("Sydney is handling 3 items (marketing)"); offer to expand on
83
92
  request. It's reassurance, not a to-do.
84
93
 
85
- Show `billing` only if the packet carries it (principals only). Highlight
86
- `new_since_last` items by title.
94
+ **Formatting rules:**
95
+ - Every section gets a header with a count: "**Done (12)**" not just a
96
+ bare list.
97
+ - Items in `new_since_last` are marked with "(new)" after the title.
98
+ - Show `billing` only if the packet carries it (principals only). Format
99
+ billing as a clear table (Date | Hours | Work), with rate and total at
100
+ the bottom — not a raw JSON dump.
87
101
 
88
102
  ### 4. Collect responses (respectful batch review)
89
103
 
@@ -109,9 +123,25 @@ in `needs_you` was resolved upstream; don't nag about it.)
109
123
  Then split `needs_you` into two groups:
110
124
 
111
125
  - **Decisions** — items with a populated `options` list (or
112
- `kind === 'decision'`). These are **never** batch-approved. Walk each
113
- one individually: present the options conversationally, let the client
114
- choose. A real decision always gets explicit engagement.
126
+ `kind === 'decision'`). These are **never** batch-approved walk each
127
+ one individually with exactly **one `AskUserQuestion` call per decision
128
+ item**. Never bundle two decisions into a single call. Pre-specify the
129
+ call this way:
130
+ - `header`: `"Decision"` (within the 12-char cap).
131
+ - `question`: the item's client-facing prompt/title.
132
+ - `options`: the item's own `options` list, one labeled choice each.
133
+ **If an item has more than 4 options**, AskUserQuestion can't hold
134
+ them — fall back to presenting that item's options as prose and let
135
+ the client answer in their own words.
136
+ - `multiSelect: false` — a single verdict per decision.
137
+ - Do **not** add an "Other" choice; the harness adds it automatically.
138
+ If the client picks "Other" or says something off-menu, handle it
139
+ conversationally and map to the right verdict (see the table below).
140
+
141
+ A real decision always gets explicit engagement. (AskUserQuestion is
142
+ available here because `/engagement` runs in the main session, not a
143
+ subagent. See `.claude/cabinet/skill-output-conventions.md` for the
144
+ conventions behind this.)
115
145
  - **Approvals / FYIs** — items with no options. These are eligible for
116
146
  the batch default.
117
147
 
@@ -32,8 +32,16 @@ Access pib-db via [pib-db-access.md](../../cabinet/pib-db-access.md).
32
32
  - "What's the item?" (internal `text`)
33
33
  - "Who should see it?" — map to `audience:<id>` (and, for a delegate,
34
34
  `assignee:<id>` — the **sole** basis for a delegate seeing it).
35
- - "Is it a decision the client must make, or a credential they must
36
- provide?" `needs:decision` / `needs:credential` (or neither).
35
+ - Collect item type via `AskUserQuestion`:
36
+ - `header`: `"Item type"`
37
+ - `question`: "What kind of item is this?"
38
+ - `options`:
39
+ - Decision — "Client must choose among options you provide"
40
+ - Credential — "Client must hand over a secret (secure dialog)"
41
+ - Neither — "Informational or approval item, no special handling"
42
+ - `multiSelect: false`
43
+ Map: Decision → tag `needs:decision`. Credential → tag
44
+ `needs:credential`. Neither → no needs tag.
37
45
  - "What scope?" (optional `scope:<scope>`)
38
46
  - To make a **project itself** visible to clients as a roll-up
39
47
  (e.g., "E-signing: Done"), tag the project `client-visible` and author
@@ -50,6 +58,9 @@ Access pib-db via [pib-db-access.md](../../cabinet/pib-db-access.md).
50
58
  - Always include `client-visible` (an item with no `client-visible`
51
59
  tag appears in **no** packet).
52
60
 
61
+ (See `.claude/cabinet/skill-output-conventions.md` for when structured
62
+ choices apply.)
63
+
53
64
  3. **Prompt for the client-facing copy block** (optional — the
54
65
  consultant can write it now or let `/engagement-sync` draft it):
55
66
  - "Want to write the client-facing copy now, or let `/engagement-sync`
@@ -53,14 +53,24 @@ id, name, email, and their role."
53
53
  For each recipient, capture:
54
54
  - **id** — short handle (e.g. `ed`, `sydney`), used in tags
55
55
  - **name**, **email**
56
- - **role** — `principal` (sees everything, gets billing) or `delegate`
57
- (sees only items assigned to them, no billing)
56
+ - **role** — collect via `AskUserQuestion`:
57
+ - `header`: `"Role"`
58
+ - `question`: "What role for [name]?"
59
+ - `options`:
60
+ - Principal — "Sees everything, gets billing, defaults to interactive delivery"
61
+ - Delegate — "Sees only assigned items, no billing, defaults to email"
62
+ - `multiSelect: false`
58
63
  - **scopes** — topical areas they care about (e.g. `[marketing]`); a
59
64
  principal is usually `[all]`
60
- - **has_claude_code** — "Does [name] have Claude Code installed? If yes:
61
- they get the interactive `/engagement` view with rich status updates.
62
- If no: they get a formatted email summary instead. (Principals default
63
- to yes, delegates default to no.)"
65
+ - **has_claude_code** — collect via `AskUserQuestion`:
66
+ - `header`: `"Delivery"`
67
+ - `question`: "Does [name] have Claude Code installed?"
68
+ - `options`:
69
+ - Yes — "Interactive /engagement view with rich status"
70
+ - No — "Formatted email summary instead"
71
+ - `multiSelect: false`
72
+ (Principals default to yes, delegates default to no — use as the
73
+ pre-selected suggestion in the question phrasing.)
64
74
 
65
75
  The default `roles` block is:
66
76
  ```yaml
@@ -86,8 +96,17 @@ confirmation before proceeding.
86
96
  "Do you bill time on this engagement?" If yes:
87
97
  - **timelog** path (default `./timelog.md`)
88
98
  - **rate** (default `190`), **currency** (default `USD`)
89
- - **period** — `engagement` (running total), `month`, or
90
- `since-last-sync` (only since the last packet) — default `engagement`
99
+ - **period** — collect via `AskUserQuestion`:
100
+ - `header`: `"Billing"`
101
+ - `question`: "How should billing be calculated?"
102
+ - `options`:
103
+ - Running total — "Cumulative from engagement start (default)"
104
+ - Monthly — "Reset each calendar month"
105
+ - Since last sync — "Only time since the last packet was sent"
106
+ - `multiSelect: false`
107
+
108
+ (See `.claude/cabinet/skill-output-conventions.md` for when structured
109
+ choices apply.)
91
110
 
92
111
  Billing appears only in `principal`-role packets.
93
112
 
@@ -150,15 +150,33 @@ For every `client-visible` action or project **lacking** a `<!-- client-facing -
150
150
  summary: "N items need new copy (no block). M items have stale copy
151
151
  (status changed). Total: N+M items to review."
152
152
 
153
- **Batch option for large sets.** If more than 5 items need review, offer:
154
- "Review each individually, or review as a batch? (Batch shows all drafts
155
- at once — you confirm, edit by number, or skip any.)"
153
+ **Batch option for large sets.** If more than 5 items need review,
154
+ collect the choice via `AskUserQuestion`:
155
+ - `header`: `"Review"`
156
+ - `question`: "N items need copy review. How to proceed?"
157
+ - `options`:
158
+ - Individual — "One at a time, decide each separately"
159
+ - Batch — "Show all drafts at once, confirm/edit by number"
160
+ - `multiSelect: false`
156
161
 
157
162
  **Skip if block exists.** An existing block (hand-written or previously approved) is authoritative. Never overwrite it. Check: `parseClientCopy(notes) !== null → skip`.
158
163
 
159
164
  **Draft from structured data.** Call `buildGenerationSource(item)` (from `engagement.mjs`) to get the structured metadata — status, completion, needs, options. For projects, pre-compute `child_summary` via `computeChildSummary(actions, project.fid)` and attach it to the project object before calling. Claude drafts a `client_title` + `client_why` from this metadata. The internal `text` field is NEVER an input to generation.
160
165
 
161
- **Per-item review loop (not batch approve).** For each generated draft, show the consultant: the structured source data and the generated title + why. Consultant chooses **Accept / Edit / Skip** per item. Accepted or edited copy proceeds to write-back. Skipped items remain without copy (the dispatch gate drops them).
166
+ **Per-item review loop (not batch approve).** For each generated draft,
167
+ show the consultant the structured source data and the generated title +
168
+ why, then collect the verdict via `AskUserQuestion`:
169
+ - `header`: `"Copy"`
170
+ - `question`: "[item title] — accept this draft?"
171
+ - `options`:
172
+ - Accept — "Use this copy as-is"
173
+ - Edit — "I'll provide revised text"
174
+ - Skip — "Leave without copy (dispatch gate will drop it)"
175
+ - `multiSelect: false`
176
+
177
+ Accepted or edited copy proceeds to write-back. Skipped items remain
178
+ without copy (the dispatch gate drops them). If "Edit" is chosen, ask
179
+ for the revised text conversationally, then write back.
162
180
 
163
181
  **Write-back.** On approval, call `spliceClientCopy(notes, newBlock, markerLine)` (from `engagement.mjs`) to produce the updated notes. `newBlock` is the `<!-- client-facing ... -->` block. `markerLine` is `<!-- cc-generated:<ISO timestamp> status:<current status> -->` — placed OUTSIDE the copy block (never inside, or `parseClientCopy` would include it in `client_why`). Write via `pib_update_action({ fid, notes: splicedNotes })` or `pib_update_project({ fid, notes: splicedNotes })`.
164
182
 
@@ -180,12 +198,19 @@ For each recipient: load their previous packet from
180
198
  `engagement-packets/<recipient>-sent.json` (if any) as `prevPacket`,
181
199
  then:
182
200
  ```
183
- const { packet, refmap, notClientReady } = renderPacket(config, actions, events, billing, recipient, prevPacket, projects)
201
+ const { packet, refmap, notClientReady, allDropped } = renderPacket(config, actions, events, billing, recipient, prevPacket, projects)
184
202
  ```
185
203
  `notClientReady` reconfirms (defensively) the not-client-ready items the
186
204
  engine dropped — they never reach the client (no internal text leaks).
187
205
  Fold these into the step-3 block report.
188
206
 
207
+ **`allDropped` guard:** If `allDropped` is true, **STOP** — do not
208
+ proceed to dispatch. Every visible item was dropped because it lacked
209
+ client-facing copy. This almost always means the query used
210
+ `pib_list_actions` (which omits `notes`) instead of `pib_query` with
211
+ `SELECT *`. Surface: "Empty packet — all N client-visible items are
212
+ missing client-facing copy. Did you use pib_query with SELECT *?"
213
+
189
214
  ### 5. Hard gate
190
215
 
191
216
  `assertPacketInvariants(packet, recipient, config.roles)`. If it throws
@@ -211,14 +236,23 @@ For project roll-ups, show: `E-signing (roll-up: 3 done, 1 in progress)`
211
236
  vs action items: `DNS cutover (action, needs:decision)`. For projects
212
237
  with individually-visible children, note the count.
213
238
 
214
- Then ask: **"Send these updates now, or review a recipient's packet in
215
- detail first?"**
216
-
217
- - **"Send"** → proceed to archive + dispatch (steps 7–9).
218
- - **"Show me Ed's"** render Ed's full packet (needs_you titles, in_flight,
219
- shipped, messages, billing) so the consultant can read exactly what Ed will
220
- see. Then re-ask.
221
- - **"Cancel"** → abort the sync, nothing sent.
239
+ Then collect the dispatch decision via `AskUserQuestion`:
240
+ - `header`: `"Dispatch"`
241
+ - `question`: "Ready to send to N recipients?"
242
+ - `options`:
243
+ - Send "Archive and dispatch all packets now"
244
+ - Review "Show me a recipient's packet in detail first"
245
+ - Cancel — "Abort this sync, nothing sent"
246
+ - `multiSelect: false`
247
+
248
+ - **Send** → proceed to archive + dispatch (steps 7–9).
249
+ - **Review** → ask which recipient, render their full packet (needs_you,
250
+ in_flight, shipped, messages, billing), then re-present this same
251
+ AskUserQuestion (loop until Send or Cancel).
252
+ - **Cancel** → abort, nothing sent.
253
+
254
+ (See `.claude/cabinet/skill-output-conventions.md` for when structured
255
+ choices apply.)
222
256
 
223
257
  This confirmation step is the human-in-the-loop gate — the safety checks
224
258
  (assertPacketInvariants) catch structural violations, but only the
@@ -282,7 +282,19 @@ items are returned, skip silently. Otherwise:
282
282
  (`triggered | still-waiting | needs-info | condition-obsolete`)
283
283
  and brief reasoning in notes.
284
284
  4. Include any items that evaluated to `triggered` in the briefing's
285
- **Attention Items** section. Do not auto-reopen the user decides.
285
+ **Attention Items** section. For each, collect the user's decision
286
+ via `AskUserQuestion`:
287
+ - `header`: `"Triggered"`
288
+ - `question`: "[fid] appears triggered — [trigger summary]. What to do?"
289
+ - `options`:
290
+ - Reopen — "Change status back to open, ready to work"
291
+ - Leave deferred — "Keep it deferred, not ready yet"
292
+ - Mark obsolete — "Trigger condition no longer relevant"
293
+ - `multiSelect: false`
294
+
295
+ Map: Reopen → status to open; Leave deferred → no-op; Mark obsolete
296
+ → `pib_mark_trigger_checked` with `condition-obsolete`.
297
+ (See `skill-output-conventions.md`.)
286
298
 
287
299
  **Cost control:** Cap this phase at 30 seconds total. If N > 10 items,
288
300
  evaluate only the 5 least-recently-checked.
@@ -344,9 +356,18 @@ done
344
356
  Also check `git worktree list` for active worktrees whose branches
345
357
  have diverged from main.
346
358
 
347
- Surface as advisory:
348
- > Branch `feature-x` has N commits ahead of main (last touched
349
- > [date]). Merge, continue working, or discard?
359
+ Surface as advisory, then collect the decision via `AskUserQuestion`:
360
+ - `header`: `"Branch"`
361
+ - `question`: "Branch `[name]` has N commits ahead of main. What to do?"
362
+ - `options`:
363
+ - Merge — "Merge into main now"
364
+ - Continue — "Leave it, still working on it"
365
+ - Discard — "Delete the branch (destructive)"
366
+ - `multiSelect: false`
367
+
368
+ Map: Merge → `git merge [branch]` (surface conflicts if any). Continue →
369
+ no-op. Discard → warn "permanently deletes N commits" and require
370
+ explicit second confirmation before `git branch -D`.
350
371
 
351
372
  **Known platform limitation:** The Claude Code Agent tool with
352
373
  `isolation: "worktree"` branches from the remote tracking ref, not
@@ -136,8 +136,23 @@ assumption, evidence, question, and your commentary. The user needs to
136
136
  see everything to make good decisions.
137
137
 
138
138
  **Fallback (no browser available):** Present findings in the conversation
139
- grouped by cabinet member, severity-ordered within each group. Ask for
140
- verdicts one group at a time to avoid overwhelming.
139
+ grouped by cabinet member, severity-ordered within each group. For each
140
+ finding, show its full context (title, description, assumption, evidence,
141
+ question, and your commentary), then collect the verdict via one
142
+ `AskUserQuestion` call:
143
+
144
+ - `header`: `"Verdict"`
145
+ - `question`: the finding's title
146
+ - `options`:
147
+ - Fix — "Real finding, approve for action"
148
+ - Defer — "Real but not now — revisit later"
149
+ - Reject — "Not a real problem for this project"
150
+ - Question — "Need more information before deciding"
151
+ - `multiSelect: false`
152
+
153
+ One call per finding, walked individually within each group. (See
154
+ `.claude/cabinet/skill-output-conventions.md` for when this pattern
155
+ applies.)
141
156
 
142
157
  ### 3. Apply Verdicts (core)
143
158