clean-room-skill 0.1.3 → 0.1.5

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.
@@ -9,7 +9,7 @@
9
9
  "name": "clean-room",
10
10
  "source": "./",
11
11
  "description": "Spec-first clean-room workflow for authorized source analysis without replacement code.",
12
- "version": "0.1.3",
12
+ "version": "0.1.5",
13
13
  "author": {
14
14
  "name": "whit3rabbit"
15
15
  },
@@ -2,7 +2,7 @@
2
2
  "name": "clean-room",
3
3
  "displayName": "Clean Room",
4
4
  "description": "Spec-first clean-room workflow for authorized source analysis without replacement code.",
5
- "version": "0.1.3",
5
+ "version": "0.1.5",
6
6
  "author": {
7
7
  "name": "whit3rabbit"
8
8
  },
@@ -14,6 +14,5 @@
14
14
  "reverse-engineering",
15
15
  "compliance"
16
16
  ],
17
- "skills": "./skills/",
18
- "agents": "./agents/"
17
+ "skills": "./skills/"
19
18
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clean-room",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Spec-first clean-room workflow for authorized source analysis without replacement code.",
5
5
  "author": {
6
6
  "name": "whit3rabbit"
package/bin/verify.sh CHANGED
@@ -39,6 +39,11 @@ echo "Validating JSON metadata..."
39
39
  "$python_cmd" -m json.tool .codex-plugin/plugin.json >/dev/null
40
40
  "$python_cmd" -m json.tool .claude-plugin/plugin.json >/dev/null
41
41
 
42
+ if command -v claude >/dev/null 2>&1; then
43
+ echo "Validating Claude plugin manifest..."
44
+ claude plugin validate .claude-plugin/plugin.json
45
+ fi
46
+
42
47
  echo "Compiling Python hooks and scripts..."
43
48
  "$python_cmd" -m compileall -q hooks skills/clean-room/scripts
44
49
 
package/docs/REFERENCE.md CHANGED
@@ -165,6 +165,7 @@ Usage:
165
165
  ```bash
166
166
  npx clean-room-skill@latest preflight --template --output ~/Documents/CleanRoom/task-1234abcd/contaminated/preflight-goal.json
167
167
  npx clean-room-skill@latest preflight --input ./preflight-goal.json --output ~/Documents/CleanRoom/task-1234abcd/contaminated/preflight-goal.json
168
+ npx clean-room-skill@latest preflight --template --bootstrap ~/Documents/CleanRoom/task-1234abcd
168
169
  ```
169
170
 
170
171
  Options:
@@ -174,6 +175,7 @@ Options:
174
175
  | `--template` | Write an attended draft with blocking open questions. |
175
176
  | `--input <path>` | Validate and normalize/copy a completed preflight goal. |
176
177
  | `--output <path>` | Destination `preflight-goal.json`. |
178
+ | `--bootstrap <path>` | Generated task root or `clean-room-bootstrap.json`; writes to the generated contaminated artifact root after scaffold validation. |
177
179
  | `--mode <mode>` | `attended` or `unattended`; template supports attended only. |
178
180
  | `--dry-run` | Print actions without writing files. |
179
181
  | `--force` | Overwrite output if it already exists. |
package/lib/bootstrap.cjs CHANGED
@@ -9,6 +9,7 @@ const {
9
9
  assertManagedPath,
10
10
  atomicWriteFile,
11
11
  atomicWriteFileNoOverwrite,
12
+ readJsonFile,
12
13
  } = require('./fs-utils.cjs');
13
14
  const { packageVersion } = require('./install-artifacts.cjs');
14
15
  const { expandTilde } = require('./runtime-layout.cjs');
@@ -21,6 +22,13 @@ const TARGET_PROFILES = new Set([
21
22
  ]);
22
23
 
23
24
  const TASK_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
25
+ const BOOTSTRAP_METADATA_FILE = 'clean-room-bootstrap.json';
26
+ const BOOTSTRAP_REPO_STUB = '.clean-room/README.md';
27
+ const BOOTSTRAP_DIRS = Object.freeze({
28
+ contaminated: 'contaminated',
29
+ clean: 'clean',
30
+ quarantine: 'quarantine',
31
+ });
24
32
 
25
33
  function defaultArtifactBase(homeDir = os.homedir()) {
26
34
  return path.join(homeDir, 'Documents', 'CleanRoom');
@@ -131,8 +139,8 @@ function resolveInitOptions(options, env = process.env, homeDir = os.homedir())
131
139
  artifactBase,
132
140
  outputRoot,
133
141
  roots,
134
- metadataPath: assertManagedPath(outputRoot, 'clean-room-bootstrap.json'),
135
- repoStubPath: assertManagedPath(targetDir, '.clean-room/README.md'),
142
+ metadataPath: assertManagedPath(outputRoot, BOOTSTRAP_METADATA_FILE),
143
+ repoStubPath: assertManagedPath(targetDir, BOOTSTRAP_REPO_STUB),
136
144
  };
137
145
  }
138
146
 
@@ -170,15 +178,165 @@ Start the runtime skill from your agent and provide the external output folder p
170
178
  }
171
179
 
172
180
  function assertWritableTargets(options) {
173
- const conflicts = [];
181
+ const fileConflicts = [];
174
182
  for (const filePath of [options.metadataPath, options.repoStubPath]) {
175
183
  if (fs.existsSync(filePath) && !options.force) {
176
- conflicts.push(filePath);
184
+ fileConflicts.push(filePath);
177
185
  }
178
186
  }
179
- if (conflicts.length > 0) {
180
- throw new Error(`bootstrap file already exists; use --force to overwrite: ${conflicts.join(', ')}`);
187
+ if (fileConflicts.length > 0) {
188
+ throw new Error(`bootstrap file already exists; use --force to overwrite: ${fileConflicts.join(', ')}`);
181
189
  }
190
+
191
+ const pathConflicts = [];
192
+ for (const dirPath of Object.values(options.roots)) {
193
+ if (fs.existsSync(dirPath) && !options.force) {
194
+ pathConflicts.push(dirPath);
195
+ }
196
+ }
197
+ if (pathConflicts.length > 0) {
198
+ throw new Error(`bootstrap generated path already exists; use --force to reuse it: ${pathConflicts.join(', ')}`);
199
+ }
200
+
201
+ for (const dirPath of Object.values(options.roots)) {
202
+ const stat = lstatIfExists(dirPath);
203
+ if (stat && !stat.isDirectory()) {
204
+ throw new Error(`bootstrap generated path is not a directory: ${dirPath}`);
205
+ }
206
+ }
207
+ }
208
+
209
+ function lstatIfExists(filePath) {
210
+ try {
211
+ return fs.lstatSync(filePath);
212
+ } catch (err) {
213
+ if (err?.code === 'ENOENT') return null;
214
+ throw err;
215
+ }
216
+ }
217
+
218
+ function requireDirectory(dirPath, label, errors) {
219
+ const stat = lstatIfExists(dirPath);
220
+ if (!stat) {
221
+ errors.push(`${label} missing: ${dirPath}`);
222
+ return;
223
+ }
224
+ if (!stat.isDirectory()) {
225
+ errors.push(`${label} is not a directory: ${dirPath}`);
226
+ }
227
+ }
228
+
229
+ function requireFile(filePath, label, errors) {
230
+ const stat = lstatIfExists(filePath);
231
+ if (!stat) {
232
+ errors.push(`${label} missing: ${filePath}`);
233
+ return;
234
+ }
235
+ if (!stat.isFile()) {
236
+ errors.push(`${label} is not a file: ${filePath}`);
237
+ }
238
+ }
239
+
240
+ function expectMetadataString(metadata, field, errors) {
241
+ if (typeof metadata?.[field] !== 'string' || metadata[field].length === 0) {
242
+ errors.push(`bootstrap metadata ${field} must be a non-empty string`);
243
+ return null;
244
+ }
245
+ return metadata[field];
246
+ }
247
+
248
+ function assertMetadataPath(metadata, field, expectedPath, errors) {
249
+ const value = expectMetadataString(metadata, field, errors);
250
+ if (!value) return;
251
+ if (path.resolve(expandTilde(value)) !== expectedPath) {
252
+ errors.push(`bootstrap metadata ${field} must match ${expectedPath}`);
253
+ }
254
+ }
255
+
256
+ function validateBootstrapScaffold(taskRoot) {
257
+ if (typeof taskRoot !== 'string' || taskRoot.trim() === '') {
258
+ throw new Error('bootstrap path requires a task root');
259
+ }
260
+ const outputRoot = path.resolve(taskRoot);
261
+ const metadataPath = assertManagedPath(outputRoot, BOOTSTRAP_METADATA_FILE);
262
+ requireFileOrThrow(metadataPath, 'bootstrap metadata');
263
+
264
+ const metadata = readJsonFile(metadataPath, null);
265
+ const errors = [];
266
+ if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
267
+ errors.push('bootstrap metadata must be an object');
268
+ } else {
269
+ if (metadata.schema !== 1) {
270
+ errors.push('bootstrap metadata schema must be 1');
271
+ }
272
+ if (metadata.package !== 'clean-room-skill') {
273
+ errors.push('bootstrap metadata package must be clean-room-skill');
274
+ }
275
+ const taskId = expectMetadataString(metadata, 'task_id', errors);
276
+ if (taskId && taskId !== path.basename(outputRoot)) {
277
+ errors.push('bootstrap metadata task_id must match the task root basename');
278
+ }
279
+ assertMetadataPath(metadata, 'output_root', outputRoot, errors);
280
+ }
281
+
282
+ const roots = {
283
+ contaminated: path.join(outputRoot, BOOTSTRAP_DIRS.contaminated),
284
+ clean: path.join(outputRoot, BOOTSTRAP_DIRS.clean),
285
+ quarantine: path.join(outputRoot, BOOTSTRAP_DIRS.quarantine),
286
+ };
287
+ for (const [label, dirPath] of Object.entries(roots)) {
288
+ requireDirectory(dirPath, `bootstrap ${label} directory`, errors);
289
+ }
290
+
291
+ if (metadata && typeof metadata === 'object' && !Array.isArray(metadata)) {
292
+ if (!metadata.roots || typeof metadata.roots !== 'object' || Array.isArray(metadata.roots)) {
293
+ errors.push('bootstrap metadata roots must be an object');
294
+ } else {
295
+ assertMetadataPath(metadata.roots, 'contaminated_artifacts', roots.contaminated, errors);
296
+ assertMetadataPath(metadata.roots, 'clean_artifacts', roots.clean, errors);
297
+ assertMetadataPath(metadata.roots, 'quarantine', roots.quarantine, errors);
298
+ }
299
+ }
300
+
301
+ const targetDir = metadata && typeof metadata === 'object' && !Array.isArray(metadata)
302
+ ? expectMetadataString(metadata, 'target_dir', errors)
303
+ : null;
304
+ const repoStubPath = targetDir ? assertManagedPath(path.resolve(expandTilde(targetDir)), BOOTSTRAP_REPO_STUB) : null;
305
+ if (repoStubPath) {
306
+ requireFile(repoStubPath, 'bootstrap repo stub', errors);
307
+ }
308
+
309
+ if (errors.length > 0) {
310
+ throw new Error(`bootstrap scaffold is invalid:\n ${errors.join('\n ')}`);
311
+ }
312
+
313
+ return {
314
+ outputRoot,
315
+ metadataPath,
316
+ metadata,
317
+ roots,
318
+ repoStubPath,
319
+ };
320
+ }
321
+
322
+ function requireFileOrThrow(filePath, label) {
323
+ const errors = [];
324
+ requireFile(filePath, label, errors);
325
+ if (errors.length > 0) {
326
+ throw new Error(`bootstrap scaffold is invalid:\n ${errors.join('\n ')}`);
327
+ }
328
+ }
329
+
330
+ function resolveBootstrapScaffold(value, cwd = process.cwd(), homeDir = os.homedir()) {
331
+ if (typeof value !== 'string' || value.trim() === '') {
332
+ throw new Error('--bootstrap requires a path');
333
+ }
334
+ const expanded = expandTilde(value, homeDir);
335
+ const resolved = path.resolve(cwd, expanded);
336
+ const taskRoot = path.basename(resolved) === BOOTSTRAP_METADATA_FILE
337
+ ? path.dirname(resolved)
338
+ : resolved;
339
+ return validateBootstrapScaffold(taskRoot);
182
340
  }
183
341
 
184
342
  function writeBootstrapFile(filePath, data, force) {
@@ -220,9 +378,14 @@ function printInitResult(options) {
220
378
  console.log(` repo stub: ${options.repoStubPath}`);
221
379
  console.log('');
222
380
  console.log('Next steps:');
223
- console.log(' install safe hooks: npx clean-room-skill@latest --codex --global --hooks=safe --yes');
224
- console.log(' start in your runtime: invoke the clean-room init skill, then clean-room');
225
- console.log(' uninstall runtime install: npx clean-room-skill@latest --codex --global --uninstall --yes');
381
+ console.log(' Codex:');
382
+ console.log(' install safe hooks: npx clean-room-skill@latest --codex --global --hooks=safe --yes');
383
+ console.log(' start in Codex: invoke the init skill, then clean-room through @ or the skills UI');
384
+ console.log(' uninstall runtime install: npx clean-room-skill@latest --codex --global --uninstall --yes');
385
+ console.log(' Claude Code:');
386
+ console.log(' install safe hooks: npx clean-room-skill@latest --claude --global --hooks=safe --yes');
387
+ console.log(' start in Claude Code: /clean-room:init, then /clean-room or /clean-room:attended');
388
+ console.log(' uninstall runtime install: npx clean-room-skill@latest --claude --global --uninstall --yes');
226
389
  console.log(' strict hooks are only for dedicated clean-room Codex or Claude homes');
227
390
  }
228
391
 
@@ -238,10 +401,13 @@ function runInit(argv, context = {}) {
238
401
  }
239
402
 
240
403
  module.exports = {
404
+ BOOTSTRAP_METADATA_FILE,
241
405
  defaultArtifactBase,
242
406
  generateTaskId,
243
407
  parseInitArgs,
408
+ resolveBootstrapScaffold,
244
409
  resolveInitOptions,
245
410
  runInit,
246
411
  TARGET_PROFILES,
412
+ validateBootstrapScaffold,
247
413
  };
package/lib/preflight.cjs CHANGED
@@ -9,6 +9,7 @@ const {
9
9
  atomicWriteFileNoOverwrite,
10
10
  readJsonFile,
11
11
  } = require('./fs-utils.cjs');
12
+ const { resolveBootstrapScaffold } = require('./bootstrap.cjs');
12
13
 
13
14
  const VALID_MODES = new Set(['attended', 'unattended']);
14
15
  const VALID_INTENTS = new Set([
@@ -26,7 +27,7 @@ const VALID_NETWORK_POLICIES = new Set(['off', 'deps-only', 'on']);
26
27
  const VALID_DEPENDENCY_INSTALL_POLICIES = new Set(['offline', 'locked', 'allow-new']);
27
28
 
28
29
  function printPreflightHelp() {
29
- console.log(`Usage: clean-room-skill preflight (--template | --input <path>) --output <path> [options]
30
+ console.log(`Usage: clean-room-skill preflight (--template | --input <path>) (--output <path> | --bootstrap <path>) [options]
30
31
 
31
32
  Create or validate a clean-room preflight goal contract.
32
33
 
@@ -34,6 +35,7 @@ Options:
34
35
  --template Write an attended draft with blocking open questions
35
36
  --input <path> Validate and normalize/copy a completed preflight goal
36
37
  --output <path> Destination preflight-goal.json
38
+ --bootstrap <path> Generated task root or clean-room-bootstrap.json
37
39
  --mode <mode> attended or unattended (template supports attended only)
38
40
  --dry-run Print actions without writing files
39
41
  --force Overwrite output if it already exists
@@ -46,6 +48,7 @@ function parsePreflightArgs(argv) {
46
48
  template: false,
47
49
  input: null,
48
50
  output: null,
51
+ bootstrap: null,
49
52
  mode: 'attended',
50
53
  dryRun: false,
51
54
  force: false,
@@ -72,6 +75,11 @@ function parsePreflightArgs(argv) {
72
75
  options.output = requiredValue(argv, index, '--output');
73
76
  } else if (arg.startsWith('--output=')) {
74
77
  options.output = arg.slice('--output='.length);
78
+ } else if (arg === '--bootstrap') {
79
+ index += 1;
80
+ options.bootstrap = requiredValue(argv, index, '--bootstrap');
81
+ } else if (arg.startsWith('--bootstrap=')) {
82
+ options.bootstrap = arg.slice('--bootstrap='.length);
75
83
  } else if (arg === '--mode') {
76
84
  index += 1;
77
85
  options.mode = requiredValue(argv, index, '--mode');
@@ -444,9 +452,18 @@ function runPreflight(argv, context = {}) {
444
452
  if (parsed.template === Boolean(parsed.input)) {
445
453
  throw new Error('specify exactly one of --template or --input');
446
454
  }
455
+ if (parsed.bootstrap && parsed.output) {
456
+ throw new Error('--bootstrap conflicts with --output');
457
+ }
458
+ if (!parsed.bootstrap && !parsed.output) {
459
+ throw new Error('specify exactly one of --output or --bootstrap');
460
+ }
447
461
  const cwd = context.cwd || process.cwd();
448
462
  const homeDir = context.homeDir || os.homedir();
449
- const outputPath = resolveOutputPath(parsed.output, cwd, homeDir);
463
+ const bootstrap = parsed.bootstrap ? resolveBootstrapScaffold(parsed.bootstrap, cwd, homeDir) : null;
464
+ const outputPath = bootstrap
465
+ ? path.join(bootstrap.roots.contaminated, 'preflight-goal.json')
466
+ : resolveOutputPath(parsed.output, cwd, homeDir);
450
467
  let goal;
451
468
  if (parsed.template) {
452
469
  goal = buildTemplate(parsed.mode);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clean-room-skill",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Spec-first clean-room workflow for authorized source analysis without replacement code.",
5
5
  "bin": {
6
6
  "clean-room-skill": "bin/install.js"
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clean-room",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Spec-first clean-room workflow for authorized source analysis without replacement code.",
5
5
  "author": {
6
6
  "name": "whit3rabbit"
@@ -21,6 +21,8 @@ Use the canonical `clean-room` skill workflow and references in this plugin. Pre
21
21
 
22
22
  The CLI command `clean-room-skill init` may have pre-created neutral external folders and a clean-safe `.clean-room/README.md` stub in the target repository. Treat that bootstrap output as convenience scaffolding only. It does not replace this skill's initialization workflow, and it must not be treated as an active `preflight-goal.json`, `init-config.json`, `task-manifest.json`, or `clean-run-context.json`.
23
23
 
24
+ When using an existing CLI bootstrap, check `clean-room-bootstrap.json`, `contaminated/`, `clean/`, `quarantine/`, and the target repo `.clean-room/README.md` before recording active init preferences. Stop if metadata is missing, invalid, mismatched with the task root, or any generated path is missing or the wrong type. Do not infer active workflow state from those bootstrap files.
25
+
24
26
  ## Gather
25
27
 
26
28
  Collect only setup decisions that affect correctness, safety, resumability, or output shape:
@@ -11,6 +11,8 @@ Create or validate `preflight-goal.json` before active clean-room artifacts star
11
11
 
12
12
  Use the canonical `clean-room` workflow and read `skills/clean-room/references/PREFLIGHT.md` when collecting missing goal details. Preserve the clean-room boundary: `preflight-goal.json` is a controller/contaminated-side artifact and must not be placed in clean-role readable roots.
13
13
 
14
+ If the user provides output from CLI `clean-room-skill init`, check the generated bootstrap scaffold before creating or copying `preflight-goal.json`: `clean-room-bootstrap.json`, `contaminated/`, `clean/`, `quarantine/`, and the target repo `.clean-room/README.md` must exist and agree. Treat that scaffold as convenience output only; it is not an active `preflight-goal.json`, `init-config.json`, `task-manifest.json`, or `clean-run-context.json`.
15
+
14
16
  ## Required Contract
15
17
 
16
18
  Record these decisions:
@@ -45,9 +47,10 @@ Use the CLI only for template creation or validation/copying:
45
47
  ```bash
46
48
  clean-room-skill preflight --template --output ~/Documents/CleanRoom/task-xxxxxxxx/contaminated/preflight-goal.json
47
49
  clean-room-skill preflight --input ./preflight-goal.json --output ~/Documents/CleanRoom/task-xxxxxxxx/contaminated/preflight-goal.json
50
+ clean-room-skill preflight --template --bootstrap ~/Documents/CleanRoom/task-xxxxxxxx
48
51
  ```
49
52
 
50
- `--template` writes an attended draft with blocking open questions. It does not support unattended mode. Use `--input` for completed contracts.
53
+ `--template` writes an attended draft with blocking open questions. It does not support unattended mode. Use `--input` for completed contracts. `--bootstrap` accepts either the generated task root or `clean-room-bootstrap.json` and writes to the generated contaminated artifact root after scaffold validation.
51
54
 
52
55
  ## Handoff
53
56