clean-room-skill 0.2.2 → 0.3.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 (36) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +5 -1
  5. package/agents/clean-architect.md +4 -0
  6. package/agents/clean-implementer-verifier-shell.md +4 -0
  7. package/agents/clean-polish-reviewer.md +4 -0
  8. package/agents/clean-qa-editor.md +4 -0
  9. package/agents/contaminated-handoff-sanitizer.md +4 -0
  10. package/agents/contaminated-manager-verifier.md +4 -0
  11. package/agents/contaminated-source-analyst.md +4 -0
  12. package/docs/ARCHITECTURE.md +2 -0
  13. package/docs/REFERENCE.md +28 -1
  14. package/lib/bootstrap.cjs +282 -30
  15. package/lib/fs-utils.cjs +1 -0
  16. package/lib/run-cli.cjs +1 -1
  17. package/lib/run-constants.cjs +7 -0
  18. package/lib/run-controller.cjs +38 -3
  19. package/lib/run-hooks.cjs +44 -11
  20. package/lib/run-results.cjs +23 -0
  21. package/lib/run-roots.cjs +56 -11
  22. package/package.json +1 -1
  23. package/plugin.json +1 -1
  24. package/skills/attended/SKILL.md +2 -1
  25. package/skills/clean-room/SKILL.md +6 -4
  26. package/skills/clean-room/assets/init-config.schema.json +8 -0
  27. package/skills/clean-room/assets/task-manifest.schema.json +16 -0
  28. package/skills/clean-room/references/LEAKAGE-RULES.md +2 -0
  29. package/skills/clean-room/references/PREFLIGHT.md +1 -0
  30. package/skills/clean-room/references/PROCESS.md +4 -1
  31. package/skills/clean-room/references/SPEC-SCHEMA.md +3 -0
  32. package/skills/init/SKILL.md +3 -2
  33. package/skills/preflight/SKILL.md +2 -1
  34. package/skills/resume-cr/SKILL.md +3 -2
  35. package/skills/start-over/SKILL.md +2 -0
  36. package/skills/unattended/SKILL.md +3 -2
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 = {
@@ -10,6 +10,9 @@ const {
10
10
  } = require('./fs-utils.cjs');
11
11
  const {
12
12
  CLEAN_RUN_CONTEXT_NAME,
13
+ IMPLEMENTATION_LOCK_NAME,
14
+ IMPLEMENTATION_LOCK_POLL_MS,
15
+ IMPLEMENTATION_LOCK_WAIT_MS,
13
16
  MAX_LEDGER_ITERATIONS,
14
17
  POLISH_REPORT_NAME,
15
18
  RUN_LOCK_NAME,
@@ -431,6 +434,25 @@ async function withRunLock(contaminatedRoot, dryRun, fn) {
431
434
  }, fn);
432
435
  }
433
436
 
437
+ async function withImplementationRootLocks(implementationRoots, dryRun, fn) {
438
+ if (dryRun) return fn();
439
+ // Realpath dedupe: root-separation checks do not compare implementation
440
+ // roots against each other, so symlink-aliased duplicates would otherwise
441
+ // self-deadlock on their own live-pid lock. Sorting gives every run the
442
+ // same global acquisition order, preventing AB/BA deadlocks.
443
+ const roots = [...new Set(implementationRoots.map((root) => {
444
+ fs.mkdirSync(root, { recursive: true });
445
+ return fs.realpathSync(root);
446
+ }))].sort();
447
+ const run = roots.reduceRight((next, root) => () => withDirectoryLock({
448
+ lockPath: path.join(root, IMPLEMENTATION_LOCK_NAME),
449
+ waitMs: IMPLEMENTATION_LOCK_WAIT_MS,
450
+ pollMs: IMPLEMENTATION_LOCK_POLL_MS,
451
+ label: 'clean-room implementation lock',
452
+ }, next), fn);
453
+ return run();
454
+ }
455
+
434
456
  module.exports = {
435
457
  buildResult,
436
458
  completeResultOrSpecDelta,
@@ -439,6 +461,7 @@ module.exports = {
439
461
  loadLedger,
440
462
  noProgressResult,
441
463
  stageFailureResult,
464
+ withImplementationRootLocks,
442
465
  withRunLock,
443
466
  writeLedger,
444
467
  writeResult,
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.3.0",
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.3.0",
4
4
  "description": "Spec-first clean-room workflow for authorized source analysis without replacement code.",
5
5
  "author": {
6
6
  "name": "whit3rabbit"
@@ -13,7 +13,7 @@ In Pi, this entry point is invoked as `/skill:attended`.
13
13
 
14
14
  Use the canonical `clean-room` skill workflow and references in this plugin. Preserve the same clean-room boundary, role separation, artifact schemas, leakage rules, implementation-root rules, and hook expectations.
15
15
 
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.
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-*` (legacy) and `~/Documents/CleanRoom/*/tasks/task-*` (project layout) 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
18
  Load or create `preflight-goal.json` first. Attended mode may continue with unresolved questions only when they are recorded as `open_questions`; blocking questions become pause gates before affected work starts.
19
19
 
@@ -21,6 +21,7 @@ Gather only required setup facts:
21
21
 
22
22
  - Authorization statement, requester, allowed actions, prohibited actions, and evidence handling.
23
23
  - Artifact base root, defaulting to `~/Documents/CleanRoom/<task-id>/`. If the user does not provide an explicitly approved neutral task ID, generate one as `task-` plus 8 lowercase hex characters. Do not derive task IDs or output directory names from source folder names.
24
+ - Optional project grouping for multi-task destinations, following the canonical `clean-room` project layout rules: `<base>/<project>/tasks/<task-id>/` with one shared `<base>/<project>/implementation/` root, a neutral project name (random word pair or `proj-` plus 8 lowercase hex, matching `[a-z0-9][a-z0-9-]{0,63}`, never source-derived), and at most one active task per project.
24
25
  - Source roots, contaminated artifact root, clean artifact root, clean implementation root, quarantine root, and optional public or destination reference roots.
25
26
  - Target stack, destination constraints, dependency/license policy, exactness policy, feature policy, code hygiene policy, and output policy from `preflight-goal.json`.
26
27
  - Target schema profile: `openspec-delta`, `gsd-planning-package`, `speckit-feature-folder`, or `kiro-spec-folder`.
@@ -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
 
@@ -80,15 +80,16 @@ Discovery order:
80
80
 
81
81
  1. Resolve explicit user-provided paths first. Accept a task root, `task-manifest.json`, `preflight-goal.json`, or `clean-room-bootstrap.json`.
82
82
  2. Inspect configured clean-room roots from the current request or environment, including `CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS`, `CLEAN_ROOM_CLEAN_ROOTS`, and `CLEAN_ROOM_IMPLEMENTATION_ROOTS` when present.
83
- 3. Scan `~/Documents/CleanRoom/task-*` as a bounded fallback. Inspect only immediate task directories and their expected artifact names.
83
+ 3. Scan `~/Documents/CleanRoom/task-*` (legacy single-task layout) and `~/Documents/CleanRoom/*/clean-room-project.json` plus `~/Documents/CleanRoom/*/tasks/task-*` (project layout) as a bounded fallback. Inspect only immediate project directories, their `tasks/` children, and expected artifact names.
84
84
 
85
- If more than one candidate run is found without an explicit user path, list the candidate task roots and stop for explicit selection. Do not choose the newest candidate automatically.
85
+ If more than one candidate run is found without an explicit user path, list the candidate task roots grouped by project and stop for explicit selection. Do not choose the newest candidate automatically.
86
86
 
87
87
  Classify the selected candidate before starting the wizard:
88
88
 
89
89
  - Valid `task-manifest.json`: route to `resume-cr` and continue from the earliest incomplete gate.
90
90
  - Valid canonical `preflight-goal.json` without `task-manifest.json`: continue at source/destination discovery and manifest creation. Do not ask the preflight wizard again.
91
91
  - `clean-room-bootstrap.json` only: run preflight using the bootstrap roots.
92
+ - `clean-room-project.json` with no task directories under `tasks/`: treat as an empty project and offer to create its first task inside that project.
92
93
  - Invalid `preflight-goal.json`: stop, report canonical schema or required-field errors, and do not create a replacement preflight.
93
94
  - No artifacts found: start the normal preflight wizard.
94
95
 
@@ -98,6 +99,7 @@ Gather only the setup facts needed to decide whether the workflow may start, or
98
99
 
99
100
  - Authorization statement, requester, allowed actions, prohibited actions, and evidence handling.
100
101
  - Artifact base root. Default to `~/Documents/CleanRoom/<task-id>/`. If the user does not provide an explicitly approved neutral task ID, generate one as `task-` plus 8 lowercase hex characters. Do not derive task IDs or output directory names from source folder names.
102
+ - Optional project grouping. Ask whether to group this run under a clean-room project when multiple tasks will target the same destination; default to the legacy single-task layout. Project layout is `<base>/<project>/tasks/<task-id>/` with one shared `<base>/<project>/implementation/` root for every task in the project. When the user does not supply an approved neutral project name, generate a random neutral word pair such as `amber-meadow`; it must match `[a-z0-9][a-z0-9-]{0,63}`, must never be derived from source or destination folder basenames or meaningful source-name tokens (project names appear in paths clean roles can see), and falls back to `proj-` plus 8 lowercase hex characters when no neutral word pair is available. Only one task per project may run at a time because tasks share the implementation root; the durable runner enforces this with an advisory `.clean-room-implementation.lock` in each implementation root.
101
103
  - Source roots or fallback visual evidence roots, contaminated artifact root, clean artifact root, clean implementation root, quarantine root, and optional public or destination reference roots.
102
104
  - Target stack and destination constraints from `preflight-goal.json`.
103
105
  - Target schema profile: `openspec-delta`, `gsd-planning-package`, `speckit-feature-folder`, or `kiro-spec-folder`.
@@ -107,7 +109,7 @@ Gather only the setup facts needed to decide whether the workflow may start, or
107
109
  - Run state. New runs use `generation: 1`, current `started_at`, and `restart_reason: user-requested`.
108
110
  - Role hook environment block. Derive `CLEAN_ROOM_ROLE`, `CLEAN_ROOM_SOURCE_ROOTS`, `CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS`, `CLEAN_ROOM_CLEAN_ROOTS`, `CLEAN_ROOM_IMPLEMENTATION_ROOTS`, `CLEAN_ROOM_ALLOWED_READ_ROOTS`, `CLEAN_ROOM_SCHEMA_DIR`, and optional hook-only denylist variables from the approved roots before launching any role session. Do not ask the user to export `CLEAN_ROOM_HOOK_ENFORCE` for normal safe-hook runs.
109
111
 
110
- Before indexing or artifact generation, confirm that source roots, contaminated artifact roots, clean artifact roots, clean implementation roots, approved public reference roots, and schema directory are separate paths, and that clean/contaminated/implementation root path names are not source-derived. Stop if authorization is unclear, if clean and contaminated roots overlap, if implementation roots overlap any other trust-domain root, or if artifact/root paths contain source root basenames or meaningful non-generic source-name tokens. Agent 2, Agent 3, and Agent 4 must not receive source mounts or the full task manifest.
112
+ Before indexing or artifact generation, confirm that source roots, contaminated artifact roots, clean artifact roots, clean implementation roots, approved public reference roots, and schema directory are separate paths, and that clean/contaminated/implementation root path names are not source-derived. In project layout the implementation root is the shared project-level folder; per-task contaminated, clean, and quarantine roots stay under the task root, and the project name itself must pass the same source-derived-name checks. Stop if authorization is unclear, if clean and contaminated roots overlap, if implementation roots overlap any other trust-domain root, or if artifact/root paths contain source root basenames or meaningful non-generic source-name tokens. Agent 2, Agent 3, and Agent 4 must not receive source mounts or the full task manifest.
111
113
 
112
114
  For `attended` mode, record a `controller_policy` that pauses for human review at scope gate, clean handoff, terminal implementation deltas, blocked units, and final coverage. Include stop conditions for `authorization-missing`, `scope-change`, `contamination-suspected`, `schema-validation-failed`, `leakage-scan-failed`, `unit-blocked`, `implementation-complete`, and `coverage-complete`; attended mode does not add an iteration-limit stop unless the user explicitly sets one.
113
115
 
@@ -27,6 +27,14 @@
27
27
  "type": "string",
28
28
  "minLength": 1
29
29
  },
30
+ "project_id": {
31
+ "type": "string",
32
+ "pattern": "^[a-z0-9][a-z0-9-]{0,63}$"
33
+ },
34
+ "project_root": {
35
+ "type": "string",
36
+ "minLength": 1
37
+ },
30
38
  "root_preferences": {
31
39
  "type": "object",
32
40
  "additionalProperties": false,
@@ -33,6 +33,14 @@
33
33
  "type": "string",
34
34
  "minLength": 1
35
35
  },
36
+ "project_id": {
37
+ "type": "string",
38
+ "pattern": "^[a-z0-9][a-z0-9-]{0,63}$"
39
+ },
40
+ "project_root": {
41
+ "type": "string",
42
+ "minLength": 1
43
+ },
36
44
  "target_identifier": {
37
45
  "type": "string",
38
46
  "minLength": 1
@@ -1215,6 +1223,14 @@
1215
1223
  "type": "string",
1216
1224
  "minLength": 1
1217
1225
  },
1226
+ "project_id": {
1227
+ "type": "string",
1228
+ "pattern": "^[a-z0-9][a-z0-9-]{0,63}$"
1229
+ },
1230
+ "project_root": {
1231
+ "type": "string",
1232
+ "minLength": 1
1233
+ },
1218
1234
  "target_profile": {
1219
1235
  "enum": [
1220
1236
  "openspec-delta",
@@ -47,6 +47,8 @@ Treat implementation identifiers as contaminated by default. Package names, name
47
47
 
48
48
  Public compatibility surface means the name is externally documented, required by an existing integration, visible in a public protocol or file format, or explicitly required by the destination scope. If a name is retained, place it in `public_surface` or `public_contracts` with `name`, `kind`, `visibility`, and a concrete compatibility reason. Valid `visibility` values are `public`, `destination`, `protocol`, and `user-required`. Do not mention source-private names in summaries, claims, tests, open questions, skeleton areas, QC findings, or delta tickets.
49
49
 
50
+ Task IDs and project names are clean-visible path components: they appear in roots that clean roles read and in paths recorded by clean artifacts. Both must stay neutral. Use generated `task-` plus 8 lowercase hex IDs, and for projects a random neutral word pair or `proj-` plus 8 lowercase hex, matching `[a-z0-9][a-z0-9-]{0,63}`. Never derive a task ID or project name from source folder basenames or meaningful source-name tokens.
51
+
50
52
  The contaminated side should maintain a private identifier denylist for guardrail scanning when practical. The denylist is line-oriented, ignores blank lines and `#` comments, and is bounded to 1,000,000 bytes per file, 20,000 total terms, and 512 characters per term. Keep that list out of clean/source-denied readable roots and do not paste its contents into clean artifacts or model-visible reports.
51
53
 
52
54
  Agent 1.5 may use the denylist only through hook scanning. Do not include denylist terms in the neutral sanitizer brief, sanitizer prompts, sanitizer reports, clean artifacts, or model-visible feedback.
@@ -24,6 +24,7 @@ Record every default as an assumption. Good defaults:
24
24
 
25
25
  - Artifact base: `~/Documents/CleanRoom/<task-id>/`.
26
26
  - Implementation root: `~/Documents/CleanRoom/<task-id>/implementation/`.
27
+ - Project layout (when grouping multiple tasks): task root `~/Documents/CleanRoom/<project>/tasks/<task-id>/` with shared implementation root `~/Documents/CleanRoom/<project>/implementation/`.
27
28
  - Existing destination policy: `inspect-and-preserve`.
28
29
  - Dependency policy: allow new dependencies, prefer standard library, require approval for native/system dependencies.
29
30
  - Dependency licenses: allow MIT, Apache-2.0, BSD-2-Clause, and BSD-3-Clause; block GPL-3.0 and AGPL-3.0 unless the user explicitly approves otherwise.
@@ -20,7 +20,9 @@ Use separate locations for each trust domain:
20
20
 
21
21
  Clean, contaminated, and implementation paths must remain neutral. If the user does not provide an explicitly approved neutral task ID, generate one as `task-` plus 8 lowercase hex characters and use it under `~/Documents/CleanRoom/`.
22
22
 
23
- Do not derive task IDs, clean roots, contaminated artifact roots, or implementation roots from source folder names. The initialization wizard and environment preflight reject artifact paths that contain a source root basename or meaningful non-generic tokens from that basename.
23
+ Multiple tasks targeting the same destination may be grouped under an optional clean-room project: `~/Documents/CleanRoom/<project>/tasks/<task-id>/` with one shared `<project>/implementation/` root. Project names follow the same neutrality rules as task IDs: a random neutral word pair (such as `amber-meadow`) or `proj-` plus 8 lowercase hex characters, matching `[a-z0-9][a-z0-9-]{0,63}`. Because the shared implementation root serves every task in the project, run at most one active task per project at a time; root-separation checks for one task cannot see a sibling task's concurrent clean implementation session. `clean-room-skill run` enforces this with an advisory `.clean-room-implementation.lock` in each implementation root, but manually launched role sessions outside the runner must still respect the rule.
24
+
25
+ Do not derive task IDs, project names, clean roots, contaminated artifact roots, or implementation roots from source folder names. The initialization wizard and environment preflight reject artifact paths that contain a source root basename or meaningful non-generic tokens from that basename.
24
26
 
25
27
  Prefer separate agent profiles or homes when the host supports them. Do not rely on one chat context with role labels as the only separation control.
26
28
 
@@ -212,6 +214,7 @@ Clean polish reviewer:
212
214
  2. Initialization gate:
213
215
  - Record reusable preferences in `init-config.json` when requested.
214
216
  - Default the artifact base root to `~/Documents/CleanRoom/<task-id>/` unless the user selects another separated location. Generate a neutral `task-` plus 8 lowercase hex characters when the user does not provide an explicitly approved neutral task ID.
217
+ - When grouping tasks under a project, place the task root at `~/Documents/CleanRoom/<project>/tasks/<task-id>/`, share the project-level `implementation/` root, and record `project_id` and `project_root` in `init-config.json` and the manifest `initialization_snapshot`.
215
218
  - Reject clean, contaminated, or implementation roots that mirror source root basenames or meaningful non-generic source-name tokens.
216
219
  - Record model preferences as a default model plus optional domain or role overrides.
217
220
  - Split user rules into clean-safe and contaminated-only rules.
@@ -100,11 +100,14 @@ Unattended mode requires `unattended_allowed_after_preflight: true`, finite `max
100
100
 
101
101
  Default artifact roots live under `~/Documents/CleanRoom/<task-id>/`. If the user does not provide an explicitly approved neutral task ID, generate one as `task-` plus 8 lowercase hex characters. Do not use the source folder name as the task ID.
102
102
 
103
+ When tasks are grouped under a project, roots live under `~/Documents/CleanRoom/<project>/tasks/<task-id>/` with a shared `~/Documents/CleanRoom/<project>/implementation/` root. Project names follow the same neutrality rules: a random neutral word pair or `proj-` plus 8 lowercase hex characters, never derived from source folder names.
104
+
103
105
  Clean artifact, contaminated artifact, and implementation roots must not contain source root basenames or meaningful non-generic tokens from those basenames. The environment preflight enforces this for `CLEAN_ROOM_CLEAN_ROOTS`, `CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS`, and `CLEAN_ROOM_IMPLEMENTATION_ROOTS`.
104
106
 
105
107
  Capture:
106
108
 
107
109
  - artifact base root, defaulting to `~/Documents/CleanRoom/<task-id>/` with a neutral task ID
110
+ - optional `project_id` and `project_root` when grouping tasks under a clean-room project
108
111
  - source roots or fallback visual evidence roots, contaminated artifact root, clean artifact root, clean implementation roots, quarantine root, and approved public references
109
112
  - target profile
110
113
  - default model plus optional clean, contaminated, or per-role overrides
@@ -19,9 +19,9 @@ Keep `preflight-goal.json` in the controller/contaminated artifact domain. Clean
19
19
 
20
20
  Use the canonical `clean-room` skill workflow and references in this plugin. Preserve the clean-room boundary, role separation, artifact schemas, leakage rules, implementation-root rules, and hook expectations.
21
21
 
22
- The CLI command `clean-room-skill init` (or `npx clean-room-skill@latest init` if the binary is not available) may have pre-created neutral external folders and a clean-safe `.clean-room/README.md` stub in the target repository. The bootstrap task root must contain `contaminated/`, `clean/`, `implementation/`, and `quarantine/`. 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`.
22
+ The CLI command `clean-room-skill init` (or `npx clean-room-skill@latest init` if the binary is not available) may have pre-created neutral external folders and a clean-safe `.clean-room/README.md` stub in the target repository. The bootstrap has two shapes. The legacy single-task root contains `contaminated/`, `clean/`, `implementation/`, and `quarantine/`. The project layout (`--project` or `--new-project`) places the task root at `<base>/<project>/tasks/<task-id>/` with per-task `contaminated/`, `clean/`, and `quarantine/`, plus a shared project-level `implementation/` and a `clean-room-project.json` metadata file at the project root. 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/`, `implementation/`, `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.
24
+ When using an existing CLI bootstrap, check `clean-room-bootstrap.json`, `contaminated/`, `clean/`, `quarantine/`, the implementation root (task-level in the legacy layout, project-level in the project layout), and the target repo `.clean-room/README.md` before recording active init preferences. In the project layout also check `clean-room-project.json` and that the task root sits under the project's `tasks/` directory. 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
25
 
26
26
  ## Gather
27
27
 
@@ -30,6 +30,7 @@ Collect only setup decisions that affect correctness, safety, resumability, or o
30
30
  - Requester authorization, allowed actions, prohibited actions, and evidence handling.
31
31
  - Source roots, contaminated artifact root, clean artifact root, clean implementation roots, quarantine root, and approved public or destination reference roots.
32
32
  - Artifact base root. Default to `~/Documents/CleanRoom/<task-id>/`, never to the source workspace or a temporary directory unless the user explicitly chooses it. If the user does not provide an explicitly approved neutral task ID, generate one as `task-` plus 8 lowercase hex characters. Do not derive task IDs or output directory names from source folder names.
33
+ - Optional project grouping. When multiple tasks will target the same destination, record `project_id` and `project_root` for the `<base>/<project>/tasks/<task-id>/` layout with its shared project-level implementation root. Project names follow the same neutrality rules as task IDs: a random neutral word pair or `proj-` plus 8 lowercase hex, matching `[a-z0-9][a-z0-9-]{0,63}`, never derived from source folder names. Record both fields in `init-config.json` and the manifest `initialization_snapshot`.
33
34
  - Target schema profile: `openspec-delta`, `gsd-planning-package`, `speckit-feature-folder`, or `kiro-spec-folder`.
34
35
  - Goal contract choices from `preflight-goal.json`, including target stack, dependency/license policy, exactness policy, feature policy, code hygiene, output policy, and controller mode.
35
36
  - Default model plus optional overrides for contaminated roles, clean roles, or individual roles. Keep model ids as runtime-specific strings.
@@ -11,7 +11,7 @@ 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` (or `npx clean-room-skill@latest init` if the binary is not available), check the generated bootstrap scaffold before creating or copying `preflight-goal.json`: `clean-room-bootstrap.json`, `contaminated/`, `clean/`, `implementation/`, `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`.
14
+ If the user provides output from CLI `clean-room-skill init` (or `npx clean-room-skill@latest init` if the binary is not available), check the generated bootstrap scaffold before creating or copying `preflight-goal.json`: `clean-room-bootstrap.json`, `contaminated/`, `clean/`, the implementation root, `quarantine/`, and the target repo `.clean-room/README.md` must exist and agree. In the project layout the task root sits at `<base>/<project>/tasks/<task-id>/`, the implementation root is the shared project-level `implementation/`, and `clean-room-project.json` must exist at the project root. 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
15
 
16
16
  ## Required Contract
17
17
 
@@ -52,6 +52,7 @@ Use the CLI (`clean-room-skill` if installed, or `npx clean-room-skill@latest` a
52
52
  clean-room-skill preflight --template --output ~/Documents/CleanRoom/task-xxxxxxxx/contaminated/preflight-goal.json
53
53
  clean-room-skill preflight --input ./preflight-goal.json --output ~/Documents/CleanRoom/task-xxxxxxxx/contaminated/preflight-goal.json
54
54
  clean-room-skill preflight --template --bootstrap ~/Documents/CleanRoom/task-xxxxxxxx
55
+ clean-room-skill preflight --template --bootstrap ~/Documents/CleanRoom/<project>/tasks/task-xxxxxxxx
55
56
  ```
56
57
 
57
58
  `--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`, writes to the generated contaminated artifact root after scaffold validation, and requires completed input contracts to match the bootstrap artifact and implementation roots.
@@ -11,11 +11,11 @@ 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
 
18
- Load these artifacts from the paths recorded in `task-manifest.json` and the configured root environment. Treat missing optional artifacts as blockers only when the current gate requires them.
18
+ Load these artifacts from the paths recorded in `task-manifest.json` and the configured root environment. Treat missing optional artifacts as blockers only when the current gate requires them. A task may live at `<base>/<project>/tasks/<task-id>/` with a shared project-level implementation root; trust the absolute paths recorded in `task-manifest.json` `artifact_paths` and never re-derive the layout from folder positions.
19
19
 
20
20
  - `task-manifest.json`
21
21
  - `preflight-goal.json`, when referenced by `task-manifest.json`, only on the contaminated/controller side
@@ -46,6 +46,7 @@ Before choosing work:
46
46
  - Confirm authorization still covers the recorded source scope and allowed actions.
47
47
  - Report `run_state` when present; do not infer generation from chat history when it is missing.
48
48
  - Trust `initialization_snapshot` before any reusable `init-config.json`. If they differ, report drift and stop before changing roots, model policy, schema profile, or rule classification.
49
+ - When `initialization_snapshot` records `project_id` or `project_root`, confirm the on-disk `clean-room-project.json` at the project root still agrees with them and with the shared implementation root. Report drift and stop on mismatch.
49
50
  - Preserve the existing `controller_policy`; missing policy means `attended`.
50
51
  - Stop if new-run artifacts lack `preflight_goal_ref`, `preflight_goal_sha256`, or the required `handoff_sequence`. Treat this as legacy or incomplete preflight state and ask for a reviewed preflight goal before resuming.
51
52
  - Validate referenced `preflight-goal.json` before using goal, stack, dependency, license, exactness, output, or hygiene decisions.
@@ -24,6 +24,7 @@ Archive or quarantine previous artifacts before creating new ones:
24
24
  - Do not mix contaminated and clean archives.
25
25
  - Do not delete artifacts.
26
26
  - Do not overwrite an existing archive path; create a unique archive directory.
27
+ - In the project layout, archive scope is the task folder only. Never archive, move, or quarantine the project root, `clean-room-project.json`, or the shared project-level `implementation/` root; sibling tasks depend on them, and implementation content is destination state, not per-task artifact state.
27
28
  - Preserve existing `preflight-goal.json`, `task-manifest.json`, ledgers, handoff packages, behavior specs, skeleton manifests, implementation plans, implementation reports, QC reports, incident records, and open delta tickets.
28
29
 
29
30
  If safe archive targets cannot be proven from `task-manifest.json`, root environment, or explicit user input, stop before moving anything.
@@ -37,6 +38,7 @@ Start from the preflight gate, not from prior QC:
37
38
  - Reconfirm source roots or visual roots, contaminated artifact roots, clean roots, implementation roots, and clean allowed-read roots are separated, and that root path names are not source-derived.
38
39
  - Preserve source or visual roots and authorization only when they are still valid for the requested restart.
39
40
  - Create a fresh neutral `task_id` by default. Use `task-` plus 8 lowercase hex characters unless the user provides an explicitly approved neutral ID. Do not derive the new ID or output directory names from source folder names.
41
+ - In the project layout, the new task joins the same project by default. Starting a new project instead requires an explicit user choice and a fresh neutral project name (random word pair or `proj-` plus 8 lowercase hex, never source-derived).
40
42
  - Record `run_state.generation`, `run_state.started_at`, optional `run_state.previous_generation_ref`, and `run_state.restart_reason`.
41
43
  - Recreate `clean-run-context.json` from the new effective preflight and initialization choices; do not carry forward an old clean context by default.
42
44
  - Rebuild `source-index.json`, or `visual-index.json` for visual fallback runs, unless the user explicitly says the source or visual scope is unchanged and a recorded old index hash can still be validated.
@@ -13,9 +13,9 @@ In Pi, this entry point is invoked as `/skill:unattended`.
13
13
 
14
14
  Use the canonical `clean-room` skill workflow and references in this plugin. Read `skills/clean-room/references/CONTROLLER-LOOP.md` before defining unattended loop behavior. Preserve the same clean-room boundary, role separation, artifact schemas, leakage rules, implementation-root rules, and hook expectations.
15
15
 
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.
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-*` (legacy) and `~/Documents/CleanRoom/*/tasks/task-*` (project layout) 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
 
@@ -25,6 +25,7 @@ Gather only required setup facts:
25
25
 
26
26
  - Authorization statement, requester, allowed actions, prohibited actions, and evidence handling.
27
27
  - Artifact base root, defaulting to `~/Documents/CleanRoom/<task-id>/`. If the user does not provide an explicitly approved neutral task ID, generate one as `task-` plus 8 lowercase hex characters. Do not derive task IDs or output directory names from source folder names.
28
+ - Optional project grouping for multi-task destinations, following the canonical `clean-room` project layout rules: `<base>/<project>/tasks/<task-id>/` with one shared `<base>/<project>/implementation/` root, a neutral project name (random word pair or `proj-` plus 8 lowercase hex, matching `[a-z0-9][a-z0-9-]{0,63}`, never source-derived), and at most one active task per project.
28
29
  - Source roots, contaminated artifact root, clean artifact root, clean implementation root, quarantine root, and optional public or destination reference roots.
29
30
  - Target stack, destination constraints, dependency/license policy, exactness policy, feature policy, code hygiene policy, and output policy from `preflight-goal.json`.
30
31
  - Target schema profile: `openspec-delta`, `gsd-planning-package`, `speckit-feature-folder`, or `kiro-spec-folder`.