sneakoscope 3.0.4 → 3.1.1

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 (85) hide show
  1. package/README.md +1 -1
  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/cli/command-registry.js +1 -0
  8. package/dist/cli/context7-command.js +29 -5
  9. package/dist/cli/install-helpers.js +15 -7
  10. package/dist/commands/zellij-slot-column-anchor.js +3 -1
  11. package/dist/commands/zellij-slot-pane.js +19 -2
  12. package/dist/core/agents/agent-janitor.js +10 -1
  13. package/dist/core/agents/agent-orchestrator.js +1 -0
  14. package/dist/core/agents/agent-runner-ollama.js +11 -4
  15. package/dist/core/agents/native-cli-session-swarm.js +69 -9
  16. package/dist/core/agents/runtime-proof-summary.js +4 -0
  17. package/dist/core/codex-control/codex-task-runner.js +9 -0
  18. package/dist/core/commands/goal-command.js +19 -1
  19. package/dist/core/commands/loop-command.js +176 -0
  20. package/dist/core/commands/naruto-command.js +26 -17
  21. package/dist/core/commands/team-command.js +1 -0
  22. package/dist/core/fsx.js +1 -1
  23. package/dist/core/init.js +6 -1
  24. package/dist/core/locks/file-lock.js +88 -0
  25. package/dist/core/loops/goal-to-loop-compat.js +23 -0
  26. package/dist/core/loops/loop-artifacts.js +72 -0
  27. package/dist/core/loops/loop-checkpoint.js +22 -0
  28. package/dist/core/loops/loop-decomposer.js +56 -0
  29. package/dist/core/loops/loop-finalizer.js +54 -0
  30. package/dist/core/loops/loop-gate-ladder.js +16 -0
  31. package/dist/core/loops/loop-gate-registry.js +96 -0
  32. package/dist/core/loops/loop-gate-runner.js +177 -0
  33. package/dist/core/loops/loop-gate-selector.js +52 -0
  34. package/dist/core/loops/loop-gpt-final-arbiter.js +61 -0
  35. package/dist/core/loops/loop-integration-merge.js +75 -0
  36. package/dist/core/loops/loop-iteration-runner.js +2 -0
  37. package/dist/core/loops/loop-lease.js +91 -0
  38. package/dist/core/loops/loop-observability.js +19 -0
  39. package/dist/core/loops/loop-owner-inference.js +57 -0
  40. package/dist/core/loops/loop-owner-ledger.js +2 -0
  41. package/dist/core/loops/loop-planner.js +170 -0
  42. package/dist/core/loops/loop-proof-summary.js +10 -0
  43. package/dist/core/loops/loop-proof.js +2 -0
  44. package/dist/core/loops/loop-risk-classifier.js +42 -0
  45. package/dist/core/loops/loop-runtime-control.js +25 -0
  46. package/dist/core/loops/loop-runtime.js +314 -0
  47. package/dist/core/loops/loop-scheduler.js +69 -0
  48. package/dist/core/loops/loop-schema.js +63 -0
  49. package/dist/core/loops/loop-state.js +61 -0
  50. package/dist/core/loops/loop-worker-prompts.js +43 -0
  51. package/dist/core/loops/loop-worker-runtime.js +275 -0
  52. package/dist/core/loops/loop-worktree-runtime.js +92 -0
  53. package/dist/core/naruto/naruto-finalizer.js +7 -2
  54. package/dist/core/naruto/naruto-loop-mesh.js +39 -0
  55. package/dist/core/naruto/naruto-loop-worker-router.js +38 -0
  56. package/dist/core/pipeline-internals/runtime-core.js +82 -2
  57. package/dist/core/proof/proof-schema.js +6 -0
  58. package/dist/core/proof/proof-writer.js +5 -2
  59. package/dist/core/proof/root-cause-policy.js +70 -0
  60. package/dist/core/proof/route-adapter.js +18 -1
  61. package/dist/core/proof/route-proof-gate.js +4 -0
  62. package/dist/core/release/release-gate-batch-runner.js +56 -10
  63. package/dist/core/release/release-gate-cache-v2.js +18 -3
  64. package/dist/core/release/release-gate-dag.js +65 -17
  65. package/dist/core/release/release-gate-node.js +2 -1
  66. package/dist/core/release/release-gate-resource-governor.js +27 -6
  67. package/dist/core/skills/core-skill-meta-update.js +24 -0
  68. package/dist/core/skills/core-skill-reflection.js +94 -0
  69. package/dist/core/skills/core-skill-trainer.js +103 -0
  70. package/dist/core/trust-kernel/completion-contract.js +4 -0
  71. package/dist/core/trust-kernel/route-contract.js +4 -1
  72. package/dist/core/version.js +1 -1
  73. package/dist/core/zellij/zellij-right-column-manager.js +13 -2
  74. package/dist/core/zellij/zellij-slot-column-anchor.js +45 -5
  75. package/dist/core/zellij/zellij-slot-pane-renderer.js +37 -10
  76. package/dist/core/zellij/zellij-slot-telemetry.js +96 -44
  77. package/dist/core/zellij/zellij-worker-pane-manager.js +42 -4
  78. package/dist/scripts/loop-directive-check-lib.js +388 -0
  79. package/dist/scripts/loop-worker-fixture-child.js +53 -0
  80. package/dist/scripts/naruto-real-local-gpt-final-smoke.js +10 -1
  81. package/package.json +38 -3
  82. package/schemas/loops/loop-node.schema.json +21 -0
  83. package/schemas/loops/loop-plan.schema.json +21 -0
  84. package/schemas/loops/loop-proof.schema.json +20 -0
  85. package/schemas/loops/loop-state.schema.json +19 -0
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import fsp from 'node:fs/promises';
3
- import { appendJsonlBounded, ensureDir, nowIso, readJson, readText } from '../fsx.js';
3
+ import { appendJsonlBounded, ensureDir, nowIso, readJson, readText, writeTextAtomic } from '../fsx.js';
4
+ import { withFileLock } from '../locks/file-lock.js';
4
5
  export const ZELLIJ_SLOT_TELEMETRY_EVENT_SCHEMA = 'sks.zellij-slot-telemetry-event.v1';
5
6
  export const ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA = 'sks.zellij-slot-telemetry-snapshot.v1';
6
7
  const telemetrySnapshotCache = new Map();
@@ -71,7 +72,7 @@ export function mergeTelemetrySnapshots(base, overlay, opts = {}) {
71
72
  const slots = { ...(base.slots || {}) };
72
73
  for (const [key, row] of Object.entries(overlay.slots || {})) {
73
74
  const existing = slots[key];
74
- slots[key] = !existing || telemetryTsMs(row.latest_ts) >= telemetryTsMs(existing.latest_ts) ? row : existing;
75
+ slots[key] = !existing ? row : newerSlotTelemetry(existing, row);
75
76
  }
76
77
  const baseTs = Date.parse(String(base.updated_at || '')) || 0;
77
78
  const overlayTs = Date.parse(String(overlay.updated_at || '')) || 0;
@@ -88,6 +89,21 @@ function telemetryTsMs(value) {
88
89
  const parsed = Date.parse(String(value || ''));
89
90
  return Number.isFinite(parsed) ? parsed : 0;
90
91
  }
92
+ function newerSlotTelemetry(current, incoming) {
93
+ const currentTs = telemetryTsMs(current.latest_ts);
94
+ const incomingTs = telemetryTsMs(incoming.latest_ts);
95
+ const currentTerminal = isTelemetryTerminalStatus(current.status);
96
+ const incomingTerminal = isTelemetryTerminalStatus(incoming.status);
97
+ if (currentTerminal && !incomingTerminal)
98
+ return current;
99
+ if (!currentTerminal && incomingTerminal)
100
+ return incoming;
101
+ if (incomingTs > currentTs)
102
+ return incoming;
103
+ if (incomingTs < currentTs)
104
+ return current;
105
+ return incoming;
106
+ }
91
107
  async function statTelemetryFile(file) {
92
108
  try {
93
109
  const st = await fsp.stat(file);
@@ -99,17 +115,17 @@ async function statTelemetryFile(file) {
99
115
  }
100
116
  export function applyTelemetryEventToSnapshot(snapshot, event) {
101
117
  const key = slotTelemetryKey(event.slot_id || event.worker_id, event.generation_index);
102
- const slots = {
103
- ...(snapshot.slots || {}),
104
- [key]: mergeSlotTelemetry(snapshot.slots?.[key], event)
105
- };
118
+ const previous = snapshot.slots?.[key];
119
+ const nextSlot = mergeSlotTelemetry(previous, event);
120
+ const slots = snapshot.slots || {};
121
+ slots[key] = nextSlot;
106
122
  return {
107
123
  schema: ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA,
108
124
  mission_id: event.mission_id || snapshot.mission_id,
109
125
  updated_at: nowIso(),
110
126
  flush_count: snapshot.flush_count || 0,
111
127
  slots,
112
- counts: countSlotTelemetry(slots)
128
+ counts: adjustSlotTelemetryCounts(snapshot.counts || countSlotTelemetry(snapshot.slots || {}), previous, nextSlot)
113
129
  };
114
130
  }
115
131
  export async function rebuildZellijSlotTelemetrySnapshot(root, missionId) {
@@ -176,11 +192,19 @@ function normalizeTelemetryEvent(event) {
176
192
  };
177
193
  }
178
194
  function mergeSlotTelemetry(previous, event) {
195
+ const previousTs = telemetryTsMs(previous?.latest_ts);
196
+ const eventTs = telemetryTsMs(event.ts);
197
+ const previousTerminal = isTelemetryTerminalStatus(previous?.status);
198
+ const eventTerminal = isTelemetryTerminalStatus(event.status);
199
+ const terminalRegression = Boolean(previous && previousTerminal && !eventTerminal);
200
+ const stale = Boolean(previous && (terminalRegression
201
+ || (!eventTerminal && eventTs < previousTs)
202
+ || (previousTerminal && eventTerminal && eventTs < previousTs)));
179
203
  return {
180
- slot_id: event.slot_id,
181
- generation_index: event.generation_index,
182
- worker_id: event.worker_id,
183
- status: event.status,
204
+ slot_id: stale ? previous.slot_id : event.slot_id,
205
+ generation_index: stale ? previous.generation_index : event.generation_index,
206
+ worker_id: stale ? previous.worker_id : event.worker_id,
207
+ status: stale ? previous.status : event.status,
184
208
  role: event.role || previous?.role || 'worker',
185
209
  backend: event.backend || previous?.backend || 'unknown',
186
210
  provider: event.provider || previous?.provider || 'unknown',
@@ -188,34 +212,55 @@ function mergeSlotTelemetry(previous, event) {
188
212
  worktree_id: event.worktree_id ?? previous?.worktree_id ?? null,
189
213
  worktree_path: event.worktree_path ?? previous?.worktree_path ?? null,
190
214
  task_title: event.task_title || previous?.task_title || 'waiting for task',
191
- current_file: event.current_file ?? previous?.current_file ?? null,
192
- latest_event_type: event.event_type,
193
- latest_ts: event.ts,
194
- progress: event.progress || previous?.progress || null,
215
+ current_file: stale ? previous.current_file ?? (terminalRegression ? null : event.current_file ?? null) : event.current_file ?? previous?.current_file ?? null,
216
+ latest_event_type: stale ? previous.latest_event_type : event.event_type,
217
+ latest_ts: stale ? previous.latest_ts : event.ts,
218
+ progress: stale ? previous.progress || (terminalRegression ? null : event.progress || null) : event.progress || previous?.progress || null,
195
219
  artifact_paths: unique([...(previous?.artifact_paths || []), ...(event.artifact_paths || [])]),
196
220
  blockers: unique([...(previous?.blockers || []), ...(event.blockers || [])]),
197
- log_tail: event.log_tail || previous?.log_tail || ''
221
+ log_tail: stale ? previous.log_tail || (terminalRegression ? '' : event.log_tail || '') : event.log_tail || previous?.log_tail || ''
198
222
  };
199
223
  }
200
224
  function countSlotTelemetry(slots) {
201
225
  const counts = { queued: 0, running: 0, verifying: 0, completed: 0, failed: 0, headless: 0 };
202
226
  for (const row of Object.values(slots)) {
203
- const status = normalizeStatus(row.status);
204
- if (status === 'queued' || status === 'launching')
205
- counts.queued += 1;
206
- else if (status === 'running')
207
- counts.running += 1;
208
- else if (status === 'verifying')
209
- counts.verifying += 1;
210
- else if (status === 'completed' || status === 'drained')
211
- counts.completed += 1;
212
- else if (status === 'failed')
213
- counts.failed += 1;
214
- else if (status === 'headless')
215
- counts.headless += 1;
227
+ const bucket = slotTelemetryCountBucket(row.status);
228
+ counts[bucket] += 1;
229
+ }
230
+ return counts;
231
+ }
232
+ function adjustSlotTelemetryCounts(current, previous, next) {
233
+ const counts = { ...current };
234
+ const nextBucket = slotTelemetryCountBucket(next.status);
235
+ if (previous) {
236
+ const previousBucket = slotTelemetryCountBucket(previous.status);
237
+ if (previousBucket !== nextBucket) {
238
+ counts[previousBucket] = Math.max(0, Number(counts[previousBucket] || 0) - 1);
239
+ counts[nextBucket] = Number(counts[nextBucket] || 0) + 1;
240
+ }
241
+ return counts;
216
242
  }
243
+ counts[nextBucket] = Number(counts[nextBucket] || 0) + 1;
217
244
  return counts;
218
245
  }
246
+ function slotTelemetryCountBucket(status) {
247
+ const normalized = normalizeStatus(status);
248
+ if (normalized === 'queued' || normalized === 'launching')
249
+ return 'queued';
250
+ if (normalized === 'verifying')
251
+ return 'verifying';
252
+ if (normalized === 'completed' || normalized === 'drained')
253
+ return 'completed';
254
+ if (normalized === 'failed')
255
+ return 'failed';
256
+ if (normalized === 'headless')
257
+ return 'headless';
258
+ return 'running';
259
+ }
260
+ function isTelemetryTerminalStatus(status) {
261
+ const normalized = normalizeStatus(status);
262
+ return normalized === 'completed' || normalized === 'failed' || normalized === 'drained';
263
+ }
219
264
  function normalizeStatus(value) {
220
265
  const text = String(value || '').toLowerCase();
221
266
  if (text === 'closed' || text === 'done' || text === 'passed')
@@ -258,20 +303,26 @@ function tail(value, max) {
258
303
  }
259
304
  async function writeTelemetrySnapshotFast(file, snapshot) {
260
305
  await ensureDir(path.dirname(file));
261
- const flushCount = Number(telemetrySnapshotFlushCounts.get(file) || 0) + 1;
262
- telemetrySnapshotFlushCounts.set(file, flushCount);
263
- telemetrySnapshotLastFlushMs.set(file, Date.now());
264
- // Merge with the on-disk snapshot before overwriting: multiple processes
265
- // (orchestrator + worker children) flush this file concurrently and a plain
266
- // overwrite would drop slots that only the other process has observed.
267
- const disk = await readJson(file, null);
268
- const merged = disk?.schema === ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA ? mergeTelemetrySnapshots(disk, snapshot) : snapshot;
269
- const next = { ...merged, flush_count: Math.max(flushCount, Number(merged.flush_count || 0)) };
270
- telemetrySnapshotCache.set(file, next);
271
- await fsp.writeFile(file, `${JSON.stringify(next)}\n`, 'utf8');
272
- const stat = await statTelemetryFile(file);
273
- if (stat)
274
- telemetrySnapshotDiskStat.set(file, stat);
306
+ await withFileLock({
307
+ lockPath: `${file}.lock`,
308
+ timeoutMs: 30000,
309
+ staleMs: 2 * 60 * 1000
310
+ }, async () => {
311
+ const flushCount = Number(telemetrySnapshotFlushCounts.get(file) || 0) + 1;
312
+ telemetrySnapshotFlushCounts.set(file, flushCount);
313
+ telemetrySnapshotLastFlushMs.set(file, Date.now());
314
+ // Multiple processes (orchestrator + worker children) flush this file.
315
+ // Serialize the read/merge/write critical section so a slower writer
316
+ // cannot overwrite a newer slot observed by a different process.
317
+ const disk = await readJson(file, null);
318
+ const merged = disk?.schema === ZELLIJ_SLOT_TELEMETRY_SNAPSHOT_SCHEMA ? mergeTelemetrySnapshots(disk, snapshot) : snapshot;
319
+ const next = { ...merged, flush_count: Math.max(flushCount, Number(merged.flush_count || 0)) };
320
+ telemetrySnapshotCache.set(file, next);
321
+ await writeTextAtomic(file, `${JSON.stringify(next)}\n`);
322
+ const stat = await statTelemetryFile(file);
323
+ if (stat)
324
+ telemetrySnapshotDiskStat.set(file, stat);
325
+ });
275
326
  }
276
327
  function shouldFlushTelemetrySnapshot(file, event) {
277
328
  const next = (telemetrySnapshotWriteCounts.get(file) || 0) + 1;
@@ -289,7 +340,8 @@ function shouldFlushTelemetrySnapshot(file, event) {
289
340
  || event.event_type === 'worker_completed'
290
341
  || event.event_type === 'worker_failed'
291
342
  || event.status === 'completed'
292
- || event.status === 'failed';
343
+ || event.status === 'failed'
344
+ || event.status === 'drained';
293
345
  const should = next === 1
294
346
  || important
295
347
  || now - last >= flushMs
@@ -14,12 +14,49 @@ export const ZELLIJ_PANE_CREATION_LOCK_METRICS_SCHEMA = 'sks.zellij-pane-creatio
14
14
  export function buildWorkerPaneName(slotId, generationIndex) {
15
15
  return `${slotId}/gen-${Math.max(1, Math.floor(Number(generationIndex) || 1))}`;
16
16
  }
17
- export function buildWorkerPaneTitle(slotId, generationIndex, context, serviceTier, backend, status, worktree) {
17
+ export function buildWorkerPaneTitle(slotId, generationIndex, context, serviceTier, backend, status, worktree, taskTitle) {
18
+ // When a task label is available, the pane title is JUST the label + status:
19
+ // users identify panes by what they are doing, not by slot ids. Slot/gen,
20
+ // backend, provider, and worktree details stay inside the pane body and the
21
+ // SLOTS anchor rows. The verbose format remains the fallback for callers
22
+ // that have no task context.
23
+ const task = shortWorkerTaskLabel(taskTitle);
24
+ if (task)
25
+ return `${task} · ${workerBackendTag(backend, context?.provider)} · ${status || 'launching'}`;
18
26
  const base = buildWorkerPaneName(slotId, generationIndex);
19
27
  const normalized = normalizePaneProviderContext(context, serviceTier);
20
28
  const wt = worktree ? ` · WT:${worktree.id} · branch:${worktree.branch}` : '';
21
29
  return `${base}${wt} · ${backend || 'codex-sdk'} · ${providerPaneLabel(normalized)} · ${status || 'launching'}`;
22
30
  }
31
+ // Local-LLM workers must be visually distinguishable from GPT/codex workers:
32
+ // only trivial long-running edits belong on local models, while web research
33
+ // and code review must run on GPT. The tag makes a misrouted task obvious.
34
+ export function workerBackendTag(backend, provider) {
35
+ const text = `${String(backend || '')} ${String(provider || '')}`.toLowerCase();
36
+ if (/ollama|local/.test(text))
37
+ return 'LOCAL';
38
+ if (/fake|fixture/.test(text))
39
+ return 'fixture';
40
+ return 'GPT';
41
+ }
42
+ // Lead the pane title with a 1-3 word task label so users can tell WHAT each
43
+ // slot is doing at a glance instead of an anonymous "slot-002/gen-1".
44
+ export function shortWorkerTaskLabel(value, maxChars = 24) {
45
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
46
+ if (!text)
47
+ return '';
48
+ const words = text.split(' ');
49
+ let label = '';
50
+ for (const word of words.slice(0, 3)) {
51
+ const next = label ? `${label} ${word}` : word;
52
+ if (next.length > maxChars)
53
+ break;
54
+ label = next;
55
+ }
56
+ if (!label)
57
+ label = text.slice(0, maxChars);
58
+ return label;
59
+ }
23
60
  export function isRealZellijWorkerPaneIdSource(value) {
24
61
  return value === 'zellij_worker_new_pane_stdout' || value === 'zellij_worker_list_panes';
25
62
  }
@@ -162,7 +199,7 @@ export async function openWorkerPane(input) {
162
199
  });
163
200
  return record;
164
201
  }
165
- const paneName = buildWorkerPaneTitle(input.slotId, input.generationIndex, providerContext, input.serviceTier, input.backend, input.statusLabel || 'running', input.worktree || null);
202
+ const paneName = buildWorkerPaneTitle(input.slotId, input.generationIndex, providerContext, input.serviceTier, input.backend, input.statusLabel || 'running', input.worktree || null, input.taskTitle || null);
166
203
  // CRITICAL: serialize anchor + worker pane creation per session. Workers
167
204
  // launch concurrently, and without this lock every worker raced past the
168
205
  // "does the SLOTS anchor exist yet?" check and created its OWN anchor with
@@ -695,7 +732,8 @@ async function withZellijPaneCreationLock(input, fn) {
695
732
  const current = new Promise((resolve) => {
696
733
  release = resolve;
697
734
  });
698
- zellijPaneCreationLocks.set(key, previous.then(() => current, () => current));
735
+ const chained = previous.then(() => current, () => current);
736
+ zellijPaneCreationLocks.set(key, chained);
699
737
  await previous.catch(() => undefined);
700
738
  const acquiredAt = nowIso();
701
739
  const acquiredMs = Date.now();
@@ -741,7 +779,7 @@ async function withZellijPaneCreationLock(input, fn) {
741
779
  meta: { wait_ms: metrics.wait_ms, held_ms: metrics.held_ms }
742
780
  }).catch(() => undefined);
743
781
  release();
744
- if (zellijPaneCreationLocks.get(key) === current)
782
+ if (zellijPaneCreationLocks.get(key) === chained)
745
783
  zellijPaneCreationLocks.delete(key);
746
784
  }
747
785
  }