clean-room-skill 0.1.14 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +7 -3
- package/docs/HOOKS.md +2 -0
- package/docs/REFERENCE.md +8 -4
- package/hooks/validate-json-schema.py +499 -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/clean-room/SKILL.md +2 -0
- package/skills/resume-cr/SKILL.md +2 -0
- package/skills/unattended/SKILL.md +2 -0
|
@@ -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
|
});
|