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 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 hasPibrc = fs.existsSync(path.join(dir, '.ccrc.json'));
108
+ const hasCcrc = fs.existsSync(path.join(dir, '.ccrc.json'));
109
+ const hasCorrc = fs.existsSync(path.join(dir, '.corrc.json'));
109
110
 
110
- if (hasPibrc) return 'existing-install';
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
- results.manifest[relPath] = incomingHash;
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 (!fs.existsSync(file)) return null;
13
- return JSON.parse(fs.readFileSync(file, 'utf8'));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Claude Cabinet — opinionated process scaffolding for Claude Code projects",
5
5
  "bin": {
6
6
  "create-claude-cabinet": "bin/create-claude-cabinet.js"
@@ -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-template.yaml` | 5 | Technology-implied starter committees for organizing cabinet members (ux, code, health, process). Copy as `committees.yaml` and customize. |
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-template.yaml .claude/skills/cabinet/
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. **Set up committees** (optional):
298
- ```bash
299
- cp .claude/skills/cabinet/committees-template.yaml .claude/skills/cabinet/committees.yaml
300
- ```
301
- Uncomment the committees relevant to your project.
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** — Copy `committees-template.yaml` as `committees.yaml`
350
- and uncomment the committees relevant to your technology stack.
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` for the canonical grouping.*
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-template.yaml`).
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 committee.
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
- "perspective": "{cabinet-member-name}",
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
- "perspective": "{cabinet-member-name}",
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
- "perspective": "{cabinet-member-name}",
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", "perspective", "severity", "title", "description", "autoFixable"],
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
- "perspective": {
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": ["perspective", "timestamp"],
84
+ "required": ["cabinet-member", "timestamp"],
85
85
  "properties": {
86
- "perspective": { "type": "string" },
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, perspective, title FROM audit_findings
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, perspective, title FROM audit_findings
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
- perspective: r.perspective,
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
- perspective: r.perspective,
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 = { perspective: v.perspective, title: v.title };
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 perspectiveCounts = {};
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 perspective = data.meta?.perspective || basename(file, '.json');
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
- perspectiveCounts[perspective] = (perspectiveCounts[perspective] || 0) + 1;
77
+ memberCounts[member] = (memberCounts[member] || 0) + 1;
78
78
  }
79
79
  }
80
80
 
81
- console.log(` ${perspective}: ${findings.length} findings`);
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
- perspectives: Object.keys(perspectiveCounts),
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
- byPerspective: perspectiveCounts,
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
- perspective TEXT NOT NULL,
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, perspective, severity, title, description, assumption,
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.perspective, f.severity, f.title,
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, perspective, title FROM audit_findings
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, perspective, title FROM audit_findings
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 => ({ perspective: r.perspective, title: r.title })),
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 => ({ perspective: r.perspective, title: r.title })),
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
- .perspective-group {
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
- .perspective-header {
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
- .perspective-header:hover { background: #2c2e33; }
49
- .perspective-name {
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
- .perspective-stats {
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
- .perspective-body { padding: 0; }
62
- .perspective-body.collapsed { display: none; }
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
- .perspective-controls {
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
- .perspective-controls-row {
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
- .perspective-controls-row label { color: #909296; flex-shrink: 0; }
154
- .perspective-input {
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
- .perspective-input::placeholder { color: #5c5f66; }
164
- .perspective-q-btn {
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
- .perspective-q-btn:hover { background: #1c7ed622; }
176
- .perspective-q-btn.active { background: #1c7ed6; color: #fff; }
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 perspectiveNotes = {}; // { perspective: { comment, question } }
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
- perspectiveNotes: Object.fromEntries(
308
- Object.entries(perspectiveNotes).filter(([_, v]) => v.comment || v.question)
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
- perspective: f.perspective,
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 perspective
350
+ // Group by cabinet member
351
351
  const groups = {};
352
352
  findings.forEach(f => {
353
- if (!groups[f.perspective]) groups[f.perspective] = [];
354
- groups[f.perspective].push(f);
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 perspectives alphabetically
361
- const perspectives = Object.keys(groups).sort();
361
+ // Sort members alphabetically
362
+ const members = Object.keys(groups).sort();
362
363
 
363
- perspectives.forEach(perspective => {
364
- const items = groups[perspective];
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 = 'perspective-group';
369
+ group.className = 'member-group';
369
370
 
370
371
  // Header
371
372
  const header = document.createElement('div');
372
- header.className = 'perspective-header';
373
+ header.className = 'member-header';
373
374
  header.innerHTML = `
374
- <span class="perspective-name">${perspective} (${items.length})</span>
375
- <span class="perspective-stats">
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 = 'perspective-body';
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="${perspective}" data-v="fix">All Fix</button>
417
- <button class="bulk-btn" data-p="${perspective}" data-v="defer">All Defer</button>
418
- <button class="bulk-btn" data-p="${perspective}" data-v="reject">All Reject</button>
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
- // Perspective-level controls
423
- const pn = perspectiveNotes[perspective] || { comment: '', question: false };
423
+ // Member-level controls
424
+ const pn = memberNotes[member] || { comment: '', question: false };
424
425
  const pControls = document.createElement('div');
425
- pControls.className = 'perspective-controls';
426
+ pControls.className = 'member-controls';
426
427
  pControls.innerHTML = `
427
- <div class="perspective-controls-row">
428
- <button class="perspective-q-btn ${pn.question ? 'active' : ''}" data-perspective="${perspective}">?</button>
429
- <input class="perspective-input" data-perspective="${perspective}"
430
- placeholder="${pn.question ? 'your question about this perspective...' : 'comment on this perspective...'}"
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 perspective = e.target.dataset.p;
483
+ const member = e.target.dataset.p;
483
484
  const v = e.target.dataset.v;
484
- findings.filter(f => f.perspective === perspective).forEach(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('perspective-q-btn')) {
494
- const p = e.target.dataset.perspective;
495
- if (!perspectiveNotes[p]) perspectiveNotes[p] = { comment: '', question: false };
496
- perspectiveNotes[p].question = !perspectiveNotes[p].question;
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('perspective-input')) {
508
- const p = e.target.dataset.perspective;
509
- if (!perspectiveNotes[p]) perspectiveNotes[p] = { comment: '', question: false };
510
- perspectiveNotes[p].comment = e.target.value;
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`. If `cabinet/committees.yaml`
115
- exists (copied from `committees-template.yaml`), present committees and let the
116
- user choose which to run. If no committees file exists, run all discovered
117
- cabinet members.
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`, present committees if
8
- `committees.yaml` exists, otherwise run all. To explicitly skip member
9
- selection (and run no audit), write only `skip: true`.
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
- Read `cabinet/committees.yaml` for committee definitions.
30
- Present committees to the user:
31
- 1. ux accessibility, small-screen
32
- 2. code — technical-debt, architecture
33
- 3. health security, data-integrity, speed-freak
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/skills/cabinet-*/committees.yaml
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/skills/cabinet-*/committees-template.yaml
34
- role: "Committee structure template"
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
- Read `committees.yaml` to understand which cabinet members are active and how they're
131
- grouped. Cross-reference against:
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, `committees.yaml`,
391
- `_briefing.md`, `_lifecycle.md`, `committees.yaml`
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 in `committees.yaml`.
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 `committees.yaml`
276
- committee, but based on the last few sessions, probably should be? A
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 `committees.yaml` (or equivalent), note
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 (unless it's
73
- cross-portfolio, in which case it activates via `standing-mandate` signals)
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