sneakoscope 2.0.4 → 2.0.6

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 (120) hide show
  1. package/README.md +18 -11
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/.sks-build-stamp.json +4 -4
  6. package/dist/bin/sks.js +1 -1
  7. package/dist/build-manifest.json +78 -8
  8. package/dist/cli/install-helpers.js +23 -0
  9. package/dist/commands/codex-app.js +25 -3
  10. package/dist/commands/doctor.js +33 -4
  11. package/dist/commands/mad-sks.js +2 -2
  12. package/dist/core/agents/agent-orchestrator.js +22 -3
  13. package/dist/core/agents/agent-proof-evidence.js +59 -2
  14. package/dist/core/agents/agent-roster.js +35 -6
  15. package/dist/core/agents/agent-schema.js +1 -1
  16. package/dist/core/agents/agent-worker-pipeline.js +9 -1
  17. package/dist/core/agents/native-worker-backend-router.js +50 -10
  18. package/dist/core/agents/ollama-worker-config.js +164 -15
  19. package/dist/core/codex/codex-0-137-compat.js +119 -0
  20. package/dist/core/codex-app.js +124 -2
  21. package/dist/core/codex-control/codex-control-proof.js +4 -1
  22. package/dist/core/codex-control/codex-sdk-capability.js +1 -1
  23. package/dist/core/codex-control/codex-task-runner.js +329 -5
  24. package/dist/core/codex-control/python-codex-sdk-adapter.js +197 -0
  25. package/dist/core/codex-control/python-codex-sdk-event-translator.js +14 -0
  26. package/dist/core/commands/local-model-command.js +65 -19
  27. package/dist/core/commands/naruto-command.js +124 -8
  28. package/dist/core/commands/run-command.js +1 -1
  29. package/dist/core/doctor/doctor-readiness-matrix.js +21 -2
  30. package/dist/core/fsx.js +1 -1
  31. package/dist/core/hooks-runtime.js +2 -233
  32. package/dist/core/init.js +8 -8
  33. package/dist/core/local-llm/local-llm-backpressure.js +20 -0
  34. package/dist/core/local-llm/local-llm-capability.js +29 -0
  35. package/dist/core/local-llm/local-llm-client.js +100 -0
  36. package/dist/core/local-llm/local-llm-config.js +6 -1
  37. package/dist/core/local-llm/local-llm-context-cache.js +21 -0
  38. package/dist/core/local-llm/local-llm-control-adapter.js +101 -0
  39. package/dist/core/local-llm/local-llm-json-repair.js +52 -0
  40. package/dist/core/local-llm/local-llm-metrics.js +42 -0
  41. package/dist/core/local-llm/local-llm-ollama-client.js +67 -0
  42. package/dist/core/local-llm/local-llm-openai-compatible-client.js +30 -0
  43. package/dist/core/local-llm/local-llm-prompt-cache.js +12 -0
  44. package/dist/core/local-llm/local-llm-scheduler.js +29 -0
  45. package/dist/core/local-llm/local-llm-schema-enforcer.js +15 -0
  46. package/dist/core/local-llm/local-llm-smoke.js +83 -0
  47. package/dist/core/local-llm/local-llm-warmup.js +20 -0
  48. package/dist/core/local-llm/local-worker-eligibility.js +27 -0
  49. package/dist/core/naruto/hardware-capacity-probe.js +36 -0
  50. package/dist/core/naruto/naruto-active-pool.js +134 -0
  51. package/dist/core/naruto/naruto-backpressure.js +13 -0
  52. package/dist/core/naruto/naruto-concurrency-governor.js +65 -0
  53. package/dist/core/naruto/naruto-finalizer.js +18 -0
  54. package/dist/core/naruto/naruto-generation-scheduler.js +18 -0
  55. package/dist/core/naruto/naruto-gpt-final-pack.js +49 -0
  56. package/dist/core/naruto/naruto-parallel-patch-apply.js +95 -0
  57. package/dist/core/naruto/naruto-patch-transaction-batch.js +42 -0
  58. package/dist/core/naruto/naruto-role-policy.js +107 -0
  59. package/dist/core/naruto/naruto-verification-dag.js +42 -0
  60. package/dist/core/naruto/naruto-verification-pool.js +18 -0
  61. package/dist/core/naruto/naruto-work-graph.js +198 -0
  62. package/dist/core/naruto/naruto-work-item.js +40 -0
  63. package/dist/core/naruto/naruto-work-stealing.js +11 -0
  64. package/dist/core/naruto/resource-pressure-monitor.js +32 -0
  65. package/dist/core/pipeline/finalize-pipeline-result.js +58 -0
  66. package/dist/core/pipeline/gpt-final-required.js +12 -0
  67. package/dist/core/pipeline-internals/runtime-core.js +1 -1
  68. package/dist/core/ppt.js +31 -8
  69. package/dist/core/product-design-app-server.js +410 -0
  70. package/dist/core/product-design-plugin.js +139 -0
  71. package/dist/core/prompt/prompt-placeholder-guard.js +30 -0
  72. package/dist/core/router/capability-card.js +13 -0
  73. package/dist/core/router/route-cache.js +3 -0
  74. package/dist/core/router/ultra-router.js +2 -1
  75. package/dist/core/routes.js +12 -12
  76. package/dist/core/version.js +1 -1
  77. package/dist/core/zellij/zellij-lane-runtime.js +2 -2
  78. package/dist/core/zellij/zellij-naruto-dashboard.js +36 -0
  79. package/dist/core/zellij/zellij-worker-pane-manager.js +4 -4
  80. package/dist/scripts/blackbox-command-import-smoke.js +10 -1
  81. package/dist/scripts/check-package-boundary.js +12 -3
  82. package/dist/scripts/codex-0-137-compat-check.js +27 -0
  83. package/dist/scripts/codex-environment-scoped-approvals-check.js +10 -0
  84. package/dist/scripts/codex-plugin-list-json-check.js +8 -0
  85. package/dist/scripts/codex-thread-runtime-choice-check.js +10 -0
  86. package/dist/scripts/local-collab-all-pipelines-final-gpt-check.js +21 -0
  87. package/dist/scripts/local-llm-all-pipelines-check.js +11 -0
  88. package/dist/scripts/local-llm-cache-performance-check.js +10 -0
  89. package/dist/scripts/local-llm-capability-check.js +14 -0
  90. package/dist/scripts/local-llm-smoke-check.js +23 -0
  91. package/dist/scripts/local-llm-structured-output-check.js +11 -0
  92. package/dist/scripts/local-llm-throughput-check.js +10 -0
  93. package/dist/scripts/local-llm-tool-call-repair-check.js +10 -0
  94. package/dist/scripts/local-llm-warmup-check.js +11 -0
  95. package/dist/scripts/naruto-active-pool-check.js +39 -0
  96. package/dist/scripts/naruto-concurrency-governor-check.js +52 -0
  97. package/dist/scripts/naruto-gpt-final-pack-check.js +34 -0
  98. package/dist/scripts/naruto-parallel-patch-apply-check.js +41 -0
  99. package/dist/scripts/naruto-readonly-routing-check.js +116 -0
  100. package/dist/scripts/naruto-real-local-gpt-final-smoke.js +16 -0
  101. package/dist/scripts/naruto-role-distribution-check.js +23 -0
  102. package/dist/scripts/naruto-shadow-clone-swarm-check.js +13 -0
  103. package/dist/scripts/naruto-verification-pool-check.js +36 -0
  104. package/dist/scripts/naruto-work-graph-check.js +24 -0
  105. package/dist/scripts/naruto-zellij-massive-ui-check.js +23 -0
  106. package/dist/scripts/product-design-auto-install-check.js +119 -0
  107. package/dist/scripts/product-design-plugin-routing-check.js +101 -0
  108. package/dist/scripts/prompt-placeholder-guard-check.js +33 -0
  109. package/dist/scripts/python-codex-sdk-all-pipelines-check.js +47 -0
  110. package/dist/scripts/python-codex-sdk-capability-check.js +75 -0
  111. package/dist/scripts/python-codex-sdk-sandbox-policy-check.js +10 -0
  112. package/dist/scripts/python-codex-sdk-stream-bridge-check.js +12 -0
  113. package/dist/scripts/release-parallel-check.js +16 -2
  114. package/dist/scripts/release-provenance-check.js +21 -0
  115. package/dist/scripts/release-real-check.js +5 -0
  116. package/dist/scripts/zellij-worker-pane-manager-check.js +1 -1
  117. package/package.json +36 -4
  118. package/schemas/local-llm/local-model-config.schema.json +74 -0
  119. package/schemas/naruto/naruto-concurrency-governor.schema.json +21 -0
  120. package/schemas/naruto/naruto-work-graph.schema.json +22 -0
@@ -31,6 +31,13 @@ export async function writeAgentProofEvidence(root, input) {
31
31
  const patchSwarm = input.patchSwarm || await readJson(path.join(root, 'agent-patch-swarm-runtime.json'), null);
32
32
  const localCollaborationPolicy = input.localCollaborationPolicy || await readJson(path.join(root, 'local-collaboration-policy.json'), null) || resolveLocalCollaborationPolicy();
33
33
  const gptFinalArbiter = input.gptFinalArbiter || await readJson(path.join(root, 'gpt-final-arbiter', 'gpt-final-arbiter.json'), null);
34
+ const narutoWorkGraph = await readJson(path.join(root, 'naruto-work-graph.json'), null);
35
+ const narutoRoleDistribution = await readJson(path.join(root, 'naruto-role-distribution.json'), null);
36
+ const narutoConcurrencyGovernor = await readJson(path.join(root, 'naruto-concurrency-governor.json'), null);
37
+ const narutoActivePool = await readJson(path.join(root, 'naruto-active-pool.json'), null);
38
+ const narutoVerificationDag = await readJson(path.join(root, 'naruto-verification-dag.json'), null);
39
+ const narutoGptFinalPack = await readJson(path.join(root, 'naruto-gpt-final-pack.json'), null);
40
+ const narutoZellijDashboard = await readJson(path.join(root, 'naruto-zellij-dashboard.json'), null);
34
41
  const localParticipated = localCollaborationParticipated(input.results || []) || Number(gptFinalArbiter?.local_outputs_count || 0) > 0;
35
42
  const finalGptPatchStage = input.finalGptPatchStage || null;
36
43
  const localFinalGate = gptFinalArbiter?.final_gate || evaluateLocalCollaborationFinalGate({
@@ -74,6 +81,7 @@ export async function writeAgentProofEvidence(root, input) {
74
81
  const workQueueGoalRefsOk = Boolean(workQueue?.items?.length) && workQueue.items.every((item) => item.goal_mode_ref);
75
82
  const workQueueStrategyRefsOk = Boolean(workQueue?.items?.length) && workQueue.items.every((item) => item.slice?.strategy_refs);
76
83
  const route = String(input.route || taskGraph?.route_type || '$Agent');
84
+ const isNarutoRoute = route === '$Naruto';
77
85
  const routeCommand = String(input.routeCommand || 'sks agent run');
78
86
  const genericAgentRouteStandIn = !/\$?agent$/i.test(route) && /\bagent\s+run\b/i.test(routeCommand) && /--route/i.test(routeCommand);
79
87
  const realRouteCommandUsed = !genericAgentRouteStandIn;
@@ -111,6 +119,14 @@ export async function writeAgentProofEvidence(root, input) {
111
119
  return !changed || Boolean(row.rollback_digest);
112
120
  }) : true;
113
121
  const parallelPatchApplyVerified = patchSwarm ? Array.isArray(patchProof?.wall_clock_parallel_evidence) && patchProof.wall_clock_parallel_evidence.length > 0 || Number(patchSwarm?.parallel_apply_count || 0) > 1 : false;
122
+ const readOnlyNoWriteLeaseMode = isReadOnlyNoWriteLeaseMode({
123
+ results: input.results || [],
124
+ leases: input.partition?.leases || [],
125
+ parallelWritePolicy,
126
+ taskGraph,
127
+ narutoWorkGraph
128
+ });
129
+ const changedFileLeaseBlockers = readOnlyNoWriteLeaseMode ? [] : agentChangedFileLeaseViolations(input.results || [], input.partition?.leases || []);
114
130
  const blockers = [
115
131
  ...(lifecycle.ok ? [] : ['agent_lifecycle_not_all_closed']),
116
132
  ...(lifecycle.ok ? [] : lifecycle.open_sessions.map((id) => 'session_open:' + id)),
@@ -177,7 +193,17 @@ export async function writeAgentProofEvidence(root, input) {
177
193
  ...(localParticipated && localFinalGate.ok !== true ? localFinalGate.blockers || ['gpt_final_arbiter_gate_not_ok'] : []),
178
194
  ...(localParticipated && gptFinalArbiter?.ok !== true ? gptFinalArbiter?.blockers || ['gpt_final_arbiter_not_ok'] : []),
179
195
  ...(localParticipated && finalGptPatchStage?.ok === false ? finalGptPatchStage.blockers || ['final_gpt_patch_stage_not_ok'] : []),
180
- ...agentChangedFileLeaseViolations(input.results || [], input.partition?.leases || [])
196
+ ...(isNarutoRoute && !narutoWorkGraph ? ['naruto_work_graph_missing'] : []),
197
+ ...(isNarutoRoute && narutoWorkGraph?.ok === false ? narutoWorkGraph.blockers || ['naruto_work_graph_not_ok'] : []),
198
+ ...(isNarutoRoute && !narutoRoleDistribution ? ['naruto_role_distribution_missing'] : []),
199
+ ...(isNarutoRoute && narutoRoleDistribution?.ok === false ? narutoRoleDistribution.blockers || ['naruto_role_distribution_not_ok'] : []),
200
+ ...(isNarutoRoute && !narutoConcurrencyGovernor ? ['naruto_concurrency_governor_missing'] : []),
201
+ ...(isNarutoRoute && !narutoActivePool ? ['naruto_active_pool_missing'] : []),
202
+ ...(isNarutoRoute && narutoActivePool?.ok === false ? narutoActivePool.blockers || ['naruto_active_pool_not_ok'] : []),
203
+ ...(isNarutoRoute && !narutoVerificationDag ? ['naruto_verification_dag_missing'] : []),
204
+ ...(isNarutoRoute && !narutoGptFinalPack ? ['naruto_gpt_final_pack_missing'] : []),
205
+ ...(isNarutoRoute && !narutoZellijDashboard ? ['naruto_zellij_dashboard_missing'] : []),
206
+ ...changedFileLeaseBlockers
181
207
  ];
182
208
  const evidence = {
183
209
  schema: AGENT_PROOF_EVIDENCE_SCHEMA,
@@ -222,6 +248,23 @@ export async function writeAgentProofEvidence(root, input) {
222
248
  gpt_final_patch_source: finalGptPatchStage?.final_patch_source || (localParticipated ? 'blocked' : 'not_applicable'),
223
249
  gpt_final_gate_ok: localFinalGate.ok === true,
224
250
  gpt_final_gate: localFinalGate,
251
+ naruto_work_graph: narutoWorkGraph ? 'naruto-work-graph.json' : null,
252
+ naruto_total_work_items: Number(narutoWorkGraph?.total_work_items || 0),
253
+ naruto_mixed_work_kinds: narutoWorkGraph?.mixed_work_kinds || [],
254
+ naruto_write_allowed_count: Number(narutoWorkGraph?.write_allowed_count || 0),
255
+ naruto_role_distribution: narutoRoleDistribution ? 'naruto-role-distribution.json' : null,
256
+ naruto_role_distribution_entries: narutoRoleDistribution?.entries || [],
257
+ naruto_verifier_only: narutoRoleDistribution?.verifier_only === true,
258
+ naruto_implementation_like_ratio: Number(narutoRoleDistribution?.implementation_like_ratio || 0),
259
+ naruto_concurrency_governor: narutoConcurrencyGovernor ? 'naruto-concurrency-governor.json' : null,
260
+ naruto_safe_active_workers: Number(narutoConcurrencyGovernor?.safe_active_workers || 0),
261
+ naruto_safe_zellij_visible_panes: Number(narutoConcurrencyGovernor?.safe_zellij_visible_panes || 0),
262
+ naruto_headless_workers: Number(narutoConcurrencyGovernor?.headless_workers || 0),
263
+ naruto_active_pool: narutoActivePool ? 'naruto-active-pool.json' : null,
264
+ naruto_active_pool_refill_events: Number(narutoActivePool?.refill_events || 0),
265
+ naruto_verification_dag: narutoVerificationDag ? 'naruto-verification-dag.json' : null,
266
+ naruto_gpt_final_pack: narutoGptFinalPack ? 'naruto-gpt-final-pack.json' : null,
267
+ naruto_zellij_dashboard: narutoZellijDashboard ? 'naruto-zellij-dashboard.json' : null,
225
268
  patch_swarm_runtime: patchSwarm ? 'agent-patch-swarm-runtime.json' : null,
226
269
  patch_queue: patchSwarm ? 'agent-patch-queue.json' : null,
227
270
  patch_queue_events: patchSwarm ? 'agent-patch-queue-events.jsonl' : null,
@@ -334,7 +377,7 @@ export async function writeAgentProofEvidence(root, input) {
334
377
  triwiki_use_first_count: Number(input.triwikiContext?.use_first?.length || 0),
335
378
  triwiki_hydrate_first_count: Number(input.triwikiContext?.hydrate_first?.length || 0),
336
379
  triwiki_claim_count: Number(input.triwikiContext?.claim_count || 0),
337
- changed_files_lease_checked: true,
380
+ changed_files_lease_checked: !readOnlyNoWriteLeaseMode,
338
381
  dependency_collision_risk: input.partition?.no_overlap_proof?.dependency_collision_risk || [],
339
382
  blockers
340
383
  };
@@ -413,6 +456,20 @@ function agentChangedFileLeaseViolations(results, leases) {
413
456
  }
414
457
  return violations;
415
458
  }
459
+ function isReadOnlyNoWriteLeaseMode(input) {
460
+ const writeLeaseCount = input.leases.filter((lease) => lease.kind === 'write').length;
461
+ if (writeLeaseCount > 0)
462
+ return false;
463
+ const resultWriteSignals = input.results.some((result) => (Array.isArray(result?.writes) && result.writes.length > 0)
464
+ || (Array.isArray(result?.patch_envelopes) && result.patch_envelopes.length > 0));
465
+ if (resultWriteSignals)
466
+ return false;
467
+ const policyReadonly = input.parallelWritePolicy?.readonly === true;
468
+ const policyWriteOff = String(input.parallelWritePolicy?.write_mode || 'off') === 'off';
469
+ const narutoReadOnly = input.narutoWorkGraph?.readonly === true || Number(input.narutoWorkGraph?.write_allowed_count || 0) === 0;
470
+ const taskGraphNoWrites = Number(input.taskGraph?.write_allowed_count || 0) === 0;
471
+ return policyReadonly || policyWriteOff || narutoReadOnly || taskGraphNoWrites;
472
+ }
416
473
  function pathWithin(file, leasePath) {
417
474
  const left = String(file || '').replace(/\\/g, '/').replace(/^\.\//, '');
418
475
  const right = String(leasePath || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
@@ -2,6 +2,7 @@ import os from 'node:os';
2
2
  import { DEFAULT_AGENT_CONCURRENCY, DEFAULT_AGENT_COUNT, DEFAULT_NARUTO_CLONES, MAX_AGENT_COUNT, MAX_NARUTO_AGENT_COUNT, agentSessionId } from './agent-schema.js';
3
3
  import { defaultAgentPersonas, validatePersonaUniqueness } from './agent-persona.js';
4
4
  import { buildAgentEffortPolicy, decideAgentEffort, decideNarutoCloneEffort } from './agent-effort-policy.js';
5
+ import { mapNarutoRoleToAgentRole, narutoRoleAllowsWrite } from '../naruto/naruto-role-policy.js';
5
6
  // $Naruto must never blindly spawn the full clone count at once, but the live
6
7
  // CONCURRENCY ceiling is NOT a function of CPU cores. Each clone is a separate CLI
7
8
  // worker process that spends ~all of its wall-clock awaiting the Codex API
@@ -156,28 +157,40 @@ export function buildNarutoCloneRoster(opts = {}) {
156
157
  const basePool = pool.length ? pool : defaultAgentPersonas(DEFAULT_AGENT_COUNT);
157
158
  const personas = [];
158
159
  const roster = [];
160
+ const roleCycle = narutoRoleCycle(readonly);
159
161
  for (let index = 0; index < cloneCount; index += 1) {
160
162
  const base = basePool[index % basePool.length];
161
163
  const cloneTag = 'clone-' + String(index + 1).padStart(3, '0');
162
164
  const id = 'naruto_' + cloneTag.replace(/-/g, '_');
163
- const cloneReadonly = readonly || base.read_only;
165
+ const narutoRole = roleCycle[index % roleCycle.length] || 'verifier';
166
+ const writeAllowed = !readonly && narutoRoleAllowsWrite(narutoRole);
167
+ const cloneReadonly = readonly || !writeAllowed;
168
+ const role = mapNarutoRoleToAgentRole(narutoRole);
169
+ const allowedTools = writeAllowed ? ['read', 'search', 'edit', 'test'] : narutoRole === 'verifier' ? ['read', 'search', 'test'] : ['read', 'search'];
164
170
  // Dynamic per-clone effort like team mode, capped at low/medium and always fast.
165
- const effort = decideNarutoCloneEffort({ persona: base, prompt: opts.prompt || '', agentId: id, readonly: cloneReadonly });
171
+ const effort = decideNarutoCloneEffort({ persona: { ...base, role, allowed_tools: allowedTools, read_only: cloneReadonly, write_policy: writeAllowed ? 'exclusive Naruto patch-envelope lease required' : 'read-only Naruto role' }, prompt: opts.prompt || '', agentId: id, readonly: cloneReadonly });
166
172
  const persona = {
167
173
  ...base,
168
174
  id,
169
175
  stable_id: base.stable_id + '-' + cloneTag,
170
- read_only: readonly || base.read_only,
171
- prompt: 'SHADOW CLONE: ' + cloneTag + ' (Kage Bunshin of ' + base.stable_id + ')\n' + base.prompt
176
+ role,
177
+ naruto_role: narutoRole,
178
+ write_allowed: writeAllowed,
179
+ read_only: cloneReadonly,
180
+ allowed_tools: allowedTools,
181
+ write_policy: writeAllowed ? 'exclusive Naruto patch-envelope lease required' : 'read-only Naruto role',
182
+ prompt: 'SHADOW CLONE: ' + cloneTag + ' (Kage Bunshin of ' + base.stable_id + ')\nNARUTO ROLE: ' + narutoRole + '\n' + base.prompt
172
183
  };
173
184
  personas.push(persona);
174
185
  roster.push({
175
186
  id,
176
187
  session_id: agentSessionId(id, index + 1),
177
188
  persona_id: id,
178
- role: base.role,
189
+ role,
190
+ naruto_role: narutoRole,
191
+ write_allowed: writeAllowed,
179
192
  index: index + 1,
180
- write_policy: cloneReadonly ? 'read-only' : base.write_policy,
193
+ write_policy: cloneReadonly ? 'read-only' : 'exclusive Naruto patch-envelope lease required',
181
194
  status: 'pending',
182
195
  reasoning_effort: effort.reasoning_effort,
183
196
  model_reasoning_effort: effort.model_reasoning_effort,
@@ -208,4 +221,20 @@ export function buildNarutoCloneRoster(opts = {}) {
208
221
  };
209
222
  return { ...result, effort_policy: buildAgentEffortPolicy(result) };
210
223
  }
224
+ function narutoRoleCycle(readonly) {
225
+ if (readonly)
226
+ return ['verifier', 'researcher', 'verifier', 'gpt_final_arbiter'];
227
+ return [
228
+ 'implementer',
229
+ 'modifier',
230
+ 'test_writer',
231
+ 'verifier',
232
+ 'researcher',
233
+ 'conflict_resolver',
234
+ 'rollback_planner',
235
+ 'integrator',
236
+ 'modifier',
237
+ 'test_writer'
238
+ ];
239
+ }
211
240
  //# sourceMappingURL=agent-roster.js.map
@@ -14,7 +14,7 @@ export const DEFAULT_AGENT_CONCURRENCY = 5;
14
14
  // cap; every other roster/scheduler caller keeps MAX_AGENT_COUNT as the default.
15
15
  export const MAX_NARUTO_AGENT_COUNT = 100;
16
16
  export const DEFAULT_NARUTO_CLONES = 12;
17
- export const AGENT_BACKENDS = ['fake', 'process', 'codex-sdk', 'zellij', 'ollama'];
17
+ export const AGENT_BACKENDS = ['fake', 'process', 'codex-sdk', 'zellij', 'ollama', 'local-llm'];
18
18
  export function normalizeAgentBackend(input) {
19
19
  const value = String(input || 'codex-sdk');
20
20
  return AGENT_BACKENDS.includes(value) ? value : 'codex-sdk';
@@ -67,7 +67,8 @@ export function validateAgentWorkerResult(result) {
67
67
  normalized.blockers.push(...patchEnvelopeValidation.blockers.map((issue) => 'patch_envelope_invalid:' + issue));
68
68
  normalized.verification = { status: 'failed', checks: [...normalized.verification.checks, 'agent-patch-envelope-schema'] };
69
69
  }
70
- if (patchEnvelopeValidation.envelopes.length === 0 && (normalized.changed_files.length > 0 || normalized.writes.length > 0)) {
70
+ const readOnlyOrNoopWithoutPatch = acceptsNoPatchReadOnlyOrNoop(result?.no_patch_reason);
71
+ if (patchEnvelopeValidation.envelopes.length === 0 && (normalized.writes.length > 0 || (!readOnlyOrNoopWithoutPatch && normalized.changed_files.length > 0))) {
71
72
  normalized.status = 'blocked';
72
73
  normalized.blockers.push('no_patch_generated');
73
74
  normalized.verification = { status: 'failed', checks: [...normalized.verification.checks, 'agent-patch-envelope-required-for-write'] };
@@ -132,4 +133,11 @@ function normalizeVerification(value) {
132
133
  checks: Array.isArray(value?.checks) ? value.checks : []
133
134
  };
134
135
  }
136
+ function acceptsNoPatchReadOnlyOrNoop(value) {
137
+ if (!value || typeof value !== 'object')
138
+ return false;
139
+ return value.ok === true
140
+ && value.read_only_or_noop_evidence === true
141
+ && String(value.reason || '') === 'read_only_or_no_write_paths';
142
+ }
135
143
  //# sourceMappingURL=agent-worker-pipeline.js.map
@@ -57,6 +57,7 @@ export async function runNativeWorkerBackendRouter(input) {
57
57
  result = validateAgentWorkerResult({
58
58
  ...processRun,
59
59
  patch_envelopes: patchEnvelopes,
60
+ ...(patchEnvelopes.length ? {} : { no_patch_reason: buildNoPatchReason(input, backend) }),
60
61
  artifacts: [...new Set([...(processRun.artifacts || []), ...(patchEnvelopes.length ? [input.patchRel] : [])])],
61
62
  process_child_report: processReport,
62
63
  model_authored_patch_envelopes: false,
@@ -81,12 +82,14 @@ export async function runNativeWorkerBackendRouter(input) {
81
82
  ...ollamaRun,
82
83
  backend: 'ollama',
83
84
  patch_envelopes: patchEnvelopes,
85
+ ...(patchEnvelopes.length ? {} : { no_patch_reason: buildNoPatchReason(input, backend) }),
84
86
  model_authored_patch_envelopes: patchEnvelopes.length > 0,
85
87
  fixture_patch_envelopes: false,
86
88
  verification: { status: ollamaRun.status === 'done' ? 'passed' : 'failed', checks: [...(ollamaRun.verification?.checks || []), 'native-worker-backend-router', 'ollama-api-generate'] }
87
89
  });
88
90
  }
89
- else if (backend === 'codex-sdk' || backend === 'zellij') {
91
+ else if (backend === 'codex-sdk' || backend === 'zellij' || backend === 'local-llm') {
92
+ const localPreferred = backend === 'local-llm';
90
93
  const sdkTask = await runCodexTask({
91
94
  route: String(input.intake.route || '$Agent'),
92
95
  tier: 'worker',
@@ -111,6 +114,12 @@ export async function runNativeWorkerBackendRouter(input) {
111
114
  user_confirmed_full_access: false,
112
115
  mad_sks_authorized: input.intake.mad_sks_authorized === true || process.env.SKS_MAD_SKS_ACTIVE === '1'
113
116
  },
117
+ backendPreference: localPreferred ? ['local-llm', 'codex-sdk'] : ['codex-sdk'],
118
+ allowLocalLlm: localPreferred,
119
+ ...(localPreferred ? { localLlmPolicy: {
120
+ mode: 'local_preferred',
121
+ requiresGptFinal: true
122
+ } } : {}),
114
123
  mutationLedgerRoot: path.join(root, input.workerDirRel),
115
124
  zellijPaneId: await readZellijPaneId(root, input.workerDirRel),
116
125
  reliabilityPolicy: {
@@ -121,12 +130,14 @@ export async function runNativeWorkerBackendRouter(input) {
121
130
  outputLastMessagePath = sdkTask.workerResultPath;
122
131
  const sdkWorkerResult = await readJson(sdkTask.workerResultPath, null);
123
132
  patchEnvelopes = normalizeSdkPatchEnvelopes(sdkWorkerResult?.patch_envelopes || [], input, sdkTask.sdkThreadId);
124
- proofLevel = sdkTask.ok ? (patchEnvelopes.length ? 'model_authored' : 'codex_sdk_thread_proven') : 'blocked';
133
+ proofLevel = sdkTask.ok ? (patchEnvelopes.length ? 'model_authored' : sdkTask.backend === 'local-llm' ? 'local_llm_worker_proven' : 'codex_sdk_thread_proven') : 'blocked';
125
134
  const sdkReport = {
126
135
  schema: 'sks.codex-sdk-worker-adapter.v1',
127
- backend: 'codex-sdk',
136
+ backend: sdkTask.backend,
137
+ backend_family: sdkTask.backend_family,
128
138
  sdk_thread_id: sdkTask.sdkThreadId,
129
139
  sdk_run_id: sdkTask.sdkRunId,
140
+ local_llm_proof_path: sdkTask.localLlmProofPath || null,
130
141
  stream_event_count: sdkTask.streamEventCount,
131
142
  structured_output_valid: sdkTask.structuredOutputValid,
132
143
  worker_result_path: sdkTask.workerResultPath,
@@ -136,17 +147,31 @@ export async function runNativeWorkerBackendRouter(input) {
136
147
  childReports = [sdkReport];
137
148
  result = validateAgentWorkerResult({
138
149
  ...sdkWorkerResult,
139
- backend: 'codex-sdk',
150
+ backend: sdkTask.backend === 'local-llm' ? 'local-llm' : 'codex-sdk',
140
151
  patch_envelopes: patchEnvelopes,
152
+ ...(patchEnvelopes.length ? {} : { no_patch_reason: buildNoPatchReason(input, sdkTask.backend || backend) }),
141
153
  codex_child_report: sdkReport,
142
154
  codex_sdk_thread: sdkReport,
143
155
  model_authored_patch_envelopes: patchEnvelopes.length > 0,
144
156
  fixture_patch_envelopes: false,
145
- artifacts: [...new Set([...(sdkWorkerResult?.artifacts || []), path.relative(root, sdkTask.workerResultPath), path.join(input.workerDirRel, 'codex-control-proof.json'), path.join(input.workerDirRel, 'codex-thread-registry.json'), path.join(input.workerDirRel, 'codex-sdk-events.jsonl')])],
157
+ artifacts: [...new Set([
158
+ ...(sdkWorkerResult?.artifacts || []),
159
+ path.relative(root, sdkTask.workerResultPath),
160
+ path.join(input.workerDirRel, 'codex-control-proof.json'),
161
+ path.join(input.workerDirRel, 'codex-thread-registry.json'),
162
+ sdkTask.backend === 'local-llm' ? path.join(input.workerDirRel, 'local-llm-events.jsonl') : path.join(input.workerDirRel, 'codex-sdk-events.jsonl'),
163
+ ...(sdkTask.localLlmProofPath ? [path.relative(root, sdkTask.localLlmProofPath)] : [])
164
+ ])],
146
165
  blockers: [...(sdkWorkerResult?.blockers || []), ...sdkTask.blockers],
147
166
  verification: {
148
167
  status: sdkTask.ok ? 'passed' : 'failed',
149
- checks: [...(sdkWorkerResult?.verification?.checks || []), 'codex-sdk-control-plane', 'codex-sdk-event-stream', 'codex-sdk-structured-output']
168
+ checks: [
169
+ ...(sdkWorkerResult?.verification?.checks || []),
170
+ sdkTask.backend === 'local-llm' ? 'local-llm-control-plane' : 'codex-sdk-control-plane',
171
+ sdkTask.backend === 'local-llm' ? 'local-llm-event-stream' : 'codex-sdk-event-stream',
172
+ sdkTask.backend === 'local-llm' ? 'local-llm-structured-output' : 'codex-sdk-structured-output',
173
+ ...(sdkTask.backend === 'local-llm' ? ['gpt-final-required-before-acceptance'] : [])
174
+ ]
150
175
  }
151
176
  });
152
177
  }
@@ -167,6 +192,7 @@ export async function runNativeWorkerBackendRouter(input) {
167
192
  result = validateAgentWorkerResult({
168
193
  ...zellijRun,
169
194
  patch_envelopes: patchEnvelopes,
195
+ ...(patchEnvelopes.length ? {} : { no_patch_reason: buildNoPatchReason(input, backend) }),
170
196
  zellij_child_report: zellijReport,
171
197
  model_authored_patch_envelopes: false,
172
198
  fixture_patch_envelopes: false,
@@ -221,13 +247,13 @@ async function maybeAutoSelectOllamaBackend(backend, input) {
221
247
  model: input.intake?.ollama_model || null,
222
248
  baseUrl: input.intake?.ollama_base_url || null
223
249
  }).catch(() => null);
224
- if (!config?.ok || config.enabled !== true)
250
+ if (!config?.ok || config.enabled !== true || config.status !== 'verified')
225
251
  return backend;
226
252
  const policy = classifyOllamaWorkerSlice(input.slice, { route: input.intake?.route, agent: input.agent });
227
- return policy.ok ? 'ollama' : backend;
253
+ return policy.ok ? 'local-llm' : backend;
228
254
  }
229
255
  function normalizeBackend(value) {
230
- return value === 'fake' || value === 'process' || value === 'codex-sdk' || value === 'zellij' || value === 'ollama' ? value : null;
256
+ return value === 'fake' || value === 'process' || value === 'codex-sdk' || value === 'zellij' || value === 'ollama' || value === 'local-llm' ? value : null;
231
257
  }
232
258
  function envelopeOpts(input, source, childPid) {
233
259
  return {
@@ -257,10 +283,24 @@ function buildWorkerPrompt(slice) {
257
283
  '',
258
284
  write.length
259
285
  ? `Write-capable slice. Return JSON matching ${CODEX_AGENT_WORKER_RESULT_SCHEMA_ID}; include patch_envelopes for write_paths=${JSON.stringify(write)}.`
260
- : `Read-only slice. Return JSON matching ${CODEX_AGENT_WORKER_RESULT_SCHEMA_ID}.`,
286
+ : `Read-only slice. Return JSON matching ${CODEX_AGENT_WORKER_RESULT_SCHEMA_ID}; do not report pre-existing repository dirtiness as changed_files.`,
261
287
  'Required JSON fields: status, summary, findings, changed_files, patch_envelopes, verification, rollback_notes, blockers.'
262
288
  ].join('\n');
263
289
  }
290
+ function buildNoPatchReason(input, backend) {
291
+ const writePathCount = writePaths(input.slice, input.intake).length;
292
+ return {
293
+ schema: 'sks.native-cli-worker-no-patch-reason.v1',
294
+ generated_at: nowIso(),
295
+ ok: writePathCount === 0,
296
+ reason: writePathCount ? 'write_capable_task_without_backend_patch_envelope' : 'read_only_or_no_write_paths',
297
+ route_justification: writePathCount ? 'backend returned no patch envelopes for a write-capable task' : 'task has no write paths',
298
+ read_only_or_noop_evidence: writePathCount === 0,
299
+ task_slice_id: input.slice?.id || null,
300
+ backend,
301
+ blockers: writePathCount && backend !== 'fake' ? ['write_capable_no_patch_envelope'] : []
302
+ };
303
+ }
264
304
  function hasWriteLease(slice, intake) {
265
305
  return writePaths(slice, intake).length > 0;
266
306
  }
@@ -2,12 +2,16 @@ import os from 'node:os';
2
2
  import path from 'node:path';
3
3
  import { ensureDir, exists, nowIso, readJson, writeJsonAtomic } from '../fsx.js';
4
4
  export const LOCAL_MODEL_CONFIG_SCHEMA = 'sks.local-model-config.v1';
5
+ export const LOCAL_MODEL_CONFIG_SCHEMA_V2 = 'sks.local-model-config.v2';
5
6
  export const OLLAMA_WORKER_CONFIG_SCHEMA = 'sks.ollama-worker-config.v1';
6
7
  export const DEFAULT_OLLAMA_CODER_MODEL = 'rafw007/qwen36-a3b-claude-coder:q4_K_M';
7
8
  export const DEFAULT_OLLAMA_BASE_URL = 'http://127.0.0.1:11434';
9
+ export const DEFAULT_MLX_LM_MODEL = 'mlx-community/Qwen3.6-35B-A3B-4bit';
10
+ export const DEFAULT_MLX_LM_BASE_URL = 'http://127.0.0.1:8080';
8
11
  export const DEFAULT_OLLAMA_KEEP_ALIVE = '30m';
9
12
  export const DEFAULT_OLLAMA_TIMEOUT_MS = 120_000;
10
13
  export const DEFAULT_OLLAMA_THINK = false;
14
+ export const LOCAL_LLM_SMOKE_TTL_MS = 24 * 60 * 60 * 1000;
11
15
  export function localModelConfigPath() {
12
16
  return process.env.SKS_LOCAL_MODEL_CONFIG
13
17
  ? path.resolve(process.env.SKS_LOCAL_MODEL_CONFIG)
@@ -30,28 +34,38 @@ export async function resolveOllamaWorkerConfig(input = {}) {
30
34
  const explicitDisable = boolEnv(process.env.SKS_OLLAMA_WORKERS) === false;
31
35
  const explicitEnable = boolEnv(process.env.SKS_OLLAMA_WORKERS) === true || input.ollamaEnabled === true || input.backend === 'ollama';
32
36
  const enabled = explicitDisable ? false : explicitEnable || stored.enabled === true;
33
- const model = firstText(process.env.SKS_OLLAMA_MODEL, input.model, stored.model, DEFAULT_OLLAMA_CODER_MODEL);
34
- const baseUrl = trimTrailingSlash(firstText(process.env.SKS_OLLAMA_BASE_URL, input.baseUrl, stored.base_url, DEFAULT_OLLAMA_BASE_URL));
37
+ const explicitProvider = firstText(process.env.SKS_LOCAL_LLM_PROVIDER, input.provider, input.backend === 'ollama' ? 'ollama' : '');
38
+ const provider = explicitProvider ? normalizeProvider(explicitProvider) : normalizeProvider(stored.provider);
39
+ const storedMatchesProvider = stored.provider === provider;
40
+ const model = firstText(process.env.SKS_LOCAL_LLM_MODEL, process.env.SKS_OLLAMA_MODEL, input.model, storedMatchesProvider ? stored.model : '', defaultModelForProvider(provider));
41
+ const baseUrl = trimTrailingSlash(firstText(process.env.SKS_LOCAL_LLM_BASE_URL, process.env.SKS_OLLAMA_BASE_URL, input.baseUrl, storedMatchesProvider ? stored.base_url : '', defaultBaseUrlForProvider(provider)));
35
42
  const keepAlive = firstText(process.env.SKS_OLLAMA_KEEP_ALIVE, input.keepAlive, stored.keep_alive, DEFAULT_OLLAMA_KEEP_ALIVE);
36
43
  const timeoutMs = positiveNumber(process.env.SKS_OLLAMA_TIMEOUT_MS, input.timeoutMs, stored.timeout_ms, DEFAULT_OLLAMA_TIMEOUT_MS);
37
44
  const temperature = finiteNumber(process.env.SKS_OLLAMA_TEMPERATURE, input.temperature, stored.temperature, 0.1);
38
45
  const think = boolEnv(process.env.SKS_OLLAMA_THINK) ?? input.think ?? stored.think ?? DEFAULT_OLLAMA_THINK;
46
+ const status = enabled ? resolveEnabledStatus(stored) : 'disabled';
39
47
  const blockers = [
40
48
  ...(enabled ? [] : ['ollama_workers_disabled']),
41
- ...(!model ? ['ollama_model_missing'] : []),
42
- ...(!baseUrl ? ['ollama_base_url_missing'] : [])
49
+ ...(enabled && status !== 'verified' ? [`local_llm_${status}`] : []),
50
+ ...(!model ? ['local_model_missing'] : []),
51
+ ...(!baseUrl ? ['local_model_base_url_missing'] : [])
43
52
  ];
44
53
  return {
45
54
  schema: OLLAMA_WORKER_CONFIG_SCHEMA,
46
55
  ok: blockers.length === 0,
47
56
  enabled,
48
- provider: 'ollama',
57
+ status,
58
+ provider,
49
59
  model,
50
60
  base_url: baseUrl,
61
+ endpoint: baseUrl,
51
62
  keep_alive: keepAlive,
52
63
  timeout_ms: timeoutMs,
53
64
  temperature,
54
65
  think,
66
+ policy: stored.policy,
67
+ capability: stored.capability,
68
+ last_smoke: stored.last_smoke,
55
69
  config_path: localModelConfigPath(),
56
70
  explicit_disable: explicitDisable,
57
71
  explicit_enable: explicitEnable,
@@ -59,25 +73,62 @@ export async function resolveOllamaWorkerConfig(input = {}) {
59
73
  };
60
74
  }
61
75
  export function normalizeLocalModelConfig(raw = {}) {
76
+ const enabled = raw.enabled === true;
77
+ const lastSmoke = normalizeSmoke(raw.last_smoke || raw.lastSmoke || null);
78
+ const status = enabled ? normalizeStatus(raw.status, lastSmoke) : 'disabled';
79
+ const provider = normalizeProvider(raw.provider);
80
+ const baseUrl = trimTrailingSlash(firstText(raw.base_url, raw.baseUrl, raw.endpoint, defaultBaseUrlForProvider(provider)));
62
81
  return {
63
- schema: LOCAL_MODEL_CONFIG_SCHEMA,
82
+ schema: LOCAL_MODEL_CONFIG_SCHEMA_V2,
64
83
  ...(raw.generated_at ? { generated_at: String(raw.generated_at) } : { generated_at: nowIso() }),
65
84
  ...(raw.updated_at ? { updated_at: String(raw.updated_at) } : {}),
66
- enabled: raw.enabled === true,
67
- provider: 'ollama',
68
- model: firstText(raw.model, DEFAULT_OLLAMA_CODER_MODEL),
69
- base_url: trimTrailingSlash(firstText(raw.base_url, raw.baseUrl, DEFAULT_OLLAMA_BASE_URL)),
85
+ enabled,
86
+ status,
87
+ provider,
88
+ model: firstText(raw.model, defaultModelForProvider(provider)),
89
+ endpoint: baseUrl,
90
+ base_url: baseUrl,
70
91
  keep_alive: firstText(raw.keep_alive, raw.keepAlive, DEFAULT_OLLAMA_KEEP_ALIVE),
71
92
  timeout_ms: positiveNumber(raw.timeout_ms, raw.timeoutMs, DEFAULT_OLLAMA_TIMEOUT_MS),
72
93
  temperature: finiteNumber(raw.temperature, 0.1),
73
94
  think: typeof raw.think === 'boolean' ? raw.think : DEFAULT_OLLAMA_THINK,
74
- policy: {
75
- worker_only: true,
76
- no_strategy_planning_design: true,
77
- allowed_work: ['simple_code_patch_envelopes', 'read_only_collection']
78
- }
95
+ policy: normalizePolicy(raw.policy),
96
+ capability: normalizeCapability(raw.capability),
97
+ last_smoke: lastSmoke,
98
+ blockers: Array.isArray(raw.blockers) ? raw.blockers.map(String) : status === 'blocked' ? ['local_llm_smoke_failed'] : []
79
99
  };
80
100
  }
101
+ export function applyLocalLlmSmokeResult(config, smoke) {
102
+ const blockers = Array.isArray(smoke.blockers) ? smoke.blockers.map(String) : [];
103
+ const status = smoke.skipped
104
+ ? 'enabled_unverified'
105
+ : smoke.ok && smoke.schema_valid !== false
106
+ ? 'verified'
107
+ : smoke.status === 'degraded'
108
+ ? 'degraded'
109
+ : 'blocked';
110
+ return normalizeLocalModelConfig({
111
+ ...config,
112
+ enabled: true,
113
+ status,
114
+ capability: {
115
+ ...config.capability,
116
+ api_reachable: smoke.ok !== false,
117
+ model_installed: smoke.ok !== false
118
+ },
119
+ last_smoke: {
120
+ ...smoke,
121
+ status
122
+ },
123
+ blockers
124
+ });
125
+ }
126
+ export function localModelSmokeFresh(smoke, now = Date.now()) {
127
+ if (!smoke?.ok || smoke.schema_valid === false || !smoke.ran_at)
128
+ return false;
129
+ const ranAt = Date.parse(smoke.ran_at);
130
+ return Number.isFinite(ranAt) && now - ranAt <= LOCAL_LLM_SMOKE_TTL_MS;
131
+ }
81
132
  export function boolEnv(value) {
82
133
  const text = String(value ?? '').trim().toLowerCase();
83
134
  if (!text)
@@ -99,6 +150,104 @@ function firstText(...values) {
99
150
  function trimTrailingSlash(value) {
100
151
  return value.replace(/\/+$/, '');
101
152
  }
153
+ export function normalizeProvider(...values) {
154
+ for (const value of values) {
155
+ const text = String(value ?? '').trim().toLowerCase();
156
+ if (!text)
157
+ continue;
158
+ if (['ollama'].includes(text))
159
+ return 'ollama';
160
+ if (['mlx', 'mlx-lm', 'mlx_lm', 'mlxlm'].includes(text))
161
+ return 'mlx-lm';
162
+ if (['openai-compatible', 'openai_compatible', 'openai', 'openai-compatible-local'].includes(text))
163
+ return 'openai-compatible';
164
+ }
165
+ return 'ollama';
166
+ }
167
+ export function defaultModelForProvider(provider) {
168
+ if (provider === 'mlx-lm')
169
+ return DEFAULT_MLX_LM_MODEL;
170
+ if (provider === 'openai-compatible')
171
+ return '';
172
+ return DEFAULT_OLLAMA_CODER_MODEL;
173
+ }
174
+ export function defaultBaseUrlForProvider(provider) {
175
+ if (provider === 'mlx-lm')
176
+ return DEFAULT_MLX_LM_BASE_URL;
177
+ if (provider === 'openai-compatible')
178
+ return '';
179
+ return DEFAULT_OLLAMA_BASE_URL;
180
+ }
181
+ function resolveEnabledStatus(config) {
182
+ if (config.status === 'verified' && !localModelSmokeFresh(config.last_smoke))
183
+ return 'enabled_unverified';
184
+ return config.status === 'disabled' ? 'enabled_unverified' : config.status;
185
+ }
186
+ function normalizeStatus(value, smoke) {
187
+ const text = String(value ?? '').trim();
188
+ const known = ['disabled', 'enabled_unverified', 'verified', 'degraded', 'blocked'];
189
+ if (known.includes(text)) {
190
+ if (text === 'verified' && !localModelSmokeFresh(smoke))
191
+ return 'enabled_unverified';
192
+ return text;
193
+ }
194
+ return localModelSmokeFresh(smoke) ? 'verified' : 'enabled_unverified';
195
+ }
196
+ function normalizePolicy(value) {
197
+ const defaultAllowed = ['simple_patch_envelope', 'read_only_collection', 'grep_like_qa', 'test_generation_draft'];
198
+ const allowed = [
199
+ ...defaultAllowed,
200
+ ...(Array.isArray(value?.allowed_task_classes) ? value.allowed_task_classes : []),
201
+ ...(Array.isArray(value?.allowed_work) ? value.allowed_work : [])
202
+ ];
203
+ const forbidden = Array.isArray(value?.forbidden_task_classes) ? value.forbidden_task_classes : [
204
+ 'planning',
205
+ 'strategy',
206
+ 'final_review',
207
+ 'verification_authority',
208
+ 'safety_authority',
209
+ 'integration_authority'
210
+ ];
211
+ return {
212
+ role: 'worker_only',
213
+ allowed_task_classes: [...new Set(allowed.map(String))],
214
+ forbidden_task_classes: forbidden.map(String),
215
+ requires_gpt_final: value?.requires_gpt_final !== false
216
+ };
217
+ }
218
+ function normalizeCapability(value) {
219
+ return {
220
+ api_reachable: value?.api_reachable === true,
221
+ model_installed: value?.model_installed === true,
222
+ supports_streaming: value?.supports_streaming !== false,
223
+ supports_json_schema: value?.supports_json_schema === true,
224
+ supports_tools: value?.supports_tools === true,
225
+ supports_images: value?.supports_images === true,
226
+ context_window: positiveNumber(value?.context_window, 32768),
227
+ max_parallel_requests: Math.max(1, Math.min(16, positiveNumber(value?.max_parallel_requests, 4)))
228
+ };
229
+ }
230
+ function normalizeSmoke(value) {
231
+ if (!value || typeof value !== 'object')
232
+ return null;
233
+ return {
234
+ ok: value.ok === true,
235
+ ...(value.skipped === true ? { skipped: true } : {}),
236
+ ...(value.ran_at ? { ran_at: String(value.ran_at) } : {}),
237
+ ...(value.prompt_hash ? { prompt_hash: String(value.prompt_hash) } : {}),
238
+ ...(Number.isFinite(Number(value.latency_ms)) ? { latency_ms: Number(value.latency_ms) } : {}),
239
+ ...(Number.isFinite(Number(value.tokens_per_second)) ? { tokens_per_second: Number(value.tokens_per_second) } : {}),
240
+ ...(typeof value.schema_valid === 'boolean' ? { schema_valid: value.schema_valid } : {}),
241
+ ...(value.result_path ? { result_path: String(value.result_path) } : {}),
242
+ ...(normalizeKnownStatus(value.status) ? { status: normalizeKnownStatus(value.status) } : {}),
243
+ ...(value.reason ? { reason: String(value.reason) } : {}),
244
+ ...(Array.isArray(value.blockers) ? { blockers: value.blockers.map(String) } : {})
245
+ };
246
+ }
247
+ function normalizeKnownStatus(value) {
248
+ const text = String(value ?? '').trim();
249
+ return ['disabled', 'enabled_unverified', 'verified', 'degraded', 'blocked'].includes(text) ? text : undefined;
250
+ }
102
251
  function positiveNumber(...values) {
103
252
  for (const value of values) {
104
253
  const n = Number(value);