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.
Files changed (58) 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 +32 -5
  5. package/agents/clean-architect.md +3 -0
  6. package/agents/clean-implementer-verifier-shell.md +3 -0
  7. package/agents/clean-polish-reviewer.md +3 -0
  8. package/agents/clean-qa-editor.md +3 -0
  9. package/agents/contaminated-handoff-sanitizer.md +3 -0
  10. package/agents/contaminated-manager-verifier.md +3 -0
  11. package/agents/contaminated-source-analyst.md +3 -0
  12. package/bin/install.js +11 -1621
  13. package/docs/ARCHITECTURE.md +1 -1
  14. package/docs/HOOKS.md +14 -10
  15. package/docs/REFERENCE.md +24 -4
  16. package/examples/codex/.codex/agents/clean-architect.toml +3 -3
  17. package/examples/codex/.codex/agents/clean-polish-reviewer.toml +2 -2
  18. package/examples/codex/.codex/agents/clean-qa-editor.toml +2 -2
  19. package/examples/codex/.codex/agents/contaminated-handoff-sanitizer.toml +2 -2
  20. package/examples/codex/.codex/agents/contaminated-manager-verifier.toml +3 -3
  21. package/examples/codex/.codex/agents/contaminated-source-analyst.toml +2 -2
  22. package/lib/bootstrap.cjs +5 -1
  23. package/lib/doctor.cjs +157 -5
  24. package/lib/hooks.cjs +18 -0
  25. package/lib/install-artifacts.cjs +178 -4
  26. package/lib/install-claude-plugin.cjs +374 -0
  27. package/lib/install-cli.cjs +99 -0
  28. package/lib/install-operations.cjs +376 -0
  29. package/lib/install-options.cjs +149 -0
  30. package/lib/install-runtime-selection.cjs +180 -0
  31. package/lib/install-status.cjs +292 -0
  32. package/lib/install-tui.cjs +359 -0
  33. package/lib/preflight-bootstrap.cjs +39 -0
  34. package/lib/preflight-cli.cjs +95 -0
  35. package/lib/preflight-constants.cjs +25 -0
  36. package/lib/preflight-output.cjs +37 -0
  37. package/lib/preflight-paths.cjs +67 -0
  38. package/lib/preflight-template.cjs +103 -0
  39. package/lib/preflight-validation.cjs +276 -0
  40. package/lib/preflight.cjs +18 -461
  41. package/lib/run-clean-artifacts.cjs +276 -0
  42. package/lib/run-cli.cjs +90 -0
  43. package/lib/run-constants.cjs +171 -0
  44. package/lib/run-controller.cjs +247 -0
  45. package/lib/run-coverage.cjs +350 -0
  46. package/lib/run-hooks.cjs +96 -0
  47. package/lib/run-manifest.cjs +111 -0
  48. package/lib/run-progress.cjs +160 -0
  49. package/lib/run-results.cjs +433 -0
  50. package/lib/run-roots.cjs +230 -0
  51. package/lib/run-stages.cjs +409 -0
  52. package/lib/run.cjs +4 -2254
  53. package/lib/runtime-layout.cjs +12 -5
  54. package/package.json +8 -2
  55. package/plugin.json +1 -1
  56. package/skills/attended/SKILL.md +2 -0
  57. package/skills/clean-room/SKILL.md +2 -2
  58. package/skills/unattended/SKILL.md +2 -0
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Print the help/usage message for the clean-room-skill preflight command.
5
+ */
6
+ function printPreflightHelp() {
7
+ console.log(`Usage: clean-room-skill preflight (--template | --input <path>) (--output <path> | --bootstrap <path>) [options]
8
+
9
+ Create or validate a clean-room preflight goal contract.
10
+
11
+ Options:
12
+ --template Write an attended draft with blocking open questions
13
+ --input <path> Validate and normalize/copy a completed preflight goal
14
+ --output <path> Destination preflight-goal.json
15
+ --bootstrap <path> Generated task root or clean-room-bootstrap.json
16
+ --mode <mode> attended or unattended (template supports attended only)
17
+ --dry-run Print actions without writing files
18
+ --force Overwrite output if it already exists
19
+ -h, --help Show this help
20
+ `);
21
+ }
22
+
23
+ /**
24
+ * Parse command line arguments for the preflight command.
25
+ * @param {string[]} argv - The command line arguments.
26
+ * @returns {object} The parsed options.
27
+ */
28
+ function parsePreflightArgs(argv) {
29
+ const options = {
30
+ template: false,
31
+ input: null,
32
+ output: null,
33
+ bootstrap: null,
34
+ mode: 'attended',
35
+ dryRun: false,
36
+ force: false,
37
+ help: false,
38
+ };
39
+
40
+ for (let index = 0; index < argv.length; index += 1) {
41
+ const arg = argv[index];
42
+ if (arg === '-h' || arg === '--help') {
43
+ options.help = true;
44
+ } else if (arg === '--template') {
45
+ options.template = true;
46
+ } else if (arg === '--dry-run') {
47
+ options.dryRun = true;
48
+ } else if (arg === '--force') {
49
+ options.force = true;
50
+ } else if (arg === '--input') {
51
+ index += 1;
52
+ options.input = requiredValue(argv, index, '--input');
53
+ } else if (arg.startsWith('--input=')) {
54
+ options.input = arg.slice('--input='.length);
55
+ } else if (arg === '--output') {
56
+ index += 1;
57
+ options.output = requiredValue(argv, index, '--output');
58
+ } else if (arg.startsWith('--output=')) {
59
+ options.output = arg.slice('--output='.length);
60
+ } else if (arg === '--bootstrap') {
61
+ index += 1;
62
+ options.bootstrap = requiredValue(argv, index, '--bootstrap');
63
+ } else if (arg.startsWith('--bootstrap=')) {
64
+ options.bootstrap = arg.slice('--bootstrap='.length);
65
+ } else if (arg === '--mode') {
66
+ index += 1;
67
+ options.mode = requiredValue(argv, index, '--mode');
68
+ } else if (arg.startsWith('--mode=')) {
69
+ options.mode = arg.slice('--mode='.length);
70
+ } else {
71
+ throw new Error(`unknown preflight option: ${arg}`);
72
+ }
73
+ }
74
+
75
+ return options;
76
+ }
77
+
78
+ /**
79
+ * Get a required argument value or throw an error.
80
+ * @param {string[]} argv - The command line arguments.
81
+ * @param {number} index - The index of the argument.
82
+ * @param {string} flag - The name of the flag.
83
+ * @returns {string} The flag's value.
84
+ */
85
+ function requiredValue(argv, index, flag) {
86
+ if (index >= argv.length || argv[index] === '') {
87
+ throw new Error(`${flag} requires a value`);
88
+ }
89
+ return argv[index];
90
+ }
91
+
92
+ module.exports = {
93
+ parsePreflightArgs,
94
+ printPreflightHelp,
95
+ };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ const VALID_MODES = new Set(['attended', 'unattended']);
4
+ const VALID_INTENTS = new Set([
5
+ 'clean-room-reimplementation',
6
+ 'behavior-compatible-port',
7
+ 'api-compatible-clone',
8
+ 'modernization',
9
+ 'partial-feature-extraction',
10
+ 'test-spec-generation-only',
11
+ 'other',
12
+ ]);
13
+ const VALID_EXECUTION_BACKENDS = new Set(['host', 'docker', 'podman']);
14
+ const VALID_CONTAINER_PROFILES = new Set(['node22', 'python312', 'go126', 'rust-stable']);
15
+ const VALID_NETWORK_POLICIES = new Set(['off', 'deps-only', 'on']);
16
+ const VALID_DEPENDENCY_INSTALL_POLICIES = new Set(['offline', 'locked', 'allow-new']);
17
+
18
+ module.exports = {
19
+ VALID_CONTAINER_PROFILES,
20
+ VALID_DEPENDENCY_INSTALL_POLICIES,
21
+ VALID_EXECUTION_BACKENDS,
22
+ VALID_INTENTS,
23
+ VALID_MODES,
24
+ VALID_NETWORK_POLICIES,
25
+ };
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ atomicWriteFile,
5
+ atomicWriteFileNoOverwrite,
6
+ } = require('./fs-utils.cjs');
7
+
8
+ /**
9
+ * Write preflight goal object to file or dry-run print it.
10
+ * @param {string} outputPath - Target file path.
11
+ * @param {object} goal - Preflight goal object.
12
+ * @param {object} options - Write options (force, dryRun).
13
+ */
14
+ function writePreflightOutput(outputPath, goal, options) {
15
+ const data = `${JSON.stringify(goal, null, 2)}\n`;
16
+ if (options.dryRun) {
17
+ console.log(`Would write preflight goal: ${outputPath}`);
18
+ return;
19
+ }
20
+ try {
21
+ if (options.force) {
22
+ atomicWriteFile(outputPath, data, 'utf8');
23
+ } else {
24
+ atomicWriteFileNoOverwrite(outputPath, data, 'utf8');
25
+ }
26
+ } catch (err) {
27
+ if (err?.code === 'EEXIST') {
28
+ throw new Error(`preflight output already exists; use --force to overwrite: ${outputPath}`);
29
+ }
30
+ throw err;
31
+ }
32
+ console.log(`Wrote preflight goal: ${outputPath}`);
33
+ }
34
+
35
+ module.exports = {
36
+ writePreflightOutput,
37
+ };
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+
6
+ /**
7
+ * Expand tilde character `~` to home directory.
8
+ * @param {string} value - Path value which may start with `~`.
9
+ * @param {string} [homeDir=os.homedir()] - User home directory.
10
+ * @returns {string} The expanded path.
11
+ */
12
+ function expandTilde(value, homeDir = os.homedir()) {
13
+ if (value === '~') return homeDir;
14
+ if (typeof value === 'string' && value.startsWith('~/')) return path.join(homeDir, value.slice(2));
15
+ return value;
16
+ }
17
+
18
+ /**
19
+ * Resolve the destination preflight output path.
20
+ * @param {string} value - Path value.
21
+ * @param {string} [cwd=process.cwd()] - Current working directory.
22
+ * @param {string} [homeDir=os.homedir()] - User home directory.
23
+ * @returns {string} The resolved absolute path.
24
+ */
25
+ function resolveOutputPath(value, cwd = process.cwd(), homeDir = os.homedir()) {
26
+ if (typeof value !== 'string' || value.trim() === '') {
27
+ throw new Error('--output requires a path');
28
+ }
29
+ const expanded = expandTilde(value, homeDir);
30
+ return path.resolve(cwd, expanded);
31
+ }
32
+
33
+ /**
34
+ * Resolve the input preflight goal path.
35
+ * @param {string} value - Path value.
36
+ * @param {string} [cwd=process.cwd()] - Current working directory.
37
+ * @param {string} [homeDir=os.homedir()] - User home directory.
38
+ * @returns {string} The resolved absolute path.
39
+ */
40
+ function resolveInputPath(value, cwd = process.cwd(), homeDir = os.homedir()) {
41
+ if (typeof value !== 'string' || value.trim() === '') {
42
+ throw new Error('--input requires a path');
43
+ }
44
+ const expanded = expandTilde(value, homeDir);
45
+ return path.resolve(cwd, expanded);
46
+ }
47
+
48
+ /**
49
+ * Resolve a path value within the preflight goal configuration.
50
+ * @param {string|null|undefined} value - Path value to resolve.
51
+ * @param {string} cwd - Current working directory.
52
+ * @param {string} homeDir - User home directory.
53
+ * @returns {string|null} Resolved absolute path or null.
54
+ */
55
+ function resolveGoalPath(value, cwd, homeDir) {
56
+ if (typeof value !== 'string' || value.trim() === '') {
57
+ return null;
58
+ }
59
+ return path.resolve(cwd, expandTilde(value, homeDir));
60
+ }
61
+
62
+ module.exports = {
63
+ expandTilde,
64
+ resolveGoalPath,
65
+ resolveInputPath,
66
+ resolveOutputPath,
67
+ };
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Build a default attended preflight goal template object.
5
+ * @param {string} [mode='attended'] - Controller mode.
6
+ * @returns {object} The preflight goal template object.
7
+ */
8
+ function buildTemplate(mode = 'attended') {
9
+ if (mode !== 'attended') {
10
+ throw new Error('preflight --template supports attended mode only');
11
+ }
12
+ return {
13
+ goal_id: 'goal-task-xxxxxxxx',
14
+ created_at: new Date().toISOString(),
15
+ end_goal: {
16
+ intent: 'clean-room-reimplementation',
17
+ success_definition: 'TBD: define the observable result the clean implementation must achieve.',
18
+ destination_kind: 'new-project',
19
+ existing_destination_policy: 'inspect-and-preserve',
20
+ },
21
+ target_stack: {
22
+ language: 'TBD',
23
+ runtime: null,
24
+ framework: null,
25
+ package_manager: null,
26
+ test_framework: null,
27
+ },
28
+ license_policy: {
29
+ source_license_notes: 'unknown',
30
+ destination_license: 'TBD',
31
+ dependency_license_allowlist: ['MIT', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause'],
32
+ dependency_license_blocklist: ['GPL-3.0', 'AGPL-3.0'],
33
+ },
34
+ dependency_policy: {
35
+ allow_new_dependencies: true,
36
+ prefer_stdlib: true,
37
+ require_user_approval_for_native_deps: true,
38
+ blocked_dependencies: [],
39
+ },
40
+ compatibility_policy: {
41
+ mirror_public_behavior: true,
42
+ mirror_public_api_names: true,
43
+ mirror_private_structure: false,
44
+ mirror_comments_or_internal_names: false,
45
+ allowed_exactness: [
46
+ 'public API names',
47
+ 'CLI flags',
48
+ 'serialized outputs',
49
+ 'documented protocol behavior',
50
+ 'public error codes',
51
+ ],
52
+ },
53
+ feature_policy: {
54
+ preserve_features: [],
55
+ remove_features: [],
56
+ add_features: [],
57
+ non_goals: [],
58
+ },
59
+ code_hygiene_policy: {
60
+ max_lines_per_code_file: 500,
61
+ max_lines_per_test_file: 800,
62
+ max_files_per_iteration: 12,
63
+ split_large_files_by: ['module boundary', 'public type', 'feature area'],
64
+ exceptions: ['generated files', 'fixtures', 'snapshots'],
65
+ forbidden_patterns: ['god file', 'source-shaped layout'],
66
+ },
67
+ output_policy: {
68
+ artifact_base_root: '~/Documents/CleanRoom/<task-id>/',
69
+ implementation_root: '~/Documents/CleanRoom/<task-id>/implementation/',
70
+ assumed_output_directory: 'implementation/',
71
+ write_mode: 'create-or-preserve-existing',
72
+ },
73
+ execution_policy: {
74
+ backend: 'host',
75
+ preferred_container_profile: 'node22',
76
+ network_policy: 'off',
77
+ dependency_install_policy: 'locked',
78
+ allow_native_toolchain: false,
79
+ resource_limits: {
80
+ cpus: 2,
81
+ memory_mb: 2048,
82
+ timeout_seconds: 300,
83
+ },
84
+ },
85
+ controller_policy: {
86
+ mode: 'attended',
87
+ unattended_allowed_after_preflight: false,
88
+ max_iterations: 10,
89
+ },
90
+ open_questions: [
91
+ {
92
+ question_id: 'goal-end-state',
93
+ question: 'Define the end goal, target stack, compatibility exactness, dependency policy, license policy, and output root before execution.',
94
+ blocking: true,
95
+ default_assumption: 'Do not start source analysis until this is answered.',
96
+ },
97
+ ],
98
+ };
99
+ }
100
+
101
+ module.exports = {
102
+ buildTemplate,
103
+ };
@@ -0,0 +1,276 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ VALID_CONTAINER_PROFILES,
5
+ VALID_DEPENDENCY_INSTALL_POLICIES,
6
+ VALID_EXECUTION_BACKENDS,
7
+ VALID_INTENTS,
8
+ VALID_MODES,
9
+ VALID_NETWORK_POLICIES,
10
+ } = require('./preflight-constants.cjs');
11
+
12
+ /**
13
+ * Assert that a value is an object (not null and not an array), appending errors on failure.
14
+ * @param {any} value - Value to check.
15
+ * @param {string} label - Field label for error message.
16
+ * @param {string[]} errors - Array to push error messages into.
17
+ * @returns {boolean} True if validation passes, otherwise false.
18
+ */
19
+ function expectObject(value, label, errors) {
20
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
21
+ errors.push(`${label} must be an object`);
22
+ return false;
23
+ }
24
+ return true;
25
+ }
26
+
27
+ /**
28
+ * Assert that a value is an array, appending errors on failure.
29
+ * @param {any} value - Value to check.
30
+ * @param {string} label - Field label for error message.
31
+ * @param {string[]} errors - Array to push error messages into.
32
+ * @returns {boolean} True if validation passes, otherwise false.
33
+ */
34
+ function expectArray(value, label, errors) {
35
+ if (!Array.isArray(value)) {
36
+ errors.push(`${label} must be an array`);
37
+ return false;
38
+ }
39
+ return true;
40
+ }
41
+
42
+ /**
43
+ * Assert that a value is a string, appending errors on failure.
44
+ * @param {any} value - Value to check.
45
+ * @param {string} label - Field label for error message.
46
+ * @param {string[]} errors - Array to push error messages into.
47
+ * @param {boolean} [allowEmpty=false] - Whether to allow empty string.
48
+ * @returns {boolean} True if validation passes, otherwise false.
49
+ */
50
+ function expectString(value, label, errors, allowEmpty = false) {
51
+ if (typeof value !== 'string' || (!allowEmpty && value.length === 0)) {
52
+ errors.push(`${label} must be a non-empty string`);
53
+ return false;
54
+ }
55
+ return true;
56
+ }
57
+
58
+ /**
59
+ * Assert that a value is a boolean, appending errors on failure.
60
+ * @param {any} value - Value to check.
61
+ * @param {string} label - Field label for error message.
62
+ * @param {string[]} errors - Array to push error messages into.
63
+ * @returns {boolean} True if validation passes, otherwise false.
64
+ */
65
+ function expectBoolean(value, label, errors) {
66
+ if (typeof value !== 'boolean') {
67
+ errors.push(`${label} must be a boolean`);
68
+ return false;
69
+ }
70
+ return true;
71
+ }
72
+
73
+ /**
74
+ * Assert that a value is a positive integer, appending errors on failure.
75
+ * @param {any} value - Value to check.
76
+ * @param {string} label - Field label for error message.
77
+ * @param {string[]} errors - Array to push error messages into.
78
+ * @returns {boolean} True if validation passes, otherwise false.
79
+ */
80
+ function expectPositiveInteger(value, label, errors) {
81
+ if (!Number.isInteger(value) || value < 1) {
82
+ errors.push(`${label} must be a positive integer`);
83
+ return false;
84
+ }
85
+ return true;
86
+ }
87
+
88
+ /**
89
+ * Validate that an object property is a string array.
90
+ * @param {object} root - Object containing the field.
91
+ * @param {string} field - Field name.
92
+ * @param {string[]} errors - Array to push error messages into.
93
+ */
94
+ function validateStringArray(root, field, errors) {
95
+ if (!expectArray(root?.[field], field, errors)) return;
96
+ for (const [index, item] of root[field].entries()) {
97
+ expectString(item, `${field}[${index}]`, errors);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Validate a preflight goal contract object.
103
+ * @param {object} goal - Goal contract object to validate.
104
+ * @param {object} [options={}] - Validation options (e.g. requireComplete).
105
+ * @returns {string[]} Array of validation error messages.
106
+ */
107
+ function validateGoalContract(goal, options = {}) {
108
+ const errors = [];
109
+ if (!expectObject(goal, 'preflight goal', errors)) {
110
+ return errors;
111
+ }
112
+
113
+ expectString(goal.goal_id, 'goal_id', errors);
114
+ expectString(goal.created_at, 'created_at', errors);
115
+
116
+ if (expectObject(goal.end_goal, 'end_goal', errors)) {
117
+ if (!VALID_INTENTS.has(goal.end_goal.intent)) {
118
+ errors.push('end_goal.intent is not supported');
119
+ }
120
+ expectString(goal.end_goal.success_definition, 'end_goal.success_definition', errors);
121
+ expectString(goal.end_goal.destination_kind, 'end_goal.destination_kind', errors);
122
+ expectString(goal.end_goal.existing_destination_policy, 'end_goal.existing_destination_policy', errors);
123
+ }
124
+
125
+ if (expectObject(goal.target_stack, 'target_stack', errors)) {
126
+ expectString(goal.target_stack.language, 'target_stack.language', errors);
127
+ for (const field of ['runtime', 'framework', 'package_manager', 'test_framework']) {
128
+ if (goal.target_stack[field] !== null) {
129
+ expectString(goal.target_stack[field], `target_stack.${field}`, errors);
130
+ }
131
+ }
132
+ }
133
+
134
+ if (expectObject(goal.license_policy, 'license_policy', errors)) {
135
+ expectString(goal.license_policy.source_license_notes, 'license_policy.source_license_notes', errors, true);
136
+ expectString(goal.license_policy.destination_license, 'license_policy.destination_license', errors);
137
+ validateStringArray(goal.license_policy, 'dependency_license_allowlist', errors);
138
+ validateStringArray(goal.license_policy, 'dependency_license_blocklist', errors);
139
+ }
140
+
141
+ if (expectObject(goal.dependency_policy, 'dependency_policy', errors)) {
142
+ expectBoolean(goal.dependency_policy.allow_new_dependencies, 'dependency_policy.allow_new_dependencies', errors);
143
+ expectBoolean(goal.dependency_policy.prefer_stdlib, 'dependency_policy.prefer_stdlib', errors);
144
+ expectBoolean(
145
+ goal.dependency_policy.require_user_approval_for_native_deps,
146
+ 'dependency_policy.require_user_approval_for_native_deps',
147
+ errors
148
+ );
149
+ validateStringArray(goal.dependency_policy, 'blocked_dependencies', errors);
150
+ }
151
+
152
+ if (expectObject(goal.compatibility_policy, 'compatibility_policy', errors)) {
153
+ expectBoolean(goal.compatibility_policy.mirror_public_behavior, 'compatibility_policy.mirror_public_behavior', errors);
154
+ expectBoolean(goal.compatibility_policy.mirror_public_api_names, 'compatibility_policy.mirror_public_api_names', errors);
155
+ if (goal.compatibility_policy.mirror_private_structure !== false) {
156
+ errors.push('compatibility_policy.mirror_private_structure must be false');
157
+ }
158
+ if (goal.compatibility_policy.mirror_comments_or_internal_names !== false) {
159
+ errors.push('compatibility_policy.mirror_comments_or_internal_names must be false');
160
+ }
161
+ validateStringArray(goal.compatibility_policy, 'allowed_exactness', errors);
162
+ }
163
+
164
+ if (expectObject(goal.feature_policy, 'feature_policy', errors)) {
165
+ for (const field of ['preserve_features', 'remove_features', 'add_features', 'non_goals']) {
166
+ validateStringArray(goal.feature_policy, field, errors);
167
+ }
168
+ }
169
+
170
+ validateCodeHygienePolicy(goal.code_hygiene_policy, errors);
171
+
172
+ if (expectObject(goal.output_policy, 'output_policy', errors)) {
173
+ expectString(goal.output_policy.artifact_base_root, 'output_policy.artifact_base_root', errors);
174
+ expectString(goal.output_policy.implementation_root, 'output_policy.implementation_root', errors);
175
+ expectString(goal.output_policy.assumed_output_directory, 'output_policy.assumed_output_directory', errors);
176
+ expectString(goal.output_policy.write_mode, 'output_policy.write_mode', errors);
177
+ }
178
+
179
+ if (goal.execution_policy !== undefined) {
180
+ validateExecutionPolicy(goal.execution_policy, errors);
181
+ }
182
+
183
+ if (expectObject(goal.controller_policy, 'controller_policy', errors)) {
184
+ if (!VALID_MODES.has(goal.controller_policy.mode)) {
185
+ errors.push('controller_policy.mode must be attended or unattended');
186
+ }
187
+ expectBoolean(
188
+ goal.controller_policy.unattended_allowed_after_preflight,
189
+ 'controller_policy.unattended_allowed_after_preflight',
190
+ errors
191
+ );
192
+ expectPositiveInteger(goal.controller_policy.max_iterations, 'controller_policy.max_iterations', errors);
193
+ }
194
+
195
+ if (expectArray(goal.open_questions, 'open_questions', errors)) {
196
+ for (const [index, question] of goal.open_questions.entries()) {
197
+ if (!expectObject(question, `open_questions[${index}]`, errors)) continue;
198
+ expectString(question.question_id, `open_questions[${index}].question_id`, errors);
199
+ expectString(question.question, `open_questions[${index}].question`, errors);
200
+ expectBoolean(question.blocking, `open_questions[${index}].blocking`, errors);
201
+ }
202
+ }
203
+
204
+ if (goal.controller_policy?.mode === 'unattended') {
205
+ if (goal.controller_policy.unattended_allowed_after_preflight !== true) {
206
+ errors.push('unattended preflight requires unattended_allowed_after_preflight=true');
207
+ }
208
+ if (Array.isArray(goal.open_questions) && goal.open_questions.length > 0) {
209
+ errors.push('unattended preflight requires no open_questions');
210
+ }
211
+ }
212
+
213
+ if (options.requireComplete && Array.isArray(goal.open_questions)) {
214
+ const blocking = goal.open_questions.filter((question) => question?.blocking === true);
215
+ if (blocking.length > 0) {
216
+ errors.push('completed preflight input must not contain blocking open_questions');
217
+ }
218
+ }
219
+
220
+ return errors;
221
+ }
222
+
223
+ /**
224
+ * Validate the execution policy block of a preflight goal contract.
225
+ * @param {object} policy - Execution policy object.
226
+ * @param {string[]} errors - Array to push error messages into.
227
+ */
228
+ function validateExecutionPolicy(policy, errors) {
229
+ if (!expectObject(policy, 'execution_policy', errors)) return;
230
+ if (!VALID_EXECUTION_BACKENDS.has(policy.backend)) {
231
+ errors.push('execution_policy.backend must be host, docker, or podman');
232
+ }
233
+ if (!VALID_CONTAINER_PROFILES.has(policy.preferred_container_profile)) {
234
+ errors.push('execution_policy.preferred_container_profile is not supported');
235
+ }
236
+ if (!VALID_NETWORK_POLICIES.has(policy.network_policy)) {
237
+ errors.push('execution_policy.network_policy must be off, deps-only, or on');
238
+ }
239
+ if (!VALID_DEPENDENCY_INSTALL_POLICIES.has(policy.dependency_install_policy)) {
240
+ errors.push('execution_policy.dependency_install_policy must be offline, locked, or allow-new');
241
+ }
242
+ expectBoolean(policy.allow_native_toolchain, 'execution_policy.allow_native_toolchain', errors);
243
+ if (!expectObject(policy.resource_limits, 'execution_policy.resource_limits', errors)) return;
244
+ expectPositiveInteger(policy.resource_limits.memory_mb, 'execution_policy.resource_limits.memory_mb', errors);
245
+ expectPositiveInteger(policy.resource_limits.timeout_seconds, 'execution_policy.resource_limits.timeout_seconds', errors);
246
+ if (typeof policy.resource_limits.cpus !== 'number' || policy.resource_limits.cpus < 1 || policy.resource_limits.cpus > 16) {
247
+ errors.push('execution_policy.resource_limits.cpus must be a number between 1 and 16');
248
+ }
249
+ if (Number.isInteger(policy.resource_limits.memory_mb) && policy.resource_limits.memory_mb > 65536) {
250
+ errors.push('execution_policy.resource_limits.memory_mb must be at most 65536');
251
+ }
252
+ if (Number.isInteger(policy.resource_limits.timeout_seconds) && policy.resource_limits.timeout_seconds > 600) {
253
+ errors.push('execution_policy.resource_limits.timeout_seconds must be at most 600');
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Validate the code hygiene policy block of a preflight goal contract.
259
+ * @param {object} policy - Code hygiene policy object.
260
+ * @param {string[]} errors - Array to push error messages into.
261
+ */
262
+ function validateCodeHygienePolicy(policy, errors) {
263
+ if (!expectObject(policy, 'code_hygiene_policy', errors)) return;
264
+ expectPositiveInteger(policy.max_lines_per_code_file, 'code_hygiene_policy.max_lines_per_code_file', errors);
265
+ expectPositiveInteger(policy.max_lines_per_test_file, 'code_hygiene_policy.max_lines_per_test_file', errors);
266
+ expectPositiveInteger(policy.max_files_per_iteration, 'code_hygiene_policy.max_files_per_iteration', errors);
267
+ validateStringArray(policy, 'split_large_files_by', errors);
268
+ validateStringArray(policy, 'exceptions', errors);
269
+ if (policy.forbidden_patterns !== undefined) {
270
+ validateStringArray(policy, 'forbidden_patterns', errors);
271
+ }
272
+ }
273
+
274
+ module.exports = {
275
+ validateGoalContract,
276
+ };