sneakoscope 6.0.0 → 6.0.2

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 (89) hide show
  1. package/README.md +2 -1
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/bin/fast-inline.js +0 -40
  5. package/dist/bin/sks-dispatch.js +0 -16
  6. package/dist/cli/codex-app-command.js +1 -2
  7. package/dist/cli/help-fast.js +2 -2
  8. package/dist/cli/install-helpers.js +26 -21
  9. package/dist/config/skills-manifest.json +57 -57
  10. package/dist/core/agents/agent-effort-policy.js +19 -38
  11. package/dist/core/agents/agent-orchestrator.js +2 -2
  12. package/dist/core/agents/agent-role-config.js +17 -10
  13. package/dist/core/agents/agent-roster.js +19 -39
  14. package/dist/core/agents/agent-scheduler.js +6 -5
  15. package/dist/core/agents/agent-schema.js +4 -5
  16. package/dist/core/agents/agent-task-graph.js +1 -1
  17. package/dist/core/agents/agent-work-partition.js +2 -1
  18. package/dist/core/agents/native-worker-backend-router.js +1 -1
  19. package/dist/core/agents/route-collaboration-ledger.js +2 -2
  20. package/dist/core/auto-review.js +1 -4
  21. package/dist/core/codex/codex-config-guard.js +11 -9
  22. package/dist/core/codex-adapter.js +2 -2
  23. package/dist/core/codex-app/codex-agent-role-sync.js +0 -1
  24. package/dist/core/codex-app/codex-app-fast-ui-repair.js +7 -4
  25. package/dist/core/codex-app/codex-app-ui-state-snapshot.js +1 -7
  26. package/dist/core/codex-app.js +0 -2
  27. package/dist/core/codex-control/codex-model-capabilities.js +1 -2
  28. package/dist/core/codex-control/codex-model-metadata.js +13 -6
  29. package/dist/core/codex-control/codex-sdk-config-policy.js +3 -3
  30. package/dist/core/codex-control/codex-task-runner.js +2 -3
  31. package/dist/core/codex-model-guard.js +24 -89
  32. package/dist/core/commands/basic-cli.js +6 -0
  33. package/dist/core/commands/loop-command.js +2 -2
  34. package/dist/core/commands/mad-db-command.js +1 -1
  35. package/dist/core/commands/naruto-command.js +11 -11
  36. package/dist/core/commands/profile-command.js +14 -5
  37. package/dist/core/commands/qa-loop-command.js +2 -2
  38. package/dist/core/commands/research-command.js +2 -2
  39. package/dist/core/doctor/doctor-codex-startup-repair.js +0 -3
  40. package/dist/core/evaluation.js +1 -1
  41. package/dist/core/feature-fixtures.js +2 -2
  42. package/dist/core/feature-registry.js +1 -1
  43. package/dist/core/hooks-runtime.js +7 -45
  44. package/dist/core/image-ux-review/imagegen-adapter.js +17 -2
  45. package/dist/core/init/skills.js +6 -7
  46. package/dist/core/init.js +7 -20
  47. package/dist/core/loops/loop-concurrency-budget.js +6 -4
  48. package/dist/core/loops/loop-planner.js +1 -1
  49. package/dist/core/loops/loop-runtime.js +2 -2
  50. package/dist/core/loops/loop-scheduler.js +7 -7
  51. package/dist/core/managed-assets/managed-assets-manifest.js +1 -4
  52. package/dist/core/naruto/naruto-concurrency-governor.js +20 -17
  53. package/dist/core/naruto/naruto-loop-mesh.js +3 -3
  54. package/dist/core/naruto/resource-pressure-monitor.js +5 -2
  55. package/dist/core/provider/model-router.js +15 -35
  56. package/dist/core/release/release-gate-batch-runner.js +2 -1
  57. package/dist/core/release/release-gate-resource-governor.js +12 -12
  58. package/dist/core/release/resource-class-budget.js +5 -5
  59. package/dist/core/routes/constants.js +3 -4
  60. package/dist/core/routes.js +1 -1
  61. package/dist/core/structured-output-adapter.js +15 -1
  62. package/dist/core/update/update-migration-state.js +3 -16
  63. package/dist/core/version.js +1 -1
  64. package/dist/scripts/agent-backfill-replenishment-check.js +1 -1
  65. package/dist/scripts/agent-dynamic-pool-check.js +3 -3
  66. package/dist/scripts/agent-dynamic-pool-fixture.js +1 -1
  67. package/dist/scripts/agent-goal-mode-propagation-check.js +1 -1
  68. package/dist/scripts/agent-patch-swarm-gate-lib.js +2 -2
  69. package/dist/scripts/agent-role-config-repair-check.js +9 -5
  70. package/dist/scripts/agent-scheduler-proof-check.js +1 -1
  71. package/dist/scripts/agent-scheduler-proof-hardening-check.js +1 -1
  72. package/dist/scripts/agent-source-intelligence-propagation-check.js +1 -1
  73. package/dist/scripts/agent-strategy-to-lease-wiring-check.js +2 -2
  74. package/dist/scripts/agent-zellij-dynamic-backfill-panes-check.js +1 -1
  75. package/dist/scripts/codex-app-fast-ui-preservation-check.js +2 -2
  76. package/dist/scripts/codex-app-ui-preservation-check.js +6 -5
  77. package/dist/scripts/codex-lb-fast-ui-preservation-check.js +4 -4
  78. package/dist/scripts/codex-lb-gpt55-fast-profile-check.js +3 -2
  79. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +6 -6
  80. package/dist/scripts/install-update-preserves-config-check.js +3 -3
  81. package/dist/scripts/loop-hardening-check-lib.js +1 -1
  82. package/dist/scripts/naruto-parallelism-mode-check.js +2 -1
  83. package/dist/scripts/naruto-shadow-clone-swarm-check.js +25 -32
  84. package/dist/scripts/npm-publish-performance-check.js +1 -1
  85. package/dist/scripts/release-aggressive-resource-governor-check.js +2 -2
  86. package/dist/scripts/release-parallel-check.js +1 -2
  87. package/dist/scripts/release-real-check.js +1 -1
  88. package/dist/scripts/sizecheck.js +3 -3
  89. package/package.json +3 -2
@@ -3,18 +3,9 @@ import { DEFAULT_AGENT_CONCURRENCY, DEFAULT_AGENT_COUNT, DEFAULT_NARUTO_CLONES,
3
3
  import { defaultAgentPersonas, validatePersonaUniqueness } from './agent-persona.js';
4
4
  import { buildAgentEffortPolicy, decideAgentEffort, decideNarutoCloneEffort } from './agent-effort-policy.js';
5
5
  import { mapNarutoRoleToAgentRole, narutoRoleAllowsWrite } from '../naruto/naruto-role-policy.js';
6
- // $Naruto must never blindly spawn the full clone count at once, but the live
7
- // CONCURRENCY ceiling is NOT a function of CPU cores. Each clone is a separate CLI
8
- // worker process that spends ~all of its wall-clock awaiting the Codex API
9
- // (network-bound, mostly idle), so the local CPU is never the bottleneck —
10
- // oversubscribing cores is exactly the point. The real local limit is MEMORY
11
- // (resident set of N node+codex child processes) plus an absolute clone ceiling
12
- // (MAX_NARUTO_AGENT_COUNT = 100); the provider rate limit is handled separately by
13
- // the responses retry/backoff policy. So a capable host can run up to 100 in
14
- // parallel regardless of core count. Tunables:
15
- // SKS_NARUTO_MAX_CONCURRENCY hard cap (1..100), wins over everything
16
- // SKS_NARUTO_GB_PER_WORKER memory budget per heavy worker (default 0.25 GB)
17
- // SKS_NARUTO_MIN_CONCURRENCY floor so low-free-memory hosts still parallelize
6
+ // The roster may contain many queued clones, but active Codex workers are real
7
+ // Node/Codex processes. Reserve interactive CPU and memory so the desktop stays
8
+ // responsive; environment overrides can lower this cap, never raise it.
18
9
  export function systemSafeNarutoConcurrency(opts = {}) {
19
10
  const cores = Math.max(1, Number(opts.cores ?? os.cpus()?.length) || 4);
20
11
  let freeBytes = 2 * 1024 * 1024 * 1024;
@@ -31,40 +22,28 @@ export function systemSafeNarutoConcurrency(opts = {}) {
31
22
  const totalGb = totalBytes / (1024 * 1024 * 1024);
32
23
  const backend = String(opts.backend || 'codex-sdk');
33
24
  const heavy = backend === 'codex-sdk' || backend === 'zellij' || backend === 'process' || backend === 'ollama';
34
- const ceiling = MAX_NARUTO_AGENT_COUNT;
35
- // macOS reports very low freemem while reclaimable memory is still available, so
36
- // budget against a total-memory-derived floor rather than the instantaneous free.
37
- const reclaimableFloorGb = totalGb >= 32 ? 16 : totalGb >= 16 ? 8 : totalGb >= 8 ? 4 : totalGb >= 4 ? 2 : Math.max(freeGb, 1);
38
- const budgetGb = Math.max(freeGb, reclaimableFloorGb);
39
- let cap;
40
- if (heavy) {
41
- // Memory-bound, NOT core-bound. ~0.25 GB per mostly-idle Codex SDK worker.
42
- const gbPerWorker = positiveEnvNumber('SKS_NARUTO_GB_PER_WORKER', 0.25);
43
- const byMem = Math.max(1, Math.floor(budgetGb / gbPerWorker));
44
- const minParallelDefault = totalGb >= 16 ? 16 : totalGb >= 8 ? 8 : totalGb >= 4 ? 4 : 2;
45
- const minParallel = Math.floor(positiveEnvNumber('SKS_NARUTO_MIN_CONCURRENCY', minParallelDefault));
46
- cap = Math.min(Math.max(byMem, minParallel), ceiling);
47
- }
48
- else {
49
- // In-process / light workers are even cheaper; pack toward the ceiling, and never
50
- // throttle tighter than the heavy backend (invariant: heavy.cap <= fake.cap).
51
- const gbPerWorker = positiveEnvNumber('SKS_NARUTO_LIGHT_GB_PER_WORKER', 0.1);
52
- const byMem = Math.max(2, Math.floor(budgetGb / gbPerWorker));
53
- cap = Math.min(byMem, ceiling);
54
- }
25
+ const reserveGb = Math.max(2, totalGb * 0.2);
26
+ const budgetGb = Math.max(0.5, freeGb - reserveGb);
27
+ const gbPerWorker = positiveEnvNumber(heavy ? 'SKS_NARUTO_GB_PER_WORKER' : 'SKS_NARUTO_LIGHT_GB_PER_WORKER', heavy ? 1.5 : 0.5);
28
+ const byMem = Math.max(1, Math.floor(budgetGb / gbPerWorker));
29
+ const byCpu = Math.max(1, Math.min(4, Math.floor(cores * (heavy ? 0.4 : 0.5))));
30
+ const load = Math.max(0, Number(opts.loadAverage ?? os.loadavg()[0]) || 0);
31
+ const byLoad = load >= cores ? 1 : load >= cores * 0.75 ? Math.min(2, byCpu) : byCpu;
32
+ let cap = Math.min(byMem, byCpu, byLoad, 4);
55
33
  const override = Number(process.env.SKS_NARUTO_MAX_CONCURRENCY);
56
34
  if (Number.isFinite(override) && override >= 1)
57
- cap = Math.min(Math.floor(override), MAX_NARUTO_AGENT_COUNT);
58
- cap = Math.max(1, Math.min(cap, MAX_NARUTO_AGENT_COUNT));
35
+ cap = Math.min(cap, Math.floor(override));
36
+ cap = Math.max(1, Math.min(cap, 4));
59
37
  return {
60
38
  cap,
61
39
  cores,
62
40
  free_gb: Math.round(freeGb * 10) / 10,
63
41
  total_gb: Math.round(totalGb * 10) / 10,
42
+ load_average: Math.round(load * 100) / 100,
64
43
  backend,
65
44
  heavy,
66
45
  override_applied: Number.isFinite(override) && override >= 1,
67
- memory_model: heavy ? 'memory_budget_network_bound' : 'light_worker_memory_bound'
46
+ memory_model: heavy ? 'reserved_interactive_memory_heavy_worker' : 'reserved_interactive_memory_light_worker'
68
47
  };
69
48
  }
70
49
  function positiveEnvNumber(name, fallback) {
@@ -87,12 +66,13 @@ export function normalizeAgentCount(value, fallback = DEFAULT_AGENT_COUNT, maxAg
87
66
  return count;
88
67
  }
89
68
  export function normalizeAgentConcurrency(value, agents, maxAgentCount = MAX_AGENT_COUNT) {
90
- const parsed = Number(value ?? Math.min(agents, DEFAULT_AGENT_CONCURRENCY));
69
+ const desktopSafeCap = Math.max(1, Math.min(agents, maxAgentCount, DEFAULT_AGENT_CONCURRENCY));
70
+ const parsed = Number(value ?? desktopSafeCap);
91
71
  if (!Number.isFinite(parsed) || parsed < 1)
92
- return Math.min(agents, DEFAULT_AGENT_CONCURRENCY);
72
+ return desktopSafeCap;
93
73
  if (parsed > maxAgentCount)
94
74
  throw new Error('Agent concurrency ' + parsed + ' exceeds max ' + maxAgentCount);
95
- return Math.min(Math.floor(parsed), agents);
75
+ return Math.min(Math.floor(parsed), desktopSafeCap);
96
76
  }
97
77
  export function buildAgentRoster(opts = {}) {
98
78
  const maxAgentCount = resolveMaxAgentCount(opts.maxAgentCount);
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { appendJsonl, appendJsonlMany, nowIso, writeJsonAtomic } from '../fsx.js';
3
- import { MAX_AGENT_COUNT } from './agent-schema.js';
3
+ import { DEFAULT_AGENT_CONCURRENCY, MAX_AGENT_COUNT } from './agent-schema.js';
4
4
  import { appendAgentWorkQueueEvent, completeWorkItem, createAgentWorkQueue, enqueueFollowUpWorkItems, leaseNextWorkItem, pendingWorkItems, writeAgentWorkQueue } from './agent-work-queue.js';
5
5
  import { closeWorkerSlotsAfterDrain, createAgentWorkerSlots, markWorkerSlotGenerationClosed, openWorkerSlotGeneration, writeAgentWorkerSlots } from './agent-worker-slot.js';
6
6
  import { closeAgentSessionGeneration, createAgentSessionGeneration, writeAgentSessionGeneration } from './agent-session-generation.js';
@@ -9,7 +9,7 @@ export const AGENT_SCHEDULER_SCHEMA = 'sks.agent-scheduler.v1';
9
9
  export const AGENT_SCHEDULER_EVENT_SCHEMA = 'sks.agent-scheduler-event.v1';
10
10
  export async function runAgentScheduler(input) {
11
11
  const maxActiveSlots = Number.isFinite(Number(input.maxActiveSlots)) && Number(input.maxActiveSlots) >= 1 ? Math.floor(Number(input.maxActiveSlots)) : MAX_AGENT_COUNT;
12
- const targetActiveSlots = normalizeTargetActiveSlots(input.targetActiveSlots ?? input.roster?.agent_count ?? input.roster?.concurrency ?? 5, maxActiveSlots);
12
+ const targetActiveSlots = normalizeTargetActiveSlots(input.targetActiveSlots ?? input.roster?.concurrency ?? input.roster?.agent_count ?? DEFAULT_AGENT_CONCURRENCY, maxActiveSlots);
13
13
  let slots = createAgentWorkerSlots(input.roster, targetActiveSlots);
14
14
  const queue = createAgentWorkQueue({
15
15
  slices: input.partition?.slices || [],
@@ -344,10 +344,11 @@ export async function runAgentScheduler(input) {
344
344
  }
345
345
  }
346
346
  export function normalizeTargetActiveSlots(value, maxActiveSlots = MAX_AGENT_COUNT) {
347
- const cap = Number.isFinite(Number(maxActiveSlots)) && Number(maxActiveSlots) >= 1 ? Math.floor(Number(maxActiveSlots)) : MAX_AGENT_COUNT;
348
- const parsed = Number(value ?? 5);
347
+ const configuredCap = Number.isFinite(Number(maxActiveSlots)) && Number(maxActiveSlots) >= 1 ? Math.floor(Number(maxActiveSlots)) : MAX_AGENT_COUNT;
348
+ const cap = Math.max(1, Math.min(configuredCap, DEFAULT_AGENT_CONCURRENCY));
349
+ const parsed = Number(value ?? DEFAULT_AGENT_CONCURRENCY);
349
350
  if (!Number.isFinite(parsed) || parsed < 1)
350
- return Math.min(cap, 5);
351
+ return cap;
351
352
  return Math.min(cap, Math.floor(parsed));
352
353
  }
353
354
  function buildState(missionId, targetActiveSlots, queue, slots, active, opts) {
@@ -8,12 +8,11 @@ export const DEFAULT_AGENT_COUNT = 5;
8
8
  export const AGENT_COUNT = DEFAULT_AGENT_COUNT;
9
9
  export const AGENT_INTAKE_STAGE_ID = 'native_agent_intake';
10
10
  export const MAX_AGENT_COUNT = 20;
11
- export const DEFAULT_AGENT_CONCURRENCY = 5;
12
- // $Naruto "Shadow Clone" swarm: a high-scale mode that lifts the standard 20-agent
13
- // ceiling to up to 100 concurrent clone sessions. Only the naruto path opts into this
14
- // cap; every other roster/scheduler caller keeps MAX_AGENT_COUNT as the default.
11
+ export const DEFAULT_AGENT_CONCURRENCY = 4;
12
+ // Naruto may queue a large roster, but active workers are governed separately and
13
+ // remain bounded by live CPU, memory, load, and I/O pressure.
15
14
  export const MAX_NARUTO_AGENT_COUNT = 100;
16
- export const DEFAULT_NARUTO_CLONES = 32;
15
+ export const DEFAULT_NARUTO_CLONES = 8;
17
16
  export const AGENT_BACKENDS = ['fake', 'process', 'codex-sdk', 'zellij', 'ollama', 'local-llm'];
18
17
  export function normalizeAgentBackend(input) {
19
18
  const value = String(input || 'codex-sdk');
@@ -3,7 +3,7 @@ import { nowIso, sha256, writeJsonAtomic } from '../fsx.js';
3
3
  export const AGENT_TASK_GRAPH_SCHEMA = 'sks.agent-task-graph.v1';
4
4
  export function buildAgentTaskGraph(input) {
5
5
  const routeType = normalizeRouteType(input.routeType || '$Agent');
6
- const targetActiveSlots = Math.max(1, Math.floor(Number(input.targetActiveSlots || 5)));
6
+ const targetActiveSlots = Math.max(1, Math.min(4, Math.floor(Number(input.targetActiveSlots || 4))));
7
7
  const minimumWorkItems = Math.max(1, Math.floor(Number(input.minimumWorkItems || targetActiveSlots)));
8
8
  const desiredWorkItems = Math.max(minimumWorkItems, Math.floor(Number(input.desiredWorkItems || minimumWorkItems)));
9
9
  const domainTemplates = routeTemplates(routeType);
@@ -7,12 +7,13 @@ import { detectAgentLeaseConflicts } from './work-partition/conflict-detector.js
7
7
  import { buildNoOverlapProof } from './work-partition/no-overlap-proof.js';
8
8
  import { buildAgentTaskGraph } from './agent-task-graph.js';
9
9
  import { buildIntelligentWorkGraph, enhanceTaskGraphWithIntelligence } from './intelligent-work-graph.js';
10
+ import { DEFAULT_AGENT_CONCURRENCY } from './agent-schema.js';
10
11
  export async function buildAgentWorkPartition(root, roster, prompt = '', opts = {}) {
11
12
  const inventory = await collectRepoInventory(root);
12
13
  const dependency_graph = buildDependencyGraph(inventory);
13
14
  const semantic_domain_graph = buildSemanticDomainGraph(inventory);
14
15
  const sessions = Object.fromEntries((roster.roster || []).map((agent) => [agent.id, agent.session_id]));
15
- const targetActiveSlots = Number(opts.targetActiveSlots || roster.agent_count || roster.concurrency || 5);
16
+ const targetActiveSlots = Math.max(1, Math.min(DEFAULT_AGENT_CONCURRENCY, Number(opts.targetActiveSlots || roster.concurrency || roster.agent_count || DEFAULT_AGENT_CONCURRENCY)));
16
17
  const intelligent_work_graph = await buildIntelligentWorkGraph({
17
18
  root,
18
19
  inventory,
@@ -274,7 +274,7 @@ async function resolveWorkerModelRouting(input) {
274
274
  reasoning: explicitReasoning || normalizeModelReasoning(input.agent?.model_reasoning_effort) || 'medium',
275
275
  serviceTier: explicitTier || input.fastModePolicy.service_tier || 'fast'
276
276
  }
277
- : await routeModel(category, { lbHealth });
277
+ : await routeModel(category, { lbHealth, model: input.agent?.model || null });
278
278
  return {
279
279
  category,
280
280
  choice: routed,
@@ -38,7 +38,7 @@ export async function writeRouteCollaborationArtifacts(root, opts) {
38
38
  route: opts.route,
39
39
  prompt: opts.prompt || routeKey + ' native collaboration fixture',
40
40
  agents: opts.agents || 5,
41
- concurrency: opts.concurrency || 5,
41
+ concurrency: opts.concurrency || 4,
42
42
  backend,
43
43
  mock: backend === 'fake',
44
44
  readonly: true
@@ -64,7 +64,7 @@ export async function writeRouteCollaborationArtifacts(root, opts) {
64
64
  fake_backend_fixture: backend === 'fake',
65
65
  real_mode_codex_sdk_backend: {
66
66
  supported: true,
67
- command_pattern: 'sks agent run "<task>" --backend codex-sdk --real --agents <1-20> --concurrency <1-20> --json',
67
+ command_pattern: 'sks agent run "<task>" --backend codex-sdk --real --agents <1-20> --concurrency <1-4> --json',
68
68
  requires_user_runtime: true
69
69
  },
70
70
  route_specific_personas: ROUTE_PERSONAS[routeKey] || [],
@@ -2,7 +2,6 @@ import os from 'node:os';
2
2
  import path from 'node:path';
3
3
  import { ensureDir, exists, readText, writeTextAtomic } from './fsx.js';
4
4
  import { writeCodexConfigGuarded } from './codex/codex-config-guard.js';
5
- import { REQUIRED_CODEX_MODEL } from './codex-model-guard.js';
6
5
  export const AUTO_REVIEW_REVIEWER = 'auto_review';
7
6
  export const LEGACY_AUTO_REVIEW_REVIEWER = 'guardian_subagent';
8
7
  export const AUTO_REVIEW_PROFILE = 'sks-auto-review';
@@ -36,7 +35,7 @@ export const REVIEW_NATIVE_AGENT_PLAN = Object.freeze({
36
35
  }
37
36
  ],
38
37
  safety_personas_read_only_by_default: true,
39
- manual_agent_count_syntax: 'sks auto-review fixture --json and sks agent run "<review task>" --route $Review --agents 5 --concurrency 5 --mock --json',
38
+ manual_agent_count_syntax: 'sks auto-review fixture --json and sks agent run "<review task>" --route $Review --agents 5 --concurrency 4 --mock --json',
40
39
  dynamic_effort: 'parent assigns high effort to safety/integrator lanes and medium or higher to verification lanes when proof risk is present'
41
40
  });
42
41
  export function codexHome(env = process.env) {
@@ -119,7 +118,6 @@ export const SKS_CONFIG_PROFILES = [
119
118
  ];
120
119
  function sksProfileFileBlock(opts = {}) {
121
120
  return [
122
- `model = "${REQUIRED_CODEX_MODEL}"`,
123
121
  `service_tier = "${opts.serviceTier || 'fast'}"`,
124
122
  `approval_policy = "${opts.approvalPolicy || 'on-request'}"`,
125
123
  ...(opts.reviewer ? [`approvals_reviewer = "${opts.reviewer}"`] : []),
@@ -315,7 +313,6 @@ function profileConfigBlock(opts = {}) {
315
313
  const approvalPolicy = opts.approvalPolicy || 'on-request';
316
314
  const sandboxMode = opts.sandboxMode || 'workspace-write';
317
315
  return [
318
- `model = "${REQUIRED_CODEX_MODEL}"`,
319
316
  'service_tier = "fast"',
320
317
  `approval_policy = "${approvalPolicy}"`,
321
318
  `approvals_reviewer = "${reviewer}"`,
@@ -2,7 +2,6 @@ import path from 'node:path';
2
2
  import { appendJsonl, ensureDir, nowIso, readText, sha256, writeTextAtomic } from '../fsx.js';
3
3
  import { diffCodexAppUiSnapshots, snapshotCodexAppUiState } from '../codex-app/codex-app-ui-state-snapshot.js';
4
4
  import { cleanupCodexConfigBackups, validateCodexConfigRoundTrip } from './codex-config-toml.js';
5
- import { GPT55_CODEX_MODEL, REQUIRED_CODEX_MODEL } from '../codex-model-guard.js';
6
5
  // fast_mode_ui was removed from the [features] schema in the 2026-07 renewal.
7
6
  const FAST_FEATURE_KEYS = ['fast_mode'];
8
7
  export async function writeCodexConfigGuarded(input) {
@@ -154,21 +153,24 @@ export function isUnmanagedProjectCodexConfig(root, configPath, text) {
154
153
  && !hasSksManagedCodexConfigMarker(text);
155
154
  }
156
155
  export function removeLegacyTopLevelCodexModeLocks(text = '') {
157
- const lines = String(text || '').split('\n');
156
+ const lines = String(ensureTrailingNewline(text) || '').split('\n');
158
157
  const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
159
158
  const end = firstTable === -1 ? lines.length : firstTable;
160
- const topLevelModel = topLevelTomlString(text, 'model');
161
- // Current default stamp OR the legacy gpt-5.5 stamp older SKS versions wrote,
162
- // so upgrades keep cleaning locks written before the 5.6-terra default bump.
163
- const removeSksOwnedModeLock = topLevelModel === REQUIRED_CODEX_MODEL || topLevelModel === GPT55_CODEX_MODEL;
164
159
  return ensureTrailingNewline(lines.filter((line, index) => {
165
160
  if (index >= end)
166
161
  return true;
167
- if (!removeSksOwnedModeLock)
162
+ if (!/^\s*(?:model|model_reasoning_effort)\s*=/.test(line))
168
163
  return true;
169
- return !/^\s*(?:model|model_reasoning_effort)\s*=/.test(line);
164
+ return !isSksProvenanceLine(line, lines[index - 1] || '');
170
165
  }).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n'));
171
166
  }
167
+ function isSksProvenanceLine(...lines) {
168
+ return lines.some((line) => {
169
+ const trimmed = String(line || '').trim();
170
+ const comment = trimmed.startsWith('#') ? trimmed : trimmed.includes('#') ? trimmed.slice(trimmed.indexOf('#')) : '';
171
+ return /(?:SKS|Sneakoscope|codex-lb|sks fast)/i.test(comment);
172
+ });
173
+ }
172
174
  function mergeLostFastUiKeys(before, nextInput) {
173
175
  let next = String(nextInput || '');
174
176
  const keys = [];
@@ -189,7 +191,7 @@ function mergeLostFastUiKeys(before, nextInput) {
189
191
  return { text: ensureTrailingNewline(next), keys };
190
192
  }
191
193
  function topLevelModeLocks(text) {
192
- return ['model', 'model_reasoning_effort'].filter((key) => hasTopLevelTomlKey(text, key));
194
+ return ['model_reasoning_effort'].filter((key) => hasTopLevelTomlKey(text, key));
193
195
  }
194
196
  function topLevelTomlKeyLine(text, key) {
195
197
  const lines = String(text || '').split('\n');
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { runProcess } from './fsx.js';
3
- import { forceRequiredCodexModelArgs } from './codex-model-guard.js';
3
+ import { preserveCodexModelArgs } from './codex-model-guard.js';
4
4
  import { managedProxyEnvForChild } from './codex/managed-proxy-env.js';
5
5
  import { resolveCodexRuntime } from './codex-runtime/resolve-codex-runtime.js';
6
6
  export async function findCodexBinary() {
@@ -35,7 +35,7 @@ export function buildCodexExecArgs({ root, prompt, outputFile, json = true, prof
35
35
  args.push('--json');
36
36
  if (outputFile)
37
37
  args.push('--output-last-message', outputFile);
38
- args.push(...forceRequiredCodexModelArgs(extraArgs));
38
+ args.push(...preserveCodexModelArgs(extraArgs));
39
39
  args.push(prompt);
40
40
  return args;
41
41
  }
@@ -87,7 +87,6 @@ function roleToml(role, payload) {
87
87
  return [
88
88
  `name = "${role}"`,
89
89
  `description = "SKS managed ${PACKAGE_VERSION} directive role: ${role}"`,
90
- 'model_reasoning_effort = "medium"',
91
90
  role.includes('implementer') ? 'sandbox_mode = "workspace-write"' : 'sandbox_mode = "read-only"',
92
91
  'approval_policy = "never"',
93
92
  'developer_instructions = """',
@@ -6,7 +6,6 @@ import { assertCodexAppUiMutationAllowed } from './codex-app-ui-clobber-guard.js
6
6
  import { codexProviderModelUiStatus } from '../codex-app.js';
7
7
  export const CODEX_APP_FAST_UI_REPAIR_SCHEMA = 'sks.codex-app-fast-ui-repair.v1';
8
8
  const FAST_UI_TOP_LEVEL_RE = /^\s*(?:model|model_reasoning_effort|service_tier)\s*=/;
9
- const CODEX_APP_MODE_LOCK_TOP_LEVEL_RE = /^\s*(?:model|model_reasoning_effort)\s*=/;
10
9
  const FAST_UI_FEATURE_LINE_RE = /^\s*fast_mode\s*=/;
11
10
  const FAST_UI_LEGACY_TABLES = new Set(['user.fast_mode', 'profiles.sks-fast-high']);
12
11
  const SKS_CAUSED_RE = /(?:SKS|Sneakoscope|codex-lb|sks-mad|sks fast)/i;
@@ -135,14 +134,18 @@ function stripProjectLocalForbiddenKeys(text) {
135
134
  });
136
135
  }
137
136
  function stripSksCausedHostOwnedLines(text) {
138
- return stripMatchingLines(text, (line, table, previous, next) => {
139
- const isCodexAppModeLock = !table && CODEX_APP_MODE_LOCK_TOP_LEVEL_RE.test(line);
137
+ const topLevel = String(text || '').split(/\n\s*\[/)[0] || '';
138
+ const sksManagedTopLevel = SKS_CAUSED_RE.test(topLevel);
139
+ const sksManagedDocument = SKS_CAUSED_RE.test(String(text || ''));
140
+ const stripped = stripMatchingLines(text, (line, table, previous, next) => {
140
141
  const isLegacyFastTable = table ? FAST_UI_LEGACY_TABLES.has(table) : false;
141
142
  const isFastUiLine = FAST_UI_TOP_LEVEL_RE.test(line)
142
143
  || (table === 'features' && FAST_UI_FEATURE_LINE_RE.test(line))
143
144
  || isLegacyFastTable;
144
- return isCodexAppModeLock || (isFastUiLine && (SKS_CAUSED_RE.test(line) || SKS_CAUSED_RE.test(previous) || SKS_CAUSED_RE.test(next)));
145
+ const sksMarked = (!table && sksManagedTopLevel) || (isLegacyFastTable && sksManagedDocument) || SKS_CAUSED_RE.test(line) || SKS_CAUSED_RE.test(previous) || SKS_CAUSED_RE.test(next);
146
+ return isFastUiLine && sksMarked;
145
147
  });
148
+ return stripped;
146
149
  }
147
150
  function stripMatchingLines(text, shouldRemove) {
148
151
  const lines = text.split(/\r?\n/);
@@ -4,16 +4,10 @@ import path from 'node:path';
4
4
  import { exists, nowIso, readText, sha256, writeJsonAtomic } from '../fsx.js';
5
5
  export const CODEX_APP_UI_STATE_SNAPSHOT_SCHEMA = 'sks.codex-app-ui-state-snapshot.v1';
6
6
  export const PROJECT_LOCAL_FORBIDDEN_CODEX_KEYS = [
7
- 'model',
8
- 'model_reasoning_effort',
9
7
  'openai_base_url',
10
8
  'chatgpt_base_url',
11
9
  'apps_mcp_product_sku',
12
- 'model_provider',
13
- 'model_providers',
14
10
  'notify',
15
- 'profile',
16
- 'profiles',
17
11
  'experimental_realtime_ws_base_url',
18
12
  'otel'
19
13
  ];
@@ -45,7 +39,7 @@ export async function snapshotCodexAppUiState(root = process.cwd(), input = {})
45
39
  return true;
46
40
  if (signal.key_path.startsWith('user.fast_mode') && /hidden|fixed|disabled|false/i.test(signal.value_preview))
47
41
  return true;
48
- if (signal.key_path === 'model' || signal.key_path === 'model_reasoning_effort')
42
+ if ((signal.key_path === 'model' || signal.key_path === 'model_reasoning_effort') && signal.sks_related)
49
43
  return true;
50
44
  if (signal.key_path === 'service_tier' && signal.sks_related)
51
45
  return true;
@@ -858,8 +858,6 @@ async function codexFastModeConfigStatus(opts = {}) {
858
858
  if (!config.text)
859
859
  continue;
860
860
  const topLevel = topLevelToml(config.text);
861
- if (/(^|\n)\s*model\s*=/.test(topLevel))
862
- blockers.push(`${config.scope}:top_level_model`);
863
861
  if (/(^|\n)\s*model_reasoning_effort\s*=/.test(topLevel))
864
862
  blockers.push(`${config.scope}:top_level_model_reasoning_effort`);
865
863
  if (/(^|\n)\s*fast_default_opt_out\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(tomlTable(config.text, 'notice')))
@@ -1,7 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { writeJsonAtomic } from '../fsx.js';
3
3
  import { collectCodexModelMetadata } from './codex-model-metadata.js';
4
- import { REQUIRED_CODEX_MODEL } from '../codex-model-guard.js';
5
4
  export const SKS_FALLBACK_EFFORT_ORDER = ['minimal', 'low', 'medium', 'high', 'xhigh'];
6
5
  export function codexModelEffortCapability(input = {}) {
7
6
  const metadataIsFallback = input.metadata?.source === 'fallback';
@@ -10,7 +9,7 @@ export function codexModelEffortCapability(input = {}) {
10
9
  const requestedDefault = input.metadata?.default_effort || input.defaultEffort;
11
10
  const defaultEffort = order.includes(String(requestedDefault || '')) ? String(requestedDefault) : order.includes('medium') ? 'medium' : order[0] || 'medium';
12
11
  return {
13
- model: String(input.metadata?.model || input.model || process.env.SKS_CODEX_MODEL || process.env.CODEX_MODEL || REQUIRED_CODEX_MODEL),
12
+ model: String(input.metadata?.model || input.model || process.env.SKS_CODEX_MODEL || process.env.CODEX_MODEL || ''),
14
13
  advertised_efforts: order,
15
14
  default_effort: defaultEffort,
16
15
  order_source: advertised.length ? 'model-advertised' : 'sks-fallback',
@@ -1,13 +1,12 @@
1
1
  import { findCodexBinary } from '../codex-adapter.js';
2
2
  import { runProcess } from '../fsx.js';
3
- import { REQUIRED_CODEX_MODEL } from '../codex-model-guard.js';
4
3
  const FALLBACK_EFFORT_ORDER = ['minimal', 'low', 'medium', 'high', 'xhigh'];
5
4
  export async function collectCodexModelMetadata(input = {}) {
6
5
  if (process.env.SKS_CODEX_MODEL_METADATA_FAKE === '1') {
7
6
  const advertised = normalizeAdvertisedEfforts(process.env.SKS_CODEX_MODEL_EFFORTS || 'low,medium,high,xhigh');
8
- return metadata(String(input.model || process.env.SKS_CODEX_MODEL || REQUIRED_CODEX_MODEL), advertised, 'medium', 'app-server', []);
7
+ return metadata(String(input.model || process.env.SKS_CODEX_MODEL || process.env.CODEX_MODEL || ''), advertised, 'medium', 'app-server', []);
9
8
  }
10
- const model = String(input.model || process.env.SKS_CODEX_MODEL || process.env.CODEX_MODEL || REQUIRED_CODEX_MODEL);
9
+ const model = String(input.model || process.env.SKS_CODEX_MODEL || process.env.CODEX_MODEL || '').trim();
11
10
  const appServer = await readAppServerMetadata(model);
12
11
  if (appServer)
13
12
  return appServer;
@@ -58,11 +57,19 @@ async function readCodexCliMetadata(model) {
58
57
  return null;
59
58
  }
60
59
  function normalizePayload(payload, fallbackModel, source) {
61
- const row = Array.isArray(payload?.models)
62
- ? payload.models.find((candidate) => String(candidate?.id || candidate?.model || candidate?.name || '') === fallbackModel) || payload.models[0]
60
+ const catalog = Array.isArray(payload?.models) ? payload.models : null;
61
+ const reportedSelection = String(payload?.selected_model || payload?.current_model || payload?.active_model || '').trim();
62
+ const requestedModel = String(fallbackModel || reportedSelection).trim();
63
+ const row = catalog
64
+ ? requestedModel
65
+ ? catalog.find((candidate) => String(candidate?.id || candidate?.model || candidate?.name || '') === requestedModel) || null
66
+ : null
63
67
  : payload?.model_metadata || payload?.metadata || payload;
68
+ if (!row) {
69
+ return metadata(requestedModel, [], 'medium', source, [requestedModel ? 'codex_model_not_found_in_advertised_catalog' : 'codex_model_selection_unknown']);
70
+ }
64
71
  const efforts = normalizeAdvertisedEfforts(row?.advertised_efforts || row?.advertisedEfforts || row?.reasoning_efforts || row?.reasoningEfforts || payload?.advertised_efforts);
65
- return metadata(String(row?.model || row?.id || row?.name || fallbackModel), efforts, row?.default_effort || row?.defaultEffort || payload?.default_effort || 'medium', source, efforts.length ? [] : ['codex_model_metadata_efforts_missing']);
72
+ return metadata(String(row?.model || row?.id || row?.name || requestedModel), efforts, row?.default_effort || row?.defaultEffort || payload?.default_effort || 'medium', source, efforts.length ? [] : ['codex_model_metadata_efforts_missing']);
66
73
  }
67
74
  function metadata(model, efforts, defaultEffort, source, blockers) {
68
75
  const advertised = normalizeAdvertisedEfforts(efforts);
@@ -1,9 +1,7 @@
1
- import { REQUIRED_CODEX_MODEL } from '../codex-model-guard.js';
2
1
  export function buildCodexSdkConfig(input) {
3
- const model = String(input.model || process.env.SKS_CODEX_MODEL || process.env.CODEX_MODEL || REQUIRED_CODEX_MODEL);
2
+ const model = String(input.model || process.env.SKS_CODEX_MODEL || process.env.CODEX_MODEL || '').trim();
4
3
  const serviceTier = String(input.serviceTier || process.env.SKS_SERVICE_TIER || 'fast');
5
4
  const config = {
6
- model,
7
5
  service_tier: serviceTier === 'standard' ? 'standard' : 'fast',
8
6
  model_reasoning_effort: String(input.modelReasoningEffort || input.reasoningEffort || process.env.SKS_CODEX_REASONING || process.env.CODEX_MODEL_REASONING_EFFORT || 'medium'),
9
7
  mcp_servers: {},
@@ -15,6 +13,8 @@ export function buildCodexSdkConfig(input) {
15
13
  generation_index: Number(input.generationIndex || 0)
16
14
  }
17
15
  };
16
+ if (model)
17
+ config.model = model;
18
18
  if (input.requestedScopeContract?.no_mcp === true) {
19
19
  config.mcp_servers = {};
20
20
  config.sks = { ...config.sks, no_mcp: true };
@@ -18,7 +18,6 @@ import { readLocalModelConfig } from '../agents/ollama-worker-config.js';
18
18
  import { runLocalLlmTask } from '../local-llm/local-llm-control-adapter.js';
19
19
  import { detectPythonCodexSdkCapability, runPythonCodexSdkTask } from './python-codex-sdk-adapter.js';
20
20
  import { defaultModelCallBudget, withModelCallSlot } from './model-call-concurrency.js';
21
- import { REQUIRED_CODEX_MODEL } from '../codex-model-guard.js';
22
21
  export async function runCodexTask(input) {
23
22
  const root = path.resolve(input.mutationLedgerRoot);
24
23
  await ensureDir(root);
@@ -486,9 +485,9 @@ async function ensurePythonCodexLbConfig(env, config) {
486
485
  const lbBaseUrl = normalizeCodexLbBaseUrl(env.CODEX_LB_BASE_URL);
487
486
  if (!codexHome || !lbBaseUrl || !env.CODEX_LB_API_KEY)
488
487
  return;
489
- const model = String(config.model || env.SKS_CODEX_MODEL || env.CODEX_MODEL || REQUIRED_CODEX_MODEL);
488
+ const model = String(config.model || env.SKS_CODEX_MODEL || env.CODEX_MODEL || '').trim();
490
489
  const text = [
491
- `model = ${tomlQuote(model)}`,
490
+ ...(model ? [`model = ${tomlQuote(model)}`] : []),
492
491
  'model_provider = "codex-lb"',
493
492
  'service_tier = "fast"',
494
493
  `model_reasoning_effort = ${tomlQuote(String(config.model_reasoning_effort || env.SKS_CODEX_REASONING || env.CODEX_MODEL_REASONING_EFFORT || 'minimal'))}`,
@@ -1,96 +1,31 @@
1
- // Default model/effort for every SKS-managed Codex surface. gpt-5.6-terra is
2
- // served by the patched codex-lb (v1.20.1-r3, upstream PR #1173 port) with a
3
- // 6-level low~ultra reasoning range. Keys still LB-pinned to enforced_model=
4
- // gpt-5.5 will have the model rewritten server-side; unpin a key in the LB
5
- // dashboard (:1455) to actually receive 5.6 responses on it.
6
- export const REQUIRED_CODEX_MODEL = 'gpt-5.6-terra';
1
+ /**
2
+ * Codex owns the model catalog. SKS must never maintain a finite allowlist or
3
+ * silently replace a model selected by the user, workspace, or Codex client.
4
+ *
5
+ * Reasoning effort remains an SKS scheduling hint because it is independent of
6
+ * the model identifier and can be ignored by clients that do not advertise it.
7
+ */
7
8
  export const DEFAULT_CODEX_REASONING_EFFORT = 'high';
8
- export const GPT55_CODEX_MODEL = 'gpt-5.5';
9
- export const GPT54_MINI_CODEX_MODEL = 'gpt-5.4-mini';
10
- export const GPT56_CODEX_ALIAS_MODEL = 'gpt-5.6';
11
- export const GPT56_CODEX_MODELS = ['gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna'];
12
- export const SUPPORTED_CODEX_MODELS = [REQUIRED_CODEX_MODEL, GPT56_CODEX_ALIAS_MODEL, GPT55_CODEX_MODEL, GPT54_MINI_CODEX_MODEL, 'gpt-5.6-sol', 'gpt-5.6-luna'];
13
- const MODEL_VALUE_FLAGS = new Set(['--model', '-m']);
14
- const CONFIG_VALUE_FLAGS = new Set(['-c', '--config']);
15
- function isModelConfigOverride(value = '') {
16
- return /^model\s*=/.test(String(value || '').trim());
17
- }
18
- function stripCodexModelOverrides(args = []) {
19
- const out = [];
20
- const input = Array.isArray(args) ? args : [];
21
- for (let i = 0; i < input.length; i += 1) {
22
- const arg = String(input[i]);
23
- if (MODEL_VALUE_FLAGS.has(arg)) {
24
- i += 1;
25
- continue;
26
- }
27
- if (arg.startsWith('--model=') || arg.startsWith('-m='))
28
- continue;
29
- if (CONFIG_VALUE_FLAGS.has(arg)) {
30
- const value = i + 1 < input.length ? String(input[i + 1]) : '';
31
- if (isModelConfigOverride(value)) {
32
- i += 1;
33
- continue;
34
- }
35
- out.push(arg);
36
- if (i + 1 < input.length)
37
- out.push(String(input[++i]));
38
- continue;
39
- }
40
- if (arg.startsWith('-c=') || arg.startsWith('--config=')) {
41
- const value = arg.slice(arg.indexOf('=') + 1);
42
- if (isModelConfigOverride(value))
43
- continue;
44
- }
45
- out.push(arg);
46
- }
47
- return out;
48
- }
49
- function isSupportedCodexModel(value = '') {
50
- return SUPPORTED_CODEX_MODELS.includes(String(value || '').trim().toLowerCase());
51
- }
52
- // The forced model is REQUIRED_CODEX_MODEL unless the caller explicitly
53
- // requested another SUPPORTED model (via --model/-m/-c model=... args, or
54
- // SKS_CODEX_MODEL when no arg is present). Unsupported/forbidden requests are
55
- // stripped and rewritten to the required default.
56
- function resolveForcedCodexModel(args = []) {
57
- const requested = requestedCodexModelFromArgs(args);
58
- if (isSupportedCodexModel(requested))
59
- return String(requested).trim().toLowerCase();
60
- const envModel = String(process.env.SKS_CODEX_MODEL || '').trim().toLowerCase();
61
- if (!requested && isSupportedCodexModel(envModel))
62
- return envModel;
63
- return REQUIRED_CODEX_MODEL;
64
- }
65
- function requestedCodexModelFromArgs(args = []) {
66
- const input = Array.isArray(args) ? args : [];
67
- for (let i = 0; i < input.length; i += 1) {
68
- const arg = String(input[i]);
69
- if (MODEL_VALUE_FLAGS.has(arg))
70
- return i + 1 < input.length ? String(input[i + 1]) : '';
71
- if (arg.startsWith('--model=') || arg.startsWith('-m='))
72
- return arg.slice(arg.indexOf('=') + 1);
73
- if (CONFIG_VALUE_FLAGS.has(arg)) {
74
- const value = i + 1 < input.length ? String(input[i + 1]) : '';
75
- if (isModelConfigOverride(value))
76
- return value.slice(value.indexOf('=') + 1).trim().replace(/^"|"$/g, '');
77
- }
78
- if (arg.startsWith('-c=') || arg.startsWith('--config=')) {
79
- const value = arg.slice(arg.indexOf('=') + 1);
80
- if (isModelConfigOverride(value))
81
- return value.slice(value.indexOf('=') + 1).trim().replace(/^"|"$/g, '');
82
- }
83
- }
84
- return '';
85
- }
9
+ function copyArgs(args = []) {
10
+ return Array.isArray(args) ? args.map((value) => String(value)) : [];
11
+ }
12
+ /** Return Codex arguments exactly as supplied, including future model IDs. */
13
+ export function preserveCodexModelArgs(args = []) {
14
+ return copyArgs(args);
15
+ }
16
+ /**
17
+ * Backward-compatible aliases for callers compiled against older SKS builds.
18
+ * Despite the historical name, these functions intentionally do not force a
19
+ * model anymore.
20
+ */
86
21
  export function forceRequiredCodexModelArgs(args = []) {
87
- return ['--model', resolveForcedCodexModel(args), ...stripCodexModelOverrides(args)];
22
+ return preserveCodexModelArgs(args);
88
23
  }
89
24
  export function forceRequiredCodexModelConfigArgs(args = []) {
90
- return ['-c', `model="${resolveForcedCodexModel(args)}"`, ...stripCodexModelOverrides(args)];
25
+ return preserveCodexModelArgs(args);
91
26
  }
92
- export function isForbiddenCodexModel(value = '') {
93
- const model = String(value || '').trim().toLowerCase();
94
- return /^gpt-5\./.test(model) && !SUPPORTED_CODEX_MODELS.includes(model);
27
+ /** No model is forbidden by SKS; availability is decided by Codex itself. */
28
+ export function isForbiddenCodexModel(_value = '') {
29
+ return false;
95
30
  }
96
31
  //# sourceMappingURL=codex-model-guard.js.map
@@ -187,6 +187,8 @@ export async function updateCommand(sub = 'now', args = []) {
187
187
  process.exitCode = 1;
188
188
  }
189
189
  export async function setupCommand(args = []) {
190
+ if (flag(args, '--help') || flag(args, '-h'))
191
+ return usageCommand(['setup']);
190
192
  const root = await projectRoot();
191
193
  const installScope = installScopeFromArgs(args);
192
194
  let res = null;
@@ -221,9 +223,13 @@ export async function setupCommand(args = []) {
221
223
  console.log(`- ${file}`);
222
224
  }
223
225
  export async function bootstrapCommand(args = []) {
226
+ if (flag(args, '--help') || flag(args, '-h'))
227
+ return usageCommand(['bootstrap']);
224
228
  return setupCommand(['--local-only', ...args]);
225
229
  }
226
230
  export async function initCommand(args = []) {
231
+ if (flag(args, '--help') || flag(args, '-h'))
232
+ return usageCommand(['init']);
227
233
  return setupCommand(args);
228
234
  }
229
235
  export async function fixPathCommand(args = []) {