create-claude-cabinet 0.29.13 → 0.30.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 +16 -0
- package/package.json +1 -1
- package/templates/handoff/capture-and-encrypt.mjs +67 -0
- package/templates/handoff/decrypt-credential.mjs +72 -0
- package/templates/handoff/example-checklist.yaml +81 -0
- package/templates/handoff/generate-keys.mjs +35 -0
- package/templates/handoff/handoff-checklist.mjs +172 -0
- package/templates/handoff/handoff-crypto.mjs +102 -0
- package/templates/handoff/handoff-transport.mjs +105 -0
- package/templates/handoff/schema.md +92 -0
- package/templates/handoff/secure-input.mjs +99 -0
- package/templates/site-audit-runtime/package.json +1 -1
- package/templates/site-audit-runtime/src/checks/axe-core.mjs +13 -6
- package/templates/site-audit-runtime/src/checks/lighthouse.mjs +15 -1
- package/templates/site-audit-runtime/src/checks/pa11y.mjs +2 -2
- package/templates/site-audit-runtime/src/report.mjs +7 -0
- package/templates/skills/cabinet-mantine-quality/SKILL.md +21 -0
- package/templates/skills/handoff/SKILL.md +122 -0
- package/templates/skills/handoff-add/SKILL.md +44 -0
- package/templates/skills/handoff-ask/SKILL.md +45 -0
- package/templates/skills/handoff-create/SKILL.md +161 -0
- package/templates/skills/handoff-progress/SKILL.md +55 -0
- package/templates/skills/handoff-status/SKILL.md +86 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Handoff Checklist Schema
|
|
2
|
+
|
|
3
|
+
The checklist YAML (`handoff.yaml`) defines what to collect from a client.
|
|
4
|
+
It ships with the client's Claude Code plugin — the `/handoff` skill reads
|
|
5
|
+
it and walks the client through each section conversationally.
|
|
6
|
+
|
|
7
|
+
## Structure
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
meta:
|
|
11
|
+
title: "Project Go-Live Credentials" # shown to client at start
|
|
12
|
+
consultant: "Your Name" # who receives the credentials
|
|
13
|
+
public_key: "./keys/consultant.pub.jwk" # RSA public key for encryption
|
|
14
|
+
|
|
15
|
+
transport:
|
|
16
|
+
type: email # email | mcp | file
|
|
17
|
+
consultant: "you@example.com" # client sends here
|
|
18
|
+
client: "client@example.com" # consultant sends here
|
|
19
|
+
|
|
20
|
+
sections:
|
|
21
|
+
- key: section_id
|
|
22
|
+
title: "Section Title"
|
|
23
|
+
items:
|
|
24
|
+
- key: item_id
|
|
25
|
+
prompt: "What the client sees"
|
|
26
|
+
kind: decide # decide | provide | confirm | credential
|
|
27
|
+
options: [A, B, C] # only for decide kind
|
|
28
|
+
help: "Optional help text"
|
|
29
|
+
visibility:
|
|
30
|
+
depends_on: other_item_key
|
|
31
|
+
value_in: [A, B] # show this item only when parent matches
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Item Kinds
|
|
35
|
+
|
|
36
|
+
| Kind | Captured via | Stored as | Use for |
|
|
37
|
+
|------|-------------|-----------|---------|
|
|
38
|
+
| `decide` | Conversation (choice) | Plaintext value | Decisions that drive visibility |
|
|
39
|
+
| `provide` | Conversation (free text) | Plaintext value | Non-sensitive information |
|
|
40
|
+
| `confirm` | Conversation (yes/no) | Boolean | Acknowledgments |
|
|
41
|
+
| `credential` | OS dialog (hidden input) | Encrypted envelope ID | API keys, tokens, passwords |
|
|
42
|
+
|
|
43
|
+
Credential values are captured through a secure OS dialog, encrypted
|
|
44
|
+
immediately with the consultant's public key, and transported as opaque
|
|
45
|
+
envelopes. The plaintext never enters Claude's context or Anthropic's API.
|
|
46
|
+
|
|
47
|
+
## Visibility Rules
|
|
48
|
+
|
|
49
|
+
Items with a `visibility` block are hidden until their dependency is met.
|
|
50
|
+
Visibility is transitive: if B depends on A and C depends on B, hiding A
|
|
51
|
+
hides both B and C.
|
|
52
|
+
|
|
53
|
+
Cycles are rejected at load time (before any conversation starts). The
|
|
54
|
+
checklist parser runs topological sort on the dependency graph and fails
|
|
55
|
+
with a clear error if a cycle exists.
|
|
56
|
+
|
|
57
|
+
## Transport Types
|
|
58
|
+
|
|
59
|
+
**`email`** (default) — Auto-detects the connected email MCP server
|
|
60
|
+
(Gmail, Outlook, etc.) and dispatches through it. Falls back to `file`
|
|
61
|
+
if no email MCP is available.
|
|
62
|
+
|
|
63
|
+
**`mcp`** — Calls a specific MCP tool to POST the encrypted payload.
|
|
64
|
+
Requires additional config: `tool_name`, `payload_param`, `extra_params`.
|
|
65
|
+
|
|
66
|
+
**`file`** — Writes encrypted envelopes to `./handoff-out/`. Also serves
|
|
67
|
+
as automatic fallback when email transport has no MCP connected.
|
|
68
|
+
|
|
69
|
+
## State File
|
|
70
|
+
|
|
71
|
+
Progress is tracked in `handoff-state.json` (gitignored). Non-credential
|
|
72
|
+
answers store the value directly. Credential items store only the
|
|
73
|
+
envelope ID and delivery status — never the plaintext.
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"checklist": "handoff.yaml",
|
|
78
|
+
"started_at": "2026-05-30T12:00:00Z",
|
|
79
|
+
"updated_at": "2026-05-30T12:30:00Z",
|
|
80
|
+
"answers": {
|
|
81
|
+
"hosting_provider": {
|
|
82
|
+
"value": "Railway",
|
|
83
|
+
"answered_at": "2026-05-30T12:05:00Z"
|
|
84
|
+
},
|
|
85
|
+
"railway_token": {
|
|
86
|
+
"status": "sent",
|
|
87
|
+
"envelope_id": "env_abc123",
|
|
88
|
+
"sent_at": "2026-05-30T12:10:00Z"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
|
|
6
|
+
export class DialogCancelledError extends Error {
|
|
7
|
+
constructor() { super('User cancelled dialog'); this.code = 'CANCELLED'; }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class DialogUnavailableError extends Error {
|
|
11
|
+
constructor(platform) {
|
|
12
|
+
super(`No secure input dialog available on ${platform}`);
|
|
13
|
+
this.code = 'NO_DIALOG';
|
|
14
|
+
this.platform = platform;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function detectPlatform() {
|
|
19
|
+
switch (process.platform) {
|
|
20
|
+
case 'darwin': return 'macos';
|
|
21
|
+
case 'win32': return 'windows';
|
|
22
|
+
default: return 'linux';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function escapeAppleScript(str) {
|
|
27
|
+
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function captureViaMacOS(prompt) {
|
|
31
|
+
const escaped = escapeAppleScript(prompt);
|
|
32
|
+
const script = `display dialog "${escaped}" with hidden answer default answer ""`;
|
|
33
|
+
try {
|
|
34
|
+
const { stdout } = await execFileAsync('osascript', ['-e', script]);
|
|
35
|
+
const match = stdout.match(/text returned:(.*)/);
|
|
36
|
+
if (!match) throw new Error('Dialog returned no value');
|
|
37
|
+
return match[1].trim();
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (err.stderr?.includes('User canceled') || err.stderr?.includes('(-128)')) {
|
|
40
|
+
throw new DialogCancelledError();
|
|
41
|
+
}
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function captureViaZenity(prompt) {
|
|
47
|
+
try {
|
|
48
|
+
const { stdout } = await execFileAsync('zenity', ['--password', '--title', prompt]);
|
|
49
|
+
return stdout.trim();
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (err.code === 'ENOENT') throw new DialogUnavailableError('linux');
|
|
52
|
+
if (err.status === 1) throw new DialogCancelledError();
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function captureViaTerminal(prompt) {
|
|
58
|
+
process.stderr.write('Warning: No GUI dialog available — falling back to terminal input.\n');
|
|
59
|
+
process.stderr.write('Input will NOT be masked. Ensure no one is looking at your screen.\n');
|
|
60
|
+
const { createInterface } = await import('node:readline');
|
|
61
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
62
|
+
process.stderr.write(`${prompt}: `);
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
rl.question('', (answer) => {
|
|
65
|
+
rl.close();
|
|
66
|
+
process.stderr.write('\n');
|
|
67
|
+
resolve(answer);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function captureViaPowerShell(prompt) {
|
|
73
|
+
const escaped = prompt.replace(/'/g, "''");
|
|
74
|
+
const script = `$s = Read-Host -Prompt '${escaped}' -AsSecureString; [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($s))`;
|
|
75
|
+
try {
|
|
76
|
+
const { stdout } = await execFileAsync('powershell', ['-Command', script]);
|
|
77
|
+
return stdout.trim();
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (err.code === 'ENOENT') throw new DialogUnavailableError('windows');
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function captureSecureInput(prompt) {
|
|
85
|
+
const platform = detectPlatform();
|
|
86
|
+
switch (platform) {
|
|
87
|
+
case 'macos': return captureViaMacOS(prompt);
|
|
88
|
+
case 'windows': return captureViaPowerShell(prompt);
|
|
89
|
+
case 'linux': {
|
|
90
|
+
try {
|
|
91
|
+
return await captureViaZenity(prompt);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (err instanceof DialogUnavailableError) return captureViaTerminal(prompt);
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
default: throw new DialogUnavailableError(process.platform);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-cabinet/site-audit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Comprehensive deployed-site quality audit engine for Claude Cabinet. Runs checks across performance, accessibility, security, SEO, content, DNS, and privacy against a deployed URL; single-site and comparison modes; standalone HTML report.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,12 +33,19 @@ export function normalize(raw, durationMs) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const violations = Array.isArray(data) ? data.flatMap(p => p.violations || []) : (data.violations || []);
|
|
36
|
-
const findings = violations.map(v =>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
const findings = violations.map(v => {
|
|
37
|
+
const targets = (v.nodes || []).slice(0, 3).map(n =>
|
|
38
|
+
(n.target || []).flat().join(' > ')
|
|
39
|
+
).filter(Boolean);
|
|
40
|
+
return {
|
|
41
|
+
severity: IMPACT_TO_SEVERITY[v.impact] || 'info',
|
|
42
|
+
message: v.description || v.id,
|
|
43
|
+
url: v.helpUrl || undefined,
|
|
44
|
+
context: targets.length
|
|
45
|
+
? `Elements: ${targets.join(', ')}${v.nodes?.length > 3 ? ` (+${v.nodes.length - 3} more)` : ''}`
|
|
46
|
+
: (v.nodes?.[0]?.html?.slice(0, 200) || undefined),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
42
49
|
|
|
43
50
|
const worstSev = findings.length ? findings.reduce((w, f) => {
|
|
44
51
|
const o = { critical: 0, serious: 1, moderate: 2, info: 3 };
|
|
@@ -59,7 +59,21 @@ export function normalize(raw, durationMs) {
|
|
|
59
59
|
return (order[f.severity] ?? 3) < (order[w] ?? 3) ? f.severity : w;
|
|
60
60
|
}, 'info') : null;
|
|
61
61
|
|
|
62
|
-
const
|
|
62
|
+
const cwvKeys = {
|
|
63
|
+
'largest-contentful-paint': 'LCP',
|
|
64
|
+
'cumulative-layout-shift': 'CLS',
|
|
65
|
+
'first-contentful-paint': 'FCP',
|
|
66
|
+
'total-blocking-time': 'TBT',
|
|
67
|
+
'speed-index': 'Speed Index',
|
|
68
|
+
'interactive': 'TTI',
|
|
69
|
+
};
|
|
70
|
+
const metrics = {};
|
|
71
|
+
for (const [auditId, label] of Object.entries(cwvKeys)) {
|
|
72
|
+
const a = audits[auditId];
|
|
73
|
+
if (a?.displayValue) metrics[label] = a.displayValue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const details = { categories: scores, ...(Object.keys(metrics).length && { metrics }) };
|
|
63
77
|
const passSummary = Object.entries(scores)
|
|
64
78
|
.map(([k, v]) => `${k.replace(/-/g, ' ')}: ${v}`)
|
|
65
79
|
.join(', ');
|
|
@@ -28,8 +28,8 @@ export function normalize(raw, durationMs) {
|
|
|
28
28
|
const findings = issues.map(i => ({
|
|
29
29
|
severity: TYPE_TO_SEVERITY[i.type] || 'info',
|
|
30
30
|
message: i.message || i.code || 'unknown',
|
|
31
|
-
context: i.
|
|
32
|
-
url: i.
|
|
31
|
+
context: [i.code, i.selector ? `Element: ${i.selector}` : null].filter(Boolean).join(' — ') || undefined,
|
|
32
|
+
url: i.context?.slice(0, 150) || undefined,
|
|
33
33
|
}));
|
|
34
34
|
|
|
35
35
|
const errors = findings.filter(f => f.severity === 'serious' || f.severity === 'critical').length;
|
|
@@ -132,6 +132,13 @@ function renderDetails(result) {
|
|
|
132
132
|
});
|
|
133
133
|
html += `<div style="margin-bottom:.75rem;font-size:.9rem">${items.join('')}</div>`;
|
|
134
134
|
}
|
|
135
|
+
const metrics = result.details.metrics;
|
|
136
|
+
if (metrics && typeof metrics === 'object') {
|
|
137
|
+
const items = Object.entries(metrics).map(([label, value]) =>
|
|
138
|
+
`<span style="display:inline-block;margin-right:1.2rem"><strong>${esc(label)}</strong> ${esc(String(value))}</span>`
|
|
139
|
+
);
|
|
140
|
+
html += `<div style="margin-bottom:.75rem;font-size:.85rem;color:#555">Core Web Vitals: ${items.join('')}</div>`;
|
|
141
|
+
}
|
|
135
142
|
const types = result.details.types;
|
|
136
143
|
if (Array.isArray(types) && types.length) {
|
|
137
144
|
html += `<div style="margin-bottom:.75rem;font-size:.85rem;color:#555">Schema types: ${types.map(t => `<strong>${esc(String(t))}</strong>`).join(', ')}</div>`;
|
|
@@ -348,3 +348,24 @@ field-feedback. The CC maintainer adds it to this section. Project-specific
|
|
|
348
348
|
patterns that don't generalize stay in `patterns-project.md`.
|
|
349
349
|
|
|
350
350
|
<!-- Universal patterns below this line -->
|
|
351
|
+
|
|
352
|
+
### Collapse with `in={true}` on mount renders invisible children
|
|
353
|
+
|
|
354
|
+
**Pattern:** `<Collapse in={expr}>` where `expr` is truthy on initial
|
|
355
|
+
render. Children exist in the DOM but have height 0 — completely
|
|
356
|
+
invisible. Mantine's Collapse animates from closed→open, so mounting
|
|
357
|
+
already-open skips the animation and the height never resolves.
|
|
358
|
+
|
|
359
|
+
**Detection:** Flag any `<Collapse in={...}>` where the `in` prop can
|
|
360
|
+
be truthy on first render — e.g., `in={data.length > 0}`, `in={true}`,
|
|
361
|
+
`in={!!initialValue}`. Static `in={false}` or state that starts false
|
|
362
|
+
(`useState(false)`) is safe.
|
|
363
|
+
|
|
364
|
+
**Fix:** Replace `<Collapse in={opened}>...</Collapse>` with conditional
|
|
365
|
+
rendering: `{opened && <>...</>}`. Use Collapse only when you need the
|
|
366
|
+
open/close animation from a user interaction, not for initial-render
|
|
367
|
+
conditional display.
|
|
368
|
+
|
|
369
|
+
**Source:** claudeconsult-maginnis, 2026-05-30. Legal theory edit form's
|
|
370
|
+
"Default Content" section auto-expanded when data existed but all fields
|
|
371
|
+
were invisible. Required Playwright DOM inspection to diagnose.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: handoff
|
|
3
|
+
description: |
|
|
4
|
+
Walk through a consultant's handoff checklist conversationally. Collects
|
|
5
|
+
decisions, information, and credentials from the client with progressive
|
|
6
|
+
disclosure. Credentials are captured via secure OS dialog and encrypted
|
|
7
|
+
before Claude ever sees them. Use when: "handoff", "/handoff", or the
|
|
8
|
+
client needs to provide go-live credentials and configuration.
|
|
9
|
+
manual: true
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# /handoff — Complete Your Checklist
|
|
13
|
+
|
|
14
|
+
## Purpose
|
|
15
|
+
|
|
16
|
+
Walk the client through a structured checklist their consultant prepared.
|
|
17
|
+
Each item is asked conversationally — decisions drive which follow-up
|
|
18
|
+
items appear, credentials are captured securely, and progress is saved
|
|
19
|
+
so the client can leave and come back.
|
|
20
|
+
|
|
21
|
+
## Workflow
|
|
22
|
+
|
|
23
|
+
### 1. Load and Validate
|
|
24
|
+
|
|
25
|
+
1. Locate `handoff.yaml` — check in order:
|
|
26
|
+
a. Path provided as argument (the plugin's `/handoff` skill passes
|
|
27
|
+
the absolute path to the config — this is the primary path for
|
|
28
|
+
plugin-based handoffs)
|
|
29
|
+
b. Project root: `./handoff.yaml`
|
|
30
|
+
Use the first match found.
|
|
31
|
+
- **Missing:** "No handoff.yaml found — has the plugin been installed?"
|
|
32
|
+
- **Malformed:** Run validation via `handoff-checklist.mjs` and surface
|
|
33
|
+
specific errors before conversation starts.
|
|
34
|
+
- **Cycle detected:** "This checklist has a circular dependency between
|
|
35
|
+
[keys] — contact your consultant to fix it."
|
|
36
|
+
- **Public key missing:** Resolve `meta.public_key` relative to the
|
|
37
|
+
directory containing `handoff.yaml`. Check that it exists. A missing
|
|
38
|
+
key would only surface on the first credential item — catch it early.
|
|
39
|
+
|
|
40
|
+
2. **State file co-location:** The state file (`handoff-state.json`)
|
|
41
|
+
lives in the same directory as `handoff.yaml`. Derive the state path
|
|
42
|
+
from the checklist path: replace the filename. This ensures the
|
|
43
|
+
plugin's config and state stay together regardless of install location.
|
|
44
|
+
|
|
45
|
+
3. Read `handoff-state.json` if it exists (resume mode).
|
|
46
|
+
- **Corrupt state:** Offer "start fresh" vs "show me what's broken."
|
|
47
|
+
Never silently skip items.
|
|
48
|
+
|
|
49
|
+
4. **Terminal state check:** If all items are complete/sent, warn:
|
|
50
|
+
"This checklist was completed on [date]. Re-running will send
|
|
51
|
+
duplicate credentials. Continue?" Do not silently re-prompt.
|
|
52
|
+
|
|
53
|
+
5. Show progress summary: "You've completed N of M items. Let's pick up
|
|
54
|
+
with [next section]."
|
|
55
|
+
|
|
56
|
+
### 2. Check for Incoming Messages
|
|
57
|
+
|
|
58
|
+
Check the connected email MCP for incoming `[Handoff]` emails from the
|
|
59
|
+
consultant (notes, checklist updates, questions). Process and display
|
|
60
|
+
any new messages before starting the walk.
|
|
61
|
+
|
|
62
|
+
If no email MCP is connected, skip with no warning — the client may
|
|
63
|
+
be using file transport.
|
|
64
|
+
|
|
65
|
+
### 3. Walk Through Sections
|
|
66
|
+
|
|
67
|
+
Walk through sections in order. For each visible item:
|
|
68
|
+
|
|
69
|
+
**`decide`** — Present options conversationally. Record answer. After
|
|
70
|
+
answering, re-evaluate visibility — new items may appear. Tell the
|
|
71
|
+
client: "Great, since you chose [X], I'll need a few things from you
|
|
72
|
+
for that..."
|
|
73
|
+
|
|
74
|
+
**`provide`** — Ask for the value. Record in state.
|
|
75
|
+
|
|
76
|
+
**`confirm`** — Ask yes/no. Record in state.
|
|
77
|
+
|
|
78
|
+
**`credential`** — Explain what's needed. Show `help` text if present.
|
|
79
|
+
Invoke capture-and-encrypt:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
node .claude/handoff/capture-and-encrypt.mjs \
|
|
83
|
+
--prompt "<item prompt>" \
|
|
84
|
+
--public-key <absolute path to meta.public_key, resolved relative to handoff.yaml's directory>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
This returns ONLY the encrypted envelope (base64) on stdout. The
|
|
88
|
+
plaintext credential never enters this conversation or Anthropic's API.
|
|
89
|
+
|
|
90
|
+
**Important:** The stdout is a base64-serialized envelope. To send it
|
|
91
|
+
with the correct `envelope_id` in the email subject, base64-decode and
|
|
92
|
+
JSON-parse it to recover the envelope object before passing to
|
|
93
|
+
transport. The `envelope_id` field (e.g., `env_abc123`) is needed for
|
|
94
|
+
both the email subject and the state file entry.
|
|
95
|
+
|
|
96
|
+
Then send the envelope object via transport (using `handoff-transport.mjs`).
|
|
97
|
+
Pass `context.side = 'client'` so the transport addresses the email to
|
|
98
|
+
the consultant.
|
|
99
|
+
|
|
100
|
+
- **Transport failure:** Write `status: "send_failed"` + serialized
|
|
101
|
+
envelope to state so retry is possible without re-prompting.
|
|
102
|
+
- **Dialog cancel (exit code 2):** Skip item, note in state, continue.
|
|
103
|
+
- **Success:** Record `status: sent` + `envelope_id` in state. Tell
|
|
104
|
+
the client: "Got it — encrypted and sent. I never saw the value."
|
|
105
|
+
|
|
106
|
+
Save state after every answer (atomic write via `handoff-checklist.mjs`).
|
|
107
|
+
|
|
108
|
+
### 4. Completion
|
|
109
|
+
|
|
110
|
+
Show final summary: "All done! N of N items complete. [Consultant] will
|
|
111
|
+
be notified."
|
|
112
|
+
|
|
113
|
+
Send a `session_summary` with all non-credential answers via transport.
|
|
114
|
+
|
|
115
|
+
## Rules
|
|
116
|
+
|
|
117
|
+
- **Never** echo or reference a credential value in conversation
|
|
118
|
+
- **Never** store credential values in the state file
|
|
119
|
+
- If the client asks where to find something, teach them — this is
|
|
120
|
+
Claude's advantage over a form
|
|
121
|
+
- If the client needs to leave, reassure them progress is saved
|
|
122
|
+
- Show a progress table at the start and end of each section
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: handoff-add
|
|
3
|
+
description: |
|
|
4
|
+
Add items to an existing handoff checklist mid-engagement. Preserves
|
|
5
|
+
existing client progress. Use when: "handoff add", "/handoff-add",
|
|
6
|
+
"add items to the checklist", "I need something else from the client".
|
|
7
|
+
manual: true
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# /handoff-add — Add Items to an Existing Checklist
|
|
11
|
+
|
|
12
|
+
## Purpose
|
|
13
|
+
|
|
14
|
+
Amend a handoff checklist after the client has already started working
|
|
15
|
+
on it. New items appear as "not started" without affecting existing
|
|
16
|
+
progress.
|
|
17
|
+
|
|
18
|
+
## Workflow
|
|
19
|
+
|
|
20
|
+
1. Read existing `handoff.yaml`. If missing: "No checklist found. Run
|
|
21
|
+
`/handoff-create` to build one first."
|
|
22
|
+
|
|
23
|
+
2. Show current structure: sections and item counts.
|
|
24
|
+
|
|
25
|
+
3. Ask: "What do you need to add?"
|
|
26
|
+
- "Which section does this belong in?" (existing or new section)
|
|
27
|
+
- Same item-creation flow as `/handoff-create`: kind, prompt, help,
|
|
28
|
+
options, visibility rules.
|
|
29
|
+
- "Any more items to add?"
|
|
30
|
+
|
|
31
|
+
4. Re-validate the updated checklist:
|
|
32
|
+
- Cycle detection (new visibility rules may create cycles)
|
|
33
|
+
- Orphan dependency check
|
|
34
|
+
|
|
35
|
+
5. Write updated `handoff.yaml`.
|
|
36
|
+
|
|
37
|
+
6. Send a `checklist_update` notification to the client via transport:
|
|
38
|
+
- **Email subject:** `[Handoff] N new items added`
|
|
39
|
+
- **Email body:** Structured JSON with the new items and their
|
|
40
|
+
sections, so the client's `/handoff` or `/handoff-progress` can
|
|
41
|
+
surface them.
|
|
42
|
+
|
|
43
|
+
7. Confirm: "Added N items. [Client name] will see them next time they
|
|
44
|
+
run `/handoff` or `/handoff-progress`."
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: handoff-ask
|
|
3
|
+
description: |
|
|
4
|
+
Send a free-text message to the other side of a handoff engagement.
|
|
5
|
+
Works for both consultant and client. Use when: "handoff ask",
|
|
6
|
+
"/handoff-ask", "message the consultant", "message the client",
|
|
7
|
+
"ask Ed", "send a note".
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# /handoff-ask — Send a Message
|
|
11
|
+
|
|
12
|
+
## Purpose
|
|
13
|
+
|
|
14
|
+
Send a free-text message to the other party in a handoff engagement.
|
|
15
|
+
Works for both sides — the client can ask the consultant a question,
|
|
16
|
+
and the consultant can send a note to the client.
|
|
17
|
+
|
|
18
|
+
## Workflow
|
|
19
|
+
|
|
20
|
+
1. Read `handoff.yaml` for transport config and party identifiers.
|
|
21
|
+
- Determine which side we are: if `keys/consultant.priv.jwk.enc`
|
|
22
|
+
exists locally, we're the consultant. Otherwise we're the client.
|
|
23
|
+
(The public key exists on both sides — only the private key
|
|
24
|
+
distinguishes them.)
|
|
25
|
+
- Recipient is the other side's email from `transport` config.
|
|
26
|
+
|
|
27
|
+
2. Ask: "What do you want to say to [other party name]?"
|
|
28
|
+
|
|
29
|
+
3. Send as a `question` payload via transport:
|
|
30
|
+
- **Email subject:** `[Handoff] Question from [sender name]`
|
|
31
|
+
- **Email body:** The free-text message as structured JSON:
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"type": "question",
|
|
35
|
+
"from": "sender name",
|
|
36
|
+
"message": "the message text",
|
|
37
|
+
"sent_at": "ISO timestamp"
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
4. Confirm: "Message sent to [recipient name]."
|
|
42
|
+
|
|
43
|
+
5. If email transport falls back to file: "Message written to
|
|
44
|
+
`handoff-out/`. Transfer it manually or connect an email MCP
|
|
45
|
+
server."
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: handoff-create
|
|
3
|
+
description: |
|
|
4
|
+
Build a handoff checklist conversationally. Interviews the consultant
|
|
5
|
+
about what the client needs to provide, generates the YAML and keypair.
|
|
6
|
+
Use when: "handoff create", "/handoff-create", "create a handoff",
|
|
7
|
+
"build a checklist for the client", "set up handoff".
|
|
8
|
+
manual: true
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# /handoff-create — Build a Handoff Checklist
|
|
12
|
+
|
|
13
|
+
## Purpose
|
|
14
|
+
|
|
15
|
+
Interview the consultant to build the `handoff.yaml` checklist that
|
|
16
|
+
drives the client's `/handoff` experience. Generates the keypair for
|
|
17
|
+
credential encryption. This is where engagement-specific configuration
|
|
18
|
+
gets created.
|
|
19
|
+
|
|
20
|
+
## Workflow
|
|
21
|
+
|
|
22
|
+
### 1. Gather Basics
|
|
23
|
+
|
|
24
|
+
Ask one question at a time:
|
|
25
|
+
|
|
26
|
+
1. "Who's the client? What's their name and email?"
|
|
27
|
+
2. "What's this handoff for?" (becomes `meta.title`)
|
|
28
|
+
3. "What transport should we use? Email is the default — it works
|
|
29
|
+
with Gmail, Outlook, or any email MCP. Or file-based if you'll
|
|
30
|
+
transfer manually." (becomes `transport.type`)
|
|
31
|
+
4. "What's your email for receiving handoff items?" (consultant email)
|
|
32
|
+
|
|
33
|
+
### 2. Build Sections
|
|
34
|
+
|
|
35
|
+
"What categories of items does the client need to provide? For example:
|
|
36
|
+
Hosting, Email Service, Domain, Analytics..."
|
|
37
|
+
|
|
38
|
+
For each section:
|
|
39
|
+
1. "What does the client need to provide for [section]?"
|
|
40
|
+
2. For each item, determine:
|
|
41
|
+
- **Kind:** Is this a decision (choose from options), a credential
|
|
42
|
+
(API key, token, password), free-text information, or a
|
|
43
|
+
confirmation (yes/no)?
|
|
44
|
+
- **Prompt:** What should Claude ask the client?
|
|
45
|
+
- **Help text:** Where can the client find this? (optional)
|
|
46
|
+
- **Options:** If it's a decision, what are the choices?
|
|
47
|
+
- **Visibility:** "Does this item depend on a prior answer?"
|
|
48
|
+
If yes, which item and which value(s) make it visible?
|
|
49
|
+
3. "Any more items in [section]? Or ready for the next section?"
|
|
50
|
+
|
|
51
|
+
### 3. Validate
|
|
52
|
+
|
|
53
|
+
Run validation on the constructed checklist:
|
|
54
|
+
- Cycle detection on visibility graph
|
|
55
|
+
- Orphan dependency check (depends_on references a non-existent key)
|
|
56
|
+
- Duplicate key detection
|
|
57
|
+
|
|
58
|
+
If issues found, surface them and help the consultant fix them
|
|
59
|
+
interactively.
|
|
60
|
+
|
|
61
|
+
### 4. Generate
|
|
62
|
+
|
|
63
|
+
1. Write `handoff.yaml` to the project root
|
|
64
|
+
2. Check if `keys/consultant.pub.jwk` exists. If not, generate a
|
|
65
|
+
keypair:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
node .claude/handoff/generate-keys.mjs
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A secure OS dialog will prompt the consultant for a passphrase
|
|
72
|
+
(never enters conversation). Explain: "A dialog will appear — create
|
|
73
|
+
a passphrase to protect your private key. You'll need it when
|
|
74
|
+
retrieving credentials via `/handoff-status`."
|
|
75
|
+
|
|
76
|
+
3. Show summary:
|
|
77
|
+
```
|
|
78
|
+
Checklist: "Maginnis Go-Live Credentials"
|
|
79
|
+
Sections: 4 | Items: 12 (3 credentials, 5 decisions, 4 info)
|
|
80
|
+
Transport: email
|
|
81
|
+
Public key: keys/consultant.pub.jwk
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 5. Deploy to Client Plugin
|
|
85
|
+
|
|
86
|
+
Auto-detect the client plugin by scanning for `*/.claude-plugin/plugin.json`
|
|
87
|
+
from the project root (one level deep). If multiple plugins found, ask
|
|
88
|
+
which one. Always run this scan from the project root directory.
|
|
89
|
+
|
|
90
|
+
If a plugin is found:
|
|
91
|
+
|
|
92
|
+
1. Create a `handoff/` directory inside the plugin root (sibling to
|
|
93
|
+
`.claude-plugin/`, `skills/`, etc.):
|
|
94
|
+
```
|
|
95
|
+
<plugin-root>/handoff/handoff.yaml
|
|
96
|
+
<plugin-root>/handoff/consultant.pub.jwk
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
2. **Rewrite `meta.public_key`** in the deployed `handoff.yaml` copy.
|
|
100
|
+
The original says `./keys/consultant.pub.jwk` (relative to the
|
|
101
|
+
consultant's project). The deployed copy must say
|
|
102
|
+
`./consultant.pub.jwk` (relative to the deployed `handoff/`
|
|
103
|
+
directory where the key file now lives). Read the deployed YAML,
|
|
104
|
+
update the path, write it back.
|
|
105
|
+
|
|
106
|
+
3. Create a `/handoff` skill in the plugin's `skills/` directory.
|
|
107
|
+
This skill tells the client's Claude exactly where the config is
|
|
108
|
+
using a path relative to the skill file itself — this works
|
|
109
|
+
regardless of where Claude Code installs the plugin on the client's
|
|
110
|
+
machine:
|
|
111
|
+
|
|
112
|
+
**Write to `<plugin-root>/skills/handoff/SKILL.md`:**
|
|
113
|
+
```markdown
|
|
114
|
+
---
|
|
115
|
+
name: handoff
|
|
116
|
+
description: |
|
|
117
|
+
Provide go-live credentials and configuration for your consultant.
|
|
118
|
+
Credentials are captured via a secure dialog — they never enter
|
|
119
|
+
this conversation or reach Anthropic's servers.
|
|
120
|
+
Use when: "handoff", "/handoff", "provide credentials".
|
|
121
|
+
manual: true
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
# /handoff — Provide Your Credentials
|
|
125
|
+
|
|
126
|
+
This plugin bundles a handoff checklist from your consultant.
|
|
127
|
+
|
|
128
|
+
## Locating the config
|
|
129
|
+
|
|
130
|
+
This skill file lives at `skills/handoff/SKILL.md` within the
|
|
131
|
+
plugin. The handoff config is two levels up in the `handoff/`
|
|
132
|
+
directory:
|
|
133
|
+
|
|
134
|
+
- Checklist: `../../handoff/handoff.yaml` (relative to this file)
|
|
135
|
+
- Public key: `../../handoff/consultant.pub.jwk` (relative to this file)
|
|
136
|
+
|
|
137
|
+
To get the absolute paths: take this skill file's absolute path,
|
|
138
|
+
go up two directories, then into `handoff/`.
|
|
139
|
+
|
|
140
|
+
## What to do
|
|
141
|
+
|
|
142
|
+
1. Resolve the absolute path to `handoff.yaml` as described above.
|
|
143
|
+
2. Follow the `/handoff` skill workflow (from the project's CC
|
|
144
|
+
installation) using that path as the config location.
|
|
145
|
+
3. Read and write `handoff-state.json` in the same directory as
|
|
146
|
+
`handoff.yaml` (the plugin's `handoff/` directory).
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
4. Confirm: "Deployed to [plugin-name]. When the client installs the
|
|
150
|
+
plugin and runs `/handoff`, everything is ready."
|
|
151
|
+
|
|
152
|
+
If no plugin found:
|
|
153
|
+
|
|
154
|
+
Surface the manual steps:
|
|
155
|
+
```
|
|
156
|
+
No client plugin found in this project. To deploy manually:
|
|
157
|
+
1. Create a handoff/ directory in the client's plugin
|
|
158
|
+
2. Copy handoff.yaml into it (update meta.public_key to ./consultant.pub.jwk)
|
|
159
|
+
3. Copy keys/consultant.pub.jwk as handoff/consultant.pub.jwk
|
|
160
|
+
4. Add a /handoff skill to the plugin's skills/ directory (see schema.md)
|
|
161
|
+
```
|