create-claude-cabinet 0.35.0 → 0.37.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.
Files changed (36) hide show
  1. package/lib/cli.js +4 -1
  2. package/lib/engagement-setup.js +119 -11
  3. package/package.json +1 -1
  4. package/templates/cabinet/qa-dimensions-template.yaml +77 -0
  5. package/templates/cabinet/skill-best-practices.md +7 -0
  6. package/templates/cabinet/skill-output-conventions.md +153 -0
  7. package/templates/engagement/__tests__/engagement.test.mjs +334 -29
  8. package/templates/engagement/__tests__/pibdb-adapter.test.mjs +19 -0
  9. package/templates/engagement/app-guide-template.md +82 -0
  10. package/templates/engagement/engagement-schema.md +298 -321
  11. package/templates/engagement/engagement.mjs +74 -18
  12. package/templates/engagement/pib-db-patches/pib-db-lib.mjs +91 -28
  13. package/templates/engagement/pib-db-patches/pib-db-schema.sql +3 -0
  14. package/templates/engagement/pibdb-adapter.mjs +9 -0
  15. package/templates/engagement/sql-constants.mjs +27 -0
  16. package/templates/scripts/pib-db-lib.mjs +69 -5
  17. package/templates/skills/cabinet-roster-check/SKILL.md +14 -0
  18. package/templates/skills/checklist-discover/SKILL.md +217 -0
  19. package/templates/skills/collab-client/SKILL.md +188 -0
  20. package/templates/skills/collab-consultant/SKILL.md +219 -0
  21. package/templates/skills/debrief/SKILL.md +13 -2
  22. package/templates/skills/debrief/phases/checklist-feedback.md +116 -0
  23. package/templates/skills/engagement/SKILL.md +3 -222
  24. package/templates/skills/engagement-add/SKILL.md +2 -92
  25. package/templates/skills/engagement-create/SKILL.md +2 -197
  26. package/templates/skills/engagement-edit/SKILL.md +2 -115
  27. package/templates/skills/engagement-help/SKILL.md +5 -174
  28. package/templates/skills/engagement-message/SKILL.md +3 -75
  29. package/templates/skills/engagement-progress/SKILL.md +3 -51
  30. package/templates/skills/engagement-status/SKILL.md +3 -125
  31. package/templates/skills/engagement-sync/SKILL.md +2 -424
  32. package/templates/skills/execute/SKILL.md +14 -0
  33. package/templates/skills/execute/phases/post-impl-checklist.md +137 -0
  34. package/templates/skills/guide/SKILL.md +98 -0
  35. package/templates/skills/orient/SKILL.md +25 -4
  36. package/templates/skills/triage-audit/SKILL.md +17 -2
package/lib/cli.js CHANGED
@@ -486,7 +486,7 @@ const MODULES = {
486
486
  mandatory: false,
487
487
  default: true,
488
488
  lean: true,
489
- templates: ['skills/plan', 'skills/execute', 'skills/generate-plan-groups', 'skills/execute-group', 'workflows/execute-group-implement.js', 'workflows/execute-group-complete.js', 'skills/investigate', 'cabinet/checkpoint-protocol.md'],
489
+ templates: ['skills/plan', 'skills/execute', 'skills/execute/phases/post-impl-checklist.md', 'skills/debrief/phases/checklist-feedback.md', 'skills/checklist-discover', 'skills/generate-plan-groups', 'skills/execute-group', 'workflows/execute-group-implement.js', 'workflows/execute-group-complete.js', 'skills/investigate', 'cabinet/checkpoint-protocol.md', 'cabinet/qa-dimensions-template.yaml'],
490
490
  },
491
491
  'compliance': {
492
492
  name: 'Compliance Stack (rules + enforcement)',
@@ -602,6 +602,8 @@ const MODULES = {
602
602
  requires: ['work-tracking'],
603
603
  postInstall: 'engagement-setup',
604
604
  templates: [
605
+ 'skills/collab-client',
606
+ 'skills/collab-consultant',
605
607
  'skills/engagement',
606
608
  'skills/engagement-progress',
607
609
  'skills/engagement-help',
@@ -611,6 +613,7 @@ const MODULES = {
611
613
  'skills/engagement-add',
612
614
  'skills/engagement-status',
613
615
  'skills/engagement-sync',
616
+ 'skills/guide',
614
617
  'engagement',
615
618
  ],
616
619
  },
@@ -5,11 +5,16 @@
5
5
  * Dispatched from cli.js's postInstall pipeline. Two responsibilities:
6
6
  *
7
7
  * 1. FILE OVERLAY — copy the engagement-inclusive pib-db files over the
8
- * base versions. Uses a version-aware check: parse SCHEMA_VERSION from
9
- * the installed file and re-copy when below the patch's version. This
10
- * replaces the old string-marker approach, which was version-blind and
11
- * silently skipped upgrades (e.g., v5→v6 never landed because the v5
12
- * marker already matched).
8
+ * base versions. For pib-db-lib.mjs, the decision is marker-aware:
9
+ * we check whether the installed file already contains engagement code
10
+ * (not just its SCHEMA_VERSION number). This handles three cases:
11
+ * a. Installed file has engagement code + version >= patch → skip
12
+ * b. Installed file lacks engagement code + version <= patch → copy
13
+ * c. Installed file lacks engagement code + version > patch → BASE
14
+ * AHEAD: the base work-tracking module advanced past the patch
15
+ * without a coordinated engagement patch bump. Copying stale
16
+ * patch code over a newer base would drop base migrations.
17
+ * Emit a warning and skip both overlay and schema-ensure.
13
18
  *
14
19
  * 2. SCHEMA ENSURE — after the overlay, open pib.db and call migrate()
15
20
  * so existing DBs advance to the patch's schema version. Also runs an
@@ -18,8 +23,19 @@
18
23
  * engagement — the v5 migration entry is gated out, so this direct
19
24
  * exec is the only path to creating the table).
20
25
  *
21
- * The base pib-db templates (owned by work-tracking) ship schema v1-v5.
22
- * The engagement patch ships v1-v6 (v5=engagement_events, v6=projects.tags).
26
+ * SCHEMA VERSIONING COORDINATION RULES:
27
+ * - The engagement patch's SCHEMA_VERSION must always be >= the base
28
+ * work-tracking SCHEMA_VERSION (the patch is a superset).
29
+ * - When base advances, the patch must advance in the same CC release.
30
+ * - Migrations shared by both base and patch use column-existence
31
+ * checks or IF NOT EXISTS to be idempotent.
32
+ * - A base-ahead state (base version > patch version) is an error
33
+ * indicating an uncoordinated upgrade. The overlay refuses to copy
34
+ * and skips schema-ensure to avoid mismatched lib/DB state.
35
+ *
36
+ * The base pib-db templates (owned by work-tracking) ship schema v1-v6.
37
+ * The engagement patch ships v1-v8 (v5=engagement_events, v6=projects.tags,
38
+ * v7=client-facing copy columns, v8=engagement_events.visibility).
23
39
  */
24
40
 
25
41
  const fs = require('fs');
@@ -27,12 +43,17 @@ const path = require('path');
27
43
 
28
44
  const FILES = ['pib-db-lib.mjs', 'pib-db-mcp-server.mjs', 'pib-db-schema.sql', 'pib-db.mjs'];
29
45
  const VERSION_RE = /export const SCHEMA_VERSION\s*=\s*(\d+)/;
46
+ const ENGAGEMENT_MARKER_RE = /engagement_events/;
30
47
 
31
48
  function parseSchemaVersion(content) {
32
49
  const m = content.match(VERSION_RE);
33
50
  return m ? parseInt(m[1], 10) : 0;
34
51
  }
35
52
 
53
+ function hasEngagementCode(content) {
54
+ return ENGAGEMENT_MARKER_RE.test(content);
55
+ }
56
+
36
57
  function setupEngagement({ dryRun, projectDir } = {}) {
37
58
  const results = [];
38
59
  const scriptsDir = path.join(projectDir, 'scripts');
@@ -43,7 +64,7 @@ function setupEngagement({ dryRun, projectDir } = {}) {
43
64
  return { results };
44
65
  }
45
66
 
46
- // --- Step 1: File overlay (version-aware) ---
67
+ // --- Step 1: File overlay (marker-aware for pib-db-lib.mjs) ---
47
68
 
48
69
  const patchLibPath = path.join(patchDir, 'pib-db-lib.mjs');
49
70
  let patchVersion = 0;
@@ -52,6 +73,7 @@ function setupEngagement({ dryRun, projectDir } = {}) {
52
73
  }
53
74
 
54
75
  let filesUpdated = false;
76
+ let baseAhead = false;
55
77
  for (const file of FILES) {
56
78
  const target = path.join(scriptsDir, file);
57
79
  const source = path.join(patchDir, file);
@@ -67,9 +89,22 @@ function setupEngagement({ dryRun, projectDir } = {}) {
67
89
  }
68
90
 
69
91
  if (file === 'pib-db-lib.mjs') {
70
- const installedVersion = parseSchemaVersion(fs.readFileSync(target, 'utf8'));
71
- if (installedVersion >= patchVersion) {
72
- results.push(`${file}: schema v${installedVersion} >= patch v${patchVersion} — skipped`);
92
+ const installedContent = fs.readFileSync(target, 'utf8');
93
+ const installedVersion = parseSchemaVersion(installedContent);
94
+ const isEngagementInclusive = hasEngagementCode(installedContent);
95
+
96
+ if (isEngagementInclusive && installedVersion >= patchVersion) {
97
+ results.push(`${file}: engagement-inclusive v${installedVersion} >= patch v${patchVersion} — skipped`);
98
+ continue;
99
+ }
100
+
101
+ if (!isEngagementInclusive && installedVersion > patchVersion) {
102
+ baseAhead = true;
103
+ results.push(
104
+ `${file}: BASE AHEAD — installed base v${installedVersion} > patch v${patchVersion}. ` +
105
+ `The engagement patch must be updated to match. Overlay skipped to avoid dropping base migrations. ` +
106
+ `Fix: bump the engagement patch SCHEMA_VERSION to >= ${installedVersion} in the same CC release.`
107
+ );
73
108
  continue;
74
109
  }
75
110
  } else {
@@ -89,6 +124,13 @@ function setupEngagement({ dryRun, projectDir } = {}) {
89
124
  }
90
125
 
91
126
  // --- Step 2: Schema ensure (migrate + idempotent engagement_events) ---
127
+ // Skipped entirely when base-ahead is detected — running migrations
128
+ // against a mismatched library/DB pair would under-migrate the DB.
129
+
130
+ if (baseAhead) {
131
+ results.push('schema-ensure: skipped — base-ahead state detected (see overlay warning above)');
132
+ return { results };
133
+ }
92
134
 
93
135
  const dbPath = path.join(projectDir, 'pib.db');
94
136
  if (!dryRun && fs.existsSync(dbPath) && fs.existsSync(path.join(scriptsDir, 'pib-db-lib.mjs'))) {
@@ -122,6 +164,12 @@ function setupEngagement({ dryRun, projectDir } = {}) {
122
164
  // later adds engagement. Their v5 migration entry (engagement_events)
123
165
  // is gated out by user_version, and the execSync migrate above won't
124
166
  // create it either. This direct exec is the only path.
167
+ //
168
+ // SOURCE OF TRUTH: templates/engagement/sql-constants.mjs
169
+ // This inline copy exists because engagement-setup.js is CJS and
170
+ // can't import() ESM synchronously. The drift-guard test
171
+ // (test/pib-db-engagement/sql-constants-drift.test.js) verifies
172
+ // this stays in sync with sql-constants.mjs.
125
173
  try {
126
174
  db.exec(`CREATE TABLE IF NOT EXISTS engagement_events (
127
175
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -133,6 +181,7 @@ function setupEngagement({ dryRun, projectDir } = {}) {
133
181
  author TEXT NOT NULL,
134
182
  verdict TEXT CHECK(verdict IS NULL OR verdict IN ('approve','object','comment','none')),
135
183
  body TEXT CHECK(body IS NULL OR length(body) <= 10000),
184
+ visibility TEXT NOT NULL DEFAULT 'internal' CHECK(visibility IN ('client','internal')),
136
185
  addressed INTEGER NOT NULL DEFAULT 0 CHECK(addressed IN (0,1)),
137
186
  created_at TEXT NOT NULL CHECK(created_at GLOB '????-??-??T*'),
138
187
  CHECK(kind NOT IN ('client_feedback','approval')
@@ -141,10 +190,69 @@ function setupEngagement({ dryRun, projectDir } = {}) {
141
190
  db.exec("CREATE INDEX IF NOT EXISTS idx_engagement_events_eng ON engagement_events(engagement, created_at DESC)");
142
191
  db.exec("CREATE INDEX IF NOT EXISTS idx_engagement_events_tgt ON engagement_events(target_fid, created_at DESC)");
143
192
  db.exec("CREATE INDEX IF NOT EXISTS idx_engagement_events_dedup ON engagement_events(packet_id, target_fid, verdict)");
193
+ // Defensive idempotent ALTER: an engagement_events table created by a
194
+ // prior install (before the visibility column existed) won't get the
195
+ // column from CREATE IF NOT EXISTS. Add it here; swallow "duplicate
196
+ // column" when it's already present.
197
+ try {
198
+ db.exec("ALTER TABLE engagement_events ADD COLUMN visibility TEXT NOT NULL DEFAULT 'internal' CHECK(visibility IN ('client','internal'))");
199
+ } catch (e) {
200
+ if (!/duplicate column/i.test(e.message || '')) throw e;
201
+ }
144
202
  results.push('schema-ensure: engagement_events table ensured (idempotent)');
145
203
  } catch (e) {
146
204
  results.push(`schema-ensure: engagement_events ensure failed — ${e.message}`);
147
205
  }
206
+
207
+ // Data migration — extract client-facing copy from notes into columns.
208
+ // Idempotent: skips rows where any client column is already populated.
209
+ // Uses synchronous DB operations (better-sqlite3) so no async needed.
210
+ try {
211
+ const hasClientCols = db.prepare("PRAGMA table_info(actions)").all()
212
+ .some(c => c.name === 'client_title');
213
+ if (hasClientCols) {
214
+ const actionRows = db.prepare(
215
+ "SELECT fid, notes FROM actions WHERE notes LIKE '%client-facing%' AND client_title IS NULL AND client_body IS NULL AND client_generated_at IS NULL"
216
+ ).all();
217
+ const projRows = db.prepare(
218
+ "SELECT fid, notes FROM projects WHERE notes LIKE '%client-facing%' AND client_title IS NULL AND client_body IS NULL AND client_generated_at IS NULL"
219
+ ).all();
220
+
221
+ const copyRe = /<!--\s*client-facing\s*\n([\s\S]*?)-->/;
222
+ const genRe = /<!--\s*cc-generated:(\S+)\s+status:(\S+)\s*-->/;
223
+ const updateA = db.prepare("UPDATE actions SET client_title = ?, client_body = ?, client_generated_at = ?, client_generated_status = ? WHERE fid = ?");
224
+ const updateP = db.prepare("UPDATE projects SET client_title = ?, client_body = ?, client_generated_at = ?, client_generated_status = ? WHERE fid = ?");
225
+
226
+ let migrated = 0;
227
+ for (const { fid, notes } of [...actionRows, ...projRows]) {
228
+ if (!notes) continue;
229
+ const copyMatch = notes.match(copyRe);
230
+ const genMatch = notes.match(genRe);
231
+ if (!copyMatch && !genMatch) continue;
232
+
233
+ let title = null, body = null, genAt = null, genStatus = null;
234
+ if (copyMatch) {
235
+ const lines = copyMatch[1].split('\n').map(l => l.trim()).filter(Boolean);
236
+ title = lines[0] || null;
237
+ body = lines.slice(1).join('\n').trim() || null;
238
+ }
239
+ if (genMatch) {
240
+ genAt = genMatch[1] || null;
241
+ genStatus = genMatch[2] || null;
242
+ }
243
+
244
+ const stmt = actionRows.some(r => r.fid === fid) ? updateA : updateP;
245
+ stmt.run(title, body, genAt, genStatus, fid);
246
+ migrated++;
247
+ }
248
+ if (migrated > 0) {
249
+ results.push(`data-migration: migrated client-facing copy for ${migrated} row(s)`);
250
+ }
251
+ }
252
+ } catch (e) {
253
+ results.push(`data-migration: client copy migration failed — ${e.message}`);
254
+ }
255
+
148
256
  db.close();
149
257
  } catch (e) {
150
258
  if (/Cannot find module|MODULE_NOT_FOUND/.test(e.message || '')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.35.0",
3
+ "version": "0.37.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"
@@ -0,0 +1,77 @@
1
+ # Change-Impact QA Dimensions — starter template.
2
+ #
3
+ # To ACTIVATE the change-impact checklist, copy this file to:
4
+ # .claude/cabinet/qa-dimensions.yaml
5
+ # and customize the dimensions for your project. The /execute
6
+ # post-impl-checklist phase looks for `qa-dimensions.yaml` (no
7
+ # `-template` suffix) and stays silent until that file exists.
8
+ #
9
+ # How it works: after implementation, the phase reads the git diff,
10
+ # matches each changed file against every dimension's `paths` globs,
11
+ # and surfaces the matched dimensions' checks as context for the
12
+ # pre-commit cabinet sweep (Checkpoint 3). QA is the primary consumer.
13
+ #
14
+ # ── Schema ────────────────────────────────────────────────────────
15
+ # dimensions: # top-level map; keys are dimension names
16
+ # <dimension-name>:
17
+ # paths: # list of glob patterns (REQUIRED, >=1)
18
+ # - "glob/pattern/**"
19
+ # severity: high # high | moderate | info (REQUIRED)
20
+ # checks: # list of checks (REQUIRED, >=1)
21
+ # - tag: run # run | review
22
+ # check: "text" # what to verify
23
+ #
24
+ # ── Glob matching rules (canonical — the phase follows these) ───────
25
+ # * A leading "./" is stripped from both pattern and path before
26
+ # matching. Diff paths are repo-relative with no "./".
27
+ # * "*" matches within ONE path segment (no "/"). "src/api/*"
28
+ # matches "src/api/foo.js" but NOT "src/api/v2/foo.js".
29
+ # * "**" matches across segments (subtree). "src/api/**" matches
30
+ # "src/api/foo.js" AND "src/api/v2/foo.js".
31
+ # * A trailing "/" means "this directory and below": "src/api/" is
32
+ # treated as "src/api/**".
33
+ # * A bare extension glob like "*.md" is UNROOTED — it matches that
34
+ # extension at any depth. For root-only, use an explicit prefix:
35
+ # "README.md" or "docs/*.md" instead of bare "*.md".
36
+ #
37
+ # ── severity meanings ───────────────────────────────────────────────
38
+ # high — a miss here ships a real bug; sweep should treat as blocking
39
+ # moderate — worth checking; sweep treats as advisory
40
+ # info — reminder/nudge; never blocking
41
+ #
42
+ # The examples below are illustrative. Delete or replace them.
43
+
44
+ dimensions:
45
+ data-coherence:
46
+ paths:
47
+ - "**/pib-db*.mjs"
48
+ - "**/*schema*"
49
+ - "**/*migration*"
50
+ severity: high
51
+ checks:
52
+ - tag: run
53
+ check: "Run schema validation if any schema or migration file changed."
54
+ - tag: review
55
+ check: "Verify referential integrity for any new foreign keys or cross-store references."
56
+ - tag: review
57
+ check: "Confirm any migration handles existing rows, not just fresh installs."
58
+
59
+ api-drift:
60
+ paths:
61
+ - "**/index.mjs"
62
+ - "lib/*.js"
63
+ severity: high
64
+ checks:
65
+ - tag: run
66
+ check: "Grep the changed files for exported symbols; confirm the public surface is intentional."
67
+ - tag: review
68
+ check: "Check downstream consumers of any changed or removed export."
69
+
70
+ knowledge-layer:
71
+ paths:
72
+ - "templates/skills/**"
73
+ - "templates/engagement/**"
74
+ severity: moderate
75
+ checks:
76
+ - tag: review
77
+ check: "If user-facing behavior or vocabulary changed, check whether app-guide.md needs updating."
@@ -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.