clean-room-skill 0.1.14 → 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/docs/ARCHITECTURE.md +2 -2
- package/docs/REFERENCE.md +6 -4
- 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/clean-room/SKILL.md +2 -0
- package/skills/resume-cr/SKILL.md +2 -0
- package/skills/unattended/SKILL.md +2 -0
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/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
|
|
|
@@ -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
|
@@ -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.
|
|
@@ -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.
|
|
@@ -15,6 +15,8 @@ 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.
|
|
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
|
|
|
20
22
|
Do not assume target language, license policy, dependency policy, exactness policy, output directory, or feature add/remove policy during the unattended loop. Stop on ambiguity instead of inventing product decisions.
|