clean-room-skill 0.2.1 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +16 -2
- package/agents/clean-architect.md +4 -0
- package/agents/clean-implementer-verifier-shell.md +4 -0
- package/agents/clean-polish-reviewer.md +4 -0
- package/agents/clean-qa-editor.md +4 -0
- package/agents/contaminated-handoff-sanitizer.md +4 -0
- package/agents/contaminated-manager-verifier.md +4 -0
- package/agents/contaminated-source-analyst.md +4 -0
- package/docs/REFERENCE.md +5 -0
- package/lib/run-cli.cjs +1 -1
- package/lib/run-controller.cjs +35 -1
- package/lib/run-hooks.cjs +44 -11
- package/lib/run-roots.cjs +56 -11
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/clean-room/SKILL.md +1 -1
- package/skills/init/SKILL.md +1 -1
- package/skills/preflight/SKILL.md +2 -2
- package/skills/refocus/SKILL.md +1 -1
- package/skills/resume-cr/SKILL.md +1 -1
- package/skills/unattended/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,19 @@ For the full boundary model, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). F
|
|
|
31
31
|
|
|
32
32
|
Requires Node.js `>=22`.
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
You can either install the CLI globally on your system, or run the commands on-demand using `npx`.
|
|
35
|
+
|
|
36
|
+
### Global Installation (npm)
|
|
37
|
+
|
|
38
|
+
To install the `clean-room-skill` executable globally:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install -g clean-room-skill
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Direct On-Demand Execution (npx)
|
|
45
|
+
|
|
46
|
+
Preferred interactive install/onboarding flow:
|
|
35
47
|
|
|
36
48
|
```bash
|
|
37
49
|
npx clean-room-skill@latest
|
|
@@ -130,12 +142,14 @@ For unattended inner-loop execution from durable artifacts:
|
|
|
130
142
|
```bash
|
|
131
143
|
npx clean-room-skill@latest run \
|
|
132
144
|
--task-manifest ~/Documents/CleanRoom/task-1234abcd/contaminated/task-manifest.json \
|
|
133
|
-
--agent-
|
|
145
|
+
--agent-runtime claude \
|
|
134
146
|
--max-iterations 3
|
|
135
147
|
```
|
|
136
148
|
|
|
137
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.
|
|
138
150
|
|
|
151
|
+
Use `--agent-commands ./agent-commands.json` only for a custom non-Claude role-session adapter.
|
|
152
|
+
|
|
139
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`.
|
|
140
154
|
|
|
141
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
|
`);
|
package/lib/run-controller.cjs
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
220
|
+
throw new Error(`preflight goal is not a file: ${preflightGoalPath}`);
|
|
177
221
|
}
|
|
178
222
|
const actual = fileHash(preflightGoalRealPath).toLowerCase();
|
|
179
|
-
const
|
|
180
|
-
if (actual !==
|
|
181
|
-
throw new Error(
|
|
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
package/plugin.json
CHANGED
|
@@ -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` 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
|
|
package/skills/init/SKILL.md
CHANGED
|
@@ -19,7 +19,7 @@ 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` 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 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`.
|
|
23
23
|
|
|
24
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.
|
|
25
25
|
|
|
@@ -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
|
|
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`.
|
|
15
15
|
|
|
16
16
|
## Required Contract
|
|
17
17
|
|
|
@@ -46,7 +46,7 @@ Do not infer target language, license, dependency policy, exactness policy, outp
|
|
|
46
46
|
|
|
47
47
|
## CLI Helper
|
|
48
48
|
|
|
49
|
-
Use the CLI only for template creation or validation/copying:
|
|
49
|
+
Use the CLI (`clean-room-skill` if installed, or `npx clean-room-skill@latest` as fallback) only for template creation or validation/copying:
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
clean-room-skill preflight --template --output ~/Documents/CleanRoom/task-xxxxxxxx/contaminated/preflight-goal.json
|
package/skills/refocus/SKILL.md
CHANGED
|
@@ -53,7 +53,7 @@ Emit missed-gate findings only:
|
|
|
53
53
|
- Stale implementation report compared with latest implementation plan.
|
|
54
54
|
- Controller policy not preserved.
|
|
55
55
|
- Missing, invalid, or drifted preflight goal.
|
|
56
|
-
- Noncanonical manifests, reports, ledgers, or manual result summaries used as completion evidence. Mark these `not verified` unless `clean-room-skill run --dry-run` succeeds against the canonical `task-manifest.json`.
|
|
56
|
+
- Noncanonical manifests, reports, ledgers, or manual result summaries used as completion evidence. Mark these `not verified` unless `clean-room-skill run --dry-run` (or `npx clean-room-skill@latest run --dry-run` if the binary is not available) succeeds against the canonical `task-manifest.json`.
|
|
57
57
|
- Missing public-surface inventory parity: required public commands, APIs, config keys, protocol entries, or user-visible behaviors listed in approved specs are not mapped through behavior spec tests, implementation-plan `public_contract_refs`, terminal implementation reports, and coverage-ledger `public_surface_coverage`.
|
|
58
58
|
|
|
59
59
|
Do not suggest speculative improvements. Do not change source scope, target profile, public API, or implementation plan.
|
|
@@ -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` 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`. 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
|
|