clean-room-skill 0.1.12 → 0.1.13
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 +32 -5
- package/agents/clean-architect.md +3 -0
- package/agents/clean-implementer-verifier-shell.md +3 -0
- package/agents/clean-polish-reviewer.md +3 -0
- package/agents/clean-qa-editor.md +3 -0
- package/agents/contaminated-handoff-sanitizer.md +3 -0
- package/agents/contaminated-manager-verifier.md +3 -0
- package/agents/contaminated-source-analyst.md +3 -0
- package/bin/install.js +11 -1621
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/HOOKS.md +14 -10
- package/docs/REFERENCE.md +24 -4
- package/examples/codex/.codex/agents/clean-architect.toml +3 -3
- package/examples/codex/.codex/agents/clean-polish-reviewer.toml +2 -2
- package/examples/codex/.codex/agents/clean-qa-editor.toml +2 -2
- package/examples/codex/.codex/agents/contaminated-handoff-sanitizer.toml +2 -2
- package/examples/codex/.codex/agents/contaminated-manager-verifier.toml +3 -3
- package/examples/codex/.codex/agents/contaminated-source-analyst.toml +2 -2
- package/lib/bootstrap.cjs +5 -1
- package/lib/doctor.cjs +157 -5
- package/lib/hooks.cjs +18 -0
- package/lib/install-artifacts.cjs +178 -4
- package/lib/install-claude-plugin.cjs +374 -0
- package/lib/install-cli.cjs +99 -0
- package/lib/install-operations.cjs +376 -0
- package/lib/install-options.cjs +149 -0
- package/lib/install-runtime-selection.cjs +180 -0
- package/lib/install-status.cjs +292 -0
- package/lib/install-tui.cjs +359 -0
- package/lib/preflight-bootstrap.cjs +39 -0
- package/lib/preflight-cli.cjs +95 -0
- package/lib/preflight-constants.cjs +25 -0
- package/lib/preflight-output.cjs +37 -0
- package/lib/preflight-paths.cjs +67 -0
- package/lib/preflight-template.cjs +103 -0
- package/lib/preflight-validation.cjs +276 -0
- package/lib/preflight.cjs +18 -461
- package/lib/run-clean-artifacts.cjs +276 -0
- package/lib/run-cli.cjs +90 -0
- package/lib/run-constants.cjs +171 -0
- package/lib/run-controller.cjs +247 -0
- package/lib/run-coverage.cjs +350 -0
- package/lib/run-hooks.cjs +96 -0
- package/lib/run-manifest.cjs +111 -0
- package/lib/run-progress.cjs +160 -0
- package/lib/run-results.cjs +433 -0
- package/lib/run-roots.cjs +230 -0
- package/lib/run-stages.cjs +409 -0
- package/lib/run.cjs +4 -2254
- package/lib/runtime-layout.cjs +12 -5
- package/package.json +8 -2
- package/plugin.json +1 -1
- package/skills/attended/SKILL.md +2 -0
- package/skills/clean-room/SKILL.md +2 -2
- package/skills/unattended/SKILL.md +2 -0
|
@@ -0,0 +1,276 @@
|
|
|
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 {
|
|
8
|
+
fileHash,
|
|
9
|
+
readJsonFile,
|
|
10
|
+
} = require('./fs-utils.cjs');
|
|
11
|
+
const {
|
|
12
|
+
CLEAN_RUN_CONTEXT_NAME,
|
|
13
|
+
HANDOFF_PACKAGE_NAME,
|
|
14
|
+
} = require('./run-constants.cjs');
|
|
15
|
+
const { runHook } = require('./run-hooks.cjs');
|
|
16
|
+
const { pathIsUnder } = require('./run-roots.cjs');
|
|
17
|
+
|
|
18
|
+
function readOptionalJson(filePath) {
|
|
19
|
+
return fs.existsSync(filePath) ? readJsonFile(filePath, null) : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function cleanRunContextPath(roots) {
|
|
23
|
+
return path.join(roots.cleanRoot, CLEAN_RUN_CONTEXT_NAME);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readCleanRunContext(roots) {
|
|
27
|
+
const contextPath = cleanRunContextPath(roots);
|
|
28
|
+
if (!fs.existsSync(contextPath) || !fs.statSync(contextPath).isFile()) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return readJsonFile(contextPath, null);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveCleanArtifactPath(rawPath, roots, label) {
|
|
35
|
+
if (typeof rawPath !== 'string' || rawPath.trim() === '') {
|
|
36
|
+
throw new Error(`${label} must be a non-empty clean artifact path`);
|
|
37
|
+
}
|
|
38
|
+
const expanded = rawPath === '~' ? os.homedir() : rawPath.startsWith('~/') ? path.join(os.homedir(), rawPath.slice(2)) : rawPath;
|
|
39
|
+
const resolved = path.isAbsolute(expanded) ? path.resolve(expanded) : path.resolve(roots.cleanRoot, expanded);
|
|
40
|
+
if (!pathIsUnder(resolved, roots.cleanRoot)) {
|
|
41
|
+
throw new Error(`${label} must resolve under CLEAN_ROOM_CLEAN_ROOTS: ${rawPath}`);
|
|
42
|
+
}
|
|
43
|
+
return resolved;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function cleanContextBehaviorSpecPaths(context, roots) {
|
|
47
|
+
const refs = context?.clean_artifacts?.behavior_specs;
|
|
48
|
+
if (!Array.isArray(refs)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
return refs.map((ref, index) => resolveCleanArtifactPath(ref, roots, `clean-run-context behavior_specs[${index}]`));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function cleanContextArtifactPath(context, roots, key, label) {
|
|
55
|
+
const ref = context?.clean_artifacts?.[key];
|
|
56
|
+
if (typeof ref !== 'string' || ref.trim() === '') {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return resolveCleanArtifactPath(ref, roots, label);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function cleanCompletionArtifactPath(roots, key, defaultName, label) {
|
|
63
|
+
const context = readCleanRunContext(roots);
|
|
64
|
+
const contextPath = context ? cleanContextArtifactPath(context, roots, key, label) : null;
|
|
65
|
+
return contextPath || path.join(roots.cleanRoot, defaultName);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readCleanCompletionArtifact(roots, key, defaultName, label) {
|
|
69
|
+
const artifactPath = cleanCompletionArtifactPath(roots, key, defaultName, label);
|
|
70
|
+
if (!fs.existsSync(artifactPath) || !fs.statSync(artifactPath).isFile()) {
|
|
71
|
+
return { artifactPath, artifact: null };
|
|
72
|
+
}
|
|
73
|
+
return { artifactPath, artifact: readJsonFile(artifactPath, null) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validateReferencedBehaviorSpecs(python, roots, specPaths) {
|
|
77
|
+
for (const specPath of specPaths) {
|
|
78
|
+
if (!fs.existsSync(specPath) || !fs.statSync(specPath).isFile()) {
|
|
79
|
+
throw new Error(`clean-run-context behavior spec does not exist: ${specPath}`);
|
|
80
|
+
}
|
|
81
|
+
runHook(python, 'validate-json-schema.py', specPath, roots);
|
|
82
|
+
runHook(python, 'check-artifact-leakage.py', specPath, roots);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeCleanRelativePath(value) {
|
|
87
|
+
return String(value || '')
|
|
88
|
+
.replace(/\\/g, '/')
|
|
89
|
+
.replace(/^\.\//, '')
|
|
90
|
+
.replace(/\/+/g, '/')
|
|
91
|
+
.replace(/\/$/, '');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function prefixOwnsPath(prefix, relPath) {
|
|
95
|
+
const owner = normalizeCleanRelativePath(prefix);
|
|
96
|
+
const target = normalizeCleanRelativePath(relPath);
|
|
97
|
+
return owner !== '' && (target === owner || target.startsWith(`${owner}/`));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function skeletonAreaMap(skeleton) {
|
|
101
|
+
const areas = new Map();
|
|
102
|
+
for (const area of skeleton?.areas || []) {
|
|
103
|
+
if (area && typeof area.area_id === 'string') {
|
|
104
|
+
areas.set(area.area_id, area);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return areas;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function validateSkeletonArchitecture(skeleton) {
|
|
111
|
+
const areas = skeletonAreaMap(skeleton);
|
|
112
|
+
for (const area of skeleton?.areas || []) {
|
|
113
|
+
for (const dependencyRef of area.allowed_area_dependencies || []) {
|
|
114
|
+
if (dependencyRef === area.area_id) {
|
|
115
|
+
throw new Error(`skeleton-manifest architecture area must not depend on itself: ${area.area_id}`);
|
|
116
|
+
}
|
|
117
|
+
if (!areas.has(dependencyRef)) {
|
|
118
|
+
throw new Error(`skeleton-manifest architecture area references unknown dependency area: ${dependencyRef}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function validateAreaRefs(areaRefs, areas, label) {
|
|
125
|
+
if (!Array.isArray(areaRefs) || areaRefs.length === 0) {
|
|
126
|
+
throw new Error(`${label} must reference at least one architecture area`);
|
|
127
|
+
}
|
|
128
|
+
for (const areaRef of areaRefs) {
|
|
129
|
+
if (!areas.has(areaRef)) {
|
|
130
|
+
throw new Error(`${label} references unknown architecture area: ${areaRef}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validatePathsOwnedByAreas(paths, areaRefs, areas, label) {
|
|
136
|
+
validateAreaRefs(areaRefs, areas, label);
|
|
137
|
+
const candidateAreas = areaRefs.map((areaRef) => areas.get(areaRef));
|
|
138
|
+
for (const relPath of paths || []) {
|
|
139
|
+
const owned = candidateAreas.some((area) => {
|
|
140
|
+
return (area.owned_path_prefixes || []).some((prefix) => prefixOwnsPath(prefix, relPath));
|
|
141
|
+
});
|
|
142
|
+
if (!owned) {
|
|
143
|
+
throw new Error(`${label} path is outside referenced architecture areas: ${relPath}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function validateImplementationPlanArchitecture(plan, skeleton, roots, skeletonPath) {
|
|
149
|
+
const areas = skeletonAreaMap(skeleton);
|
|
150
|
+
const referencedSkeletonPath = resolveCleanArtifactPath(
|
|
151
|
+
plan.architecture_manifest_ref,
|
|
152
|
+
roots,
|
|
153
|
+
'implementation-plan architecture_manifest_ref'
|
|
154
|
+
);
|
|
155
|
+
if (path.resolve(referencedSkeletonPath) !== path.resolve(skeletonPath)) {
|
|
156
|
+
throw new Error('implementation-plan architecture_manifest_ref must match clean-run-context skeleton_manifest');
|
|
157
|
+
}
|
|
158
|
+
for (const workItem of plan.work_items || []) {
|
|
159
|
+
const label = `implementation-plan work item ${workItem.work_item_id || '<unknown>'}`;
|
|
160
|
+
validatePathsOwnedByAreas(
|
|
161
|
+
[...(workItem.target_paths || []), ...(workItem.test_paths || [])],
|
|
162
|
+
workItem.architecture_area_refs,
|
|
163
|
+
areas,
|
|
164
|
+
label
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
for (const refactor of plan.planned_refactors || []) {
|
|
168
|
+
const label = `implementation-plan planned refactor ${refactor.refactor_id || '<unknown>'}`;
|
|
169
|
+
validatePathsOwnedByAreas(
|
|
170
|
+
[...(refactor.existing_paths || []), ...(refactor.target_paths || []), ...(refactor.test_paths || [])],
|
|
171
|
+
refactor.architecture_area_refs,
|
|
172
|
+
areas,
|
|
173
|
+
label
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function handoffArtifactIndex(handoffPath, roots) {
|
|
179
|
+
const handoff = readJsonFile(handoffPath, null);
|
|
180
|
+
const index = new Map();
|
|
181
|
+
for (const item of handoff?.artifacts || []) {
|
|
182
|
+
if (!item || typeof item !== 'object' || typeof item.path !== 'string') {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const artifactPath = resolveCleanArtifactPath(item.path, roots, 'handoff artifact path');
|
|
186
|
+
index.set(artifactPath, item);
|
|
187
|
+
}
|
|
188
|
+
return index;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function validateHandoffCoversBehaviorSpecs(context, roots, specPaths) {
|
|
192
|
+
const handoffRef = context?.clean_artifacts?.handoff_package || HANDOFF_PACKAGE_NAME;
|
|
193
|
+
const handoffPath = resolveCleanArtifactPath(handoffRef, roots, 'clean-run-context handoff_package');
|
|
194
|
+
if (!fs.existsSync(handoffPath) || !fs.statSync(handoffPath).isFile()) {
|
|
195
|
+
throw new Error(`clean-run-context handoff package does not exist: ${handoffPath}`);
|
|
196
|
+
}
|
|
197
|
+
const artifacts = handoffArtifactIndex(handoffPath, roots);
|
|
198
|
+
for (const specPath of specPaths) {
|
|
199
|
+
const item = artifacts.get(specPath);
|
|
200
|
+
if (!item) {
|
|
201
|
+
throw new Error(`handoff-package.json does not include clean-run-context behavior spec: ${specPath}`);
|
|
202
|
+
}
|
|
203
|
+
const expected = String(item.sha256 || '').toLowerCase();
|
|
204
|
+
const actual = fileHash(specPath).toLowerCase();
|
|
205
|
+
if (expected !== actual) {
|
|
206
|
+
throw new Error(`handoff-package.json sha256 mismatch for behavior spec: ${specPath}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function validateCleanRunContextReferences(python, roots) {
|
|
212
|
+
const context = readCleanRunContext(roots);
|
|
213
|
+
if (!context) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const specPaths = cleanContextBehaviorSpecPaths(context, roots);
|
|
217
|
+
const skeletonPath = cleanContextArtifactPath(context, roots, 'skeleton_manifest', 'clean-run-context skeleton_manifest');
|
|
218
|
+
const planPath = cleanContextArtifactPath(context, roots, 'implementation_plan', 'clean-run-context implementation_plan');
|
|
219
|
+
let skeleton = null;
|
|
220
|
+
validateReferencedBehaviorSpecs(python, roots, specPaths);
|
|
221
|
+
if (specPaths.length > 0 || skeletonPath) {
|
|
222
|
+
if (!skeletonPath || !fs.existsSync(skeletonPath) || !fs.statSync(skeletonPath).isFile()) {
|
|
223
|
+
throw new Error(`clean-run-context skeleton manifest does not exist: ${skeletonPath || 'missing skeleton_manifest ref'}`);
|
|
224
|
+
}
|
|
225
|
+
runHook(python, 'validate-json-schema.py', skeletonPath, roots, 'clean-architect');
|
|
226
|
+
runHook(python, 'check-artifact-leakage.py', skeletonPath, roots, 'clean-architect');
|
|
227
|
+
skeleton = readJsonFile(skeletonPath, null);
|
|
228
|
+
validateSkeletonArchitecture(skeleton);
|
|
229
|
+
}
|
|
230
|
+
validateHandoffCoversBehaviorSpecs(context, roots, specPaths);
|
|
231
|
+
if (planPath && fs.existsSync(planPath) && fs.statSync(planPath).isFile() && skeletonPath) {
|
|
232
|
+
const plan = readJsonFile(planPath, null);
|
|
233
|
+
validateImplementationPlanArchitecture(plan, skeleton, roots, skeletonPath);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function behaviorSpecOpenQuestionTickets(roots) {
|
|
238
|
+
const context = readCleanRunContext(roots);
|
|
239
|
+
if (!context) {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
const specPaths = cleanContextBehaviorSpecPaths(context, roots);
|
|
243
|
+
const openSpecCount = specPaths.filter((specPath) => {
|
|
244
|
+
if (!fs.existsSync(specPath) || !fs.statSync(specPath).isFile()) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
const spec = readJsonFile(specPath, null);
|
|
248
|
+
return Array.isArray(spec.open_questions) && spec.open_questions.length > 0;
|
|
249
|
+
}).length;
|
|
250
|
+
if (openSpecCount === 0) {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
return [
|
|
254
|
+
{
|
|
255
|
+
ticket_id: 'delta-open-questions',
|
|
256
|
+
kind: 'ambiguity',
|
|
257
|
+
summary: `${openSpecCount} approved behavior spec(s) still contain open questions.`,
|
|
258
|
+
requested_clean_change: 'Resolve or remove approved behavior spec open questions before marking the spec slice complete.',
|
|
259
|
+
status: 'open',
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = {
|
|
265
|
+
behaviorSpecOpenQuestionTickets,
|
|
266
|
+
cleanCompletionArtifactPath,
|
|
267
|
+
cleanContextArtifactPath,
|
|
268
|
+
cleanContextBehaviorSpecPaths,
|
|
269
|
+
readCleanCompletionArtifact,
|
|
270
|
+
readCleanRunContext,
|
|
271
|
+
readOptionalJson,
|
|
272
|
+
resolveCleanArtifactPath,
|
|
273
|
+
skeletonAreaMap,
|
|
274
|
+
validateCleanRunContextReferences,
|
|
275
|
+
validatePathsOwnedByAreas,
|
|
276
|
+
};
|
package/lib/run-cli.cjs
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function printRunHelp() {
|
|
4
|
+
console.log(`Usage: clean-room-skill run --task-manifest <path> --agent-commands <path> [options]
|
|
5
|
+
|
|
6
|
+
Run one bounded inner clean-room controller loop for an approved spec slice.
|
|
7
|
+
|
|
8
|
+
Options:
|
|
9
|
+
--task-manifest <path> Required task-manifest.json path
|
|
10
|
+
--agent-commands <path> Required role command adapter JSON unless --dry-run is set
|
|
11
|
+
--max-iterations <n> Lower the manifest/loop iteration cap
|
|
12
|
+
--once Run at most one inner iteration
|
|
13
|
+
--dry-run Validate and print the selected unit without writing or spawning agents
|
|
14
|
+
--schema-dir <path> Schema directory override
|
|
15
|
+
--python <path> Python executable for bundled validation hooks (default: python3)
|
|
16
|
+
-h, --help Show this help
|
|
17
|
+
`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseRunArgs(argv) {
|
|
21
|
+
const options = {
|
|
22
|
+
taskManifest: null,
|
|
23
|
+
agentCommands: null,
|
|
24
|
+
maxIterations: null,
|
|
25
|
+
once: false,
|
|
26
|
+
dryRun: false,
|
|
27
|
+
schemaDir: null,
|
|
28
|
+
python: 'python3',
|
|
29
|
+
help: false,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
33
|
+
const arg = argv[index];
|
|
34
|
+
if (arg === '-h' || arg === '--help') {
|
|
35
|
+
options.help = true;
|
|
36
|
+
} else if (arg === '--once') {
|
|
37
|
+
options.once = true;
|
|
38
|
+
} else if (arg === '--dry-run') {
|
|
39
|
+
options.dryRun = true;
|
|
40
|
+
} else if (arg === '--task-manifest') {
|
|
41
|
+
index += 1;
|
|
42
|
+
options.taskManifest = requiredValue(argv, index, '--task-manifest');
|
|
43
|
+
} else if (arg.startsWith('--task-manifest=')) {
|
|
44
|
+
options.taskManifest = arg.slice('--task-manifest='.length);
|
|
45
|
+
} else if (arg === '--agent-commands') {
|
|
46
|
+
index += 1;
|
|
47
|
+
options.agentCommands = requiredValue(argv, index, '--agent-commands');
|
|
48
|
+
} else if (arg.startsWith('--agent-commands=')) {
|
|
49
|
+
options.agentCommands = arg.slice('--agent-commands='.length);
|
|
50
|
+
} else if (arg === '--max-iterations') {
|
|
51
|
+
index += 1;
|
|
52
|
+
options.maxIterations = parsePositiveInteger(requiredValue(argv, index, '--max-iterations'), '--max-iterations');
|
|
53
|
+
} else if (arg.startsWith('--max-iterations=')) {
|
|
54
|
+
options.maxIterations = parsePositiveInteger(arg.slice('--max-iterations='.length), '--max-iterations');
|
|
55
|
+
} else if (arg === '--schema-dir') {
|
|
56
|
+
index += 1;
|
|
57
|
+
options.schemaDir = requiredValue(argv, index, '--schema-dir');
|
|
58
|
+
} else if (arg.startsWith('--schema-dir=')) {
|
|
59
|
+
options.schemaDir = arg.slice('--schema-dir='.length);
|
|
60
|
+
} else if (arg === '--python') {
|
|
61
|
+
index += 1;
|
|
62
|
+
options.python = requiredValue(argv, index, '--python');
|
|
63
|
+
} else if (arg.startsWith('--python=')) {
|
|
64
|
+
options.python = arg.slice('--python='.length);
|
|
65
|
+
} else {
|
|
66
|
+
throw new Error(`unknown run option: ${arg}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return options;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function requiredValue(argv, index, flag) {
|
|
74
|
+
if (index >= argv.length || argv[index] === '') {
|
|
75
|
+
throw new Error(`${flag} requires a value`);
|
|
76
|
+
}
|
|
77
|
+
return argv[index];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parsePositiveInteger(value, flag) {
|
|
81
|
+
if (!/^[1-9][0-9]*$/.test(String(value))) {
|
|
82
|
+
throw new Error(`${flag} must be a positive integer`);
|
|
83
|
+
}
|
|
84
|
+
return Number(value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
parseRunArgs,
|
|
89
|
+
printRunHelp,
|
|
90
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
4
|
+
const MAX_TIMEOUT_MS = 60 * 60 * 1000;
|
|
5
|
+
const MAX_OUTPUT_BYTES = 256 * 1024;
|
|
6
|
+
const RUN_LOCK_NAME = '.clean-room-run.lock';
|
|
7
|
+
const RUN_LOCK_WAIT_MS = envPositiveInteger('CLEAN_ROOM_RUN_LOCK_WAIT_MS', 30_000);
|
|
8
|
+
const RUN_LOCK_POLL_MS = 100;
|
|
9
|
+
const RUN_HOOK_TIMEOUT_MS = envPositiveInteger('CLEAN_ROOM_RUN_HOOK_TIMEOUT_MS', 30_000);
|
|
10
|
+
const LEDGER_NAME = 'controller-run-ledger.json';
|
|
11
|
+
const RESULT_NAME = 'clean-room-result.json';
|
|
12
|
+
const STATUS_NAME = 'controller-status.json';
|
|
13
|
+
const CLEAN_RUN_CONTEXT_NAME = 'clean-run-context.json';
|
|
14
|
+
const HANDOFF_PACKAGE_NAME = 'handoff-package.json';
|
|
15
|
+
const POLISH_REPORT_NAME = 'polish-report.json';
|
|
16
|
+
const REQUIRED_COVERAGE_PHASE = 'contaminated-coverage-verify';
|
|
17
|
+
const POLISH_PHASE = 'clean-polish-review';
|
|
18
|
+
const PUBLIC_SURFACE_COMPLETION_LEVELS = new Set(['exact-public-contract', 'behavior-compatible']);
|
|
19
|
+
const MAX_LEDGER_ITERATIONS = 50;
|
|
20
|
+
|
|
21
|
+
const BASE_ENV_ALLOWLIST = Object.freeze([
|
|
22
|
+
'PATH',
|
|
23
|
+
'HOME',
|
|
24
|
+
'LANG',
|
|
25
|
+
'LC_ALL',
|
|
26
|
+
'LC_CTYPE',
|
|
27
|
+
'TMPDIR',
|
|
28
|
+
'TEMP',
|
|
29
|
+
'TMP',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const CI_ENV_ALLOWLIST = Object.freeze([
|
|
33
|
+
'CI',
|
|
34
|
+
'CONTINUOUS_INTEGRATION',
|
|
35
|
+
'BUILD_ID',
|
|
36
|
+
'BUILD_NUMBER',
|
|
37
|
+
'RUN_ID',
|
|
38
|
+
'TEAMCITY_VERSION',
|
|
39
|
+
'TF_BUILD',
|
|
40
|
+
'GITHUB_ACTIONS',
|
|
41
|
+
'GITHUB_ACTOR',
|
|
42
|
+
'GITHUB_EVENT_NAME',
|
|
43
|
+
'GITHUB_JOB',
|
|
44
|
+
'GITHUB_REF',
|
|
45
|
+
'GITHUB_REF_NAME',
|
|
46
|
+
'GITHUB_REF_TYPE',
|
|
47
|
+
'GITHUB_REPOSITORY',
|
|
48
|
+
'GITHUB_REPOSITORY_OWNER',
|
|
49
|
+
'GITHUB_RUN_ATTEMPT',
|
|
50
|
+
'GITHUB_RUN_ID',
|
|
51
|
+
'GITHUB_RUN_NUMBER',
|
|
52
|
+
'GITHUB_SHA',
|
|
53
|
+
'GITHUB_WORKFLOW',
|
|
54
|
+
'GITLAB_CI',
|
|
55
|
+
'CI_COMMIT_REF_NAME',
|
|
56
|
+
'CI_COMMIT_SHA',
|
|
57
|
+
'CI_JOB_ID',
|
|
58
|
+
'CI_PIPELINE_ID',
|
|
59
|
+
'CI_PROJECT_PATH',
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const HOOK_ONLY_ENV_ALLOWLIST = Object.freeze([
|
|
63
|
+
'CLEAN_ROOM_AUXILIARY_JSON_ALLOWLIST',
|
|
64
|
+
'CLEAN_ROOM_PRIVATE_IDENTIFIER_DENYLIST',
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
const ROLE_BY_PHASE = Object.freeze({
|
|
68
|
+
'contaminated-analysis': 'contaminated-source-analyst',
|
|
69
|
+
'sanitize-handoff': 'contaminated-handoff-sanitizer',
|
|
70
|
+
'clean-plan': 'clean-architect',
|
|
71
|
+
'clean-implement-qc': 'clean-qa-editor',
|
|
72
|
+
[POLISH_PHASE]: 'clean-polish-reviewer',
|
|
73
|
+
'contaminated-coverage-verify': 'contaminated-manager-verifier',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const TERMINAL_RESULTS = new Set([
|
|
77
|
+
'spec-slice-complete',
|
|
78
|
+
'spec-slice-blocked',
|
|
79
|
+
'spec-delta-required',
|
|
80
|
+
'contamination-suspected',
|
|
81
|
+
'iteration-limit-reached',
|
|
82
|
+
'no-progress-detected',
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
const VOLATILE_PROGRESS_KEYS = new Set([
|
|
86
|
+
'artifact_hashes',
|
|
87
|
+
'created_at',
|
|
88
|
+
'generated_at',
|
|
89
|
+
'recorded_at',
|
|
90
|
+
'returned_at',
|
|
91
|
+
'reviewed_at',
|
|
92
|
+
'started_at',
|
|
93
|
+
'updated_at',
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
const IMPLEMENTATION_IGNORE_NAMES = Object.freeze([
|
|
97
|
+
'.git',
|
|
98
|
+
'node_modules',
|
|
99
|
+
'target',
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
const SOURCE_DENIED_BRIEF_BLOCKED_NAMES = new Set([
|
|
103
|
+
'source-index.json',
|
|
104
|
+
'visual-index.json',
|
|
105
|
+
'coverage-ledger.json',
|
|
106
|
+
'evidence-ledger.json',
|
|
107
|
+
'task-manifest.json',
|
|
108
|
+
'init-config.json',
|
|
109
|
+
'preflight-goal.json',
|
|
110
|
+
'controller-status.json',
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const CLEAN_ROOM_ARTIFACT_PREFIXES = Object.freeze([
|
|
114
|
+
'behavior-spec',
|
|
115
|
+
'clean-room-result',
|
|
116
|
+
'clean-run-context',
|
|
117
|
+
'contamination-incident',
|
|
118
|
+
'controller-status',
|
|
119
|
+
'coverage-ledger',
|
|
120
|
+
'evidence-ledger',
|
|
121
|
+
'handoff-package',
|
|
122
|
+
'implementation-plan',
|
|
123
|
+
'implementation-report',
|
|
124
|
+
'init-config',
|
|
125
|
+
'polish-report',
|
|
126
|
+
'preflight-goal',
|
|
127
|
+
'qc-report',
|
|
128
|
+
'role-session-brief',
|
|
129
|
+
'skeleton-manifest',
|
|
130
|
+
'source-index',
|
|
131
|
+
'task-manifest',
|
|
132
|
+
'visual-index',
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
function envPositiveInteger(name, fallback) {
|
|
136
|
+
const value = process.env[name];
|
|
137
|
+
if (value === undefined || value === '') {
|
|
138
|
+
return fallback;
|
|
139
|
+
}
|
|
140
|
+
return /^[1-9][0-9]*$/.test(value) ? Number(value) : fallback;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = {
|
|
144
|
+
BASE_ENV_ALLOWLIST,
|
|
145
|
+
CI_ENV_ALLOWLIST,
|
|
146
|
+
CLEAN_ROOM_ARTIFACT_PREFIXES,
|
|
147
|
+
CLEAN_RUN_CONTEXT_NAME,
|
|
148
|
+
DEFAULT_TIMEOUT_MS,
|
|
149
|
+
HANDOFF_PACKAGE_NAME,
|
|
150
|
+
HOOK_ONLY_ENV_ALLOWLIST,
|
|
151
|
+
IMPLEMENTATION_IGNORE_NAMES,
|
|
152
|
+
LEDGER_NAME,
|
|
153
|
+
MAX_LEDGER_ITERATIONS,
|
|
154
|
+
MAX_OUTPUT_BYTES,
|
|
155
|
+
MAX_TIMEOUT_MS,
|
|
156
|
+
POLISH_PHASE,
|
|
157
|
+
POLISH_REPORT_NAME,
|
|
158
|
+
PUBLIC_SURFACE_COMPLETION_LEVELS,
|
|
159
|
+
REQUIRED_COVERAGE_PHASE,
|
|
160
|
+
RESULT_NAME,
|
|
161
|
+
ROLE_BY_PHASE,
|
|
162
|
+
RUN_HOOK_TIMEOUT_MS,
|
|
163
|
+
RUN_LOCK_NAME,
|
|
164
|
+
RUN_LOCK_POLL_MS,
|
|
165
|
+
RUN_LOCK_WAIT_MS,
|
|
166
|
+
SOURCE_DENIED_BRIEF_BLOCKED_NAMES,
|
|
167
|
+
STATUS_NAME,
|
|
168
|
+
TERMINAL_RESULTS,
|
|
169
|
+
VOLATILE_PROGRESS_KEYS,
|
|
170
|
+
envPositiveInteger,
|
|
171
|
+
};
|