clean-room-skill 0.3.0 → 0.4.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/README.md +2 -2
- package/docs/ARCHITECTURE.md +3 -1
- package/docs/REFERENCE.md +14 -12
- package/lib/bootstrap.cjs +64 -26
- package/lib/fs-utils.cjs +4 -0
- package/lib/install-cli.cjs +5 -0
- package/lib/install-options.cjs +1 -0
- package/lib/run-constants.cjs +5 -0
- package/lib/run-progress.cjs +3 -2
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/attended/SKILL.md +2 -2
- package/skills/clean-room/SKILL.md +2 -2
- package/skills/clean-room/references/PREFLIGHT.md +3 -3
- package/skills/clean-room/references/SPEC-SCHEMA.md +4 -4
- package/skills/init/SKILL.md +3 -3
- package/skills/unattended/SKILL.md +2 -2
package/README.md
CHANGED
|
@@ -107,9 +107,9 @@ Optionally create neutral external run folders and a clean-safe repository stub:
|
|
|
107
107
|
npx clean-room-skill@latest init
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
The default task root is `~/Documents/CleanRoom/<task-id>/` with `contaminated/`, `clean/`,
|
|
110
|
+
By default this creates a neutral clean-room project. The default task root is `~/Documents/CleanRoom/<project>/tasks/<task-id>/` with per-task `contaminated/`, `clean/`, and `quarantine/` children, plus one shared `~/Documents/CleanRoom/<project>/implementation/` root. Use `init --single-task` only when you need the legacy flat `~/Documents/CleanRoom/<task-id>/` layout.
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
To add another task to the same destination project, pass `init --project <name>` with the existing neutral project name. Plain `init` creates a new generated `proj-xxxxxxxx` project. Project names must stay neutral, like task IDs: a random word pair such as `amber-meadow` or a generated `proj-xxxxxxxx`, never derived from source folder names. Run at most one active task per project at a time because tasks share the implementation root; `clean-room-skill run` enforces this with an advisory `.clean-room-implementation.lock` in each implementation root.
|
|
113
113
|
|
|
114
114
|
In Claude Code, invoke skills with the plugin namespace:
|
|
115
115
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -62,7 +62,9 @@ To assist in logical unit decomposition, the workflow supports an optional sourc
|
|
|
62
62
|
|
|
63
63
|
Runtime installs and uninstalls serialize per target root with `.clean-room-install.lock`. The installer plans desired file changes from manifest hashes, then rechecks each managed file immediately before write or removal. Managed files that changed after planning are backed up before mutation.
|
|
64
64
|
|
|
65
|
-
The manifest is written with `phase: "installing"` after file copy and before hook config mutation. It is updated to `phase: "complete"` only after hook config succeeds. If hook config mutation fails, the installer records `hook_registration.status: "failed"` in the manifest when possible. A manifest left in `installing` is recoverable by re-running the same installer command. Bootstrap writes use atomic no-clobber creation unless `--force` is set.
|
|
65
|
+
The manifest is written with `phase: "installing"` after file copy and before hook config mutation. It is updated to `phase: "complete"` only after hook config succeeds. If hook config mutation fails, the installer records `hook_registration.status: "failed"` in the manifest when possible. A manifest left in `installing` is recoverable by re-running the same installer command. Bootstrap writes use atomic no-clobber creation unless `--force` is set. When `--force` adopts an existing project root that lacks or has invalid project metadata, the installer emits a warning so operators know they are reusing existing `tasks/` and `implementation/` content.
|
|
66
|
+
|
|
67
|
+
The implementation lock (`.clean-room-implementation.lock`) uses rename-based stale recovery: when a stale lock directory is detected, it is renamed to `<name>.stale.<ts>.<pid>` rather than deleted so the original can be inspected post-mortem. Implementation-root scans exclude both the active lock name and the stale prefix pattern to prevent orphaned stale lock directories from appearing in progress snapshots or misplaced-artifact checks.
|
|
66
68
|
|
|
67
69
|
---
|
|
68
70
|
|
package/docs/REFERENCE.md
CHANGED
|
@@ -165,6 +165,7 @@ npx clean-room-skill@latest init
|
|
|
165
165
|
npx clean-room-skill@latest init --target-dir . --target-profile speckit-feature-folder
|
|
166
166
|
npx clean-room-skill@latest init --artifact-base ~/Documents/CleanRoom --task-id task-1234abcd
|
|
167
167
|
npx clean-room-skill@latest init --project amber-meadow --task-id task-1234abcd
|
|
168
|
+
npx clean-room-skill@latest init --single-task --task-id task-1234abcd
|
|
168
169
|
```
|
|
169
170
|
|
|
170
171
|
Options:
|
|
@@ -174,22 +175,14 @@ Options:
|
|
|
174
175
|
| `--target-dir <path>` | Repository to initialize; default is current directory. |
|
|
175
176
|
| `--artifact-base <path>` | External CleanRoom base; default is `~/Documents/CleanRoom`. |
|
|
176
177
|
| `--task-id <id>` | Neutral task id; default is generated `task-xxxxxxxx`. |
|
|
177
|
-
| `--project <name>` | Group the task under a clean-room project; joins the project when it already exists. Names must be neutral (`[a-z0-9][a-z0-9-]{0,63}`, never derived from source or workspace folder names). |
|
|
178
|
-
| `--new-project` |
|
|
178
|
+
| `--project <name>` | Group the task under a named clean-room project; joins the project when it already exists. Names must be neutral (`[a-z0-9][a-z0-9-]{0,63}`, never derived from source or workspace folder names). |
|
|
179
|
+
| `--new-project` | Explicitly create a project with a generated `proj-xxxxxxxx` name. This is the default unless `--single-task` is passed. Cannot be combined with `--project`. |
|
|
180
|
+
| `--single-task` | Use the legacy flat layout under `<artifact-base>/<task-id>`. Cannot be combined with `--project` or `--new-project`. |
|
|
179
181
|
| `--target-profile <name>` | `openspec-delta`, `gsd-planning-package`, `speckit-feature-folder`, or `kiro-spec-folder`. |
|
|
180
182
|
| `--dry-run` | Print actions without writing files. |
|
|
181
183
|
| `--force` | Overwrite existing bootstrap metadata and repo stub. |
|
|
182
184
|
|
|
183
|
-
By default, `init` creates a
|
|
184
|
-
|
|
185
|
-
- `contaminated/`
|
|
186
|
-
- `clean/`
|
|
187
|
-
- `implementation/`
|
|
188
|
-
- `quarantine/`
|
|
189
|
-
- `clean-room-bootstrap.json`
|
|
190
|
-
- `.clean-room/README.md` in the target repository
|
|
191
|
-
|
|
192
|
-
With `--project` or `--new-project`, `init` creates a project layout instead. Tasks live under `<artifact-base>/<project>/tasks/<task-id>/` with per-task `contaminated/`, `clean/`, and `quarantine/`, while every task in the project shares one `<artifact-base>/<project>/implementation/` root:
|
|
185
|
+
By default, `init` creates a project layout. Plain `init` creates a new generated `proj-xxxxxxxx` project; pass `--project <name>` to add a task to an existing project. Tasks live under `<artifact-base>/<project>/tasks/<task-id>/` with per-task `contaminated/`, `clean/`, and `quarantine/`, while every task in the project shares one `<artifact-base>/<project>/implementation/` root:
|
|
193
186
|
|
|
194
187
|
```text
|
|
195
188
|
<artifact-base>/<project>/
|
|
@@ -203,6 +196,15 @@ With `--project` or `--new-project`, `init` creates a project layout instead. Ta
|
|
|
203
196
|
└── clean-room-bootstrap.json
|
|
204
197
|
```
|
|
205
198
|
|
|
199
|
+
With `--single-task`, `init` creates the legacy flat layout under `<artifact-base>/<task-id>/`:
|
|
200
|
+
|
|
201
|
+
- `contaminated/`
|
|
202
|
+
- `clean/`
|
|
203
|
+
- `implementation/`
|
|
204
|
+
- `quarantine/`
|
|
205
|
+
- `clean-room-bootstrap.json`
|
|
206
|
+
- `.clean-room/README.md` in the target repository
|
|
207
|
+
|
|
206
208
|
Re-running `init --project <name>` with a new task id joins the existing project without `--force`: the project metadata and shared `implementation/` are reused, and only the new task folders must not already exist. Because tasks share the implementation root, run at most one active task per project at a time; `clean-room-skill run` enforces this with an advisory `.clean-room-implementation.lock` in each implementation root.
|
|
207
209
|
|
|
208
210
|
Do not commit source roots, contaminated artifact paths, private identifiers, source-derived names, `preflight-goal.json`, `init-config.json`, `task-manifest.json`, `controller-status.json`, `role-session-brief.json`, or `clean-run-context.json` into the clean implementation repository.
|
package/lib/bootstrap.cjs
CHANGED
|
@@ -56,13 +56,16 @@ Options:
|
|
|
56
56
|
--target-dir <path> Repository to initialize (default: current directory)
|
|
57
57
|
--artifact-base <path> External CleanRoom base (default: ~/Documents/CleanRoom)
|
|
58
58
|
--task-id <id> Neutral task id (default: generated task-xxxxxxxx)
|
|
59
|
-
--project <name> Group this task under a clean-room project; joins the
|
|
59
|
+
--project <name> Group this task under a named clean-room project; joins the
|
|
60
60
|
project when it already exists. Project names must be
|
|
61
61
|
neutral ([a-z0-9][a-z0-9-]{0,63}) and never derived
|
|
62
62
|
from source or workspace folder names. Project layout:
|
|
63
63
|
<base>/<project>/tasks/<task-id> with one shared
|
|
64
64
|
<base>/<project>/implementation root for all tasks
|
|
65
|
-
--new-project
|
|
65
|
+
--new-project Explicitly create a project with a generated
|
|
66
|
+
proj-xxxxxxxx name (the default unless --single-task
|
|
67
|
+
is passed)
|
|
68
|
+
--single-task Use the legacy flat layout under <base>/<task-id>
|
|
66
69
|
--target-profile <name> openspec-delta, gsd-planning-package,
|
|
67
70
|
speckit-feature-folder, or kiro-spec-folder
|
|
68
71
|
(default: speckit-feature-folder)
|
|
@@ -79,6 +82,7 @@ function parseInitArgs(argv) {
|
|
|
79
82
|
taskId: null,
|
|
80
83
|
projectId: null,
|
|
81
84
|
newProject: false,
|
|
85
|
+
singleTask: false,
|
|
82
86
|
targetProfile: 'speckit-feature-folder',
|
|
83
87
|
dryRun: false,
|
|
84
88
|
force: false,
|
|
@@ -110,6 +114,8 @@ function parseInitArgs(argv) {
|
|
|
110
114
|
options.taskId = arg.slice('--task-id='.length);
|
|
111
115
|
} else if (arg === '--new-project') {
|
|
112
116
|
options.newProject = true;
|
|
117
|
+
} else if (arg === '--single-task') {
|
|
118
|
+
options.singleTask = true;
|
|
113
119
|
} else if (arg === '--project') {
|
|
114
120
|
i += 1;
|
|
115
121
|
options.projectId = requiredValue(argv, i, '--project');
|
|
@@ -130,6 +136,9 @@ function parseInitArgs(argv) {
|
|
|
130
136
|
if (options.projectId !== null && options.newProject) {
|
|
131
137
|
throw new Error('--project and --new-project cannot be combined');
|
|
132
138
|
}
|
|
139
|
+
if (options.singleTask && (options.projectId !== null || options.newProject)) {
|
|
140
|
+
throw new Error('--single-task cannot be combined with --project or --new-project');
|
|
141
|
+
}
|
|
133
142
|
|
|
134
143
|
return options;
|
|
135
144
|
}
|
|
@@ -148,10 +157,12 @@ function normalizeNameToken(value) {
|
|
|
148
157
|
function assertNeutralProjectId(projectId, targetDir) {
|
|
149
158
|
const projectToken = normalizeNameToken(projectId);
|
|
150
159
|
const targetToken = normalizeNameToken(path.basename(targetDir));
|
|
151
|
-
//
|
|
152
|
-
//
|
|
160
|
+
// Clause 2 (project contains workspace token) is the primary leakage vector and
|
|
161
|
+
// needs a lower minimum length so that short-but-meaningful workspace names (len 2+)
|
|
162
|
+
// are still caught. Clause 3 keeps the >=4 guard: a short project token included in
|
|
163
|
+
// a long workspace token is a weak signal and would produce too many false rejections.
|
|
153
164
|
const overlapping = projectToken === targetToken
|
|
154
|
-
|| (targetToken.length >=
|
|
165
|
+
|| (targetToken.length >= 2 && projectToken.includes(targetToken))
|
|
155
166
|
|| (projectToken.length >= 4 && targetToken.includes(projectToken));
|
|
156
167
|
if (overlapping) {
|
|
157
168
|
throw new Error('--project must be a neutral name; do not derive project names from workspace or source folder names');
|
|
@@ -170,7 +181,7 @@ function resolveInitOptions(options, env = process.env, homeDir = os.homedir())
|
|
|
170
181
|
const targetDir = path.resolve(expandTilde(options.targetDir, homeDir));
|
|
171
182
|
const artifactBase = path.resolve(expandTilde(options.artifactBase, homeDir));
|
|
172
183
|
|
|
173
|
-
const projectMode = options.
|
|
184
|
+
const projectMode = !options.singleTask;
|
|
174
185
|
const projectId = projectMode ? (options.projectId || generateProjectId()) : null;
|
|
175
186
|
if (projectMode) {
|
|
176
187
|
if (!PROJECT_ID_PATTERN.test(projectId)) {
|
|
@@ -252,18 +263,21 @@ function buildProjectMetadata(options) {
|
|
|
252
263
|
};
|
|
253
264
|
}
|
|
254
265
|
|
|
255
|
-
function renderRepoStub(
|
|
266
|
+
function renderRepoStub(options) {
|
|
267
|
+
const layoutDescription = options.projectId
|
|
268
|
+
? 'The bootstrap task root contains per-task `contaminated/`, `clean/`, and `quarantine/` directories. The project root contains the shared `implementation/` clean destination. Do not commit source roots, contaminated artifact paths, private identifiers, source-derived names, or active `init-config.json`, `task-manifest.json`, or `clean-run-context.json` files here.'
|
|
269
|
+
: 'The bootstrap task root contains `contaminated/`, `clean/`, `implementation/`, and `quarantine/`. Do not commit source roots, contaminated artifact paths, private identifiers, source-derived names, or active `init-config.json`, `task-manifest.json`, or `clean-run-context.json` files here.';
|
|
256
270
|
return `# Clean Room Bootstrap
|
|
257
271
|
|
|
258
272
|
This repository has a clean-room bootstrap stub.
|
|
259
273
|
|
|
260
|
-
Active clean-room run artifacts are stored outside this repository.
|
|
274
|
+
Active clean-room run artifacts are stored outside this repository. ${layoutDescription}
|
|
261
275
|
|
|
262
276
|
The final clean polish stage may create or update implementation-root \`AGENTS.md\`, \`.gitignore\`, and one local git commit through the bounded Agent 4 polish runner. That commit belongs to the clean implementation root, not to contaminated artifacts or source roots.
|
|
263
277
|
|
|
264
|
-
Default target profile: \`${targetProfile}\`
|
|
278
|
+
Default target profile: \`${options.targetProfile}\`
|
|
265
279
|
|
|
266
|
-
Start the runtime skill from your agent and provide the external
|
|
280
|
+
Start the runtime skill from your agent and provide the external task root printed by \`clean-room-skill init\`.
|
|
267
281
|
`;
|
|
268
282
|
}
|
|
269
283
|
|
|
@@ -282,6 +296,26 @@ function resolveExistingProject(options) {
|
|
|
282
296
|
throw new Error(`project root is not a directory: ${options.projectRoot}`);
|
|
283
297
|
}
|
|
284
298
|
if (options.force) {
|
|
299
|
+
// Warn when adopting a directory that lacks valid project metadata so the
|
|
300
|
+
// operator knows they are reusing existing tasks/ and implementation/ content.
|
|
301
|
+
if (!fs.existsSync(options.projectMetadataPath)) {
|
|
302
|
+
process.stderr.write(`warning: --force is adopting project root without ${PROJECT_METADATA_FILE}: ${options.projectRoot}\n`);
|
|
303
|
+
} else {
|
|
304
|
+
try {
|
|
305
|
+
const adoptErrors = [];
|
|
306
|
+
validateProjectMetadataObject(readJsonFile(options.projectMetadataPath, null), options.projectRoot, adoptErrors);
|
|
307
|
+
if (adoptErrors.length > 0) {
|
|
308
|
+
process.stderr.write(`warning: --force is adopting project root with invalid ${PROJECT_METADATA_FILE}:\n ${adoptErrors.join('\n ')}\n`);
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
process.stderr.write(`warning: --force is adopting project root with unreadable ${PROJECT_METADATA_FILE}: ${options.projectRoot}\n`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { mode: 'existing' };
|
|
315
|
+
}
|
|
316
|
+
// During dry-run the metadata is not written, so skip validation that would
|
|
317
|
+
// fail on an imperfect existing project root and just preview the layout.
|
|
318
|
+
if (options.dryRun) {
|
|
285
319
|
return { mode: 'existing' };
|
|
286
320
|
}
|
|
287
321
|
if (!fs.existsSync(options.projectMetadataPath)) {
|
|
@@ -376,6 +410,15 @@ function assertMetadataPath(metadata, field, expectedPath, errors, label = 'boot
|
|
|
376
410
|
}
|
|
377
411
|
}
|
|
378
412
|
|
|
413
|
+
function validateProjectIdValue(projectId, projectRoot, errors, label) {
|
|
414
|
+
if (!PROJECT_ID_PATTERN.test(projectId)) {
|
|
415
|
+
errors.push(`${label} project_id must match [a-z0-9][a-z0-9-]{0,63}`);
|
|
416
|
+
}
|
|
417
|
+
if (projectId !== path.basename(projectRoot)) {
|
|
418
|
+
errors.push(`${label} project_id must match the project root basename`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
379
422
|
function validateProjectMetadataObject(metadata, projectRoot, errors) {
|
|
380
423
|
const label = 'project metadata';
|
|
381
424
|
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
@@ -390,12 +433,7 @@ function validateProjectMetadataObject(metadata, projectRoot, errors) {
|
|
|
390
433
|
}
|
|
391
434
|
const projectId = expectMetadataString(metadata, 'project_id', errors, label);
|
|
392
435
|
if (projectId) {
|
|
393
|
-
|
|
394
|
-
errors.push(`${label} project_id must match [a-z0-9][a-z0-9-]{0,63}`);
|
|
395
|
-
}
|
|
396
|
-
if (projectId !== path.basename(projectRoot)) {
|
|
397
|
-
errors.push(`${label} project_id must match the project root basename`);
|
|
398
|
-
}
|
|
436
|
+
validateProjectIdValue(projectId, projectRoot, errors, label);
|
|
399
437
|
}
|
|
400
438
|
assertMetadataPath(metadata, 'project_root', projectRoot, errors, label);
|
|
401
439
|
assertMetadataPath(metadata, 'implementation_root', path.join(projectRoot, BOOTSTRAP_DIRS.implementation), errors, label);
|
|
@@ -446,8 +484,10 @@ function validateBootstrapScaffold(taskRoot) {
|
|
|
446
484
|
// project-level implementation root. Never trust the metadata paths
|
|
447
485
|
// directly: derive the project root from the task root location, then
|
|
448
486
|
// require the metadata to match the derived layout.
|
|
449
|
-
|
|
450
|
-
|
|
487
|
+
// Use metadata.layout === 'project' as the single authoritative signal:
|
|
488
|
+
// stray project_id/project_root fields on a flat task must not silently flip
|
|
489
|
+
// layout detection and redirect the shared implementation root.
|
|
490
|
+
const projectLayout = metadataIsObject && metadata.layout === 'project';
|
|
451
491
|
let projectRoot = null;
|
|
452
492
|
let projectId = null;
|
|
453
493
|
let projectMetadataPath = null;
|
|
@@ -466,12 +506,7 @@ function validateBootstrapScaffold(taskRoot) {
|
|
|
466
506
|
assertMetadataPath(metadata, 'project_root', projectRoot, errors);
|
|
467
507
|
projectId = expectMetadataString(metadata, 'project_id', errors);
|
|
468
508
|
if (projectId) {
|
|
469
|
-
|
|
470
|
-
errors.push('bootstrap metadata project_id must match [a-z0-9][a-z0-9-]{0,63}');
|
|
471
|
-
}
|
|
472
|
-
if (projectId !== path.basename(projectRoot)) {
|
|
473
|
-
errors.push('bootstrap metadata project_id must match the project root basename');
|
|
474
|
-
}
|
|
509
|
+
validateProjectIdValue(projectId, projectRoot, errors, 'bootstrap metadata');
|
|
475
510
|
}
|
|
476
511
|
try {
|
|
477
512
|
projectMetadataPath = assertManagedPath(projectRoot, PROJECT_METADATA_FILE);
|
|
@@ -620,7 +655,7 @@ function applyBootstrap(options) {
|
|
|
620
655
|
const metadata = `${JSON.stringify(buildBootstrapMetadata(options), null, 2)}\n`;
|
|
621
656
|
writeBootstrapFile(options.metadataPath, metadata, options.force);
|
|
622
657
|
if (options.force || !fs.existsSync(options.repoStubPath)) {
|
|
623
|
-
writeBootstrapFile(options.repoStubPath, renderRepoStub(options
|
|
658
|
+
writeBootstrapFile(options.repoStubPath, renderRepoStub(options), options.force);
|
|
624
659
|
}
|
|
625
660
|
}
|
|
626
661
|
printInitResult(options, projectState);
|
|
@@ -634,7 +669,7 @@ function printInitResult(options, projectState = { mode: 'none' }) {
|
|
|
634
669
|
console.log(` project: ${options.projectId} (${projectLabel})`);
|
|
635
670
|
console.log(` project root: ${options.projectRoot}`);
|
|
636
671
|
}
|
|
637
|
-
console.log(`
|
|
672
|
+
console.log(` task root: ${options.outputRoot}`);
|
|
638
673
|
console.log(` contaminated artifacts: ${options.roots.contaminated}`);
|
|
639
674
|
console.log(` clean artifacts: ${options.roots.clean}`);
|
|
640
675
|
if (options.projectId) {
|
|
@@ -648,6 +683,9 @@ function printInitResult(options, projectState = { mode: 'none' }) {
|
|
|
648
683
|
console.log(` project metadata: ${options.projectMetadataPath}`);
|
|
649
684
|
}
|
|
650
685
|
console.log(` repo stub: ${options.repoStubPath}`);
|
|
686
|
+
if (options.projectId && projectState.mode === 'existing') {
|
|
687
|
+
console.log(' note: shared repo stub kept from first task; --target-profile for this task is in per-task metadata');
|
|
688
|
+
}
|
|
651
689
|
console.log('');
|
|
652
690
|
console.log('Next steps:');
|
|
653
691
|
console.log(' Codex:');
|
package/lib/fs-utils.cjs
CHANGED
|
@@ -162,6 +162,7 @@ function listFiles(root, options = {}) {
|
|
|
162
162
|
return [];
|
|
163
163
|
}
|
|
164
164
|
const ignoreNames = new Set(options.ignoreNames || []);
|
|
165
|
+
const ignoreNamePrefixes = options.ignoreNamePrefixes || [];
|
|
165
166
|
const maxDepth = Number.isInteger(options.maxDepth) ? options.maxDepth : 64;
|
|
166
167
|
const maxFiles = Number.isInteger(options.maxFiles) ? options.maxFiles : 10000;
|
|
167
168
|
const files = [];
|
|
@@ -180,6 +181,9 @@ function listFiles(root, options = {}) {
|
|
|
180
181
|
if (ignoreNames.has(entry.name)) {
|
|
181
182
|
continue;
|
|
182
183
|
}
|
|
184
|
+
if (ignoreNamePrefixes.length > 0 && ignoreNamePrefixes.some((p) => entry.name.startsWith(p))) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
183
187
|
const fullPath = path.join(dir, entry.name);
|
|
184
188
|
const relPath = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
185
189
|
if (entry.isDirectory()) {
|
package/lib/install-cli.cjs
CHANGED
|
@@ -4,6 +4,7 @@ const { runInit } = require('./bootstrap.cjs');
|
|
|
4
4
|
const { runDoctor } = require('./doctor.cjs');
|
|
5
5
|
const { runPreflight } = require('./preflight.cjs');
|
|
6
6
|
const { parseRunArgs, runCleanRoom } = require('./run.cjs');
|
|
7
|
+
const { packageVersion } = require('./install-artifacts.cjs');
|
|
7
8
|
const { resolveInteractiveOptions } = require('./install-tui.cjs');
|
|
8
9
|
const {
|
|
9
10
|
operationForOptions,
|
|
@@ -22,6 +23,10 @@ const {
|
|
|
22
23
|
|
|
23
24
|
async function main() {
|
|
24
25
|
const argv = process.argv.slice(2);
|
|
26
|
+
if (argv.length === 1 && argv[0] === '--version') {
|
|
27
|
+
console.log(packageVersion());
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
25
30
|
if (argv[0] === 'init') {
|
|
26
31
|
runInit(argv.slice(1));
|
|
27
32
|
return;
|
package/lib/install-options.cjs
CHANGED
|
@@ -117,6 +117,7 @@ Options:
|
|
|
117
117
|
--dry-run Print actions without writing files
|
|
118
118
|
--yes Non-interactive mode; unknown conflicts still abort
|
|
119
119
|
--uninstall Remove manifest-managed files and clean-room hook entries
|
|
120
|
+
--version Print the installed clean-room-skill version
|
|
120
121
|
|
|
121
122
|
Run without runtime and scope flags for interactive install or uninstall.
|
|
122
123
|
Interactive runtime selection accepts names, numbers, ranges, all, or installed.
|
package/lib/run-constants.cjs
CHANGED
|
@@ -7,6 +7,10 @@ const RUN_LOCK_NAME = '.clean-room-run.lock';
|
|
|
7
7
|
const RUN_LOCK_WAIT_MS = envPositiveInteger('CLEAN_ROOM_RUN_LOCK_WAIT_MS', 30_000);
|
|
8
8
|
const RUN_LOCK_POLL_MS = 100;
|
|
9
9
|
const IMPLEMENTATION_LOCK_NAME = '.clean-room-implementation.lock';
|
|
10
|
+
// Stale lock recovery renames the lock dir to <name>.stale.<ts>.<pid> rather than
|
|
11
|
+
// removing it, so the original can be inspected post-mortem. Filter these orphans
|
|
12
|
+
// out of implementation-root scans with IMPLEMENTATION_LOCK_STALE_PREFIX.
|
|
13
|
+
const IMPLEMENTATION_LOCK_STALE_PREFIX = `${IMPLEMENTATION_LOCK_NAME}.stale.`;
|
|
10
14
|
const IMPLEMENTATION_LOCK_WAIT_MS = envPositiveInteger('CLEAN_ROOM_IMPLEMENTATION_LOCK_WAIT_MS', 30_000);
|
|
11
15
|
const IMPLEMENTATION_LOCK_POLL_MS = 100;
|
|
12
16
|
const RUN_HOOK_TIMEOUT_MS = envPositiveInteger('CLEAN_ROOM_RUN_HOOK_TIMEOUT_MS', 30_000);
|
|
@@ -157,6 +161,7 @@ module.exports = {
|
|
|
157
161
|
IMPLEMENTATION_IGNORE_NAMES,
|
|
158
162
|
IMPLEMENTATION_LOCK_NAME,
|
|
159
163
|
IMPLEMENTATION_LOCK_POLL_MS,
|
|
164
|
+
IMPLEMENTATION_LOCK_STALE_PREFIX,
|
|
160
165
|
IMPLEMENTATION_LOCK_WAIT_MS,
|
|
161
166
|
LEDGER_NAME,
|
|
162
167
|
MAX_LEDGER_ITERATIONS,
|
package/lib/run-progress.cjs
CHANGED
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
const {
|
|
13
13
|
CLEAN_ROOM_ARTIFACT_PREFIXES,
|
|
14
14
|
IMPLEMENTATION_IGNORE_NAMES,
|
|
15
|
+
IMPLEMENTATION_LOCK_STALE_PREFIX,
|
|
15
16
|
LEDGER_NAME,
|
|
16
17
|
STATUS_NAME,
|
|
17
18
|
VOLATILE_PROGRESS_KEYS,
|
|
@@ -36,7 +37,7 @@ function misplacedImplementationArtifacts(roots) {
|
|
|
36
37
|
const misplaced = [];
|
|
37
38
|
for (const root of roots.implementationRoots) {
|
|
38
39
|
if (!root || !fs.existsSync(root)) continue;
|
|
39
|
-
for (const relPath of listFiles(root, { ignoreNames: IMPLEMENTATION_IGNORE_NAMES })) {
|
|
40
|
+
for (const relPath of listFiles(root, { ignoreNames: IMPLEMENTATION_IGNORE_NAMES, ignoreNamePrefixes: [IMPLEMENTATION_LOCK_STALE_PREFIX] })) {
|
|
40
41
|
if (isCleanRoomArtifactName(path.basename(relPath))) {
|
|
41
42
|
misplaced.push(path.join(root, relPath));
|
|
42
43
|
}
|
|
@@ -115,7 +116,7 @@ function semanticProgressSnapshot(manifestPath, roots) {
|
|
|
115
116
|
}
|
|
116
117
|
roots.implementationRoots.forEach((root, rootIndex) => {
|
|
117
118
|
if (!root || !fs.existsSync(root)) return;
|
|
118
|
-
for (const relPath of listFiles(root, { ignoreNames: IMPLEMENTATION_IGNORE_NAMES })) {
|
|
119
|
+
for (const relPath of listFiles(root, { ignoreNames: IMPLEMENTATION_IGNORE_NAMES, ignoreNamePrefixes: [IMPLEMENTATION_LOCK_STALE_PREFIX] })) {
|
|
119
120
|
const filePath = path.join(root, relPath);
|
|
120
121
|
entries[`implementation:${rootIndex}:${relPath}`] = fileHash(filePath);
|
|
121
122
|
}
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/skills/attended/SKILL.md
CHANGED
|
@@ -20,8 +20,8 @@ Load or create `preflight-goal.json` first. Attended mode may continue with unre
|
|
|
20
20
|
Gather only required setup facts:
|
|
21
21
|
|
|
22
22
|
- Authorization statement, requester, allowed actions, prohibited actions, and evidence handling.
|
|
23
|
-
- Artifact base root, defaulting to `~/Documents/CleanRoom/<task-id>/`. If the user does not provide an explicitly approved neutral task ID, generate one as `task-` plus 8 lowercase hex characters. Do not derive task IDs or output directory names from source folder names.
|
|
24
|
-
-
|
|
23
|
+
- Artifact base root, defaulting the task root to `~/Documents/CleanRoom/<project>/tasks/<task-id>/`. If the user does not provide an explicitly approved neutral task ID, generate one as `task-` plus 8 lowercase hex characters. Do not derive task IDs or output directory names from source folder names.
|
|
24
|
+
- Project grouping, following the canonical `clean-room` project layout rules: `<base>/<project>/tasks/<task-id>/` with one shared `<base>/<project>/implementation/` root, a neutral project name (`proj-` plus 8 lowercase hex unless the user supplies an approved neutral name, matching `[a-z0-9][a-z0-9-]{0,63}`, never source-derived), and at most one active task per project. Use legacy flat `<base>/<task-id>/` roots only when the user explicitly chooses single-task compatibility.
|
|
25
25
|
- Source roots, contaminated artifact root, clean artifact root, clean implementation root, quarantine root, and optional public or destination reference roots.
|
|
26
26
|
- Target stack, destination constraints, dependency/license policy, exactness policy, feature policy, code hygiene policy, and output policy from `preflight-goal.json`.
|
|
27
27
|
- Target schema profile: `openspec-delta`, `gsd-planning-package`, `speckit-feature-folder`, or `kiro-spec-folder`.
|
|
@@ -98,8 +98,8 @@ Load or create `preflight-goal.json` only after this discovery step. Do not star
|
|
|
98
98
|
Gather only the setup facts needed to decide whether the workflow may start, or invoke `init` when the user wants a dedicated setup pass:
|
|
99
99
|
|
|
100
100
|
- Authorization statement, requester, allowed actions, prohibited actions, and evidence handling.
|
|
101
|
-
- Artifact base root. Default to `~/Documents/CleanRoom/<task-id>/`. If the user does not provide an explicitly approved neutral task ID, generate one as `task-` plus 8 lowercase hex characters. Do not derive task IDs or output directory names from source folder names.
|
|
102
|
-
-
|
|
101
|
+
- Artifact base root. Default the task root to `~/Documents/CleanRoom/<project>/tasks/<task-id>/`. If the user does not provide an explicitly approved neutral task ID, generate one as `task-` plus 8 lowercase hex characters. Do not derive task IDs or output directory names from source folder names.
|
|
102
|
+
- Project grouping. Default to the clean-room project layout: `<base>/<project>/tasks/<task-id>/` with one shared `<base>/<project>/implementation/` root for every task in the project. When the user does not supply an approved neutral project name, generate `proj-` plus 8 lowercase hex characters; it must match `[a-z0-9][a-z0-9-]{0,63}`, must never be derived from source or destination folder basenames or meaningful source-name tokens, and appears in paths clean roles can see. Use the legacy flat `<base>/<task-id>/` layout only when the user explicitly chooses single-task compatibility. Only one task per project may run at a time because tasks share the implementation root; the durable runner enforces this with an advisory `.clean-room-implementation.lock` in each implementation root.
|
|
103
103
|
- Source roots or fallback visual evidence roots, contaminated artifact root, clean artifact root, clean implementation root, quarantine root, and optional public or destination reference roots.
|
|
104
104
|
- Target stack and destination constraints from `preflight-goal.json`.
|
|
105
105
|
- Target schema profile: `openspec-delta`, `gsd-planning-package`, `speckit-feature-folder`, or `kiro-spec-folder`.
|
|
@@ -22,9 +22,9 @@ Ask only enough to fill `preflight-goal.json`:
|
|
|
22
22
|
|
|
23
23
|
Record every default as an assumption. Good defaults:
|
|
24
24
|
|
|
25
|
-
- Artifact base: `~/Documents/CleanRoom/<task-id>/`.
|
|
26
|
-
- Implementation root: `~/Documents/CleanRoom/<
|
|
27
|
-
-
|
|
25
|
+
- Artifact base: `~/Documents/CleanRoom/<project>/tasks/<task-id>/`.
|
|
26
|
+
- Implementation root: `~/Documents/CleanRoom/<project>/implementation/`.
|
|
27
|
+
- Single-task compatibility layout: task root `~/Documents/CleanRoom/<task-id>/` with implementation root `~/Documents/CleanRoom/<task-id>/implementation/`.
|
|
28
28
|
- Existing destination policy: `inspect-and-preserve`.
|
|
29
29
|
- Dependency policy: allow new dependencies, prefer standard library, require approval for native/system dependencies.
|
|
30
30
|
- Dependency licenses: allow MIT, Apache-2.0, BSD-2-Clause, and BSD-3-Clause; block GPL-3.0 and AGPL-3.0 unless the user explicitly approves otherwise.
|
|
@@ -98,16 +98,16 @@ Unattended mode requires `unattended_allowed_after_preflight: true`, finite `max
|
|
|
98
98
|
|
|
99
99
|
### Path Naming Guards
|
|
100
100
|
|
|
101
|
-
Default artifact roots live under `~/Documents/CleanRoom/<task-id
|
|
101
|
+
Default artifact roots live under `~/Documents/CleanRoom/<project>/tasks/<task-id>/` with a shared `~/Documents/CleanRoom/<project>/implementation/` root. If the user does not provide an explicitly approved neutral task ID, generate one as `task-` plus 8 lowercase hex characters. If the user does not provide an explicitly approved neutral project ID, generate one as `proj-` plus 8 lowercase hex characters. Do not use the source folder name as the task ID or project ID.
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
The legacy flat `~/Documents/CleanRoom/<task-id>/` layout remains valid only for explicit single-task compatibility. Project names follow the same neutrality rules as task IDs and are never derived from source folder names.
|
|
104
104
|
|
|
105
105
|
Clean artifact, contaminated artifact, and implementation roots must not contain source root basenames or meaningful non-generic tokens from those basenames. The environment preflight enforces this for `CLEAN_ROOM_CLEAN_ROOTS`, `CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS`, and `CLEAN_ROOM_IMPLEMENTATION_ROOTS`.
|
|
106
106
|
|
|
107
107
|
Capture:
|
|
108
108
|
|
|
109
|
-
- artifact base root, defaulting to `~/Documents/CleanRoom/<task-id>/` with
|
|
110
|
-
-
|
|
109
|
+
- artifact base root, defaulting to `~/Documents/CleanRoom/<project>/tasks/<task-id>/` with neutral project and task IDs
|
|
110
|
+
- `project_id` and `project_root` for the default project layout, omitted only for explicit single-task compatibility
|
|
111
111
|
- source roots or fallback visual evidence roots, contaminated artifact root, clean artifact root, clean implementation roots, quarantine root, and approved public references
|
|
112
112
|
- target profile
|
|
113
113
|
- default model plus optional clean, contaminated, or per-role overrides
|
package/skills/init/SKILL.md
CHANGED
|
@@ -19,7 +19,7 @@ Keep `preflight-goal.json` in the controller/contaminated artifact domain. Clean
|
|
|
19
19
|
|
|
20
20
|
Use the canonical `clean-room` skill workflow and references in this plugin. Preserve the clean-room boundary, role separation, artifact schemas, leakage rules, implementation-root rules, and hook expectations.
|
|
21
21
|
|
|
22
|
-
The CLI command `clean-room-skill init` (or `npx clean-room-skill@latest init` if the binary is not available) may have pre-created neutral external folders and a clean-safe `.clean-room/README.md` stub in the target repository. The
|
|
22
|
+
The CLI command `clean-room-skill init` (or `npx clean-room-skill@latest init` if the binary is not available) may have pre-created neutral external folders and a clean-safe `.clean-room/README.md` stub in the target repository. The default project layout places the task root at `<base>/<project>/tasks/<task-id>/` with per-task `contaminated/`, `clean/`, and `quarantine/`, plus a shared project-level `implementation/` and a `clean-room-project.json` metadata file at the project root. The legacy single-task root is created only when `--single-task` is passed and contains `contaminated/`, `clean/`, `implementation/`, and `quarantine/`. Treat that bootstrap output as convenience scaffolding only. It does not replace this skill's initialization workflow, and it must not be treated as an active `preflight-goal.json`, `init-config.json`, `task-manifest.json`, or `clean-run-context.json`.
|
|
23
23
|
|
|
24
24
|
When using an existing CLI bootstrap, check `clean-room-bootstrap.json`, `contaminated/`, `clean/`, `quarantine/`, the implementation root (task-level in the legacy layout, project-level in the project layout), and the target repo `.clean-room/README.md` before recording active init preferences. In the project layout also check `clean-room-project.json` and that the task root sits under the project's `tasks/` directory. Stop if metadata is missing, invalid, mismatched with the task root, or any generated path is missing or the wrong type. Do not infer active workflow state from those bootstrap files.
|
|
25
25
|
|
|
@@ -29,8 +29,8 @@ Collect only setup decisions that affect correctness, safety, resumability, or o
|
|
|
29
29
|
|
|
30
30
|
- Requester authorization, allowed actions, prohibited actions, and evidence handling.
|
|
31
31
|
- Source roots, contaminated artifact root, clean artifact root, clean implementation roots, quarantine root, and approved public or destination reference roots.
|
|
32
|
-
- Artifact base root. Default to `~/Documents/CleanRoom/<task-id>/`, never to the source workspace or a temporary directory unless the user explicitly chooses it. If the user does not provide an explicitly approved neutral task ID, generate one as `task-` plus 8 lowercase hex characters. Do not derive task IDs or output directory names from source folder names.
|
|
33
|
-
-
|
|
32
|
+
- Artifact base root. Default the task root to `~/Documents/CleanRoom/<project>/tasks/<task-id>/`, never to the source workspace or a temporary directory unless the user explicitly chooses it. If the user does not provide an explicitly approved neutral task ID, generate one as `task-` plus 8 lowercase hex characters. Do not derive task IDs or output directory names from source folder names.
|
|
33
|
+
- Project grouping. Default to a clean-room project with shared `~/Documents/CleanRoom/<project>/implementation/`. When adding a task to an existing destination project, record the user-supplied `project_id` and `project_root`; otherwise generate a neutral `proj-` plus 8 lowercase hex project id. Project names follow the same neutrality rules as task IDs, match `[a-z0-9][a-z0-9-]{0,63}`, and are never derived from source folder names. Record both fields in `init-config.json` and the manifest `initialization_snapshot`. Use the legacy flat `~/Documents/CleanRoom/<task-id>/` layout only when the user explicitly chooses single-task compatibility.
|
|
34
34
|
- Target schema profile: `openspec-delta`, `gsd-planning-package`, `speckit-feature-folder`, or `kiro-spec-folder`.
|
|
35
35
|
- Goal contract choices from `preflight-goal.json`, including target stack, dependency/license policy, exactness policy, feature policy, code hygiene, output policy, and controller mode.
|
|
36
36
|
- Default model plus optional overrides for contaminated roles, clean roles, or individual roles. Keep model ids as runtime-specific strings.
|
|
@@ -24,8 +24,8 @@ Do not assume target language, license policy, dependency policy, exactness poli
|
|
|
24
24
|
Gather only required setup facts:
|
|
25
25
|
|
|
26
26
|
- Authorization statement, requester, allowed actions, prohibited actions, and evidence handling.
|
|
27
|
-
- Artifact base root, defaulting to `~/Documents/CleanRoom/<task-id>/`. If the user does not provide an explicitly approved neutral task ID, generate one as `task-` plus 8 lowercase hex characters. Do not derive task IDs or output directory names from source folder names.
|
|
28
|
-
-
|
|
27
|
+
- Artifact base root, defaulting the task root to `~/Documents/CleanRoom/<project>/tasks/<task-id>/`. If the user does not provide an explicitly approved neutral task ID, generate one as `task-` plus 8 lowercase hex characters. Do not derive task IDs or output directory names from source folder names.
|
|
28
|
+
- Project grouping, following the canonical `clean-room` project layout rules: `<base>/<project>/tasks/<task-id>/` with one shared `<base>/<project>/implementation/` root, a neutral project name (`proj-` plus 8 lowercase hex unless the user supplies an approved neutral name, matching `[a-z0-9][a-z0-9-]{0,63}`, never source-derived), and at most one active task per project. Use legacy flat `<base>/<task-id>/` roots only when the user explicitly chooses single-task compatibility.
|
|
29
29
|
- Source roots, contaminated artifact root, clean artifact root, clean implementation root, quarantine root, and optional public or destination reference roots.
|
|
30
30
|
- Target stack, destination constraints, dependency/license policy, exactness policy, feature policy, code hygiene policy, and output policy from `preflight-goal.json`.
|
|
31
31
|
- Target schema profile: `openspec-delta`, `gsd-planning-package`, `speckit-feature-folder`, or `kiro-spec-folder`.
|