sneakoscope 2.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +26 -5
  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 +15 -8
  8. package/dist/cli/command-registry.js +2 -0
  9. package/dist/commands/doctor.js +29 -3
  10. package/dist/core/agents/agent-command-surface.js +13 -3
  11. package/dist/core/agents/agent-orchestrator.js +22 -0
  12. package/dist/core/agents/agent-output-validator.js +2 -1
  13. package/dist/core/agents/agent-patch-schema.js +2 -1
  14. package/dist/core/agents/agent-roster.js +1 -1
  15. package/dist/core/agents/agent-runner-ollama.js +411 -0
  16. package/dist/core/agents/agent-schema.js +1 -1
  17. package/dist/core/agents/intelligent-work-graph.js +45 -3
  18. package/dist/core/agents/native-cli-session-swarm.js +8 -1
  19. package/dist/core/agents/native-cli-worker.js +1 -1
  20. package/dist/core/agents/native-worker-backend-router.js +44 -2
  21. package/dist/core/agents/ollama-worker-config.js +118 -0
  22. package/dist/core/auto-review.js +39 -6
  23. package/dist/core/codex-app/codex-app-fast-ui-repair.js +42 -3
  24. package/dist/core/commands/basic-cli.js +36 -1
  25. package/dist/core/commands/local-model-command.js +105 -0
  26. package/dist/core/commands/mad-sks-command.js +58 -9
  27. package/dist/core/commands/run-command.js +29 -1
  28. package/dist/core/commands/team-command.js +31 -2
  29. package/dist/core/doctor/doctor-readiness-matrix.js +4 -0
  30. package/dist/core/feature-fixtures.js +1 -0
  31. package/dist/core/fsx.js +1 -1
  32. package/dist/core/hooks-runtime.js +1 -1
  33. package/dist/core/init.js +2 -0
  34. package/dist/core/provider/provider-context.js +72 -9
  35. package/dist/core/retention.js +11 -0
  36. package/dist/core/routes.js +21 -1
  37. package/dist/core/team-live.js +7 -1
  38. package/dist/core/update-check.js +156 -1
  39. package/dist/core/verification/verification-worker-pool.js +12 -0
  40. package/dist/core/version.js +1 -1
  41. package/dist/core/zellij/zellij-worker-pane-manager.js +19 -2
  42. package/dist/scripts/agent-ast-aware-work-graph-check.js +1 -1
  43. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +12 -2
  44. package/dist/scripts/mad-sks-app-ui-no-mutation-check.js +92 -0
  45. package/dist/scripts/mad-sks-zellij-default-pane-worker-check.js +37 -0
  46. package/dist/scripts/mad-sks-zellij-launch-check.js +2 -1
  47. package/dist/scripts/provider-context-config-toml-check.js +63 -0
  48. package/dist/scripts/release-gate-existence-audit.js +4 -0
  49. package/dist/scripts/runtime-no-mjs-scripts-check.js +3 -2
  50. package/dist/scripts/zellij-worker-pane-manager-check.js +3 -0
  51. package/dist/scripts/zellij-worker-pane-manager-single-owner-check.js +39 -0
  52. package/package.json +7 -3
@@ -0,0 +1,411 @@
1
+ import path from 'node:path';
2
+ import { nowIso, sha256, writeJsonAtomic } from '../fsx.js';
3
+ import { loadTriWikiRuntimeContext, triWikiContextBlock, triWikiProofRecord } from '../triwiki-runtime.js';
4
+ import { validateAgentWorkerResult } from './agent-worker-pipeline.js';
5
+ import { normalizeAgentPatchEnvelope } from './agent-patch-schema.js';
6
+ import { resolveOllamaWorkerConfig } from './ollama-worker-config.js';
7
+ export const OLLAMA_WORKER_POLICY_SCHEMA = 'sks.ollama-worker-policy.v1';
8
+ export const OLLAMA_WORKER_REQUEST_SCHEMA = 'sks.ollama-worker-request.v1';
9
+ export const OLLAMA_WORKER_RESPONSE_SCHEMA = 'sks.ollama-worker-response.v1';
10
+ export async function runOllamaAgent(agent, slice, opts = {}) {
11
+ const root = path.resolve(opts.agentRoot || opts.cwd || process.cwd());
12
+ const workerDirRel = String(opts.workerDirRel || agent.session_artifact_dir || path.join('sessions', String(agent.id || 'ollama-worker'), 'worker'));
13
+ const workerDir = path.join(root, workerDirRel);
14
+ const triwikiContext = await loadTriWikiRuntimeContext(root);
15
+ const config = await resolveOllamaWorkerConfig({
16
+ backend: 'ollama',
17
+ ollamaEnabled: opts.ollamaEnabled === true || opts.ollama_enabled === true,
18
+ model: opts.ollamaModel || opts.ollama_model || null,
19
+ baseUrl: opts.ollamaBaseUrl || opts.ollama_base_url || null,
20
+ keepAlive: opts.ollamaKeepAlive || opts.ollama_keep_alive || null,
21
+ timeoutMs: Number(opts.ollamaTimeoutMs || opts.ollama_timeout_ms || 0) || null,
22
+ temperature: Number(opts.ollamaTemperature || opts.ollama_temperature || 0),
23
+ think: typeof opts.ollamaThink === 'boolean' ? opts.ollamaThink
24
+ : typeof opts.ollama_think === 'boolean' ? opts.ollama_think
25
+ : null
26
+ });
27
+ const policy = classifyOllamaWorkerSlice(slice, { route: opts.route, agent });
28
+ await writeJsonAtomic(path.join(workerDir, 'ollama-worker-config.json'), config);
29
+ await writeJsonAtomic(path.join(workerDir, 'ollama-worker-policy.json'), policy);
30
+ await writeJsonAtomic(path.join(workerDir, 'ollama-triwiki-context.json'), buildOllamaTriWikiArtifact(triwikiContext));
31
+ if (!config.ok || !policy.ok) {
32
+ return validateAgentWorkerResult(blockedResult(agent, slice, opts, [...config.blockers, ...policy.blockers], [
33
+ path.join(workerDirRel, 'ollama-worker-config.json'),
34
+ path.join(workerDirRel, 'ollama-worker-policy.json'),
35
+ path.join(workerDirRel, 'ollama-triwiki-context.json')
36
+ ]));
37
+ }
38
+ const requestId = `ollama:${sha256(`${nowIso()}:${agent.session_id || agent.id}:${slice?.id || ''}`).slice(0, 16)}`;
39
+ const request = buildOllamaGenerateRequest(agent, slice, opts, config, requestId, triwikiContext);
40
+ await writeJsonAtomic(path.join(workerDir, 'ollama-request.json'), {
41
+ schema: OLLAMA_WORKER_REQUEST_SCHEMA,
42
+ generated_at: nowIso(),
43
+ request_id: requestId,
44
+ endpoint: `${config.base_url}/api/generate`,
45
+ model: config.model,
46
+ keep_alive: config.keep_alive,
47
+ stream: false,
48
+ think: config.think,
49
+ policy: 'worker_only_no_strategy_planning_design',
50
+ triwiki_context: triWikiProofRecord(triwikiContext),
51
+ stack_current_docs_required: true,
52
+ prompt_sha256: sha256(request.prompt)
53
+ });
54
+ const response = await callOllamaGenerate(config, request)
55
+ .catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error) }));
56
+ const workerText = response.ok === true ? extractOllamaWorkerText(response.data) : { text: '', source: 'empty' };
57
+ await writeJsonAtomic(path.join(workerDir, 'ollama-response.json'), {
58
+ schema: OLLAMA_WORKER_RESPONSE_SCHEMA,
59
+ generated_at: nowIso(),
60
+ request_id: requestId,
61
+ ok: response.ok === true,
62
+ model: config.model,
63
+ response_sha256: response.ok === true ? sha256(workerText.text) : null,
64
+ response_source: response.ok === true ? workerText.source : null,
65
+ data: response.ok === true ? safeResponseData(response.data) : null,
66
+ error: response.ok === true ? null : response.error
67
+ });
68
+ if (response.ok !== true) {
69
+ return validateAgentWorkerResult(blockedResult(agent, slice, opts, ['ollama_generate_failed', String(response.error || 'unknown_error')], [
70
+ path.join(workerDirRel, 'ollama-triwiki-context.json'),
71
+ path.join(workerDirRel, 'ollama-request.json'),
72
+ path.join(workerDirRel, 'ollama-response.json')
73
+ ]));
74
+ }
75
+ const parsed = parseWorkerJson(workerText.text);
76
+ if (!parsed.ok) {
77
+ return validateAgentWorkerResult(blockedResult(agent, slice, opts, ['ollama_worker_json_parse_failed'], [
78
+ path.join(workerDirRel, 'ollama-triwiki-context.json'),
79
+ path.join(workerDirRel, 'ollama-request.json'),
80
+ path.join(workerDirRel, 'ollama-response.json')
81
+ ]));
82
+ }
83
+ const patchEnvelopes = normalizeOllamaPatchEnvelopes(parsed.value, agent, slice, opts, requestId);
84
+ const changedFiles = [...new Set(patchEnvelopes.flatMap((envelope) => envelope.operations.map((operation) => operation.path)))];
85
+ const writePaths = collectWritePaths(slice, opts);
86
+ return validateAgentWorkerResult({
87
+ mission_id: String(opts.missionId || opts.mission_id || ''),
88
+ agent_id: String(agent.id || 'ollama-worker'),
89
+ session_id: String(agent.session_id || ''),
90
+ persona_id: String(agent.persona_id || agent.id || 'ollama-worker'),
91
+ task_slice_id: String(slice?.id || ''),
92
+ status: writePaths.length > 0 && patchEnvelopes.length === 0 ? 'blocked' : 'done',
93
+ backend: 'ollama',
94
+ summary: String(parsed.value.summary || parsed.value.result || 'Ollama local worker completed.'),
95
+ findings: [
96
+ 'ollama local worker executed through /api/generate',
97
+ 'triwiki context consulted before local worker prompt',
98
+ ...stringArray(parsed.value.findings)
99
+ ],
100
+ proposed_changes: stringArray(parsed.value.proposed_changes || parsed.value.proposedChanges),
101
+ changed_files: changedFiles,
102
+ lease_compliance: { ok: true, violations: [] },
103
+ artifacts: [
104
+ path.join(workerDirRel, 'ollama-worker-config.json'),
105
+ path.join(workerDirRel, 'ollama-worker-policy.json'),
106
+ path.join(workerDirRel, 'ollama-triwiki-context.json'),
107
+ path.join(workerDirRel, 'ollama-request.json'),
108
+ path.join(workerDirRel, 'ollama-response.json')
109
+ ],
110
+ blockers: writePaths.length > 0 && patchEnvelopes.length === 0 ? ['ollama_no_patch_envelopes_for_write_task'] : [],
111
+ confidence: 'model_authored_local',
112
+ handoff_notes: 'Local Ollama worker produced worker JSON; parent SKS remains responsible for merge, apply, verification, and rollback.',
113
+ unverified: [
114
+ 'local_model_output_not_strategy_or_verification_authority',
115
+ ...(triwikiContext.present ? [] : ['triwiki_context_missing_parent_should_refresh_with_context7_or_official_docs_before_relying_on_local_worker'])
116
+ ],
117
+ writes: changedFiles,
118
+ ...(patchEnvelopes.length ? { patch_envelopes: patchEnvelopes } : {}),
119
+ model_authored_patch_envelopes: patchEnvelopes.length > 0,
120
+ fixture_patch_envelopes: false,
121
+ source_intelligence_refs: agent.source_intelligence_refs || opts.source_intelligence_refs || null,
122
+ goal_mode_ref: agent.goal_mode_ref || opts.goal_mode_ref || null,
123
+ verification: { status: 'passed', checks: ['ollama-api-generate', 'ollama-worker-policy', 'triwiki-runtime-context', 'agent-patch-envelope-schema'] },
124
+ recursion_guard: { ok: true, violations: [] }
125
+ });
126
+ }
127
+ export function classifyOllamaWorkerSlice(slice, input = {}) {
128
+ const writePaths = collectWritePaths(slice, {});
129
+ const text = [
130
+ input.route,
131
+ input.agent?.role,
132
+ input.agent?.persona_id,
133
+ slice?.role,
134
+ slice?.domain,
135
+ slice?.title,
136
+ slice?.description,
137
+ ...(Array.isArray(slice?.target_paths) ? slice.target_paths : [])
138
+ ].map((value) => String(value || '')).join('\n');
139
+ const bannedRole = /(?:^|\b)(architect|verifier|safety|integrator|schema|release|ux|db)(?:\b|$)/i.test(String(input.agent?.role || slice?.role || ''));
140
+ const collection = /\b(collect|gather|extract|inventory|list|scan|grep|tail|summarize|catalog)\b|수집|추출|목록|스캔|인벤토리/i.test(text);
141
+ const coding = /\b(code|implement|patch|write|edit|fix|mechanical|simple)\b|코드|작성|수정|구현|패치|단순/i.test(text);
142
+ const banned = /\b(strategy|strategize|planning|plan|architecture|architect|design|review|verify|verification|safety|risk|consensus|debate|orchestrate|policy|decide|decision|migration|database|schema)\b|전략|기획|설계|디자인|검증|리뷰|안전|위험|합의|토론|결정|마이그레이션|데이터베이스/i.test(text);
143
+ const allowed = !bannedRole && !banned && (writePaths.length > 0 || collection || coding);
144
+ const blockers = [
145
+ ...(allowed ? [] : ['ollama_worker_task_not_simple_code_or_collection']),
146
+ ...(bannedRole ? ['ollama_worker_role_blocked'] : []),
147
+ ...(banned ? ['ollama_worker_strategy_planning_design_blocked'] : [])
148
+ ];
149
+ return {
150
+ schema: OLLAMA_WORKER_POLICY_SCHEMA,
151
+ generated_at: nowIso(),
152
+ ok: blockers.length === 0,
153
+ worker_only: true,
154
+ no_strategy_planning_design: true,
155
+ allowed_work: ['simple_code_patch_envelopes', 'read_only_collection'],
156
+ write_path_count: writePaths.length,
157
+ collection_detected: collection,
158
+ coding_detected: coding,
159
+ blockers
160
+ };
161
+ }
162
+ function buildOllamaTriWikiArtifact(triwikiContext) {
163
+ return {
164
+ ...triwikiContext,
165
+ proof: triWikiProofRecord(triwikiContext),
166
+ stack_current_docs_policy: {
167
+ required: true,
168
+ memory_path: '.sneakoscope/memory/q2_facts/stack-current-docs.md',
169
+ refresh_command: 'sks wiki refresh',
170
+ validate_command: 'sks wiki validate .sneakoscope/wiki/context-pack.json',
171
+ current_docs_source: 'Context7 or official vendor docs',
172
+ parent_action_when_stale_or_missing: 'Refresh stack-current-docs evidence with Context7 or official docs, then refresh/validate TriWiki before retrying the local worker.'
173
+ }
174
+ };
175
+ }
176
+ function buildOllamaGenerateRequest(agent, slice, opts, config, requestId, triwikiContext) {
177
+ const writePaths = collectWritePaths(slice, opts);
178
+ const prompt = [
179
+ 'You are an SKS local Ollama worker. You are not an architect, planner, reviewer, verifier, safety judge, or strategist.',
180
+ 'Only perform the narrow worker task below. If the task asks for strategy, planning, design, review, verification, risk judgment, or orchestration, return JSON with status "blocked" and blockers.',
181
+ 'Before writing or collecting, consult the TriWiki context below first. Treat use_first as high-trust project memory and hydrate_first as source/evidence that the parent must verify before risky or user-visible work.',
182
+ 'If TriWiki is missing, stale, or lacks current stack syntax/version guidance, do not invent from model memory. Return blocked and tell the parent SKS route to update .sneakoscope/memory/q2_facts/stack-current-docs.md with Context7 or official vendor docs, then run `sks wiki refresh` and `sks wiki validate .sneakoscope/wiki/context-pack.json` before retrying.',
183
+ 'Return JSON only. Do not wrap it in markdown.',
184
+ 'Required shape: {"summary": string, "findings": string[], "proposed_changes": string[], "patch_envelopes": [patchEnvelope]}.',
185
+ 'Each patchEnvelope must contain operations: [{"op":"write","path":"relative/path","content":"text"}] or replace/unified_diff operations.',
186
+ 'Patch envelope operations may be write, replace, or unified_diff. Use only allowed write paths.',
187
+ '',
188
+ triWikiContextBlock(triwikiContext),
189
+ '',
190
+ JSON.stringify({
191
+ request_id: requestId,
192
+ mission_id: opts.missionId || opts.mission_id || '',
193
+ route: opts.route || '$Agent',
194
+ agent: {
195
+ id: agent.id || '',
196
+ session_id: agent.session_id || '',
197
+ slot_id: agent.slot_id || agent.id || '',
198
+ generation_index: Number(agent.generation_index || 1),
199
+ role: agent.role || agent.persona_id || ''
200
+ },
201
+ task_slice: slice || {},
202
+ allowed_write_paths: writePaths,
203
+ triwiki_context: triWikiProofRecord(triwikiContext),
204
+ stack_current_docs_policy: {
205
+ required: true,
206
+ memory_path: '.sneakoscope/memory/q2_facts/stack-current-docs.md',
207
+ refresh: 'sks wiki refresh',
208
+ validate: 'sks wiki validate .sneakoscope/wiki/context-pack.json',
209
+ current_docs_source: 'Context7 or official vendor docs'
210
+ }
211
+ }, null, 2)
212
+ ].join('\n');
213
+ return {
214
+ model: config.model,
215
+ prompt,
216
+ stream: false,
217
+ format: 'json',
218
+ think: config.think,
219
+ keep_alive: config.keep_alive,
220
+ options: {
221
+ temperature: config.temperature
222
+ }
223
+ };
224
+ }
225
+ async function callOllamaGenerate(config, request) {
226
+ const controller = new AbortController();
227
+ const timer = setTimeout(() => controller.abort(), config.timeout_ms);
228
+ try {
229
+ const response = await fetch(`${config.base_url}/api/generate`, {
230
+ method: 'POST',
231
+ headers: { 'Content-Type': 'application/json' },
232
+ body: JSON.stringify(request),
233
+ signal: controller.signal
234
+ });
235
+ const text = await response.text();
236
+ if (!response.ok)
237
+ return { ok: false, error: `http_${response.status}:${text.slice(0, 500)}` };
238
+ return { ok: true, data: JSON.parse(text) };
239
+ }
240
+ finally {
241
+ clearTimeout(timer);
242
+ }
243
+ }
244
+ function parseWorkerJson(text) {
245
+ const trimmed = text.trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
246
+ try {
247
+ return { ok: true, value: JSON.parse(trimmed) };
248
+ }
249
+ catch {
250
+ const match = trimmed.match(/\{[\s\S]*\}/);
251
+ if (!match)
252
+ return { ok: false };
253
+ try {
254
+ return { ok: true, value: JSON.parse(match[0] || '{}') };
255
+ }
256
+ catch {
257
+ return { ok: false };
258
+ }
259
+ }
260
+ }
261
+ function normalizeOllamaPatchEnvelopes(value, agent, slice, opts, requestId) {
262
+ const rawEnvelopes = Array.isArray(value?.patch_envelopes) ? value.patch_envelopes
263
+ : Array.isArray(value?.patchEnvelopes) ? value.patchEnvelopes
264
+ : value?.patch_envelope ? [value.patch_envelope]
265
+ : value?.patchEnvelope ? [value.patchEnvelope]
266
+ : Array.isArray(value?.operations) ? [{ operations: value.operations }]
267
+ : value?.operation ? [{ operation: value.operation }]
268
+ : looksLikePatchOperation(value) ? [value]
269
+ : [];
270
+ return rawEnvelopes.map((raw, index) => {
271
+ const operations = normalizeOllamaOperations(raw);
272
+ const allowedPaths = collectWritePaths(slice, opts);
273
+ const firstPath = String(operations[0]?.path || allowedPaths[index] || slice?.id || `ollama-${index + 1}`);
274
+ const leaseId = String(raw?.lease_id || raw?.lease_proof?.lease_id || `write:${String(agent.id || 'ollama')}:${firstPath}`);
275
+ const nodeId = String(slice?.micro_win_id || slice?.id || `ollama-patch-${index + 1}`);
276
+ const verificationNodeId = String(slice?.verification_node_id || `verify:${nodeId}`);
277
+ const rollbackNodeId = String(slice?.rollback_node_id || `rollback:${nodeId}`);
278
+ return normalizeAgentPatchEnvelope({
279
+ ...raw,
280
+ source: 'model_authored',
281
+ mission_id: String(opts.missionId || opts.mission_id || raw?.mission_id || ''),
282
+ route: String(opts.route || raw?.route || '$Agent'),
283
+ agent_id: String(agent.id || raw?.agent_id || 'ollama-worker'),
284
+ session_id: String(agent.session_id || raw?.session_id || ''),
285
+ slot_id: String(agent.slot_id || raw?.slot_id || agent.id || 'ollama-worker'),
286
+ generation_index: Number(agent.generation_index || raw?.generation_index || 1),
287
+ task_slice_id: String(slice?.id || raw?.task_slice_id || ''),
288
+ native_cli_worker_session_id: String(agent.session_id || raw?.native_cli_worker_session_id || ''),
289
+ native_cli_process_id: process.pid,
290
+ worker_process_id: process.pid,
291
+ backend_ollama_request_id: requestId,
292
+ fast_mode: opts.fastMode !== false,
293
+ service_tier: opts.serviceTier === 'standard' ? 'standard' : 'fast',
294
+ lease_id: leaseId,
295
+ allowed_paths: allowedPaths.length ? allowedPaths : raw?.allowed_paths,
296
+ strategy_task_id: nodeId,
297
+ verification_node_id: verificationNodeId,
298
+ rollback_node_id: rollbackNodeId,
299
+ lease_proof: {
300
+ lease_id: leaseId,
301
+ owner_agent: String(agent.id || 'ollama-worker'),
302
+ owner_persona: String(agent.persona_id || agent.role || 'ollama-worker'),
303
+ allowed_paths: allowedPaths.length ? allowedPaths : raw?.allowed_paths,
304
+ strategy_task_id: nodeId,
305
+ micro_win_id: slice?.micro_win_id ? String(slice.micro_win_id) : undefined,
306
+ protected_path_check: 'passed',
307
+ conflict_prediction_id: `conflict:${nodeId}`,
308
+ verification_node_id: verificationNodeId,
309
+ rollback_node_id: rollbackNodeId
310
+ },
311
+ operations
312
+ });
313
+ });
314
+ }
315
+ function normalizeOllamaOperations(raw) {
316
+ if (Array.isArray(raw?.operations))
317
+ return raw.operations;
318
+ if (raw?.operation)
319
+ return normalizeOllamaOperations(raw.operation);
320
+ if (Array.isArray(raw?.changes))
321
+ return raw.changes.flatMap((change) => normalizeOllamaOperations(change));
322
+ if (Array.isArray(raw?.edits))
323
+ return raw.edits.flatMap((edit) => normalizeOllamaOperations(edit));
324
+ const pathValue = raw?.path || raw?.file || raw?.filepath || raw?.file_path || raw?.target_path || raw?.targetPath;
325
+ if (pathValue && (raw?.content !== undefined || raw?.text !== undefined)) {
326
+ return [{ op: 'write', path: String(pathValue), content: String(raw.content ?? raw.text ?? '') }];
327
+ }
328
+ if (pathValue && (raw?.diff !== undefined || raw?.unified_diff !== undefined || raw?.unifiedDiff !== undefined)) {
329
+ return [{ op: 'unified_diff', path: String(pathValue), diff: String(raw.diff ?? raw.unified_diff ?? raw.unifiedDiff ?? '') }];
330
+ }
331
+ if (pathValue && (raw?.search !== undefined || raw?.find !== undefined || raw?.replace !== undefined)) {
332
+ return [{
333
+ op: 'replace',
334
+ path: String(pathValue),
335
+ search: String(raw.search ?? raw.find ?? ''),
336
+ replace: String(raw.replace || '')
337
+ }];
338
+ }
339
+ return [];
340
+ }
341
+ function looksLikePatchOperation(value) {
342
+ return Boolean(value && typeof value === 'object' && (value.path !== undefined ||
343
+ value.file !== undefined ||
344
+ value.filepath !== undefined ||
345
+ value.file_path !== undefined ||
346
+ value.target_path !== undefined ||
347
+ value.targetPath !== undefined ||
348
+ value.content !== undefined ||
349
+ value.text !== undefined ||
350
+ value.diff !== undefined ||
351
+ value.unified_diff !== undefined ||
352
+ value.unifiedDiff !== undefined ||
353
+ value.search !== undefined ||
354
+ value.find !== undefined));
355
+ }
356
+ function blockedResult(agent, slice, opts, blockers, artifacts) {
357
+ return {
358
+ mission_id: String(opts.missionId || opts.mission_id || ''),
359
+ agent_id: String(agent.id || 'ollama-worker'),
360
+ session_id: String(agent.session_id || ''),
361
+ persona_id: String(agent.persona_id || agent.id || 'ollama-worker'),
362
+ task_slice_id: String(slice?.id || ''),
363
+ status: 'blocked',
364
+ backend: 'ollama',
365
+ summary: 'Ollama local worker blocked by configuration or worker-only policy.',
366
+ findings: [],
367
+ proposed_changes: [],
368
+ changed_files: [],
369
+ lease_compliance: { ok: true, violations: [] },
370
+ artifacts,
371
+ blockers,
372
+ confidence: 'blocked',
373
+ handoff_notes: 'Local model did not run.',
374
+ unverified: [],
375
+ writes: [],
376
+ verification: { status: 'failed', checks: ['ollama-worker-policy'] },
377
+ recursion_guard: { ok: true, violations: [] }
378
+ };
379
+ }
380
+ function collectWritePaths(slice, opts) {
381
+ return [
382
+ ...(Array.isArray(slice?.write_paths) ? slice.write_paths : []),
383
+ ...(Array.isArray(opts?.write_paths) ? opts.write_paths : [])
384
+ ].map(String).filter(Boolean);
385
+ }
386
+ function stringArray(value) {
387
+ return Array.isArray(value) ? value.map(String) : [];
388
+ }
389
+ function safeResponseData(data) {
390
+ return {
391
+ model: data?.model || null,
392
+ created_at: data?.created_at || null,
393
+ done: data?.done === true,
394
+ response_present: typeof data?.response === 'string' && data.response.length > 0,
395
+ thinking_present: typeof data?.thinking === 'string' && data.thinking.length > 0,
396
+ total_duration: data?.total_duration || null,
397
+ load_duration: data?.load_duration || null,
398
+ prompt_eval_count: data?.prompt_eval_count || null,
399
+ eval_count: data?.eval_count || null
400
+ };
401
+ }
402
+ function extractOllamaWorkerText(data) {
403
+ const response = String(data?.response || '').trim();
404
+ if (response)
405
+ return { text: response, source: 'response' };
406
+ const thinking = String(data?.thinking || '').trim();
407
+ if (thinking)
408
+ return { text: thinking, source: 'thinking' };
409
+ return { text: '', source: 'empty' };
410
+ }
411
+ //# sourceMappingURL=agent-runner-ollama.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'];
17
+ export const AGENT_BACKENDS = ['fake', 'process', 'codex-sdk', 'zellij', 'ollama'];
18
18
  export function normalizeAgentBackend(input) {
19
19
  const value = String(input || 'codex-sdk');
20
20
  return AGENT_BACKENDS.includes(value) ? value : 'codex-sdk';
@@ -77,6 +77,8 @@ export function enhanceTaskGraphWithIntelligence(taskGraph, graph) {
77
77
  return taskGraph;
78
78
  const bottleneckTargets = new Set((graph.integration_bottlenecks?.bottlenecks || []).map((row) => String(row.file)));
79
79
  const criticalTargets = new Set(graph.critical_path?.path || []);
80
+ const targetActiveSlots = Math.max(1, Math.floor(Number(taskGraph.target_active_slots || 1)));
81
+ const canSerializeCriticalPath = taskGraph.work_items.length > targetActiveSlots;
80
82
  const workItems = taskGraph.work_items.map((item, index) => {
81
83
  const targets = Array.isArray(item.target_paths) ? item.target_paths.map(String) : [];
82
84
  const critical = targets.some((file) => criticalTargets.has(file));
@@ -90,7 +92,7 @@ export function enhanceTaskGraphWithIntelligence(taskGraph, graph) {
90
92
  test_ownership_ref: 'agent-test-ownership-map.json',
91
93
  critical_path_ref: 'agent-critical-path.json',
92
94
  integration_bottleneck_ref: 'agent-integration-bottlenecks.json',
93
- dependencies: mergeDependencies(item.dependencies, critical && index > 0 ? [taskGraph.work_items[index - 1]?.work_item_id].filter(Boolean) : []),
95
+ dependencies: mergeDependencies(item.dependencies, canSerializeCriticalPath && critical && index > 0 ? [taskGraph.work_items[index - 1]?.work_item_id].filter(Boolean) : []),
94
96
  lease_requirements: [
95
97
  ...(Array.isArray(item.lease_requirements) ? item.lease_requirements : []),
96
98
  ...(bottleneck ? targets.map((file) => ({ kind: 'integration-bottleneck-read', path: file })) : [])
@@ -109,8 +111,9 @@ export function enhanceTaskGraphWithIntelligence(taskGraph, graph) {
109
111
  };
110
112
  }
111
113
  export async function writeIntelligentWorkGraphArtifacts(root, graph) {
112
- await writeJsonAtomic(path.join(root, 'agent-intelligent-work-graph.json'), graph);
113
- await writeJsonAtomic(path.join(root, 'agent-intelligent-work-graph-v2.json'), graph);
114
+ const compact = compactIntelligentWorkGraph(graph);
115
+ await writeJsonAtomic(path.join(root, 'agent-intelligent-work-graph.json'), compact);
116
+ await writeJsonAtomic(path.join(root, 'agent-intelligent-work-graph-v2.json'), compact);
114
117
  await writeJsonAtomic(path.join(root, 'agent-symbol-ownership-map.json'), {
115
118
  schema: 'sks.agent-symbol-ownership-map.v1',
116
119
  generated_at: graph.generated_at,
@@ -180,6 +183,45 @@ export async function writeIntelligentWorkGraphArtifacts(root, graph) {
180
183
  ...graph.integration_bottlenecks
181
184
  });
182
185
  }
186
+ export function compactIntelligentWorkGraph(graph) {
187
+ return {
188
+ schema: graph.schema,
189
+ generated_at: graph.generated_at,
190
+ ok: graph.ok,
191
+ mode: graph.mode,
192
+ compact: true,
193
+ source_inventory_count: graph.source_inventory_count,
194
+ test_inventory_count: graph.test_inventory_count,
195
+ docs_inventory_count: graph.docs_inventory_count,
196
+ script_schema_inventory_count: graph.script_schema_inventory_count,
197
+ dependency_edge_count: graph.dependency_edge_count,
198
+ ast_coverage: graph.ast_coverage,
199
+ test_ownership_confidence: graph.test_ownership_confidence,
200
+ changed_file_candidates: limitArray(graph.changed_file_candidates, 200),
201
+ route_domain_priority: limitArray(graph.route_domain_priority, 100),
202
+ critical_path: graph.critical_path,
203
+ integration_bottlenecks: graph.integration_bottlenecks,
204
+ parallelizable_groups: limitArray(graph.parallelizable_groups, 80),
205
+ serial_dependency_groups: limitArray(graph.serial_dependency_groups, 40),
206
+ work_graph_quality_score: graph.work_graph_quality_score,
207
+ proof_level: graph.proof_level,
208
+ ast_parser_limitations: limitArray(graph.ast_parser_limitations, 100),
209
+ warnings: limitArray(graph.warnings, 200),
210
+ blockers: graph.blockers,
211
+ expanded_artifacts: {
212
+ symbol_ownership: 'agent-symbol-ownership-map.json',
213
+ route_ownership: 'agent-route-ownership-map.json',
214
+ command_ownership: 'agent-command-ownership-map.json',
215
+ test_ownership: 'agent-test-ownership-map.json',
216
+ source_test_ownership: 'agent-source-test-ownership-v2.json',
217
+ critical_path: 'agent-critical-path-v2.json',
218
+ integration_bottlenecks: 'agent-integration-bottlenecks-v2.json'
219
+ }
220
+ };
221
+ }
222
+ function limitArray(items, limit) {
223
+ return Array.isArray(items) ? items.slice(0, limit) : [];
224
+ }
183
225
  function buildTestOwnershipMap(sourceFiles, testFiles, ast, dependencyGraph = {}) {
184
226
  const ownerBySource = {};
185
227
  const relations = [];
@@ -42,6 +42,8 @@ class NativeCliSessionSwarmRecorder {
42
42
  parent_mission_id: this.input.missionId,
43
43
  route: this.input.route,
44
44
  backend: this.input.backend,
45
+ backend_explicit: this.input.backendExplicit === true,
46
+ no_ollama: this.input.noOllama === true || ctx.opts.noOllama === true,
45
47
  agent_root: this.root,
46
48
  agent: ctx.agent,
47
49
  slice: ctx.slice,
@@ -51,6 +53,9 @@ class NativeCliSessionSwarmRecorder {
51
53
  patch_envelope_path: patchRel,
52
54
  service_tier: this.input.fastModePolicy.service_tier,
53
55
  fast_mode: this.input.fastModePolicy.fast_mode,
56
+ ollama_enabled: ctx.opts.ollamaEnabled === true || this.input.backend === 'ollama',
57
+ ollama_model: ctx.opts.ollamaModel || null,
58
+ ollama_base_url: ctx.opts.ollamaBaseUrl || null,
54
59
  source_intelligence_refs: ctx.agent.source_intelligence_refs || null,
55
60
  goal_mode_ref: ctx.agent.goal_mode_ref || null,
56
61
  strategy_refs: ctx.slice?.strategy_refs || null,
@@ -192,6 +197,8 @@ class NativeCliSessionSwarmRecorder {
192
197
  SKS_AGENT_SESSION_ID: String(input.ctx.agent.session_id || ''),
193
198
  SKS_AGENT_SLOT_ID: slotId,
194
199
  SKS_AGENT_GENERATION_INDEX: String(input.ctx.agent.generation_index || 1),
200
+ ...(input.ctx.opts.ollamaModel ? { SKS_OLLAMA_MODEL: String(input.ctx.opts.ollamaModel) } : {}),
201
+ ...(input.ctx.opts.ollamaBaseUrl ? { SKS_OLLAMA_BASE_URL: String(input.ctx.opts.ollamaBaseUrl) } : {}),
195
202
  SKS_ZELLIJ_WORKER_PANE: '1',
196
203
  SKS_ZELLIJ_SESSION_NAME: sessionName
197
204
  }
@@ -220,7 +227,7 @@ class NativeCliSessionSwarmRecorder {
220
227
  serviceTier: this.input.fastModePolicy.service_tier
221
228
  });
222
229
  const launchBlockers = paneRecord.blockers || [];
223
- input.record.command_line = ['zellij', '--session', sessionName, 'action', 'new-pane', '--name', paneRecord.pane_name, '--', 'sh', '-lc', '<native-cli-worker-command>'];
230
+ input.record.command_line = ['zellij', '--session', sessionName, 'action', 'new-pane', '--direction', paneRecord.direction_applied, '--name', paneRecord.pane_name, '--', 'sh', '-lc', '<native-cli-worker-command>'];
224
231
  input.record.zellij_session_name = sessionName;
225
232
  input.record.zellij_pane_id = paneRecord.pane_id || null;
226
233
  input.record.zellij_pane_id_source = paneRecord.pane_id_source;
@@ -159,7 +159,7 @@ export async function runNativeCliWorker(input = {}) {
159
159
  structured_output_valid: routed.report?.structured_output_valid === true,
160
160
  backend_router_report: routed.report,
161
161
  backend_child_process_ids: routed.report.child_process_ids,
162
- backend_child_execution: routed.report.child_process_ids.length > 0 || Boolean(routed.report?.sdk_thread_id) || backend === 'fake',
162
+ backend_child_execution: routed.report.child_process_ids.length > 0 || Boolean(routed.report?.sdk_thread_id) || backend === 'fake' || backend === 'ollama',
163
163
  recursion_guard_env: process.env.SKS_DISABLE_ROUTE_RECURSION === '1',
164
164
  worker_env: process.env.SKS_AGENT_WORKER === '1',
165
165
  exit_code: guard.ok ? 0 : 1
@@ -2,6 +2,8 @@ import path from 'node:path';
2
2
  import { nowIso, readJson, writeJsonAtomic } from '../fsx.js';
3
3
  import { buildFixturePatchEnvelopes } from './agent-runner-fake.js';
4
4
  import { runProcessAgent } from './agent-runner-process.js';
5
+ import { classifyOllamaWorkerSlice, runOllamaAgent } from './agent-runner-ollama.js';
6
+ import { resolveOllamaWorkerConfig } from './ollama-worker-config.js';
5
7
  import { runZellijAgent } from './agent-runner-zellij.js';
6
8
  import { validateAgentWorkerResult } from './agent-worker-pipeline.js';
7
9
  import { normalizeAgentPatchEnvelope } from './agent-patch-schema.js';
@@ -11,7 +13,8 @@ export const NATIVE_WORKER_BACKEND_ROUTER_SCHEMA = 'sks.native-worker-backend-ro
11
13
  export async function runNativeWorkerBackendRouter(input) {
12
14
  const root = path.resolve(input.agentRoot);
13
15
  const requestedBackend = String(input.backend || '');
14
- const backend = normalizeBackend(requestedBackend);
16
+ let backend = normalizeBackend(requestedBackend);
17
+ backend = await maybeAutoSelectOllamaBackend(backend, input);
15
18
  const reportRel = path.join(input.workerDirRel, 'worker-backend-router-report.json');
16
19
  const startedAt = nowIso();
17
20
  let result;
@@ -61,6 +64,28 @@ export async function runNativeWorkerBackendRouter(input) {
61
64
  verification: { status: processRun.status === 'done' ? 'passed' : 'failed', checks: [...(processRun.verification?.checks || []), 'native-worker-backend-router', 'process-child-execution'] }
62
65
  });
63
66
  }
67
+ else if (backend === 'ollama') {
68
+ const ollamaRun = await runOllamaAgent(input.agent, input.slice, {
69
+ ...input.intake,
70
+ missionId: input.intake.mission_id || input.intake.parent_mission_id || '',
71
+ agentRoot: root,
72
+ workerDirRel: input.workerDirRel,
73
+ cwd: input.intake.cwd || root,
74
+ route: input.intake.route || '$Agent',
75
+ fastMode: input.fastModePolicy.fast_mode,
76
+ serviceTier: input.fastModePolicy.service_tier
77
+ });
78
+ patchEnvelopes = Array.isArray(ollamaRun.patch_envelopes) ? ollamaRun.patch_envelopes : [];
79
+ proofLevel = ollamaRun.status === 'done' ? (patchEnvelopes.length ? 'model_authored' : 'ollama_worker_proven') : 'blocked';
80
+ result = validateAgentWorkerResult({
81
+ ...ollamaRun,
82
+ backend: 'ollama',
83
+ patch_envelopes: patchEnvelopes,
84
+ model_authored_patch_envelopes: patchEnvelopes.length > 0,
85
+ fixture_patch_envelopes: false,
86
+ verification: { status: ollamaRun.status === 'done' ? 'passed' : 'failed', checks: [...(ollamaRun.verification?.checks || []), 'native-worker-backend-router', 'ollama-api-generate'] }
87
+ });
88
+ }
64
89
  else if (backend === 'codex-sdk' || backend === 'zellij') {
65
90
  const sdkTask = await runCodexTask({
66
91
  route: String(input.intake.route || '$Agent'),
@@ -155,6 +180,7 @@ export async function runNativeWorkerBackendRouter(input) {
155
180
  finished_at: nowIso(),
156
181
  ok: result.status === 'done',
157
182
  selected_backend: backend || input.backend,
183
+ requested_backend: requestedBackend,
158
184
  agent_id: input.agent.id,
159
185
  session_id: input.agent.session_id,
160
186
  worker_process_id: process.pid,
@@ -170,6 +196,7 @@ export async function runNativeWorkerBackendRouter(input) {
170
196
  sdk_run_id: childReports.find((report) => report?.sdk_run_id)?.sdk_run_id || null,
171
197
  stream_event_count: Number(childReports.find((report) => report?.stream_event_count)?.stream_event_count || 0),
172
198
  structured_output_valid: childReports.some((report) => report?.structured_output_valid === true),
199
+ ollama_request_ids: patchEnvelopes.map((envelope) => envelope.backend_ollama_request_id).filter(Boolean),
173
200
  blockers: result.blockers || []
174
201
  };
175
202
  await writeJsonAtomic(path.join(root, reportRel), report);
@@ -184,8 +211,23 @@ export async function runNativeWorkerBackendRouter(input) {
184
211
  patchEnvelopes
185
212
  };
186
213
  }
214
+ async function maybeAutoSelectOllamaBackend(backend, input) {
215
+ if (backend !== 'codex-sdk')
216
+ return backend;
217
+ if (input.intake?.backend_explicit === true || input.intake?.no_ollama === true)
218
+ return backend;
219
+ const config = await resolveOllamaWorkerConfig({
220
+ ollamaEnabled: input.intake?.ollama_enabled === true,
221
+ model: input.intake?.ollama_model || null,
222
+ baseUrl: input.intake?.ollama_base_url || null
223
+ }).catch(() => null);
224
+ if (!config?.ok || config.enabled !== true)
225
+ return backend;
226
+ const policy = classifyOllamaWorkerSlice(input.slice, { route: input.intake?.route, agent: input.agent });
227
+ return policy.ok ? 'ollama' : backend;
228
+ }
187
229
  function normalizeBackend(value) {
188
- return value === 'fake' || value === 'process' || value === 'codex-sdk' || value === 'zellij' ? value : null;
230
+ return value === 'fake' || value === 'process' || value === 'codex-sdk' || value === 'zellij' || value === 'ollama' ? value : null;
189
231
  }
190
232
  function envelopeOpts(input, source, childPid) {
191
233
  return {