clean-room-skill 0.1.13 → 0.1.15
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 +4 -4
- package/docs/ARCHITECTURE.md +2 -2
- package/docs/HOOKS.md +1 -0
- package/docs/REFERENCE.md +6 -4
- package/hooks/agent3-verification-runner.py +2 -0
- package/hooks/agent4-polish-runner.py +2 -0
- package/lib/claude-agents.cjs +132 -0
- package/lib/doctor.cjs +25 -1
- package/lib/install-status.cjs +15 -2
- package/lib/run-claude-agent-runtime.cjs +79 -0
- package/lib/run-cli.cjs +27 -2
- package/lib/run-constants.cjs +3 -0
- package/lib/run-controller.cjs +131 -51
- package/lib/run-stages.cjs +9 -4
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/attended/SKILL.md +2 -2
- package/skills/clean-room/SKILL.md +4 -2
- package/skills/clean-room/references/PREFLIGHT.md +1 -1
- package/skills/clean-room/references/PROCESS.md +1 -1
- package/skills/clean-room/references/SPEC-SCHEMA.md +1 -1
- package/skills/{resume → resume-cr}/SKILL.md +3 -1
- package/skills/unattended/SKILL.md +4 -2
package/README.md
CHANGED
|
@@ -99,7 +99,7 @@ In Claude Code, invoke skills with the plugin namespace:
|
|
|
99
99
|
/clean-room
|
|
100
100
|
/clean-room:attended
|
|
101
101
|
/clean-room:unattended
|
|
102
|
-
/clean-room:resume
|
|
102
|
+
/clean-room:resume-cr
|
|
103
103
|
/clean-room:start-over
|
|
104
104
|
/clean-room:refocus
|
|
105
105
|
```
|
|
@@ -114,7 +114,7 @@ In Pi, invoke package skills with `/skill:<name>`:
|
|
|
114
114
|
/skill:clean-room
|
|
115
115
|
/skill:attended
|
|
116
116
|
/skill:unattended
|
|
117
|
-
/skill:resume
|
|
117
|
+
/skill:resume-cr
|
|
118
118
|
/skill:start-over
|
|
119
119
|
/skill:refocus
|
|
120
120
|
```
|
|
@@ -156,7 +156,7 @@ In strict context-management mode, every `agent-commands.json` stage must set `c
|
|
|
156
156
|
|
|
157
157
|
Use recovery skills instead of chat history:
|
|
158
158
|
|
|
159
|
-
- `/clean-room:resume`: continue from durable artifacts.
|
|
159
|
+
- `/clean-room:resume-cr`: continue from durable artifacts.
|
|
160
160
|
- `/clean-room:start-over`: archive or quarantine current artifacts without deletion, then restart with a fresh neutral task id.
|
|
161
161
|
- `/clean-room:refocus`: audit current artifacts against declared scope without expanding scope.
|
|
162
162
|
|
|
@@ -190,7 +190,7 @@ Use recovery skills instead of chat history:
|
|
|
190
190
|
|
|
191
191
|
| Skill | Use it for |
|
|
192
192
|
| --- | --- |
|
|
193
|
-
| `/clean-room:resume` | Continue an existing run from durable artifacts. |
|
|
193
|
+
| `/clean-room:resume-cr` | Continue an existing run from durable artifacts. |
|
|
194
194
|
| `/clean-room:start-over` | Non-destructively archive or quarantine current artifacts and restart. |
|
|
195
195
|
| `/clean-room:refocus` | Audit a run and route it back to missed gates without adding scope. |
|
|
196
196
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -250,14 +250,14 @@ The outer loop owns spec development: scope, behavior specs, acceptance criteria
|
|
|
250
250
|
|
|
251
251
|
Agent 3's terminal report is not enough to return. If configured, Agent 4 must produce a passing `polish-report.json`. Agent 0 must then consume the terminal clean reports, verify contaminated-side coverage, and write `clean-room-result.json`.
|
|
252
252
|
|
|
253
|
-
`clean-room-skill run` is the executable v1 inner-loop runner. It requires preflight refs, the required handoff sequence, unattended `controller_policy`, schema-valid `loop_context`, and a user-supplied agent command adapter. It does not automate outer spec development. The runner:
|
|
253
|
+
`clean-room-skill run` is the executable v1 inner-loop runner. It requires preflight refs, the required handoff sequence, unattended `controller_policy`, schema-valid `loop_context`, and either a user-supplied agent command adapter or the built-in Claude Code agent runtime. It does not automate outer spec development. The runner:
|
|
254
254
|
|
|
255
255
|
* Locks the contaminated artifact root with `.clean-room-run.lock`.
|
|
256
256
|
* Reloads durable artifacts before each iteration.
|
|
257
257
|
* Selects at most one pending or gap unit inside `loop_context.approved_scope_refs`.
|
|
258
258
|
* Requires exactly one `unit_kind: "foundation"` unit, named by `loop_context.foundation_unit_ref`; behavior units cannot run or complete until that foundation unit is covered.
|
|
259
259
|
* Spawns configured role commands with `shell: false`, bounded output, and bounded timeout.
|
|
260
|
-
* In strict context-management mode, requires each configured stage to provide `context.fresh_session: true` and `context.brief_path`, then validates the session brief before spawn.
|
|
260
|
+
* In strict context-management mode, requires each configured worker stage after `contaminated-manager-prepare` to provide `context.fresh_session: true` and `context.brief_path`, then validates the session brief before spawn.
|
|
261
261
|
* Supports the optional `clean-polish-review` phase between `clean-implement-qc` and `contaminated-coverage-verify`.
|
|
262
262
|
* Validates schema, leakage, and handoff integrity before advancing state.
|
|
263
263
|
* Rejects `covered` coverage-ledger units that still have unresolved high-priority `discovery_leads`.
|
package/docs/HOOKS.md
CHANGED
|
@@ -68,6 +68,7 @@ Role sessions must provide the full clean-room environment block so the hooks ca
|
|
|
68
68
|
| `CLEAN_ROOM_PRIVATE_IDENTIFIER_DENYLIST` | Optional path-separated denylist files for leakage scanning. |
|
|
69
69
|
| `CLEAN_ROOM_AUXILIARY_JSON_ALLOWLIST` | Optional path-separated allowlist for unrecognized auxiliary JSON files under clean roots. |
|
|
70
70
|
| `CLEAN_ROOM_ALLOW_AGENT3_SHELL` | Must be `1` before Agent 3 can invoke the verification runner through a shell-style tool. |
|
|
71
|
+
| `CLEAN_ROOM_ALLOW_AGENT4_SHELL` | Must be `1` before Agent 4 can invoke the polish runner through a shell-style tool. |
|
|
71
72
|
| `CLEAN_ROOM_HOOK_ENFORCE` | Forces enforcement in `safe` mode when truthy. |
|
|
72
73
|
| `CLEAN_ROOM_HOOK_CHECK_TIMEOUT_SECONDS` | Optional per-check wrapper timeout. Defaults to 10 seconds. |
|
|
73
74
|
|
package/docs/REFERENCE.md
CHANGED
|
@@ -214,7 +214,7 @@ Usage:
|
|
|
214
214
|
```bash
|
|
215
215
|
npx clean-room-skill@latest run \
|
|
216
216
|
--task-manifest ~/Documents/CleanRoom/task-1234abcd/contaminated/task-manifest.json \
|
|
217
|
-
--agent-
|
|
217
|
+
--agent-runtime claude \
|
|
218
218
|
--max-iterations 3
|
|
219
219
|
```
|
|
220
220
|
|
|
@@ -223,7 +223,9 @@ Options:
|
|
|
223
223
|
| Option | Description |
|
|
224
224
|
| --- | --- |
|
|
225
225
|
| `--task-manifest <path>` | Required path to `task-manifest.json`. |
|
|
226
|
-
| `--agent-commands <path>` |
|
|
226
|
+
| `--agent-commands <path>` | Role command adapter JSON unless `--agent-runtime` or `--dry-run` is set. |
|
|
227
|
+
| `--agent-runtime claude` | Use the built-in Claude Code adapter to launch plugin role agents. Mutually exclusive with `--agent-commands`. |
|
|
228
|
+
| `--agent-config-dir <path>` | Claude config directory for `--agent-runtime claude`; defaults to `CLAUDE_CONFIG_DIR` or `~/.claude`. |
|
|
227
229
|
| `--max-iterations <n>` | May only lower the manifest and `loop_context` cap. |
|
|
228
230
|
| `--once` | Run at most one inner-loop iteration. |
|
|
229
231
|
| `--dry-run` | Validate and print the selected unit without writing or spawning agents. |
|
|
@@ -259,9 +261,9 @@ Minimal agent command adapter shape for advisory or disabled context management:
|
|
|
259
261
|
}
|
|
260
262
|
```
|
|
261
263
|
|
|
262
|
-
Supported phases are `contaminated-analysis`, `sanitize-handoff`, `clean-plan`, `clean-implement-qc`, optional `clean-polish-review`, and `contaminated-coverage-verify`. The coverage verification phase is required. When present, `clean-polish-review` must run after `clean-implement-qc` and before `contaminated-coverage-verify`.
|
|
264
|
+
Supported phases are `contaminated-manager-prepare`, `contaminated-analysis`, `sanitize-handoff`, `clean-plan`, `clean-implement-qc`, optional `clean-polish-review`, and `contaminated-coverage-verify`. The coverage verification phase is required. The built-in Claude adapter includes `contaminated-manager-prepare` so Agent 0 can prepare controller state before downstream role agents run. When present, `clean-polish-review` must run after `clean-implement-qc` and before `contaminated-coverage-verify`.
|
|
263
265
|
|
|
264
|
-
When `task-manifest.json` sets `context_management.mode` to `role-session-briefs` and `context_management.enforcement` to `strict`, every configured stage must include `context.fresh_session: true` and `context.brief_path`. The runner validates the brief before spawn, passes only the brief path plus environment facts in the stage prompt, and records the brief ref/hash in `controller-run-ledger.json`.
|
|
266
|
+
When `task-manifest.json` sets `context_management.mode` to `role-session-briefs` and `context_management.enforcement` to `strict`, every configured worker stage after `contaminated-manager-prepare` must include `context.fresh_session: true` and `context.brief_path`. The runner validates the brief before spawn, passes only the brief path plus environment facts in the stage prompt, and records the brief ref/hash in `controller-run-ledger.json`.
|
|
265
267
|
|
|
266
268
|
Strict context-management adapter example:
|
|
267
269
|
|
|
@@ -138,6 +138,8 @@ def run_command(argv: list[str], cwd: Path, timeout: int, blocked_roots: list[Pa
|
|
|
138
138
|
cwd=cwd,
|
|
139
139
|
env=safe_env(blocked_roots),
|
|
140
140
|
text=True,
|
|
141
|
+
encoding="utf-8",
|
|
142
|
+
errors="replace",
|
|
141
143
|
stdout=subprocess.PIPE,
|
|
142
144
|
stderr=subprocess.PIPE,
|
|
143
145
|
shell=False,
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const { readJsonFile } = require('./fs-utils.cjs');
|
|
8
|
+
|
|
9
|
+
const CLAUDE_AGENT_FILES = Object.freeze([
|
|
10
|
+
'clean-architect.md',
|
|
11
|
+
'clean-implementer-verifier-shell.md',
|
|
12
|
+
'clean-polish-reviewer.md',
|
|
13
|
+
'clean-qa-editor.md',
|
|
14
|
+
'contaminated-handoff-sanitizer.md',
|
|
15
|
+
'contaminated-manager-verifier.md',
|
|
16
|
+
'contaminated-source-analyst.md',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function packageRoot() {
|
|
20
|
+
return path.resolve(__dirname, '..');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function localClaudePluginDir() {
|
|
24
|
+
return packageRoot();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function defaultClaudeConfigDir(env = process.env) {
|
|
28
|
+
if (env.CLAUDE_CONFIG_DIR) {
|
|
29
|
+
return path.resolve(expandTilde(env.CLAUDE_CONFIG_DIR));
|
|
30
|
+
}
|
|
31
|
+
return path.join(os.homedir(), '.claude');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function expandTilde(value) {
|
|
35
|
+
if (value === '~') return os.homedir();
|
|
36
|
+
if (typeof value === 'string' && value.startsWith('~/')) {
|
|
37
|
+
return path.join(os.homedir(), value.slice(2));
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function claudePluginDirFromInstallManifest(configDir) {
|
|
43
|
+
const manifestPath = path.join(configDir, 'clean-room-install-manifest.json');
|
|
44
|
+
if (!fs.existsSync(manifestPath)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const manifest = readJsonFile(manifestPath, null);
|
|
48
|
+
const installPath = manifest?.claude_plugin?.install_path;
|
|
49
|
+
return typeof installPath === 'string' && installPath !== '' ? path.resolve(installPath) : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function claudePluginCandidates(configDir, options = {}) {
|
|
53
|
+
const candidates = [];
|
|
54
|
+
const add = (label, pluginDir) => {
|
|
55
|
+
if (typeof pluginDir !== 'string' || pluginDir === '') return;
|
|
56
|
+
const resolved = path.resolve(pluginDir);
|
|
57
|
+
if (candidates.some((candidate) => candidate.pluginDir === resolved)) return;
|
|
58
|
+
candidates.push({ label, pluginDir: resolved });
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (options.pluginDir) {
|
|
62
|
+
add('explicit', options.pluginDir);
|
|
63
|
+
}
|
|
64
|
+
if (configDir) {
|
|
65
|
+
add('installed-plugin', claudePluginDirFromInstallManifest(path.resolve(configDir)));
|
|
66
|
+
}
|
|
67
|
+
if (options.includePackageFallback !== false) {
|
|
68
|
+
add('package-plugin', localClaudePluginDir());
|
|
69
|
+
}
|
|
70
|
+
if (configDir) {
|
|
71
|
+
add('local-claude-agents', path.resolve(configDir));
|
|
72
|
+
}
|
|
73
|
+
return candidates;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function claudeAgentStatus(configDir, options = {}) {
|
|
77
|
+
const candidates = claudePluginCandidates(configDir, options);
|
|
78
|
+
for (const candidate of candidates) {
|
|
79
|
+
const agentDir = path.join(candidate.pluginDir, 'agents');
|
|
80
|
+
const missing = missingClaudeAgentFiles(candidate.pluginDir);
|
|
81
|
+
if (missing.length === 0) {
|
|
82
|
+
return {
|
|
83
|
+
status: 'ok',
|
|
84
|
+
source: candidate.label,
|
|
85
|
+
pluginDir: candidate.pluginDir,
|
|
86
|
+
agentDir,
|
|
87
|
+
present: CLAUDE_AGENT_FILES.length,
|
|
88
|
+
missing,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const preferred = candidates[0] || { label: 'none', pluginDir: configDir ? path.resolve(configDir) : null };
|
|
94
|
+
const missing = preferred.pluginDir ? missingClaudeAgentFiles(preferred.pluginDir) : [...CLAUDE_AGENT_FILES];
|
|
95
|
+
return {
|
|
96
|
+
status: 'missing',
|
|
97
|
+
source: preferred.label,
|
|
98
|
+
pluginDir: preferred.pluginDir,
|
|
99
|
+
agentDir: preferred.pluginDir ? path.join(preferred.pluginDir, 'agents') : null,
|
|
100
|
+
present: CLAUDE_AGENT_FILES.length - missing.length,
|
|
101
|
+
missing,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function missingClaudeAgentFiles(pluginDir) {
|
|
106
|
+
const agentDir = path.join(pluginDir, 'agents');
|
|
107
|
+
return CLAUDE_AGENT_FILES.filter((name) => {
|
|
108
|
+
const filePath = path.join(agentDir, name);
|
|
109
|
+
try {
|
|
110
|
+
return !fs.statSync(filePath).isFile();
|
|
111
|
+
} catch {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function assertClaudeAgentsAvailable(configDir, options = {}) {
|
|
118
|
+
const status = claudeAgentStatus(configDir, options);
|
|
119
|
+
if (status.status !== 'ok') {
|
|
120
|
+
const base = status.pluginDir || String(configDir || '<unknown>');
|
|
121
|
+
throw new Error(`Claude role-agent dispatch unavailable: missing ${status.missing.join(', ')} under ${base}`);
|
|
122
|
+
}
|
|
123
|
+
return status;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
CLAUDE_AGENT_FILES,
|
|
128
|
+
assertClaudeAgentsAvailable,
|
|
129
|
+
claudeAgentStatus,
|
|
130
|
+
defaultClaudeConfigDir,
|
|
131
|
+
localClaudePluginDir,
|
|
132
|
+
};
|
package/lib/doctor.cjs
CHANGED
|
@@ -6,6 +6,7 @@ const path = require('node:path');
|
|
|
6
6
|
const { spawnSync } = require('node:child_process');
|
|
7
7
|
|
|
8
8
|
const { readJsonFile } = require('./fs-utils.cjs');
|
|
9
|
+
const { claudeAgentStatus } = require('./claude-agents.cjs');
|
|
9
10
|
const {
|
|
10
11
|
CLEAN_ROOM_HOOKS,
|
|
11
12
|
configPathForRuntime,
|
|
@@ -397,6 +398,22 @@ function printOpenCodeCoverage(plugin, hookMode) {
|
|
|
397
398
|
console.log(` strict required: ${hookMode === 'strict' ? 'yes' : 'no'}`);
|
|
398
399
|
}
|
|
399
400
|
|
|
401
|
+
function assertClaudeAgentAvailability(layout) {
|
|
402
|
+
const status = claudeAgentStatus(layout.targetRoot, { includePackageFallback: false });
|
|
403
|
+
if (status.status !== 'ok') {
|
|
404
|
+
const base = status.pluginDir || layout.targetRoot;
|
|
405
|
+
throw new Error(`Claude role-agent dispatch unavailable: missing ${status.missing.join(', ')} under ${base}`);
|
|
406
|
+
}
|
|
407
|
+
return status;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function printClaudeAgentCoverage(status) {
|
|
411
|
+
console.log('clean-room Claude plugin agent coverage:');
|
|
412
|
+
console.log(` ok agents ${status.present}`);
|
|
413
|
+
console.log(` source: ${status.source}`);
|
|
414
|
+
console.log(` path: ${status.agentDir}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
400
417
|
function runOpenCodeDoctor(options, layout) {
|
|
401
418
|
const plugin = assertOpenCodePlugin(layout, options.hookMode);
|
|
402
419
|
const pathEnv = { PATH: process.env.PATH || '' };
|
|
@@ -497,6 +514,10 @@ function runDoctor(argv) {
|
|
|
497
514
|
if (options.coverage) {
|
|
498
515
|
printCoverage(entries, options.hookMode);
|
|
499
516
|
}
|
|
517
|
+
const claudeAgents = layout.runtime === 'claude' ? assertClaudeAgentAvailability(layout) : null;
|
|
518
|
+
if (options.coverage && claudeAgents) {
|
|
519
|
+
printClaudeAgentCoverage(claudeAgents);
|
|
520
|
+
}
|
|
500
521
|
if (options.hookMode === 'strict') {
|
|
501
522
|
assertStrictCoverage(entries);
|
|
502
523
|
}
|
|
@@ -556,8 +577,11 @@ function runDoctor(argv) {
|
|
|
556
577
|
console.log(`clean-room doctor passed for ${options.runtime}`);
|
|
557
578
|
console.log(` hooks config: ${configPath}`);
|
|
558
579
|
console.log(` managed hooks: ${entries.length}`);
|
|
580
|
+
if (claudeAgents) {
|
|
581
|
+
console.log(` plugin agents: ${claudeAgents.present}`);
|
|
582
|
+
}
|
|
559
583
|
console.log(` mode: ${options.hookMode}`);
|
|
560
|
-
return { configPath, managedHooks: entries.length };
|
|
584
|
+
return { configPath, managedHooks: entries.length, pluginAgents: claudeAgents?.present || 0 };
|
|
561
585
|
}
|
|
562
586
|
|
|
563
587
|
module.exports = {
|
package/lib/install-status.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
|
|
5
|
+
const { claudeAgentStatus } = require('./claude-agents.cjs');
|
|
5
6
|
const { assertManagedPath, fileHash } = require('./fs-utils.cjs');
|
|
6
7
|
const {
|
|
7
8
|
configPathForRuntime,
|
|
@@ -98,6 +99,7 @@ function collectRuntimeStatus(runtime, scope, configDir) {
|
|
|
98
99
|
hookRegistration: layout.supportsHookRegistration ? 'none' : 'unsupported',
|
|
99
100
|
updateAvailable: false,
|
|
100
101
|
claudePlugin: null,
|
|
102
|
+
claudeAgents: null,
|
|
101
103
|
issues: [],
|
|
102
104
|
};
|
|
103
105
|
|
|
@@ -167,11 +169,18 @@ function collectRuntimeStatus(runtime, scope, configDir) {
|
|
|
167
169
|
if (layout.supportsHookRegistration && hooksMode !== 'copy-only' && hookState !== 'present') {
|
|
168
170
|
issues.push('managed hook registration missing');
|
|
169
171
|
}
|
|
172
|
+
const claudeAgents = runtime === 'claude'
|
|
173
|
+
? claudeAgentStatus(layout.targetRoot, { includePackageFallback: false })
|
|
174
|
+
: null;
|
|
175
|
+
if (claudeAgents && claudeAgents.status !== 'ok') {
|
|
176
|
+
issues.push(`Claude role-agent dispatch unavailable: missing ${claudeAgents.missing.join(', ')}`);
|
|
177
|
+
}
|
|
170
178
|
|
|
171
|
-
const updateAvailable = manifest.version !== packageVersion() ||
|
|
179
|
+
const updateAvailable = Boolean(manifest.version !== packageVersion() ||
|
|
172
180
|
plan.removals.length > 0 ||
|
|
173
181
|
plan.unknownConflicts.length > 0 ||
|
|
174
|
-
fileStats.missing > 0
|
|
182
|
+
fileStats.missing > 0 ||
|
|
183
|
+
(claudeAgents && claudeAgents.status !== 'ok'));
|
|
175
184
|
|
|
176
185
|
return {
|
|
177
186
|
...base,
|
|
@@ -188,6 +197,7 @@ function collectRuntimeStatus(runtime, scope, configDir) {
|
|
|
188
197
|
hookRegistration: hookState,
|
|
189
198
|
updateAvailable,
|
|
190
199
|
claudePlugin: manifest.claude_plugin || null,
|
|
200
|
+
claudeAgents,
|
|
191
201
|
issues,
|
|
192
202
|
};
|
|
193
203
|
}
|
|
@@ -243,6 +253,9 @@ function printStatusReport(statuses) {
|
|
|
243
253
|
if (status.claudePlugin) {
|
|
244
254
|
console.log(` plugin: ${status.claudePlugin.plugin_id || CLAUDE_PLUGIN_ID}; marketplace ${status.claudePlugin.marketplace_name || CLAUDE_PLUGIN_MARKETPLACE_NAME}`);
|
|
245
255
|
}
|
|
256
|
+
if (status.claudeAgents) {
|
|
257
|
+
console.log(` plugin agents: ${status.claudeAgents.status}; present ${status.claudeAgents.present}; missing ${status.claudeAgents.missing.length}`);
|
|
258
|
+
}
|
|
246
259
|
} else if (status.hookRegistration === 'present') {
|
|
247
260
|
console.log(' hooks: managed hook registration present without install manifest');
|
|
248
261
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const { assertClaudeAgentsAvailable, defaultClaudeConfigDir } = require('./claude-agents.cjs');
|
|
6
|
+
const { resolveClaudeExecutable } = require('./install-claude-plugin.cjs');
|
|
7
|
+
const {
|
|
8
|
+
MANAGER_PREPARE_PHASE,
|
|
9
|
+
REQUIRED_COVERAGE_PHASE,
|
|
10
|
+
ROLE_BY_PHASE,
|
|
11
|
+
} = require('./run-constants.cjs');
|
|
12
|
+
const { resolvePath } = require('./run-roots.cjs');
|
|
13
|
+
|
|
14
|
+
const CLAUDE_PERMISSION_MODE = 'acceptEdits';
|
|
15
|
+
|
|
16
|
+
function buildClaudeAgentCommandConfig(options, roots, cwd = process.cwd()) {
|
|
17
|
+
const agentConfigDir = options.agentConfigDir
|
|
18
|
+
? resolvePath(options.agentConfigDir, cwd)
|
|
19
|
+
: defaultClaudeConfigDir();
|
|
20
|
+
const agentStatus = assertClaudeAgentsAvailable(agentConfigDir);
|
|
21
|
+
const { executable, searchPath } = resolveClaudeExecutable();
|
|
22
|
+
const env = {
|
|
23
|
+
CLAUDE_CONFIG_DIR: agentConfigDir,
|
|
24
|
+
PATH: searchPath,
|
|
25
|
+
};
|
|
26
|
+
const pluginArgs = agentStatus.source === 'installed-plugin' || agentStatus.source === 'package-plugin'
|
|
27
|
+
? ['--plugin-dir', agentStatus.pluginDir]
|
|
28
|
+
: [];
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
configDir: agentConfigDir,
|
|
32
|
+
config: {
|
|
33
|
+
version: 1,
|
|
34
|
+
stages: claudeStages(roots, executable, env, pluginArgs),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function claudeStages(roots, executable, env, pluginArgs) {
|
|
40
|
+
const contaminatedCwd = roots.contaminatedRoot;
|
|
41
|
+
const cleanCwd = roots.cleanRoot;
|
|
42
|
+
const implementationCwd = roots.implementationRoots[0] || roots.cleanRoot;
|
|
43
|
+
return [
|
|
44
|
+
claudeStage(MANAGER_PREPARE_PHASE, contaminatedCwd, executable, env, pluginArgs),
|
|
45
|
+
claudeStage('contaminated-analysis', contaminatedCwd, executable, env, pluginArgs),
|
|
46
|
+
claudeStage('sanitize-handoff', contaminatedCwd, executable, env, pluginArgs),
|
|
47
|
+
claudeStage('clean-plan', cleanCwd, executable, env, pluginArgs),
|
|
48
|
+
claudeStage('clean-implement-qc', implementationCwd, executable, env, pluginArgs),
|
|
49
|
+
claudeStage(REQUIRED_COVERAGE_PHASE, contaminatedCwd, executable, env, pluginArgs),
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function claudeStage(phase, cwd, executable, env, pluginArgs) {
|
|
54
|
+
const role = ROLE_BY_PHASE[phase];
|
|
55
|
+
return {
|
|
56
|
+
phase,
|
|
57
|
+
role,
|
|
58
|
+
cwd,
|
|
59
|
+
argv: [
|
|
60
|
+
executable,
|
|
61
|
+
'--print',
|
|
62
|
+
'--input-format',
|
|
63
|
+
'text',
|
|
64
|
+
'--output-format',
|
|
65
|
+
'text',
|
|
66
|
+
'--no-session-persistence',
|
|
67
|
+
'--permission-mode',
|
|
68
|
+
CLAUDE_PERMISSION_MODE,
|
|
69
|
+
'--agent',
|
|
70
|
+
`clean-room:${role}`,
|
|
71
|
+
...pluginArgs,
|
|
72
|
+
],
|
|
73
|
+
env,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
buildClaudeAgentCommandConfig,
|
|
79
|
+
};
|
package/lib/run-cli.cjs
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const AGENT_RUNTIMES = new Set(['claude']);
|
|
4
|
+
|
|
3
5
|
function printRunHelp() {
|
|
4
|
-
console.log(`Usage: clean-room-skill run --task-manifest <path> --agent-commands <path> [options]
|
|
6
|
+
console.log(`Usage: clean-room-skill run --task-manifest <path> (--agent-commands <path> | --agent-runtime claude) [options]
|
|
5
7
|
|
|
6
8
|
Run one bounded inner clean-room controller loop for an approved spec slice.
|
|
7
9
|
|
|
8
10
|
Options:
|
|
9
11
|
--task-manifest <path> Required task-manifest.json path
|
|
10
|
-
--agent-commands <path>
|
|
12
|
+
--agent-commands <path> Role command adapter JSON unless --agent-runtime or --dry-run is set
|
|
13
|
+
--agent-runtime <name> Built-in role agent runtime; currently supports claude
|
|
14
|
+
--agent-config-dir <path>
|
|
15
|
+
Runtime config dir for --agent-runtime claude
|
|
11
16
|
--max-iterations <n> Lower the manifest/loop iteration cap
|
|
12
17
|
--once Run at most one inner iteration
|
|
13
18
|
--dry-run Validate and print the selected unit without writing or spawning agents
|
|
@@ -21,6 +26,8 @@ function parseRunArgs(argv) {
|
|
|
21
26
|
const options = {
|
|
22
27
|
taskManifest: null,
|
|
23
28
|
agentCommands: null,
|
|
29
|
+
agentRuntime: null,
|
|
30
|
+
agentConfigDir: null,
|
|
24
31
|
maxIterations: null,
|
|
25
32
|
once: false,
|
|
26
33
|
dryRun: false,
|
|
@@ -47,6 +54,16 @@ function parseRunArgs(argv) {
|
|
|
47
54
|
options.agentCommands = requiredValue(argv, index, '--agent-commands');
|
|
48
55
|
} else if (arg.startsWith('--agent-commands=')) {
|
|
49
56
|
options.agentCommands = arg.slice('--agent-commands='.length);
|
|
57
|
+
} else if (arg === '--agent-runtime') {
|
|
58
|
+
index += 1;
|
|
59
|
+
options.agentRuntime = parseAgentRuntime(requiredValue(argv, index, '--agent-runtime'));
|
|
60
|
+
} else if (arg.startsWith('--agent-runtime=')) {
|
|
61
|
+
options.agentRuntime = parseAgentRuntime(arg.slice('--agent-runtime='.length));
|
|
62
|
+
} else if (arg === '--agent-config-dir') {
|
|
63
|
+
index += 1;
|
|
64
|
+
options.agentConfigDir = requiredValue(argv, index, '--agent-config-dir');
|
|
65
|
+
} else if (arg.startsWith('--agent-config-dir=')) {
|
|
66
|
+
options.agentConfigDir = arg.slice('--agent-config-dir='.length);
|
|
50
67
|
} else if (arg === '--max-iterations') {
|
|
51
68
|
index += 1;
|
|
52
69
|
options.maxIterations = parsePositiveInteger(requiredValue(argv, index, '--max-iterations'), '--max-iterations');
|
|
@@ -70,6 +87,13 @@ function parseRunArgs(argv) {
|
|
|
70
87
|
return options;
|
|
71
88
|
}
|
|
72
89
|
|
|
90
|
+
function parseAgentRuntime(value) {
|
|
91
|
+
if (!AGENT_RUNTIMES.has(value)) {
|
|
92
|
+
throw new Error('--agent-runtime must be claude');
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
|
|
73
97
|
function requiredValue(argv, index, flag) {
|
|
74
98
|
if (index >= argv.length || argv[index] === '') {
|
|
75
99
|
throw new Error(`${flag} requires a value`);
|
|
@@ -85,6 +109,7 @@ function parsePositiveInteger(value, flag) {
|
|
|
85
109
|
}
|
|
86
110
|
|
|
87
111
|
module.exports = {
|
|
112
|
+
AGENT_RUNTIMES,
|
|
88
113
|
parseRunArgs,
|
|
89
114
|
printRunHelp,
|
|
90
115
|
};
|
package/lib/run-constants.cjs
CHANGED
|
@@ -13,6 +13,7 @@ const STATUS_NAME = 'controller-status.json';
|
|
|
13
13
|
const CLEAN_RUN_CONTEXT_NAME = 'clean-run-context.json';
|
|
14
14
|
const HANDOFF_PACKAGE_NAME = 'handoff-package.json';
|
|
15
15
|
const POLISH_REPORT_NAME = 'polish-report.json';
|
|
16
|
+
const MANAGER_PREPARE_PHASE = 'contaminated-manager-prepare';
|
|
16
17
|
const REQUIRED_COVERAGE_PHASE = 'contaminated-coverage-verify';
|
|
17
18
|
const POLISH_PHASE = 'clean-polish-review';
|
|
18
19
|
const PUBLIC_SURFACE_COMPLETION_LEVELS = new Set(['exact-public-contract', 'behavior-compatible']);
|
|
@@ -65,6 +66,7 @@ const HOOK_ONLY_ENV_ALLOWLIST = Object.freeze([
|
|
|
65
66
|
]);
|
|
66
67
|
|
|
67
68
|
const ROLE_BY_PHASE = Object.freeze({
|
|
69
|
+
[MANAGER_PREPARE_PHASE]: 'contaminated-manager-verifier',
|
|
68
70
|
'contaminated-analysis': 'contaminated-source-analyst',
|
|
69
71
|
'sanitize-handoff': 'contaminated-handoff-sanitizer',
|
|
70
72
|
'clean-plan': 'clean-architect',
|
|
@@ -153,6 +155,7 @@ module.exports = {
|
|
|
153
155
|
MAX_LEDGER_ITERATIONS,
|
|
154
156
|
MAX_OUTPUT_BYTES,
|
|
155
157
|
MAX_TIMEOUT_MS,
|
|
158
|
+
MANAGER_PREPARE_PHASE,
|
|
156
159
|
POLISH_PHASE,
|
|
157
160
|
POLISH_REPORT_NAME,
|
|
158
161
|
PUBLIC_SURFACE_COMPLETION_LEVELS,
|
package/lib/run-controller.cjs
CHANGED
|
@@ -24,6 +24,7 @@ const {
|
|
|
24
24
|
validateArtifacts,
|
|
25
25
|
validateTaskManifestSchema,
|
|
26
26
|
} = require('./run-hooks.cjs');
|
|
27
|
+
const { buildClaudeAgentCommandConfig } = require('./run-claude-agent-runtime.cjs');
|
|
27
28
|
const {
|
|
28
29
|
effectiveIterationCap,
|
|
29
30
|
validateTaskManifestForRun,
|
|
@@ -70,6 +71,70 @@ function repeatedUnitSelection(previous, selectedUnit) {
|
|
|
70
71
|
return previous?.unit_id === selectedUnit.unit_id && previous?.stop_reason === 'no-progress-detected';
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
function validateRunState(options, taskManifestPath, roots, manifest, coverageLedgerPath) {
|
|
75
|
+
validateImplementationArtifactPlacement(roots);
|
|
76
|
+
validateArtifacts(options.python, taskManifestPath, roots);
|
|
77
|
+
validateCleanRunContextReferences(options.python, roots);
|
|
78
|
+
const coverageLedger = readOptionalJson(coverageLedgerPath);
|
|
79
|
+
validateCoverageLedgerIntegrity(manifest, roots, coverageLedger);
|
|
80
|
+
validateFoundationCoverageGate(manifest, coverageLedger);
|
|
81
|
+
return coverageLedger;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function rootListEqual(left, right) {
|
|
85
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function assertStableRunRoots(initialRoots, currentRoots) {
|
|
89
|
+
if (
|
|
90
|
+
!rootListEqual(initialRoots.sourceRoots, currentRoots.sourceRoots) ||
|
|
91
|
+
initialRoots.contaminatedRoot !== currentRoots.contaminatedRoot ||
|
|
92
|
+
initialRoots.cleanRoot !== currentRoots.cleanRoot ||
|
|
93
|
+
!rootListEqual(initialRoots.implementationRoots, currentRoots.implementationRoots) ||
|
|
94
|
+
!rootListEqual(initialRoots.allowedReadRoots, currentRoots.allowedReadRoots) ||
|
|
95
|
+
initialRoots.schemaDir !== currentRoots.schemaDir
|
|
96
|
+
) {
|
|
97
|
+
throw new Error('task manifest root drift detected during unattended run');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function reloadManifestForIteration(options, taskManifestPath, manifestDir, roots, schemaDir) {
|
|
102
|
+
validateTaskManifestSchema(options.python, taskManifestPath, schemaDir);
|
|
103
|
+
const currentManifest = readJsonFile(taskManifestPath, null);
|
|
104
|
+
validateTaskManifestForRun(currentManifest);
|
|
105
|
+
const currentRoots = resolveRoots(currentManifest, manifestDir, schemaDir);
|
|
106
|
+
assertStableRunRoots(roots, currentRoots);
|
|
107
|
+
validateTaskManifestLocation(taskManifestPath, currentRoots);
|
|
108
|
+
verifyPreflightGoal(currentManifest, manifestDir, currentRoots);
|
|
109
|
+
return currentManifest;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resolveAgentConfig(options, context, roots, manifest, agentConfigPath) {
|
|
113
|
+
if (options.agentCommands && options.agentRuntime) {
|
|
114
|
+
throw new Error('--agent-runtime cannot be used with --agent-commands');
|
|
115
|
+
}
|
|
116
|
+
if (!options.agentCommands && !options.agentRuntime) {
|
|
117
|
+
return { agentConfig: null, configDir: process.cwd() };
|
|
118
|
+
}
|
|
119
|
+
if (options.agentRuntime === 'claude') {
|
|
120
|
+
const builtIn = buildClaudeAgentCommandConfig(options, roots, context.cwd || process.cwd());
|
|
121
|
+
validateCommandConfig(builtIn.config, {
|
|
122
|
+
roots,
|
|
123
|
+
configDir: builtIn.configDir,
|
|
124
|
+
contextManagement: manifest.context_management,
|
|
125
|
+
});
|
|
126
|
+
return { agentConfig: builtIn.config, configDir: builtIn.configDir };
|
|
127
|
+
}
|
|
128
|
+
const agentConfig = readJsonFile(agentConfigPath, null);
|
|
129
|
+
const configDir = path.dirname(agentConfigPath);
|
|
130
|
+
validateCommandConfig(agentConfig, { roots, configDir, contextManagement: manifest.context_management });
|
|
131
|
+
return { agentConfig, configDir };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function shouldContinueAfterUnitComplete(manifest, coverageLedger) {
|
|
135
|
+
return Boolean(selectUnit(manifest, coverageLedger));
|
|
136
|
+
}
|
|
137
|
+
|
|
73
138
|
async function runCleanRoom(options, context = {}) {
|
|
74
139
|
if (options.help) {
|
|
75
140
|
printRunHelp();
|
|
@@ -78,8 +143,11 @@ async function runCleanRoom(options, context = {}) {
|
|
|
78
143
|
if (!options.taskManifest) {
|
|
79
144
|
throw new Error('--task-manifest is required');
|
|
80
145
|
}
|
|
81
|
-
if (!options.dryRun && !options.agentCommands) {
|
|
82
|
-
throw new Error('--agent-commands is required unless --dry-run is set');
|
|
146
|
+
if (!options.dryRun && !options.agentCommands && !options.agentRuntime) {
|
|
147
|
+
throw new Error('--agent-commands or --agent-runtime is required unless --dry-run is set');
|
|
148
|
+
}
|
|
149
|
+
if (options.agentCommands && options.agentRuntime) {
|
|
150
|
+
throw new Error('--agent-runtime cannot be used with --agent-commands');
|
|
83
151
|
}
|
|
84
152
|
|
|
85
153
|
const taskManifestPath = resolvePath(options.taskManifest, context.cwd || process.cwd());
|
|
@@ -96,20 +164,13 @@ async function runCleanRoom(options, context = {}) {
|
|
|
96
164
|
verifyPreflightGoal(manifest, manifestDir, roots);
|
|
97
165
|
const cap = effectiveIterationCap(manifest, options);
|
|
98
166
|
const agentConfigPath = options.agentCommands ? resolvePath(options.agentCommands, context.cwd || process.cwd()) : null;
|
|
99
|
-
const agentConfig
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
validateCommandConfig(agentConfig, { roots, configDir, contextManagement: manifest.context_management });
|
|
103
|
-
}
|
|
167
|
+
const { agentConfig, configDir } = options.dryRun
|
|
168
|
+
? { agentConfig: null, configDir: process.cwd() }
|
|
169
|
+
: resolveAgentConfig(options, context, roots, manifest, agentConfigPath);
|
|
104
170
|
|
|
105
171
|
return withRunLock(roots.contaminatedRoot, options.dryRun, async () => {
|
|
106
172
|
const coverageLedgerPath = path.join(roots.contaminatedRoot, 'coverage-ledger.json');
|
|
107
|
-
|
|
108
|
-
validateArtifacts(options.python, taskManifestPath, roots);
|
|
109
|
-
validateCleanRunContextReferences(options.python, roots);
|
|
110
|
-
const coverageLedger = readOptionalJson(coverageLedgerPath);
|
|
111
|
-
validateCoverageLedgerIntegrity(manifest, roots, coverageLedger);
|
|
112
|
-
validateFoundationCoverageGate(manifest, coverageLedger);
|
|
173
|
+
const coverageLedger = validateRunState(options, taskManifestPath, roots, manifest, coverageLedgerPath);
|
|
113
174
|
const selectedUnit = selectUnit(manifest, coverageLedger);
|
|
114
175
|
if (!selectedUnit) {
|
|
115
176
|
const result = completeResultOrSpecDelta(manifest, roots, coverageLedger);
|
|
@@ -121,28 +182,6 @@ async function runCleanRoom(options, context = {}) {
|
|
|
121
182
|
const ledgerPath = path.join(roots.contaminatedRoot, LEDGER_NAME);
|
|
122
183
|
const resultPath = path.join(roots.contaminatedRoot, RESULT_NAME);
|
|
123
184
|
const ledger = loadLedger(ledgerPath, manifest);
|
|
124
|
-
const previous = previousIteration(ledger);
|
|
125
|
-
if (repeatedUnitSelection(previous, selectedUnit)) {
|
|
126
|
-
const result = buildResult(manifest, 'no-progress-detected', 'partial', null, null, [
|
|
127
|
-
{
|
|
128
|
-
kind: 'other',
|
|
129
|
-
summary: 'The same unit was selected again after a no-progress iteration.',
|
|
130
|
-
status: 'open',
|
|
131
|
-
},
|
|
132
|
-
]);
|
|
133
|
-
if (!options.dryRun) {
|
|
134
|
-
writeResult(resultPath, result);
|
|
135
|
-
ledger.iterations.push({
|
|
136
|
-
iteration: ledger.iterations.length + 1,
|
|
137
|
-
unit_id: selectedUnit.unit_id,
|
|
138
|
-
stop_reason: 'repeated-unit-selection',
|
|
139
|
-
phases: [],
|
|
140
|
-
});
|
|
141
|
-
writeLedger(ledgerPath, ledger);
|
|
142
|
-
}
|
|
143
|
-
console.log('clean-room run: repeated-unit-selection');
|
|
144
|
-
return result;
|
|
145
|
-
}
|
|
146
185
|
|
|
147
186
|
if (options.dryRun) {
|
|
148
187
|
console.log(`clean-room run dry-run: selected ${selectedUnit.unit_id}`);
|
|
@@ -156,10 +195,39 @@ async function runCleanRoom(options, context = {}) {
|
|
|
156
195
|
}
|
|
157
196
|
|
|
158
197
|
let terminalResult = null;
|
|
198
|
+
let resultManifest = manifest;
|
|
159
199
|
const polishRequired = agentConfig.stages.some((stage) => stage.phase === POLISH_PHASE);
|
|
160
|
-
const strictContext = strictContextManagement(manifest.context_management);
|
|
161
200
|
for (let offset = 0; offset < cap; offset += 1) {
|
|
162
|
-
const
|
|
201
|
+
const currentManifest = reloadManifestForIteration(options, taskManifestPath, manifestDir, roots, schemaDir);
|
|
202
|
+
resultManifest = currentManifest;
|
|
203
|
+
const strictContext = strictContextManagement(currentManifest.context_management);
|
|
204
|
+
const currentCoverageLedger = validateRunState(options, taskManifestPath, roots, currentManifest, coverageLedgerPath);
|
|
205
|
+
const selected = selectUnit(currentManifest, currentCoverageLedger);
|
|
206
|
+
if (!selected) {
|
|
207
|
+
terminalResult = completeResultOrSpecDelta(currentManifest, roots, currentCoverageLedger);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
const previous = previousIteration(ledger);
|
|
211
|
+
if (repeatedUnitSelection(previous, selected)) {
|
|
212
|
+
terminalResult = buildResult(currentManifest, 'no-progress-detected', 'partial', null, null, [
|
|
213
|
+
{
|
|
214
|
+
kind: 'other',
|
|
215
|
+
summary: 'The same unit was selected again after a no-progress iteration.',
|
|
216
|
+
status: 'open',
|
|
217
|
+
},
|
|
218
|
+
]);
|
|
219
|
+
ledger.iterations.push({
|
|
220
|
+
iteration: ledger.iterations.length + 1,
|
|
221
|
+
unit_id: selected.unit_id,
|
|
222
|
+
stop_reason: 'repeated-unit-selection',
|
|
223
|
+
phases: [],
|
|
224
|
+
});
|
|
225
|
+
writeLedger(ledgerPath, ledger);
|
|
226
|
+
console.log('clean-room run: repeated-unit-selection');
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const iteration = (currentManifest.loop_context.inner_iteration || 0) + offset + 1;
|
|
163
231
|
const before = semanticProgressSnapshot(taskManifestPath, roots);
|
|
164
232
|
const phaseResults = [];
|
|
165
233
|
let coveragePhaseRan = false;
|
|
@@ -172,19 +240,19 @@ async function runCleanRoom(options, context = {}) {
|
|
|
172
240
|
stage,
|
|
173
241
|
configDir,
|
|
174
242
|
roots,
|
|
175
|
-
|
|
176
|
-
|
|
243
|
+
currentManifest,
|
|
244
|
+
selected,
|
|
177
245
|
strictContext
|
|
178
246
|
);
|
|
179
|
-
const stageResult = runStage(stage, configDir, roots,
|
|
247
|
+
const stageResult = runStage(stage, configDir, roots, currentManifest, selected, iteration, sessionContext);
|
|
180
248
|
const afterStage = artifactSnapshot(taskManifestPath, roots);
|
|
181
249
|
phaseResults.push(stageResult);
|
|
182
250
|
validateImplementationArtifactPlacement(roots);
|
|
183
251
|
validateArtifacts(options.python, taskManifestPath, roots, changedSnapshotPaths(beforeStage, afterStage));
|
|
184
252
|
validateCleanRunContextReferences(options.python, roots);
|
|
185
253
|
const stageCoverageLedger = readOptionalJson(coverageLedgerPath);
|
|
186
|
-
validateCoverageLedgerIntegrity(
|
|
187
|
-
validateFoundationCoverageGate(
|
|
254
|
+
validateCoverageLedgerIntegrity(currentManifest, roots, stageCoverageLedger);
|
|
255
|
+
validateFoundationCoverageGate(currentManifest, stageCoverageLedger);
|
|
188
256
|
if (stage.phase === REQUIRED_COVERAGE_PHASE && stageResult.status === 'passed') {
|
|
189
257
|
coveragePhaseRan = true;
|
|
190
258
|
}
|
|
@@ -198,25 +266,37 @@ async function runCleanRoom(options, context = {}) {
|
|
|
198
266
|
const progressDetected = !snapshotsEqual(before, after);
|
|
199
267
|
const ledgerEntry = {
|
|
200
268
|
iteration,
|
|
201
|
-
unit_id:
|
|
202
|
-
spec_slice_ref:
|
|
269
|
+
unit_id: selected.unit_id,
|
|
270
|
+
spec_slice_ref: currentManifest.loop_context.spec_slice_ref,
|
|
203
271
|
phases: phaseResults,
|
|
204
272
|
progress_detected: progressDetected,
|
|
205
273
|
};
|
|
206
274
|
|
|
207
275
|
if (failedStage) {
|
|
208
|
-
terminalResult = stageFailureResult(
|
|
276
|
+
terminalResult = stageFailureResult(currentManifest, failedStage);
|
|
209
277
|
ledgerEntry.stop_reason = 'spec-slice-blocked';
|
|
210
278
|
} else if (!progressDetected) {
|
|
211
|
-
terminalResult = noProgressResult(
|
|
279
|
+
terminalResult = noProgressResult(currentManifest);
|
|
212
280
|
ledgerEntry.stop_reason = 'no-progress-detected';
|
|
213
281
|
} else if (coveragePhaseRan) {
|
|
214
|
-
terminalResult = inferTerminalResult(
|
|
282
|
+
terminalResult = inferTerminalResult(currentManifest, roots, selected, {
|
|
215
283
|
polishRequired,
|
|
216
284
|
observedChangedPaths: changedImplementationPaths(before, after),
|
|
217
285
|
});
|
|
218
286
|
if (terminalResult) {
|
|
219
|
-
|
|
287
|
+
if (terminalResult.result === 'spec-slice-complete') {
|
|
288
|
+
const latestCoverageLedger = readOptionalJson(coverageLedgerPath);
|
|
289
|
+
validateCoverageLedgerIntegrity(currentManifest, roots, latestCoverageLedger);
|
|
290
|
+
validateFoundationCoverageGate(currentManifest, latestCoverageLedger);
|
|
291
|
+
if (shouldContinueAfterUnitComplete(currentManifest, latestCoverageLedger)) {
|
|
292
|
+
ledgerEntry.stop_reason = 'unit-complete';
|
|
293
|
+
terminalResult = null;
|
|
294
|
+
} else {
|
|
295
|
+
ledgerEntry.stop_reason = terminalResult.result;
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
ledgerEntry.stop_reason = terminalResult.result;
|
|
299
|
+
}
|
|
220
300
|
}
|
|
221
301
|
}
|
|
222
302
|
|
|
@@ -228,15 +308,15 @@ async function runCleanRoom(options, context = {}) {
|
|
|
228
308
|
}
|
|
229
309
|
|
|
230
310
|
if (!terminalResult) {
|
|
231
|
-
terminalResult = iterationLimitResult(
|
|
311
|
+
terminalResult = iterationLimitResult(resultManifest);
|
|
232
312
|
}
|
|
233
313
|
writeResult(resultPath, terminalResult);
|
|
234
314
|
validateImplementationArtifactPlacement(roots);
|
|
235
315
|
validateArtifacts(options.python, taskManifestPath, roots);
|
|
236
316
|
validateCleanRunContextReferences(options.python, roots);
|
|
237
317
|
const finalCoverageLedger = readOptionalJson(coverageLedgerPath);
|
|
238
|
-
validateCoverageLedgerIntegrity(
|
|
239
|
-
validateFoundationCoverageGate(
|
|
318
|
+
validateCoverageLedgerIntegrity(resultManifest, roots, finalCoverageLedger);
|
|
319
|
+
validateFoundationCoverageGate(resultManifest, finalCoverageLedger);
|
|
240
320
|
console.log(`clean-room run: ${terminalResult.result}`);
|
|
241
321
|
return terminalResult;
|
|
242
322
|
});
|
package/lib/run-stages.cjs
CHANGED
|
@@ -8,6 +8,7 @@ const { spawnSync } = require('node:child_process');
|
|
|
8
8
|
const { fileHash } = require('./fs-utils.cjs');
|
|
9
9
|
const {
|
|
10
10
|
DEFAULT_TIMEOUT_MS,
|
|
11
|
+
MANAGER_PREPARE_PHASE,
|
|
11
12
|
MAX_OUTPUT_BYTES,
|
|
12
13
|
MAX_TIMEOUT_MS,
|
|
13
14
|
POLISH_PHASE,
|
|
@@ -60,7 +61,7 @@ function validateStageBoundaries(stage, index, context) {
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
let allowed = false;
|
|
63
|
-
if (stage.phase === 'contaminated-analysis' || stage.phase === 'contaminated-coverage-verify') {
|
|
64
|
+
if (stage.phase === MANAGER_PREPARE_PHASE || stage.phase === 'contaminated-analysis' || stage.phase === 'contaminated-coverage-verify') {
|
|
64
65
|
allowed = pathIsUnder(cwd, roots.contaminatedRoot) || pathIsUnder(cwd, configDir);
|
|
65
66
|
} else if (stage.phase === 'sanitize-handoff') {
|
|
66
67
|
allowed = pathIsUnder(cwd, roots.contaminatedRoot);
|
|
@@ -114,7 +115,7 @@ function resolveStageBriefPath(stage, configDir, roots) {
|
|
|
114
115
|
function validateStageContext(stage, index, context = {}) {
|
|
115
116
|
const strict = strictContextManagement(context.contextManagement);
|
|
116
117
|
if (stage.context === undefined) {
|
|
117
|
-
if (strict) {
|
|
118
|
+
if (strict && stage.phase !== MANAGER_PREPARE_PHASE) {
|
|
118
119
|
throw new Error(`agent command stage ${index} must provide context in strict context-management mode`);
|
|
119
120
|
}
|
|
120
121
|
return;
|
|
@@ -134,10 +135,10 @@ function validateStageContext(stage, index, context = {}) {
|
|
|
134
135
|
if (stage.context.brief_path !== undefined && (typeof stage.context.brief_path !== 'string' || stage.context.brief_path === '')) {
|
|
135
136
|
throw new Error(`agent command stage ${index} context.brief_path must be a non-empty string`);
|
|
136
137
|
}
|
|
137
|
-
if (strict && stage.context.fresh_session !== true) {
|
|
138
|
+
if (strict && stage.phase !== MANAGER_PREPARE_PHASE && stage.context.fresh_session !== true) {
|
|
138
139
|
throw new Error(`agent command stage ${index} context.fresh_session must be true in strict context-management mode`);
|
|
139
140
|
}
|
|
140
|
-
if (strict && !stage.context.brief_path) {
|
|
141
|
+
if (strict && stage.phase !== MANAGER_PREPARE_PHASE && !stage.context.brief_path) {
|
|
141
142
|
throw new Error(`agent command stage ${index} context.brief_path is required in strict context-management mode`);
|
|
142
143
|
}
|
|
143
144
|
if (stage.context.brief_path && context.roots && context.configDir) {
|
|
@@ -330,6 +331,10 @@ function stagePrompt(stage, manifest, unit, iteration, sessionContext = null) {
|
|
|
330
331
|
'',
|
|
331
332
|
'Run only this configured clean-room stage from durable artifacts.',
|
|
332
333
|
'Do not use prior chat history as state.',
|
|
334
|
+
...(stage.phase === MANAGER_PREPARE_PHASE ? [
|
|
335
|
+
'Act only as Agent 0 manager/controller for this selected unit.',
|
|
336
|
+
'Prepare durable controller status or role-session briefs as needed, then return. Do not perform downstream role work.',
|
|
337
|
+
] : []),
|
|
333
338
|
...(sessionContext ? ['Read CLEAN_ROOM_SESSION_BRIEF_PATH first and load only the artifact refs it permits.'] : []),
|
|
334
339
|
'',
|
|
335
340
|
].join('\n');
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/skills/attended/SKILL.md
CHANGED
|
@@ -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`. 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-*` 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
|
|
|
@@ -30,4 +30,4 @@ Before indexing or artifact generation, confirm that source roots, contaminated
|
|
|
30
30
|
|
|
31
31
|
Record `preflight_goal_ref`, `preflight_goal_sha256`, required `handoff_sequence`, and `controller_policy.mode` as `attended`. Pause for human review at preflight open questions, scope gate, clean handoff, terminal implementation or polish 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.
|
|
32
32
|
|
|
33
|
-
For multi-file source scope, guide agent zero/controller to run `skills/clean-room/scripts/build_source_index.py` as preflight outside clean-room role sessions. Store `source-index.json` only under the contaminated artifact root and never include it in clean handoff packages. If no indexable source code exists and screenshots/images are the only authorized evidence, guide agent zero/controller to run `skills/clean-room/scripts/build_visual_index.py` instead, store `visual-index.json` only under the contaminated artifact root, include visual roots in `CLEAN_ROOM_SOURCE_ROOTS
|
|
33
|
+
For multi-file source scope, guide agent zero/controller to run `skills/clean-room/scripts/build_source_index.py` as preflight outside clean-room role sessions. Store `source-index.json` only under the contaminated artifact root and never include it in clean handoff packages. If no indexable source code exists and screenshots/images are the only authorized evidence, guide agent zero/controller to run `skills/clean-room/scripts/build_visual_index.py` instead, store `visual-index.json` only under the contaminated artifact root, include visual roots in `CLEAN_ROOM_SOURCE_ROOTS` (ensuring screenshot evidence directories are explicitly added to `CLEAN_ROOM_SOURCE_ROOTS` during execution so that path-aware read hooks such as `hooks/deny-clean-source-read.py` can protect them as expected), and pause before decomposition to clarify the product goal, target user flow, screenshot coverage, target stack, UI exactness boundary, and public-compatibility status of visible words.
|
|
@@ -54,6 +54,8 @@ 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.
|
|
58
|
+
|
|
57
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.
|
|
58
60
|
|
|
59
61
|
Post-write hook failures are policy failures, not implementation guidance. If a clean or staged artifact cannot be read, scanned, schema-checked, or hashed because the filesystem changed, report the controlled redacted failure and ask the controller/user to restore readable artifact state before retrying.
|
|
@@ -62,7 +64,7 @@ Post-write hook failures are policy failures, not implementation guidance. If a
|
|
|
62
64
|
|
|
63
65
|
Use the recovery skills when a run already has durable artifacts:
|
|
64
66
|
|
|
65
|
-
- `resume`: reload `task-manifest.json`, its `initialization_snapshot`, ledgers, `implementation-plan.json`, `implementation-report.json`, `qc-report.json`, and abstract delta tickets, then continue from the earliest incomplete gate using the recorded `controller_policy`. If `init-config.json` differs from the snapshot, report drift and wait for explicit confirmation.
|
|
67
|
+
- `resume-cr`: reload `task-manifest.json`, its `initialization_snapshot`, ledgers, `implementation-plan.json`, `implementation-report.json`, `qc-report.json`, and abstract delta tickets, then continue from the earliest incomplete gate using the recorded `controller_policy`. If `init-config.json` differs from the snapshot, report drift and wait for explicit confirmation.
|
|
66
68
|
- `start-over`: after explicit confirmation, non-destructively archive or quarantine existing artifacts and restart from the scope gate with a fresh `task_id`.
|
|
67
69
|
- `refocus`: audit declared scope against current artifacts and steer the workflow back to missed gates without expanding scope.
|
|
68
70
|
|
|
@@ -84,7 +86,7 @@ If more than one candidate run is found without an explicit user path, list the
|
|
|
84
86
|
|
|
85
87
|
Classify the selected candidate before starting the wizard:
|
|
86
88
|
|
|
87
|
-
- Valid `task-manifest.json`: route to `resume` and continue from the earliest incomplete gate.
|
|
89
|
+
- Valid `task-manifest.json`: route to `resume-cr` and continue from the earliest incomplete gate.
|
|
88
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.
|
|
89
91
|
- `clean-room-bootstrap.json` only: run preflight using the bootstrap roots.
|
|
90
92
|
- Invalid `preflight-goal.json`: stop, report canonical schema or required-field errors, and do not create a replacement preflight.
|
|
@@ -75,6 +75,6 @@ When `context_management.enforcement` is `strict`, no role can start until Agent
|
|
|
75
75
|
|
|
76
76
|
## Recovery
|
|
77
77
|
|
|
78
|
-
`resume` and `refocus` must stop when new-run artifacts lack `preflight_goal_ref`, `preflight_goal_sha256`, or a complete `handoff_sequence`. Report this as legacy or incomplete preflight state; do not infer intent from prior chat or source.
|
|
78
|
+
`resume-cr` and `refocus` must stop when new-run artifacts lack `preflight_goal_ref`, `preflight_goal_sha256`, or a complete `handoff_sequence`. Report this as legacy or incomplete preflight state; do not infer intent from prior chat or source.
|
|
79
79
|
|
|
80
80
|
`start-over` must create a new preflight goal or explicitly reuse a reviewed goal contract before recreating active artifacts.
|
|
@@ -104,7 +104,7 @@ The durable tasklist is `task-manifest.json` `units`, generated by agent zero du
|
|
|
104
104
|
|
|
105
105
|
Use recovery entry points only when durable artifacts already exist:
|
|
106
106
|
|
|
107
|
-
- `resume`: reload the manifest, referenced preflight goal, initialization snapshot, ledgers, clean run context, handoff artifacts, implementation plan, implementation report, QC report, and abstract delta tickets; validate schema and leakage state; continue from the earliest incomplete gate under the recorded controller policy. Agent 0 may write or refresh `controller-status.json`, then create the next role-specific `role-session-brief.json`. Clean roles must receive the brief and clean artifact refs, not full resume state. If reusable `init-config.json` differs from the manifest snapshot, report drift and stop before applying changes. If new-run artifacts lack preflight refs or handoff sequence, stop for reviewed preflight migration.
|
|
107
|
+
- `resume-cr`: reload the manifest, referenced preflight goal, initialization snapshot, ledgers, clean run context, handoff artifacts, implementation plan, implementation report, QC report, and abstract delta tickets; validate schema and leakage state; continue from the earliest incomplete gate under the recorded controller policy. Agent 0 may write or refresh `controller-status.json`, then create the next role-specific `role-session-brief.json`. Clean roles must receive the brief and clean artifact refs, not full resume state. If reusable `init-config.json` differs from the manifest snapshot, report drift and stop before applying changes. If new-run artifacts lack preflight refs or handoff sequence, stop for reviewed preflight migration.
|
|
108
108
|
- `start-over`: require explicit confirmation, archive or quarantine current artifacts without deletion, then return to the preflight gate with a fresh `task_id`.
|
|
109
109
|
- `refocus`: compare current artifacts to declared scope and preflight goal, identify missed gates or open deltas, and steer Agent 0 back to the earliest required gate without expanding scope.
|
|
110
110
|
|
|
@@ -112,7 +112,7 @@ Capture:
|
|
|
112
112
|
- user rules split into `clean_safe` and `contaminated_only`
|
|
113
113
|
- reconfiguration policy requiring confirmation for root, schema, and model changes
|
|
114
114
|
|
|
115
|
-
`clean-run-context.json` is the only run context Agent 2, Agent 3, and Agent 4 should read. It may contain clean artifact paths, implementation root environment references, target profile, native artifact expectations, clean-safe goal contract fields, code hygiene policy, approved public references, clean-safe rules, clean-side model preferences, optional Agent 4 local commit policy, and the artifact-only coordination boundary. It must not contain source roots, visual roots, contaminated artifact roots, source index refs, visual index refs, coverage ledgers, evidence ledgers, contaminated-only rules, full `preflight-goal.json`, or the full `task-manifest.json`.
|
|
115
|
+
`clean-run-context.json` is the only run context Agent 2, Agent 3, and Agent 4 should read. It may contain clean artifact paths, implementation root environment references, target profile, native artifact expectations, clean-safe goal contract fields, code hygiene policy, approved public references, clean-safe rules, clean-side model preferences, optional Agent 4 local commit policy (e.g. `implementation.polish_commit` with `agent4_shell_allowed`, `cwd_policy`, and `git_policy`), and the artifact-only coordination boundary. It must not contain source roots, visual roots, contaminated artifact roots, source index refs, visual index refs, coverage ledgers, evidence ledgers, contaminated-only rules, full `preflight-goal.json`, or the full `task-manifest.json`.
|
|
116
116
|
|
|
117
117
|
`context_management` is optional on `task-manifest.json` and `clean-run-context.json`. When present with `mode: "role-session-briefs"`, it records advisory or strict enforcement plus budgets for prompt characters, brief characters, artifact refs, and referenced artifact bytes. Strict mode requires a fresh role session and a valid `role-session-brief.json` for each stage.
|
|
118
118
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: resume
|
|
2
|
+
name: resume-cr
|
|
3
3
|
description: Continues an existing Clean Room run from durable artifacts without relying on prior chat history.
|
|
4
4
|
argument-hint: [existing task-manifest.json or artifact roots]
|
|
5
5
|
disable-model-invocation: true
|
|
@@ -11,6 +11,8 @@ 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.
|
|
15
|
+
|
|
14
16
|
## Load Order
|
|
15
17
|
|
|
16
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.
|
|
@@ -13,7 +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`. 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-*` 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
|
+
|
|
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.
|
|
17
19
|
|
|
18
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`.
|
|
19
21
|
|
|
@@ -36,4 +38,4 @@ Record `preflight_goal_ref`, `preflight_goal_sha256`, required `handoff_sequence
|
|
|
36
38
|
|
|
37
39
|
The inner loop returns only after Agent 0 consumes the terminal Agent 3 report, any configured Agent 4 `polish-report.json`, and completes contaminated-side coverage verification. Write `clean-room-result.json` before returning control to the outer spec loop.
|
|
38
40
|
|
|
39
|
-
For multi-file source scope, guide agent zero/controller to run `skills/clean-room/scripts/build_source_index.py` as preflight outside clean-room role sessions. Store `source-index.json` only under the contaminated artifact root and never include it in clean handoff packages. If no indexable source code exists and screenshots/images are the only authorized evidence, unattended mode may use `skills/clean-room/scripts/build_visual_index.py` only after preflight already answers the visual-fallback questions with no open questions. Store `visual-index.json` only under the contaminated artifact root and include visual roots in `CLEAN_ROOM_SOURCE_ROOTS
|
|
41
|
+
For multi-file source scope, guide agent zero/controller to run `skills/clean-room/scripts/build_source_index.py` as preflight outside clean-room role sessions. Store `source-index.json` only under the contaminated artifact root and never include it in clean handoff packages. If no indexable source code exists and screenshots/images are the only authorized evidence, unattended mode may use `skills/clean-room/scripts/build_visual_index.py` only after preflight already answers the visual-fallback questions with no open questions. Store `visual-index.json` only under the contaminated artifact root and include visual roots in `CLEAN_ROOM_SOURCE_ROOTS` (ensuring screenshot evidence directories are explicitly added to `CLEAN_ROOM_SOURCE_ROOTS` during execution so that path-aware read hooks such as `hooks/deny-clean-source-read.py` can protect them as expected).
|