sneakoscope 2.0.2 → 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 (126) 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 +86 -8
  8. package/dist/commands/doctor.js +14 -0
  9. package/dist/core/agents/agent-orchestrator.js +70 -4
  10. package/dist/core/agents/agent-patch-proof.js +5 -0
  11. package/dist/core/agents/agent-proof-evidence.js +61 -0
  12. package/dist/core/agents/agent-roster.js +35 -6
  13. package/dist/core/agents/agent-schema.js +1 -1
  14. package/dist/core/agents/native-worker-backend-router.js +31 -9
  15. package/dist/core/agents/ollama-worker-config.js +164 -15
  16. package/dist/core/codex/codex-0-137-compat.js +119 -0
  17. package/dist/core/codex-control/codex-control-proof.js +4 -1
  18. package/dist/core/codex-control/codex-fake-sdk-adapter.js +20 -0
  19. package/dist/core/codex-control/codex-output-schemas.js +5 -1
  20. package/dist/core/codex-control/codex-sdk-capability.js +1 -1
  21. package/dist/core/codex-control/codex-task-runner.js +329 -5
  22. package/dist/core/codex-control/gpt-final-arbiter.js +160 -0
  23. package/dist/core/codex-control/gpt-final-context-compressor.js +17 -0
  24. package/dist/core/codex-control/gpt-final-proof-pack.js +120 -0
  25. package/dist/core/codex-control/gpt-final-review-schema.js +71 -0
  26. package/dist/core/codex-control/python-codex-sdk-adapter.js +197 -0
  27. package/dist/core/codex-control/python-codex-sdk-event-translator.js +14 -0
  28. package/dist/core/commands/local-model-command.js +79 -18
  29. package/dist/core/commands/naruto-command.js +195 -12
  30. package/dist/core/commands/run-command.js +6 -2
  31. package/dist/core/doctor/doctor-readiness-matrix.js +34 -0
  32. package/dist/core/feature-fixtures.js +4 -0
  33. package/dist/core/fsx.js +1 -1
  34. package/dist/core/git-simple.js +143 -4
  35. package/dist/core/local-llm/local-collaboration-policy.js +93 -0
  36. package/dist/core/local-llm/local-llm-backpressure.js +20 -0
  37. package/dist/core/local-llm/local-llm-capability.js +29 -0
  38. package/dist/core/local-llm/local-llm-client.js +100 -0
  39. package/dist/core/local-llm/local-llm-config.js +20 -0
  40. package/dist/core/local-llm/local-llm-context-cache.js +21 -0
  41. package/dist/core/local-llm/local-llm-control-adapter.js +101 -0
  42. package/dist/core/local-llm/local-llm-json-repair.js +52 -0
  43. package/dist/core/local-llm/local-llm-metrics.js +42 -0
  44. package/dist/core/local-llm/local-llm-ollama-client.js +67 -0
  45. package/dist/core/local-llm/local-llm-openai-compatible-client.js +30 -0
  46. package/dist/core/local-llm/local-llm-prompt-cache.js +12 -0
  47. package/dist/core/local-llm/local-llm-scheduler.js +29 -0
  48. package/dist/core/local-llm/local-llm-schema-enforcer.js +15 -0
  49. package/dist/core/local-llm/local-llm-smoke.js +83 -0
  50. package/dist/core/local-llm/local-llm-warmup.js +20 -0
  51. package/dist/core/local-llm/local-worker-eligibility.js +27 -0
  52. package/dist/core/naruto/hardware-capacity-probe.js +36 -0
  53. package/dist/core/naruto/naruto-active-pool.js +118 -0
  54. package/dist/core/naruto/naruto-backpressure.js +13 -0
  55. package/dist/core/naruto/naruto-concurrency-governor.js +65 -0
  56. package/dist/core/naruto/naruto-finalizer.js +18 -0
  57. package/dist/core/naruto/naruto-generation-scheduler.js +18 -0
  58. package/dist/core/naruto/naruto-gpt-final-pack.js +49 -0
  59. package/dist/core/naruto/naruto-parallel-patch-apply.js +95 -0
  60. package/dist/core/naruto/naruto-patch-transaction-batch.js +42 -0
  61. package/dist/core/naruto/naruto-role-policy.js +107 -0
  62. package/dist/core/naruto/naruto-verification-dag.js +42 -0
  63. package/dist/core/naruto/naruto-verification-pool.js +18 -0
  64. package/dist/core/naruto/naruto-work-graph.js +198 -0
  65. package/dist/core/naruto/naruto-work-item.js +40 -0
  66. package/dist/core/naruto/naruto-work-stealing.js +11 -0
  67. package/dist/core/naruto/resource-pressure-monitor.js +32 -0
  68. package/dist/core/pipeline/final-gpt-patch-stage.js +31 -0
  69. package/dist/core/pipeline/final-gpt-review-stage.js +5 -0
  70. package/dist/core/pipeline/finalize-pipeline-result.js +58 -0
  71. package/dist/core/pipeline/gpt-final-required.js +12 -0
  72. package/dist/core/prompt/prompt-placeholder-guard.js +30 -0
  73. package/dist/core/router/capability-card.js +13 -0
  74. package/dist/core/router/route-cache.js +3 -0
  75. package/dist/core/router/ultra-router.js +2 -1
  76. package/dist/core/routes.js +4 -4
  77. package/dist/core/safety/mutation-guard.js +2 -0
  78. package/dist/core/update-check.js +60 -25
  79. package/dist/core/version.js +1 -1
  80. package/dist/core/zellij/zellij-lane-runtime.js +2 -2
  81. package/dist/core/zellij/zellij-naruto-dashboard.js +36 -0
  82. package/dist/core/zellij/zellij-worker-pane-manager.js +4 -4
  83. package/dist/scripts/blackbox-command-import-smoke.js +10 -1
  84. package/dist/scripts/check-package-boundary.js +12 -3
  85. package/dist/scripts/codex-0-137-compat-check.js +27 -0
  86. package/dist/scripts/codex-environment-scoped-approvals-check.js +10 -0
  87. package/dist/scripts/codex-plugin-list-json-check.js +8 -0
  88. package/dist/scripts/codex-sdk-team-naruto-agent-pipeline-check.js +2 -1
  89. package/dist/scripts/codex-thread-runtime-choice-check.js +10 -0
  90. package/dist/scripts/gpt-final-arbiter-check.js +63 -0
  91. package/dist/scripts/gpt-final-arbiter-performance-check.js +36 -0
  92. package/dist/scripts/local-collab-all-pipelines-final-gpt-check.js +21 -0
  93. package/dist/scripts/local-collab-gpt-final-availability-check.js +58 -0
  94. package/dist/scripts/local-collab-no-local-only-final-check.js +27 -0
  95. package/dist/scripts/local-collab-policy-check.js +17 -0
  96. package/dist/scripts/local-llm-all-pipelines-check.js +11 -0
  97. package/dist/scripts/local-llm-cache-performance-check.js +10 -0
  98. package/dist/scripts/local-llm-capability-check.js +14 -0
  99. package/dist/scripts/local-llm-smoke-check.js +23 -0
  100. package/dist/scripts/local-llm-structured-output-check.js +11 -0
  101. package/dist/scripts/local-llm-throughput-check.js +10 -0
  102. package/dist/scripts/local-llm-tool-call-repair-check.js +10 -0
  103. package/dist/scripts/local-llm-warmup-check.js +11 -0
  104. package/dist/scripts/naruto-active-pool-check.js +27 -0
  105. package/dist/scripts/naruto-concurrency-governor-check.js +52 -0
  106. package/dist/scripts/naruto-gpt-final-pack-check.js +34 -0
  107. package/dist/scripts/naruto-parallel-patch-apply-check.js +41 -0
  108. package/dist/scripts/naruto-real-local-gpt-final-smoke.js +16 -0
  109. package/dist/scripts/naruto-role-distribution-check.js +23 -0
  110. package/dist/scripts/naruto-shadow-clone-swarm-check.js +6 -0
  111. package/dist/scripts/naruto-verification-pool-check.js +36 -0
  112. package/dist/scripts/naruto-work-graph-check.js +24 -0
  113. package/dist/scripts/naruto-zellij-massive-ui-check.js +23 -0
  114. package/dist/scripts/prompt-placeholder-guard-check.js +33 -0
  115. package/dist/scripts/python-codex-sdk-all-pipelines-check.js +47 -0
  116. package/dist/scripts/python-codex-sdk-capability-check.js +75 -0
  117. package/dist/scripts/python-codex-sdk-sandbox-policy-check.js +10 -0
  118. package/dist/scripts/python-codex-sdk-stream-bridge-check.js +12 -0
  119. package/dist/scripts/release-parallel-check.js +1 -1
  120. package/dist/scripts/release-real-check.js +5 -0
  121. package/dist/scripts/zellij-worker-pane-manager-check.js +1 -1
  122. package/package.json +38 -4
  123. package/schemas/local-llm/local-collaboration-policy.schema.json +57 -0
  124. package/schemas/local-llm/local-model-config.schema.json +74 -0
  125. package/schemas/naruto/naruto-concurrency-governor.schema.json +21 -0
  126. package/schemas/naruto/naruto-work-graph.schema.json +22 -0
@@ -0,0 +1,120 @@
1
+ import { nowIso, sha256 } from '../fsx.js';
2
+ export const GPT_FINAL_PROOF_PACK_SCHEMA = 'sks.gpt-final-proof-pack.v1';
3
+ export function buildGptFinalProofPack(input, opts = {}) {
4
+ const maxCriticalLogChars = Math.max(1000, Number(opts.maxCriticalLogChars || 6000));
5
+ const localOutputs = Array.isArray(input.local_outputs) ? input.local_outputs : [];
6
+ const candidatePatchEnvelopes = Array.isArray(input.candidate_patch_envelopes) ? input.candidate_patch_envelopes : [];
7
+ const verificationResults = Array.isArray(input.verification_results) ? input.verification_results : [];
8
+ const pack = {
9
+ schema: GPT_FINAL_PROOF_PACK_SCHEMA,
10
+ generated_at: nowIso(),
11
+ route: String(input.route || ''),
12
+ mission_id: String(input.mission_id || ''),
13
+ local_mode: String(input.local_mode || ''),
14
+ worker_count: localOutputs.length,
15
+ changed_files: sortedUnique([
16
+ ...candidatePatchEnvelopes.flatMap((envelope) => envelopeChangedFiles(envelope)),
17
+ ...localOutputs.flatMap((output) => Array.isArray(output?.changed_files) ? output.changed_files.map(String) : [])
18
+ ]),
19
+ local_output_summaries: localOutputs.map((output, index) => ({
20
+ worker_id: String(output?.worker_id || output?.agent_id || output?.id || `worker-${index + 1}`),
21
+ backend: String(output?.backend || 'local-llm'),
22
+ status: String(output?.status || 'unknown'),
23
+ summary: trim(String(output?.summary || ''), 600),
24
+ proof: trim(String(output?.proof || output?.verification?.status || ''), 600),
25
+ blocker_count: Array.isArray(output?.blockers) ? output.blockers.length : 0,
26
+ patch_envelope_count: Array.isArray(output?.patch_envelopes) ? output.patch_envelopes.length : output?.patch_envelope ? 1 : 0
27
+ })),
28
+ candidate_diff_sha256: sha256(String(input.candidate_diff || '')),
29
+ candidate_diff_tail: tail(String(input.candidate_diff || ''), maxCriticalLogChars),
30
+ candidate_patch_envelopes: candidatePatchEnvelopes.map((envelope, index) => summarizeEnvelope(envelope, index)),
31
+ verification_failures: verificationResults.filter((result) => result?.ok === false || result?.status === 'failed').map((result) => ({
32
+ id: String(result?.id || result?.patch_entry_id || result?.name || 'verification'),
33
+ status: String(result?.status || (result?.ok === false ? 'failed' : 'unknown')),
34
+ blockers: stringArray(result?.blockers || result?.violations)
35
+ })),
36
+ side_effect_ledger_summary: summarizeObject(input.side_effect_report),
37
+ mutation_ledger_summary: summarizeObject(input.mutation_ledger),
38
+ rollback_plan_summary: summarizeObject(input.rollback_plan),
39
+ conflict_map: buildConflictMap(candidatePatchEnvelopes),
40
+ critical_logs_tail: tail(JSON.stringify({
41
+ local_outputs: localOutputs.map((output) => ({
42
+ id: output?.worker_id || output?.agent_id || output?.id,
43
+ blockers: output?.blockers || [],
44
+ unverified: output?.unverified || []
45
+ })),
46
+ verification_results: verificationResults
47
+ }), maxCriticalLogChars),
48
+ token_budget_estimate: 0
49
+ };
50
+ return {
51
+ ...pack,
52
+ token_budget_estimate: estimateTokens(pack)
53
+ };
54
+ }
55
+ export function buildGptFinalLatencyBudgetReport(input) {
56
+ const workerCount = Math.max(0, Number(input.workerCount || 0));
57
+ const tokenBudgetEstimate = Math.max(0, Number(input.tokenBudgetEstimate || 0));
58
+ const capAdjustment = tokenBudgetEstimate > 8000 || workerCount > 20 ? 'reduce_local_parallelism_or_pack_size' : 'within_budget';
59
+ return {
60
+ schema: 'sks.gpt-final-latency-budget.v1',
61
+ generated_at: nowIso(),
62
+ worker_count: workerCount,
63
+ token_budget_estimate: tokenBudgetEstimate,
64
+ latency_ms: input.latencyMs ?? null,
65
+ cap_adjustment: capAdjustment,
66
+ ok: capAdjustment === 'within_budget'
67
+ };
68
+ }
69
+ function summarizeEnvelope(envelope, index) {
70
+ return {
71
+ id: String(envelope?.id || envelope?.patch_id || `patch-${index + 1}`),
72
+ agent_id: String(envelope?.agent_id || 'unknown'),
73
+ source: String(envelope?.source || 'local-llm'),
74
+ operations: Array.isArray(envelope?.operations) ? envelope.operations.length : 0,
75
+ write_paths: envelopeChangedFiles(envelope),
76
+ lease_id: envelope?.lease_id || envelope?.lease_proof?.lease_id || null,
77
+ rollback_ready: Boolean(envelope?.rollback_hint || envelope?.lease_proof?.rollback_node_id)
78
+ };
79
+ }
80
+ function envelopeChangedFiles(envelope) {
81
+ return Array.isArray(envelope?.operations) ? envelope.operations.map((operation) => String(operation?.path || '')).filter(Boolean) : [];
82
+ }
83
+ function buildConflictMap(envelopes) {
84
+ const owners = new Map();
85
+ for (const envelope of envelopes) {
86
+ const agent = String(envelope?.agent_id || 'unknown');
87
+ for (const file of envelopeChangedFiles(envelope))
88
+ owners.set(file, [...(owners.get(file) || []), agent]);
89
+ }
90
+ return [...owners.entries()]
91
+ .filter(([, agents]) => new Set(agents).size > 1)
92
+ .map(([file, agents]) => ({ file, agents: sortedUnique(agents) }));
93
+ }
94
+ function summarizeObject(value) {
95
+ if (!value || typeof value !== 'object')
96
+ return null;
97
+ return {
98
+ schema: value.schema || null,
99
+ ok: typeof value.ok === 'boolean' ? value.ok : null,
100
+ status: value.status || null,
101
+ blockers: stringArray(value.blockers).slice(0, 20),
102
+ keys: Object.keys(value).slice(0, 30)
103
+ };
104
+ }
105
+ function estimateTokens(value) {
106
+ return Math.ceil(JSON.stringify(value).length / 4);
107
+ }
108
+ function tail(value, max) {
109
+ return value.length <= max ? value : value.slice(value.length - max);
110
+ }
111
+ function trim(value, max) {
112
+ return value.length <= max ? value : value.slice(0, max);
113
+ }
114
+ function sortedUnique(values) {
115
+ return [...new Set(values.map(String).filter(Boolean))].sort();
116
+ }
117
+ function stringArray(value) {
118
+ return Array.isArray(value) ? value.map((entry) => String(entry || '').trim()).filter(Boolean) : [];
119
+ }
120
+ //# sourceMappingURL=gpt-final-proof-pack.js.map
@@ -0,0 +1,71 @@
1
+ export const GPT_FINAL_ARBITER_RESULT_SCHEMA_ID = 'sks.gpt-final-arbiter-result.v1';
2
+ export const GPT_FINAL_ARBITER_INPUT_SCHEMA = 'sks.gpt-final-arbiter-input.v1';
3
+ export const gptFinalArbiterResultSchema = {
4
+ type: 'object',
5
+ required: [
6
+ 'schema',
7
+ 'status',
8
+ 'summary',
9
+ 'gpt_review_findings',
10
+ 'accepted_patch_envelopes',
11
+ 'modified_patch_envelopes',
12
+ 'rejected_patch_envelopes',
13
+ 'required_followup_work',
14
+ 'verification_plan',
15
+ 'rollback_notes',
16
+ 'blockers',
17
+ 'confidence'
18
+ ],
19
+ properties: {
20
+ schema: { const: GPT_FINAL_ARBITER_RESULT_SCHEMA_ID },
21
+ status: { enum: ['approved', 'modified', 'rejected', 'needs_more_work'] },
22
+ summary: { type: 'string' },
23
+ gpt_review_findings: { type: 'array', items: { type: 'object' } },
24
+ accepted_patch_envelopes: { type: 'array', items: { type: 'object' } },
25
+ modified_patch_envelopes: { type: 'array', items: { type: 'object' } },
26
+ rejected_patch_envelopes: { type: 'array', items: { type: 'object' } },
27
+ required_followup_work: { type: 'array', items: { type: 'object' } },
28
+ verification_plan: { type: 'array', items: { type: 'string' } },
29
+ rollback_notes: { type: 'array', items: { type: 'string' } },
30
+ blockers: { type: 'array', items: { type: 'string' } },
31
+ confidence: { enum: ['low', 'medium', 'high'] }
32
+ },
33
+ additionalProperties: true
34
+ };
35
+ export function normalizeGptFinalArbiterResult(value) {
36
+ const status = normalizeStatus(value?.status);
37
+ return {
38
+ schema: GPT_FINAL_ARBITER_RESULT_SCHEMA_ID,
39
+ status,
40
+ summary: String(value?.summary || defaultSummary(status)),
41
+ gpt_review_findings: array(value?.gpt_review_findings),
42
+ accepted_patch_envelopes: array(value?.accepted_patch_envelopes),
43
+ modified_patch_envelopes: array(value?.modified_patch_envelopes),
44
+ rejected_patch_envelopes: array(value?.rejected_patch_envelopes),
45
+ required_followup_work: array(value?.required_followup_work),
46
+ verification_plan: stringArray(value?.verification_plan),
47
+ rollback_notes: stringArray(value?.rollback_notes),
48
+ blockers: stringArray(value?.blockers),
49
+ confidence: normalizeConfidence(value?.confidence)
50
+ };
51
+ }
52
+ function normalizeStatus(value) {
53
+ return value === 'approved' || value === 'modified' || value === 'rejected' || value === 'needs_more_work'
54
+ ? value
55
+ : 'needs_more_work';
56
+ }
57
+ function normalizeConfidence(value) {
58
+ return value === 'low' || value === 'medium' || value === 'high' ? value : 'medium';
59
+ }
60
+ function array(value) {
61
+ return Array.isArray(value) ? value : [];
62
+ }
63
+ function stringArray(value) {
64
+ return Array.isArray(value) ? value.map((entry) => String(entry || '').trim()).filter(Boolean) : [];
65
+ }
66
+ function defaultSummary(status) {
67
+ return status === 'approved' || status === 'modified'
68
+ ? 'GPT final arbiter accepted the candidate result.'
69
+ : 'GPT final arbiter did not approve the candidate result.';
70
+ }
71
+ //# sourceMappingURL=gpt-final-review-schema.js.map
@@ -0,0 +1,197 @@
1
+ import { spawn } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { packageRoot, runProcess, which } from '../fsx.js';
4
+ import { translatePythonCodexSdkEvents } from './python-codex-sdk-event-translator.js';
5
+ export async function detectPythonCodexSdkCapability() {
6
+ const python = await resolvePythonCodexSdkPython();
7
+ if (!python.path)
8
+ return capability(false, null, '', ['python_missing'], null, 'Install Python >= 3.10 and install the Codex Python SDK.');
9
+ const pyOk = parsePythonVersion(python.versionText) >= 3.10;
10
+ const probes = [];
11
+ let detected = null;
12
+ if (pyOk) {
13
+ for (const candidate of PYTHON_CODEX_SDK_CANDIDATES) {
14
+ const importProbe = await runProcess(python.path, ['-c', `import ${candidate.importName}; print("ok")`], { timeoutMs: 5000, maxOutputBytes: 4096 })
15
+ .catch((err) => ({ code: 1, stdout: '', stderr: err.message || String(err) }));
16
+ probes.push({
17
+ package_name: candidate.packageName,
18
+ import_name: candidate.importName,
19
+ ok: importProbe.code === 0,
20
+ stderr: String(importProbe.stderr || '').slice(-500)
21
+ });
22
+ if (importProbe.code === 0) {
23
+ detected = candidate;
24
+ break;
25
+ }
26
+ }
27
+ }
28
+ const blockers = [
29
+ ...(pyOk ? [] : ['python_version_below_3_10']),
30
+ ...(detected ? [] : ['python_codex_sdk_unavailable'])
31
+ ];
32
+ return capability(blockers.length === 0, python.path, python.versionText, blockers, detected, blockers.length ? setupAction(python.path) : null, probes);
33
+ }
34
+ export async function runPythonCodexSdkTask(input, opts = {}) {
35
+ const cap = await detectPythonCodexSdkCapability();
36
+ if (!cap.ok && process.env.SKS_PYTHON_CODEX_SDK_FAKE !== '1') {
37
+ return { ok: false, events: [], translatedEvents: [], finalResponse: '', blockers: cap.blockers, capability: cap };
38
+ }
39
+ const python = opts.pythonBin || cap.python_bin || await which('python3') || 'python3';
40
+ const request = {
41
+ schema: 'sks.python-codex-sdk-request.v1',
42
+ route: input.route,
43
+ thread_policy: input.requestedScopeContract?.resume_thread_id ? 'resume' : 'new',
44
+ sandbox: mapSandbox(input.sandboxPolicy),
45
+ cwd: input.cwd,
46
+ model: typeof opts.config?.model === 'string' ? opts.config.model : '',
47
+ model_reasoning_effort: typeof opts.config?.model_reasoning_effort === 'string' ? opts.config.model_reasoning_effort : 'minimal',
48
+ prompt: input.prompt,
49
+ output_schema: input.outputSchema || {}
50
+ };
51
+ const events = await runPythonRunner(python, request, opts.env);
52
+ const translatedEvents = translatePythonCodexSdkEvents(events);
53
+ const last = [...events].reverse().find((event) => event?.event === 'turn_completed');
54
+ const errors = events.filter((event) => event?.event === 'error').map((event) => String(event.message || 'python_codex_sdk_error'));
55
+ return {
56
+ ok: errors.length === 0 && Boolean(last),
57
+ events,
58
+ translatedEvents,
59
+ finalResponse: String(last?.final_response || ''),
60
+ threadId: String(events.find((event) => event?.thread_id)?.thread_id || ''),
61
+ turnId: String(last?.turn_id || ''),
62
+ blockers: errors,
63
+ capability: cap
64
+ };
65
+ }
66
+ function runPythonRunner(python, request, envOverride) {
67
+ const runner = path.join(packageRoot(), 'pytools', 'codex_sdk_runner.py');
68
+ return new Promise((resolve, reject) => {
69
+ const child = spawn(python, [runner], {
70
+ stdio: ['pipe', 'pipe', 'pipe'],
71
+ env: envOverride ? { ...process.env, ...envOverride } : process.env,
72
+ detached: process.platform !== 'win32'
73
+ });
74
+ const events = [];
75
+ let stderr = '';
76
+ let timedOut = false;
77
+ const timeoutMs = Number(process.env.SKS_PYTHON_CODEX_SDK_TIMEOUT_MS || 120000);
78
+ const timer = setTimeout(() => {
79
+ timedOut = true;
80
+ events.push({ event: 'error', retryable: true, message: `python_codex_sdk_timeout:${timeoutMs}` });
81
+ terminatePythonRunner(child);
82
+ }, timeoutMs);
83
+ timer.unref?.();
84
+ child.stdout.setEncoding('utf8');
85
+ child.stderr.setEncoding('utf8');
86
+ child.stdout.on('data', (chunk) => {
87
+ for (const line of String(chunk).split(/\n/).filter(Boolean)) {
88
+ try {
89
+ events.push(JSON.parse(line));
90
+ }
91
+ catch {
92
+ events.push({ event: 'error', retryable: false, message: `invalid_python_jsonl:${line.slice(0, 200)}` });
93
+ }
94
+ }
95
+ });
96
+ child.stderr.on('data', (chunk) => { stderr += String(chunk); });
97
+ child.on('error', reject);
98
+ child.on('close', () => {
99
+ clearTimeout(timer);
100
+ if (stderr.trim())
101
+ events.push({ event: 'stderr', message: stderr.slice(-1000) });
102
+ if (timedOut && !events.some((event) => String(event?.message || '').startsWith('python_codex_sdk_timeout:'))) {
103
+ events.push({ event: 'error', retryable: true, message: `python_codex_sdk_timeout:${timeoutMs}` });
104
+ }
105
+ resolve(events);
106
+ });
107
+ child.stdin.end(JSON.stringify(request));
108
+ });
109
+ }
110
+ function terminatePythonRunner(child) {
111
+ if (!child.pid)
112
+ return;
113
+ try {
114
+ if (process.platform !== 'win32')
115
+ process.kill(-child.pid, 'SIGTERM');
116
+ else
117
+ child.kill('SIGTERM');
118
+ }
119
+ catch {
120
+ try {
121
+ child.kill('SIGTERM');
122
+ }
123
+ catch { }
124
+ }
125
+ setTimeout(() => {
126
+ try {
127
+ if (process.platform !== 'win32')
128
+ process.kill(-child.pid, 'SIGKILL');
129
+ else
130
+ child.kill('SIGKILL');
131
+ }
132
+ catch { }
133
+ }, 5000).unref?.();
134
+ }
135
+ function mapSandbox(value) {
136
+ if (value === 'workspace-write')
137
+ return 'workspace_write';
138
+ if (value === 'full-access')
139
+ return 'full_access';
140
+ return 'read_only';
141
+ }
142
+ function parsePythonVersion(text) {
143
+ const match = text.match(/Python\s+(\d+)\.(\d+)/i);
144
+ return match ? Number(`${match[1]}.${match[2]}`) : 0;
145
+ }
146
+ async function resolvePythonCodexSdkPython() {
147
+ const requested = [
148
+ process.env.SKS_PYTHON_CODEX_SDK_PYTHON,
149
+ process.env.PYTHON
150
+ ].filter((value) => Boolean(value && value.trim()));
151
+ const candidates = [...requested, 'python3.12', 'python3.11', 'python3.10', 'python3', 'python'];
152
+ const seen = new Set();
153
+ const probes = [];
154
+ for (const candidate of candidates) {
155
+ const resolved = candidate.includes('/') ? candidate : await which(candidate);
156
+ if (!resolved || seen.has(resolved))
157
+ continue;
158
+ seen.add(resolved);
159
+ const version = await runProcess(resolved, ['--version'], { timeoutMs: 5000, maxOutputBytes: 4096 })
160
+ .catch((err) => ({ code: 1, stdout: '', stderr: err.message || String(err) }));
161
+ const versionText = `${version.stdout || ''}${version.stderr || ''}`.trim();
162
+ probes.push({ path: resolved, versionText, score: parsePythonVersion(versionText) });
163
+ }
164
+ const supported = probes.find((probe) => probe.score >= 3.10);
165
+ return supported || probes[0] || { path: null, versionText: '' };
166
+ }
167
+ const PYTHON_CODEX_SDK_CANDIDATES = [
168
+ { packageName: 'codex-app-server', importName: 'codex_app_server', source: 'developers.openai.com/codex/sdk' },
169
+ { packageName: 'openai-codex', importName: 'openai_codex', source: 'sks-2.0.5-directive' },
170
+ { packageName: 'openai-codex-sdk', importName: 'openai_codex_sdk', source: 'pypi-wheel' }
171
+ ];
172
+ function setupAction(pythonBin) {
173
+ return [
174
+ `Install the current Codex Python SDK in \`${pythonBin}\`.`,
175
+ 'Preferred official source install: from the Codex repository root run `cd sdk/python && python -m pip install -e .`.',
176
+ `If using the published wrapper package, run \`${pythonBin} -m pip install openai-codex-sdk\`.`,
177
+ `If your environment provides the directive package, run \`${pythonBin} -m pip install openai-codex\`.`
178
+ ].join(' ');
179
+ }
180
+ function capability(ok, pythonBin, versionText, blockers, detected, setupActionValue, probes = []) {
181
+ const selected = detected || PYTHON_CODEX_SDK_CANDIDATES[0] || { packageName: 'codex-app-server', importName: 'codex_app_server', source: 'developers.openai.com/codex/sdk' };
182
+ return {
183
+ schema: 'sks.python-codex-sdk-capability.v1',
184
+ ok,
185
+ python_bin: pythonBin,
186
+ python_version: versionText,
187
+ package_name: selected.packageName,
188
+ import_name: selected.importName,
189
+ source: selected.source,
190
+ supported_packages: PYTHON_CODEX_SDK_CANDIDATES.map((candidate) => candidate.packageName),
191
+ supported_imports: PYTHON_CODEX_SDK_CANDIDATES.map((candidate) => candidate.importName),
192
+ import_probes: probes,
193
+ setup_action: setupActionValue,
194
+ blockers
195
+ };
196
+ }
197
+ //# sourceMappingURL=python-codex-sdk-adapter.js.map
@@ -0,0 +1,14 @@
1
+ export function translatePythonCodexSdkEvents(events = []) {
2
+ return events.map((event, index) => ({
3
+ schema: 'sks.python-codex-sdk-event.v1',
4
+ index,
5
+ event_type: String(event?.event || 'unknown'),
6
+ thread_id: event?.thread_id ? String(event.thread_id) : null,
7
+ turn_id: event?.turn_id ? String(event.turn_id) : null,
8
+ status: event?.status ? String(event.status) : null,
9
+ retryable: event?.retryable === true,
10
+ message: event?.message ? String(event.message) : null,
11
+ final_response: event?.final_response ? String(event.final_response) : null
12
+ }));
13
+ }
14
+ //# sourceMappingURL=python-codex-sdk-event-translator.js.map
@@ -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')
@@ -26,18 +28,64 @@ function normalizeLocalModelAction(value) {
26
28
  return text;
27
29
  }
28
30
  async function enable(args) {
29
- const model = readOption(args, '--model', args[0] || '');
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) {
@@ -102,4 +148,19 @@ function readBoolFlag(args, trueName, falseName) {
102
148
  return false;
103
149
  return null;
104
150
  }
151
+ function firstPositional(args = []) {
152
+ for (let i = 0; i < args.length; i += 1) {
153
+ const arg = String(args[i] || '');
154
+ if (arg === '--model' || arg === '--base-url' || arg === '--provider') {
155
+ if (args[i + 1] && !String(args[i + 1]).startsWith('--'))
156
+ i += 1;
157
+ continue;
158
+ }
159
+ if (arg.startsWith('--model=') || arg.startsWith('--base-url=') || arg.startsWith('--provider='))
160
+ continue;
161
+ if (!arg.startsWith('--'))
162
+ return arg;
163
+ }
164
+ return '';
165
+ }
105
166
  //# sourceMappingURL=local-model-command.js.map