create-claude-cabinet 0.41.0 → 0.42.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/lib/cli.js +23 -6
- package/lib/settings-merge.js +51 -1
- package/package.json +1 -1
- package/templates/cabinet/advisories-state-schema.md +68 -0
- package/templates/cabinet/elicitation-methods.md +70 -0
- package/templates/cabinet/eval-protocol.md +20 -0
- package/templates/cabinet/skill-output-conventions.md +21 -3
- package/templates/engagement-server/__tests__/e2e-skills.test.mjs +147 -0
- package/templates/engagement-server/__tests__/server-harness.mjs +72 -0
- package/templates/engagement-server/__tests__/server.test.mjs +181 -0
- package/templates/hooks/action-completion-gate.sh +5 -2
- package/templates/hooks/action-quality-gate.sh +6 -3
- package/templates/hooks/bash-output-compress.sh +147 -0
- package/templates/hooks/cc-upstream-guard.sh +7 -3
- package/templates/hooks/git-guardrails.sh +7 -3
- package/templates/hooks/work-tracker-guard.sh +5 -2
- package/templates/mux/config/manage-dx.py +2 -3
- package/templates/mux/config/muxlib.py +3 -1
- package/templates/mux/config/show-dx.py +3 -4
- package/templates/rules/enforcement-pipeline.md +1 -1
- package/templates/rules/markdown-prose.md +9 -0
- package/templates/scripts/skill-usage.mjs +208 -0
- package/templates/scripts/watchtower-ring1.mjs +37 -8
- package/templates/scripts/watchtower-ring3-close.mjs +24 -6
- package/templates/skills/cabinet-anthropic-insider/SKILL.md +6 -0
- package/templates/skills/cabinet-deployment/SKILL.md +265 -0
- package/templates/skills/cabinet-deployment/phases/scan-scope.md +40 -0
- package/templates/skills/cabinet-seo/SKILL.md +150 -0
- package/templates/skills/cabinet-vision/SKILL.md +7 -0
- package/templates/skills/cc-link/SKILL.md +1 -0
- package/templates/skills/cc-publish/SKILL.md +1 -0
- package/templates/skills/cc-remember/SKILL.md +1 -0
- package/templates/skills/cc-unlink/SKILL.md +1 -0
- package/templates/skills/checklist-discover/SKILL.md +27 -25
- package/templates/skills/memory/SKILL.md +1 -0
- package/templates/skills/menu/SKILL.md +1 -0
- package/templates/skills/onboard/SKILL.md +5 -0
- package/templates/skills/orient/SKILL.md +80 -8
- package/templates/skills/orient/phases/dx-captures.md +5 -3
- package/templates/skills/plan/SKILL.md +60 -1
- package/templates/skills/seed/SKILL.md +4 -1
- package/templates/skills/threads/SKILL.md +144 -0
- package/templates/skills/unwrap/SKILL.md +43 -0
- package/templates/skills/watchtower/SKILL.md +1 -1
- package/templates/workflows/deliberative-audit.js +38 -15
package/lib/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
const { copyTemplates } = require('./copy');
|
|
7
|
-
const { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks } = require('./settings-merge');
|
|
7
|
+
const { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks, mergeBashCompressHooks } = require('./settings-merge');
|
|
8
8
|
const { create: createMetadata, read: readMetadata } = require('./metadata');
|
|
9
9
|
const { setupDb } = require('./db-setup');
|
|
10
10
|
const { setupVerifyRuntime } = require('./verify-setup');
|
|
@@ -471,7 +471,7 @@ const MODULES = {
|
|
|
471
471
|
mandatory: false,
|
|
472
472
|
default: true,
|
|
473
473
|
lean: true,
|
|
474
|
-
templates: ['hooks/git-guardrails.sh', 'hooks/cc-upstream-guard.sh', 'hooks/skill-telemetry.sh', 'hooks/skill-tool-telemetry.sh', 'hooks/work-tracker-guard.sh', 'hooks/action-quality-gate.sh', 'hooks/action-completion-gate.sh', 'hooks/memory-index-guard.sh', 'scripts/cc-drift-check.cjs'],
|
|
474
|
+
templates: ['hooks/git-guardrails.sh', 'hooks/cc-upstream-guard.sh', 'hooks/skill-telemetry.sh', 'hooks/skill-tool-telemetry.sh', 'hooks/work-tracker-guard.sh', 'hooks/action-quality-gate.sh', 'hooks/action-completion-gate.sh', 'hooks/memory-index-guard.sh', 'scripts/cc-drift-check.cjs', 'scripts/skill-usage.mjs'],
|
|
475
475
|
},
|
|
476
476
|
'work-tracking': {
|
|
477
477
|
name: 'Work Tracking (pib-db or markdown)',
|
|
@@ -488,7 +488,7 @@ const MODULES = {
|
|
|
488
488
|
mandatory: false,
|
|
489
489
|
default: true,
|
|
490
490
|
lean: true,
|
|
491
|
-
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', 'scripts/qa-dimensions-validator.cjs', 'skills/orient/phases/checklist-status.md'],
|
|
491
|
+
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/elicitation-methods.md', 'cabinet/qa-dimensions-template.yaml', 'scripts/qa-dimensions-validator.cjs', 'skills/orient/phases/checklist-status.md'],
|
|
492
492
|
},
|
|
493
493
|
'compliance': {
|
|
494
494
|
name: 'Compliance Stack (rules + enforcement)',
|
|
@@ -496,7 +496,7 @@ const MODULES = {
|
|
|
496
496
|
mandatory: false,
|
|
497
497
|
default: true,
|
|
498
498
|
lean: false,
|
|
499
|
-
templates: ['rules/enforcement-pipeline.md', 'rules/maintainability.md', 'memory/patterns/_pattern-template.md', 'memory/patterns/pattern-intelligence-first.md'],
|
|
499
|
+
templates: ['rules/enforcement-pipeline.md', 'rules/maintainability.md', 'rules/markdown-prose.md', 'skills/unwrap', 'memory/patterns/_pattern-template.md', 'memory/patterns/pattern-intelligence-first.md'],
|
|
500
500
|
},
|
|
501
501
|
'memory': {
|
|
502
502
|
name: 'Built-In Memory (cc-remember + reader + validator)',
|
|
@@ -533,10 +533,12 @@ const MODULES = {
|
|
|
533
533
|
'skills/cabinet-boundary-man',
|
|
534
534
|
'skills/cabinet-anthropic-insider', 'skills/cabinet-cc-health',
|
|
535
535
|
'skills/cabinet-data-integrity',
|
|
536
|
-
'skills/cabinet-debugger', 'skills/cabinet-
|
|
536
|
+
'skills/cabinet-debugger', 'skills/cabinet-deployment',
|
|
537
|
+
'skills/cabinet-historian',
|
|
537
538
|
'skills/cabinet-organized-mind', 'skills/cabinet-process-therapist',
|
|
538
539
|
'skills/cabinet-qa', 'skills/cabinet-record-keeper',
|
|
539
540
|
'skills/cabinet-roster-check', 'skills/cabinet-security',
|
|
541
|
+
'skills/cabinet-seo',
|
|
540
542
|
'skills/cabinet-small-screen', 'skills/cabinet-speed-freak',
|
|
541
543
|
'skills/cabinet-system-advocate', 'skills/cabinet-technical-debt',
|
|
542
544
|
'skills/cabinet-usability', 'skills/cabinet-workflow-cop',
|
|
@@ -559,7 +561,7 @@ const MODULES = {
|
|
|
559
561
|
mandatory: false,
|
|
560
562
|
default: true,
|
|
561
563
|
lean: true,
|
|
562
|
-
templates: ['skills/onboard', 'skills/seed', 'skills/cc-upgrade', 'skills/cc-link', 'skills/cc-unlink', 'skills/cc-extract', 'skills/cc-feedback'],
|
|
564
|
+
templates: ['skills/onboard', 'skills/seed', 'skills/cc-upgrade', 'skills/cc-link', 'skills/cc-unlink', 'skills/cc-extract', 'skills/cc-feedback', 'cabinet/elicitation-methods.md'],
|
|
563
565
|
},
|
|
564
566
|
'validate': {
|
|
565
567
|
name: 'Validate',
|
|
@@ -654,6 +656,7 @@ const MODULES = {
|
|
|
654
656
|
'scripts/watchtower-ring3-close.mjs',
|
|
655
657
|
'scripts/watchtower-status.sh',
|
|
656
658
|
'skills/briefing',
|
|
659
|
+
'skills/threads',
|
|
657
660
|
],
|
|
658
661
|
},
|
|
659
662
|
mux: {
|
|
@@ -675,6 +678,15 @@ const MODULES = {
|
|
|
675
678
|
postInstall: 'engagement-server-setup',
|
|
676
679
|
templates: [],
|
|
677
680
|
},
|
|
681
|
+
'bash-compress': {
|
|
682
|
+
name: 'Bash Output Compression Hook',
|
|
683
|
+
description: 'PostToolUse hook that compresses noisy Bash stdout (git status walls, npm/yarn install output, find/ls dumps) to reclaim context in long sessions. Off by default. stderr and error/warning lines pass through verbatim; every rewrite carries a visible [compressed] marker; fail-open on any error. Requires the hooks module (it wires into .claude/settings.json).',
|
|
684
|
+
mandatory: false,
|
|
685
|
+
default: false,
|
|
686
|
+
lean: false,
|
|
687
|
+
requires: ['hooks'],
|
|
688
|
+
templates: ['hooks/bash-output-compress.sh'],
|
|
689
|
+
},
|
|
678
690
|
};
|
|
679
691
|
|
|
680
692
|
/** Recursively collect all relative file paths under a directory. */
|
|
@@ -1292,6 +1304,11 @@ async function run() {
|
|
|
1292
1304
|
mergeMuxHooks(settingsPath);
|
|
1293
1305
|
console.log(' ⚙️ Registered mux worktree health SessionStart hook');
|
|
1294
1306
|
}
|
|
1307
|
+
|
|
1308
|
+
if (selectedModules.includes('bash-compress')) {
|
|
1309
|
+
mergeBashCompressHooks(settingsPath);
|
|
1310
|
+
console.log(' ⚙️ Registered bash-output compression PostToolUse hook');
|
|
1311
|
+
}
|
|
1295
1312
|
}
|
|
1296
1313
|
|
|
1297
1314
|
// --- Heal user-level ~/.claude/settings.json ---
|
package/lib/settings-merge.js
CHANGED
|
@@ -121,6 +121,25 @@ const MUX_HOOKS = {
|
|
|
121
121
|
],
|
|
122
122
|
};
|
|
123
123
|
|
|
124
|
+
// Opt-in bash-output compression. PostToolUse hook on Bash that compresses
|
|
125
|
+
// known-noisy stdout (git status walls, npm/yarn install output) to reclaim
|
|
126
|
+
// context. Off by default; registered only when the `bash-compress` module
|
|
127
|
+
// is selected. The hook itself is fail-open (passes output through untouched
|
|
128
|
+
// on any error) — see templates/hooks/bash-output-compress.sh.
|
|
129
|
+
const BASH_COMPRESS_HOOKS = {
|
|
130
|
+
PostToolUse: [
|
|
131
|
+
{
|
|
132
|
+
matcher: 'Bash',
|
|
133
|
+
hooks: [
|
|
134
|
+
{
|
|
135
|
+
type: 'command',
|
|
136
|
+
command: '.claude/hooks/bash-output-compress.sh',
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
|
|
124
143
|
// Legacy hook script names that should be stripped on any merge.
|
|
125
144
|
// Centralizes cleanup so a user who skips --migrate-memory but runs
|
|
126
145
|
// any other CC operation still gets omega-era hooks pruned.
|
|
@@ -308,4 +327,35 @@ function mergeMuxHooks(settingsPath) {
|
|
|
308
327
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
309
328
|
}
|
|
310
329
|
|
|
311
|
-
|
|
330
|
+
/**
|
|
331
|
+
* Merge the opt-in bash-output compression hook into project settings.
|
|
332
|
+
* Called from the bash-compress module's install path in cli.js — only
|
|
333
|
+
* registers the PostToolUse Bash hook when that module is selected.
|
|
334
|
+
* Idempotent (de-dupes by command path).
|
|
335
|
+
*/
|
|
336
|
+
function mergeBashCompressHooks(settingsPath) {
|
|
337
|
+
if (!fs.existsSync(settingsPath)) return;
|
|
338
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
339
|
+
if (!settings.hooks) settings.hooks = {};
|
|
340
|
+
|
|
341
|
+
for (const [event, newHooks] of Object.entries(BASH_COMPRESS_HOOKS)) {
|
|
342
|
+
if (!settings.hooks[event]) {
|
|
343
|
+
settings.hooks[event] = newHooks;
|
|
344
|
+
} else {
|
|
345
|
+
for (const newHook of newHooks) {
|
|
346
|
+
const hookKey = h => h.command || h.prompt || '';
|
|
347
|
+
const existingKeys = settings.hooks[event].flatMap(h =>
|
|
348
|
+
h.hooks.map(hh => hookKey(hh))
|
|
349
|
+
);
|
|
350
|
+
const newKeys = newHook.hooks.map(h => hookKey(h));
|
|
351
|
+
if (!newKeys.every(k => existingKeys.includes(k))) {
|
|
352
|
+
settings.hooks[event].push(newHook);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
module.exports = { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks, mergeBashCompressHooks, DEFAULT_HOOKS, WATCHTOWER_HOOKS, MUX_HOOKS, BASH_COMPRESS_HOOKS, LEGACY_HOOK_COMMANDS };
|
package/package.json
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Advisory dismissal state — schema and rules
|
|
2
|
+
|
|
3
|
+
Orient surfaces stack-aware advisories (install the Ruby language server, register the Railway MCP, install `hookify`, …). Without memory, every advisory re-nags every session — the same attention-fatigue pattern the watchtower rings were built to eliminate. This file defines the per-project state that gives advisories a memory, and the exact rules orient follows so an advisory is never *permanently* silenced by accident.
|
|
4
|
+
|
|
5
|
+
## Where it lives
|
|
6
|
+
|
|
7
|
+
`.claude/cabinet/advisories-state.json` — **per project, generated at runtime**, NOT shipped as a template. Orient creates it on first write. It must never be added to a module's template array: a shipped stub would overwrite a project's real dismissal history on reinstall (the `.ccrc.json` clobber class of bug). If the file is absent, every advisory is treated as never-seen.
|
|
8
|
+
|
|
9
|
+
> Worktree note: `.claude/cabinet/` is copied per worktree, so dismissal state can diverge between a worktree and its main checkout. That is acceptable — advisories are advisory — and is the reason this is project-local, not user-global.
|
|
10
|
+
|
|
11
|
+
## Schema
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"<advisoryId>": {
|
|
16
|
+
"status": "suggested" | "declined" | "installed",
|
|
17
|
+
"count": 2,
|
|
18
|
+
"last_shown": "2026-06-07",
|
|
19
|
+
"signal": "gemfile+rb"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- **`advisoryId`** — a stable id per advisory, e.g. `lsp:ruby`, `lsp:typescript`, `mcp:railway`, `plugin:hookify`.
|
|
25
|
+
- **`status`**
|
|
26
|
+
- `suggested` — shown, not yet acted on.
|
|
27
|
+
- `declined` — the user explicitly waved it off.
|
|
28
|
+
- `installed` — the thing is present (probe confirmed). **Terminal** — never surface again.
|
|
29
|
+
- **`count`** — how many sessions it has been surfaced while still actionable. Drives the "stop nagging" rule.
|
|
30
|
+
- **`last_shown`** — ISO date of the most recent surfacing.
|
|
31
|
+
- **`signal`** — *the key field that makes "resurface if the stack changed" actually work.* A short, deterministic fingerprint of the stack indicators present when the advisory was last shown/declined. For a multi-indicator advisory like Ruby (`Gemfile` OR `*.rb`), the fingerprint records *which* indicators were present (e.g. `gemfile` vs `gemfile+rb`), so a later change is detectable. Without this stored snapshot, orient has only the *current* indicators and no baseline to diff against — which is the gap this schema closes.
|
|
32
|
+
|
|
33
|
+
## The rules orient follows
|
|
34
|
+
|
|
35
|
+
Before surfacing any advisory, orient computes the advisory's **current signal** (fingerprint of the indicators present now) and reads the stored entry:
|
|
36
|
+
|
|
37
|
+
1. **No entry / file absent** → surface it. Write `{status:"suggested", count:1, last_shown:today, signal:current}`.
|
|
38
|
+
2. **`installed`** → never surface (terminal). (Re-probe may flip a `suggested`/`declined` entry to `installed`; never the reverse automatically.)
|
|
39
|
+
3. **`declined`**
|
|
40
|
+
- current signal **==** stored signal → **silent.** (Optionally surfaced in `/pulse` only.)
|
|
41
|
+
- current signal **!=** stored signal → the stack changed since the user declined → **re-surface exactly once.** Reset to `{status:"suggested", count:1, signal:current}`.
|
|
42
|
+
4. **`suggested`**
|
|
43
|
+
- `count < 2` and signal unchanged → surface again, `count++`, `last_shown=today`.
|
|
44
|
+
- `count >= 2` and signal unchanged → **go quiet** (mention only in `/pulse`). Do not keep incrementing.
|
|
45
|
+
- signal **!=** stored signal (at any count) → the stack changed → reset to `{count:1, signal:current}` and surface.
|
|
46
|
+
|
|
47
|
+
### The anti-trap guarantee
|
|
48
|
+
|
|
49
|
+
**Any change in the stack signal resets an advisory to actionable** — so no advisory is permanently invisible while the thing it suggests is still relevant *and the project keeps evolving*.
|
|
50
|
+
|
|
51
|
+
The one deliberately-sticky case: an advisory whose signal **never changes** once it fires. Example: `plugin:hookify`, keyed on the existence of `.claude/rules/enforcement-pipeline.md` — a file that, once created, stays. A `declined` hookify therefore stays declined. That is intended (the user said no, and nothing about the project changed to revisit it), but it must remain **escapable, not a black hole**:
|
|
52
|
+
|
|
53
|
+
- It is still listed in `/pulse` (quiet, not gone).
|
|
54
|
+
- Clearing its entry from `advisories-state.json` (or setting `status` back to `suggested`) re-arms it.
|
|
55
|
+
|
|
56
|
+
Document any new advisory's signal source here when you add it, and call out explicitly if its signal is static (like hookify) so the sticky behavior is a known property, not a surprise.
|
|
57
|
+
|
|
58
|
+
## Advisory ids in use
|
|
59
|
+
|
|
60
|
+
| advisoryId | indicator(s) → signal | install action shown (advisory only — orient never runs it) |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `lsp:typescript` | `tsconfig.json` or `*.ts` | `/plugin install typescript-lsp` |
|
|
63
|
+
| `lsp:python` | `pyproject.toml` / `requirements.txt` / `*.py` | `/plugin install pyright-lsp` |
|
|
64
|
+
| `lsp:rust` | `Cargo.toml` | `/plugin install rust-analyzer-lsp` |
|
|
65
|
+
| `lsp:go` | `go.mod` | `/plugin install gopls-lsp` |
|
|
66
|
+
| `lsp:ruby` | `Gemfile` or `*.rb` | `/plugin install ruby-lsp@claude-plugins-official` (also needs `gem install ruby-lsp` AND `ENABLE_LSP_TOOL=1`) |
|
|
67
|
+
| `mcp:railway` | `railway.toml` and no railway key in `~/.claude.json` | local: `railway setup agent -y` · remote: register `mcp.railway.com` (OAuth) |
|
|
68
|
+
| `plugin:hookify` | `.claude/rules/enforcement-pipeline.md` exists and hookify not in `claude plugin list` (signal is **static**) | `/plugin install hookify` |
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Elicitation Methods — structured ways to draw out what the user knows
|
|
2
|
+
|
|
3
|
+
CC's interviewing moments (onboard, seed, `/plan` scoping, checklist-discover, debrief) improvise their questioning. This is the shared shelf of structured elicitation techniques skills can consult when they need to draw something out — surfacing a hidden assumption, pressure-testing a plan, widening the option space. It sits on the same shelf as `skill-output-conventions.md`: a reference skills *cite*, not a phase they run.
|
|
4
|
+
|
|
5
|
+
Derived from the BMAD-METHOD advanced-elicitation method set (Apache-2.0; see attribution at the end), curated and adapted to CC's constraints. Several entries are classic requirements-elicitation / creative-thinking craft that BMAD also draws on; those are noted.
|
|
6
|
+
|
|
7
|
+
## How to use this file
|
|
8
|
+
|
|
9
|
+
A skill at an interview step consults this file to *choose how to ask* — it does not run a menu. Pick the one or two techniques that fit the moment and the gap you're trying to close, then ask. The technique shapes the question; the conversation stays a conversation.
|
|
10
|
+
|
|
11
|
+
## Fit criteria (why these and not the other 70-odd)
|
|
12
|
+
|
|
13
|
+
A technique earns a place here only if it meets all four:
|
|
14
|
+
|
|
15
|
+
1. **Conversational register** — it works as plain dialogue, not a worksheet or a numbered menu (terminal prose constraint).
|
|
16
|
+
2. **One question at a time** — it can be run as a sequence of single questions, never a batch. This is a CC hard rule (`CLAUDE.md`): write interview questions one at a time, never batched.
|
|
17
|
+
3. **Fits a named CC moment** — onboard, seed, `/plan` scoping, checklist-discover, or debrief.
|
|
18
|
+
4. **No persona-roleplay dependency** — it doesn't require the user (or Claude) to adopt and switch between named personas to function.
|
|
19
|
+
|
|
20
|
+
## The methods
|
|
21
|
+
|
|
22
|
+
### First-Principles Thinking
|
|
23
|
+
*Moments: /plan scoping, onboard.* Strip away how it's done today and rebuild from what the thing actually needs to do. Use when a plan is anchored on an existing implementation and you suspect the real requirement is simpler or different.
|
|
24
|
+
- Ask: *"Ignore how this works today — what does it actually need to accomplish, at minimum?"* then, one at a time, *"Which of those are truly required versus inherited from the current approach?"*
|
|
25
|
+
|
|
26
|
+
### Pre-mortem
|
|
27
|
+
*Moments: /plan scoping, checklist-discover.* Assume the work shipped and failed; reason backward to the cause. Surfaces risks and edge cases the optimistic framing hides. (BMAD-named.)
|
|
28
|
+
- Ask: *"Imagine this shipped and quietly failed a month later — what's the single most likely reason?"* then *"What would we have needed to know up front to prevent that?"*
|
|
29
|
+
|
|
30
|
+
### Inversion
|
|
31
|
+
*Moments: /plan, checklist-discover.* Ask how to *guarantee* failure, then avoid those things. Often easier to enumerate than success conditions. (BMAD-named.)
|
|
32
|
+
- Ask: *"What's the surest way to make this go wrong?"* then *"Which of those are we closest to doing by accident?"*
|
|
33
|
+
|
|
34
|
+
### Assumption Surfacing
|
|
35
|
+
*Moments: /plan scoping (pairs with the plan-completeness `[NEEDS CLARIFICATION]` marker), investigate.* Name the unspoken assumptions a plan rests on so the shaky ones get checked before building. (Classic requirements-elicitation craft.)
|
|
36
|
+
- Ask: *"What are we assuming is true here that we haven't actually verified?"* then take them one at a time: *"How would we confirm that one cheaply?"*
|
|
37
|
+
|
|
38
|
+
### Socratic Questioning
|
|
39
|
+
*Moments: any.* Challenge a claim with "why?" and "how do you know?" until it rests on something solid. Use sparingly — it's a scalpel, not a default. (BMAD-named.)
|
|
40
|
+
- Ask: *"What makes you confident that's the right call?"* then follow the answer down one level at a time.
|
|
41
|
+
|
|
42
|
+
### Constraint Removal
|
|
43
|
+
*Moments: onboard (vision), seed (member design), /plan (widen options).* Drop a constraint, see what becomes possible, then add it back deliberately. Widens the option space when thinking feels boxed in. (BMAD-named.)
|
|
44
|
+
- Ask: *"If [time / scope / the existing schema] weren't a limit, what would you do instead?"* then *"What's the smallest version of that we could actually do?"*
|
|
45
|
+
|
|
46
|
+
### Stakeholder Lens
|
|
47
|
+
*Moments: onboard (who is served), seed (whose perspective the member encodes).* Re-ask the question from one stakeholder's point of view at a time — the user, a future maintainer, the end customer. One lens per question; never a round-table battery. (Adapted from BMAD's Stakeholder Mapping to honor the one-question rule.)
|
|
48
|
+
- Ask: *"From the end user's point of view, what would make this a win?"* then, next turn, *"Now from the person who maintains it a year from now — same question."*
|
|
49
|
+
|
|
50
|
+
### Analogical Reasoning
|
|
51
|
+
*Moments: seed (member design), onboard (mental model).* Find the closest parallel in another domain and borrow its lessons. Good for naming a fuzzy concept or designing something with no obvious precedent. (BMAD-named.)
|
|
52
|
+
- Ask: *"What existing thing — in or out of software — is this most like?"* then *"What does that parallel get right that we should copy, and where does it break down?"*
|
|
53
|
+
|
|
54
|
+
### Five Whys
|
|
55
|
+
*Moments: investigate, /plan problem-framing, debrief.* Trace a stated problem to its root by asking "why" about each answer in turn. Naturally one-at-a-time. (Classic root-cause craft BMAD draws on.)
|
|
56
|
+
- Ask: *"Why is that a problem?"* — and about each answer, *"And why is that?"* — usually three to five levels reaches the root.
|
|
57
|
+
|
|
58
|
+
### Expand or Contract for Audience
|
|
59
|
+
*Moments: checklist-discover, onboard, debrief presentation.* Deliberately widen or narrow the level of detail to fit who the output serves. Use when scope or depth feels mismatched to the audience. (BMAD-named.)
|
|
60
|
+
- Ask: *"Who reads this, and do they need more breadth or more depth than we have?"* then adjust one dimension at a time.
|
|
61
|
+
|
|
62
|
+
## Considered, not kept
|
|
63
|
+
|
|
64
|
+
- **Red Team vs Blue Team** — a multi-round attack/defend battery; the full technique needs adversarial persona-switching and several exchanges. The useful core (steelman the opposing case) is covered by Socratic Questioning and Inversion as single questions.
|
|
65
|
+
- **Six Thinking Hats** — a six-perspective battery requiring sequential persona adoption; fails the no-persona-roleplay and one-question criteria. The Stakeholder Lens covers the salvageable part, one lens at a time.
|
|
66
|
+
- **Any numbered-menu / "pick 1-9" selection flows** — BMAD presents methods as an interactive numbered menu; that interaction model fails the terminal-prose constraint. CC skills choose a technique themselves and just ask.
|
|
67
|
+
|
|
68
|
+
## Attribution
|
|
69
|
+
|
|
70
|
+
Several methods here are derived from the BMAD-METHOD advanced-elicitation method set (`bmad-code-org/BMAD-METHOD`, Apache License 2.0). Method *names and descriptions* that originate there are marked "(BMAD-named)" above; the adaptations to one-question-at-a-time conversational flow, the CC-moment mapping, and the classic-craft additions are CC's own. BMAD is Apache-2.0 licensed; this derived reference preserves that attribution.
|
|
@@ -70,6 +70,26 @@ Also check:
|
|
|
70
70
|
If a skill hasn't been invoked 3 times in the last month, that itself is
|
|
71
71
|
a finding (coverage gap or trigger problem).
|
|
72
72
|
|
|
73
|
+
**Invocation data — don't eyeball it.** The hooks module's skill-telemetry
|
|
74
|
+
hooks log every skill run to `~/.claude/telemetry/telemetry.jsonl`. Read it
|
|
75
|
+
with the dead-skill reader instead of guessing:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
node scripts/skill-usage.mjs # human report (dead / stale / active)
|
|
79
|
+
node scripts/skill-usage.mjs --quiet # prints only if there's something to flag
|
|
80
|
+
node scripts/skill-usage.mjs --json # structured, for programmatic checks
|
|
81
|
+
node scripts/skill-usage.mjs --days 60 # widen the stale threshold
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
It cross-references installed user-invocable skills against the telemetry and
|
|
85
|
+
surfaces **DEAD** (never invoked — removal or trigger-phrase candidates) and
|
|
86
|
+
**STALE** (not invoked within the threshold). Telemetry is global across
|
|
87
|
+
projects, so a skill flagged DEAD was invoked in *no* project — a strong
|
|
88
|
+
signal. Cabinet members and other `user-invocable: false` skills are excluded
|
|
89
|
+
(they run as agents, not slash/Skill calls, so they never appear). Treat the
|
|
90
|
+
output as candidates for judgment, not a verdict — a never-invoked skill may
|
|
91
|
+
have bad trigger phrasing rather than no purpose.
|
|
92
|
+
|
|
73
93
|
### 3. Score Each Assertion
|
|
74
94
|
|
|
75
95
|
For each assertion, review the sampled executions and score:
|
|
@@ -53,9 +53,27 @@ behaves unexpectedly.
|
|
|
53
53
|
pick more than one.
|
|
54
54
|
- **"Other" is auto-added** by the harness. Never add an "Other" /
|
|
55
55
|
"Something else" option manually — it duplicates.
|
|
56
|
-
- **
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
- **A dialog swallows same-turn prose. Never pair one with an
|
|
57
|
+
explanation the user must read.** When AskUserQuestion fires, the
|
|
58
|
+
dialog takes over the screen — any prose streamed in the same turn
|
|
59
|
+
is effectively invisible at decision time. Two compliant shapes
|
|
60
|
+
(user-confirmed 2026-06-06, after being bitten twice in one day):
|
|
61
|
+
1. **Explanation-first (default for read-then-decide loops):** end
|
|
62
|
+
the turn with the full explanation as prose and a plain-text
|
|
63
|
+
question ("Accept, edit, or skip?"). Take the user's prose
|
|
64
|
+
answer. No dialog at all — this also satisfies the bounded-list
|
|
65
|
+
caveat below for sequential same-shaped decisions.
|
|
66
|
+
2. **Self-sufficient dialog (for small, comparable artifacts):**
|
|
67
|
+
the dialog carries ALL decision content itself. Single-select
|
|
68
|
+
options support a `preview` field (markdown, side-by-side) —
|
|
69
|
+
put the artifact in the preview, attach the SAME preview to
|
|
70
|
+
every option so it stays visible whichever option is focused,
|
|
71
|
+
tradeoffs in option `description`s. Same-turn prose: one-line
|
|
72
|
+
pointer max. If the content doesn't fit a preview pane, you're
|
|
73
|
+
in shape 1.
|
|
74
|
+
(Updated 2026-06-06: earlier guidance said preview was
|
|
75
|
+
undependable; the harness now renders it reliably for single-select
|
|
76
|
+
questions. multiSelect still has no preview.)
|
|
59
77
|
- **Unavailable in Task-spawned subagents.** Agents launched via the
|
|
60
78
|
Task tool (execute-group worktree agents, Workflow agents,
|
|
61
79
|
`context:fork`) cannot call AskUserQuestion — they must use prose.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// End-to-end test of the engagement "skills" machinery against a live server.
|
|
2
|
+
// Run: node --test templates/engagement-server/__tests__/e2e-skills.test.mjs
|
|
3
|
+
//
|
|
4
|
+
// The collab-consultant / collab-client / setup-accounts SKILL.md files are
|
|
5
|
+
// prompts, not code — what's testable is the machinery they drive:
|
|
6
|
+
// - engagement-checklist.mjs (walkthrough state: visibility, answers, persistence)
|
|
7
|
+
// - engagement-transport.mjs (builds the API calls the skills POST/GET)
|
|
8
|
+
// - engagement-crypto.mjs (the secure credential envelope)
|
|
9
|
+
// This suite wires all three through the real server with a simulated
|
|
10
|
+
// consultant and a simulated client (a stand-in for Ed) — including the
|
|
11
|
+
// encrypted-credential round-trip, the highest-stakes path in /setup-accounts.
|
|
12
|
+
import { test, before, after } from 'node:test';
|
|
13
|
+
import assert from 'node:assert';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { rmSync } from 'node:fs';
|
|
17
|
+
import { freshDb, addEngagement, addUser, addToken, startServer, tmpDbPath } from './server-harness.mjs';
|
|
18
|
+
import { buildApiSendInstruction, buildApiReceiveInstruction } from '../../engagement/engagement-transport.mjs';
|
|
19
|
+
import { generateKeypair, encryptCredential, decryptCredential, serializeEnvelope, deserializeEnvelope } from '../../engagement/engagement-crypto.mjs';
|
|
20
|
+
import { createState, recordAnswer, recordCredentialSent, computeVisibility, getProgress, saveState, loadState } from '../../engagement/engagement-checklist.mjs';
|
|
21
|
+
|
|
22
|
+
const DB = tmpDbPath('e2e');
|
|
23
|
+
const PORT = 3992;
|
|
24
|
+
const TOK_CONSULTANT = 'e2e-consultant-AAA';
|
|
25
|
+
const TOK_CLIENT = 'e2e-client-BBB';
|
|
26
|
+
|
|
27
|
+
let server;
|
|
28
|
+
const cfg = (token) => ({ endpoint: server.base, token });
|
|
29
|
+
|
|
30
|
+
// Execute a transport "instruction" (what the skill hands to the HTTP layer).
|
|
31
|
+
async function exec(instr) {
|
|
32
|
+
const res = await fetch(instr.url, {
|
|
33
|
+
method: instr.method,
|
|
34
|
+
headers: instr.headers,
|
|
35
|
+
body: instr.body ? JSON.stringify(instr.body) : undefined,
|
|
36
|
+
});
|
|
37
|
+
return { status: res.status, json: await res.json().catch(() => null) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
before(async () => {
|
|
41
|
+
const db = freshDb(DB);
|
|
42
|
+
addEngagement(db, { id: 'eng_e2e', name: 'E2E' });
|
|
43
|
+
addUser(db, { id: 'usr_con', engagementId: 'eng_e2e', email: 'consultant@x', role: 'consultant' });
|
|
44
|
+
addUser(db, { id: 'usr_cli', engagementId: 'eng_e2e', email: 'client@x', role: 'client' });
|
|
45
|
+
addToken(db, { rawToken: TOK_CONSULTANT, userId: 'usr_con' });
|
|
46
|
+
addToken(db, { rawToken: TOK_CLIENT, userId: 'usr_cli' });
|
|
47
|
+
db.close();
|
|
48
|
+
server = await startServer({ dbPath: DB, port: PORT });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
after(() => { server?.stop(); });
|
|
52
|
+
|
|
53
|
+
// --- 1. Checklist walkthrough state (what /setup-accounts persists) ---
|
|
54
|
+
|
|
55
|
+
test('checklist visibility gates a dependent item until its parent is answered', () => {
|
|
56
|
+
const checklist = {
|
|
57
|
+
sections: [{
|
|
58
|
+
key: 'go_live', items: [
|
|
59
|
+
{ key: 'domain', kind: 'decide', prompt: 'Which domain?' },
|
|
60
|
+
{ key: 'dns_manager', kind: 'decide', prompt: 'Who manages DNS?', visibility: { depends_on: 'domain', value_in: ['feeshame.com'] } },
|
|
61
|
+
],
|
|
62
|
+
}],
|
|
63
|
+
};
|
|
64
|
+
const state = createState('engagement.yaml');
|
|
65
|
+
|
|
66
|
+
let visible = computeVisibility(checklist, state.answers);
|
|
67
|
+
assert.ok(visible.has('domain'));
|
|
68
|
+
assert.ok(!visible.has('dns_manager'), 'dependent item hidden until parent answered');
|
|
69
|
+
|
|
70
|
+
recordAnswer(state, 'domain', 'feeshame.com');
|
|
71
|
+
visible = computeVisibility(checklist, state.answers);
|
|
72
|
+
assert.ok(visible.has('dns_manager'), 'dependent item appears once parent value matches');
|
|
73
|
+
|
|
74
|
+
const progress = getProgress(checklist, state);
|
|
75
|
+
assert.strictEqual(progress.completed, 1);
|
|
76
|
+
assert.strictEqual(progress.total, 2);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('checklist state round-trips through save/load', async () => {
|
|
80
|
+
const path = join(tmpdir(), `cc-e2e-state-${process.pid}.json`);
|
|
81
|
+
try {
|
|
82
|
+
const state = createState('engagement.yaml');
|
|
83
|
+
recordAnswer(state, 'mail_from', 'hello@carolinalaw.com');
|
|
84
|
+
await saveState(path, state);
|
|
85
|
+
const reloaded = await loadState(path);
|
|
86
|
+
assert.strictEqual(reloaded.answers.mail_from.value, 'hello@carolinalaw.com');
|
|
87
|
+
assert.ok(reloaded.updated_at);
|
|
88
|
+
} finally {
|
|
89
|
+
try { rmSync(path); } catch {}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// --- 2. Transport round-trip: consultant sync <-> client, via real instructions ---
|
|
94
|
+
|
|
95
|
+
test('consultant sync reaches the client inbox (transport -> server)', async () => {
|
|
96
|
+
const send = buildApiSendInstruction('packet-payload-001', { ...cfg(TOK_CONSULTANT), message_type: 'packet' });
|
|
97
|
+
assert.strictEqual(send.url, `${server.base}/api/messages`);
|
|
98
|
+
assert.strictEqual((await exec(send)).status, 201);
|
|
99
|
+
|
|
100
|
+
const recv = await exec(buildApiReceiveInstruction(cfg(TOK_CLIENT)));
|
|
101
|
+
assert.strictEqual(recv.status, 200);
|
|
102
|
+
assert.strictEqual(recv.json.messages.length, 1);
|
|
103
|
+
assert.strictEqual(recv.json.messages[0].payload, 'packet-payload-001');
|
|
104
|
+
assert.strictEqual(recv.json.messages[0].from_role, 'consultant');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('client response reaches the consultant inbox', async () => {
|
|
108
|
+
assert.strictEqual((await exec(buildApiSendInstruction('client-feedback-001', { ...cfg(TOK_CLIENT), message_type: 'item_feedback' }))).status, 201);
|
|
109
|
+
const recv = await exec(buildApiReceiveInstruction(cfg(TOK_CONSULTANT), { type: 'item_feedback' }));
|
|
110
|
+
assert.strictEqual(recv.json.messages.length, 1);
|
|
111
|
+
assert.strictEqual(recv.json.messages[0].payload, 'client-feedback-001');
|
|
112
|
+
assert.strictEqual(recv.json.messages[0].from_role, 'client');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// --- 3. Encrypted credential round-trip (the /setup-accounts secure path) ---
|
|
116
|
+
|
|
117
|
+
test('credential is encrypted client-side, delivered, and only the consultant can decrypt it', async () => {
|
|
118
|
+
const SECRET = 'postmark-server-token-SUPER-SECRET';
|
|
119
|
+
const { publicKey, privateKey } = await generateKeypair();
|
|
120
|
+
|
|
121
|
+
// Client encrypts to the consultant's public key and sends the envelope.
|
|
122
|
+
const envelope = await encryptCredential(SECRET, publicKey);
|
|
123
|
+
const serialized = serializeEnvelope(envelope);
|
|
124
|
+
const send = buildApiSendInstruction(serialized, { ...cfg(TOK_CLIENT), message_type: 'credential' });
|
|
125
|
+
assert.strictEqual((await exec(send)).status, 201);
|
|
126
|
+
|
|
127
|
+
// The plaintext never travels in the clear.
|
|
128
|
+
assert.ok(!serialized.includes(SECRET), 'serialized envelope must not contain the plaintext');
|
|
129
|
+
assert.ok(!JSON.stringify(envelope).includes(SECRET), 'envelope fields must not contain the plaintext');
|
|
130
|
+
|
|
131
|
+
// Consultant receives the credential message and decrypts with the private key.
|
|
132
|
+
const recv = await exec(buildApiReceiveInstruction(cfg(TOK_CONSULTANT), { type: 'credential' }));
|
|
133
|
+
assert.strictEqual(recv.json.messages.length, 1);
|
|
134
|
+
const received = deserializeEnvelope(recv.json.messages[0].payload);
|
|
135
|
+
const decrypted = await decryptCredential(received, privateKey);
|
|
136
|
+
assert.strictEqual(decrypted, SECRET, 'consultant recovers the original secret');
|
|
137
|
+
|
|
138
|
+
// Wrong key cannot decrypt.
|
|
139
|
+
const other = await generateKeypair();
|
|
140
|
+
await assert.rejects(decryptCredential(received, other.privateKey), 'a different private key must fail to decrypt');
|
|
141
|
+
|
|
142
|
+
// The walkthrough records the credential as sent (by envelope id, not value).
|
|
143
|
+
const state = createState('engagement.yaml');
|
|
144
|
+
recordCredentialSent(state, 'postmark_token', envelope.envelope_id);
|
|
145
|
+
assert.strictEqual(state.answers.postmark_token.status, 'sent');
|
|
146
|
+
assert.strictEqual(state.answers.postmark_token.envelope_id, envelope.envelope_id);
|
|
147
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Shared harness for engagement-server integration tests.
|
|
2
|
+
// Spawns the real server.mjs against a throwaway SQLite DB and provides
|
|
3
|
+
// helpers to seed engagements/users/tokens and inspect the DB. Lets the
|
|
4
|
+
// tests exercise the deployed server code without touching any real
|
|
5
|
+
// engagement (every test runs against an isolated temp DB).
|
|
6
|
+
import Database from 'better-sqlite3';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import { readFileSync, rmSync } from 'node:fs';
|
|
10
|
+
import { join, dirname } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
export const SERVER_DIR = join(__dirname, '..');
|
|
16
|
+
|
|
17
|
+
export const sha = (raw) => createHash('sha256').update(raw).digest('hex');
|
|
18
|
+
|
|
19
|
+
export function tmpDbPath(name) {
|
|
20
|
+
return join(tmpdir(), `cc-eng-test-${name}-${process.pid}.db`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Create a fresh DB with the server's own schema applied. Returns an open
|
|
24
|
+
// handle the caller seeds, then closes before the server starts.
|
|
25
|
+
export function freshDb(dbPath) {
|
|
26
|
+
for (const suffix of ['', '-wal', '-shm']) { try { rmSync(dbPath + suffix); } catch {} }
|
|
27
|
+
const db = new Database(dbPath);
|
|
28
|
+
db.pragma('journal_mode = WAL');
|
|
29
|
+
db.exec(readFileSync(join(SERVER_DIR, 'schema.sql'), 'utf-8'));
|
|
30
|
+
db.pragma('user_version = 1');
|
|
31
|
+
return db;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function addEngagement(db, { id, name = id, authMode = 'local', authConfig = null }) {
|
|
35
|
+
db.prepare(`INSERT INTO engagements (id,name,auth_mode,auth_config) VALUES (?,?,?,?)`)
|
|
36
|
+
.run(id, name, authMode, authConfig);
|
|
37
|
+
}
|
|
38
|
+
export function addUser(db, { id, engagementId, name = id, email = null, role }) {
|
|
39
|
+
db.prepare(`INSERT INTO users (id,engagement_id,name,email,role) VALUES (?,?,?,?,?)`)
|
|
40
|
+
.run(id, engagementId, name, email, role);
|
|
41
|
+
}
|
|
42
|
+
export function addToken(db, { rawToken, userId, label = null }) {
|
|
43
|
+
db.prepare(`INSERT INTO api_tokens (token_hash,user_id,label) VALUES (?,?,?)`)
|
|
44
|
+
.run(sha(rawToken), userId, label);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Read-only DB peek (server holds the writer handle; WAL allows concurrent reads).
|
|
48
|
+
export function usersByEmail(dbPath, email) {
|
|
49
|
+
const db = new Database(dbPath, { readonly: true });
|
|
50
|
+
const rows = db.prepare(`SELECT id, role FROM users WHERE email = ?`).all(email);
|
|
51
|
+
db.close();
|
|
52
|
+
return rows;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
56
|
+
|
|
57
|
+
// Boot the real server.mjs against the throwaway DB and wait for /health.
|
|
58
|
+
export async function startServer({ dbPath, port }) {
|
|
59
|
+
const env = { ...process.env, DB_PATH: dbPath, PORT: String(port) };
|
|
60
|
+
delete env.RAILWAY_ENVIRONMENT; // skip HTTPS enforcement locally
|
|
61
|
+
const proc = spawn('node', ['server.mjs'], { cwd: SERVER_DIR, env });
|
|
62
|
+
let log = '';
|
|
63
|
+
proc.stdout.on('data', (d) => { log += d; });
|
|
64
|
+
proc.stderr.on('data', (d) => { log += d; });
|
|
65
|
+
const base = `http://127.0.0.1:${port}`;
|
|
66
|
+
for (let i = 0; i < 100; i++) {
|
|
67
|
+
try { if ((await fetch(`${base}/health`)).ok) return { base, stop: () => proc.kill('SIGKILL'), log: () => log }; } catch {}
|
|
68
|
+
await sleep(100);
|
|
69
|
+
}
|
|
70
|
+
proc.kill('SIGKILL');
|
|
71
|
+
throw new Error('engagement server did not become healthy:\n' + log);
|
|
72
|
+
}
|