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
package/lib/preflight.cjs CHANGED
@@ -1,470 +1,27 @@
1
1
  'use strict';
2
2
 
3
- const fs = require('node:fs');
4
3
  const os = require('node:os');
5
4
  const path = require('node:path');
6
5
 
7
- const {
8
- atomicWriteFile,
9
- atomicWriteFileNoOverwrite,
10
- readJsonFile,
11
- } = require('./fs-utils.cjs');
6
+ const { readJsonFile } = require('./fs-utils.cjs');
12
7
  const { resolveBootstrapScaffold } = require('./bootstrap.cjs');
13
-
14
- const VALID_MODES = new Set(['attended', 'unattended']);
15
- const VALID_INTENTS = new Set([
16
- 'clean-room-reimplementation',
17
- 'behavior-compatible-port',
18
- 'api-compatible-clone',
19
- 'modernization',
20
- 'partial-feature-extraction',
21
- 'test-spec-generation-only',
22
- 'other',
23
- ]);
24
- const VALID_EXECUTION_BACKENDS = new Set(['host', 'docker', 'podman']);
25
- const VALID_CONTAINER_PROFILES = new Set(['node22', 'python312', 'go126', 'rust-stable']);
26
- const VALID_NETWORK_POLICIES = new Set(['off', 'deps-only', 'on']);
27
- const VALID_DEPENDENCY_INSTALL_POLICIES = new Set(['offline', 'locked', 'allow-new']);
28
-
29
- function printPreflightHelp() {
30
- console.log(`Usage: clean-room-skill preflight (--template | --input <path>) (--output <path> | --bootstrap <path>) [options]
31
-
32
- Create or validate a clean-room preflight goal contract.
33
-
34
- Options:
35
- --template Write an attended draft with blocking open questions
36
- --input <path> Validate and normalize/copy a completed preflight goal
37
- --output <path> Destination preflight-goal.json
38
- --bootstrap <path> Generated task root or clean-room-bootstrap.json
39
- --mode <mode> attended or unattended (template supports attended only)
40
- --dry-run Print actions without writing files
41
- --force Overwrite output if it already exists
42
- -h, --help Show this help
43
- `);
44
- }
45
-
46
- function parsePreflightArgs(argv) {
47
- const options = {
48
- template: false,
49
- input: null,
50
- output: null,
51
- bootstrap: null,
52
- mode: 'attended',
53
- dryRun: false,
54
- force: false,
55
- help: false,
56
- };
57
-
58
- for (let index = 0; index < argv.length; index += 1) {
59
- const arg = argv[index];
60
- if (arg === '-h' || arg === '--help') {
61
- options.help = true;
62
- } else if (arg === '--template') {
63
- options.template = true;
64
- } else if (arg === '--dry-run') {
65
- options.dryRun = true;
66
- } else if (arg === '--force') {
67
- options.force = true;
68
- } else if (arg === '--input') {
69
- index += 1;
70
- options.input = requiredValue(argv, index, '--input');
71
- } else if (arg.startsWith('--input=')) {
72
- options.input = arg.slice('--input='.length);
73
- } else if (arg === '--output') {
74
- index += 1;
75
- options.output = requiredValue(argv, index, '--output');
76
- } else if (arg.startsWith('--output=')) {
77
- options.output = arg.slice('--output='.length);
78
- } else if (arg === '--bootstrap') {
79
- index += 1;
80
- options.bootstrap = requiredValue(argv, index, '--bootstrap');
81
- } else if (arg.startsWith('--bootstrap=')) {
82
- options.bootstrap = arg.slice('--bootstrap='.length);
83
- } else if (arg === '--mode') {
84
- index += 1;
85
- options.mode = requiredValue(argv, index, '--mode');
86
- } else if (arg.startsWith('--mode=')) {
87
- options.mode = arg.slice('--mode='.length);
88
- } else {
89
- throw new Error(`unknown preflight option: ${arg}`);
90
- }
91
- }
92
-
93
- return options;
94
- }
95
-
96
- function requiredValue(argv, index, flag) {
97
- if (index >= argv.length || argv[index] === '') {
98
- throw new Error(`${flag} requires a value`);
99
- }
100
- return argv[index];
101
- }
102
-
103
- function expandTilde(value, homeDir = os.homedir()) {
104
- if (value === '~') return homeDir;
105
- if (typeof value === 'string' && value.startsWith('~/')) return path.join(homeDir, value.slice(2));
106
- return value;
107
- }
108
-
109
- function resolveOutputPath(value, cwd = process.cwd(), homeDir = os.homedir()) {
110
- if (typeof value !== 'string' || value.trim() === '') {
111
- throw new Error('--output requires a path');
112
- }
113
- const expanded = expandTilde(value, homeDir);
114
- return path.resolve(cwd, expanded);
115
- }
116
-
117
- function resolveInputPath(value, cwd = process.cwd(), homeDir = os.homedir()) {
118
- if (typeof value !== 'string' || value.trim() === '') {
119
- throw new Error('--input requires a path');
120
- }
121
- const expanded = expandTilde(value, homeDir);
122
- return path.resolve(cwd, expanded);
123
- }
124
-
125
- function resolveGoalPath(value, cwd, homeDir) {
126
- if (typeof value !== 'string' || value.trim() === '') {
127
- return null;
128
- }
129
- return path.resolve(cwd, expandTilde(value, homeDir));
130
- }
131
-
132
- function applyBootstrapOutputPolicy(goal, bootstrap) {
133
- goal.output_policy.artifact_base_root = bootstrap.outputRoot;
134
- goal.output_policy.implementation_root = bootstrap.roots.implementation;
135
- }
136
-
137
- function validateBootstrapOutputPolicy(goal, bootstrap, cwd, homeDir) {
138
- const errors = [];
139
- const artifactBaseRoot = resolveGoalPath(goal?.output_policy?.artifact_base_root, cwd, homeDir);
140
- const implementationRoot = resolveGoalPath(goal?.output_policy?.implementation_root, cwd, homeDir);
141
- if (artifactBaseRoot !== bootstrap.outputRoot) {
142
- errors.push(`output_policy.artifact_base_root must match bootstrap task root: ${bootstrap.outputRoot}`);
143
- }
144
- if (implementationRoot !== bootstrap.roots.implementation) {
145
- errors.push(`output_policy.implementation_root must match bootstrap implementation root: ${bootstrap.roots.implementation}`);
146
- }
147
- return errors;
148
- }
149
-
150
- function buildTemplate(mode = 'attended') {
151
- if (mode !== 'attended') {
152
- throw new Error('preflight --template supports attended mode only');
153
- }
154
- return {
155
- goal_id: 'goal-task-xxxxxxxx',
156
- created_at: new Date().toISOString(),
157
- end_goal: {
158
- intent: 'clean-room-reimplementation',
159
- success_definition: 'TBD: define the observable result the clean implementation must achieve.',
160
- destination_kind: 'new-project',
161
- existing_destination_policy: 'inspect-and-preserve',
162
- },
163
- target_stack: {
164
- language: 'TBD',
165
- runtime: null,
166
- framework: null,
167
- package_manager: null,
168
- test_framework: null,
169
- },
170
- license_policy: {
171
- source_license_notes: 'unknown',
172
- destination_license: 'TBD',
173
- dependency_license_allowlist: ['MIT', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause'],
174
- dependency_license_blocklist: ['GPL-3.0', 'AGPL-3.0'],
175
- },
176
- dependency_policy: {
177
- allow_new_dependencies: true,
178
- prefer_stdlib: true,
179
- require_user_approval_for_native_deps: true,
180
- blocked_dependencies: [],
181
- },
182
- compatibility_policy: {
183
- mirror_public_behavior: true,
184
- mirror_public_api_names: true,
185
- mirror_private_structure: false,
186
- mirror_comments_or_internal_names: false,
187
- allowed_exactness: [
188
- 'public API names',
189
- 'CLI flags',
190
- 'serialized outputs',
191
- 'documented protocol behavior',
192
- 'public error codes',
193
- ],
194
- },
195
- feature_policy: {
196
- preserve_features: [],
197
- remove_features: [],
198
- add_features: [],
199
- non_goals: [],
200
- },
201
- code_hygiene_policy: {
202
- max_lines_per_code_file: 500,
203
- max_lines_per_test_file: 800,
204
- max_files_per_iteration: 12,
205
- split_large_files_by: ['module boundary', 'public type', 'feature area'],
206
- exceptions: ['generated files', 'fixtures', 'snapshots'],
207
- forbidden_patterns: ['god file', 'source-shaped layout'],
208
- },
209
- output_policy: {
210
- artifact_base_root: '~/Documents/CleanRoom/<task-id>/',
211
- implementation_root: '~/Documents/CleanRoom/<task-id>/implementation/',
212
- assumed_output_directory: 'implementation/',
213
- write_mode: 'create-or-preserve-existing',
214
- },
215
- execution_policy: {
216
- backend: 'host',
217
- preferred_container_profile: 'node22',
218
- network_policy: 'off',
219
- dependency_install_policy: 'locked',
220
- allow_native_toolchain: false,
221
- resource_limits: {
222
- cpus: 2,
223
- memory_mb: 2048,
224
- timeout_seconds: 300,
225
- },
226
- },
227
- controller_policy: {
228
- mode: 'attended',
229
- unattended_allowed_after_preflight: false,
230
- max_iterations: 10,
231
- },
232
- open_questions: [
233
- {
234
- question_id: 'goal-end-state',
235
- question: 'Define the end goal, target stack, compatibility exactness, dependency policy, license policy, and output root before execution.',
236
- blocking: true,
237
- default_assumption: 'Do not start source analysis until this is answered.',
238
- },
239
- ],
240
- };
241
- }
242
-
243
- function expectObject(value, label, errors) {
244
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
245
- errors.push(`${label} must be an object`);
246
- return false;
247
- }
248
- return true;
249
- }
250
-
251
- function expectArray(value, label, errors) {
252
- if (!Array.isArray(value)) {
253
- errors.push(`${label} must be an array`);
254
- return false;
255
- }
256
- return true;
257
- }
258
-
259
- function expectString(value, label, errors, allowEmpty = false) {
260
- if (typeof value !== 'string' || (!allowEmpty && value.length === 0)) {
261
- errors.push(`${label} must be a non-empty string`);
262
- return false;
263
- }
264
- return true;
265
- }
266
-
267
- function expectBoolean(value, label, errors) {
268
- if (typeof value !== 'boolean') {
269
- errors.push(`${label} must be a boolean`);
270
- return false;
271
- }
272
- return true;
273
- }
274
-
275
- function expectPositiveInteger(value, label, errors) {
276
- if (!Number.isInteger(value) || value < 1) {
277
- errors.push(`${label} must be a positive integer`);
278
- return false;
279
- }
280
- return true;
281
- }
282
-
283
- function validateStringArray(root, field, errors) {
284
- if (!expectArray(root?.[field], field, errors)) return;
285
- for (const [index, item] of root[field].entries()) {
286
- expectString(item, `${field}[${index}]`, errors);
287
- }
288
- }
289
-
290
- function validateGoalContract(goal, options = {}) {
291
- const errors = [];
292
- if (!expectObject(goal, 'preflight goal', errors)) {
293
- return errors;
294
- }
295
-
296
- expectString(goal.goal_id, 'goal_id', errors);
297
- expectString(goal.created_at, 'created_at', errors);
298
-
299
- if (expectObject(goal.end_goal, 'end_goal', errors)) {
300
- if (!VALID_INTENTS.has(goal.end_goal.intent)) {
301
- errors.push('end_goal.intent is not supported');
302
- }
303
- expectString(goal.end_goal.success_definition, 'end_goal.success_definition', errors);
304
- expectString(goal.end_goal.destination_kind, 'end_goal.destination_kind', errors);
305
- expectString(goal.end_goal.existing_destination_policy, 'end_goal.existing_destination_policy', errors);
306
- }
307
-
308
- if (expectObject(goal.target_stack, 'target_stack', errors)) {
309
- expectString(goal.target_stack.language, 'target_stack.language', errors);
310
- for (const field of ['runtime', 'framework', 'package_manager', 'test_framework']) {
311
- if (goal.target_stack[field] !== null) {
312
- expectString(goal.target_stack[field], `target_stack.${field}`, errors);
313
- }
314
- }
315
- }
316
-
317
- if (expectObject(goal.license_policy, 'license_policy', errors)) {
318
- expectString(goal.license_policy.source_license_notes, 'license_policy.source_license_notes', errors, true);
319
- expectString(goal.license_policy.destination_license, 'license_policy.destination_license', errors);
320
- validateStringArray(goal.license_policy, 'dependency_license_allowlist', errors);
321
- validateStringArray(goal.license_policy, 'dependency_license_blocklist', errors);
322
- }
323
-
324
- if (expectObject(goal.dependency_policy, 'dependency_policy', errors)) {
325
- expectBoolean(goal.dependency_policy.allow_new_dependencies, 'dependency_policy.allow_new_dependencies', errors);
326
- expectBoolean(goal.dependency_policy.prefer_stdlib, 'dependency_policy.prefer_stdlib', errors);
327
- expectBoolean(
328
- goal.dependency_policy.require_user_approval_for_native_deps,
329
- 'dependency_policy.require_user_approval_for_native_deps',
330
- errors
331
- );
332
- validateStringArray(goal.dependency_policy, 'blocked_dependencies', errors);
333
- }
334
-
335
- if (expectObject(goal.compatibility_policy, 'compatibility_policy', errors)) {
336
- expectBoolean(goal.compatibility_policy.mirror_public_behavior, 'compatibility_policy.mirror_public_behavior', errors);
337
- expectBoolean(goal.compatibility_policy.mirror_public_api_names, 'compatibility_policy.mirror_public_api_names', errors);
338
- if (goal.compatibility_policy.mirror_private_structure !== false) {
339
- errors.push('compatibility_policy.mirror_private_structure must be false');
340
- }
341
- if (goal.compatibility_policy.mirror_comments_or_internal_names !== false) {
342
- errors.push('compatibility_policy.mirror_comments_or_internal_names must be false');
343
- }
344
- validateStringArray(goal.compatibility_policy, 'allowed_exactness', errors);
345
- }
346
-
347
- if (expectObject(goal.feature_policy, 'feature_policy', errors)) {
348
- for (const field of ['preserve_features', 'remove_features', 'add_features', 'non_goals']) {
349
- validateStringArray(goal.feature_policy, field, errors);
350
- }
351
- }
352
-
353
- validateCodeHygienePolicy(goal.code_hygiene_policy, errors);
354
-
355
- if (expectObject(goal.output_policy, 'output_policy', errors)) {
356
- expectString(goal.output_policy.artifact_base_root, 'output_policy.artifact_base_root', errors);
357
- expectString(goal.output_policy.implementation_root, 'output_policy.implementation_root', errors);
358
- expectString(goal.output_policy.assumed_output_directory, 'output_policy.assumed_output_directory', errors);
359
- expectString(goal.output_policy.write_mode, 'output_policy.write_mode', errors);
360
- }
361
-
362
- if (goal.execution_policy !== undefined) {
363
- validateExecutionPolicy(goal.execution_policy, errors);
364
- }
365
-
366
- if (expectObject(goal.controller_policy, 'controller_policy', errors)) {
367
- if (!VALID_MODES.has(goal.controller_policy.mode)) {
368
- errors.push('controller_policy.mode must be attended or unattended');
369
- }
370
- expectBoolean(
371
- goal.controller_policy.unattended_allowed_after_preflight,
372
- 'controller_policy.unattended_allowed_after_preflight',
373
- errors
374
- );
375
- expectPositiveInteger(goal.controller_policy.max_iterations, 'controller_policy.max_iterations', errors);
376
- }
377
-
378
- if (expectArray(goal.open_questions, 'open_questions', errors)) {
379
- for (const [index, question] of goal.open_questions.entries()) {
380
- if (!expectObject(question, `open_questions[${index}]`, errors)) continue;
381
- expectString(question.question_id, `open_questions[${index}].question_id`, errors);
382
- expectString(question.question, `open_questions[${index}].question`, errors);
383
- expectBoolean(question.blocking, `open_questions[${index}].blocking`, errors);
384
- }
385
- }
386
-
387
- if (goal.controller_policy?.mode === 'unattended') {
388
- if (goal.controller_policy.unattended_allowed_after_preflight !== true) {
389
- errors.push('unattended preflight requires unattended_allowed_after_preflight=true');
390
- }
391
- if (Array.isArray(goal.open_questions) && goal.open_questions.length > 0) {
392
- errors.push('unattended preflight requires no open_questions');
393
- }
394
- }
395
-
396
- if (options.requireComplete && Array.isArray(goal.open_questions)) {
397
- const blocking = goal.open_questions.filter((question) => question?.blocking === true);
398
- if (blocking.length > 0) {
399
- errors.push('completed preflight input must not contain blocking open_questions');
400
- }
401
- }
402
-
403
- return errors;
404
- }
405
-
406
- function validateExecutionPolicy(policy, errors) {
407
- if (!expectObject(policy, 'execution_policy', errors)) return;
408
- if (!VALID_EXECUTION_BACKENDS.has(policy.backend)) {
409
- errors.push('execution_policy.backend must be host, docker, or podman');
410
- }
411
- if (!VALID_CONTAINER_PROFILES.has(policy.preferred_container_profile)) {
412
- errors.push('execution_policy.preferred_container_profile is not supported');
413
- }
414
- if (!VALID_NETWORK_POLICIES.has(policy.network_policy)) {
415
- errors.push('execution_policy.network_policy must be off, deps-only, or on');
416
- }
417
- if (!VALID_DEPENDENCY_INSTALL_POLICIES.has(policy.dependency_install_policy)) {
418
- errors.push('execution_policy.dependency_install_policy must be offline, locked, or allow-new');
419
- }
420
- expectBoolean(policy.allow_native_toolchain, 'execution_policy.allow_native_toolchain', errors);
421
- if (!expectObject(policy.resource_limits, 'execution_policy.resource_limits', errors)) return;
422
- expectPositiveInteger(policy.resource_limits.memory_mb, 'execution_policy.resource_limits.memory_mb', errors);
423
- expectPositiveInteger(policy.resource_limits.timeout_seconds, 'execution_policy.resource_limits.timeout_seconds', errors);
424
- if (typeof policy.resource_limits.cpus !== 'number' || policy.resource_limits.cpus < 1 || policy.resource_limits.cpus > 16) {
425
- errors.push('execution_policy.resource_limits.cpus must be a number between 1 and 16');
426
- }
427
- if (Number.isInteger(policy.resource_limits.memory_mb) && policy.resource_limits.memory_mb > 65536) {
428
- errors.push('execution_policy.resource_limits.memory_mb must be at most 65536');
429
- }
430
- if (Number.isInteger(policy.resource_limits.timeout_seconds) && policy.resource_limits.timeout_seconds > 600) {
431
- errors.push('execution_policy.resource_limits.timeout_seconds must be at most 600');
432
- }
433
- }
434
-
435
- function validateCodeHygienePolicy(policy, errors) {
436
- if (!expectObject(policy, 'code_hygiene_policy', errors)) return;
437
- expectPositiveInteger(policy.max_lines_per_code_file, 'code_hygiene_policy.max_lines_per_code_file', errors);
438
- expectPositiveInteger(policy.max_lines_per_test_file, 'code_hygiene_policy.max_lines_per_test_file', errors);
439
- expectPositiveInteger(policy.max_files_per_iteration, 'code_hygiene_policy.max_files_per_iteration', errors);
440
- validateStringArray(policy, 'split_large_files_by', errors);
441
- validateStringArray(policy, 'exceptions', errors);
442
- if (policy.forbidden_patterns !== undefined) {
443
- validateStringArray(policy, 'forbidden_patterns', errors);
444
- }
445
- }
446
-
447
- function writePreflightOutput(outputPath, goal, options) {
448
- const data = `${JSON.stringify(goal, null, 2)}\n`;
449
- if (options.dryRun) {
450
- console.log(`Would write preflight goal: ${outputPath}`);
451
- return;
452
- }
453
- try {
454
- if (options.force) {
455
- atomicWriteFile(outputPath, data, 'utf8');
456
- } else {
457
- atomicWriteFileNoOverwrite(outputPath, data, 'utf8');
458
- }
459
- } catch (err) {
460
- if (err?.code === 'EEXIST') {
461
- throw new Error(`preflight output already exists; use --force to overwrite: ${outputPath}`);
462
- }
463
- throw err;
464
- }
465
- console.log(`Wrote preflight goal: ${outputPath}`);
466
- }
467
-
8
+ const { VALID_MODES } = require('./preflight-constants.cjs');
9
+ const { parsePreflightArgs, printPreflightHelp } = require('./preflight-cli.cjs');
10
+ const {
11
+ applyBootstrapOutputPolicy,
12
+ validateBootstrapOutputPolicy,
13
+ } = require('./preflight-bootstrap.cjs');
14
+ const { resolveInputPath, resolveOutputPath } = require('./preflight-paths.cjs');
15
+ const { buildTemplate } = require('./preflight-template.cjs');
16
+ const { validateGoalContract } = require('./preflight-validation.cjs');
17
+ const { writePreflightOutput } = require('./preflight-output.cjs');
18
+
19
+ /**
20
+ * Run the preflight flow from argv.
21
+ * @param {string[]} argv - Command line arguments.
22
+ * @param {object} [context={}] - Environment context containing cwd and homeDir.
23
+ * @returns {object|null} Parsed preflight result containing outputPath and goal, or null if help printed.
24
+ */
468
25
  function runPreflight(argv, context = {}) {
469
26
  const parsed = parsePreflightArgs(argv);
470
27
  if (parsed.help) {