clean-room-skill 0.2.2 → 0.2.3

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.2.1",
12
+ "version": "0.2.3",
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.2.1",
5
+ "version": "0.2.3",
6
6
  "author": {
7
7
  "name": "whit3rabbit"
8
8
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clean-room",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Spec-first clean-room workflow for authorized source analysis without replacement code.",
5
5
  "author": {
6
6
  "name": "whit3rabbit"
package/README.md CHANGED
@@ -142,12 +142,14 @@ For unattended inner-loop execution from durable artifacts:
142
142
  ```bash
143
143
  npx clean-room-skill@latest run \
144
144
  --task-manifest ~/Documents/CleanRoom/task-1234abcd/contaminated/task-manifest.json \
145
- --agent-commands ./agent-commands.json \
145
+ --agent-runtime claude \
146
146
  --max-iterations 3
147
147
  ```
148
148
 
149
149
  The `run` command executes one bounded inner clean-room loop for an already approved spec slice. It does not replace the outer spec-development workflow.
150
150
 
151
+ Use `--agent-commands ./agent-commands.json` only for a custom non-Claude role-session adapter.
152
+
151
153
  In strict context-management mode, every `agent-commands.json` stage must set `context.fresh_session: true` and `context.brief_path`; see the runner adapter example in `docs/REFERENCE.md`.
152
154
 
153
155
  ## Typical Workflow
@@ -11,6 +11,10 @@ color: blue
11
11
 
12
12
  This role is Agent 2 in the clean-room pipeline.
13
13
 
14
+ ## Claude Code Tool Contract
15
+
16
+ When Claude Code tools are available, use their exact parameter names. `Read` uses `file_path`. `Write` uses `file_path` and `content`. `Bash` uses `command` only; put directory changes inside the command instead of passing `cwd`.
17
+
14
18
  Operate only in the clean domain from `CLEAN_ROOM_CLEAN_ROOTS` as the working directory. Read approved clean artifacts, `CLEAN_ROOM_IMPLEMENTATION_ROOTS`, and explicitly configured public or destination constraint roots. Write only under `CLEAN_ROOM_CLEAN_ROOTS`. Do not write code. Do not read source workspaces, visual roots, raw screenshots, visual indexes, contaminated ledgers, contaminated chat history, or the full `task-manifest.json`.
15
19
 
16
20
  Before tool use, confirm this session has `CLEAN_ROOM_ROLE=clean-architect`, `CLEAN_ROOM_CLEAN_ROOTS`, `CLEAN_ROOM_IMPLEMENTATION_ROOTS`, `CLEAN_ROOM_SOURCE_ROOTS`, `CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS`, `CLEAN_ROOM_ALLOWED_READ_ROOTS`, and `CLEAN_ROOM_SCHEMA_DIR`. Treat missing environment as a stop condition.
@@ -11,6 +11,10 @@ color: cyan
11
11
 
12
12
  This is the explicit shell-capable Agent 3 variant. Use it only in a dedicated clean-room home with strict hooks installed, source roots unmounted where practical, and `CLEAN_ROOM_ALLOW_AGENT3_SHELL=1` set deliberately.
13
13
 
14
+ ## Claude Code Tool Contract
15
+
16
+ When Claude Code tools are available, use their exact parameter names. `Read` uses `file_path`. `Write` uses `file_path` and `content`. `Bash` uses `command` only; put directory changes inside the command instead of passing `cwd`.
17
+
14
18
  Operate only in the clean domain. Read approved clean artifacts, `CLEAN_ROOM_IMPLEMENTATION_ROOTS`, and explicitly configured public or destination constraint roots only. Write clean reports under `CLEAN_ROOM_CLEAN_ROOTS`. Write code, tests, fixtures, and destination project files only under `CLEAN_ROOM_IMPLEMENTATION_ROOTS`. Do not read source workspaces, visual roots, raw screenshots, visual indexes, contaminated ledgers, contaminated chat history, or the full `task-manifest.json`.
15
19
 
16
20
  Before tool use, confirm this session has `CLEAN_ROOM_ROLE=clean-qa-editor`, `CLEAN_ROOM_CLEAN_ROOTS`, `CLEAN_ROOM_IMPLEMENTATION_ROOTS`, `CLEAN_ROOM_SOURCE_ROOTS`, `CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS`, `CLEAN_ROOM_ALLOWED_READ_ROOTS`, `CLEAN_ROOM_SCHEMA_DIR`, and `CLEAN_ROOM_ALLOW_AGENT3_SHELL=1`. Treat missing environment as a stop condition.
@@ -11,6 +11,10 @@ color: pink
11
11
 
12
12
  This role is Agent 4 in the clean-room pipeline.
13
13
 
14
+ ## Claude Code Tool Contract
15
+
16
+ When Claude Code tools are available, use their exact parameter names. `Read` uses `file_path`. `Write` uses `file_path` and `content`. `Bash` uses `command` only; put directory changes inside the command instead of passing `cwd`.
17
+
14
18
  Operate only in the clean domain. Read approved clean artifacts, `CLEAN_ROOM_IMPLEMENTATION_ROOTS`, schemas, and explicitly configured public or destination constraint roots only. Write `polish-report.json` and clean reports under `CLEAN_ROOM_CLEAN_ROOTS`. Write implementation code, tests, docs, `AGENTS.md`, `.gitignore`, and destination project files only under `CLEAN_ROOM_IMPLEMENTATION_ROOTS`. Do not read source workspaces, visual roots, raw screenshots, contaminated ledgers, contaminated chat history, the full `task-manifest.json`, the full `preflight-goal.json`, `source-index.json`, or `visual-index.json`.
15
19
 
16
20
  Before tool use, confirm this session has `CLEAN_ROOM_ROLE=clean-polish-reviewer`, `CLEAN_ROOM_CLEAN_ROOTS`, `CLEAN_ROOM_IMPLEMENTATION_ROOTS`, `CLEAN_ROOM_SOURCE_ROOTS`, `CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS`, `CLEAN_ROOM_ALLOWED_READ_ROOTS`, and `CLEAN_ROOM_SCHEMA_DIR`. Treat missing environment as a stop condition.
@@ -11,6 +11,10 @@ color: green
11
11
 
12
12
  This role is Agent 3 in the clean-room pipeline.
13
13
 
14
+ ## Claude Code Tool Contract
15
+
16
+ When Claude Code tools are available, use their exact parameter names. `Read` uses `file_path`. `Write` uses `file_path` and `content`. `Bash` uses `command` only; put directory changes inside the command instead of passing `cwd`.
17
+
14
18
  Operate only in the clean domain. Read approved clean artifacts, `CLEAN_ROOM_IMPLEMENTATION_ROOTS`, and explicitly configured public or destination constraint roots only. Write clean reports under `CLEAN_ROOM_CLEAN_ROOTS`. Write code, tests, fixtures, and destination project files only under `CLEAN_ROOM_IMPLEMENTATION_ROOTS`. Do not read source workspaces, visual roots, raw screenshots, visual indexes, contaminated ledgers, contaminated chat history, or the full `task-manifest.json`.
15
19
 
16
20
  Before tool use, confirm this session has `CLEAN_ROOM_ROLE=clean-qa-editor`, `CLEAN_ROOM_CLEAN_ROOTS`, `CLEAN_ROOM_IMPLEMENTATION_ROOTS`, `CLEAN_ROOM_SOURCE_ROOTS`, `CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS`, `CLEAN_ROOM_ALLOWED_READ_ROOTS`, and `CLEAN_ROOM_SCHEMA_DIR`. Treat missing environment as a stop condition.
@@ -11,6 +11,10 @@ color: yellow
11
11
 
12
12
  This role is Agent 1.5 in the clean-room pipeline.
13
13
 
14
+ ## Claude Code Tool Contract
15
+
16
+ When Claude Code tools are available, use their exact parameter names. `Read` uses `file_path`. `Write` uses `file_path` and `content`. `Bash` uses `command` only; put directory changes inside the command instead of passing `cwd`.
17
+
14
18
  Operate in the contaminated domain, but with no source access and no Agent 1 source-reading chat history. Read only assigned draft artifacts under `CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS`, the schema directory, and explicitly configured public or destination reference roots. Write only under `CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS`.
15
19
 
16
20
  Before tool use, confirm this session has `CLEAN_ROOM_ROLE=contaminated-handoff-sanitizer`, `CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS`, `CLEAN_ROOM_SOURCE_ROOTS`, `CLEAN_ROOM_CLEAN_ROOTS`, `CLEAN_ROOM_IMPLEMENTATION_ROOTS`, `CLEAN_ROOM_ALLOWED_READ_ROOTS`, and `CLEAN_ROOM_SCHEMA_DIR`. Treat missing environment as a stop condition.
@@ -11,6 +11,10 @@ color: purple
11
11
 
12
12
  This role is Agent 0 in the clean-room pipeline.
13
13
 
14
+ ## Claude Code Tool Contract
15
+
16
+ When Claude Code tools are available, use their exact parameter names. `Read` uses `file_path`. `Write` uses `file_path` and `content`. `Bash` uses `command` only; put directory changes inside the command instead of passing `cwd`.
17
+
14
18
  Operate only in the contaminated domain. Read authorized source and contaminated ledgers as needed. Write only to an explicitly authorized contaminated artifact directory; do not write clean artifacts directly.
15
19
 
16
20
  ## Required Handoff Inputs
@@ -11,6 +11,10 @@ color: orange
11
11
 
12
12
  This role is Agent 1 in the clean-room pipeline.
13
13
 
14
+ ## Claude Code Tool Contract
15
+
16
+ When Claude Code tools are available, use their exact parameter names. `Read` uses `file_path`. `Write` uses `file_path` and `content`. `Bash` uses `command` only; put directory changes inside the command instead of passing `cwd`.
17
+
14
18
  Operate only in the contaminated domain. Treat source access as read-only. Write only under `CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS`.
15
19
 
16
20
  Do not use shell-style tools in this role.
package/docs/REFERENCE.md CHANGED
@@ -360,7 +360,12 @@ The runner exports `CLEAN_ROOM_SESSION_BRIEF_PATH`, `CLEAN_ROOM_ROLE_SESSION_ID`
360
360
  | `install lock is held` | Another install or uninstall is mutating the same target root | Wait for the other process to finish; stale locks are handled conservatively. |
361
361
  | Hook config write failed after files copied | Partial installer state | Fix the filesystem error, then re-run the same installer command. |
362
362
  | Install manifest remains `installing` | The previous install did not complete | Re-run the same installer command for that runtime and target root. |
363
+ | `clean-room-skill` is not found | The CLI is not globally installed or the runtime PATH does not include it | Use `npx clean-room-skill@latest ...` immediately; do not search plugin caches for package internals. |
364
+ | `--schema-dir` reports missing schemas | The override points at a stale plugin cache, clean root, or non-directory path | Omit `--schema-dir` to use bundled schemas. Pass it only for a real directory containing `task-manifest.schema.json`. Do not use `/dev/null`. |
365
+ | `task manifest not found` for a task root | The runner needs the contaminated-side manifest file | Pass `~/Documents/CleanRoom/<task-id>/contaminated/task-manifest.json`, not the task root or clean root. |
366
+ | `preflight goal not found` | `task-manifest.json` references a missing or misplaced contaminated-side preflight file | Restore `preflight-goal.json` under the contaminated artifact root, update `preflight_goal_ref` and `preflight_goal_sha256`, then retry `--dry-run`. |
363
367
  | `clean-room run` rejects the manifest | Invalid or incomplete unattended loop metadata | Fix `controller_policy`, `loop_context.foundation_unit_ref`, and `approved_scope_refs`, then retry `--dry-run`. |
368
+ | `clean-room artifact validation failed` lists stale JSON files | Old or hand-written clean-room artifacts are still under contaminated or clean artifact roots | Update those artifacts to current schemas or move stale/legacy JSON to quarantine, then retry `--dry-run`. |
364
369
  | `clean-room run` rejects a covered unit with `discovery_leads` | A high-priority contaminated discovery lead is still unresolved | Analyze the lead in an authorized follow-up unit, mark it resolved, or keep coverage partial/blocked and return an abstract delta. |
365
370
  | `clean-room run` rejects an agent command stage in strict context mode | The stage is missing `context.fresh_session: true`, missing `context.brief_path`, or points the brief outside the allowed artifact root | Fix the stage context and regenerate the role-session brief for the selected unit. |
366
371
  | `clean-room run` reports no progress | Configured stages exited without durable artifact changes | Check role command cwd/argv, selected unit, and artifact write roots. |
package/lib/run-cli.cjs CHANGED
@@ -16,7 +16,7 @@ Options:
16
16
  --max-iterations <n> Lower the manifest/loop iteration cap
17
17
  --once Run at most one inner iteration
18
18
  --dry-run Validate and print the selected unit without writing or spawning agents
19
- --schema-dir <path> Schema directory override
19
+ --schema-dir <path> Schema directory override; omit to use bundled schemas
20
20
  --python <path> Python executable for bundled validation hooks (default: python3)
21
21
  -h, --help Show this help
22
22
  `);
@@ -43,6 +43,7 @@ const {
43
43
  resolvePath,
44
44
  resolveRoots,
45
45
  validateTaskManifestLocation,
46
+ validateSchemaDir,
46
47
  verifyPreflightGoal,
47
48
  } = require('./run-roots.cjs');
48
49
  const {
@@ -144,6 +145,35 @@ function markStageFailed(stageResult, error) {
144
145
  : message;
145
146
  }
146
147
 
148
+ function inferredTaskManifestCandidate(taskManifestPath) {
149
+ if (fs.existsSync(taskManifestPath)) {
150
+ const stat = fs.statSync(taskManifestPath);
151
+ if (stat.isDirectory()) {
152
+ return path.join(taskManifestPath, 'contaminated', 'task-manifest.json');
153
+ }
154
+ }
155
+ if (path.basename(taskManifestPath) !== 'task-manifest.json') {
156
+ return null;
157
+ }
158
+ const parent = path.dirname(taskManifestPath);
159
+ if (path.basename(parent) === 'contaminated') {
160
+ return taskManifestPath;
161
+ }
162
+ return path.join(parent, 'contaminated', 'task-manifest.json');
163
+ }
164
+
165
+ function taskManifestNotFoundMessage(taskManifestPath) {
166
+ const parts = [
167
+ `task manifest not found: ${taskManifestPath}`,
168
+ 'expected task manifest layout: <task-root>/contaminated/task-manifest.json',
169
+ ];
170
+ const candidate = inferredTaskManifestCandidate(taskManifestPath);
171
+ if (candidate && candidate !== taskManifestPath) {
172
+ parts.push(`candidate path: ${candidate}`);
173
+ }
174
+ return parts.join('; ');
175
+ }
176
+
147
177
  async function runCleanRoom(options, context = {}) {
148
178
  if (options.help) {
149
179
  printRunHelp();
@@ -161,10 +191,14 @@ async function runCleanRoom(options, context = {}) {
161
191
 
162
192
  const taskManifestPath = resolvePath(options.taskManifest, context.cwd || process.cwd());
163
193
  if (!fs.existsSync(taskManifestPath)) {
164
- throw new Error(`task manifest not found: ${taskManifestPath}`);
194
+ throw new Error(taskManifestNotFoundMessage(taskManifestPath));
195
+ }
196
+ if (fs.statSync(taskManifestPath).isDirectory()) {
197
+ throw new Error(taskManifestNotFoundMessage(taskManifestPath));
165
198
  }
166
199
  const manifestDir = path.dirname(taskManifestPath);
167
200
  const schemaDir = options.schemaDir ? resolvePath(options.schemaDir, context.cwd || process.cwd()) : defaultSchemaDir();
201
+ validateSchemaDir(schemaDir, Boolean(options.schemaDir));
168
202
  validateTaskManifestSchema(options.python, taskManifestPath, schemaDir);
169
203
  const manifest = readJsonFile(taskManifestPath, null);
170
204
  validateTaskManifestForRun(manifest);
package/lib/run-hooks.cjs CHANGED
@@ -17,6 +17,8 @@ const {
17
17
  packageRoot,
18
18
  } = require('./run-roots.cjs');
19
19
 
20
+ const MAX_ARTIFACT_VALIDATION_FAILURES = 3;
21
+
20
22
  function hookEnv(roots, role = 'contaminated-manager-verifier') {
21
23
  return {
22
24
  ...envFromAllowlist(HOOK_ONLY_ENV_ALLOWLIST),
@@ -52,14 +54,18 @@ function validateTaskManifestSchema(python, manifestPath, schemaDir) {
52
54
  maxBuffer: MAX_OUTPUT_BYTES,
53
55
  });
54
56
  if (result.status !== 0) {
55
- const stderr = String(result.stderr || '').trim();
56
- const stdout = String(result.stdout || '').trim();
57
- const error = result.error?.message || '';
58
- throw new Error(`${scriptName} failed for ${manifestPath}: ${stderr || stdout || error || `exit ${result.status}`}`);
57
+ throw new Error(hookFailureMessage(scriptName, manifestPath, result));
59
58
  }
60
59
  }
61
60
 
62
61
  function runHook(python, scriptName, filePath, roots, role = 'contaminated-manager-verifier') {
62
+ const error = runHookFailure(python, scriptName, filePath, roots, role);
63
+ if (error) {
64
+ throw new Error(error);
65
+ }
66
+ }
67
+
68
+ function runHookFailure(python, scriptName, filePath, roots, role = 'contaminated-manager-verifier') {
63
69
  const result = spawnSync(python, [hookPath(scriptName)], {
64
70
  cwd: packageRoot(),
65
71
  env: hookEnv(roots, role),
@@ -69,23 +75,50 @@ function runHook(python, scriptName, filePath, roots, role = 'contaminated-manag
69
75
  maxBuffer: MAX_OUTPUT_BYTES,
70
76
  });
71
77
  if (result.status !== 0) {
72
- const stderr = String(result.stderr || '').trim();
73
- const stdout = String(result.stdout || '').trim();
74
- const error = result.error?.message || '';
75
- throw new Error(`${scriptName} failed for ${filePath}: ${stderr || stdout || error || `exit ${result.status}`}`);
78
+ return hookFailureMessage(scriptName, filePath, result);
76
79
  }
80
+ return null;
81
+ }
82
+
83
+ function hookFailureMessage(scriptName, filePath, result) {
84
+ const stderr = String(result.stderr || '').trim();
85
+ const stdout = String(result.stdout || '').trim();
86
+ const error = result.error?.message || '';
87
+ return `${scriptName} failed for ${filePath}: ${stderr || stdout || error || `exit ${result.status}`}`;
77
88
  }
78
89
 
79
90
  function validateArtifacts(python, manifestPath, roots, filePaths = null) {
80
91
  const paths = filePaths || trackedArtifactPaths(manifestPath, roots);
92
+ const failures = [];
81
93
  for (const filePath of paths) {
82
94
  if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) continue;
83
- runHook(python, 'validate-json-schema.py', filePath, roots);
84
- runHook(python, 'check-artifact-leakage.py', filePath, roots);
95
+ const schemaError = runHookFailure(python, 'validate-json-schema.py', filePath, roots);
96
+ if (schemaError) {
97
+ failures.push(schemaError);
98
+ if (failures.length >= MAX_ARTIFACT_VALIDATION_FAILURES) break;
99
+ continue;
100
+ }
101
+ const leakageError = runHookFailure(python, 'check-artifact-leakage.py', filePath, roots);
102
+ if (leakageError) {
103
+ failures.push(leakageError);
104
+ if (failures.length >= MAX_ARTIFACT_VALIDATION_FAILURES) break;
105
+ continue;
106
+ }
85
107
  if (path.basename(filePath) === HANDOFF_PACKAGE_NAME) {
86
- runHook(python, 'validate-handoff-package.py', filePath, roots);
108
+ const handoffError = runHookFailure(python, 'validate-handoff-package.py', filePath, roots);
109
+ if (handoffError) {
110
+ failures.push(handoffError);
111
+ if (failures.length >= MAX_ARTIFACT_VALIDATION_FAILURES) break;
112
+ }
87
113
  }
88
114
  }
115
+ if (failures.length > 0) {
116
+ throw new Error([
117
+ 'clean-room artifact validation failed:',
118
+ ...failures.map((failure) => `- ${failure}`),
119
+ 'Recovery: update stale artifacts to current schemas or move stale/legacy JSON out of contaminated and clean artifact roots, for example into quarantine/, then retry --dry-run.',
120
+ ].join('\n'));
121
+ }
89
122
  }
90
123
 
91
124
  module.exports = {
package/lib/run-roots.cjs CHANGED
@@ -18,6 +18,40 @@ function defaultSchemaDir() {
18
18
  return path.join(packageRoot(), 'skills', 'clean-room', 'assets');
19
19
  }
20
20
 
21
+ function validateSchemaDir(schemaDir, explicit = false) {
22
+ let stat;
23
+ try {
24
+ stat = fs.statSync(schemaDir);
25
+ } catch (err) {
26
+ if (err?.code === 'ENOENT') {
27
+ throw new Error(schemaDirError('schema directory not found', schemaDir, explicit));
28
+ }
29
+ throw err;
30
+ }
31
+ if (!stat.isDirectory()) {
32
+ throw new Error(schemaDirError('schema path is not a directory', schemaDir, explicit));
33
+ }
34
+ const taskManifestSchema = path.join(schemaDir, 'task-manifest.schema.json');
35
+ try {
36
+ const schemaStat = fs.statSync(taskManifestSchema);
37
+ if (!schemaStat.isFile()) {
38
+ throw new Error(schemaDirError('schema directory is missing task-manifest.schema.json', schemaDir, explicit));
39
+ }
40
+ } catch (err) {
41
+ if (err?.code === 'ENOENT') {
42
+ throw new Error(schemaDirError('schema directory is missing task-manifest.schema.json', schemaDir, explicit));
43
+ }
44
+ throw err;
45
+ }
46
+ }
47
+
48
+ function schemaDirError(reason, schemaDir, explicit) {
49
+ if (explicit) {
50
+ return `${reason}: ${schemaDir}. Omit --schema-dir to use bundled schemas at ${defaultSchemaDir()}.`;
51
+ }
52
+ return `${reason}: ${schemaDir}. The bundled schema directory should contain task-manifest.schema.json.`;
53
+ }
54
+
21
55
  function hookPath(scriptName) {
22
56
  return path.join(packageRoot(), 'hooks', scriptName);
23
57
  }
@@ -115,13 +149,18 @@ function validateRootSeparation(roots) {
115
149
  }
116
150
 
117
151
  function validateTaskManifestLocation(taskManifestPath, roots) {
152
+ const expected = path.join(roots.contaminatedRoot, 'task-manifest.json');
118
153
  if (!pathIsUnder(taskManifestPath, roots.contaminatedRoot)) {
119
- throw new Error('task manifest must be under contaminated artifact root');
154
+ throw new Error(
155
+ `task manifest must be under contaminated artifact root; resolved path: ${taskManifestPath}; expected: ${expected}`
156
+ );
120
157
  }
121
158
  const realTaskManifestPath = realpathIfExists(taskManifestPath);
122
159
  const realContaminatedRoot = realpathIfExists(roots.contaminatedRoot) || roots.contaminatedRoot;
123
160
  if (!realTaskManifestPath || !pathIsUnder(realTaskManifestPath, realContaminatedRoot)) {
124
- throw new Error('task manifest must resolve under contaminated artifact root');
161
+ throw new Error(
162
+ `task manifest must resolve under contaminated artifact root; resolved path: ${taskManifestPath}; expected: ${expected}`
163
+ );
125
164
  }
126
165
  }
127
166
 
@@ -144,41 +183,46 @@ function envFromAllowlist(extraNames = []) {
144
183
 
145
184
  function verifyPreflightGoal(manifest, manifestDir, roots) {
146
185
  const preflightGoalPath = resolveManifestRoot(manifest.preflight_goal_ref, manifestDir);
186
+ const expected = path.join(roots.contaminatedRoot, 'preflight-goal.json');
147
187
  if (!preflightGoalPath) {
148
- throw new Error('clean-room run requires task-manifest preflight_goal_ref');
188
+ throw new Error(`clean-room run requires task-manifest preflight_goal_ref; expected: ${expected}`);
149
189
  }
150
190
  if (!pathIsUnder(preflightGoalPath, roots.contaminatedRoot)) {
151
- throw new Error('preflight goal must resolve under contaminated artifact root');
191
+ throw new Error(
192
+ `preflight goal must resolve under contaminated artifact root; resolved path: ${preflightGoalPath}; expected under: ${roots.contaminatedRoot}`
193
+ );
152
194
  }
153
195
  let preflightGoalRealPath;
154
196
  try {
155
197
  preflightGoalRealPath = fs.realpathSync(preflightGoalPath);
156
198
  } catch (err) {
157
199
  if (err?.code === 'ENOENT') {
158
- throw new Error('preflight goal not found');
200
+ throw new Error(`preflight goal not found: ${preflightGoalPath}; expected: ${expected}`);
159
201
  }
160
202
  throw err;
161
203
  }
162
204
  const contaminatedRootRealPath = realpathIfExists(roots.contaminatedRoot) || roots.contaminatedRoot;
163
205
  if (!pathIsUnder(preflightGoalRealPath, contaminatedRootRealPath)) {
164
- throw new Error('preflight goal must resolve under contaminated artifact root');
206
+ throw new Error(
207
+ `preflight goal must resolve under contaminated artifact root; resolved path: ${preflightGoalPath}; expected under: ${roots.contaminatedRoot}`
208
+ );
165
209
  }
166
210
  let stat;
167
211
  try {
168
212
  stat = fs.statSync(preflightGoalRealPath);
169
213
  } catch (err) {
170
214
  if (err?.code === 'ENOENT') {
171
- throw new Error('preflight goal not found');
215
+ throw new Error(`preflight goal not found: ${preflightGoalPath}; expected: ${expected}`);
172
216
  }
173
217
  throw err;
174
218
  }
175
219
  if (!stat.isFile()) {
176
- throw new Error('preflight goal is not a file');
220
+ throw new Error(`preflight goal is not a file: ${preflightGoalPath}`);
177
221
  }
178
222
  const actual = fileHash(preflightGoalRealPath).toLowerCase();
179
- const expected = manifest.preflight_goal_sha256.toLowerCase();
180
- if (actual !== expected) {
181
- throw new Error('preflight goal sha256 mismatch');
223
+ const expectedHash = manifest.preflight_goal_sha256.toLowerCase();
224
+ if (actual !== expectedHash) {
225
+ throw new Error(`preflight goal sha256 mismatch: ${preflightGoalPath}`);
182
226
  }
183
227
  }
184
228
 
@@ -226,5 +270,6 @@ module.exports = {
226
270
  resolvePath,
227
271
  resolveRoots,
228
272
  validateTaskManifestLocation,
273
+ validateSchemaDir,
229
274
  verifyPreflightGoal,
230
275
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clean-room-skill",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
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.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Spec-first clean-room workflow for authorized source analysis without replacement code.",
5
5
  "author": {
6
6
  "name": "whit3rabbit"
@@ -54,7 +54,7 @@ Optional AST/indexing helpers are detected before the controller loop through `s
54
54
 
55
55
  Controller mode defaults to `attended` when `task-manifest.json` has no `controller_policy`. The outer loop evolves specs and selects one approved spec slice. Code-development runs start with exactly one `unit_kind: "foundation"` unit named by `loop_context.foundation_unit_ref`; non-foundation behavior slices wait until that unit is covered. The inner clean-room loop completes the approved slice through sanitized handoff, implementation, QC, optional final polish review, and contaminated-side coverage verification, then returns `clean-room-result.json` to the outer loop. In `attended` mode, agent zero pauses for human review at scope gate, handoff, QC deltas, polish deltas, blocked units, and final coverage. In `unattended` mode, agent zero may run a bounded inner loop: reload durable artifacts for each iteration, select at most one pending or gap unit inside `loop_context.approved_scope_refs`, start each role from fresh context with the required environment block, validate before advancing, and stop on any configured safety or ambiguity condition.
56
56
 
57
- In Claude Code unattended mode, launch the durable runner with `clean-room-skill run --task-manifest <path> --agent-runtime claude` (or `npx clean-room-skill@latest run --task-manifest <path> --agent-runtime claude` if the binary is not available) when possible. The main conversation must not do Agent 1, Agent 2, Agent 3, or Agent 4 work, and must not ask to continue while unattended policy still allows bounded progress. If role-agent dispatch is unavailable, fail closed with a blocker.
57
+ In Claude Code unattended mode, launch the durable runner with `clean-room-skill run --task-manifest <path> --agent-runtime claude` when possible. If `clean-room-skill` is not on `PATH`, immediately use `npx clean-room-skill@latest run --task-manifest <path> --agent-runtime claude`. Do not search plugin cache paths for schema files, and do not pass `--schema-dir /dev/null`; the runner uses bundled schemas by default. The main conversation must not do Agent 1, Agent 2, Agent 3, or Agent 4 work, and must not ask to continue while unattended policy still allows bounded progress. If role-agent dispatch is unavailable, fail closed with a blocker.
58
58
 
59
59
  Do not grant shell-style tools to Agent 0, Agent 1, Agent 1.5, Agent 2, or the default Agent 3/4 role sessions. Agent 3 terminal verification may use shell-style tools only when `CLEAN_ROOM_ALLOW_AGENT3_SHELL=1`, the command cwd is under `CLEAN_ROOM_IMPLEMENTATION_ROOTS`, and the command invokes the installed `agent3-verification-runner.py`. Agent 4 polish verification and commit may use shell-style tools only when `CLEAN_ROOM_ALLOW_AGENT4_SHELL=1`, cwd is under `CLEAN_ROOM_IMPLEMENTATION_ROOTS`, and the command invokes the installed `agent4-polish-runner.py`. Use `--hooks=strict` for dedicated Codex, Claude, or OpenCode clean-room homes so hooks fail closed if required environment is missing or shell tools are invoked outside the allowed runner boundaries. Safe hook installs are compatibility-only between runs; during init/onboarding, prepare the role environment block and pass it into every clean-room role session so safe hooks enforce during active work.
60
60
 
@@ -11,7 +11,7 @@ Resume an existing clean-room run from durable artifacts. Never use prior chat h
11
11
 
12
12
  Use the canonical `clean-room` skill workflow and references in this plugin. Read `skills/clean-room/references/CONTROLLER-LOOP.md` when the manifest records `loop_context` or unattended mode. Preserve the same clean-room boundary, role separation, artifact schemas, leakage rules, implementation-root rules, and hook expectations.
13
13
 
14
- If `task-manifest.json` records `controller_policy.mode: "unattended"` in Claude Code, prefer launching `clean-room-skill run --task-manifest <path> --agent-runtime claude` (or `npx clean-room-skill@latest run --task-manifest <path> --agent-runtime claude` if the binary is not available) and let the durable runner assign role agents. The main conversation must not perform Agent 1, Agent 2, Agent 3, or Agent 4 work. Do not ask to continue while unattended policy, iteration budget, and approved pending or gap units still permit progress. If the runner or Claude role-agent dispatch is unavailable, stop with `BLOCKERS: Claude role-agent dispatch unavailable` rather than silently continuing in the main chat.
14
+ If `task-manifest.json` records `controller_policy.mode: "unattended"` in Claude Code, prefer launching `clean-room-skill run --task-manifest <path> --agent-runtime claude` and let the durable runner assign role agents. If `clean-room-skill` is not on `PATH`, immediately use `npx clean-room-skill@latest run --task-manifest <path> --agent-runtime claude` instead of searching for the installed package. Do not search plugin cache paths for schema files, and do not pass `--schema-dir /dev/null`. The runner uses bundled schemas by default; pass `--schema-dir` only when the user provides a real schema directory. The main conversation must not perform Agent 1, Agent 2, Agent 3, or Agent 4 work. Do not ask to continue while unattended policy, iteration budget, and approved pending or gap units still permit progress. If the runner or Claude role-agent dispatch is unavailable, stop with `BLOCKERS: Claude role-agent dispatch unavailable` rather than silently continuing in the main chat.
15
15
 
16
16
  ## Load Order
17
17
 
@@ -15,7 +15,7 @@ Use the canonical `clean-room` skill workflow and references in this plugin. Rea
15
15
 
16
16
  Before asking setup or preflight questions, use the canonical `clean-room` "Run State Discovery Before Wizard" rules. Resolve explicit artifact paths first, then configured clean-room roots, then bounded `~/Documents/CleanRoom/task-*` candidates. If a valid `task-manifest.json` exists, route to `resume-cr`. If a valid canonical `preflight-goal.json` exists without a manifest, continue at source/destination discovery and manifest creation. If a preflight artifact exists but is invalid, stop with schema errors instead of restarting preflight. If multiple candidates are found without an explicit path, list them and stop for selection.
17
17
 
18
- When resuming a valid unattended `task-manifest.json` in Claude Code, prefer launching the durable runner with `clean-room-skill run --task-manifest <path> --agent-runtime claude` (or `npx clean-room-skill@latest run --task-manifest <path> --agent-runtime claude` if the binary is not available). The main conversation must not perform Agent 1, Agent 2, Agent 3, or Agent 4 work. Do not ask to continue while `controller_policy.mode` is `unattended`, the iteration budget remains, and approved pending or gap units remain. If Claude role-agent dispatch or the runner is unavailable, stop with `BLOCKERS: Claude role-agent dispatch unavailable` instead of falling back to main-chat execution.
18
+ When resuming a valid unattended `task-manifest.json` in Claude Code, prefer launching the durable runner with `clean-room-skill run --task-manifest <path> --agent-runtime claude`. If `clean-room-skill` is not on `PATH`, immediately use `npx clean-room-skill@latest run --task-manifest <path> --agent-runtime claude` instead of searching for the installed package. Do not search plugin cache paths for schema files, and do not pass `--schema-dir /dev/null`. The runner uses bundled schemas by default; pass `--schema-dir` only when the user provides a real schema directory. The main conversation must not perform Agent 1, Agent 2, Agent 3, or Agent 4 work. Do not ask to continue while `controller_policy.mode` is `unattended`, the iteration budget remains, and approved pending or gap units remain. If Claude role-agent dispatch or the runner is unavailable, stop with `BLOCKERS: Claude role-agent dispatch unavailable` instead of falling back to main-chat execution.
19
19
 
20
20
  Load or create `preflight-goal.json` first. Unattended mode requires a complete goal contract with no blocking or non-blocking `open_questions`, `controller_policy.unattended_allowed_after_preflight: true`, and a finite `controller_policy.max_iterations`.
21
21