clean-room-skill 0.1.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 (56) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +20 -0
  3. package/.codex-plugin/plugin.json +36 -0
  4. package/LICENSE +21 -0
  5. package/README.md +376 -0
  6. package/agents/clean-architect.md +27 -0
  7. package/agents/clean-qa-editor.md +27 -0
  8. package/agents/contaminated-manager-verifier.md +35 -0
  9. package/agents/contaminated-source-analyst.md +26 -0
  10. package/bin/install.js +535 -0
  11. package/examples/codex/.codex/agents/clean-architect.toml +17 -0
  12. package/examples/codex/.codex/agents/clean-qa-editor.toml +17 -0
  13. package/examples/codex/.codex/agents/contaminated-manager-verifier.toml +21 -0
  14. package/examples/codex/.codex/agents/contaminated-source-analyst.toml +17 -0
  15. package/hooks/check-artifact-leakage.py +317 -0
  16. package/hooks/clean-room-hook.py +88 -0
  17. package/hooks/clean_room_paths.py +130 -0
  18. package/hooks/deny-clean-room-shell.py +30 -0
  19. package/hooks/deny-clean-source-read.py +104 -0
  20. package/hooks/deny-contaminated-clean-write.py +134 -0
  21. package/hooks/hooks.json +44 -0
  22. package/hooks/require-clean-room-env.py +127 -0
  23. package/hooks/validate-handoff-package.py +140 -0
  24. package/hooks/validate-json-schema.py +283 -0
  25. package/lib/fs-utils.cjs +123 -0
  26. package/lib/hooks.cjs +214 -0
  27. package/package.json +49 -0
  28. package/plugin.json +20 -0
  29. package/skills/attended/SKILL.md +25 -0
  30. package/skills/clean-room/SKILL.md +134 -0
  31. package/skills/clean-room/assets/behavior-spec.schema.json +367 -0
  32. package/skills/clean-room/assets/contamination-incident.schema.json +60 -0
  33. package/skills/clean-room/assets/coverage-ledger.schema.json +139 -0
  34. package/skills/clean-room/assets/evidence-ledger.schema.json +80 -0
  35. package/skills/clean-room/assets/handoff-package.schema.json +114 -0
  36. package/skills/clean-room/assets/qc-report.schema.json +248 -0
  37. package/skills/clean-room/assets/skeleton-manifest.schema.json +239 -0
  38. package/skills/clean-room/assets/source-index.schema.json +622 -0
  39. package/skills/clean-room/assets/task-manifest.schema.json +593 -0
  40. package/skills/clean-room/examples/README.md +18 -0
  41. package/skills/clean-room/examples/minimal-spec-package/behavior-spec.json +61 -0
  42. package/skills/clean-room/examples/minimal-spec-package/coverage-ledger.json +27 -0
  43. package/skills/clean-room/examples/minimal-spec-package/evidence-ledger.json +17 -0
  44. package/skills/clean-room/examples/minimal-spec-package/handoff-package.json +26 -0
  45. package/skills/clean-room/examples/minimal-spec-package/qc-report.json +25 -0
  46. package/skills/clean-room/examples/minimal-spec-package/skeleton-manifest.json +45 -0
  47. package/skills/clean-room/examples/minimal-spec-package/source-index.json +156 -0
  48. package/skills/clean-room/examples/minimal-spec-package/task-manifest.json +220 -0
  49. package/skills/clean-room/references/LEAKAGE-RULES.md +92 -0
  50. package/skills/clean-room/references/PROCESS.md +185 -0
  51. package/skills/clean-room/references/SPEC-SCHEMA.md +185 -0
  52. package/skills/clean-room/references/TARGET-LANGUAGE-GUIDE.md +43 -0
  53. package/skills/clean-room/scripts/build_source_index.py +1253 -0
  54. package/skills/clean-room/scripts/clean_room_tool_manager.py +199 -0
  55. package/skills/clean-room/scripts/clean_room_tooling.py +370 -0
  56. package/skills/unattended/SKILL.md +26 -0
package/bin/install.js ADDED
@@ -0,0 +1,535 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const readline = require('node:readline/promises');
8
+ const { spawnSync } = require('node:child_process');
9
+
10
+ const {
11
+ atomicWriteFile,
12
+ fileHash,
13
+ listFiles,
14
+ readJsonFile,
15
+ removeEmptyParents,
16
+ resolveInside,
17
+ sha256Bytes,
18
+ writeJsonFile,
19
+ } = require('../lib/fs-utils.cjs');
20
+ const {
21
+ buildHookEntries,
22
+ configPathForRuntime,
23
+ mergeHookEntries,
24
+ removeHookEntries,
25
+ renderPackageHooksJson,
26
+ } = require('../lib/hooks.cjs');
27
+
28
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
29
+ const MANIFEST_NAME = 'clean-room-install-manifest.json';
30
+ const PATCHES_DIR_NAME = 'clean-room-patches';
31
+ const RUNTIMES = ['codex', 'claude', 'antigravity'];
32
+ const IGNORE_NAMES = new Set(['.DS_Store', '__pycache__', 'node_modules', '.syntext']);
33
+ const HOOK_MODES = new Set(['safe', 'copy-only', 'strict']);
34
+
35
+ function packageVersion() {
36
+ const pkg = readJsonFile(path.join(PACKAGE_ROOT, 'package.json'), null);
37
+ return pkg && typeof pkg.version === 'string' ? pkg.version : '0.0.0';
38
+ }
39
+
40
+ function parseArgs(argv) {
41
+ const options = {
42
+ runtimes: [],
43
+ scope: null,
44
+ dryRun: false,
45
+ yes: false,
46
+ uninstall: false,
47
+ hookMode: 'safe',
48
+ configDir: null,
49
+ };
50
+
51
+ for (let i = 0; i < argv.length; i += 1) {
52
+ const arg = argv[i];
53
+ if (arg === '--codex') options.runtimes.push('codex');
54
+ else if (arg === '--claude') options.runtimes.push('claude');
55
+ else if (arg === '--antigravity') options.runtimes.push('antigravity');
56
+ else if (arg === '--all') options.runtimes = [...RUNTIMES];
57
+ else if (arg === '--global') options.scope = setExclusive(options.scope, 'global', '--global');
58
+ else if (arg === '--local') options.scope = setExclusive(options.scope, 'local', '--local');
59
+ else if (arg === '--dry-run') options.dryRun = true;
60
+ else if (arg === '--yes') options.yes = true;
61
+ else if (arg === '--uninstall') options.uninstall = true;
62
+ else if (arg === '--no-hooks') options.hookMode = 'copy-only';
63
+ else if (arg === '--config-dir') {
64
+ i += 1;
65
+ if (i >= argv.length) throw new Error('--config-dir requires a path');
66
+ options.configDir = argv[i];
67
+ } else if (arg.startsWith('--config-dir=')) {
68
+ options.configDir = arg.slice('--config-dir='.length);
69
+ } else if (arg === '--hooks') {
70
+ i += 1;
71
+ if (i >= argv.length) throw new Error('--hooks requires safe, copy-only, or strict');
72
+ options.hookMode = argv[i];
73
+ } else if (arg.startsWith('--hooks=')) {
74
+ options.hookMode = arg.slice('--hooks='.length);
75
+ } else if (arg === '-h' || arg === '--help') {
76
+ printHelp();
77
+ process.exit(0);
78
+ } else {
79
+ throw new Error(`unknown option: ${arg}`);
80
+ }
81
+ }
82
+
83
+ options.runtimes = [...new Set(options.runtimes)];
84
+ if (!HOOK_MODES.has(options.hookMode)) {
85
+ throw new Error('--hooks must be one of safe, copy-only, or strict');
86
+ }
87
+ if (options.configDir && options.runtimes.length > 1) {
88
+ throw new Error('--config-dir can only be used with one runtime');
89
+ }
90
+ return options;
91
+ }
92
+
93
+ function setExclusive(current, next, flag) {
94
+ if (current && current !== next) {
95
+ throw new Error(`${flag} conflicts with --${current}`);
96
+ }
97
+ return next;
98
+ }
99
+
100
+ function printHelp() {
101
+ console.log(`Usage: clean-room-skill [runtime] [scope] [options]
102
+
103
+ Runtime:
104
+ --codex Install for Codex
105
+ --claude Install for Claude Code
106
+ --antigravity Install for Antigravity
107
+ --all Install for all supported runtimes
108
+
109
+ Scope:
110
+ --global Install to the runtime user config
111
+ --local Install to the current project config
112
+
113
+ Options:
114
+ --hooks=<mode> safe, copy-only, or strict (default: safe)
115
+ --no-hooks Alias for --hooks=copy-only
116
+ --config-dir <path> Override the target root for one runtime
117
+ --dry-run Print actions without writing files
118
+ --yes Non-interactive mode; unknown conflicts still abort
119
+ --uninstall Remove manifest-managed files and clean-room hook entries
120
+ `);
121
+ }
122
+
123
+ async function resolveInteractiveOptions(options) {
124
+ if (options.runtimes.length > 0 && options.scope) {
125
+ return options;
126
+ }
127
+ if (!process.stdin.isTTY || options.yes) {
128
+ throw new Error('specify runtime and scope flags when running non-interactively');
129
+ }
130
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
131
+ try {
132
+ if (options.runtimes.length === 0) {
133
+ const answer = await rl.question('Runtime [codex/claude/antigravity/all]: ');
134
+ const runtime = answer.trim().toLowerCase() || 'codex';
135
+ if (runtime === 'all') options.runtimes = [...RUNTIMES];
136
+ else if (RUNTIMES.includes(runtime)) options.runtimes = [runtime];
137
+ else throw new Error(`unsupported runtime: ${answer}`);
138
+ }
139
+ if (!options.scope) {
140
+ const answer = await rl.question('Scope [global/local]: ');
141
+ const scope = answer.trim().toLowerCase() || 'global';
142
+ if (scope !== 'global' && scope !== 'local') {
143
+ throw new Error(`unsupported scope: ${answer}`);
144
+ }
145
+ options.scope = scope;
146
+ }
147
+ return options;
148
+ } finally {
149
+ rl.close();
150
+ }
151
+ }
152
+
153
+ function homePath(...parts) {
154
+ return path.join(os.homedir(), ...parts);
155
+ }
156
+
157
+ function resolveTargetRoot(runtime, scope, configDir) {
158
+ if (runtime === 'antigravity' && scope === 'local') {
159
+ throw new Error('Antigravity local installs are not supported in v1');
160
+ }
161
+ if (configDir) return path.resolve(configDir);
162
+ if (runtime === 'codex') {
163
+ return scope === 'global'
164
+ ? path.resolve(process.env.CODEX_HOME || homePath('.codex'))
165
+ : path.resolve(process.cwd(), '.codex');
166
+ }
167
+ if (runtime === 'claude') {
168
+ return scope === 'global'
169
+ ? path.resolve(process.env.CLAUDE_CONFIG_DIR || homePath('.claude'))
170
+ : path.resolve(process.cwd(), '.claude');
171
+ }
172
+ if (runtime === 'antigravity') {
173
+ return path.resolve(
174
+ process.env.ANTIGRAVITY_PLUGIN_DIR ||
175
+ homePath('.gemini', 'config', 'plugins', 'clean-room-skill')
176
+ );
177
+ }
178
+ throw new Error(`unsupported runtime: ${runtime}`);
179
+ }
180
+
181
+ function sourceFile(relPath, hookMode) {
182
+ if (relPath === 'hooks/hooks.json') {
183
+ return Buffer.from(renderPackageHooksJson(hookMode === 'strict' ? 'strict' : 'safe'), 'utf8');
184
+ }
185
+ return fs.readFileSync(path.join(PACKAGE_ROOT, relPath));
186
+ }
187
+
188
+ function addFile(desired, sourceRel, destRel, hookMode) {
189
+ desired.set(destRel.replace(/\\/g, '/'), sourceFile(sourceRel, hookMode));
190
+ }
191
+
192
+ function addTree(desired, sourceRel, destRel, hookMode, options = {}) {
193
+ const sourceRoot = path.join(PACKAGE_ROOT, sourceRel);
194
+ for (const rel of listFiles(sourceRoot, { ignoreNames: IGNORE_NAMES })) {
195
+ if (options.filter && !options.filter(rel)) continue;
196
+ const sourcePath = `${sourceRel}/${rel}`.replace(/\\/g, '/');
197
+ const destPath = destRel ? `${destRel}/${rel}` : rel;
198
+ addFile(desired, sourcePath, destPath, hookMode);
199
+ }
200
+ }
201
+
202
+ function buildDesiredFiles(runtime, hookMode) {
203
+ const desired = new Map();
204
+ if (runtime === 'codex') {
205
+ addTree(desired, 'skills', 'skills', hookMode);
206
+ addTree(desired, 'examples/codex/.codex/agents', 'agents', hookMode);
207
+ addTree(desired, 'hooks', 'hooks/clean-room', hookMode, {
208
+ filter: (rel) => rel.endsWith('.py'),
209
+ });
210
+ return desired;
211
+ }
212
+ if (runtime === 'claude') {
213
+ addTree(desired, 'skills', 'skills', hookMode);
214
+ addTree(desired, 'agents', 'agents', hookMode);
215
+ addTree(desired, 'hooks', 'hooks/clean-room', hookMode, {
216
+ filter: (rel) => rel.endsWith('.py'),
217
+ });
218
+ return desired;
219
+ }
220
+ if (runtime === 'antigravity') {
221
+ for (const rel of ['plugin.json', 'README.md', 'LICENSE']) {
222
+ addFile(desired, rel, rel, hookMode);
223
+ }
224
+ for (const rel of ['.codex-plugin', '.claude-plugin', 'skills', 'agents', 'hooks', 'examples']) {
225
+ addTree(desired, rel, rel, hookMode);
226
+ }
227
+ return desired;
228
+ }
229
+ throw new Error(`unsupported runtime: ${runtime}`);
230
+ }
231
+
232
+ function readManifest(targetRoot) {
233
+ const manifestPath = path.join(targetRoot, MANIFEST_NAME);
234
+ if (!fs.existsSync(manifestPath)) return null;
235
+ const manifest = readJsonFile(manifestPath, null);
236
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
237
+ throw new Error(`${manifestPath} must contain a JSON object`);
238
+ }
239
+ return manifest;
240
+ }
241
+
242
+ function manifestHash(manifest, relPath) {
243
+ const entry = manifest?.files?.[relPath];
244
+ if (!entry) return null;
245
+ if (typeof entry === 'string') return entry;
246
+ if (entry && typeof entry.sha256 === 'string') return entry.sha256;
247
+ return null;
248
+ }
249
+
250
+ function planInstall(targetRoot, desired, manifest) {
251
+ const unknownConflicts = [];
252
+ const writes = [];
253
+ const removals = [];
254
+ const backups = [];
255
+
256
+ for (const [relPath, bytes] of desired) {
257
+ const fullPath = resolveInside(targetRoot, relPath);
258
+ const desiredHash = sha256Bytes(bytes);
259
+ const knownHash = manifestHash(manifest, relPath);
260
+ if (fs.existsSync(fullPath)) {
261
+ const currentHash = fileHash(fullPath);
262
+ if (knownHash && currentHash !== knownHash) {
263
+ backups.push(relPath);
264
+ } else if (!knownHash && currentHash !== desiredHash) {
265
+ unknownConflicts.push(relPath);
266
+ }
267
+ }
268
+ writes.push(relPath);
269
+ }
270
+
271
+ for (const relPath of Object.keys(manifest?.files || {})) {
272
+ if (desired.has(relPath)) continue;
273
+ const fullPath = resolveInside(targetRoot, relPath);
274
+ if (!fs.existsSync(fullPath)) continue;
275
+ const knownHash = manifestHash(manifest, relPath);
276
+ if (knownHash && fileHash(fullPath) !== knownHash) {
277
+ backups.push(relPath);
278
+ }
279
+ removals.push(relPath);
280
+ }
281
+
282
+ return { unknownConflicts, writes, removals, backups };
283
+ }
284
+
285
+ async function confirmUnknownConflicts(conflicts, options) {
286
+ if (conflicts.length === 0) return false;
287
+ if (options.dryRun) return false;
288
+ if (options.yes || !process.stdin.isTTY) {
289
+ throw new Error(
290
+ `unknown existing file(s) would be overwritten: ${conflicts.join(', ')}. ` +
291
+ 'Run interactively to confirm or remove the conflict.'
292
+ );
293
+ }
294
+ console.log('Unknown existing files would be overwritten:');
295
+ for (const conflict of conflicts) {
296
+ console.log(` ${conflict}`);
297
+ }
298
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
299
+ try {
300
+ const answer = await rl.question('Overwrite these files? Type yes to continue: ');
301
+ if (answer.trim() !== 'yes') {
302
+ throw new Error('aborted by user');
303
+ }
304
+ return true;
305
+ } finally {
306
+ rl.close();
307
+ }
308
+ }
309
+
310
+ function backupFile(targetRoot, relPath, backupRoot) {
311
+ const source = resolveInside(targetRoot, relPath);
312
+ const dest = resolveInside(backupRoot, relPath);
313
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
314
+ fs.copyFileSync(source, dest);
315
+ }
316
+
317
+ function timestampForPath() {
318
+ return new Date().toISOString().replace(/[:.]/g, '-');
319
+ }
320
+
321
+ function createBackupWriter(targetRoot, dryRun) {
322
+ let backupRoot = null;
323
+ return {
324
+ backup(relPath) {
325
+ if (dryRun) return null;
326
+ if (!backupRoot) {
327
+ backupRoot = path.join(targetRoot, PATCHES_DIR_NAME, timestampForPath());
328
+ }
329
+ backupFile(targetRoot, relPath, backupRoot);
330
+ return backupRoot;
331
+ },
332
+ get root() {
333
+ return backupRoot;
334
+ },
335
+ };
336
+ }
337
+
338
+ function applyInstall(targetRoot, desired, manifest, plan, options) {
339
+ const backupWriter = createBackupWriter(targetRoot, options.dryRun);
340
+ if (options.dryRun) return null;
341
+ fs.mkdirSync(targetRoot, { recursive: true });
342
+
343
+ const backedUp = new Set();
344
+ for (const relPath of [...plan.backups, ...plan.unknownConflicts]) {
345
+ const fullPath = resolveInside(targetRoot, relPath);
346
+ if (fs.existsSync(fullPath) && !backedUp.has(relPath)) {
347
+ backupWriter.backup(relPath);
348
+ backedUp.add(relPath);
349
+ }
350
+ }
351
+
352
+ for (const relPath of plan.removals) {
353
+ const fullPath = resolveInside(targetRoot, relPath);
354
+ if (fs.existsSync(fullPath)) {
355
+ fs.rmSync(fullPath, { force: true });
356
+ removeEmptyParents(path.dirname(fullPath), targetRoot);
357
+ }
358
+ }
359
+
360
+ for (const [relPath, bytes] of desired) {
361
+ const fullPath = resolveInside(targetRoot, relPath);
362
+ atomicWriteFile(fullPath, bytes);
363
+ }
364
+
365
+ const nextManifest = {
366
+ schema: 1,
367
+ package: 'clean-room-skill',
368
+ version: packageVersion(),
369
+ runtime: manifest?.runtime || null,
370
+ scope: manifest?.scope || null,
371
+ hooks_mode: options.hookMode,
372
+ installed_at: new Date().toISOString(),
373
+ files: {},
374
+ };
375
+ for (const [relPath, bytes] of desired) {
376
+ nextManifest.files[relPath] = { sha256: sha256Bytes(bytes) };
377
+ }
378
+ return { backupRoot: backupWriter.root, manifest: nextManifest };
379
+ }
380
+
381
+ function writeInstallManifest(targetRoot, manifest, runtime, scope, hookMode, dryRun) {
382
+ if (dryRun) return;
383
+ const next = {
384
+ ...manifest,
385
+ runtime,
386
+ scope,
387
+ hooks_mode: hookMode,
388
+ };
389
+ writeJsonFile(path.join(targetRoot, MANIFEST_NAME), next);
390
+ }
391
+
392
+ function resolvePython3() {
393
+ const result = spawnSync('python3', ['-c', 'import sys; print(sys.executable)'], {
394
+ encoding: 'utf8',
395
+ stdio: ['ignore', 'pipe', 'pipe'],
396
+ });
397
+ if (result.status !== 0) {
398
+ throw new Error('python3 is required to install clean-room hooks');
399
+ }
400
+ const pythonPath = String(result.stdout || '').trim();
401
+ if (!path.isAbsolute(pythonPath)) {
402
+ throw new Error('python3 did not resolve to an absolute executable path');
403
+ }
404
+ return pythonPath;
405
+ }
406
+
407
+ function validateRuntimeOptions(options) {
408
+ for (const runtime of options.runtimes) {
409
+ resolveTargetRoot(runtime, options.scope, options.configDir);
410
+ }
411
+ }
412
+
413
+ function configureHooks(runtime, targetRoot, hookMode, dryRun) {
414
+ if (hookMode === 'copy-only' || runtime === 'antigravity') {
415
+ return;
416
+ }
417
+ const configPath = configPathForRuntime(runtime, targetRoot);
418
+ if (!configPath) return;
419
+ const pythonPath = resolvePython3();
420
+ const wrapperPath = path.join(targetRoot, 'hooks', 'clean-room', 'clean-room-hook.py');
421
+ const entries = buildHookEntries({ pythonPath, wrapperPath, mode: hookMode });
422
+ mergeHookEntries(configPath, entries, { dryRun });
423
+ }
424
+
425
+ async function installRuntime(runtime, options) {
426
+ const targetRoot = resolveTargetRoot(runtime, options.scope, options.configDir);
427
+ const manifest = readManifest(targetRoot);
428
+ const desired = buildDesiredFiles(runtime, options.hookMode);
429
+ const plan = planInstall(targetRoot, desired, manifest);
430
+ const adoptedUnknowns = await confirmUnknownConflicts(plan.unknownConflicts, options);
431
+
432
+ console.log(`${options.dryRun ? 'Would install' : 'Installing'} ${runtime} to ${targetRoot}`);
433
+ console.log(` files: ${plan.writes.length}`);
434
+ if (plan.removals.length) console.log(` stale managed removals: ${plan.removals.length}`);
435
+ if (plan.backups.length || adoptedUnknowns) {
436
+ console.log(` backups: ${plan.backups.length + (adoptedUnknowns ? plan.unknownConflicts.length : 0)}`);
437
+ }
438
+ if (options.dryRun && plan.unknownConflicts.length) {
439
+ console.log(` unknown conflicts: ${plan.unknownConflicts.length}`);
440
+ }
441
+
442
+ configureHooks(runtime, targetRoot, options.hookMode, true);
443
+ const result = applyInstall(targetRoot, desired, manifest, plan, options);
444
+ if (!options.dryRun) {
445
+ configureHooks(runtime, targetRoot, options.hookMode, false);
446
+ }
447
+ if (result) {
448
+ writeInstallManifest(targetRoot, result.manifest, runtime, options.scope, options.hookMode, options.dryRun);
449
+ if (result.backupRoot) {
450
+ console.log(` backed up modified files to ${result.backupRoot}`);
451
+ }
452
+ }
453
+ }
454
+
455
+ function planUninstall(targetRoot, manifest) {
456
+ const files = Object.keys(manifest?.files || {});
457
+ const backups = [];
458
+ const removals = [];
459
+ for (const relPath of files) {
460
+ const fullPath = resolveInside(targetRoot, relPath);
461
+ if (!fs.existsSync(fullPath)) continue;
462
+ const knownHash = manifestHash(manifest, relPath);
463
+ if (knownHash && fileHash(fullPath) !== knownHash) {
464
+ backups.push(relPath);
465
+ }
466
+ removals.push(relPath);
467
+ }
468
+ return { backups, removals };
469
+ }
470
+
471
+ function uninstallRuntime(runtime, options) {
472
+ const targetRoot = resolveTargetRoot(runtime, options.scope, options.configDir);
473
+ const manifest = readManifest(targetRoot);
474
+ console.log(`${options.dryRun ? 'Would uninstall' : 'Uninstalling'} ${runtime} from ${targetRoot}`);
475
+ if (!manifest) {
476
+ console.log(' no install manifest found');
477
+ return;
478
+ }
479
+ const plan = planUninstall(targetRoot, manifest);
480
+ console.log(` managed removals: ${plan.removals.length}`);
481
+ if (plan.backups.length) {
482
+ console.log(` backups: ${plan.backups.length}`);
483
+ }
484
+
485
+ if (options.dryRun) return;
486
+ const backupWriter = createBackupWriter(targetRoot, false);
487
+ for (const relPath of plan.backups) {
488
+ backupWriter.backup(relPath);
489
+ }
490
+ for (const relPath of plan.removals) {
491
+ const fullPath = resolveInside(targetRoot, relPath);
492
+ if (fs.existsSync(fullPath)) {
493
+ fs.rmSync(fullPath, { force: true });
494
+ removeEmptyParents(path.dirname(fullPath), targetRoot);
495
+ }
496
+ }
497
+ const configPath = configPathForRuntime(runtime, targetRoot);
498
+ if (configPath) {
499
+ removeHookEntries(configPath, { dryRun: false });
500
+ }
501
+ fs.rmSync(path.join(targetRoot, MANIFEST_NAME), { force: true });
502
+ removeEmptyParents(targetRoot, path.dirname(targetRoot));
503
+ if (backupWriter.root) {
504
+ console.log(` backed up modified files to ${backupWriter.root}`);
505
+ }
506
+ }
507
+
508
+ async function main() {
509
+ const options = await resolveInteractiveOptions(parseArgs(process.argv.slice(2)));
510
+ if (!options.scope) {
511
+ options.scope = 'global';
512
+ }
513
+ validateRuntimeOptions(options);
514
+ for (const runtime of options.runtimes) {
515
+ if (options.uninstall) {
516
+ uninstallRuntime(runtime, options);
517
+ } else {
518
+ await installRuntime(runtime, options);
519
+ }
520
+ }
521
+ }
522
+
523
+ if (require.main === module) {
524
+ main().catch((err) => {
525
+ console.error(`clean-room-skill: ${err.message}`);
526
+ process.exitCode = 1;
527
+ });
528
+ }
529
+
530
+ module.exports = {
531
+ buildDesiredFiles,
532
+ parseArgs,
533
+ planInstall,
534
+ resolveTargetRoot,
535
+ };
@@ -0,0 +1,17 @@
1
+ name = "clean-architect"
2
+ description = "Manages the selected clean schema base and organizes clean behavior specs into target-neutral skeleton manifests."
3
+ sandbox_mode = "workspace-write"
4
+ model_reasoning_effort = "medium"
5
+ enabled_skills = ["clean-room"]
6
+
7
+ instructions = """
8
+ Act as Agent 2 in the clean-room pipeline.
9
+ Run only from the clean workspace.
10
+ Before tool use, require CLEAN_ROOM_ROLE=clean-architect, CLEAN_ROOM_CLEAN_ROOTS, CLEAN_ROOM_SOURCE_ROOTS, CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS, CLEAN_ROOM_ALLOWED_READ_ROOTS, and CLEAN_ROOM_SCHEMA_DIR.
11
+ Read approved clean artifacts and explicitly configured public or destination constraint roots only.
12
+ Do not read source workspaces, contaminated ledgers, or contaminated chat history.
13
+ Manage the selected clean schema base from task-manifest.json format_selection.
14
+ Merge only approved handoff artifacts into the selected clean schema base.
15
+ Create or update skeleton-manifest.json only from clean inputs.
16
+ Stop if contaminated material appears in clean inputs.
17
+ """
@@ -0,0 +1,17 @@
1
+ name = "clean-qa-editor"
2
+ description = "Reviews clean artifacts for schema conformance, leakage, coverage, testability, and abstract reporting back to Agent 0."
3
+ sandbox_mode = "workspace-write"
4
+ model_reasoning_effort = "high"
5
+ enabled_skills = ["clean-room"]
6
+
7
+ instructions = """
8
+ Act as Agent 3 in the clean-room pipeline.
9
+ Run only from the clean workspace.
10
+ Before tool use, require CLEAN_ROOM_ROLE=clean-qa-editor, CLEAN_ROOM_CLEAN_ROOTS, CLEAN_ROOM_SOURCE_ROOTS, CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS, CLEAN_ROOM_ALLOWED_READ_ROOTS, and CLEAN_ROOM_SCHEMA_DIR.
11
+ Read approved clean artifacts and explicitly configured public or destination constraint roots only.
12
+ Validate clean artifacts against schema assets.
13
+ Review leakage risk and record contamination incidents.
14
+ Write qc-report.json and abstract delta tickets.
15
+ Report final QC status and abstract delta tickets back to Agent 0.
16
+ Do not read source workspaces, contaminated ledgers, or contaminated chat history.
17
+ """
@@ -0,0 +1,21 @@
1
+ name = "contaminated-manager-verifier"
2
+ description = "Consumes contaminated source indexes, tracks source coverage, and emits only abstract clean-room delta tickets."
3
+ sandbox_mode = "workspace-write"
4
+ model_reasoning_effort = "medium"
5
+ enabled_skills = ["clean-room"]
6
+
7
+ instructions = """
8
+ Act as Agent 0 in the clean-room pipeline.
9
+ Operate only in the contaminated domain.
10
+ Read authorized source and contaminated ledgers as needed.
11
+ When acting as agent zero/controller, define and pass CLEAN_ROOM_ROLE, CLEAN_ROOM_SOURCE_ROOTS, CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS, CLEAN_ROOM_CLEAN_ROOTS, CLEAN_ROOM_SCHEMA_DIR, and clean-role CLEAN_ROOM_ALLOWED_READ_ROOTS into every new role session.
12
+ Missing controller_policy means attended. In unattended mode, reload durable artifacts before each iteration, select at most one pending or gap unit, launch roles from fresh context, validate schema and leakage before advancing state, and stop on configured safety or ambiguity conditions.
13
+ Record the user's format_selection target profile and Agent 0-3 agent_pipeline contract in task-manifest.json.
14
+ Use contaminated source-index.json when controller preflight produced one.
15
+ Maintain the tasklist as neutral task-manifest.json units, map at most one source-index batch or large-file segment into each unit, and track coverage under CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS.
16
+ Do not write clean artifacts.
17
+ If source-index.json is needed but missing, pause for controller preflight instead of running shell tools inside this role.
18
+ Receive Agent 3 final QC reports and convert gaps into abstract delta tickets.
19
+ Send only abstract delta tickets across the wall.
20
+ Never include source excerpts, raw diffs, copied comments, private helper names, or source-shaped pseudocode.
21
+ """
@@ -0,0 +1,17 @@
1
+ name = "contaminated-source-analyst"
2
+ description = "Reads authorized source and writes neutral task slices plus scrubbed behavior specs with evidence references."
3
+ sandbox_mode = "workspace-write"
4
+ model_reasoning_effort = "medium"
5
+ enabled_skills = ["clean-room"]
6
+
7
+ instructions = """
8
+ Act as Agent 1 in the clean-room pipeline.
9
+ Operate only in the contaminated domain.
10
+ Read the minimum authorized source needed for the assigned unit.
11
+ When the unit has source_index_refs, stay within the referenced batch unless Agent 0 explicitly assigns a related gap.
12
+ Write only under CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS.
13
+ Generate neutral task slices and behavioral spec material for Agent 0-controlled units.
14
+ Produce neutral behavioral requirements and evidence refs.
15
+ Do not produce replacement implementation code.
16
+ Do not include copied source expression, raw diffs, comments, private helper names, or implementation-shaped pseudocode.
17
+ """