create-claude-cabinet 0.27.4 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +25 -0
  2. package/lib/cli.js +134 -13
  3. package/lib/site-audit-setup.js +84 -0
  4. package/package.json +1 -1
  5. package/templates/cabinet/critique-contract.md +63 -0
  6. package/templates/cabinet/output-contract.md +19 -0
  7. package/templates/scripts/finding-schema.json +49 -0
  8. package/templates/scripts/merge-findings.js +23 -14
  9. package/templates/scripts/pib-db-lib.mjs +31 -8
  10. package/templates/scripts/pib-db-schema.sql +5 -1
  11. package/templates/scripts/triage-ui.html +178 -5
  12. package/templates/site-audit-runtime/bin/cc-site-audit +10 -0
  13. package/templates/site-audit-runtime/package.json +27 -0
  14. package/templates/site-audit-runtime/src/checks/axe-core.mjs +57 -0
  15. package/templates/site-audit-runtime/src/checks/blacklight.mjs +86 -0
  16. package/templates/site-audit-runtime/src/checks/dns.mjs +76 -0
  17. package/templates/site-audit-runtime/src/checks/lighthouse.mjs +83 -0
  18. package/templates/site-audit-runtime/src/checks/linkinator.mjs +58 -0
  19. package/templates/site-audit-runtime/src/checks/meta-og.mjs +74 -0
  20. package/templates/site-audit-runtime/src/checks/nuclei.mjs +85 -0
  21. package/templates/site-audit-runtime/src/checks/observatory.mjs +43 -0
  22. package/templates/site-audit-runtime/src/checks/pa11y.mjs +51 -0
  23. package/templates/site-audit-runtime/src/checks/security-headers.mjs +58 -0
  24. package/templates/site-audit-runtime/src/checks/ssl-cert.mjs +71 -0
  25. package/templates/site-audit-runtime/src/checks/structured-data.mjs +78 -0
  26. package/templates/site-audit-runtime/src/checks/testssl.mjs +70 -0
  27. package/templates/site-audit-runtime/src/checks/unlighthouse.mjs +69 -0
  28. package/templates/site-audit-runtime/src/checks/website-carbon.mjs +60 -0
  29. package/templates/site-audit-runtime/src/cli.mjs +184 -0
  30. package/templates/site-audit-runtime/src/diff.mjs +72 -0
  31. package/templates/site-audit-runtime/src/orchestrator.mjs +293 -0
  32. package/templates/site-audit-runtime/src/report.mjs +328 -0
  33. package/templates/site-audit-runtime/src/schema.mjs +140 -0
  34. package/templates/site-audit-runtime/src/security.mjs +116 -0
  35. package/templates/site-audit-runtime/tests/checks-tier1.test.mjs +262 -0
  36. package/templates/site-audit-runtime/tests/checks-tier2.test.mjs +75 -0
  37. package/templates/site-audit-runtime/tests/checks-tier3.test.mjs +70 -0
  38. package/templates/site-audit-runtime/tests/fixtures/axe-core.json +1 -0
  39. package/templates/site-audit-runtime/tests/fixtures/blacklight.json +1 -0
  40. package/templates/site-audit-runtime/tests/fixtures/lighthouse.json +1 -0
  41. package/templates/site-audit-runtime/tests/fixtures/linkinator.json +1 -0
  42. package/templates/site-audit-runtime/tests/fixtures/nuclei.json +3 -0
  43. package/templates/site-audit-runtime/tests/fixtures/observatory.json +10 -0
  44. package/templates/site-audit-runtime/tests/fixtures/pa11y.json +1 -0
  45. package/templates/site-audit-runtime/tests/fixtures/testssl.json +1 -0
  46. package/templates/site-audit-runtime/tests/fixtures/unlighthouse.json +1 -0
  47. package/templates/site-audit-runtime/tests/orchestrator.test.mjs +179 -0
  48. package/templates/site-audit-runtime/tests/report.test.mjs +128 -0
  49. package/templates/site-audit-runtime/tests/schema.test.mjs +154 -0
  50. package/templates/skills/audit/SKILL.md +43 -1
  51. package/templates/skills/cc-site-audit/SKILL.md +151 -0
  52. package/templates/skills/cc-site-audit/install.sh +90 -0
  53. package/templates/skills/verify/SKILL.md +177 -27
  54. package/templates/skills/verify/install.sh +2 -1
  55. package/templates/skills/verify/phases/cleanup.md +38 -0
  56. package/templates/skills/verify/phases/post-run.md +16 -0
  57. package/templates/skills/verify/phases/run.md +24 -0
  58. package/templates/skills/verify/phases/scenario-template.md +30 -0
  59. package/templates/verify-runtime/CONVENTIONS.md +37 -0
  60. package/templates/verify-runtime/package-lock.json +165 -50
  61. package/templates/verify-runtime/package.json +2 -2
  62. package/templates/verify-runtime/src/baseline-steps.ts +1 -0
  63. package/templates/verify-runtime/src/demo-recorder.ts +46 -0
  64. package/templates/verify-runtime/src/human-verdict.ts +79 -14
  65. package/templates/verify-runtime/src/index.ts +20 -1
  66. package/templates/verify-runtime/src/launch-options.ts +28 -0
  67. package/templates/verify-runtime/src/output.ts +13 -0
  68. package/templates/verify-runtime/src/pause-on-failure.ts +53 -0
  69. package/templates/verify-runtime/src/trace.ts +15 -0
  70. package/templates/verify-runtime/src/world.ts +66 -16
  71. package/templates/verify-runtime/test/demo-mode.test.ts +85 -0
  72. package/templates/verify-runtime/test/trace.test.ts +40 -0
  73. package/templates/workflows/deliberative-audit.js +325 -0
package/README.md CHANGED
@@ -90,6 +90,16 @@ few sessions, or before a release) to get a full review from every
90
90
  relevant member. You don't need to audit every session. The cabinet
91
91
  waits until called.
92
92
 
93
+ Each member is also a registered **agent type** — invoke any member
94
+ directly with `@cabinet-security`, `@cabinet-architecture`, etc.
95
+
96
+ When the Workflow tool is available, `/audit` runs a **deliberative
97
+ workflow**: Stage-1 members investigate, then Stage-2 critics
98
+ (anti-confirmation, QA, architecture) annotate findings — challenging
99
+ assumptions, adding context, or confirming. Findings arrive to triage
100
+ with the debate already attached. Optional `--rebuttal` mode lets
101
+ challenged members respond before triage.
102
+
93
103
  Members are organized into **committees** — groups by concern, so you
94
104
  can convene just the experts you need. Security review? Convene the
95
105
  security committee. Performance concerns? Just the speed committee.
@@ -114,6 +124,19 @@ Local SQLite database for actions, projects, and status tracking. Claude
114
124
  reads and writes it directly — no external service needed. Skip this if
115
125
  you already use GitHub Issues, Linear, or something else.
116
126
 
127
+ ### Memory (included in lean)
128
+
129
+ Claude Code has built-in file memory, but no guardrails around it.
130
+ The memory module adds structure:
131
+
132
+ - **`/cc-remember`** — write a new memory with automatic indexing.
133
+ Every memory gets its own file and an entry in `MEMORY.md` so
134
+ `/orient` can find it next session.
135
+ - **`/memory`** — browse and search what Claude remembers.
136
+ - **Validation** — `validate-memory.mjs` checks that the index stays
137
+ within Claude Code's session-start budget and that every file is
138
+ indexed. A PostToolUse hook flags unindexed writes in real time.
139
+
117
140
  ### Compliance Stack (full install)
118
141
 
119
142
  Scoped instructions in `.claude/rules/` that load by file path. An
@@ -222,6 +245,8 @@ source code.
222
245
  │ # (incl. pib-db-access.md, pib-db-triggers.md)
223
246
  ├── briefing/ # project briefing templates
224
247
  ├── hooks/ # git guardrails, telemetry
248
+ ├── agents/ # generated agent-type wrappers (enables @cabinet-*)
249
+ ├── workflows/ # deliberative-audit.js (two-stage audit workflow)
225
250
  ├── rules/ # enforcement pipeline
226
251
  ├── memory/ # pattern templates
227
252
  └── settings.json # hook configuration
package/lib/cli.js CHANGED
@@ -8,6 +8,7 @@ const { mergeSettings, healUserSettings } = require('./settings-merge');
8
8
  const { create: createMetadata, read: readMetadata } = require('./metadata');
9
9
  const { setupDb } = require('./db-setup');
10
10
  const { setupVerifyRuntime } = require('./verify-setup');
11
+ const { setupSiteAuditRuntime } = require('./site-audit-setup');
11
12
  const { reset } = require('./reset');
12
13
 
13
14
  const VERSION = require('../package.json').version;
@@ -343,6 +344,97 @@ function generateSkillIndex(projectDir) {
343
344
  return entries.length;
344
345
  }
345
346
 
347
+ /**
348
+ * Generate .claude/agents/cabinet-*.md wrapper definitions from installed
349
+ * cabinet SKILL.md files. Each wrapper is frontmatter-only and uses the
350
+ * `skills:` field to preload the skill body into the agent's context — the
351
+ * skill stays the single source of truth, so no prose is duplicated.
352
+ *
353
+ * The wrappers give each cabinet member a registered subagent identity:
354
+ * the transcript shows `subagent_type: cabinet-security` (trustworthy, not
355
+ * spoofable) instead of `general-purpose`, and `@cabinet-security` resolves
356
+ * as an agent type. This runs unconditionally — the identity layer benefits
357
+ * plan/execute/orient cabinet consultations, not just audits.
358
+ *
359
+ * Tool grants: cabinet `tools:` frontmatter is human-readable documentation
360
+ * (e.g., "fetch_docs (all projects -- ...)"), NOT a machine-parseable
361
+ * allowlist. So we grant a safe read-only base and ADD web tools only when
362
+ * the skill signals it needs them — never strip a capability the skill assumes.
363
+ *
364
+ * Reconciles the directory: wrappers whose cabinet skill no longer exists are
365
+ * deleted, so removing a member upstream doesn't leave a zombie wrapper.
366
+ */
367
+ function generateAgentWrappers(projectDir) {
368
+ const skillsDir = path.join(projectDir, '.claude', 'skills');
369
+ if (!fs.existsSync(skillsDir)) return 0;
370
+
371
+ const agentsDir = path.join(projectDir, '.claude', 'agents');
372
+
373
+ // Discover cabinet members from installed skills.
374
+ const wanted = new Map(); // name -> wrapper content
375
+ const dirs = fs.readdirSync(skillsDir, { withFileTypes: true });
376
+ for (const dir of dirs) {
377
+ if (!dir.isDirectory() || !dir.name.startsWith('cabinet-')) continue;
378
+ const skillFile = path.join(skillsDir, dir.name, 'SKILL.md');
379
+ if (!fs.existsSync(skillFile)) continue;
380
+
381
+ const content = fs.readFileSync(skillFile, 'utf8');
382
+ const fm = parseFrontmatter(content);
383
+ if (!fm || !fm.name) continue;
384
+
385
+ const description = (fm.description || '').replace(/\s+/g, ' ').trim();
386
+ // YAML-safe single-line double-quoted scalar.
387
+ const descYaml = '"' + description.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
388
+
389
+ // Base read-only investigation tools every cabinet member gets.
390
+ const tools = ['Read', 'Grep', 'Glob', 'Bash'];
391
+ // Cabinet tools: frontmatter is descriptive text, not an allowlist —
392
+ // scan it for real web-tool signals and add them.
393
+ const toolSignal = (typeof fm.tools === 'string' ? fm.tools : '').toLowerCase();
394
+ if (/websearch/.test(toolSignal)) tools.push('WebSearch');
395
+ if (/webfetch|fetch_docs/.test(toolSignal)) tools.push('WebFetch');
396
+
397
+ // model: none of the cabinet skills declare one today; default to sonnet,
398
+ // but honor an explicit declaration if a member ever sets one.
399
+ const model = (typeof fm.model === 'string' && fm.model.trim()) || 'sonnet';
400
+
401
+ const wrapper =
402
+ `---\n` +
403
+ `name: ${fm.name}\n` +
404
+ `description: ${descYaml}\n` +
405
+ `tools: ${[...new Set(tools)].join(', ')}\n` +
406
+ `model: ${model}\n` +
407
+ `skills: [${fm.name}]\n` +
408
+ `---\n`;
409
+
410
+ wanted.set(fm.name, wrapper);
411
+ }
412
+
413
+ if (wanted.size === 0) {
414
+ // No cabinet members installed — reconcile away any stale wrappers below,
415
+ // but only if the agents dir exists.
416
+ if (!fs.existsSync(agentsDir)) return 0;
417
+ }
418
+
419
+ fs.mkdirSync(agentsDir, { recursive: true });
420
+
421
+ // Write current wrappers.
422
+ for (const [name, wrapper] of wanted) {
423
+ fs.writeFileSync(path.join(agentsDir, `${name}.md`), wrapper);
424
+ }
425
+
426
+ // Reconcile: remove cabinet-*.md wrappers with no matching skill (zombies).
427
+ for (const file of fs.readdirSync(agentsDir)) {
428
+ if (!file.startsWith('cabinet-') || !file.endsWith('.md')) continue;
429
+ const name = file.slice(0, -3);
430
+ if (!wanted.has(name)) {
431
+ fs.unlinkSync(path.join(agentsDir, file));
432
+ }
433
+ }
434
+
435
+ return wanted.size;
436
+ }
437
+
346
438
  // MODULES is the manifest: every template path here is copied into a
347
439
  // consumer's project on install. A skill/hook/script that exists under
348
440
  // templates/ but is NOT listed in any module never ships — that is the
@@ -454,6 +546,7 @@ const MODULES = {
454
546
  'scripts/triage-server.mjs', 'scripts/triage-ui.html',
455
547
  'scripts/finding-schema.json', 'scripts/resolve-committees.cjs',
456
548
  'scripts/review-server.mjs', 'scripts/review-ui.html',
549
+ 'workflows/deliberative-audit.js', 'cabinet/critique-contract.md',
457
550
  ],
458
551
  },
459
552
  'lifecycle': {
@@ -487,6 +580,18 @@ const MODULES = {
487
580
  ],
488
581
  postInstall: 'verify-setup',
489
582
  },
583
+ 'site-audit': {
584
+ name: 'Site Audit (deployed-site quality)',
585
+ description: '15-check audit for deployed websites: performance, accessibility (3 engines), security, SEO, DNS, privacy, sustainability. Standalone HTML report + comparison mode.',
586
+ mandatory: false,
587
+ default: false,
588
+ lean: false,
589
+ templates: [
590
+ 'skills/cc-site-audit',
591
+ 'site-audit-runtime',
592
+ ],
593
+ postInstall: 'site-audit-setup',
594
+ },
490
595
  };
491
596
 
492
597
  /** Recursively collect all relative file paths under a directory. */
@@ -1115,21 +1220,26 @@ async function run() {
1115
1220
 
1116
1221
  // --- Run module postInstall hooks ---
1117
1222
  // Modules with a `postInstall` field dispatch to a matching setup
1118
- // function after templates are copied. Currently only 'verify-setup'
1119
- // (the cabinet-verify tarball builder) is wired.
1223
+ // function. Table-driven: add new runtime installers here.
1224
+ const POST_INSTALL_HANDLERS = {
1225
+ 'verify-setup': setupVerifyRuntime,
1226
+ 'site-audit-setup': setupSiteAuditRuntime,
1227
+ };
1120
1228
  for (const moduleKey of selectedModules) {
1121
1229
  const mod = MODULES[moduleKey];
1122
1230
  if (!mod.postInstall) continue;
1123
- if (mod.postInstall === 'verify-setup') {
1124
- try {
1125
- console.log('');
1126
- const verifyResult = setupVerifyRuntime({ dryRun: !!flags.dryRun });
1127
- for (const r of verifyResult.results || []) console.log(` 📋 ${r}`);
1128
- } catch (err) {
1129
- console.log(` ⚠ cabinet-verify runtime setup failed: ${err.message}`);
1130
- console.log(' /verify install.sh will fail until the runtime is installed.');
1131
- console.log(' Re-run the installer to retry.');
1132
- }
1231
+ const handler = POST_INSTALL_HANDLERS[mod.postInstall];
1232
+ if (!handler) {
1233
+ console.log(` ⚠ Unknown postInstall handler: ${mod.postInstall}`);
1234
+ continue;
1235
+ }
1236
+ try {
1237
+ console.log('');
1238
+ const result = handler({ dryRun: !!flags.dryRun });
1239
+ for (const r of result.results || []) console.log(` 📋 ${r}`);
1240
+ } catch (err) {
1241
+ console.log(` ⚠ ${mod.postInstall} failed: ${err.message}`);
1242
+ console.log(' Re-run the installer to retry.');
1133
1243
  }
1134
1244
  }
1135
1245
 
@@ -1290,6 +1400,17 @@ async function run() {
1290
1400
  }
1291
1401
  }
1292
1402
 
1403
+ // --- Generate cabinet agent-type wrappers ---
1404
+ // Unconditional (not audit-gated): the registered subagent identity benefits
1405
+ // plan/execute/orient cabinet consultations, not just /audit. No-op when no
1406
+ // cabinet members are installed.
1407
+ if (!flags.dryRun) {
1408
+ const wrapperCount = generateAgentWrappers(projectDir);
1409
+ if (wrapperCount > 0) {
1410
+ console.log(` 🪪 Generated ${wrapperCount} cabinet agent wrappers in .claude/agents/`);
1411
+ }
1412
+ }
1413
+
1293
1414
  // --- Write metadata ---
1294
1415
  if (!flags.dryRun) {
1295
1416
  createMetadata(projectDir, {
@@ -1353,4 +1474,4 @@ async function run() {
1353
1474
  console.log('');
1354
1475
  }
1355
1476
 
1356
- module.exports = { run, MODULES };
1477
+ module.exports = { run, MODULES, generateAgentWrappers };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * site-audit-setup.js — install the @claude-cabinet/site-audit runtime
3
+ * to ~/.claude-cabinet/site-audit/<version>/.
4
+ *
5
+ * Mirrors verify-setup.js: npm-pack the source, install to a versioned
6
+ * dir, write a current/VERSION pointer. Idempotent.
7
+ */
8
+
9
+ const { execSync } = require('child_process');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const os = require('os');
13
+
14
+ const CC_HOME = path.join(os.homedir(), '.claude-cabinet');
15
+ const SITE_AUDIT_BASE = path.join(CC_HOME, 'site-audit');
16
+
17
+ function setupSiteAuditRuntime(opts = {}) {
18
+ const dryRun = !!opts.dryRun;
19
+ const runtimeSourceDir =
20
+ opts.runtimeSourceDir || path.resolve(__dirname, '..', 'templates', 'site-audit-runtime');
21
+
22
+ const packageJsonPath = path.join(runtimeSourceDir, 'package.json');
23
+ if (!fs.existsSync(packageJsonPath)) {
24
+ throw new Error(`site-audit-setup: ${packageJsonPath} not found.`);
25
+ }
26
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
27
+ const version = pkg.version;
28
+ if (typeof version !== 'string' || version.length === 0) {
29
+ throw new Error(`site-audit-setup: ${packageJsonPath} has no version field`);
30
+ }
31
+
32
+ const installDir = path.join(SITE_AUDIT_BASE, version, 'dist');
33
+ const tarballName = `claude-cabinet-site-audit-${version}.tgz`;
34
+ const tarballPath = path.join(installDir, tarballName);
35
+ const versionPointer = path.join(SITE_AUDIT_BASE, 'current', 'VERSION');
36
+ const results = [];
37
+
38
+ if (dryRun) {
39
+ results.push(`Would install @claude-cabinet/site-audit@${version}`);
40
+ results.push(` source: ${runtimeSourceDir}`);
41
+ results.push(` target: ${tarballPath}`);
42
+ results.push(` pointer: ${versionPointer}`);
43
+ return { installPath: installDir, version, status: 'dry-run', results };
44
+ }
45
+
46
+ if (fs.existsSync(tarballPath) && fs.statSync(tarballPath).size > 1024) {
47
+ results.push(`@claude-cabinet/site-audit@${version} already installed (${tarballPath})`);
48
+ writeVersionPointer(versionPointer, version);
49
+ return { installPath: installDir, version, status: 'skipped', results };
50
+ }
51
+
52
+ if (fs.existsSync(tarballPath)) fs.unlinkSync(tarballPath);
53
+
54
+ fs.mkdirSync(installDir, { recursive: true });
55
+ const packStdout = execSync(`npm pack --silent --pack-destination "${installDir}"`, {
56
+ cwd: runtimeSourceDir,
57
+ encoding: 'utf8',
58
+ }).trim();
59
+
60
+ const lastLine = packStdout.split('\n').filter(Boolean).pop() || '';
61
+ const producedName = path.basename(lastLine);
62
+ if (producedName && producedName !== tarballName) {
63
+ const producedPath = path.join(installDir, producedName);
64
+ if (fs.existsSync(producedPath)) {
65
+ fs.renameSync(producedPath, tarballPath);
66
+ }
67
+ }
68
+
69
+ if (!fs.existsSync(tarballPath)) {
70
+ throw new Error(`site-audit-setup: tarball not found after npm pack: ${tarballPath}`);
71
+ }
72
+
73
+ writeVersionPointer(versionPointer, version);
74
+ results.push(`Installed @claude-cabinet/site-audit@${version}`);
75
+ results.push(` ${tarballPath}`);
76
+ return { installPath: installDir, version, status: 'installed', results };
77
+ }
78
+
79
+ function writeVersionPointer(pointerPath, version) {
80
+ fs.mkdirSync(path.dirname(pointerPath), { recursive: true });
81
+ fs.writeFileSync(pointerPath, version + '\n', 'utf8');
82
+ }
83
+
84
+ module.exports = { setupSiteAuditRuntime };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.27.4",
3
+ "version": "0.29.0",
4
4
  "description": "Claude Cabinet — opinionated process scaffolding for Claude Code projects",
5
5
  "bin": {
6
6
  "create-claude-cabinet": "bin/create-claude-cabinet.js"
@@ -0,0 +1,63 @@
1
+ # Critique Contract — Stage-2 Audit Output
2
+
3
+ This contract defines how **Stage-2 critics** annotate Stage-1 findings
4
+ during a deliberative audit. Stage-1 members produce findings (see
5
+ `output-contract.md`). Stage-2 members review those findings through
6
+ their domain lens and produce annotations.
7
+
8
+ ## Input
9
+
10
+ You receive a JSON array of findings from Stage-1 members. Each finding
11
+ has the fields defined in `output-contract.md`: id, cabinet-member,
12
+ severity, title, description, evidence, etc.
13
+
14
+ ## Your Task
15
+
16
+ Review each finding through your domain lens. For findings that touch
17
+ your expertise, produce an annotation. For findings outside your domain,
18
+ stay silent — silence means "no objection from my perspective."
19
+
20
+ Default to no annotation. Only speak when you have something to add:
21
+ a factual correction, a challenge to an assumption, supporting evidence,
22
+ or additional context that changes how the finding should be interpreted.
23
+
24
+ ## Annotation Types
25
+
26
+ | Type | When to use | Example |
27
+ |------|------------|---------|
28
+ | `challenge` | You disagree with the finding's conclusion or severity | "This input is already escaped by the ORM — the vulnerability described doesn't exist in the current implementation" |
29
+ | `support` | You have independent evidence confirming the finding | "The audit log also replicates to analytics DB — blast radius is wider than stated" |
30
+ | `context` | Additional information that changes interpretation | "This coupling is intentional — it's the auth boundary described in the architecture doc" |
31
+ | `correction` | A factual error in the finding | "The file path referenced was renamed in commit abc123 — the actual location is..." |
32
+
33
+ ## Output Format
34
+
35
+ Return a JSON object matching this schema:
36
+
37
+ ```json
38
+ {
39
+ "annotations": [
40
+ {
41
+ "findingId": "security-0001",
42
+ "type": "challenge",
43
+ "text": "Your annotation here",
44
+ "severitySuggestion": "info"
45
+ }
46
+ ]
47
+ }
48
+ ```
49
+
50
+ Fields:
51
+ - **findingId** (required): the `id` of the Stage-1 finding you're annotating
52
+ - **type** (required): one of `challenge`, `support`, `context`, `correction`
53
+ - **text** (required): your annotation — be specific and cite evidence
54
+ - **severitySuggestion** (optional): if you think the severity should change,
55
+ suggest the new level. Omit if the current severity is appropriate.
56
+
57
+ ## Scope Rules
58
+
59
+ - Annotate only findings that intersect your domain expertise.
60
+ - You may annotate findings from any Stage-1 member, not just your own domain.
61
+ - Do not produce new findings — that's Stage 1's job.
62
+ - Do not repeat or rephrase a finding. Add to it or challenge it.
63
+ - If two findings contradict each other, annotate both to surface the tension.
@@ -146,3 +146,22 @@ Return valid JSON matching `scripts/finding-schema.json`.
146
146
 
147
147
  Your response must be ONLY the JSON object — no markdown fences, no
148
148
  commentary outside the JSON.
149
+
150
+ ## Deliberation Fields (Two-Stage Audit)
151
+
152
+ When the audit runs in two-stage deliberative mode, findings may acquire
153
+ additional fields after initial creation:
154
+
155
+ - `status` — Set during deliberation. Values: `upheld` (no challenges),
156
+ `challenged` (critic disagreed), `modified` (author accepted critique),
157
+ `withdrawn` (author conceded), `rebutted` (author defended).
158
+ - `annotations[]` — Array of Stage-2 critic annotations. Each has
159
+ `cabinet-member`, `type` (challenge/support/context/correction),
160
+ `text`, and optional `severity-suggestion`.
161
+ - `rebuttal` — The original author's response to challenges. Has
162
+ `response` (withdraw/modify/defend) and `comment`.
163
+
164
+ **Stage-1 members:** Emit findings as normal using the schema above.
165
+ These deliberation fields are added later by the workflow.
166
+
167
+ **Stage-2 members:** Use `critique-contract.md` instead of this file.
@@ -75,6 +75,55 @@
75
75
  "type": "array",
76
76
  "items": { "type": "string" },
77
77
  "description": "Categorization tags for filtering"
78
+ },
79
+ "status": {
80
+ "type": "string",
81
+ "enum": ["upheld", "challenged", "modified", "withdrawn", "rebutted"],
82
+ "description": "Set during Stage 2/3 deliberation. Absent for single-stage audits."
83
+ },
84
+ "annotations": {
85
+ "type": "array",
86
+ "items": {
87
+ "type": "object",
88
+ "required": ["cabinet-member", "type", "text"],
89
+ "properties": {
90
+ "cabinet-member": {
91
+ "type": "string",
92
+ "description": "Stage-2 critic who made this annotation"
93
+ },
94
+ "type": {
95
+ "type": "string",
96
+ "enum": ["challenge", "support", "context", "correction"],
97
+ "description": "challenge = disagrees, support = confirms, context = adds info, correction = factual fix"
98
+ },
99
+ "text": {
100
+ "type": "string",
101
+ "description": "The annotation content"
102
+ },
103
+ "severity-suggestion": {
104
+ "type": "string",
105
+ "enum": ["critical", "warn", "info", "idea"],
106
+ "description": "Critic's suggested severity change, if any"
107
+ }
108
+ }
109
+ },
110
+ "description": "Stage-2 critic annotations from deliberative audit. Absent for single-stage audits."
111
+ },
112
+ "rebuttal": {
113
+ "type": "object",
114
+ "properties": {
115
+ "response": {
116
+ "type": "string",
117
+ "enum": ["withdraw", "modify", "defend"],
118
+ "description": "Stage-1 member's response to challenges"
119
+ },
120
+ "comment": {
121
+ "type": "string",
122
+ "description": "Explanation of the response"
123
+ }
124
+ },
125
+ "required": ["response", "comment"],
126
+ "description": "Stage-1 member's response to Stage-2 challenges (opt-in rebuttal mode). Absent when no rebuttal requested or finding was not challenged."
78
127
  }
79
128
  }
80
129
  }
@@ -90,23 +90,32 @@ for (const file of files) {
90
90
  const timestamp = new Date().toISOString();
91
91
  const runId = `run-${basename(runDir)}`;
92
92
 
93
- const summary = {
94
- findings: allFindings,
95
- meta: {
96
- runId,
97
- timestamp,
98
- trigger: 'manual',
99
- members: Object.keys(memberCounts),
100
- counts: {
101
- total: allFindings.length,
102
- findings: allFindings.length - positiveCount,
103
- positive: positiveCount,
104
- ...severityCounts,
105
- },
106
- byMember: memberCounts,
93
+ const meta = {
94
+ runId,
95
+ timestamp,
96
+ trigger: 'manual',
97
+ members: Object.keys(memberCounts),
98
+ counts: {
99
+ total: allFindings.length,
100
+ findings: allFindings.length - positiveCount,
101
+ positive: positiveCount,
102
+ ...severityCounts,
107
103
  },
104
+ byMember: memberCounts,
108
105
  };
109
106
 
107
+ if (allFindings.some(f => f.annotations && f.annotations.length > 0)) {
108
+ meta.deliberation = {
109
+ annotatedCount: allFindings.filter(f => f.annotations && f.annotations.length > 0).length,
110
+ challengedCount: allFindings.filter(f => f.status === 'challenged' || f.status === 'rebutted').length,
111
+ upheldCount: allFindings.filter(f => f.status === 'upheld').length,
112
+ withdrawnCount: allFindings.filter(f => f.status === 'withdrawn').length,
113
+ modifiedCount: allFindings.filter(f => f.status === 'modified').length,
114
+ };
115
+ }
116
+
117
+ const summary = { findings: allFindings, meta };
118
+
110
119
  const summaryPath = join(runDir, 'run-summary.json');
111
120
  writeFileSync(summaryPath, JSON.stringify(summary, null, 2));
112
121
  console.log(`\nMerged ${allFindings.length} findings → ${summaryPath}`);
@@ -285,21 +285,44 @@ export function ingestFindings(db, { runDir }) {
285
285
  VALUES (?, ?, ?, ?, ?)
286
286
  `).run(runId, dateStr, timestamp, data.meta?.trigger || 'manual', data.findings?.length || 0);
287
287
 
288
- const insert = db.prepare(`
289
- INSERT OR REPLACE INTO audit_findings
290
- (id, run_id, cabinet_member, severity, title, description, assumption,
291
- evidence, question, file, line, suggested_fix, auto_fixable, type)
292
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
293
- `);
288
+ // Try the full INSERT with deliberation columns first; fall back to the
289
+ // legacy shape for existing databases that haven't added them yet.
290
+ let insert;
291
+ let hasDeliberationCols = true;
292
+ try {
293
+ insert = db.prepare(`
294
+ INSERT OR REPLACE INTO audit_findings
295
+ (id, run_id, cabinet_member, severity, title, description, assumption,
296
+ evidence, question, file, line, suggested_fix, auto_fixable, type,
297
+ status, annotations, rebuttal)
298
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
299
+ `);
300
+ } catch {
301
+ hasDeliberationCols = false;
302
+ insert = db.prepare(`
303
+ INSERT OR REPLACE INTO audit_findings
304
+ (id, run_id, cabinet_member, severity, title, description, assumption,
305
+ evidence, question, file, line, suggested_fix, auto_fixable, type)
306
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
307
+ `);
308
+ }
294
309
 
295
310
  let count = 0;
296
311
  for (const f of (data.findings || [])) {
297
- insert.run(
312
+ const base = [
298
313
  f.id, runId, f['cabinet-member'], f.severity, f.title,
299
314
  f.description || null, f.assumption || null, f.evidence || null,
300
315
  f.question || null, f.file || null, f.line || null,
301
316
  f.suggestedFix || null, f.autoFixable ? 1 : 0, f.type || 'finding'
302
- );
317
+ ];
318
+ if (hasDeliberationCols) {
319
+ base.push(
320
+ f.status || null,
321
+ f.annotations ? JSON.stringify(f.annotations) : null,
322
+ f.rebuttal ? JSON.stringify(f.rebuttal) : null
323
+ );
324
+ }
325
+ insert.run(...base);
303
326
  count++;
304
327
  }
305
328
  return { count, runId, message: `Ingested ${count} findings from ${runDir} (run: ${runId})` };
@@ -66,7 +66,11 @@ CREATE TABLE IF NOT EXISTS audit_findings (
66
66
  CHECK(triage_status IN ('open','approved','rejected','deferred','fixed','archived')),
67
67
  triage_notes TEXT,
68
68
  triaged_at TEXT,
69
- fix_description TEXT
69
+ fix_description TEXT,
70
+ -- Deliberation fields (two-stage audit). Absent for single-stage audits.
71
+ status TEXT CHECK(status IS NULL OR status IN ('upheld','challenged','modified','withdrawn','rebutted')),
72
+ annotations TEXT, -- JSON array of Stage-2 critic annotations
73
+ rebuttal TEXT -- JSON object: Stage-1 member's response to challenges
70
74
  );
71
75
 
72
76
  -- Append-only history of trigger-condition evaluations.