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
@@ -1,10 +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
+ import { classifyOllamaWorkerSlice } from '../agents/agent-runner-ollama.js';
5
6
  import { buildNarutoCloneRoster, systemSafeNarutoConcurrency } from '../agents/agent-roster.js';
6
7
  import { DEFAULT_NARUTO_CLONES, MAX_NARUTO_AGENT_COUNT } from '../agents/agent-schema.js';
8
+ import { resolveOllamaWorkerConfig } from '../agents/ollama-worker-config.js';
7
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';
8
18
  const NARUTO_RESULT_SCHEMA = 'sks.naruto-command-result.v1';
9
19
  const NARUTO_ROUTE = '$Naruto';
10
20
  // $Naruto — Shadow Clone Swarm (影分身 / Kage Bunshin no Jutsu).
@@ -23,6 +33,27 @@ export async function narutoCommand(commandOrArgs = 'naruto', maybeArgs = []) {
23
33
  }
24
34
  async function narutoRun(parsed) {
25
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
+ }
26
57
  const roster = buildNarutoCloneRoster({
27
58
  clones: parsed.clones,
28
59
  prompt: parsed.prompt,
@@ -32,10 +63,55 @@ async function narutoRun(parsed) {
32
63
  // The clone roster is the full work fan-out; live concurrency is throttled to a
33
64
  // system-safe number so naruto never spawns the whole count at once unless an
34
65
  // explicit operator override asks for a higher target.
35
- const safe = systemSafeNarutoConcurrency({ backend: parsed.backend });
36
- const activeSlots = Math.max(1, Math.min(roster.agent_count, parsed.concurrency || safe.cap));
66
+ const localWorker = await resolveNarutoLocalWorkerMode(parsed);
67
+ const schedulerBackend = localWorker.auto_select_eligible ? 'ollama' : parsed.backend;
68
+ const safe = systemSafeNarutoConcurrency({ backend: schedulerBackend });
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
+ });
37
103
  const mission = await createMission(root, { mode: 'naruto', prompt: parsed.prompt });
38
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
+ });
39
115
  let liveZellij = null;
40
116
  if (!parsed.json && !parsed.mock && !parsed.noOpenZellij) {
41
117
  liveZellij = await launchZellijLayout({
@@ -43,12 +119,12 @@ async function narutoRun(parsed) {
43
119
  missionId: mission.id,
44
120
  ledgerRoot,
45
121
  kind: 'naruto',
46
- slotCount: roster.agent_count,
122
+ slotCount: zellijVisiblePanes,
47
123
  dryRun: false,
48
124
  attach: false
49
125
  });
50
126
  if (liveZellij?.ok && liveZellij.capability?.status === 'ok') {
51
- 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));
52
128
  if (parsed.attach)
53
129
  attachZellijSessionInteractive(liveZellij.session_name, { cwd: process.cwd(), configPath: liveZellij.clipboard_config_path });
54
130
  }
@@ -69,12 +145,17 @@ async function narutoRun(parsed) {
69
145
  agents: roster.agent_count,
70
146
  concurrency: activeSlots,
71
147
  targetActiveSlots: activeSlots,
72
- visualLaneCount: roster.agent_count,
148
+ visualLaneCount: zellijVisiblePanes,
73
149
  desiredWorkItemCount: parsed.workItems,
74
150
  maxAgentCount: MAX_NARUTO_AGENT_COUNT,
75
151
  narutoMode: true,
76
152
  clones: roster.agent_count,
77
153
  backend: parsed.backend,
154
+ backendExplicit: parsed.backendExplicit,
155
+ noOllama: parsed.noOllama,
156
+ ollamaEnabled: parsed.ollamaEnabled,
157
+ ollamaModel: parsed.ollamaModel,
158
+ ollamaBaseUrl: parsed.ollamaBaseUrl,
78
159
  mock: parsed.mock,
79
160
  real: parsed.real,
80
161
  readonly: parsed.readonly,
@@ -86,6 +167,7 @@ async function narutoRun(parsed) {
86
167
  json: parsed.json
87
168
  });
88
169
  const clones = result.roster?.agent_count ?? roster.agent_count;
170
+ const localWorkerSummary = summarizeNarutoLocalWorkerResult(localWorker, result);
89
171
  const summary = {
90
172
  schema: NARUTO_RESULT_SCHEMA,
91
173
  ok: result.ok === true,
@@ -99,6 +181,21 @@ async function narutoRun(parsed) {
99
181
  target_active_slots: result.target_active_slots ?? activeSlots,
100
182
  concurrency_capped: clones > (result.target_active_slots ?? activeSlots),
101
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
+ },
198
+ local_worker: localWorkerSummary,
102
199
  proof: result.proof?.status || 'missing',
103
200
  run: result,
104
201
  zellij: null
@@ -109,13 +206,27 @@ async function narutoRun(parsed) {
109
206
  console.log('Mission: ' + result.mission_id);
110
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)` : ''));
111
208
  console.log('Backend: ' + result.backend);
209
+ console.log('Roles: ' + roleDistribution.entries.map((entry) => `${entry.role}:${entry.count}`).join(', '));
112
210
  console.log('Proof: ' + summary.proof);
113
211
  if (summary.zellij?.ok && summary.zellij.capability?.status === 'ok')
114
- 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)');
115
213
  else if (summary.zellij?.ok)
116
214
  console.log('Zellij: optional live panes unavailable (' + ((summary.zellij.warnings || []).join('; ') || summary.zellij.capability?.status || 'unknown') + ')');
117
215
  });
118
216
  }
217
+ function summarizeNarutoLocalWorkerResult(localWorker, result) {
218
+ const backendCounts = {};
219
+ const rows = Array.isArray(result?.results) ? result.results : [];
220
+ for (const row of rows) {
221
+ const selected = String(row?.backend_router_report?.selected_backend || row?.backend || 'unknown');
222
+ backendCounts[selected] = (backendCounts[selected] || 0) + 1;
223
+ }
224
+ return {
225
+ ...localWorker,
226
+ selected_worker_count: backendCounts.ollama || 0,
227
+ backend_counts: backendCounts
228
+ };
229
+ }
119
230
  async function narutoStatus(parsed) {
120
231
  const root = await sksRoot();
121
232
  const id = parsed.missionId && parsed.missionId !== 'latest' ? parsed.missionId : await findLatestMission(root);
@@ -124,6 +235,9 @@ async function narutoStatus(parsed) {
124
235
  const { dir } = await loadMission(root, id);
125
236
  const proof = await readJson(path.join(dir, 'agents', 'agent-proof-evidence.json'), null);
126
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);
127
241
  const summary = {
128
242
  schema: NARUTO_RESULT_SCHEMA,
129
243
  ok: proof !== null,
@@ -132,13 +246,22 @@ async function narutoStatus(parsed) {
132
246
  proof: proof?.status || 'missing',
133
247
  target_active_slots: scheduler?.target_active_slots ?? null,
134
248
  max_active_slots: scheduler?.max_active_slots ?? null,
135
- 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
136
257
  };
137
258
  return emit(parsed, summary, () => {
138
259
  console.log('🍥 Naruto mission: ' + id);
139
260
  console.log('Proof: ' + summary.proof);
140
261
  if (summary.target_active_slots !== null)
141
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(', '));
142
265
  });
143
266
  }
144
267
  async function narutoHelp(parsed) {
@@ -149,7 +272,7 @@ async function narutoHelp(parsed) {
149
272
  mode: 'NARUTO',
150
273
  description: 'Shadow Clone Swarm: fan out up to ' + MAX_NARUTO_AGENT_COUNT + ' parallel clone sessions.',
151
274
  usage: [
152
- 'sks naruto run "<task>" [--clones N] [--backend codex-sdk|fake] [--work-items N] [--real] [--readonly] [--json]',
275
+ 'sks naruto run "<task>" [--clones N] [--backend codex-sdk|fake|ollama] [--local-model|--no-ollama] [--work-items N] [--real] [--readonly] [--json]',
153
276
  'sks naruto status [--mission <id>] [--json]'
154
277
  ],
155
278
  defaults: { clones: DEFAULT_NARUTO_CLONES, max_clones: MAX_NARUTO_AGENT_COUNT, backend: 'codex-sdk' }
@@ -162,6 +285,8 @@ async function narutoHelp(parsed) {
162
285
  });
163
286
  }
164
287
  function parseNarutoArgs(args = []) {
288
+ if (hasFlag(args, '--help') || hasFlag(args, '-h'))
289
+ args = ['help', ...args.filter((arg) => arg !== '--help' && arg !== '-h')];
165
290
  const first = args[0] && !String(args[0]).startsWith('--') ? String(args[0]) : '';
166
291
  const actions = new Set(['run', 'status', 'help']);
167
292
  const action = (actions.has(first) ? first : 'run');
@@ -171,18 +296,33 @@ function parseNarutoArgs(args = []) {
171
296
  const clones = clampClones(requestedClones);
172
297
  const workItems = clampWorkItems(Number(readOption(args, '--work-items', clones)), clones);
173
298
  const concurrency = normalizeConcurrency(readOption(args, '--concurrency', readOption(args, '--target-active-slots', null)), clones);
174
- const backend = String(readOption(args, '--backend', hasFlag(args, '--mock') ? 'fake' : 'codex-sdk'));
299
+ const useOllama = hasFlag(args, '--ollama') || hasFlag(args, '--local-model');
300
+ const noOllama = hasFlag(args, '--no-ollama') || hasFlag(args, '--no-local-model');
301
+ const backendExplicit = hasOption(args, '--backend') || useOllama || noOllama;
302
+ const backend = String(readOption(args, '--backend', hasFlag(args, '--mock') ? 'fake' : useOllama && !noOllama ? 'ollama' : 'codex-sdk'));
175
303
  const mock = hasFlag(args, '--mock') || backend === 'fake';
176
304
  const real = hasFlag(args, '--real');
177
305
  const readonly = hasFlag(args, '--readonly') || hasFlag(args, '--read-only');
178
306
  const writeModeRaw = String(readOption(args, '--write-mode', hasFlag(args, '--parallel-write') ? 'parallel' : '') || '');
179
307
  const writeMode = (['proof-safe', 'parallel', 'serial', 'off'].includes(writeModeRaw) ? writeModeRaw : null);
180
308
  const missionId = String(readOption(args, '--mission', readOption(args, '--mission-id', 'latest')));
309
+ const ollamaModel = String(readOption(args, '--ollama-model', readOption(args, '--local-model-model', '')) || '') || null;
310
+ const ollamaBaseUrl = String(readOption(args, '--ollama-base-url', readOption(args, '--local-model-base-url', '')) || '') || null;
181
311
  const noOpenZellij = hasFlag(args, '--no-open-zellij') || hasFlag(args, '--no-zellij');
182
312
  const attach = hasFlag(args, '--attach');
183
- const valueFlags = new Set(['--clones', '--agents', '--work-items', '--concurrency', '--target-active-slots', '--backend', '--write-mode', '--mission', '--mission-id']);
313
+ const valueFlags = new Set(['--clones', '--agents', '--work-items', '--concurrency', '--target-active-slots', '--backend', '--write-mode', '--mission', '--mission-id', '--ollama-model', '--local-model-model', '--ollama-base-url', '--local-model-base-url']);
184
314
  const prompt = positionalArgs(rest, valueFlags).join(' ').trim() || 'Naruto shadow clone swarm run';
185
- return { action, prompt, clones, workItems, concurrency, backend, mock, real, readonly, writeMode, json, missionId, noOpenZellij, attach };
315
+ return { action, prompt, clones, workItems, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, json, missionId, noOpenZellij, attach };
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);
186
326
  }
187
327
  function clampClones(value) {
188
328
  if (!Number.isFinite(value) || value < 1)
@@ -212,6 +352,9 @@ function readOption(args, name, fallback) {
212
352
  const prefixed = args.find((arg) => String(arg).startsWith(name + '='));
213
353
  return prefixed ? prefixed.slice(name.length + 1) : fallback;
214
354
  }
355
+ function hasOption(args, name) {
356
+ return args.includes(name) || args.some((arg) => String(arg).startsWith(name + '='));
357
+ }
215
358
  function positionalArgs(args, valueFlags) {
216
359
  const out = [];
217
360
  for (let i = 0; i < args.length; i += 1) {
@@ -233,4 +376,44 @@ function emit(parsed, result, text) {
233
376
  text();
234
377
  return result;
235
378
  }
379
+ async function resolveNarutoLocalWorkerMode(parsed) {
380
+ const configInput = {
381
+ ollamaEnabled: parsed.ollamaEnabled,
382
+ model: parsed.ollamaModel,
383
+ baseUrl: parsed.ollamaBaseUrl
384
+ };
385
+ if (parsed.backend === 'ollama')
386
+ configInput.backend = 'ollama';
387
+ const config = await resolveOllamaWorkerConfig(configInput).catch(() => null);
388
+ const policy = classifyOllamaWorkerSlice({
389
+ id: 'naruto-local-worker-probe',
390
+ role: parsed.readonly ? 'collector' : 'implementer',
391
+ description: parsed.prompt,
392
+ write_paths: parsed.readonly ? [] : ['<lease-scoped-worker-path>']
393
+ }, { route: NARUTO_ROUTE, agent: { role: parsed.readonly ? 'collector' : 'implementer' } });
394
+ const autoSelectEligible = parsed.backend === 'codex-sdk'
395
+ && parsed.backendExplicit !== true
396
+ && parsed.noOllama !== true
397
+ && config?.ok === true
398
+ && config.enabled === true
399
+ && policy.ok === true;
400
+ return {
401
+ schema: 'sks.naruto-local-worker-mode.v1',
402
+ enabled: config?.enabled === true,
403
+ provider: config?.provider || 'ollama',
404
+ model: config?.model || null,
405
+ requested_backend: parsed.backend,
406
+ backend_explicit: parsed.backendExplicit,
407
+ auto_select_eligible: autoSelectEligible,
408
+ worker_only: true,
409
+ no_strategy_planning_design: true,
410
+ policy,
411
+ blockers: [
412
+ ...(config?.blockers || (config ? [] : ['ollama_worker_config_unavailable'])),
413
+ ...(policy.blockers || []),
414
+ ...(parsed.backendExplicit ? ['backend_explicit'] : []),
415
+ ...(parsed.noOllama ? ['no_ollama_requested'] : [])
416
+ ]
417
+ };
418
+ }
236
419
  //# sourceMappingURL=naruto-command.js.map
@@ -231,7 +231,7 @@ async function executeRouteCommand(root, route, prompt, { auto = false } = {}) {
231
231
  return routeExecutionResult(route, ['sks', ...commandArgs].join(' '), result, {
232
232
  okStatus: 'completed',
233
233
  trustStatus: 'verified_partial',
234
- executionKind: route.command === '$DB' || route.command === '$Wiki' || route.command === '$Fast-Mode' || route.command === '$with-local-llm-on' ? 'safe_deterministic' : 'mock_safe',
234
+ executionKind: route.command === '$DB' || route.command === '$Wiki' || route.command === '$Fast-Mode' || route.command === '$with-local-llm-on' || route.command === '$Commit' || route.command === '$Commit-And-Push' ? 'safe_deterministic' : 'mock_safe',
235
235
  });
236
236
  }
237
237
  async function runAutoVerification(root, missionId) {
@@ -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 = {}) {
@@ -348,6 +348,10 @@ function safeRouteExecutionArgs(route, prompt, { auto = false } = {}) {
348
348
  return ['fast-mode', fastModeActionFromPrompt(prompt), '--json'];
349
349
  if (route.command === '$with-local-llm-on')
350
350
  return ['with-local-llm', localModelActionFromPrompt(prompt), '--json'];
351
+ if (route.command === '$Commit')
352
+ return ['commit', '--json'];
353
+ if (route.command === '$Commit-And-Push')
354
+ return ['commit-and-push', '--json'];
351
355
  return ['team', prompt, '--mock', '--json', ...(auto ? ['--no-open-zellij'] : [])];
352
356
  }
353
357
  function fastModeActionFromPrompt(prompt = '') {
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { nowIso, writeJsonAtomic } from '../fsx.js';
3
+ import { resolveLocalCollaborationPolicy } from '../local-llm/local-collaboration-policy.js';
3
4
  export const DOCTOR_READINESS_MATRIX_SCHEMA = 'sks.doctor-readiness-matrix.v1';
4
5
  export async function writeDoctorReadinessMatrix(root, input = {}) {
5
6
  const matrix = buildDoctorReadinessMatrix(input);
@@ -53,6 +54,20 @@ export function buildDoctorReadinessMatrix(input = {}) {
53
54
  warnings.add('codex_app_fast_selector_repaired_restart_app_if_needed');
54
55
  if (input.codex_lb?.ok === false)
55
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');
65
+ const localCollaborationPolicy = resolveLocalCollaborationPolicy({ mode: input.local_collaboration?.mode || null });
66
+ const gptFinalAvailable = input.local_collaboration?.gpt_final_arbiter_available === undefined
67
+ ? codexBinOk
68
+ : input.local_collaboration.gpt_final_arbiter_available === true;
69
+ if (localCollaborationPolicy.gpt_final_required && !gptFinalAvailable)
70
+ blockers.add('gpt_final_arbiter_unavailable');
56
71
  const codexConfigNode = nodeRead.ok !== false && codexConfig.ok !== false;
57
72
  const codexConfigChild = childRead.ok !== false && codexConfig.ok !== false;
58
73
  const cliReady = codexBinOk && codexConfigNode && codexConfigChild && cliConfigOk;
@@ -92,6 +107,25 @@ export function buildDoctorReadinessMatrix(input = {}) {
92
107
  hooks_ready: input.hooks_ready !== false,
93
108
  codex_app_ready: input.codex_app?.ok === true,
94
109
  codex_app_required_for_cli: false,
110
+ local_collaboration: {
111
+ mode: localCollaborationPolicy.mode,
112
+ local_backend: input.local_collaboration?.local_backend || localModel.provider || 'ollama',
113
+ local_model: input.local_collaboration?.local_model || localModel.model || null,
114
+ final_arbiter: gptFinalAvailable ? 'GPT available' : 'missing',
115
+ final_apply_allowed: localCollaborationPolicy.gpt_final_required ? gptFinalAvailable : localCollaborationPolicy.mode === 'disabled',
116
+ blockers: localCollaborationPolicy.gpt_final_required && !gptFinalAvailable ? ['gpt_final_arbiter_unavailable'] : localCollaborationPolicy.blockers
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
+ },
95
129
  ready: blockers.size === 0 && cliReady,
96
130
  primary_blocker: [...blockers][0] || null,
97
131
  blockers: [...blockers],
@@ -25,6 +25,7 @@ const FIXTURES = Object.freeze({
25
25
  'cli-status': fixture('execute', 'sks status --json', [], 'pass'),
26
26
  'cli-usage': fixture('execute', 'sks usage overview', [], 'pass'),
27
27
  'cli-quickstart': fixture('execute', 'sks quickstart', [], 'pass'),
28
+ 'cli-update': fixture('mock', 'sks update now --dry-run --json', [], 'pass'),
28
29
  'cli-update-check': fixture('static', 'sks update-check --json', [], 'pass'),
29
30
  'cli-guard': fixture('execute', 'sks guard check --json', [], 'pass'),
30
31
  'cli-conflicts': fixture('execute', 'sks conflicts check --json', [], 'pass'),
@@ -55,6 +56,7 @@ const FIXTURES = Object.freeze({
55
56
  'cli-stats': fixture('execute', 'sks stats --json', [], 'pass'),
56
57
  'cli-dollar-commands': fixture('execute', 'sks dollar-commands --json', [], 'pass'),
57
58
  'cli-fast-mode': fixture('execute', 'sks fast-mode status --json', [], 'pass'),
59
+ 'cli-with-local-llm': fixture('execute', 'sks with-local-llm status --json', [], 'pass'),
58
60
  'cli-dfix': fixture('execute_and_validate_artifacts', 'sks dfix fixture --json', ['completion-proof.json', 'dfix-gate.json', 'dfix-verification.json'], 'pass'),
59
61
  'cli-wiki': fixture('execute_and_validate_artifacts', 'sks wiki image-ingest test/fixtures/images/one-by-one.png --json', [{ path: '.sneakoscope/wiki/image-voxel-ledger.json', schema: 'sks.image-voxel-ledger.v1', require_anchors: false }], 'pass'),
60
62
  'cli-db': fixture('execute', 'sks db policy', [], 'pass'),
@@ -112,6 +114,8 @@ const FIXTURES = Object.freeze({
112
114
  'route-fast-on': fixture('mock', '$Fast-On covered by hermetic fast-mode blackbox toggle test', [], 'pass'),
113
115
  'route-fast-off': fixture('mock', '$Fast-Off covered by hermetic fast-mode blackbox toggle test', [], 'pass'),
114
116
  'route-local-model': fixture('execute', 'sks with-local-llm status --json', [], 'pass'),
117
+ 'route-with-local-llm-on': fixture('mock', '$with-local-llm-on covered by hermetic local-model dollar-command blackbox toggle test', [], 'pass'),
118
+ 'route-with-local-llm-off': fixture('mock', '$with-local-llm-off covered by hermetic local-model dollar-command blackbox toggle test', [], 'pass'),
115
119
  'route-help': fixture('mock', '$Help lightweight route', [], 'pass'),
116
120
  'route-commit': fixture('mock', '$Commit git route', ['completion-proof.json'], 'pass'),
117
121
  'route-commit-and-push': fixture('mock', '$Commit-And-Push git route', ['completion-proof.json'], 'pass'),
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.2';
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() {
@@ -1,5 +1,8 @@
1
- import { runProcess, projectRoot, isGitRepo } from './fsx.js';
1
+ import path from 'node:path';
2
+ import { runProcess, projectRoot, isGitRepo, nowIso, sha256 } from './fsx.js';
2
3
  import { redactSecrets } from './secret-redaction.js';
4
+ import { runOllamaAgent } from './agents/agent-runner-ollama.js';
5
+ import { resolveOllamaWorkerConfig } from './agents/ollama-worker-config.js';
3
6
  const TRAILER = 'Co-authored-by: Codex <noreply@openai.com>';
4
7
  export async function simpleGitCommitCommand(args = [], opts = {}) {
5
8
  const root = await projectRoot();
@@ -16,17 +19,24 @@ export async function simpleGitCommitCommand(args = [], opts = {}) {
16
19
  export async function simpleGitCommit(root, { message = null, push = false } = {}) {
17
20
  if (!await isGitRepo(root))
18
21
  return { schema: 'sks.simple-git.v1', ok: false, reason: 'not_git_repo', root };
19
- const before = await git(root, ['status', '--short']);
22
+ const [before, branch, head] = await Promise.all([
23
+ git(root, ['status', '--short']),
24
+ git(root, ['branch', '--show-current']),
25
+ git(root, ['rev-parse', '--short', 'HEAD'])
26
+ ]);
20
27
  const changed = statusLines(before.stdout);
21
28
  if (!changed.length)
22
29
  return { schema: 'sks.simple-git.v1', ok: false, reason: 'no_changes', root };
30
+ const localWorker = message
31
+ ? localWorkerSkipped('message_provided')
32
+ : await draftCommitMessageWithLocalWorker(root, changed, { push, branch: branch.stdout.trim(), head: head.stdout.trim() });
23
33
  const add = await git(root, ['add', '-A']);
24
34
  if (add.code !== 0)
25
35
  return failure(root, 'git_add_failed', before, add);
26
36
  const stagedCheck = await git(root, ['diff', '--cached', '--quiet']);
27
37
  if (stagedCheck.code === 0)
28
38
  return { schema: 'sks.simple-git.v1', ok: false, reason: 'no_staged_changes', root, changed };
29
- const commitMessage = ensureCodexTrailer(message || buildCommitMessage(changed));
39
+ const commitMessage = ensureCodexTrailer(message || localWorker.message || buildCommitMessage(changed));
30
40
  const commit = await git(root, ['commit', '-m', commitTitle(commitMessage), '-m', commitBody(commitMessage)]);
31
41
  if (commit.code !== 0)
32
42
  return failure(root, 'git_commit_failed', before, commit);
@@ -46,7 +56,8 @@ export async function simpleGitCommit(root, { message = null, push = false } = {
46
56
  commit: commitSummary(commit),
47
57
  hash: hash.stdout.trim(),
48
58
  pushed: Boolean(push && pushResult?.code === 0),
49
- push: pushResult ? { ok: pushResult.code === 0, stdout: pushResult.stdout.trim(), stderr: pushResult.stderr.trim() } : null
59
+ push: pushResult ? { ok: pushResult.code === 0, stdout: pushResult.stdout.trim(), stderr: pushResult.stderr.trim() } : null,
60
+ local_worker: localWorker.report
50
61
  });
51
62
  }
52
63
  function failure(root, reason, before, failed, extra = {}) {
@@ -77,6 +88,134 @@ function buildCommitMessage(changed = []) {
77
88
  const more = changed.length > 12 ? `\n- ...and ${changed.length - 12} more` : '';
78
89
  return `chore: update project changes\n\nSummary: ${summary}\n\nChanged files:\n${files}${more}`;
79
90
  }
91
+ async function draftCommitMessageWithLocalWorker(root, changed, context) {
92
+ const disabled = String(process.env.SKS_SIMPLE_GIT_LOCAL_LLM || '').trim() === '0';
93
+ if (disabled)
94
+ return { message: null, report: localWorkerSkipped('disabled_by_SKS_SIMPLE_GIT_LOCAL_LLM') };
95
+ const config = await resolveOllamaWorkerConfig().catch((error) => null);
96
+ if (!config?.ok || config.enabled !== true) {
97
+ return {
98
+ message: null,
99
+ report: {
100
+ schema: 'sks.simple-git-local-worker.v1',
101
+ generated_at: nowIso(),
102
+ ok: false,
103
+ used: false,
104
+ enabled: config?.enabled === true,
105
+ provider: config?.provider || 'ollama',
106
+ model: config?.model || null,
107
+ worker_only: true,
108
+ parent_owned_git_mutation: true,
109
+ task: context.push ? 'commit-and-push-message-draft' : 'commit-message-draft',
110
+ blockers: config?.blockers || ['ollama_worker_config_unavailable']
111
+ }
112
+ };
113
+ }
114
+ const runId = sha256(`${nowIso()}:${context.branch}:${context.head}:${changed.join('\n')}`).slice(0, 12);
115
+ const workerDirRel = path.join('.git', 'sks-local-workers', 'simple-git', runId);
116
+ const slice = {
117
+ id: `simple-git-message-${runId}`,
118
+ role: 'collector',
119
+ domain: 'git',
120
+ description: [
121
+ 'simple collect summarize git status for a commit message draft only',
122
+ 'Do not run git commands. Do not choose whether to commit or push.',
123
+ 'Return summary as a conventional commit title and proposed_changes as short body bullets.',
124
+ `Action: ${context.push ? 'commit-and-push' : 'commit'}`,
125
+ `Branch: ${context.branch || 'unknown'}`,
126
+ `HEAD before commit: ${context.head || 'unknown'}`,
127
+ 'Changed status lines:',
128
+ ...changed.slice(0, 80)
129
+ ].join('\n')
130
+ };
131
+ const agent = {
132
+ id: 'simple_git_local_worker',
133
+ session_id: `simple-git-${runId}`,
134
+ slot_id: 'local-worker',
135
+ generation_index: 1,
136
+ persona_id: 'local_git_summarizer',
137
+ role: 'collector'
138
+ };
139
+ const result = await runOllamaAgent(agent, slice, {
140
+ missionId: `simple-git-${runId}`,
141
+ agentRoot: root,
142
+ cwd: root,
143
+ workerDirRel,
144
+ route: context.push ? '$Commit-And-Push' : '$Commit',
145
+ fastMode: true,
146
+ serviceTier: 'fast',
147
+ ollamaTimeoutMs: Number(process.env.SKS_SIMPLE_GIT_OLLAMA_TIMEOUT_MS || 15000)
148
+ }).catch((error) => ({
149
+ status: 'blocked',
150
+ summary: '',
151
+ findings: [],
152
+ proposed_changes: [],
153
+ artifacts: [],
154
+ blockers: [error instanceof Error ? error.message : String(error)]
155
+ }));
156
+ const message = result.status === 'done' ? localWorkerCommitMessage(result, changed) : null;
157
+ return {
158
+ message,
159
+ report: {
160
+ schema: 'sks.simple-git-local-worker.v1',
161
+ generated_at: nowIso(),
162
+ ok: result.status === 'done',
163
+ used: Boolean(message),
164
+ enabled: true,
165
+ provider: config.provider,
166
+ model: config.model,
167
+ worker_only: true,
168
+ parent_owned_git_mutation: true,
169
+ task: context.push ? 'commit-and-push-message-draft' : 'commit-message-draft',
170
+ artifacts: result.artifacts || [],
171
+ summary: result.summary || null,
172
+ blockers: result.blockers || [],
173
+ fallback: message ? null : 'deterministic_commit_message'
174
+ }
175
+ };
176
+ }
177
+ function localWorkerCommitMessage(result, changed) {
178
+ const title = normalizeCommitTitle(result.summary);
179
+ if (!title)
180
+ return null;
181
+ const bodyLines = [
182
+ 'Summary: local Ollama worker drafted this message from git status; parent SKS performed all git mutations.',
183
+ '',
184
+ ...stringArray(result.proposed_changes || result.findings).slice(0, 8).map((line) => `- ${line}`),
185
+ ...(stringArray(result.proposed_changes || result.findings).length ? [] : changed.slice(0, 8).map((line) => `- ${line}`))
186
+ ];
187
+ return `${title}\n\n${bodyLines.join('\n')}`;
188
+ }
189
+ function normalizeCommitTitle(value) {
190
+ const raw = String(value || '').split(/\r?\n/)[0]?.trim() || '';
191
+ const stripped = raw.replace(/^["']|["']$/g, '').replace(/\s+/g, ' ').trim();
192
+ if (!stripped)
193
+ return '';
194
+ const conventional = /^[a-z][a-z0-9-]*(\([^)]+\))?:\s+\S/.test(stripped);
195
+ const title = conventional ? stripped : `chore: ${stripped.replace(/^(commit message|summary)\s*:\s*/i, '')}`;
196
+ return title.length > 96 ? title.slice(0, 93).trimEnd() + '...' : title;
197
+ }
198
+ function stringArray(value) {
199
+ return Array.isArray(value) ? value.map((line) => String(line || '').trim()).filter(Boolean) : [];
200
+ }
201
+ function localWorkerSkipped(reason) {
202
+ return {
203
+ message: null,
204
+ report: {
205
+ schema: 'sks.simple-git-local-worker.v1',
206
+ generated_at: nowIso(),
207
+ ok: true,
208
+ used: false,
209
+ enabled: false,
210
+ provider: 'ollama',
211
+ model: null,
212
+ worker_only: true,
213
+ parent_owned_git_mutation: true,
214
+ task: 'commit-message-draft',
215
+ blockers: [reason]
216
+ }
217
+ };
218
+ }
80
219
  function ensureCodexTrailer(message = '') {
81
220
  const withoutDuplicate = String(message || '')
82
221
  .split(/\r?\n/)