clean-room-skill 0.1.7 → 0.1.8

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.
Files changed (47) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +4 -1
  5. package/agents/clean-architect.md +2 -0
  6. package/agents/clean-implementer-verifier-shell.md +1 -0
  7. package/agents/clean-qa-editor.md +2 -0
  8. package/agents/contaminated-handoff-sanitizer.md +2 -0
  9. package/agents/contaminated-manager-verifier.md +3 -0
  10. package/agents/contaminated-source-analyst.md +2 -0
  11. package/bin/install.js +148 -9
  12. package/docs/ARCHITECTURE.md +14 -3
  13. package/docs/HOOKS.md +230 -0
  14. package/docs/REFERENCE.md +72 -2
  15. package/docs/assets/1.png +0 -0
  16. package/docs/assets/2.png +0 -0
  17. package/docs/assets/3.png +0 -0
  18. package/docs/assets/4.png +0 -0
  19. package/examples/codex/.codex/agents/clean-architect.toml +1 -0
  20. package/examples/codex/.codex/agents/clean-qa-editor.toml +1 -0
  21. package/examples/codex/.codex/agents/contaminated-handoff-sanitizer.toml +1 -0
  22. package/examples/codex/.codex/agents/contaminated-manager-verifier.toml +1 -0
  23. package/examples/codex/.codex/agents/contaminated-source-analyst.toml +1 -0
  24. package/hooks/check-artifact-leakage.py +6 -1
  25. package/hooks/deny-clean-source-read.py +18 -5
  26. package/hooks/validate-json-schema.py +68 -1
  27. package/lib/run.cjs +223 -8
  28. package/package.json +1 -1
  29. package/plugin.json +1 -1
  30. package/skills/attended/SKILL.md +1 -1
  31. package/skills/clean-room/SKILL.md +9 -4
  32. package/skills/clean-room/assets/clean-run-context.schema.json +58 -0
  33. package/skills/clean-room/assets/controller-status.schema.json +182 -0
  34. package/skills/clean-room/assets/role-session-brief.schema.json +203 -0
  35. package/skills/clean-room/assets/task-manifest.schema.json +58 -0
  36. package/skills/clean-room/examples/README.md +4 -0
  37. package/skills/clean-room/examples/contaminated-side/controller-status.json +22 -0
  38. package/skills/clean-room/examples/minimal-spec-package/role-session-brief.json +46 -0
  39. package/skills/clean-room/references/CONTROLLER-LOOP.md +4 -0
  40. package/skills/clean-room/references/LEAKAGE-RULES.md +1 -1
  41. package/skills/clean-room/references/PREFLIGHT.md +2 -0
  42. package/skills/clean-room/references/PROCESS.md +18 -2
  43. package/skills/clean-room/references/SPEC-SCHEMA.md +8 -0
  44. package/skills/init/SKILL.md +1 -1
  45. package/skills/refocus/SKILL.md +1 -0
  46. package/skills/resume/SKILL.md +4 -0
  47. package/skills/unattended/SKILL.md +1 -1
@@ -9,7 +9,7 @@
9
9
  "name": "clean-room",
10
10
  "source": "./",
11
11
  "description": "Spec-first clean-room workflow for authorized source analysis without replacement code.",
12
- "version": "0.1.7",
12
+ "version": "0.1.8",
13
13
  "author": {
14
14
  "name": "whit3rabbit"
15
15
  },
@@ -2,7 +2,7 @@
2
2
  "name": "clean-room",
3
3
  "displayName": "Clean Room",
4
4
  "description": "Spec-first clean-room workflow for authorized source analysis without replacement code.",
5
- "version": "0.1.7",
5
+ "version": "0.1.8",
6
6
  "author": {
7
7
  "name": "whit3rabbit"
8
8
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clean-room",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Spec-first clean-room workflow for authorized source analysis without replacement code.",
5
5
  "author": {
6
6
  "name": "whit3rabbit"
package/README.md CHANGED
@@ -45,7 +45,7 @@ npx clean-room-skill@latest --claude --global --yes
45
45
  npx clean-room-skill@latest --all --global --yes
46
46
  ```
47
47
 
48
- For edge cases such as ccsilo variants or modified Claude directories, add `--config-dir <path-to-claude-config-root>` to target that Claude config root explicitly.
48
+ For edge cases such as ccsilo variants or modified Claude directories, add `--config-dir <path-to-claude-config-root>` to target that Claude config root explicitly. If Claude is launched through a wrapper, set `CLEAN_ROOM_CLAUDE_EXECUTABLE=/absolute/path/to/wrapper`; the installer runs that exact executable and rejects relative, cwd-local, and `node_modules/.bin` paths.
49
49
 
50
50
  Claude global installs use Claude's plugin system for skills and agents, so entry points are namespaced as `/clean-room:init`, `/clean-room:preflight`, and `/clean-room`. The installer still manages hook files and migrates older standalone Claude skill copies out of the config root on reinstall or update.
51
51
 
@@ -108,6 +108,8 @@ npx clean-room-skill@latest run \
108
108
 
109
109
  The `run` command executes one bounded inner clean-room loop for an already approved spec slice. It does not replace the outer spec-development workflow.
110
110
 
111
+ In strict context-management mode, every `agent-commands.json` stage must set `context.fresh_session: true` and `context.brief_path`; see the runner adapter example in `docs/REFERENCE.md`.
112
+
111
113
  ## Typical Workflow
112
114
 
113
115
  ![Clean Room Architecture](assets/clean-room-arch.svg)
@@ -169,6 +171,7 @@ Reference files:
169
171
 
170
172
  - [docs/REFERENCE.md](docs/REFERENCE.md): CLI flags, hook modes, troubleshooting, and local verification.
171
173
  - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): operating model, roles, environment, guardrails, and flow details.
174
+ - [docs/HOOKS.md](docs/HOOKS.md): hook install locations, generated matchers, and per-hook behavior.
172
175
  - [skills/clean-room/references/PROCESS.md](skills/clean-room/references/PROCESS.md): detailed clean-room process.
173
176
  - [skills/clean-room/references/LEAKAGE-RULES.md](skills/clean-room/references/LEAKAGE-RULES.md): clean handoff rules.
174
177
 
@@ -18,6 +18,7 @@ Do not use shell-style tools in this role.
18
18
 
19
19
  Before planning, verify:
20
20
 
21
+ - `CLEAN_ROOM_SESSION_BRIEF_PATH`, when context management is enabled.
21
22
  - `clean-run-context.json` is present and valid.
22
23
  - `clean-run-context.json` includes clean-safe `goal_contract` fields and `code_hygiene_policy`.
23
24
  - approved `handoff-package.json` and approved behavior specs are present.
@@ -28,6 +29,7 @@ Stop if only a full `task-manifest.json`, full `preflight-goal.json`, source ind
28
29
  Responsibilities:
29
30
 
30
31
  - Treat `clean-run-context.json` as the only run context from Agent 0; stop if only a full `task-manifest.json` is provided.
32
+ - When `CLEAN_ROOM_SESSION_BRIEF_PATH` is set, read it first and load only the allowed artifact refs named there, plus destination foundation reads permitted by this role. Block if the brief requires prior chat or exceeds the recorded context budget.
31
33
  - Accept Agent 0 influence only as durable sanitized artifacts. Ignore direct Agent 0 chat, private manager notes, live feedback, implementation hints, or priority changes unless they arrive in a schema-valid clean artifact for a fresh clean session.
32
34
  - Merge only approved handoff artifacts into the selected clean schema base.
33
35
  - Read the clean destination foundation under `CLEAN_ROOM_IMPLEMENTATION_ROOTS` to identify local project structure, test conventions, public APIs, dependencies, and constraints.
@@ -18,6 +18,7 @@ Responsibilities:
18
18
 
19
19
  - Validate clean artifacts against the schema assets.
20
20
  - Validate `clean-run-context.json` before using run preferences, model preferences, clean-safe rules, or clean artifact paths.
21
+ - When `CLEAN_ROOM_SESSION_BRIEF_PATH` is set, read it first and load only the allowed artifact refs named there, plus implementation-root files permitted by this role. Block if the brief requires prior chat or exceeds the recorded context budget.
21
22
  - Accept Agent 0 influence only as durable sanitized artifacts already present in the clean workspace. Ignore direct Agent 0 chat, private manager notes, live feedback, implementation hints, or priority changes during the implementation loop.
22
23
  - Read `implementation-plan.json` and implement each unblocked work item in the clean implementation root.
23
24
  - Follow destination project conventions discovered from clean implementation files; do not import source-derived structure, names, comments, or pseudocode.
@@ -18,6 +18,7 @@ This default profile has no shell-style tools. If terminal verification is requi
18
18
 
19
19
  Before editing code, verify:
20
20
 
21
+ - `CLEAN_ROOM_SESSION_BRIEF_PATH`, when context management is enabled.
21
22
  - `clean-run-context.json` is present and valid.
22
23
  - `implementation-plan.json` is present and valid.
23
24
  - both artifacts carry the preflight-derived `code_hygiene_policy`.
@@ -28,6 +29,7 @@ Stop if asked to infer product goals from source, full `task-manifest.json`, ful
28
29
  Responsibilities:
29
30
 
30
31
  - Validate clean artifacts against the schema assets.
32
+ - When `CLEAN_ROOM_SESSION_BRIEF_PATH` is set, read it first and load only the allowed artifact refs named there, plus implementation-root files permitted by this role. Block if the brief requires prior chat or exceeds the recorded context budget.
31
33
  - Validate `clean-run-context.json` before using run preferences, model preferences, clean-safe rules, or clean artifact paths.
32
34
  - Accept Agent 0 influence only as durable sanitized artifacts already present in the clean workspace. Ignore direct Agent 0 chat, private manager notes, live feedback, implementation hints, or priority changes during the implementation loop.
33
35
  - Read `implementation-plan.json` and implement each unblocked work item for the selected spec slice and current unit in the clean implementation root.
@@ -22,12 +22,14 @@ Before reviewing drafts, verify that Agent 0 provided:
22
22
  - assigned draft artifact paths
23
23
  - schema directory
24
24
  - public compatibility allowlist, if public names are retained
25
+ - `CLEAN_ROOM_SESSION_BRIEF_PATH`, when context management is enabled
25
26
 
26
27
  Stop if given source roots, `source-index.json`, evidence ledgers, private identifier lists, full `preflight-goal.json`, full `task-manifest.json`, raw diffs, source excerpts, or Agent 1 source-reading chat history.
27
28
 
28
29
  Responsibilities:
29
30
 
30
31
  - Work only from Agent 0's neutral sanitizer brief and assigned draft artifact paths.
32
+ - When `CLEAN_ROOM_SESSION_BRIEF_PATH` is set, read it first and load only the brief's allowed artifact refs. Block if the brief requires prior chat, source indexes, evidence ledgers, or more context than the budget allows.
31
33
  - Reject any brief or artifact that includes source paths, import/export listings, dependency graphs, private identifiers, raw diffs, copied comments, source excerpts, `source-index.json` contents, or source-shaped pseudocode.
32
34
  - Scrub draft behavior specs into neutral handoff candidates without adding source facts.
33
35
  - Preserve public compatibility names only when they are listed in `public_surface` with a concrete compatibility reason.
@@ -28,6 +28,7 @@ Responsibilities:
28
28
  - Influence Agent 2 and Agent 3 only through durable sanitized artifacts. Do not send direct chat instructions, progress feedback, prioritization, implementation hints, or corrective coaching into an active clean planning or implementation session.
29
29
  - Record `controller_policy` when the task explicitly uses attended or bounded unattended mode. Missing policy means attended. Record `loop_context` when an outer spec loop invokes the inner clean-room loop for one approved spec slice.
30
30
  - Act as agent zero/controller when no separate coordinator exists: define and pass the clean-room environment block to every role session before tool use.
31
+ - When context management is enabled, maintain `controller-status.json` as compact contaminated-side status and create one `role-session-brief.json` per role launch. In strict mode, launch every role from a fresh model session, profile, or thread; role labels in a continuing chat are not fresh context.
31
32
  - Consume contaminated `source-index.json` when controller preflight produced one.
32
33
  - Split source scope into the durable tasklist as bounded `task-manifest.json` units with neutral ids that do not mirror private source layout. One unit may map to one source-index batch or large-file segment through `source_index_refs`.
33
34
  - Maintain `coverage-ledger.json` and `evidence-ledger.json` in the contaminated artifact workspace.
@@ -50,6 +51,8 @@ Every new role session must receive `CLEAN_ROOM_ROLE`, `CLEAN_ROOM_SOURCE_ROOTS`
50
51
 
51
52
  In unattended mode, reload durable artifacts before each iteration, select at most one pending or gap unit inside `loop_context.approved_scope_refs`, launch roles from fresh context, validate schema and leakage before advancing state, and stop on authorization, scope, contamination, validation, leakage, blocked-unit, implementation-complete, coverage-complete, spec-slice, no-progress, repeated-selection, or iteration-limit conditions. Do not use prior chat history as task state.
52
53
 
54
+ Role session briefs must contain only compact status, next action, allowed artifact refs with hashes, and forbidden inputs. Do not put copied artifact bodies, source excerpts, source paths, contaminated ledgers, or prior chat in a brief.
55
+
53
56
  Do not return to the outer spec loop merely because Agent 3 produced `implementation-report.json`. Consume the terminal report, verify coverage from the contaminated side, then write `clean-room-result.json`.
54
57
 
55
58
  Do not grant shell-style tools to Agent 0, Agent 1, Agent 1.5, Agent 2, or the default Agent 3 profile. Agent 3 terminal verification must use the installed verification runner with `CLEAN_ROOM_ALLOW_AGENT3_SHELL=1` and cwd under `CLEAN_ROOM_IMPLEMENTATION_ROOTS`.
@@ -22,12 +22,14 @@ Before reading source, verify that Agent 0 provided:
22
22
  - evidence handling policy
23
23
  - target stack and compatibility policy from preflight
24
24
  - neutral sanitizer brief requirements
25
+ - `CLEAN_ROOM_SESSION_BRIEF_PATH`, when context management is enabled
25
26
 
26
27
  Do not infer target language, dependency policy, license policy, or exactness policy from source code. Use the preflight goal contract.
27
28
 
28
29
  Responsibilities:
29
30
 
30
31
  - Read the minimum source needed for the assigned unit.
32
+ - When `CLEAN_ROOM_SESSION_BRIEF_PATH` is set, read it first and load only the allowed artifact refs named there, except for direct source reads already permitted by the assigned unit and role policy.
31
33
  - When the unit has `source_index_refs`, stay within the referenced batch unless Agent 0 explicitly assigns a related gap.
32
34
  - Generate neutral draft task slices and behavioral spec material for Agent 0-controlled units.
33
35
  - Write neutral behavioral requirements covering inputs, outputs, state transitions, edge cases, error conditions, invariants, and tests.
package/bin/install.js CHANGED
@@ -41,6 +41,7 @@ const INSTALL_LOCK_WAIT_MS = envPositiveInteger('CLEAN_ROOM_INSTALL_LOCK_WAIT_MS
41
41
  const INSTALL_LOCK_POLL_MS = 100;
42
42
  const PYTHON_PROBE_TIMEOUT_MS = envPositiveInteger('CLEAN_ROOM_INSTALL_PYTHON_TIMEOUT_MS', 10_000);
43
43
  const CLAUDE_PLUGIN_TIMEOUT_MS = envPositiveInteger('CLEAN_ROOM_INSTALL_CLAUDE_PLUGIN_TIMEOUT_MS', 120_000);
44
+ const CLAUDE_EXECUTABLE_ENV = 'CLEAN_ROOM_CLAUDE_EXECUTABLE';
44
45
  const CLAUDE_PLUGIN_MARKETPLACE_NAME = 'clean-room-skill';
45
46
  const CLAUDE_PLUGIN_NAME = 'clean-room';
46
47
  const CLAUDE_PLUGIN_ID = `${CLAUDE_PLUGIN_NAME}@${CLAUDE_PLUGIN_MARKETPLACE_NAME}`;
@@ -751,19 +752,155 @@ function truncateCommandOutput(value) {
751
752
  return `${text.slice(0, 2000)}...`;
752
753
  }
753
754
 
754
- function claudePluginEnv(layout) {
755
+ function pathIsUnder(candidate, root) {
756
+ return candidate === root || candidate.startsWith(`${root}${path.sep}`);
757
+ }
758
+
759
+ function currentWorkingRoots() {
760
+ const cwd = path.resolve(process.cwd());
761
+ const roots = [cwd];
762
+ try {
763
+ const real = fs.realpathSync.native(cwd);
764
+ if (!roots.includes(real)) roots.push(real);
765
+ } catch {
766
+ // Keep the resolved cwd as the policy root if realpath is unavailable.
767
+ }
768
+ return roots;
769
+ }
770
+
771
+ function pathIsUnderAny(candidate, roots) {
772
+ return roots.some((root) => pathIsUnder(candidate, root));
773
+ }
774
+
775
+ function pathContainsNodeModulesBin(candidate) {
776
+ const parts = path.resolve(candidate).split(path.sep);
777
+ for (let i = 0; i < parts.length - 1; i += 1) {
778
+ if (parts[i] === 'node_modules' && parts[i + 1] === '.bin') return true;
779
+ }
780
+ return false;
781
+ }
782
+
783
+ function unsafeClaudeExecutableReason(filePath, label) {
784
+ if (!filePath || typeof filePath !== 'string' || !path.isAbsolute(filePath)) {
785
+ return `${label} must be an absolute path`;
786
+ }
787
+ const resolved = path.resolve(filePath);
788
+ const cwdRoots = currentWorkingRoots();
789
+ if (pathIsUnderAny(resolved, cwdRoots)) {
790
+ return `${label} must not be under the current working directory`;
791
+ }
792
+ if (pathContainsNodeModulesBin(resolved)) {
793
+ return `${label} must not be under node_modules/.bin`;
794
+ }
795
+ let real;
796
+ try {
797
+ real = fs.realpathSync.native(resolved);
798
+ } catch {
799
+ return `${label} must resolve to an executable file`;
800
+ }
801
+ if (pathIsUnderAny(real, cwdRoots)) {
802
+ return `${label} target must not be under the current working directory`;
803
+ }
804
+ if (pathContainsNodeModulesBin(real)) {
805
+ return `${label} target must not be under node_modules/.bin`;
806
+ }
807
+ try {
808
+ const stat = fs.statSync(real);
809
+ fs.accessSync(real, fs.constants.X_OK);
810
+ if (!stat.isFile()) {
811
+ return `${label} must be an executable regular file`;
812
+ }
813
+ } catch {
814
+ return `${label} must be an executable regular file`;
815
+ }
816
+ return null;
817
+ }
818
+
819
+ function assertClaudeExecutable(filePath, label) {
820
+ const reason = unsafeClaudeExecutableReason(filePath, label);
821
+ if (reason) throw new Error(reason);
822
+ return path.resolve(filePath);
823
+ }
824
+
825
+ function sanitizedPathEntriesForClaude(value) {
826
+ const entries = String(value || '').split(path.delimiter).filter(Boolean);
827
+ const cwdRoots = currentWorkingRoots();
828
+ const seen = new Set();
829
+ return entries.filter((entry) => {
830
+ if (!path.isAbsolute(entry)) return false;
831
+ const normalized = path.resolve(entry);
832
+ if (pathIsUnderAny(normalized, cwdRoots)) return false;
833
+ try {
834
+ if (pathIsUnderAny(fs.realpathSync.native(normalized), cwdRoots)) return false;
835
+ } catch {
836
+ // Nonexistent PATH entries cannot provide claude; leave candidate validation to fail later.
837
+ }
838
+ if (pathContainsNodeModulesBin(normalized)) return false;
839
+ if (seen.has(normalized)) return false;
840
+ seen.add(normalized);
841
+ return true;
842
+ });
843
+ }
844
+
845
+ function sanitizePathForClaude(value) {
846
+ return sanitizedPathEntriesForClaude(value).join(path.delimiter);
847
+ }
848
+
849
+ function resolveClaudeExecutable() {
850
+ const configuredExecutable = process.env[CLAUDE_EXECUTABLE_ENV];
851
+ const searchPath = sanitizePathForClaude(process.env.PATH);
852
+ if (configuredExecutable) {
853
+ return {
854
+ executable: assertClaudeExecutable(configuredExecutable, CLAUDE_EXECUTABLE_ENV),
855
+ searchPath,
856
+ };
857
+ }
858
+
859
+ const entries = sanitizedPathEntriesForClaude(process.env.PATH);
860
+ if (entries.length === 0) {
861
+ throw new Error(`Claude plugin command requires ${CLAUDE_EXECUTABLE_ENV} or a non-empty sanitized PATH`);
862
+ }
863
+
864
+ const candidates = [];
865
+ const seenCandidates = new Set();
866
+ for (const entry of entries) {
867
+ const candidate = path.join(entry, 'claude');
868
+ if (unsafeClaudeExecutableReason(candidate, 'Claude executable')) continue;
869
+ const resolved = path.resolve(candidate);
870
+ let realCandidate;
871
+ try {
872
+ realCandidate = fs.realpathSync.native(resolved);
873
+ } catch {
874
+ continue;
875
+ }
876
+ if (seenCandidates.has(realCandidate)) continue;
877
+ seenCandidates.add(realCandidate);
878
+ candidates.push(resolved);
879
+ }
880
+
881
+ if (candidates.length === 1) {
882
+ return { executable: candidates[0], searchPath };
883
+ }
884
+ if (candidates.length > 1) {
885
+ throw new Error(`Claude plugin command found multiple claude executables on sanitized PATH; set ${CLAUDE_EXECUTABLE_ENV} to the intended absolute executable`);
886
+ }
887
+ throw new Error(`Claude plugin command requires ${CLAUDE_EXECUTABLE_ENV} or a claude executable on sanitized PATH`);
888
+ }
889
+
890
+ function claudePluginEnv(layout, searchPath) {
755
891
  return {
756
892
  ...process.env,
893
+ PATH: searchPath,
757
894
  CLAUDE_CONFIG_DIR: layout.targetRoot,
758
895
  };
759
896
  }
760
897
 
761
- function claudeCommandLabel(args) {
762
- return ['claude', ...args].join(' ');
898
+ function claudeCommandLabel(command, args) {
899
+ return [command, ...args].join(' ');
763
900
  }
764
901
 
765
- function claudePluginCommandFailure(args, result) {
766
- const parts = [`Claude plugin command failed: ${claudeCommandLabel(args)}`];
902
+ function claudePluginCommandFailure(command, args, result) {
903
+ const parts = [`Claude plugin command failed: ${claudeCommandLabel(command, args)}`];
767
904
  if (result.error) {
768
905
  parts.push(result.error.message);
769
906
  }
@@ -781,14 +918,16 @@ function claudePluginCommandFailure(args, result) {
781
918
  }
782
919
 
783
920
  function runClaudePluginCommand(layout, args, options = {}) {
784
- const result = spawnSync('claude', args, {
921
+ const { executable: claudeExecutable, searchPath } = resolveClaudeExecutable();
922
+ const result = spawnSync(claudeExecutable, args, {
785
923
  encoding: 'utf8',
786
- env: claudePluginEnv(layout),
924
+ env: claudePluginEnv(layout, searchPath),
787
925
  stdio: ['ignore', 'pipe', 'pipe'],
788
926
  timeout: CLAUDE_PLUGIN_TIMEOUT_MS,
789
927
  });
928
+ result.command = claudeExecutable;
790
929
  if (result.error || result.status !== 0) {
791
- throw new Error(claudePluginCommandFailure(args, result));
930
+ throw new Error(claudePluginCommandFailure(claudeExecutable, args, result));
792
931
  }
793
932
  if (!options.silent) {
794
933
  if (result.stdout) process.stdout.write(result.stdout);
@@ -804,7 +943,7 @@ function readClaudePluginJson(layout, args) {
804
943
  return Array.isArray(parsed) ? parsed : [];
805
944
  } catch (err) {
806
945
  throw new Error(
807
- `Claude plugin command returned invalid JSON: ${claudeCommandLabel(args)}; ` +
946
+ `Claude plugin command returned invalid JSON: ${claudeCommandLabel(result.command || 'claude', args)}; ` +
808
947
  `stdout: ${truncateCommandOutput(result.stdout)}; ${err.message}`
809
948
  );
810
949
  }
@@ -14,11 +14,13 @@ The Clean Room workflow acts as an engineering risk-reduction process by establi
14
14
 
15
15
  ## Operating Model
16
16
 
17
+ ![Operating Model](assets/1.png)
18
+
17
19
  To maintain compliance and mitigate leakage risks, the workflow utilizes strictly separated workspaces, worktrees, repositories, or profiles for contaminated and clean work:
18
20
 
19
21
  * **Contaminated Source Workspace**: Source-readable, read-only where practical. Contains the codebase under analysis.
20
- * **Contaminated Artifact Workspace**: Holds intermediate outputs like preflight goals, init configs, source indexes, task manifests, coverage ledgers, evidence ledgers, draft specs, and abstract delta tickets. Configure via `CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS`.
21
- * **Clean Artifact Workspace**: Houses sanitized clean run contexts, approved behavioral specifications, handoff packages, skeleton manifests, implementation plans, implementation reports, QC reports, and test plans. Configure via `CLEAN_ROOM_CLEAN_ROOTS`.
22
+ * **Contaminated Artifact Workspace**: Holds intermediate outputs like preflight goals, init configs, source indexes, task manifests, controller status, coverage ledgers, evidence ledgers, draft specs, contaminated-role session briefs, and abstract delta tickets. Configure via `CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS`.
23
+ * **Clean Artifact Workspace**: Houses sanitized clean run contexts, approved behavioral specifications, handoff packages, clean-role session briefs, skeleton manifests, implementation plans, implementation reports, QC reports, and test plans. Configure via `CLEAN_ROOM_CLEAN_ROOTS`.
22
24
  * **Clean Implementation Workspace**: Houses clean destination code and tests. Configure via `CLEAN_ROOM_IMPLEMENTATION_ROOTS`.
23
25
  * **Clean Allowed Reference Workspace**: Public documentation, specifications, or destination constraints explicitly approved for clean and source-denied role reads. Configure via `CLEAN_ROOM_ALLOWED_READ_ROOTS`.
24
26
 
@@ -31,10 +33,14 @@ Optional Docker or Podman support is limited to Agent 3 verification containers.
31
33
 
32
34
  Artifact roots must not disclose private source names. New runs default to `~/Documents/CleanRoom/<task-id>/`; when no explicitly approved neutral task ID is provided, the controller generates `task-` plus 8 lowercase hex characters instead of using the source folder name.
33
35
 
36
+ ![Artifact Roots](assets/2.png)
37
+
34
38
  The initialization wizard and `require-clean-room-env.py` audit clean, implementation, and contaminated artifact root names. They fail closed when a path contains a source root basename or meaningful non-generic tokens from that basename, while filtering generic terms such as `src`, `app`, `test`, `repo`, and `workspace`.
35
39
 
36
40
  ### Stage 0 Goal Contract
37
41
 
42
+ ![Stage 0 Goal Contract](assets/3.png)
43
+
38
44
  Every new run starts with `preflight-goal.json` before source discovery, source indexing, Agent 0 decomposition, attended execution, or unattended execution. The contract records end goal, target stack, license policy, dependency policy, compatibility exactness, feature changes, code hygiene, output policy, controller mode, and open questions.
39
45
 
40
46
  `preflight-goal.json` is controller/contaminated-side only. Clean roles receive only clean-safe `goal_contract` fields and `code_hygiene_policy` through `clean-run-context.json`.
@@ -217,6 +223,8 @@ The architecture delegates work across five distinct custom role agents to enfor
217
223
 
218
224
  ### Nested Controller Loop
219
225
 
226
+ ![Nested Controller Loop](assets/4.png)
227
+
220
228
  The outer loop owns spec development: scope, behavior specs, acceptance criteria, and abstract delta resolution. The inner clean-room loop owns one approved spec slice. It repeats analyze, sanitize, plan, implement, QC, and contaminated-side coverage verification until it can return `spec-slice-complete`, `spec-slice-blocked`, `spec-delta-required`, `contamination-suspected`, `iteration-limit-reached`, or `no-progress-detected`.
221
229
 
222
230
  Agent 3's terminal report is not enough to return. Agent 0 must consume that report, verify contaminated-side coverage, and then write `clean-room-result.json`.
@@ -227,11 +235,12 @@ Agent 3's terminal report is not enough to return. Agent 0 must consume that rep
227
235
  * Reloads durable artifacts before each iteration.
228
236
  * Selects at most one pending or gap unit inside `loop_context.approved_scope_refs`.
229
237
  * Spawns configured role commands with `shell: false`, bounded output, and bounded timeout.
238
+ * In strict context-management mode, requires each configured stage to provide `context.fresh_session: true` and `context.brief_path`, then validates the session brief before spawn.
230
239
  * Validates schema, leakage, and handoff integrity before advancing state.
231
240
  * Records controller memory in contaminated-side `controller-run-ledger.json`.
232
241
  * Writes `clean-room-result.json` before returning to the outer spec loop.
233
242
 
234
- Progress is durable-artifact based. `clean-room-skill run` compares semantic JSON artifact hashes that ignore volatile timestamp and artifact-hash fields, plus raw file hashes under implementation roots. Chat output alone and timestamp-only artifact churn do not count as progress.
243
+ Progress is durable-artifact based. `clean-room-skill run` compares semantic JSON artifact hashes that ignore volatile timestamp and artifact-hash fields, plus raw file hashes under implementation roots. Chat output, timestamp-only artifact churn, and `controller-status.json` updates alone do not count as progress.
235
244
 
236
245
  ---
237
246
 
@@ -247,6 +256,8 @@ Every clean-room role session requires a populated environment block before any
247
256
  * `CLEAN_ROOM_ALLOWED_READ_ROOTS`: Approved reference docs or constraints readable by clean and source-denied roles.
248
257
  * `CLEAN_ROOM_SCHEMA_DIR`: Path to the directory containing JSON schema assets.
249
258
 
259
+ When context management is enabled, role sessions also receive `CLEAN_ROOM_SESSION_BRIEF_PATH`, `CLEAN_ROOM_ROLE_SESSION_ID`, and, in strict mode, `CLEAN_ROOM_FRESH_CONTEXT_REQUIRED=1`. The brief is the low-context launch packet: compact status, next action, allowed artifact refs with SHA-256, and forbidden inputs. `controller-status.json` stays contaminated-side and is not clean input.
260
+
250
261
  Note: Even though clean and source-denied roles (such as Agent 1.5, 2, and 3) are restricted from accessing contaminated or source workspaces, they must still be configured with the full environment block. The hook guardrails require these paths to validate that tool inputs do not cross-pollinate or violate boundary constraints.
251
262
 
252
263
  ---
package/docs/HOOKS.md ADDED
@@ -0,0 +1,230 @@
1
+ # Clean Room Hooks
2
+
3
+ This page documents the hook guardrails installed by `clean-room-skill`: where they are installed, which runtime hook entries are generated, and what each hook script checks.
4
+
5
+ The hooks are engineering guardrails. They reduce accidental cross-domain reads and writes, but they are not legal advice, a sandbox boundary, or proof that a host runtime emits every event needed for enforcement.
6
+
7
+ ## Install Locations
8
+
9
+ The installer copies the Python hook files for every supported runtime layout. Runtime hook registration is verified only for Codex and Claude Code.
10
+
11
+ | Runtime | Hook files copied to | Active hook config |
12
+ | --- | --- | --- |
13
+ | Codex | `<targetRoot>/hooks/clean-room/*.py` | `<targetRoot>/hooks.json` |
14
+ | Claude Code | `<targetRoot>/hooks/clean-room/*.py` | `<targetRoot>/settings.json` |
15
+ | Antigravity | `<targetRoot>/hooks/clean-room/*.py` | Unsupported, copy only |
16
+ | Gemini CLI | `<targetRoot>/hooks/clean-room/*.py` | Unsupported, copy only |
17
+ | OpenCode | `<targetRoot>/hooks/clean-room/*.py` | Unsupported, copy only |
18
+ | Kilo | `<targetRoot>/hooks/clean-room/*.py` | Unsupported, copy only |
19
+ | Cursor | `<targetRoot>/hooks/clean-room/*.py` | Unsupported, copy only |
20
+ | GitHub Copilot | `<targetRoot>/hooks/clean-room/*.py` | Unsupported, copy only |
21
+ | Windsurf | `<targetRoot>/hooks/clean-room/*.py` | Unsupported, copy only |
22
+ | Augment | `<targetRoot>/hooks/clean-room/*.py` | Unsupported, copy only |
23
+ | Trae | `<targetRoot>/hooks/clean-room/*.py` | Unsupported, copy only |
24
+ | Qwen Code | `<targetRoot>/hooks/clean-room/*.py` | Unsupported, copy only |
25
+ | Hermes Agent | `<targetRoot>/hooks/clean-room/*.py` | Unsupported, copy only |
26
+ | CodeBuddy | `<targetRoot>/hooks/clean-room/*.py` | Unsupported, copy only |
27
+
28
+ Codex uses `CODEX_HOME` or `~/.codex` for global installs. Claude Code uses `CLAUDE_CONFIG_DIR` or `~/.claude`. Other runtime roots are listed in [REFERENCE.md](REFERENCE.md#runtime-support).
29
+
30
+ ## Hook Modes
31
+
32
+ | Mode | Behavior |
33
+ | --- | --- |
34
+ | `safe` | Default. Registers hooks for Codex or Claude, but `clean-room-hook.py` no-ops until a clean-room role environment is present or `CLEAN_ROOM_HOOK_ENFORCE` is truthy. |
35
+ | `strict` | Registers hooks for Codex or Claude and fails closed even without clean-room role environment. Use only in dedicated clean-room runtime homes. |
36
+ | `copy-only` | Copies hook files without modifying runtime hook config. This is also the effective behavior for runtimes without verified hook registration support. |
37
+
38
+ `--no-hooks` is an alias for `--hooks=copy-only`.
39
+
40
+ ## Generated Runtime Hooks
41
+
42
+ When hook mode is `safe` or `strict`, the installer registers four managed hook entries for Codex and Claude. Each entry invokes the installed `clean-room-hook.py` wrapper with an absolute Python path, an absolute wrapper path, the requested hook mode, and one or more `--check` scripts.
43
+
44
+ | Event | Matcher | Checks |
45
+ | --- | --- | --- |
46
+ | `PreToolUse` | <code>Bash&#124;Shell&#124;PowerShell&#124;Monitor&#124;exec_command&#124;shell_command&#124;write_stdin</code> | `require-clean-room-env.py`, `deny-clean-room-shell.py` |
47
+ | `PreToolUse` | <code>Read&#124;Glob&#124;Grep&#124;LS&#124;LSP&#124;NotebookRead&#124;view_image&#124;list_dir&#124;ListMcpResourcesTool&#124;ReadMcpResourceTool&#124;ListMcpResourceTemplatesTool&#124;list_mcp_resources&#124;list_mcp_resource_templates&#124;read_mcp_resource</code> | `require-clean-room-env.py`, `deny-clean-source-read.py` |
48
+ | `PreToolUse` | <code>Write&#124;Edit&#124;MultiEdit&#124;NotebookEdit&#124;apply_patch</code> | `require-clean-room-env.py`, `deny-contaminated-clean-write.py` |
49
+ | `PostToolUse` | <code>Write&#124;Edit&#124;MultiEdit&#124;NotebookEdit&#124;apply_patch</code> | `require-clean-room-env.py`, `check-artifact-leakage.py`, `validate-json-schema.py`, `validate-handoff-package.py` |
50
+
51
+ Matcher names only matter if the host runtime emits the corresponding hook event. A host tool that reads files without a hook event is not protected by listing its name here.
52
+
53
+ ## Required Environment
54
+
55
+ Role sessions must provide the full clean-room environment block so the hooks can validate boundaries:
56
+
57
+ | Variable | Use |
58
+ | --- | --- |
59
+ | `CLEAN_ROOM_ROLE` | Active role. Valid roles are `contaminated-manager-verifier`, `contaminated-source-analyst`, `contaminated-handoff-sanitizer`, `clean-architect`, and `clean-qa-editor`. |
60
+ | `CLEAN_ROOM_SOURCE_ROOTS` | Authorized source roots. Clean and source-denied roles must not read these. |
61
+ | `CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS` | Write roots for contaminated roles. |
62
+ | `CLEAN_ROOM_CLEAN_ROOTS` | Write roots for clean artifacts and reports. |
63
+ | `CLEAN_ROOM_IMPLEMENTATION_ROOTS` | Write roots for Agent 3 implementation code and tests. |
64
+ | `CLEAN_ROOM_ALLOWED_READ_ROOTS` | Approved clean-safe reference roots for clean and source-denied roles. |
65
+ | `CLEAN_ROOM_SCHEMA_DIR` | JSON schema directory. Must exist. |
66
+ | `CLEAN_ROOM_PRIVATE_IDENTIFIER_DENYLIST` | Optional path-separated denylist files for leakage scanning. |
67
+ | `CLEAN_ROOM_AUXILIARY_JSON_ALLOWLIST` | Optional path-separated allowlist for unrecognized auxiliary JSON files under clean roots. |
68
+ | `CLEAN_ROOM_ALLOW_AGENT3_SHELL` | Must be `1` before Agent 3 can invoke the verification runner through a shell-style tool. |
69
+ | `CLEAN_ROOM_HOOK_ENFORCE` | Forces enforcement in `safe` mode when truthy. |
70
+ | `CLEAN_ROOM_HOOK_CHECK_TIMEOUT_SECONDS` | Optional per-check wrapper timeout. Defaults to 10 seconds. |
71
+
72
+ Root variables may contain multiple paths separated by the platform path separator.
73
+
74
+ ## Hook Scripts
75
+
76
+ ### `clean-room-hook.py`
77
+
78
+ Dispatch wrapper used by generated runtime hook entries.
79
+
80
+ - Enforces immediately in `strict` mode.
81
+ - In `safe` mode, enforces only when `CLEAN_ROOM_HOOK_ENFORCE` is truthy or clean-room environment variables are present.
82
+ - Reads the hook payload once from stdin and passes the same payload to each configured check.
83
+ - Runs child checks with `sys.executable -I`, `PYTHONNOUSERSITE=1`, and `PYTHONDONTWRITEBYTECODE=1`.
84
+ - Rejects invalid check names and missing check scripts.
85
+ - Fails if any check fails or exceeds `CLEAN_ROOM_HOOK_CHECK_TIMEOUT_SECONDS`.
86
+
87
+ ### `require-clean-room-env.py`
88
+
89
+ Precondition check for role sessions.
90
+
91
+ - Requires non-empty role, source, contaminated artifact, clean, implementation, and schema root variables.
92
+ - Requires `CLEAN_ROOM_ALLOWED_READ_ROOTS` for clean roles and the source-denied sanitizer role.
93
+ - Validates the role name.
94
+ - Resolves configured roots and requires `CLEAN_ROOM_SCHEMA_DIR` to exist.
95
+ - Rejects overlap between source, clean, implementation, contaminated artifact, allowed-read, and schema roots where overlap would break separation.
96
+ - Rejects clean, implementation, and contaminated artifact root names that appear source-derived from the source root basename.
97
+
98
+ ### `deny-clean-room-shell.py`
99
+
100
+ Pre-tool shell policy.
101
+
102
+ - Denies shell-style tools for all clean-room roles by default.
103
+ - Allows Agent 3 (`clean-qa-editor`) only when `CLEAN_ROOM_ALLOW_AGENT3_SHELL=1`.
104
+ - Agent 3 shell use must run from an implementation root and must invoke the installed `agent3-verification-runner.py`.
105
+ - Rejects shell metacharacters, file URLs, unexpected runner flags, blocked source or contaminated roots, and runner plans outside clean roots.
106
+ - Allows runner selectors only through `--command-index <n>` or `--all`, with optional `--plan`, `--timeout`, and `--backend host|docker|podman`.
107
+
108
+ ### `agent3-verification-runner.py`
109
+
110
+ Bounded verification runner for Agent 3. This is not a host runtime hook entry by itself; it is the only shell target that `deny-clean-room-shell.py` permits for Agent 3.
111
+
112
+ - Requires `CLEAN_ROOM_ROLE=clean-qa-editor` and `CLEAN_ROOM_ALLOW_AGENT3_SHELL=1`.
113
+ - Loads verification commands from `implementation-plan.json` in clean roots, or from `--plan`.
114
+ - Accepts argv-array commands only and executes with `shell=False`.
115
+ - Requires command cwd values to use `CLEAN_ROOM_IMPLEMENTATION_ROOTS[n]`.
116
+ - Allows a small command prefix set: npm, pnpm, yarn, bun, deno test commands; pytest directly or through Python; `cargo test`; `go test`; and `zig build test`.
117
+ - Rejects shell syntax, blocked root references, file URLs, paths resolving outside the implementation root, and source or contaminated root traversal.
118
+ - Runs with a sanitized environment and bounded stdout/stderr output.
119
+ - Supports optional Docker or Podman execution with read-only clean/schema/reference mounts, a writable implementation mount, no network by default, dropped capabilities, no-new-privileges, resource limits, and no source or contaminated mounts.
120
+
121
+ ### `deny-clean-source-read.py`
122
+
123
+ Pre-tool read policy for source-denied roles.
124
+
125
+ - Applies to `clean-architect`, `clean-qa-editor`, and `contaminated-handoff-sanitizer`.
126
+ - Clean roles may read clean roots, implementation roots, allowed-read roots, and schema roots.
127
+ - The sanitizer may read contaminated artifact roots, allowed-read roots, and schema roots.
128
+ - Denies reads from source roots.
129
+ - Denies sanitizer reads from clean roots.
130
+ - Denies direct reads of `preflight-goal.json` for source-denied roles.
131
+ - Denies sanitizer reads of `source-index.json`.
132
+ - Denies MCP resource access because no MCP resource allowlist is configured.
133
+ - Fails closed when a path-required read tool has no resolvable path.
134
+
135
+ ### `deny-contaminated-clean-write.py`
136
+
137
+ Pre-tool write policy for role-specific write roots.
138
+
139
+ - Applies to contaminated and clean roles.
140
+ - Clean roles cannot write source roots or read-only allowed-read roots.
141
+ - Agent 2 (`clean-architect`) writes only under clean roots and is denied implementation roots.
142
+ - Agent 3 (`clean-qa-editor`) may write under implementation roots and clean roots.
143
+ - Contaminated roles cannot write clean, implementation, or source roots.
144
+ - Contaminated roles may write only under `CLEAN_ROOM_CONTAMINATED_ARTIFACT_ROOTS`.
145
+ - Fails closed for active clean-room roles when write paths cannot be resolved from the hook payload.
146
+
147
+ ### `check-artifact-leakage.py`
148
+
149
+ Post-write leakage scanner.
150
+
151
+ - Scans `.json`, `.md`, `.yaml`, `.yml`, and `.txt` artifacts under clean roots.
152
+ - Also scans staged sanitizer artifacts under contaminated artifact roots.
153
+ - Does not scan contaminated source analyst drafts.
154
+ - Caps scanned artifact size at 1,000,000 bytes.
155
+ - Detects raw diffs, source code fences, decompiler/source excerpt markers, stack-source lines, package/module identifiers, source-like calls, source-like scoped identifiers, and configured private denylist terms.
156
+ - Loads denylist files from `CLEAN_ROOM_PRIVATE_IDENTIFIER_DENYLIST`, capped at 1,000,000 bytes per file, 20,000 terms total, and 512 characters per term.
157
+ - Treats selected JSON fields as unscanned, denylist-only, or light-scan fields to reduce false positives in path-like and metadata fields.
158
+ - Allows public names declared in `public_surface` and `public_contracts` records when marked with public/destination/protocol/user-required visibility.
159
+
160
+ ### `validate-json-schema.py`
161
+
162
+ Post-write JSON artifact validator.
163
+
164
+ - Parses written `.json` files and validates recognized artifacts against schemas from `CLEAN_ROOM_SCHEMA_DIR`.
165
+ - Recognizes canonical clean-room artifact kinds by filename and conservative field heuristics.
166
+ - Fails closed for unrecognized JSON objects under clean roots unless the exact path is in `CLEAN_ROOM_AUXILIARY_JSON_ALLOWLIST`.
167
+ - Rejects `source-index`, `init-config`, and `preflight-goal` artifacts under clean roots.
168
+ - Implements the lightweight schema keywords used by bundled schemas, including object and array constraints, required fields, enum/const, patterns, `format: date-time`, `$ref`, `allOf`, `anyOf`, `oneOf`, and `if`/`then`/`else`.
169
+ - Adds clean-run-context path checks so clean artifact paths stay relative, do not use `~`, do not contain `..`, and do not resolve into source or contaminated roots.
170
+ - Requires task manifest handoff stages to match the expected clean-room sequence when validating the task manifest schema.
171
+
172
+ ### `validate-handoff-package.py`
173
+
174
+ Post-write handoff integrity validator.
175
+
176
+ - Runs on written JSON files that look like handoff packages.
177
+ - Requires handoff `artifacts` to be an array of object entries with non-empty paths.
178
+ - Rejects `source-index.json`, `task-manifest.json`, and `preflight-goal.json` in clean handoff packages.
179
+ - Resolves relative artifact paths against clean roots and rejects ambiguous paths across multiple clean roots.
180
+ - Rejects artifact paths outside clean roots or inside source or contaminated roots.
181
+ - Requires referenced artifacts to exist and have a 64-character `sha256`.
182
+ - Hashes referenced files and fails on checksum mismatch.
183
+
184
+ ### `clean_room_paths.py`
185
+
186
+ Shared helper module imported by the hook scripts.
187
+
188
+ - Loads and bounds hook JSON payloads at 10 MiB.
189
+ - Resolves candidate paths from common tool payload keys.
190
+ - Resolves `cwd` safely from payload data or the current process.
191
+ - Provides root parsing, path containment, root overlap checks, and active role detection.
192
+ - Redacts configured roots and source-derived private path tokens in errors for clean-room roles by default.
193
+ - Provides fail-closed written-file checks plus controlled `stat`, byte-read, and text-read helpers.
194
+
195
+ ## Failure Behavior
196
+
197
+ The hook policy is deny-by-default during active clean-room role sessions.
198
+
199
+ - Malformed hook payloads fail.
200
+ - Oversized hook payloads fail.
201
+ - Missing paths for path-required write checks fail.
202
+ - Files that disappear, cannot be statted, cannot be read, or cannot be hashed fail with controlled errors.
203
+ - Expected filesystem validation failures are reported without Python tracebacks.
204
+ - Error output is redacted through root labels such as `source-root[0]`, `clean-root[0]`, and `contaminated-root[0]` for clean-room role contexts by default.
205
+
206
+ ## Verification
207
+
208
+ Use `doctor` after installing Codex or Claude hooks:
209
+
210
+ ```bash
211
+ clean-room-skill doctor --runtime codex --hooks=safe
212
+ clean-room-skill doctor --runtime codex --hooks=strict
213
+ clean-room-skill doctor --runtime codex --hooks=strict --coverage
214
+ clean-room-skill doctor --runtime claude --hooks=strict --coverage
215
+ ```
216
+
217
+ Add `--config-dir <path>` when checking a non-default runtime config root.
218
+
219
+ `doctor` verifies that:
220
+
221
+ - The hook config exists.
222
+ - Exactly four managed clean-room hook entries are present.
223
+ - Managed commands use absolute Python and wrapper paths.
224
+ - The requested safe or strict mode is configured.
225
+ - Safe mode no-ops without clean-room environment.
226
+ - Strict mode and enforced safe mode fail without required environment.
227
+ - Smoke payloads fail for source reads, source writes, shell bypasses, and malformed post-write JSON.
228
+ - `--coverage` prints matcher and check coverage for the generated entries.
229
+
230
+ `doctor` is a smoke test. It does not prove host event coverage, legal sufficiency, or full runtime isolation.