create-claude-cabinet 0.6.2 → 0.6.4
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 +3 -2
- package/lib/copy.js +3 -1
- package/lib/metadata.js +6 -2
- package/package.json +1 -1
- package/templates/README.md +10 -9
- package/templates/briefing/_briefing-cabinet-template.md +3 -1
- package/templates/cabinet/committees.yaml +49 -0
- package/templates/cabinet/lifecycle.md +6 -2
- package/templates/cabinet/output-contract.md +3 -3
- package/templates/scripts/finding-schema.json +4 -4
- package/templates/scripts/load-triage-history.js +5 -5
- package/templates/scripts/merge-findings.js +6 -6
- package/templates/scripts/pib-db-schema.sql +1 -1
- package/templates/scripts/pib-db.js +6 -6
- package/templates/scripts/resolve-committees.js +218 -0
- package/templates/scripts/triage-ui.html +52 -51
- package/templates/skills/audit/SKILL.md +9 -4
- package/templates/skills/audit/phases/member-selection.md +10 -11
- package/templates/skills/cabinet-cc-health/SKILL.md +16 -9
- package/templates/skills/cc-upgrade/SKILL.md +16 -0
- package/templates/skills/debrief/SKILL.md +4 -4
- package/templates/skills/onboard/phases/post-onboard-audit.md +3 -2
- package/templates/skills/seed/SKILL.md +4 -1
- package/templates/skills/seed/phases/build-member.md +5 -2
- package/templates/cabinet/committees-template.yaml +0 -49
package/lib/cli.js
CHANGED
|
@@ -105,9 +105,10 @@ function detectProjectState(dir) {
|
|
|
105
105
|
const entries = fs.readdirSync(dir);
|
|
106
106
|
const signals = entries.filter(e => PROJECT_SIGNALS.includes(e));
|
|
107
107
|
const hasClaude = entries.includes('.claude');
|
|
108
|
-
const
|
|
108
|
+
const hasCcrc = fs.existsSync(path.join(dir, '.ccrc.json'));
|
|
109
|
+
const hasCorrc = fs.existsSync(path.join(dir, '.corrc.json'));
|
|
109
110
|
|
|
110
|
-
if (
|
|
111
|
+
if (hasCcrc || hasCorrc) return 'existing-install';
|
|
111
112
|
if (signals.length > 0) return 'existing-project';
|
|
112
113
|
// Allow a few dotfiles (e.g. .git) without calling it a project
|
|
113
114
|
if (entries.filter(e => !e.startsWith('.')).length === 0) return 'empty';
|
package/lib/copy.js
CHANGED
|
@@ -64,7 +64,9 @@ async function walkAndCopy(srcRoot, destRoot, currentSrc, results, dryRun, skipC
|
|
|
64
64
|
results.manifest[relPath] = incomingHash;
|
|
65
65
|
} else {
|
|
66
66
|
results.skipped.push(relPath);
|
|
67
|
-
|
|
67
|
+
// Record the hash of what's actually on disk, not the template —
|
|
68
|
+
// otherwise the manifest lies about file content after a skip.
|
|
69
|
+
results.manifest[relPath] = hashContent(existing);
|
|
68
70
|
}
|
|
69
71
|
continue;
|
|
70
72
|
}
|
package/lib/metadata.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
4
|
const METADATA_FILE = '.ccrc.json';
|
|
5
|
+
const LEGACY_METADATA_FILE = '.corrc.json';
|
|
5
6
|
|
|
6
7
|
function metadataPath(projectDir) {
|
|
7
8
|
return path.join(projectDir, METADATA_FILE);
|
|
@@ -9,8 +10,11 @@ function metadataPath(projectDir) {
|
|
|
9
10
|
|
|
10
11
|
function read(projectDir) {
|
|
11
12
|
const file = metadataPath(projectDir);
|
|
12
|
-
if (
|
|
13
|
-
|
|
13
|
+
if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
14
|
+
// Fall back to legacy manifest from pre-v0.6.0 installs
|
|
15
|
+
const legacyFile = path.join(projectDir, LEGACY_METADATA_FILE);
|
|
16
|
+
if (fs.existsSync(legacyFile)) return JSON.parse(fs.readFileSync(legacyFile, 'utf8'));
|
|
17
|
+
return null;
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
function write(projectDir, data) {
|
package/package.json
CHANGED
package/templates/README.md
CHANGED
|
@@ -117,7 +117,7 @@ execution, audit). Each is a named domain expert encoded in markdown.
|
|
|
117
117
|
| `_briefing-template.md` | 1+2 | Template for the project-specific briefing every cabinet member reads. Wave 2 added: Scan Scopes, API Configuration, Entity Types, User Context. |
|
|
118
118
|
| `_prompt-guide.md` | 2 | Craft knowledge for writing cabinet member prompts. 17 principles including Levitin's cognitive architecture. |
|
|
119
119
|
| `output-contract.md` | 5 | How cabinet members produce structured findings for the audit system. Defines the assumption/evidence/question triad, severity calibration, positive findings, autoFixable field, JSON output format. |
|
|
120
|
-
| `committees
|
|
120
|
+
| `committees.yaml` | 5 | Upstream committee definitions for organizing cabinet members (ux, code, health, process, strategic). Upstream-owned; project customizations go in `committees-project.yaml`. |
|
|
121
121
|
| `_lifecycle.md` | 5 | When to adopt, retire, and assess cabinet members. Cross-cutting vs grouped distinction. |
|
|
122
122
|
|
|
123
123
|
### Memory (1 pattern + 1 template)
|
|
@@ -281,7 +281,7 @@ when you outgrow them.
|
|
|
281
281
|
3. **Copy cabinet member infrastructure:**
|
|
282
282
|
```bash
|
|
283
283
|
cp process-in-a-box/skills/cabinet/output-contract.md .claude/skills/cabinet/
|
|
284
|
-
cp process-in-a-box/skills/cabinet/committees
|
|
284
|
+
cp process-in-a-box/skills/cabinet/committees.yaml .claude/skills/cabinet/
|
|
285
285
|
cp process-in-a-box/skills/cabinet/_lifecycle.md .claude/skills/cabinet/
|
|
286
286
|
```
|
|
287
287
|
|
|
@@ -294,11 +294,11 @@ when you outgrow them.
|
|
|
294
294
|
cp process-in-a-box/scripts/finding-schema.json your-project/scripts/
|
|
295
295
|
```
|
|
296
296
|
|
|
297
|
-
5. **
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
297
|
+
5. **Customize committees** (optional):
|
|
298
|
+
Create a `committees-project.yaml` in `.claude/cabinet/` to add custom
|
|
299
|
+
members or committees. The upstream `committees.yaml` is installed
|
|
300
|
+
automatically. Run `node scripts/resolve-committees.js` to see the
|
|
301
|
+
merged result.
|
|
302
302
|
|
|
303
303
|
6. **Run your first audit:** `/audit` — it discovers cabinet members, runs
|
|
304
304
|
them, and persists findings. Then `/triage-audit` to review results.
|
|
@@ -346,8 +346,9 @@ These are the project-specific pieces to build as you adopt:
|
|
|
346
346
|
checks as you discover what drifts.
|
|
347
347
|
- **Triage phase files** — Where findings come from, how to present them,
|
|
348
348
|
how to apply verdicts. Defaults use the local triage UI and pib-db.
|
|
349
|
-
- **Committees** —
|
|
350
|
-
|
|
349
|
+
- **Committees** — The upstream `committees.yaml` is installed automatically.
|
|
350
|
+
Create `committees-project.yaml` to add custom members to upstream
|
|
351
|
+
committees or define new project-specific committees.
|
|
351
352
|
|
|
352
353
|
None of this requires working alone. Claude is the constant companion
|
|
353
354
|
throughout — the package provides the structure, and you build the
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
## Active Cabinet Members
|
|
4
4
|
|
|
5
5
|
*Which cabinet members are active in this project and how they're organized into committees.
|
|
6
|
-
Reference `committees.yaml`
|
|
6
|
+
Reference `committees.yaml` (upstream) and `committees-project.yaml` (project
|
|
7
|
+
customizations) for the canonical grouping. Run `node scripts/resolve-committees.js`
|
|
8
|
+
to see the merged result.*
|
|
7
9
|
|
|
8
10
|
## Portfolio Rules
|
|
9
11
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Cabinet member committees — canonical groupings of upstream cabinet members.
|
|
2
|
+
#
|
|
3
|
+
# This file is upstream-owned and manifest-tracked. It is updated automatically
|
|
4
|
+
# by the installer. Do not edit — your customizations go in committees-project.yaml.
|
|
5
|
+
#
|
|
6
|
+
# Consumed by:
|
|
7
|
+
# - /audit skill (committee selection menu)
|
|
8
|
+
# - scripts/resolve-committees.js (merges with committees-project.yaml)
|
|
9
|
+
#
|
|
10
|
+
# Cross-portfolio cabinet members are NOT in any committee. They activate via
|
|
11
|
+
# standing-mandate in their SKILL.md frontmatter and run on every audit:
|
|
12
|
+
# anti-confirmation, qa, debugger, organized-mind
|
|
13
|
+
|
|
14
|
+
committees:
|
|
15
|
+
ux:
|
|
16
|
+
name: "UX & Design"
|
|
17
|
+
members:
|
|
18
|
+
- usability
|
|
19
|
+
- accessibility
|
|
20
|
+
- small-screen
|
|
21
|
+
|
|
22
|
+
code:
|
|
23
|
+
name: "Code Quality"
|
|
24
|
+
members:
|
|
25
|
+
- technical-debt
|
|
26
|
+
- architecture
|
|
27
|
+
- boundary-man
|
|
28
|
+
|
|
29
|
+
health:
|
|
30
|
+
name: "System Health"
|
|
31
|
+
members:
|
|
32
|
+
- security
|
|
33
|
+
- data-integrity
|
|
34
|
+
- speed-freak
|
|
35
|
+
|
|
36
|
+
process:
|
|
37
|
+
name: "Process & Meta"
|
|
38
|
+
members:
|
|
39
|
+
- workflow-cop
|
|
40
|
+
- record-keeper
|
|
41
|
+
- process-therapist
|
|
42
|
+
- roster-check
|
|
43
|
+
- cc-health
|
|
44
|
+
|
|
45
|
+
strategic:
|
|
46
|
+
name: "Strategic"
|
|
47
|
+
members:
|
|
48
|
+
- system-advocate
|
|
49
|
+
- historian
|
|
@@ -60,7 +60,9 @@ of calibration quality.
|
|
|
60
60
|
|
|
61
61
|
## Cross-Portfolio vs Committee-Assigned
|
|
62
62
|
|
|
63
|
-
Most cabinet members belong in exactly one committee (see `committees
|
|
63
|
+
Most cabinet members belong in exactly one committee (see `committees.yaml`
|
|
64
|
+
for upstream committees and `committees-project.yaml` for project-specific
|
|
65
|
+
additions).
|
|
64
66
|
They cover a specific domain and stay in their portfolio.
|
|
65
67
|
|
|
66
68
|
**Cross-portfolio cabinet members** intentionally span domains. Their expertise
|
|
@@ -85,7 +87,9 @@ A cabinet member is a skill with `user-invocable: false`. Create it in
|
|
|
85
87
|
5. **Calibration Examples** — good findings, wrong-portfolio findings, severity
|
|
86
88
|
anchors
|
|
87
89
|
|
|
88
|
-
Add the cabinet member to your `committees.yaml` under the appropriate
|
|
90
|
+
Add the cabinet member to your `committees-project.yaml` under the appropriate
|
|
91
|
+
committee (using `additional_members` to append to an upstream committee, or
|
|
92
|
+
a new committee definition).
|
|
89
93
|
It's automatically discovered by the audit skill.
|
|
90
94
|
|
|
91
95
|
The best cabinet members emerge from real incidents and real audit findings.
|
|
@@ -84,7 +84,7 @@ these with `"type": "positive"`:
|
|
|
84
84
|
{
|
|
85
85
|
"id": "{cabinet-member}-p{NNNN}",
|
|
86
86
|
"type": "positive",
|
|
87
|
-
"
|
|
87
|
+
"cabinet-member": "{cabinet-member-name}",
|
|
88
88
|
"severity": "info",
|
|
89
89
|
"title": "Healthy subsystem confirmation",
|
|
90
90
|
"description": "What was checked and found healthy",
|
|
@@ -127,7 +127,7 @@ Return valid JSON matching `scripts/finding-schema.json`.
|
|
|
127
127
|
{
|
|
128
128
|
"id": "{cabinet-member}-{NNNN}",
|
|
129
129
|
"type": "finding",
|
|
130
|
-
"
|
|
130
|
+
"cabinet-member": "{cabinet-member-name}",
|
|
131
131
|
"severity": "critical|warn|info|idea",
|
|
132
132
|
"title": "Short description (max 120 chars)",
|
|
133
133
|
"description": "Full explanation",
|
|
@@ -138,7 +138,7 @@ Return valid JSON matching `scripts/finding-schema.json`.
|
|
|
138
138
|
}
|
|
139
139
|
],
|
|
140
140
|
"meta": {
|
|
141
|
-
"
|
|
141
|
+
"cabinet-member": "{cabinet-member-name}",
|
|
142
142
|
"timestamp": "ISO-8601"
|
|
143
143
|
}
|
|
144
144
|
}
|
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
"type": "array",
|
|
8
8
|
"items": {
|
|
9
9
|
"type": "object",
|
|
10
|
-
"required": ["id", "
|
|
10
|
+
"required": ["id", "cabinet-member", "severity", "title", "description", "autoFixable"],
|
|
11
11
|
"properties": {
|
|
12
12
|
"id": {
|
|
13
13
|
"type": "string",
|
|
14
14
|
"description": "Unique finding ID: {cabinet-member}-{NNNN}",
|
|
15
15
|
"pattern": "^[a-z-]+-\\d{4}$"
|
|
16
16
|
},
|
|
17
|
-
"
|
|
17
|
+
"cabinet-member": {
|
|
18
18
|
"type": "string",
|
|
19
19
|
"description": "Cabinet member name matching the directory name in skills/cabinet-*/",
|
|
20
20
|
"pattern": "^[a-z][a-z0-9-]+$"
|
|
@@ -81,9 +81,9 @@
|
|
|
81
81
|
},
|
|
82
82
|
"meta": {
|
|
83
83
|
"type": "object",
|
|
84
|
-
"required": ["
|
|
84
|
+
"required": ["cabinet-member", "timestamp"],
|
|
85
85
|
"properties": {
|
|
86
|
-
"
|
|
86
|
+
"cabinet-member": { "type": "string" },
|
|
87
87
|
"timestamp": { "type": "string", "format": "date-time" },
|
|
88
88
|
"commitHash": { "type": "string" },
|
|
89
89
|
"durationSeconds": { "type": "number" },
|
|
@@ -47,12 +47,12 @@ function tryDatabase() {
|
|
|
47
47
|
const db = new Database(DB_PATH, { readonly: true });
|
|
48
48
|
|
|
49
49
|
const rejected = db.prepare(`
|
|
50
|
-
SELECT id,
|
|
50
|
+
SELECT id, cabinet_member, title FROM audit_findings
|
|
51
51
|
WHERE triage_status = 'rejected'
|
|
52
52
|
`).all();
|
|
53
53
|
|
|
54
54
|
const deferred = db.prepare(`
|
|
55
|
-
SELECT id,
|
|
55
|
+
SELECT id, cabinet_member, title FROM audit_findings
|
|
56
56
|
WHERE triage_status = 'deferred'
|
|
57
57
|
`).all();
|
|
58
58
|
|
|
@@ -60,12 +60,12 @@ function tryDatabase() {
|
|
|
60
60
|
|
|
61
61
|
result.rejectedIds = rejected.map(r => r.id);
|
|
62
62
|
result.rejectedFingerprints = rejected.map(r => ({
|
|
63
|
-
|
|
63
|
+
'cabinet-member': r.cabinet_member,
|
|
64
64
|
title: r.title,
|
|
65
65
|
}));
|
|
66
66
|
result.deferredIds = deferred.map(r => r.id);
|
|
67
67
|
result.deferredFingerprints = deferred.map(r => ({
|
|
68
|
-
|
|
68
|
+
'cabinet-member': r.cabinet_member,
|
|
69
69
|
title: r.title,
|
|
70
70
|
}));
|
|
71
71
|
|
|
@@ -116,7 +116,7 @@ function tryFilesystem() {
|
|
|
116
116
|
const verdicts = data.verdicts || data;
|
|
117
117
|
|
|
118
118
|
for (const v of (Array.isArray(verdicts) ? verdicts : [])) {
|
|
119
|
-
const fp = {
|
|
119
|
+
const fp = { 'cabinet-member': v['cabinet-member'] || v.perspective, title: v.title };
|
|
120
120
|
|
|
121
121
|
if (v.verdict === 'reject' || v.status === 'rejected') {
|
|
122
122
|
if (v.id && !result.rejectedIds.includes(v.id)) {
|
|
@@ -54,7 +54,7 @@ if (files.length === 0) {
|
|
|
54
54
|
|
|
55
55
|
const allFindings = [];
|
|
56
56
|
const seenIds = new Set();
|
|
57
|
-
const
|
|
57
|
+
const memberCounts = {};
|
|
58
58
|
const severityCounts = { critical: 0, warn: 0, info: 0, idea: 0 };
|
|
59
59
|
let positiveCount = 0;
|
|
60
60
|
|
|
@@ -62,7 +62,7 @@ for (const file of files) {
|
|
|
62
62
|
try {
|
|
63
63
|
const data = JSON.parse(readFileSync(join(runDir, file), 'utf-8'));
|
|
64
64
|
const findings = data.findings || [];
|
|
65
|
-
const
|
|
65
|
+
const member = data.meta?.['cabinet-member'] || basename(file, '.json');
|
|
66
66
|
|
|
67
67
|
for (const f of findings) {
|
|
68
68
|
if (seenIds.has(f.id)) continue;
|
|
@@ -74,11 +74,11 @@ for (const file of files) {
|
|
|
74
74
|
positiveCount++;
|
|
75
75
|
} else {
|
|
76
76
|
severityCounts[f.severity] = (severityCounts[f.severity] || 0) + 1;
|
|
77
|
-
|
|
77
|
+
memberCounts[member] = (memberCounts[member] || 0) + 1;
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
console.log(` ${
|
|
81
|
+
console.log(` ${member}: ${findings.length} findings`);
|
|
82
82
|
} catch (err) {
|
|
83
83
|
console.error(` Error reading ${file}: ${err.message}`);
|
|
84
84
|
}
|
|
@@ -96,14 +96,14 @@ const summary = {
|
|
|
96
96
|
runId,
|
|
97
97
|
timestamp,
|
|
98
98
|
trigger: 'manual',
|
|
99
|
-
|
|
99
|
+
members: Object.keys(memberCounts),
|
|
100
100
|
counts: {
|
|
101
101
|
total: allFindings.length,
|
|
102
102
|
findings: allFindings.length - positiveCount,
|
|
103
103
|
positive: positiveCount,
|
|
104
104
|
...severityCounts,
|
|
105
105
|
},
|
|
106
|
-
|
|
106
|
+
byMember: memberCounts,
|
|
107
107
|
},
|
|
108
108
|
};
|
|
109
109
|
|
|
@@ -48,7 +48,7 @@ CREATE TABLE IF NOT EXISTS audit_runs (
|
|
|
48
48
|
CREATE TABLE IF NOT EXISTS audit_findings (
|
|
49
49
|
id TEXT PRIMARY KEY,
|
|
50
50
|
run_id TEXT NOT NULL REFERENCES audit_runs(id),
|
|
51
|
-
|
|
51
|
+
cabinet_member TEXT NOT NULL, -- renamed from 'perspective' in v0.7; migration: ALTER TABLE audit_findings RENAME COLUMN perspective TO cabinet_member
|
|
52
52
|
severity TEXT NOT NULL CHECK(severity IN ('critical','warn','info','idea')),
|
|
53
53
|
title TEXT NOT NULL,
|
|
54
54
|
description TEXT,
|
|
@@ -227,7 +227,7 @@ function ingestFindings(runDir) {
|
|
|
227
227
|
|
|
228
228
|
const insert = d.prepare(`
|
|
229
229
|
INSERT OR REPLACE INTO audit_findings
|
|
230
|
-
(id, run_id,
|
|
230
|
+
(id, run_id, cabinet_member, severity, title, description, assumption,
|
|
231
231
|
evidence, question, file, line, suggested_fix, auto_fixable, type)
|
|
232
232
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
233
233
|
`);
|
|
@@ -235,7 +235,7 @@ function ingestFindings(runDir) {
|
|
|
235
235
|
let count = 0;
|
|
236
236
|
for (const f of (data.findings || [])) {
|
|
237
237
|
insert.run(
|
|
238
|
-
f.id, runId, f
|
|
238
|
+
f.id, runId, f['cabinet-member'], f.severity, f.title,
|
|
239
239
|
f.description || null, f.assumption || null, f.evidence || null,
|
|
240
240
|
f.question || null, f.file || null, f.line || null,
|
|
241
241
|
f.suggestedFix || null, f.autoFixable ? 1 : 0, f.type || 'finding'
|
|
@@ -262,20 +262,20 @@ function triageHistory() {
|
|
|
262
262
|
const d = getDb();
|
|
263
263
|
|
|
264
264
|
const rejected = d.prepare(`
|
|
265
|
-
SELECT id,
|
|
265
|
+
SELECT id, cabinet_member, title FROM audit_findings
|
|
266
266
|
WHERE triage_status = 'rejected'
|
|
267
267
|
`).all();
|
|
268
268
|
|
|
269
269
|
const deferred = d.prepare(`
|
|
270
|
-
SELECT id,
|
|
270
|
+
SELECT id, cabinet_member, title FROM audit_findings
|
|
271
271
|
WHERE triage_status = 'deferred'
|
|
272
272
|
`).all();
|
|
273
273
|
|
|
274
274
|
const result = {
|
|
275
275
|
rejectedIds: rejected.map(r => r.id),
|
|
276
|
-
rejectedFingerprints: rejected.map(r => ({
|
|
276
|
+
rejectedFingerprints: rejected.map(r => ({ 'cabinet-member': r.cabinet_member, title: r.title })),
|
|
277
277
|
deferredIds: deferred.map(r => r.id),
|
|
278
|
-
deferredFingerprints: deferred.map(r => ({
|
|
278
|
+
deferredFingerprints: deferred.map(r => ({ 'cabinet-member': r.cabinet_member, title: r.title })),
|
|
279
279
|
};
|
|
280
280
|
console.log(JSON.stringify(result, null, 2));
|
|
281
281
|
return result;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* resolve-committees.js — Merge upstream committees.yaml with project committees-project.yaml
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node scripts/resolve-committees.js [path-to-claude-dir]
|
|
9
|
+
*
|
|
10
|
+
* Default path: .claude
|
|
11
|
+
*
|
|
12
|
+
* Reads:
|
|
13
|
+
* <claude-dir>/cabinet/committees.yaml (upstream, required)
|
|
14
|
+
* <claude-dir>/cabinet/committees-project.yaml (project, optional)
|
|
15
|
+
*
|
|
16
|
+
* Merge rules:
|
|
17
|
+
* - Project `additional_members` appends to upstream member lists
|
|
18
|
+
* - Project `members` replaces upstream member lists entirely
|
|
19
|
+
* - Project `skip: true` removes that committee
|
|
20
|
+
* - Project `name` overrides upstream display name
|
|
21
|
+
* - New committees in project file are added
|
|
22
|
+
*
|
|
23
|
+
* Outputs merged JSON to stdout.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
|
|
29
|
+
// --- Simple YAML parser (handles only what we need) ---
|
|
30
|
+
|
|
31
|
+
function parseSimpleYaml(text) {
|
|
32
|
+
// Our YAML is exactly 3 levels deep with 2-space indentation:
|
|
33
|
+
// committees: (indent 0 — top level)
|
|
34
|
+
// ux: (indent 2 — committee name)
|
|
35
|
+
// name: "UX" (indent 4 — property)
|
|
36
|
+
// members: (indent 4 — property, value is list below)
|
|
37
|
+
// - usability (indent 6+ — list item)
|
|
38
|
+
const result = {};
|
|
39
|
+
const lines = text.split('\n');
|
|
40
|
+
let topKey = null; // e.g., 'committees'
|
|
41
|
+
let committeeKey = null; // e.g., 'ux'
|
|
42
|
+
let propKey = null; // e.g., 'members'
|
|
43
|
+
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
const trimmed = line.trimEnd();
|
|
46
|
+
if (trimmed === '' || /^\s*#/.test(trimmed)) continue;
|
|
47
|
+
|
|
48
|
+
const indent = line.length - line.trimStart().length;
|
|
49
|
+
const content = trimmed.trim();
|
|
50
|
+
|
|
51
|
+
if (indent === 0) {
|
|
52
|
+
const m = content.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
53
|
+
if (m) {
|
|
54
|
+
topKey = m[1];
|
|
55
|
+
result[topKey] = {};
|
|
56
|
+
committeeKey = null;
|
|
57
|
+
propKey = null;
|
|
58
|
+
}
|
|
59
|
+
} else if (indent >= 2 && indent < 4 && topKey) {
|
|
60
|
+
// Committee name level
|
|
61
|
+
const m = content.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
62
|
+
if (m) {
|
|
63
|
+
committeeKey = m[1];
|
|
64
|
+
const val = m[2].trim();
|
|
65
|
+
if (val === 'true' || val === 'false') {
|
|
66
|
+
result[topKey][committeeKey] = parseValue(val);
|
|
67
|
+
} else {
|
|
68
|
+
result[topKey][committeeKey] = {};
|
|
69
|
+
}
|
|
70
|
+
propKey = null;
|
|
71
|
+
}
|
|
72
|
+
} else if (indent >= 4 && indent < 6 && topKey && committeeKey) {
|
|
73
|
+
// Property level (name, members, additional_members, skip)
|
|
74
|
+
const m = content.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
75
|
+
if (m) {
|
|
76
|
+
propKey = m[1];
|
|
77
|
+
const val = m[2].trim();
|
|
78
|
+
if (val && val !== '') {
|
|
79
|
+
result[topKey][committeeKey][propKey] = parseValue(val);
|
|
80
|
+
} else {
|
|
81
|
+
// Empty value — will be a list (members:) or object
|
|
82
|
+
result[topKey][committeeKey][propKey] = [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} else if (indent >= 6 && topKey && committeeKey && propKey) {
|
|
86
|
+
// List item
|
|
87
|
+
const m = content.match(/^- (.+)$/);
|
|
88
|
+
if (m) {
|
|
89
|
+
const container = result[topKey][committeeKey];
|
|
90
|
+
if (!Array.isArray(container[propKey])) {
|
|
91
|
+
container[propKey] = [];
|
|
92
|
+
}
|
|
93
|
+
container[propKey].push(m[1].trim());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseValue(str) {
|
|
102
|
+
if (str === 'true') return true;
|
|
103
|
+
if (str === 'false') return false;
|
|
104
|
+
if (str === 'null') return null;
|
|
105
|
+
// Strip quotes
|
|
106
|
+
if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) {
|
|
107
|
+
return str.slice(1, -1);
|
|
108
|
+
}
|
|
109
|
+
// Try number
|
|
110
|
+
if (/^-?\d+(\.\d+)?$/.test(str)) return Number(str);
|
|
111
|
+
return str;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --- Main ---
|
|
115
|
+
|
|
116
|
+
function main() {
|
|
117
|
+
const claudeDir = process.argv[2] || '.claude';
|
|
118
|
+
const upstreamPath = path.join(claudeDir, 'cabinet', 'committees.yaml');
|
|
119
|
+
const projectPath = path.join(claudeDir, 'cabinet', 'committees-project.yaml');
|
|
120
|
+
|
|
121
|
+
// Read upstream (required)
|
|
122
|
+
if (!fs.existsSync(upstreamPath)) {
|
|
123
|
+
console.error(`Error: Upstream committees file not found at ${upstreamPath}`);
|
|
124
|
+
console.error('');
|
|
125
|
+
console.error('This file is installed by Claude Cabinet and should be at:');
|
|
126
|
+
console.error(` ${upstreamPath}`);
|
|
127
|
+
console.error('');
|
|
128
|
+
console.error('Run the Claude Cabinet installer to restore it, or check that');
|
|
129
|
+
console.error('the path to your .claude directory is correct.');
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let upstreamText;
|
|
134
|
+
try {
|
|
135
|
+
upstreamText = fs.readFileSync(upstreamPath, 'utf8');
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error(`Error reading ${upstreamPath}: ${err.message}`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let upstream;
|
|
142
|
+
try {
|
|
143
|
+
upstream = parseSimpleYaml(upstreamText);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error(`Error parsing ${upstreamPath}: ${err.message}`);
|
|
146
|
+
console.error('The file may be malformed. Check that it uses valid YAML syntax.');
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const committees = upstream.committees || {};
|
|
151
|
+
|
|
152
|
+
// Read project overrides (optional)
|
|
153
|
+
if (!fs.existsSync(projectPath)) {
|
|
154
|
+
// No project file — output upstream as-is
|
|
155
|
+
console.log(JSON.stringify({ committees }, null, 2));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let projectText;
|
|
160
|
+
try {
|
|
161
|
+
projectText = fs.readFileSync(projectPath, 'utf8');
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error(`Error reading ${projectPath}: ${err.message}`);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let project;
|
|
168
|
+
try {
|
|
169
|
+
project = parseSimpleYaml(projectText);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.error(`Error parsing ${projectPath}: ${err.message}`);
|
|
172
|
+
console.error('The file may be malformed. Check that it uses valid YAML syntax.');
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const projectCommittees = project.committees || {};
|
|
177
|
+
|
|
178
|
+
// Merge
|
|
179
|
+
const merged = Object.assign({}, committees);
|
|
180
|
+
|
|
181
|
+
for (const key of Object.keys(projectCommittees)) {
|
|
182
|
+
const projectDef = projectCommittees[key];
|
|
183
|
+
|
|
184
|
+
// skip: true removes the committee
|
|
185
|
+
if (projectDef && projectDef.skip === true) {
|
|
186
|
+
delete merged[key];
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (merged[key]) {
|
|
191
|
+
// Existing upstream committee — apply overrides
|
|
192
|
+
if (projectDef.name) {
|
|
193
|
+
merged[key] = Object.assign({}, merged[key], { name: projectDef.name });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (Array.isArray(projectDef.members)) {
|
|
197
|
+
// Full replacement
|
|
198
|
+
merged[key] = Object.assign({}, merged[key], { members: projectDef.members });
|
|
199
|
+
} else if (Array.isArray(projectDef.additional_members)) {
|
|
200
|
+
// Append to upstream
|
|
201
|
+
const existing = Array.isArray(merged[key].members) ? merged[key].members : [];
|
|
202
|
+
merged[key] = Object.assign({}, merged[key], {
|
|
203
|
+
members: existing.concat(projectDef.additional_members)
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
// New committee from project
|
|
208
|
+
merged[key] = {
|
|
209
|
+
name: projectDef.name || key,
|
|
210
|
+
members: Array.isArray(projectDef.members) ? projectDef.members : []
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log(JSON.stringify({ committees: merged }, null, 2));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
main();
|
|
@@ -30,13 +30,13 @@
|
|
|
30
30
|
.run-info { color: #909296; font-size: 12px; }
|
|
31
31
|
|
|
32
32
|
/* Perspective groups */
|
|
33
|
-
.
|
|
33
|
+
.member-group {
|
|
34
34
|
margin-bottom: 16px;
|
|
35
35
|
border: 1px solid #373a40;
|
|
36
36
|
border-radius: 8px;
|
|
37
37
|
overflow: hidden;
|
|
38
38
|
}
|
|
39
|
-
.
|
|
39
|
+
.member-header {
|
|
40
40
|
display: flex;
|
|
41
41
|
align-items: center;
|
|
42
42
|
justify-content: space-between;
|
|
@@ -45,21 +45,21 @@
|
|
|
45
45
|
cursor: pointer;
|
|
46
46
|
user-select: none;
|
|
47
47
|
}
|
|
48
|
-
.
|
|
49
|
-
.
|
|
48
|
+
.member-header:hover { background: #2c2e33; }
|
|
49
|
+
.member-name {
|
|
50
50
|
font-weight: 600;
|
|
51
51
|
color: #e9ecef;
|
|
52
52
|
font-size: 14px;
|
|
53
53
|
}
|
|
54
|
-
.
|
|
54
|
+
.member-stats {
|
|
55
55
|
display: flex;
|
|
56
56
|
gap: 8px;
|
|
57
57
|
align-items: center;
|
|
58
58
|
font-size: 12px;
|
|
59
59
|
color: #909296;
|
|
60
60
|
}
|
|
61
|
-
.
|
|
62
|
-
.
|
|
61
|
+
.member-body { padding: 0; }
|
|
62
|
+
.member-body.collapsed { display: none; }
|
|
63
63
|
|
|
64
64
|
/* Finding rows */
|
|
65
65
|
.finding {
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
.feedback-input::placeholder { color: #5c5f66; }
|
|
137
137
|
|
|
138
138
|
/* Perspective-level controls */
|
|
139
|
-
.
|
|
139
|
+
.member-controls {
|
|
140
140
|
padding: 8px 14px;
|
|
141
141
|
background: #2c2e33;
|
|
142
142
|
border-top: 1px solid #373a40;
|
|
@@ -144,14 +144,14 @@
|
|
|
144
144
|
flex-direction: column;
|
|
145
145
|
gap: 6px;
|
|
146
146
|
}
|
|
147
|
-
.
|
|
147
|
+
.member-controls-row {
|
|
148
148
|
display: flex;
|
|
149
149
|
gap: 8px;
|
|
150
150
|
align-items: center;
|
|
151
151
|
font-size: 12px;
|
|
152
152
|
}
|
|
153
|
-
.
|
|
154
|
-
.
|
|
153
|
+
.member-controls-row label { color: #909296; flex-shrink: 0; }
|
|
154
|
+
.member-input {
|
|
155
155
|
flex: 1;
|
|
156
156
|
padding: 4px 8px;
|
|
157
157
|
border: 1px solid #373a40;
|
|
@@ -160,8 +160,8 @@
|
|
|
160
160
|
color: #c1c2c5;
|
|
161
161
|
font-size: 12px;
|
|
162
162
|
}
|
|
163
|
-
.
|
|
164
|
-
.
|
|
163
|
+
.member-input::placeholder { color: #5c5f66; }
|
|
164
|
+
.member-q-btn {
|
|
165
165
|
padding: 3px 10px;
|
|
166
166
|
border: 1px solid #1c7ed6;
|
|
167
167
|
border-radius: 4px;
|
|
@@ -172,8 +172,8 @@
|
|
|
172
172
|
font-weight: 500;
|
|
173
173
|
flex-shrink: 0;
|
|
174
174
|
}
|
|
175
|
-
.
|
|
176
|
-
.
|
|
175
|
+
.member-q-btn:hover { background: #1c7ed622; }
|
|
176
|
+
.member-q-btn.active { background: #1c7ed6; color: #fff; }
|
|
177
177
|
|
|
178
178
|
/* Bulk actions */
|
|
179
179
|
.bulk-bar {
|
|
@@ -284,7 +284,7 @@
|
|
|
284
284
|
// ── State ──
|
|
285
285
|
let findings = [];
|
|
286
286
|
const verdicts = {}; // { findingId: { verdict, feedback } }
|
|
287
|
-
const
|
|
287
|
+
const memberNotes = {}; // { member: { comment, question } }
|
|
288
288
|
|
|
289
289
|
// ── Public API for Claude (javascript_tool) ──
|
|
290
290
|
window.loadFindings = function(data) {
|
|
@@ -304,12 +304,12 @@
|
|
|
304
304
|
total: findings.length,
|
|
305
305
|
triaged: Object.keys(verdicts).length,
|
|
306
306
|
submitted: !!window._submitted,
|
|
307
|
-
|
|
308
|
-
Object.entries(
|
|
307
|
+
memberNotes: Object.fromEntries(
|
|
308
|
+
Object.entries(memberNotes).filter(([_, v]) => v.comment || v.question)
|
|
309
309
|
),
|
|
310
310
|
verdicts: findings.map(f => ({
|
|
311
311
|
id: f.id,
|
|
312
|
-
|
|
312
|
+
'cabinet-member': f['cabinet-member'],
|
|
313
313
|
severity: f.severity,
|
|
314
314
|
title: f.title,
|
|
315
315
|
verdict: verdicts[f.id]?.verdict || null,
|
|
@@ -347,32 +347,33 @@
|
|
|
347
347
|
document.getElementById('loading').style.display = 'none';
|
|
348
348
|
document.getElementById('bottom-bar').style.display = 'flex';
|
|
349
349
|
|
|
350
|
-
// Group by
|
|
350
|
+
// Group by cabinet member
|
|
351
351
|
const groups = {};
|
|
352
352
|
findings.forEach(f => {
|
|
353
|
-
|
|
354
|
-
groups[
|
|
353
|
+
const m = f['cabinet-member'];
|
|
354
|
+
if (!groups[m]) groups[m] = [];
|
|
355
|
+
groups[m].push(f);
|
|
355
356
|
});
|
|
356
357
|
|
|
357
358
|
const root = document.getElementById('findings-root');
|
|
358
359
|
root.innerHTML = '';
|
|
359
360
|
|
|
360
|
-
// Sort
|
|
361
|
-
const
|
|
361
|
+
// Sort members alphabetically
|
|
362
|
+
const members = Object.keys(groups).sort();
|
|
362
363
|
|
|
363
|
-
|
|
364
|
-
const items = groups[
|
|
364
|
+
members.forEach(member => {
|
|
365
|
+
const items = groups[member];
|
|
365
366
|
const triaged = items.filter(f => verdicts[f.id]?.verdict).length;
|
|
366
367
|
|
|
367
368
|
const group = document.createElement('div');
|
|
368
|
-
group.className = '
|
|
369
|
+
group.className = 'member-group';
|
|
369
370
|
|
|
370
371
|
// Header
|
|
371
372
|
const header = document.createElement('div');
|
|
372
|
-
header.className = '
|
|
373
|
+
header.className = 'member-header';
|
|
373
374
|
header.innerHTML = `
|
|
374
|
-
<span class="
|
|
375
|
-
<span class="
|
|
375
|
+
<span class="member-name">${member} (${items.length})</span>
|
|
376
|
+
<span class="member-stats">
|
|
376
377
|
${triaged}/${items.length} triaged
|
|
377
378
|
</span>
|
|
378
379
|
`;
|
|
@@ -383,7 +384,7 @@
|
|
|
383
384
|
|
|
384
385
|
// Body
|
|
385
386
|
const body = document.createElement('div');
|
|
386
|
-
body.className = '
|
|
387
|
+
body.className = 'member-body';
|
|
387
388
|
|
|
388
389
|
items.forEach(f => {
|
|
389
390
|
const row = document.createElement('div');
|
|
@@ -413,21 +414,21 @@
|
|
|
413
414
|
bulk.className = 'bulk-bar';
|
|
414
415
|
bulk.innerHTML = `
|
|
415
416
|
<label>Bulk:</label>
|
|
416
|
-
<button class="bulk-btn" data-p="${
|
|
417
|
-
<button class="bulk-btn" data-p="${
|
|
418
|
-
<button class="bulk-btn" data-p="${
|
|
417
|
+
<button class="bulk-btn" data-p="${member}" data-v="fix">All Fix</button>
|
|
418
|
+
<button class="bulk-btn" data-p="${member}" data-v="defer">All Defer</button>
|
|
419
|
+
<button class="bulk-btn" data-p="${member}" data-v="reject">All Reject</button>
|
|
419
420
|
`;
|
|
420
421
|
body.appendChild(bulk);
|
|
421
422
|
|
|
422
|
-
//
|
|
423
|
-
const pn =
|
|
423
|
+
// Member-level controls
|
|
424
|
+
const pn = memberNotes[member] || { comment: '', question: false };
|
|
424
425
|
const pControls = document.createElement('div');
|
|
425
|
-
pControls.className = '
|
|
426
|
+
pControls.className = 'member-controls';
|
|
426
427
|
pControls.innerHTML = `
|
|
427
|
-
<div class="
|
|
428
|
-
<button class="
|
|
429
|
-
<input class="
|
|
430
|
-
placeholder="${pn.question ? 'your question about this
|
|
428
|
+
<div class="member-controls-row">
|
|
429
|
+
<button class="member-q-btn ${pn.question ? 'active' : ''}" data-member="${member}">?</button>
|
|
430
|
+
<input class="member-input" data-member="${member}"
|
|
431
|
+
placeholder="${pn.question ? 'your question about this member...' : 'comment on this member...'}"
|
|
431
432
|
value="${esc(pn.comment || '')}">
|
|
432
433
|
</div>
|
|
433
434
|
`;
|
|
@@ -479,9 +480,9 @@
|
|
|
479
480
|
}
|
|
480
481
|
// Bulk buttons
|
|
481
482
|
if (e.target.classList.contains('bulk-btn')) {
|
|
482
|
-
const
|
|
483
|
+
const member = e.target.dataset.p;
|
|
483
484
|
const v = e.target.dataset.v;
|
|
484
|
-
findings.filter(f => f
|
|
485
|
+
findings.filter(f => f['cabinet-member'] === member).forEach(f => {
|
|
485
486
|
if (!verdicts[f.id]) verdicts[f.id] = { verdict: '', feedback: '' };
|
|
486
487
|
verdicts[f.id].verdict = v;
|
|
487
488
|
});
|
|
@@ -490,10 +491,10 @@
|
|
|
490
491
|
});
|
|
491
492
|
|
|
492
493
|
document.addEventListener('click', e => {
|
|
493
|
-
if (e.target.classList.contains('
|
|
494
|
-
const p = e.target.dataset.
|
|
495
|
-
if (!
|
|
496
|
-
|
|
494
|
+
if (e.target.classList.contains('member-q-btn')) {
|
|
495
|
+
const p = e.target.dataset.member;
|
|
496
|
+
if (!memberNotes[p]) memberNotes[p] = { comment: '', question: false };
|
|
497
|
+
memberNotes[p].question = !memberNotes[p].question;
|
|
497
498
|
render();
|
|
498
499
|
}
|
|
499
500
|
});
|
|
@@ -504,10 +505,10 @@
|
|
|
504
505
|
if (!verdicts[id]) verdicts[id] = { verdict: '', feedback: '' };
|
|
505
506
|
verdicts[id].feedback = e.target.value;
|
|
506
507
|
}
|
|
507
|
-
if (e.target.classList.contains('
|
|
508
|
-
const p = e.target.dataset.
|
|
509
|
-
if (!
|
|
510
|
-
|
|
508
|
+
if (e.target.classList.contains('member-input')) {
|
|
509
|
+
const p = e.target.dataset.member;
|
|
510
|
+
if (!memberNotes[p]) memberNotes[p] = { comment: '', question: false };
|
|
511
|
+
memberNotes[p].comment = e.target.value;
|
|
511
512
|
}
|
|
512
513
|
});
|
|
513
514
|
|
|
@@ -111,10 +111,15 @@ project. Adjust.
|
|
|
111
111
|
Read `phases/member-selection.md` for which cabinet members to run.
|
|
112
112
|
|
|
113
113
|
**Default (absent/empty):** Discover all cabinet members from
|
|
114
|
-
`skills/cabinet-*/SKILL.md`.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
`skills/cabinet-*/SKILL.md`. Run `node scripts/resolve-committees.js` to
|
|
115
|
+
get the merged committee list (upstream `cabinet/committees.yaml` merged
|
|
116
|
+
with project `cabinet/committees-project.yaml`). Present the merged
|
|
117
|
+
committees and let the user choose which to run. If neither committees
|
|
118
|
+
file exists, run all discovered cabinet members.
|
|
119
|
+
|
|
120
|
+
Cross-portfolio cabinet members (anti-confirmation, qa, debugger,
|
|
121
|
+
organized-mind) always run regardless of committee selection — they
|
|
122
|
+
activate via `standing-mandate` in their SKILL.md frontmatter.
|
|
118
123
|
|
|
119
124
|
The selection determines what the audit looks at. A full audit runs
|
|
120
125
|
everything; a focused audit runs one committee or a specific set of
|
|
@@ -4,9 +4,11 @@ Define how the audit selects which cabinet members to run. The /audit skill
|
|
|
4
4
|
reads this file before spawning cabinet member agents.
|
|
5
5
|
|
|
6
6
|
When this file is absent or empty, the default behavior is: discover all
|
|
7
|
-
cabinet members from `skills/cabinet-*/SKILL.md`,
|
|
8
|
-
`committees.
|
|
9
|
-
|
|
7
|
+
cabinet members from `skills/cabinet-*/SKILL.md`, run
|
|
8
|
+
`node scripts/resolve-committees.js` to merge upstream `cabinet/committees.yaml`
|
|
9
|
+
with project `cabinet/committees-project.yaml`, present merged committees,
|
|
10
|
+
otherwise run all. Cross-portfolio members always run regardless of selection.
|
|
11
|
+
To explicitly skip member selection (and run no audit), write only `skip: true`.
|
|
10
12
|
|
|
11
13
|
## What to Include
|
|
12
14
|
|
|
@@ -26,14 +28,11 @@ Discover all cabinet members in `skills/cabinet-*/SKILL.md`.
|
|
|
26
28
|
Run every one. Good for small projects with few cabinet members.
|
|
27
29
|
|
|
28
30
|
### Committee-Based Selection
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
4. process — workflow-cop, record-keeper
|
|
35
|
-
|
|
36
|
-
Cross-portfolio cabinet members (marked in committees.yaml) always run
|
|
31
|
+
Run `node scripts/resolve-committees.js` to merge upstream
|
|
32
|
+
`cabinet/committees.yaml` with project `cabinet/committees-project.yaml`.
|
|
33
|
+
Present merged committees to the user.
|
|
34
|
+
|
|
35
|
+
Cross-portfolio cabinet members (standing-mandate in SKILL.md) always run
|
|
37
36
|
regardless of committee selection.
|
|
38
37
|
|
|
39
38
|
### Targeted Audit
|
|
@@ -15,7 +15,8 @@ files:
|
|
|
15
15
|
- .claude/skills/*/SKILL.md
|
|
16
16
|
- .claude/skills/*/phases/*.md
|
|
17
17
|
- .claude/skills/cabinet-*/SKILL.md
|
|
18
|
-
- .claude/
|
|
18
|
+
- .claude/cabinet/committees.yaml
|
|
19
|
+
- .claude/cabinet/committees-project.yaml
|
|
19
20
|
- .claude/hooks/*.sh
|
|
20
21
|
topics:
|
|
21
22
|
- cc health
|
|
@@ -30,8 +31,11 @@ related:
|
|
|
30
31
|
path: .claude/skills/cabinet-*/_lifecycle.md
|
|
31
32
|
role: "Cabinet member lifecycle — when to adopt, when to retire"
|
|
32
33
|
- type: file
|
|
33
|
-
path: .claude/
|
|
34
|
-
role: "
|
|
34
|
+
path: .claude/cabinet/committees.yaml
|
|
35
|
+
role: "Upstream committee definitions (installer-owned)"
|
|
36
|
+
- type: file
|
|
37
|
+
path: .claude/cabinet/committees-project.yaml
|
|
38
|
+
role: "Project committee customizations (user-owned)"
|
|
35
39
|
- type: file
|
|
36
40
|
path: .claude/skills/cabinet-process-therapist/SKILL.md
|
|
37
41
|
role: "Adjacent cabinet member — skill quality (not adoption health)"
|
|
@@ -90,7 +94,7 @@ Also activates when:
|
|
|
90
94
|
- New skills or cabinet members are added (adoption decision to evaluate)
|
|
91
95
|
- Phase files are modified (customization to assess)
|
|
92
96
|
- Hook scripts are changed (enforcement layer shift)
|
|
93
|
-
- `committees.yaml` is updated (committee composition change)
|
|
97
|
+
- `committees.yaml` or `committees-project.yaml` is updated (committee composition change)
|
|
94
98
|
|
|
95
99
|
## Research Method
|
|
96
100
|
|
|
@@ -127,8 +131,10 @@ empty").
|
|
|
127
131
|
|
|
128
132
|
### 2. Cabinet Member Activation Patterns
|
|
129
133
|
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
Run `node scripts/resolve-committees.js` to get the merged committee list
|
|
135
|
+
(upstream `committees.yaml` + project `committees-project.yaml`). Also
|
|
136
|
+
validate that any members listed in `committees-project.yaml` actually
|
|
137
|
+
exist as `cabinet-*` directories. Cross-reference against:
|
|
132
138
|
|
|
133
139
|
- **Technology signals.** Does the project's stack imply cabinet members that
|
|
134
140
|
aren't activated? (React app without accessibility? API without security?
|
|
@@ -387,8 +393,9 @@ entropy.
|
|
|
387
393
|
### Scan Scope
|
|
388
394
|
|
|
389
395
|
- `.claude/skills/` — all skill definitions and phase files
|
|
390
|
-
- `.claude/skills/cabinet-*/` — all cabinet member definitions
|
|
391
|
-
|
|
396
|
+
- `.claude/skills/cabinet-*/` — all cabinet member definitions
|
|
397
|
+
- `.claude/cabinet/` — `committees.yaml`, `committees-project.yaml`,
|
|
398
|
+
`_lifecycle.md`, and other infrastructure
|
|
392
399
|
- `.claude/hooks/` — hook scripts
|
|
393
400
|
- `.claude/settings.json` — hook configuration
|
|
394
401
|
- `memory/patterns/` — enforcement pipeline state
|
|
@@ -409,7 +416,7 @@ Do not cross into adjacent cabinet members' territory:
|
|
|
409
416
|
That's process-therapist. You care whether the skill is *installed and used*,
|
|
410
417
|
not whether its output is good.
|
|
411
418
|
- **One-time setup** — initial CC installation, first-time skeleton
|
|
412
|
-
adoption, bootstrapping `committees.yaml`. That's the onboard skill. You
|
|
419
|
+
adoption, bootstrapping `committees-project.yaml`. That's the onboard skill. You
|
|
413
420
|
evaluate the ongoing health of an already-adopted configuration, not the
|
|
414
421
|
initial adoption process.
|
|
415
422
|
- **Specific technology expertise** — you don't know whether React components
|
|
@@ -305,6 +305,22 @@ The upgrade skill only surfaces phase opportunities that are *relevant
|
|
|
305
305
|
to what changed* in this upgrade — including cases where the project's
|
|
306
306
|
context makes a default obviously insufficient.
|
|
307
307
|
|
|
308
|
+
#### Committees Split Migration
|
|
309
|
+
If the project has a project-owned `committees.yaml` that contains custom
|
|
310
|
+
content (members not in the new upstream `committees.yaml`), split it:
|
|
311
|
+
1. Compare the project's existing `committees.yaml` against the new upstream
|
|
312
|
+
version to identify which members are upstream and which are custom
|
|
313
|
+
2. Generate `committees-project.yaml` with only the custom additions
|
|
314
|
+
(using `additional_members` for appending to upstream committees, or
|
|
315
|
+
new committee definitions for entirely custom committees)
|
|
316
|
+
3. Present the split to the user — show what goes in `committees-project.yaml`
|
|
317
|
+
and explain that the installer's `committees.yaml` is now upstream-owned
|
|
318
|
+
4. After approval, let the installer overwrite `committees.yaml` with the
|
|
319
|
+
upstream version, and write the custom content to `committees-project.yaml`
|
|
320
|
+
|
|
321
|
+
If the project's `committees.yaml` matches the upstream version exactly
|
|
322
|
+
(no custom content), no migration is needed.
|
|
323
|
+
|
|
308
324
|
#### Schema Migrations
|
|
309
325
|
If the upstream schema has new columns or tables:
|
|
310
326
|
- Detect the difference between the shipped schema and the project's DB
|
|
@@ -262,7 +262,7 @@ relevant expertise ever activates because nobody ran `/seed`.
|
|
|
262
262
|
touched. Did the work involve a framework, library, data store, or
|
|
263
263
|
infrastructure that isn't covered by any existing cabinet member? Compare
|
|
264
264
|
against the cabinet members in `.claude/skills/cabinet-*/` and the
|
|
265
|
-
committees
|
|
265
|
+
merged committees (run `node scripts/resolve-committees.js`).
|
|
266
266
|
|
|
267
267
|
Examples of what to catch:
|
|
268
268
|
- Session used Mantine components but there's no framework-quality
|
|
@@ -272,8 +272,8 @@ Examples of what to catch:
|
|
|
272
272
|
- Session built UI but accessibility isn't in any active committee
|
|
273
273
|
|
|
274
274
|
**Check B — Dormant cabinet member that should be active.** Are there
|
|
275
|
-
cabinet members installed in the project that aren't in any
|
|
276
|
-
|
|
275
|
+
cabinet members installed in the project that aren't in any committee
|
|
276
|
+
(check merged output from `node scripts/resolve-committees.js`), but based on the last few sessions, probably should be? A
|
|
277
277
|
cabinet member sitting dormant while the project does exactly the kind of
|
|
278
278
|
work it's designed to review is a waste.
|
|
279
279
|
|
|
@@ -303,7 +303,7 @@ or:
|
|
|
303
303
|
> sense as it grows. Want me to set that up?"
|
|
304
304
|
|
|
305
305
|
If the user says yes, either:
|
|
306
|
-
- Activate a dormant cabinet member (add it to `committees.yaml`)
|
|
306
|
+
- Activate a dormant cabinet member (add it to `committees-project.yaml`)
|
|
307
307
|
- Run `/seed` to build a new one
|
|
308
308
|
- Install a Tier 3 cabinet member that isn't in the project yet
|
|
309
309
|
|
|
@@ -45,7 +45,7 @@ generated phase files:
|
|
|
45
45
|
- **Skipped modules:** No phase file should reference a skipped module's
|
|
46
46
|
infrastructure. If work-tracking was skipped, no phase should reference
|
|
47
47
|
`pib.db` or `pib-db.js`. If audit was skipped, no phase should reference
|
|
48
|
-
`committees.yaml` or cabinet member activation.
|
|
48
|
+
`committees.yaml`, `committees-project.yaml`, or cabinet member activation.
|
|
49
49
|
- **Installed modules:** Each installed module should have at least a
|
|
50
50
|
minimal presence in the generated configuration. A module that's installed
|
|
51
51
|
but has zero phase file references is a configuration gap.
|
|
@@ -75,7 +75,8 @@ technologies against the cabinet member catalog:
|
|
|
75
75
|
- **Many skills (5+)** → roster-check
|
|
76
76
|
- **Features shipping regularly** → system-advocate
|
|
77
77
|
|
|
78
|
-
If implied cabinet members aren't in
|
|
78
|
+
If implied cabinet members aren't in the merged committees (upstream
|
|
79
|
+
`committees.yaml` + project `committees-project.yaml`), note
|
|
79
80
|
it as a recommendation — not a blocker. The user may have good reasons
|
|
80
81
|
to skip them, or may want to add them later via `/seed`.
|
|
81
82
|
|
|
@@ -137,7 +137,10 @@ collaborative expertise-building conversation:
|
|
|
137
137
|
4. Walk through Identity, Research Method, Boundaries, and Calibration
|
|
138
138
|
with the user
|
|
139
139
|
5. Create the file in `cabinet-{name}/SKILL.md`
|
|
140
|
-
6. Update `committees.yaml` if the cabinet member belongs in a committee
|
|
140
|
+
6. Update `committees-project.yaml` if the cabinet member belongs in a committee
|
|
141
|
+
(add to `additional_members` of an existing upstream committee, or create
|
|
142
|
+
a new committee definition). Create `committees-project.yaml` if it
|
|
143
|
+
doesn't exist yet.
|
|
141
144
|
|
|
142
145
|
This is co-authoring, not auto-generating. The user's domain knowledge
|
|
143
146
|
is what makes a cabinet member useful for their specific project. A generic
|
|
@@ -69,8 +69,11 @@ Write the cabinet member's `SKILL.md` following `_lifecycle.md` guidance:
|
|
|
69
69
|
|
|
70
70
|
### Step 4: Wire It Up
|
|
71
71
|
|
|
72
|
-
- Add to `committees.yaml` under the appropriate committee
|
|
73
|
-
|
|
72
|
+
- Add to `committees-project.yaml` under the appropriate committee — use
|
|
73
|
+
`additional_members` to append to an existing upstream committee, or create
|
|
74
|
+
a new committee definition. Create `committees-project.yaml` if it doesn't
|
|
75
|
+
exist yet. (Unless the cabinet member is cross-portfolio, in which case it
|
|
76
|
+
activates via `standing-mandate` signals and needs no committee entry.)
|
|
74
77
|
- Verify the cabinet member is discoverable by the audit skill
|
|
75
78
|
|
|
76
79
|
### Emphasis
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# Cabinet member committees — canonical mapping of committee slugs to cabinet member lists.
|
|
2
|
-
#
|
|
3
|
-
# Copy this file as committees.yaml and uncomment/customize the committees that
|
|
4
|
-
# match your project. Technology choices imply expertise needs: if your
|
|
5
|
-
# project has a UI, you need a UI committee. If it has an API, you need
|
|
6
|
-
# a system health committee.
|
|
7
|
-
#
|
|
8
|
-
# Consumed by:
|
|
9
|
-
# - /audit skill (committee selection menu)
|
|
10
|
-
# - scripts (--committee flag, resolve-committees helpers)
|
|
11
|
-
#
|
|
12
|
-
# Cross-portfolio cabinet members are NOT in any committee. They activate via
|
|
13
|
-
# standing-mandate in their SKILL.md frontmatter:
|
|
14
|
-
# anti-confirmation, qa, debugger, organized-mind
|
|
15
|
-
|
|
16
|
-
committees:
|
|
17
|
-
# -- If your project has a user interface --
|
|
18
|
-
# ux:
|
|
19
|
-
# name: "UX & Design"
|
|
20
|
-
# members:
|
|
21
|
-
# - accessibility
|
|
22
|
-
# - small-screen
|
|
23
|
-
# # Add framework-specific cabinet members (e.g., mantine-quality, tailwind-quality)
|
|
24
|
-
|
|
25
|
-
# -- If your project has code (most projects) --
|
|
26
|
-
# code:
|
|
27
|
-
# name: "Code Quality"
|
|
28
|
-
# members:
|
|
29
|
-
# - technical-debt
|
|
30
|
-
# - boundary-man
|
|
31
|
-
# # Add: architecture (if multi-layer system)
|
|
32
|
-
|
|
33
|
-
# -- If your project has an API, database, or infrastructure --
|
|
34
|
-
# health:
|
|
35
|
-
# name: "System Health"
|
|
36
|
-
# members:
|
|
37
|
-
# - security
|
|
38
|
-
# - data-integrity
|
|
39
|
-
# - speed-freak
|
|
40
|
-
# # Add: sync-health (if remote sync), deployment (if CI/CD)
|
|
41
|
-
|
|
42
|
-
# -- If your project has established process/methodology --
|
|
43
|
-
# process:
|
|
44
|
-
# name: "Process & Meta"
|
|
45
|
-
# members:
|
|
46
|
-
# - workflow-cop
|
|
47
|
-
# - record-keeper
|
|
48
|
-
# - process-therapist
|
|
49
|
-
# - cc-health # CC adoption health, configuration drift, anti-bloat
|