sneakoscope 2.0.4 → 2.0.5

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 (104) hide show
  1. package/README.md +12 -8
  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 +73 -8
  8. package/dist/commands/doctor.js +14 -0
  9. package/dist/core/agents/agent-proof-evidence.js +35 -0
  10. package/dist/core/agents/agent-roster.js +35 -6
  11. package/dist/core/agents/agent-schema.js +1 -1
  12. package/dist/core/agents/native-worker-backend-router.js +31 -9
  13. package/dist/core/agents/ollama-worker-config.js +164 -15
  14. package/dist/core/codex/codex-0-137-compat.js +119 -0
  15. package/dist/core/codex-control/codex-control-proof.js +4 -1
  16. package/dist/core/codex-control/codex-sdk-capability.js +1 -1
  17. package/dist/core/codex-control/codex-task-runner.js +329 -5
  18. package/dist/core/codex-control/python-codex-sdk-adapter.js +197 -0
  19. package/dist/core/codex-control/python-codex-sdk-event-translator.js +14 -0
  20. package/dist/core/commands/local-model-command.js +65 -19
  21. package/dist/core/commands/naruto-command.js +118 -7
  22. package/dist/core/commands/run-command.js +1 -1
  23. package/dist/core/doctor/doctor-readiness-matrix.js +21 -2
  24. package/dist/core/fsx.js +1 -1
  25. package/dist/core/local-llm/local-llm-backpressure.js +20 -0
  26. package/dist/core/local-llm/local-llm-capability.js +29 -0
  27. package/dist/core/local-llm/local-llm-client.js +100 -0
  28. package/dist/core/local-llm/local-llm-config.js +6 -1
  29. package/dist/core/local-llm/local-llm-context-cache.js +21 -0
  30. package/dist/core/local-llm/local-llm-control-adapter.js +101 -0
  31. package/dist/core/local-llm/local-llm-json-repair.js +52 -0
  32. package/dist/core/local-llm/local-llm-metrics.js +42 -0
  33. package/dist/core/local-llm/local-llm-ollama-client.js +67 -0
  34. package/dist/core/local-llm/local-llm-openai-compatible-client.js +30 -0
  35. package/dist/core/local-llm/local-llm-prompt-cache.js +12 -0
  36. package/dist/core/local-llm/local-llm-scheduler.js +29 -0
  37. package/dist/core/local-llm/local-llm-schema-enforcer.js +15 -0
  38. package/dist/core/local-llm/local-llm-smoke.js +83 -0
  39. package/dist/core/local-llm/local-llm-warmup.js +20 -0
  40. package/dist/core/local-llm/local-worker-eligibility.js +27 -0
  41. package/dist/core/naruto/hardware-capacity-probe.js +36 -0
  42. package/dist/core/naruto/naruto-active-pool.js +118 -0
  43. package/dist/core/naruto/naruto-backpressure.js +13 -0
  44. package/dist/core/naruto/naruto-concurrency-governor.js +65 -0
  45. package/dist/core/naruto/naruto-finalizer.js +18 -0
  46. package/dist/core/naruto/naruto-generation-scheduler.js +18 -0
  47. package/dist/core/naruto/naruto-gpt-final-pack.js +49 -0
  48. package/dist/core/naruto/naruto-parallel-patch-apply.js +95 -0
  49. package/dist/core/naruto/naruto-patch-transaction-batch.js +42 -0
  50. package/dist/core/naruto/naruto-role-policy.js +107 -0
  51. package/dist/core/naruto/naruto-verification-dag.js +42 -0
  52. package/dist/core/naruto/naruto-verification-pool.js +18 -0
  53. package/dist/core/naruto/naruto-work-graph.js +198 -0
  54. package/dist/core/naruto/naruto-work-item.js +40 -0
  55. package/dist/core/naruto/naruto-work-stealing.js +11 -0
  56. package/dist/core/naruto/resource-pressure-monitor.js +32 -0
  57. package/dist/core/pipeline/finalize-pipeline-result.js +58 -0
  58. package/dist/core/pipeline/gpt-final-required.js +12 -0
  59. package/dist/core/prompt/prompt-placeholder-guard.js +30 -0
  60. package/dist/core/router/capability-card.js +13 -0
  61. package/dist/core/router/route-cache.js +3 -0
  62. package/dist/core/router/ultra-router.js +2 -1
  63. package/dist/core/routes.js +4 -4
  64. package/dist/core/version.js +1 -1
  65. package/dist/core/zellij/zellij-lane-runtime.js +2 -2
  66. package/dist/core/zellij/zellij-naruto-dashboard.js +36 -0
  67. package/dist/core/zellij/zellij-worker-pane-manager.js +4 -4
  68. package/dist/scripts/blackbox-command-import-smoke.js +10 -1
  69. package/dist/scripts/check-package-boundary.js +12 -3
  70. package/dist/scripts/codex-0-137-compat-check.js +27 -0
  71. package/dist/scripts/codex-environment-scoped-approvals-check.js +10 -0
  72. package/dist/scripts/codex-plugin-list-json-check.js +8 -0
  73. package/dist/scripts/codex-thread-runtime-choice-check.js +10 -0
  74. package/dist/scripts/local-collab-all-pipelines-final-gpt-check.js +21 -0
  75. package/dist/scripts/local-llm-all-pipelines-check.js +11 -0
  76. package/dist/scripts/local-llm-cache-performance-check.js +10 -0
  77. package/dist/scripts/local-llm-capability-check.js +14 -0
  78. package/dist/scripts/local-llm-smoke-check.js +23 -0
  79. package/dist/scripts/local-llm-structured-output-check.js +11 -0
  80. package/dist/scripts/local-llm-throughput-check.js +10 -0
  81. package/dist/scripts/local-llm-tool-call-repair-check.js +10 -0
  82. package/dist/scripts/local-llm-warmup-check.js +11 -0
  83. package/dist/scripts/naruto-active-pool-check.js +27 -0
  84. package/dist/scripts/naruto-concurrency-governor-check.js +52 -0
  85. package/dist/scripts/naruto-gpt-final-pack-check.js +34 -0
  86. package/dist/scripts/naruto-parallel-patch-apply-check.js +41 -0
  87. package/dist/scripts/naruto-real-local-gpt-final-smoke.js +16 -0
  88. package/dist/scripts/naruto-role-distribution-check.js +23 -0
  89. package/dist/scripts/naruto-shadow-clone-swarm-check.js +6 -0
  90. package/dist/scripts/naruto-verification-pool-check.js +36 -0
  91. package/dist/scripts/naruto-work-graph-check.js +24 -0
  92. package/dist/scripts/naruto-zellij-massive-ui-check.js +23 -0
  93. package/dist/scripts/prompt-placeholder-guard-check.js +33 -0
  94. package/dist/scripts/python-codex-sdk-all-pipelines-check.js +47 -0
  95. package/dist/scripts/python-codex-sdk-capability-check.js +75 -0
  96. package/dist/scripts/python-codex-sdk-sandbox-policy-check.js +10 -0
  97. package/dist/scripts/python-codex-sdk-stream-bridge-check.js +12 -0
  98. package/dist/scripts/release-parallel-check.js +1 -1
  99. package/dist/scripts/release-real-check.js +5 -0
  100. package/dist/scripts/zellij-worker-pane-manager-check.js +1 -1
  101. package/package.json +33 -4
  102. package/schemas/local-llm/local-model-config.schema.json +74 -0
  103. package/schemas/naruto/naruto-concurrency-governor.schema.json +21 -0
  104. package/schemas/naruto/naruto-work-graph.schema.json +22 -0
@@ -1,4 +1,6 @@
1
- import { resolveOllamaWorkerConfig, writeLocalModelConfig, readLocalModelConfig } from '../agents/ollama-worker-config.js';
1
+ import { applyLocalLlmSmokeResult, normalizeProvider, resolveOllamaWorkerConfig, writeLocalModelConfig, readLocalModelConfig } from '../agents/ollama-worker-config.js';
2
+ import { detectInstalledLocalModelCandidate, probeLocalLlmEndpoint } from '../local-llm/local-llm-client.js';
3
+ import { runLocalLlmGenerationSmoke, localLlmSmokeSchema } from '../local-llm/local-llm-smoke.js';
2
4
  export async function localModelCommand(args = []) {
3
5
  const action = normalizeLocalModelAction(args[0]);
4
6
  if (action === 'enable')
@@ -28,16 +30,62 @@ function normalizeLocalModelAction(value) {
28
30
  async function enable(args) {
29
31
  const model = readOption(args, '--model', firstPositional(args) || '');
30
32
  const baseUrl = readOption(args, '--base-url', '');
33
+ const provider = readOption(args, '--provider', '');
31
34
  const think = readBoolFlag(args, '--think', '--no-think');
32
- const patch = { enabled: true, provider: 'ollama' };
35
+ const skipSmoke = args.includes('--skip-smoke') || process.env.SKS_LOCAL_LLM_TOGGLE_ONLY === '1';
36
+ const patch = { enabled: true, status: 'enabled_unverified' };
37
+ const explicitConfig = Boolean(model || baseUrl || provider);
38
+ let detection = null;
39
+ if (!explicitConfig) {
40
+ detection = await detectInstalledLocalModelCandidate();
41
+ if (!detection) {
42
+ const config = await writeLocalModelConfig({ enabled: false, blockers: ['local_model_not_found'] });
43
+ process.exitCode = 1;
44
+ return {
45
+ schema: 'sks.local-model-command.v1',
46
+ ok: false,
47
+ action: 'enable',
48
+ message: '확인해보니 로컬 모델이 존재하지 않아 실행할 수 없습니다.',
49
+ config,
50
+ detection: null,
51
+ blockers: ['local_model_not_found']
52
+ };
53
+ }
54
+ patch.provider = detection.provider;
55
+ patch.model = detection.model;
56
+ patch.base_url = detection.base_url;
57
+ patch.endpoint = detection.endpoint;
58
+ }
59
+ if (provider)
60
+ patch.provider = normalizeProvider(provider);
33
61
  if (model)
34
62
  patch.model = model;
35
- if (baseUrl)
63
+ if (baseUrl) {
36
64
  patch.base_url = baseUrl;
65
+ patch.endpoint = baseUrl;
66
+ }
37
67
  if (think !== null)
38
68
  patch.think = think;
39
69
  const config = await writeLocalModelConfig(patch);
40
- return { schema: 'sks.local-model-command.v1', ok: true, action: 'enable', config };
70
+ const smoke = skipSmoke
71
+ ? { ok: false, skipped: true, status: 'enabled_unverified', reason: 'operator_skip_smoke', schema_valid: false, blockers: ['operator_skip_smoke'] }
72
+ : await runLocalLlmGenerationSmoke(config, {
73
+ prompt: 'Return strict JSON: {"status":"ok","summary":"local smoke passed"}',
74
+ schema: localLlmSmokeSchema,
75
+ timeoutMs: 20_000
76
+ });
77
+ const next = await writeLocalModelConfig(applyLocalLlmSmokeResult(config, smoke));
78
+ if (!skipSmoke && smoke.ok !== true)
79
+ process.exitCode = 1;
80
+ return {
81
+ schema: 'sks.local-model-command.v1',
82
+ ok: skipSmoke ? true : smoke.ok === true,
83
+ action: 'enable',
84
+ config: next,
85
+ detection,
86
+ smoke,
87
+ blockers: next.blockers
88
+ };
41
89
  }
42
90
  async function disable() {
43
91
  const config = await writeLocalModelConfig({ enabled: false });
@@ -55,37 +103,35 @@ async function setModel(args) {
55
103
  async function status() {
56
104
  const config = await readLocalModelConfig();
57
105
  const resolved = await resolveOllamaWorkerConfig();
58
- const api = await probeOllama(resolved.base_url);
106
+ const api = await probeLocalLlmEndpoint(resolved);
59
107
  return { schema: 'sks.local-model-command.v1', ok: true, action: 'status', config, resolved, api };
60
108
  }
61
- async function probeOllama(baseUrl) {
62
- try {
63
- const response = await fetch(`${baseUrl}/api/version`, { signal: AbortSignal.timeout(3000) });
64
- const text = await response.text();
65
- return { ok: response.ok, status: response.status, data: response.ok ? JSON.parse(text) : null };
66
- }
67
- catch (error) {
68
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
69
- }
70
- }
71
109
  function emit(result, args) {
72
110
  if (args.includes('--json')) {
73
111
  console.log(JSON.stringify(result, null, 2));
74
112
  return result;
75
113
  }
76
114
  if (result.ok !== true) {
115
+ if (result.message)
116
+ console.log(result.message);
77
117
  console.log(`Local model: blocked (${(result.blockers || []).join(', ') || 'unknown'})`);
78
118
  return result;
79
119
  }
80
120
  const config = result.config || result.resolved || {};
81
121
  console.log(`Local model: ${config.enabled ? 'enabled' : 'disabled'}`);
82
- console.log(`Provider: ollama`);
122
+ console.log(`Provider: ${config.provider || 'unknown'}`);
83
123
  console.log(`Model: ${config.model || 'unknown'}`);
84
124
  console.log(`Base URL: ${config.base_url || config.baseUrl || 'unknown'}`);
125
+ if (config.status)
126
+ console.log(`Status: ${config.status}`);
127
+ if (result.detection)
128
+ console.log(`Detected: ${result.detection.source}`);
129
+ if (config.last_smoke?.result_path)
130
+ console.log(`Smoke: ${config.last_smoke.ok ? 'ok' : 'failed'} ${config.last_smoke.result_path}`);
85
131
  if (typeof config.think === 'boolean')
86
132
  console.log(`Think: ${config.think ? 'enabled' : 'disabled'}`);
87
133
  if (result.api)
88
- console.log(`Ollama API: ${result.api.ok ? 'ok' : 'not reachable'}`);
134
+ console.log(`Local model API: ${result.api.ok ? 'ok' : 'not reachable'}`);
89
135
  return result;
90
136
  }
91
137
  function readOption(args, name, fallback) {
@@ -105,12 +151,12 @@ function readBoolFlag(args, trueName, falseName) {
105
151
  function firstPositional(args = []) {
106
152
  for (let i = 0; i < args.length; i += 1) {
107
153
  const arg = String(args[i] || '');
108
- if (arg === '--model' || arg === '--base-url') {
154
+ if (arg === '--model' || arg === '--base-url' || arg === '--provider') {
109
155
  if (args[i + 1] && !String(args[i + 1]).startsWith('--'))
110
156
  i += 1;
111
157
  continue;
112
158
  }
113
- if (arg.startsWith('--model=') || arg.startsWith('--base-url='))
159
+ if (arg.startsWith('--model=') || arg.startsWith('--base-url=') || arg.startsWith('--provider='))
114
160
  continue;
115
161
  if (!arg.startsWith('--'))
116
162
  return arg;
@@ -1,12 +1,20 @@
1
1
  import path from 'node:path';
2
2
  import { createMission, findLatestMission, loadMission } from '../mission.js';
3
- import { readJson, sksRoot } from '../fsx.js';
3
+ import { readJson, sksRoot, writeJsonAtomic } from '../fsx.js';
4
4
  import { runNativeAgentOrchestrator } from '../agents/agent-orchestrator.js';
5
5
  import { classifyOllamaWorkerSlice } from '../agents/agent-runner-ollama.js';
6
6
  import { buildNarutoCloneRoster, systemSafeNarutoConcurrency } from '../agents/agent-roster.js';
7
7
  import { DEFAULT_NARUTO_CLONES, MAX_NARUTO_AGENT_COUNT } from '../agents/agent-schema.js';
8
8
  import { resolveOllamaWorkerConfig } from '../agents/ollama-worker-config.js';
9
9
  import { attachZellijSessionInteractive, launchZellijLayout } from '../zellij/zellij-launcher.js';
10
+ import { buildNarutoWorkGraph } from '../naruto/naruto-work-graph.js';
11
+ import { buildNarutoRoleDistribution } from '../naruto/naruto-role-policy.js';
12
+ import { decideNarutoConcurrency } from '../naruto/naruto-concurrency-governor.js';
13
+ import { simulateNarutoActivePool } from '../naruto/naruto-active-pool.js';
14
+ import { buildNarutoVerificationDag } from '../naruto/naruto-verification-dag.js';
15
+ import { buildNarutoGptFinalPack } from '../naruto/naruto-gpt-final-pack.js';
16
+ import { planNarutoZellijDashboard } from '../zellij/zellij-naruto-dashboard.js';
17
+ import { checkPromptPlaceholders } from '../prompt/prompt-placeholder-guard.js';
10
18
  const NARUTO_RESULT_SCHEMA = 'sks.naruto-command-result.v1';
11
19
  const NARUTO_ROUTE = '$Naruto';
12
20
  // $Naruto — Shadow Clone Swarm (影分身 / Kage Bunshin no Jutsu).
@@ -25,6 +33,27 @@ export async function narutoCommand(commandOrArgs = 'naruto', maybeArgs = []) {
25
33
  }
26
34
  async function narutoRun(parsed) {
27
35
  const root = await sksRoot();
36
+ const writeCapable = parsed.readonly !== true && parsed.writeMode !== 'off';
37
+ const placeholderGuard = checkPromptPlaceholders({
38
+ prompt: parsed.prompt,
39
+ writeCapable,
40
+ targetPaths: writeCapable ? ['.sneakoscope/naruto/patch-envelopes'] : []
41
+ });
42
+ if (!placeholderGuard.ok) {
43
+ return emit(parsed, {
44
+ schema: NARUTO_RESULT_SCHEMA,
45
+ ok: false,
46
+ mode: 'NARUTO',
47
+ action: 'run',
48
+ status: 'blocked',
49
+ prompt_placeholder_guard: placeholderGuard,
50
+ blockers: placeholderGuard.blockers
51
+ }, () => {
52
+ console.log('$Naruto blocked before work graph creation: unresolved prompt placeholder or empty write target path.');
53
+ for (const blocker of placeholderGuard.blockers)
54
+ console.log('- ' + blocker);
55
+ });
56
+ }
28
57
  const roster = buildNarutoCloneRoster({
29
58
  clones: parsed.clones,
30
59
  prompt: parsed.prompt,
@@ -37,9 +66,52 @@ async function narutoRun(parsed) {
37
66
  const localWorker = await resolveNarutoLocalWorkerMode(parsed);
38
67
  const schedulerBackend = localWorker.auto_select_eligible ? 'ollama' : parsed.backend;
39
68
  const safe = systemSafeNarutoConcurrency({ backend: schedulerBackend });
40
- const activeSlots = Math.max(1, Math.min(roster.agent_count, parsed.concurrency || safe.cap));
69
+ const workGraph = buildNarutoWorkGraph({
70
+ prompt: parsed.prompt,
71
+ requestedClones: roster.agent_count,
72
+ totalWorkItems: parsed.workItems,
73
+ readonly: parsed.readonly,
74
+ writeCapable,
75
+ targetPaths: ['.sneakoscope/naruto/patch-envelopes'],
76
+ maxActiveWorkers: parsed.concurrency || safe.cap
77
+ });
78
+ const roleDistribution = buildNarutoRoleDistribution(workGraph.work_items, { readonly: parsed.readonly });
79
+ const governor = decideNarutoConcurrency({
80
+ requestedClones: roster.agent_count,
81
+ totalWorkItems: workGraph.total_work_items,
82
+ pendingWorkQueueSize: workGraph.total_work_items,
83
+ backend: schedulerBackend
84
+ });
85
+ const backendMinimum = schedulerBackend === 'fake' ? roster.agent_count : Math.min(roster.agent_count, 2);
86
+ const activeSlots = Math.max(1, Math.min(roster.agent_count, parsed.concurrency || Math.max(governor.safe_active_workers, backendMinimum), safe.cap));
87
+ const zellijVisiblePanes = Math.max(1, Math.min(activeSlots, governor.safe_zellij_visible_panes));
88
+ const activePool = simulateNarutoActivePool({ graph: workGraph, governor: { ...governor, safe_active_workers: activeSlots } });
89
+ const verificationDag = buildNarutoVerificationDag(workGraph, { cwd: root });
90
+ const gptFinalPack = buildNarutoGptFinalPack({
91
+ missionId: 'pending',
92
+ graph: workGraph,
93
+ roleDistribution,
94
+ localLlmMetrics: localWorker
95
+ });
96
+ const zellijDashboard = planNarutoZellijDashboard({
97
+ targetActiveWorkers: activeSlots,
98
+ visiblePaneCap: governor.safe_zellij_visible_panes,
99
+ backpressure: governor.backpressure,
100
+ roles: roleDistribution.work_item_roles.map((row) => row.role),
101
+ backend: schedulerBackend
102
+ });
41
103
  const mission = await createMission(root, { mode: 'naruto', prompt: parsed.prompt });
42
104
  const ledgerRoot = path.join(mission.dir, 'agents');
105
+ await writeNarutoArtifacts(ledgerRoot, {
106
+ workGraph,
107
+ roleDistribution,
108
+ governor,
109
+ activePool,
110
+ verificationDag,
111
+ gptFinalPack: { ...gptFinalPack, mission_id: mission.id },
112
+ zellijDashboard,
113
+ placeholderGuard
114
+ });
43
115
  let liveZellij = null;
44
116
  if (!parsed.json && !parsed.mock && !parsed.noOpenZellij) {
45
117
  liveZellij = await launchZellijLayout({
@@ -47,12 +119,12 @@ async function narutoRun(parsed) {
47
119
  missionId: mission.id,
48
120
  ledgerRoot,
49
121
  kind: 'naruto',
50
- slotCount: roster.agent_count,
122
+ slotCount: zellijVisiblePanes,
51
123
  dryRun: false,
52
124
  attach: false
53
125
  });
54
126
  if (liveZellij?.ok && liveZellij.capability?.status === 'ok') {
55
- console.log('Zellij: prepared ' + roster.agent_count + ' live clone lane(s) in ' + liveZellij.session_name + '. Attach with: ' + (liveZellij.attach_command_with_env || liveZellij.attach_command));
127
+ console.log('Zellij: prepared ' + zellijVisiblePanes + ' visible active clone lane(s) in ' + liveZellij.session_name + ' with ' + Math.max(0, activeSlots - zellijVisiblePanes) + ' headless active worker(s). Attach with: ' + (liveZellij.attach_command_with_env || liveZellij.attach_command));
56
128
  if (parsed.attach)
57
129
  attachZellijSessionInteractive(liveZellij.session_name, { cwd: process.cwd(), configPath: liveZellij.clipboard_config_path });
58
130
  }
@@ -73,7 +145,7 @@ async function narutoRun(parsed) {
73
145
  agents: roster.agent_count,
74
146
  concurrency: activeSlots,
75
147
  targetActiveSlots: activeSlots,
76
- visualLaneCount: roster.agent_count,
148
+ visualLaneCount: zellijVisiblePanes,
77
149
  desiredWorkItemCount: parsed.workItems,
78
150
  maxAgentCount: MAX_NARUTO_AGENT_COUNT,
79
151
  narutoMode: true,
@@ -109,6 +181,20 @@ async function narutoRun(parsed) {
109
181
  target_active_slots: result.target_active_slots ?? activeSlots,
110
182
  concurrency_capped: clones > (result.target_active_slots ?? activeSlots),
111
183
  system: { cores: safe.cores, free_gb: safe.free_gb, safe_concurrency: safe.cap, heavy_backend: safe.heavy },
184
+ work_graph: {
185
+ total_work_items: workGraph.total_work_items,
186
+ mixed_work_kinds: workGraph.mixed_work_kinds,
187
+ write_allowed_count: workGraph.write_allowed_count,
188
+ ok: workGraph.ok
189
+ },
190
+ role_distribution: roleDistribution,
191
+ concurrency_governor: governor,
192
+ active_pool: {
193
+ ok: activePool.ok,
194
+ max_observed_active_workers: activePool.max_observed_active_workers,
195
+ refill_events: activePool.refill_events,
196
+ completed_count: activePool.completed_count
197
+ },
112
198
  local_worker: localWorkerSummary,
113
199
  proof: result.proof?.status || 'missing',
114
200
  run: result,
@@ -120,9 +206,10 @@ async function narutoRun(parsed) {
120
206
  console.log('Mission: ' + result.mission_id);
121
207
  console.log('Clones: ' + summary.clones + ' / max ' + MAX_NARUTO_AGENT_COUNT + ', running ' + summary.target_active_slots + ' at a time' + (summary.concurrency_capped ? ` (throttled to host capacity: ${safe.cores} cores, ${safe.free_gb} GB free)` : ''));
122
208
  console.log('Backend: ' + result.backend);
209
+ console.log('Roles: ' + roleDistribution.entries.map((entry) => `${entry.role}:${entry.count}`).join(', '));
123
210
  console.log('Proof: ' + summary.proof);
124
211
  if (summary.zellij?.ok && summary.zellij.capability?.status === 'ok')
125
- console.log('Zellij: prepared ' + summary.clones + ' native clone lane(s) in ' + summary.zellij.session_name);
212
+ console.log('Zellij: prepared ' + zellijVisiblePanes + ' visible active clone lane(s) in ' + summary.zellij.session_name + '; dashboard tracks ' + Math.max(0, activeSlots - zellijVisiblePanes) + ' headless active worker(s)');
126
213
  else if (summary.zellij?.ok)
127
214
  console.log('Zellij: optional live panes unavailable (' + ((summary.zellij.warnings || []).join('; ') || summary.zellij.capability?.status || 'unknown') + ')');
128
215
  });
@@ -148,6 +235,9 @@ async function narutoStatus(parsed) {
148
235
  const { dir } = await loadMission(root, id);
149
236
  const proof = await readJson(path.join(dir, 'agents', 'agent-proof-evidence.json'), null);
150
237
  const scheduler = await readJson(path.join(dir, 'agents', 'agent-scheduler-state.json'), null);
238
+ const roleDistribution = await readJson(path.join(dir, 'agents', 'naruto-role-distribution.json'), null);
239
+ const workGraph = await readJson(path.join(dir, 'agents', 'naruto-work-graph.json'), null);
240
+ const governor = await readJson(path.join(dir, 'agents', 'naruto-concurrency-governor.json'), null);
151
241
  const summary = {
152
242
  schema: NARUTO_RESULT_SCHEMA,
153
243
  ok: proof !== null,
@@ -156,13 +246,22 @@ async function narutoStatus(parsed) {
156
246
  proof: proof?.status || 'missing',
157
247
  target_active_slots: scheduler?.target_active_slots ?? null,
158
248
  max_active_slots: scheduler?.max_active_slots ?? null,
159
- completed: scheduler?.completed_count ?? null
249
+ completed: scheduler?.completed_count ?? null,
250
+ role_distribution: roleDistribution,
251
+ work_graph: workGraph ? {
252
+ total_work_items: workGraph.total_work_items,
253
+ mixed_work_kinds: workGraph.mixed_work_kinds,
254
+ write_allowed_count: workGraph.write_allowed_count
255
+ } : null,
256
+ concurrency_governor: governor
160
257
  };
161
258
  return emit(parsed, summary, () => {
162
259
  console.log('🍥 Naruto mission: ' + id);
163
260
  console.log('Proof: ' + summary.proof);
164
261
  if (summary.target_active_slots !== null)
165
262
  console.log('Active clones: ' + summary.target_active_slots + ' / max ' + summary.max_active_slots);
263
+ if (roleDistribution?.entries)
264
+ console.log('Roles: ' + roleDistribution.entries.map((entry) => `${entry.role}:${entry.count}`).join(', '));
166
265
  });
167
266
  }
168
267
  async function narutoHelp(parsed) {
@@ -186,6 +285,8 @@ async function narutoHelp(parsed) {
186
285
  });
187
286
  }
188
287
  function parseNarutoArgs(args = []) {
288
+ if (hasFlag(args, '--help') || hasFlag(args, '-h'))
289
+ args = ['help', ...args.filter((arg) => arg !== '--help' && arg !== '-h')];
189
290
  const first = args[0] && !String(args[0]).startsWith('--') ? String(args[0]) : '';
190
291
  const actions = new Set(['run', 'status', 'help']);
191
292
  const action = (actions.has(first) ? first : 'run');
@@ -213,6 +314,16 @@ function parseNarutoArgs(args = []) {
213
314
  const prompt = positionalArgs(rest, valueFlags).join(' ').trim() || 'Naruto shadow clone swarm run';
214
315
  return { action, prompt, clones, workItems, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, json, missionId, noOpenZellij, attach };
215
316
  }
317
+ async function writeNarutoArtifacts(ledgerRoot, artifacts) {
318
+ await writeJsonAtomic(path.join(ledgerRoot, 'naruto-work-graph.json'), artifacts.workGraph);
319
+ await writeJsonAtomic(path.join(ledgerRoot, 'naruto-role-distribution.json'), artifacts.roleDistribution);
320
+ await writeJsonAtomic(path.join(ledgerRoot, 'naruto-concurrency-governor.json'), artifacts.governor);
321
+ await writeJsonAtomic(path.join(ledgerRoot, 'naruto-active-pool.json'), artifacts.activePool);
322
+ await writeJsonAtomic(path.join(ledgerRoot, 'naruto-verification-dag.json'), artifacts.verificationDag);
323
+ await writeJsonAtomic(path.join(ledgerRoot, 'naruto-gpt-final-pack.json'), artifacts.gptFinalPack);
324
+ await writeJsonAtomic(path.join(ledgerRoot, 'naruto-zellij-dashboard.json'), artifacts.zellijDashboard);
325
+ await writeJsonAtomic(path.join(ledgerRoot, 'prompt-placeholder-guard.json'), artifacts.placeholderGuard);
326
+ }
216
327
  function clampClones(value) {
217
328
  if (!Number.isFinite(value) || value < 1)
218
329
  return DEFAULT_NARUTO_CLONES;
@@ -290,7 +290,7 @@ async function runSks(root, commandArgs) {
290
290
  cwd: root,
291
291
  timeoutMs: 180_000,
292
292
  maxOutputBytes: 512 * 1024,
293
- env: { SKS_SKIP_NPM_FRESHNESS_CHECK: '1', CI: 'true' },
293
+ env: { SKS_SKIP_NPM_FRESHNESS_CHECK: '1', SKS_LOCAL_LLM_TOGGLE_ONLY: '1', CI: 'true' },
294
294
  });
295
295
  }
296
296
  function routeExecutionResult(route, command, result, options = {}) {
@@ -54,6 +54,14 @@ export function buildDoctorReadinessMatrix(input = {}) {
54
54
  warnings.add('codex_app_fast_selector_repaired_restart_app_if_needed');
55
55
  if (input.codex_lb?.ok === false)
56
56
  warnings.add(`codex_lb_${input.codex_lb?.circuit?.state || 'blocked'}`);
57
+ const localModel = input.local_model || {};
58
+ const localStatus = String(localModel.status || (localModel.enabled ? 'enabled_unverified' : 'disabled'));
59
+ if (localModel.enabled === true && localStatus === 'enabled_unverified')
60
+ warnings.add('local_llm_enabled_unverified');
61
+ if (localModel.enabled === true && localStatus === 'degraded')
62
+ warnings.add('local_llm_degraded');
63
+ if (localModel.enabled === true && localStatus === 'blocked')
64
+ warnings.add('local_llm_blocked_worker_tier_disabled');
57
65
  const localCollaborationPolicy = resolveLocalCollaborationPolicy({ mode: input.local_collaboration?.mode || null });
58
66
  const gptFinalAvailable = input.local_collaboration?.gpt_final_arbiter_available === undefined
59
67
  ? codexBinOk
@@ -101,12 +109,23 @@ export function buildDoctorReadinessMatrix(input = {}) {
101
109
  codex_app_required_for_cli: false,
102
110
  local_collaboration: {
103
111
  mode: localCollaborationPolicy.mode,
104
- local_backend: input.local_collaboration?.local_backend || input.local_model?.provider || 'ollama',
105
- local_model: input.local_collaboration?.local_model || input.local_model?.model || null,
112
+ local_backend: input.local_collaboration?.local_backend || localModel.provider || 'ollama',
113
+ local_model: input.local_collaboration?.local_model || localModel.model || null,
106
114
  final_arbiter: gptFinalAvailable ? 'GPT available' : 'missing',
107
115
  final_apply_allowed: localCollaborationPolicy.gpt_final_required ? gptFinalAvailable : localCollaborationPolicy.mode === 'disabled',
108
116
  blockers: localCollaborationPolicy.gpt_final_required && !gptFinalAvailable ? ['gpt_final_arbiter_unavailable'] : localCollaborationPolicy.blockers
109
117
  },
118
+ local_llm: {
119
+ enabled: localModel.enabled === true,
120
+ status: localStatus,
121
+ provider: localModel.provider || 'ollama',
122
+ model: localModel.model || null,
123
+ endpoint: localModel.endpoint || localModel.base_url || null,
124
+ last_smoke: localModel.last_smoke || null,
125
+ final_arbiter: 'GPT required',
126
+ worker_tier_enabled: localModel.enabled === true && localStatus === 'verified',
127
+ blockers: normalizeList(localModel.blockers)
128
+ },
110
129
  ready: blockers.size === 0 && cliReady,
111
130
  primary_blocker: [...blockers][0] || null,
112
131
  blockers: [...blockers],
package/dist/core/fsx.js CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
- export const PACKAGE_VERSION = '2.0.4';
8
+ export const PACKAGE_VERSION = '2.0.5';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
  export function nowIso() {
@@ -0,0 +1,20 @@
1
+ export function classifyLocalLlmBackpressure(input) {
2
+ const max = Math.max(1, Math.floor(Number(input.maxParallelRequests || 1)));
3
+ const active = Math.max(0, Math.floor(Number(input.activeRequests || 0)));
4
+ const queue = Math.max(0, Math.floor(Number(input.queueDepth || 0)));
5
+ const p95 = Math.max(0, Number(input.p95LatencyMs || 0));
6
+ const state = active >= max && queue >= max
7
+ ? 'saturated'
8
+ : active >= max || queue > max || p95 > 10_000
9
+ ? 'throttled'
10
+ : 'normal';
11
+ return {
12
+ schema: 'sks.local-llm-backpressure.v1',
13
+ state,
14
+ active_requests: active,
15
+ max_parallel_requests: max,
16
+ queue_depth: queue,
17
+ p95_latency_ms: p95
18
+ };
19
+ }
20
+ //# sourceMappingURL=local-llm-backpressure.js.map
@@ -0,0 +1,29 @@
1
+ import { listLocalLlmModels, probeLocalLlmEndpoint } from './local-llm-client.js';
2
+ export async function detectLocalLlmCapability(config) {
3
+ const version = await probeLocalLlmEndpoint(config);
4
+ const tags = version.ok ? await listLocalLlmModels(config) : { ok: false, models: [] };
5
+ const modelInstalled = tags.models.includes(config.model);
6
+ const capability = {
7
+ api_reachable: version.ok,
8
+ model_installed: modelInstalled,
9
+ supports_streaming: true,
10
+ supports_json_schema: config.provider === 'ollama',
11
+ supports_tools: false,
12
+ supports_images: false,
13
+ context_window: config.capability.context_window || 32768,
14
+ max_parallel_requests: config.capability.max_parallel_requests || 4
15
+ };
16
+ const blockers = [
17
+ ...(version.ok ? [] : ['local_model_endpoint_unreachable']),
18
+ ...(modelInstalled ? [] : ['local_model_missing'])
19
+ ];
20
+ return {
21
+ ok: blockers.length === 0,
22
+ provider: config.provider,
23
+ model: config.model,
24
+ endpoint: config.base_url,
25
+ capability,
26
+ blockers
27
+ };
28
+ }
29
+ //# sourceMappingURL=local-llm-capability.js.map
@@ -0,0 +1,100 @@
1
+ import { callOllamaGenerate, listOllamaModels, ollamaTokensPerSecond, probeOllamaVersion } from './local-llm-ollama-client.js';
2
+ import { callOpenAiCompatibleLocalChat } from './local-llm-openai-compatible-client.js';
3
+ export async function probeLocalLlmEndpoint(config) {
4
+ if (config.provider === 'ollama')
5
+ return probeOllamaVersion(config.base_url, Math.min(5000, Number(config.timeout_ms || 3000)));
6
+ return probeOpenAiCompatibleModels(config.base_url, Math.min(5000, Number(config.timeout_ms || 3000)));
7
+ }
8
+ export async function listLocalLlmModels(config) {
9
+ if (config.provider === 'ollama')
10
+ return listOllamaModels(config.base_url, Math.min(5000, Number(config.timeout_ms || 5000)));
11
+ return listOpenAiCompatibleModels(config.base_url, Math.min(5000, Number(config.timeout_ms || 5000)));
12
+ }
13
+ export async function callLocalLlmGenerate(config, request) {
14
+ if (config.provider === 'ollama')
15
+ return callOllamaGenerate(config, request);
16
+ const response = await callOpenAiCompatibleLocalChat({
17
+ endpoint: config.base_url,
18
+ model: request.model,
19
+ messages: request.messages || [{ role: 'user', content: request.prompt }],
20
+ temperature: Number((request.options || {}).temperature ?? config.temperature ?? 0)
21
+ }, Number(config.timeout_ms || 20_000));
22
+ if (!response.ok)
23
+ return { ok: false, status: response.status, error: `http_${response.status}:${String(response.error || '').slice(0, 500)}` };
24
+ const text = extractOpenAiCompatibleText(response.data);
25
+ return {
26
+ ok: true,
27
+ data: {
28
+ provider: config.provider,
29
+ model: request.model,
30
+ response: text,
31
+ raw: response.data
32
+ },
33
+ text
34
+ };
35
+ }
36
+ export function localLlmTokensPerSecond(data, fallbackText = '', latencyMs = 0) {
37
+ return ollamaTokensPerSecond(data, fallbackText, latencyMs);
38
+ }
39
+ export async function detectInstalledLocalModelCandidate(input = {}) {
40
+ const timeoutMs = input.timeoutMs || 3000;
41
+ const endpoints = [
42
+ { provider: 'mlx-lm', base_url: trimTrailingSlash(input.mlxBaseUrl || process.env.SKS_MLX_LM_BASE_URL || process.env.SKS_LOCAL_LLM_BASE_URL || 'http://127.0.0.1:8080'), source: 'mlx_lm_server_v1_models' },
43
+ { provider: 'openai-compatible', base_url: trimTrailingSlash(input.openAiCompatibleBaseUrl || process.env.SKS_OPENAI_COMPATIBLE_BASE_URL || process.env.SKS_LOCAL_OPENAI_COMPATIBLE_BASE_URL || process.env.LM_STUDIO_BASE_URL || 'http://127.0.0.1:1234'), source: 'openai_compatible_v1_models' },
44
+ { provider: 'ollama', base_url: trimTrailingSlash(input.ollamaBaseUrl || process.env.SKS_OLLAMA_BASE_URL || 'http://127.0.0.1:11434'), source: 'ollama_api_tags' }
45
+ ];
46
+ const seen = new Set();
47
+ for (const endpoint of endpoints) {
48
+ if (!endpoint.base_url)
49
+ continue;
50
+ const key = `${endpoint.provider}:${endpoint.base_url}`;
51
+ if (seen.has(key))
52
+ continue;
53
+ seen.add(key);
54
+ const listed = await listLocalLlmModels({ ...endpoint, timeout_ms: timeoutMs }).catch(() => ({ ok: false, models: [] }));
55
+ if (!listed.ok || listed.models.length === 0)
56
+ continue;
57
+ const model = chooseModel(listed.models, input.preferredModel);
58
+ return { ...endpoint, endpoint: endpoint.base_url, model, models: listed.models };
59
+ }
60
+ return null;
61
+ }
62
+ async function probeOpenAiCompatibleModels(baseUrl, timeoutMs = 3000) {
63
+ const models = await listOpenAiCompatibleModels(baseUrl, timeoutMs);
64
+ return { ...models, data: models.ok ? { models: models.models } : null };
65
+ }
66
+ async function listOpenAiCompatibleModels(baseUrl, timeoutMs = 5000) {
67
+ try {
68
+ const response = await fetch(`${trimTrailingSlash(baseUrl)}/v1/models`, { signal: AbortSignal.timeout(timeoutMs) });
69
+ const text = await response.text();
70
+ const data = response.ok ? JSON.parse(text) : null;
71
+ const models = Array.isArray(data?.data) ? data.data.map((model) => String(model?.id || '')).filter(Boolean) : [];
72
+ return { ok: response.ok, status: response.status, models, data, error: response.ok ? null : text.slice(0, 500) };
73
+ }
74
+ catch (error) {
75
+ return { ok: false, status: 0, models: [], data: null, error: error instanceof Error ? error.message : String(error) };
76
+ }
77
+ }
78
+ function extractOpenAiCompatibleText(data) {
79
+ const choice = Array.isArray(data?.choices) ? data.choices[0] : null;
80
+ if (typeof choice?.message?.content === 'string')
81
+ return choice.message.content;
82
+ if (typeof choice?.text === 'string')
83
+ return choice.text;
84
+ if (typeof data?.response === 'string')
85
+ return data.response;
86
+ if (typeof data?.content === 'string')
87
+ return data.content;
88
+ return '';
89
+ }
90
+ function chooseModel(models, preferredModel = '') {
91
+ const preferred = String(preferredModel || '').trim();
92
+ if (preferred && models.includes(preferred))
93
+ return preferred;
94
+ const qwen = models.find((model) => /qwen/i.test(model));
95
+ return qwen || models[0] || '';
96
+ }
97
+ function trimTrailingSlash(value) {
98
+ return String(value || '').replace(/\/+$/, '');
99
+ }
100
+ //# sourceMappingURL=local-llm-client.js.map
@@ -2,13 +2,18 @@ import { resolveOllamaWorkerConfig } from '../agents/ollama-worker-config.js';
2
2
  export async function resolveLocalLlmConfig(input = {}) {
3
3
  const config = await resolveOllamaWorkerConfig(input);
4
4
  return {
5
- schema: 'sks.local-llm-config.v1',
5
+ schema: 'sks.local-llm-config.v2',
6
6
  ok: config.ok,
7
7
  enabled: config.enabled,
8
+ status: config.status,
8
9
  provider: config.provider,
9
10
  model: config.model,
11
+ endpoint: config.endpoint,
10
12
  base_url: config.base_url,
11
13
  worker_only: true,
14
+ requires_gpt_final: config.policy.requires_gpt_final,
15
+ capability: config.capability,
16
+ last_smoke: config.last_smoke,
12
17
  blockers: config.blockers
13
18
  };
14
19
  }
@@ -0,0 +1,21 @@
1
+ import { sha256 } from '../fsx.js';
2
+ const SECRET_PATTERNS = [/api[_-]?key/i, /token/i, /secret/i, /password/i, /authorization/i];
3
+ export function buildLocalLlmContextCacheKey(parts) {
4
+ const redacted = redactSecrets(parts);
5
+ return {
6
+ schema: 'sks.local-llm-context-cache-key.v1',
7
+ key: sha256(JSON.stringify(redacted)),
8
+ redacted
9
+ };
10
+ }
11
+ export function redactSecrets(value) {
12
+ if (Array.isArray(value))
13
+ return value.map(redactSecrets);
14
+ if (!value || typeof value !== 'object')
15
+ return value;
16
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [
17
+ key,
18
+ SECRET_PATTERNS.some((pattern) => pattern.test(key)) ? '[redacted]' : redactSecrets(child)
19
+ ]));
20
+ }
21
+ //# sourceMappingURL=local-llm-context-cache.js.map