sneakoscope 3.1.0 → 3.1.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 (84) 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/install-helpers.js +6 -7
  8. package/dist/commands/zellij-slot-column-anchor.js +3 -1
  9. package/dist/commands/zellij-slot-pane.js +19 -2
  10. package/dist/core/agents/agent-janitor.js +10 -1
  11. package/dist/core/agents/agent-orchestrator.js +8 -2
  12. package/dist/core/agents/agent-proof-evidence.js +20 -0
  13. package/dist/core/agents/agent-runner-ollama.js +11 -4
  14. package/dist/core/agents/fast-mode-policy.js +7 -5
  15. package/dist/core/agents/intelligent-work-graph.js +93 -14
  16. package/dist/core/agents/native-cli-session-swarm.js +115 -9
  17. package/dist/core/agents/no-subagent-scaling-policy.js +10 -1
  18. package/dist/core/agents/official-subagent-helper-policy.js +62 -0
  19. package/dist/core/codex-app.js +0 -2
  20. package/dist/core/codex-control/codex-task-runner.js +9 -0
  21. package/dist/core/commands/fast-mode-command.js +1 -1
  22. package/dist/core/commands/loop-command.js +86 -13
  23. package/dist/core/commands/naruto-command.js +34 -21
  24. package/dist/core/commands/team-command.js +1 -0
  25. package/dist/core/commands/wiki-command.js +35 -1
  26. package/dist/core/fsx.js +1 -1
  27. package/dist/core/init.js +1 -2
  28. package/dist/core/locks/file-lock.js +88 -0
  29. package/dist/core/loops/loop-artifacts.js +54 -2
  30. package/dist/core/loops/loop-checkpoint.js +22 -0
  31. package/dist/core/loops/loop-concurrency-budget.js +55 -0
  32. package/dist/core/loops/loop-final-arbiter-contract.js +28 -0
  33. package/dist/core/loops/loop-finalizer.js +55 -7
  34. package/dist/core/loops/loop-fixture-policy.js +58 -0
  35. package/dist/core/loops/loop-gate-registry.js +96 -0
  36. package/dist/core/loops/loop-gate-runner.js +206 -17
  37. package/dist/core/loops/loop-gpt-final-arbiter.js +81 -0
  38. package/dist/core/loops/loop-integration-merge.js +80 -0
  39. package/dist/core/loops/loop-interrupt-registry.js +118 -0
  40. package/dist/core/loops/loop-lease.js +35 -20
  41. package/dist/core/loops/loop-merge-strategy.js +105 -0
  42. package/dist/core/loops/loop-mutation-ledger.js +103 -0
  43. package/dist/core/loops/loop-planner.js +36 -5
  44. package/dist/core/loops/loop-runtime-control.js +27 -0
  45. package/dist/core/loops/loop-runtime.js +254 -96
  46. package/dist/core/loops/loop-scheduler.js +14 -5
  47. package/dist/core/loops/loop-side-effect-scanner.js +91 -0
  48. package/dist/core/loops/loop-worker-prompts.js +43 -0
  49. package/dist/core/loops/loop-worker-runtime.js +281 -0
  50. package/dist/core/loops/loop-worktree-runtime.js +92 -0
  51. package/dist/core/naruto/naruto-finalizer.js +7 -2
  52. package/dist/core/naruto/naruto-loop-mesh.js +10 -1
  53. package/dist/core/proof/auto-finalize.js +3 -2
  54. package/dist/core/proof/proof-schema.js +6 -0
  55. package/dist/core/proof/proof-writer.js +5 -2
  56. package/dist/core/proof/root-cause-policy.js +70 -0
  57. package/dist/core/proof/route-adapter.js +18 -1
  58. package/dist/core/proof/route-finalizer.js +71 -6
  59. package/dist/core/proof/route-proof-gate.js +4 -0
  60. package/dist/core/release/release-gate-batch-runner.js +56 -10
  61. package/dist/core/release/release-gate-cache-v2.js +18 -3
  62. package/dist/core/release/release-gate-dag.js +121 -18
  63. package/dist/core/release/release-gate-node.js +2 -1
  64. package/dist/core/release/release-gate-resource-governor.js +27 -6
  65. package/dist/core/skills/core-skill-meta-update.js +24 -0
  66. package/dist/core/skills/core-skill-reflection.js +94 -0
  67. package/dist/core/skills/core-skill-trainer.js +103 -0
  68. package/dist/core/trust-kernel/completion-contract.js +4 -0
  69. package/dist/core/trust-kernel/route-contract.js +4 -1
  70. package/dist/core/version.js +1 -1
  71. package/dist/core/zellij/zellij-right-column-manager.js +13 -2
  72. package/dist/core/zellij/zellij-slot-column-anchor.js +40 -3
  73. package/dist/core/zellij/zellij-slot-pane-renderer.js +36 -11
  74. package/dist/core/zellij/zellij-slot-telemetry.js +96 -44
  75. package/dist/core/zellij/zellij-worker-pane-manager.js +42 -4
  76. package/dist/scripts/lib/native-cli-session-swarm-check-lib.js +14 -2
  77. package/dist/scripts/loop-directive-check-lib.js +225 -2
  78. package/dist/scripts/loop-hardening-check-lib.js +289 -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/dist/scripts/prepublish-release-check-or-fast.js +38 -10
  82. package/dist/scripts/release-check-stamp.js +29 -4
  83. package/dist/scripts/release-gate-existence-audit.js +1 -0
  84. package/package.json +32 -2
@@ -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
  }
@@ -3,7 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { execFileSync } from 'node:child_process';
5
5
  import { assertGate, root } from '../sks-1-18-gate-lib.js';
6
- export function runNativeCliSwarmCheck({ agents, workItems = agents, reportName, backend = 'fake', extraArgs = [] }) {
6
+ export function runNativeCliSwarmCheck({ agents, workItems = agents, reportName, backend = 'fake', extraArgs = [], expectedFastMode = null }) {
7
7
  const distCli = path.join(root, 'dist', 'bin', 'sks.js');
8
8
  assertGate(fs.existsSync(distCli), 'dist CLI missing for native CLI swarm check', { distCli });
9
9
  const args = [
@@ -37,7 +37,11 @@ export function runNativeCliSwarmCheck({ agents, workItems = agents, reportName,
37
37
  const result = JSON.parse(stdout);
38
38
  const proof = result.native_cli_session_proof || {};
39
39
  const noSubagent = result.no_subagent_scaling_policy || {};
40
+ const officialHelper = result.official_subagent_helper_policy || {};
40
41
  const fast = result.fast_mode_propagation || {};
42
+ const policy = fast.policy || result.fast_mode_policy || {};
43
+ const expectedFast = expectedFastMode === null || expectedFastMode === undefined ? Boolean(policy.fast_mode) : Boolean(expectedFastMode);
44
+ const expectedTier = expectedFast ? 'fast' : 'standard';
41
45
  const report = {
42
46
  schema: 'sks.native-cli-session-swarm-check.v1',
43
47
  ok: result.ok === true,
@@ -47,6 +51,7 @@ export function runNativeCliSwarmCheck({ agents, workItems = agents, reportName,
47
51
  backend: result.backend,
48
52
  native_cli_session_proof: proof,
49
53
  no_subagent_scaling_policy: noSubagent,
54
+ official_subagent_helper_policy: officialHelper,
50
55
  fast_mode_propagation: fast,
51
56
  proof_status: result.proof?.status || null
52
57
  };
@@ -60,7 +65,14 @@ export function runNativeCliSwarmCheck({ agents, workItems = agents, reportName,
60
65
  assertGate(Array.isArray(proof.process_ids) && proof.process_ids.length >= agents, 'process ids missing from native CLI proof', report);
61
66
  assertGate(proof.close_report_count >= agents, 'worker close report count below requested agents', report);
62
67
  assertGate(noSubagent.ok === true && noSubagent.subagent_events_counted_as_worker_sessions === false, 'no-subagent scaling policy must pass', report);
63
- assertGate(fast.ok === true && fast.fast_mode === true && fast.service_tier === 'fast', 'fast mode must propagate by default', report);
68
+ assertGate(officialHelper.ok === true && officialHelper.official_codex_subagent_helper_lane_enabled === true, 'official subagent helper policy must pass', report);
69
+ assertGate(officialHelper.worker_capacity_credit === 0 && officialHelper.subagent_events_counted_as_worker_sessions === false, 'official helper lane must not count toward worker capacity', report);
70
+ assertGate(noSubagent.official_codex_subagent_helper_lane_allowed === true && noSubagent.official_helper_lane_worker_capacity_credit === 0, 'no-subagent policy must allow helper lane with zero capacity credit', report);
71
+ assertGate(fast.ok === true, 'fast mode propagation proof must pass', report);
72
+ assertGate(fast.fast_mode === expectedFast && fast.service_tier === expectedTier, 'worker service tier must match the selected fast-mode policy', { ...report, expected_fast_mode: expectedFast, expected_service_tier: expectedTier });
73
+ if (expectedFast) {
74
+ assertGate(policy.explicit_fast === true || policy.preference_mode === 'fast' || policy.explicit_service_tier === 'fast', 'fast-mode propagation gate must use explicit fast opt-in', report);
75
+ }
64
76
  assertGate((proof.worker_command_lines || []).every((line) => line.includes('--agent') && line.includes('worker')), 'worker command lines must use --agent worker', report);
65
77
  return report;
66
78
  }
@@ -4,6 +4,7 @@ import fs from 'node:fs/promises';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { COMMANDS } from '../cli/command-registry.js';
7
+ import { runProcess } from '../core/fsx.js';
7
8
  import { compileGoalToLoopPlan } from '../core/loops/goal-to-loop-compat.js';
8
9
  import { loopGraphProofPath, loopPlanPath, loopProofPath, loopRoot, loopStatePath } from '../core/loops/loop-artifacts.js';
9
10
  import { decomposeRequestIntoLoopDomains } from '../core/loops/loop-decomposer.js';
@@ -29,7 +30,18 @@ export async function runLoopDirectiveCheck(id) {
29
30
  const request = 'fix zellij telemetry, release cache, and codex probe docs';
30
31
  const plan = await planLoopsFromRequest({ root, missionId, request, sourceCommand: 'loop' });
31
32
  const byId = new Map(plan.graph.nodes.map((node) => [node.loop_id, node]));
32
- const result = await runLoopPlan({ root, plan, parallelism: 'extreme', noMutation: id.includes('runtime') ? false : true });
33
+ const realRuntimeMode = process.env.SKS_LOOP_RUNTIME_REAL === '1'
34
+ || id === 'loop:runtime-real-workers'
35
+ || id === 'loop:maker-checker-real'
36
+ || id === 'loop:integration-finalizer-real'
37
+ || id === 'loop:real-maker-checker-blackbox'
38
+ || id === 'naruto:loop-mesh-real-blackbox'
39
+ || id === 'goal:loop-runtime-real-blackbox';
40
+ if (!realRuntimeMode && process.env.SKS_LOOP_RUNTIME_FIXTURE !== '1') {
41
+ process.env.SKS_LOOP_RUNTIME_FIXTURE = '1';
42
+ }
43
+ const fixtureMode = process.env.SKS_LOOP_RUNTIME_FIXTURE === '1' || process.env.SKS_LOOP_GATE_FIXTURE === '1';
44
+ const result = await runLoopPlan({ root, plan, parallelism: 'extreme', noMutation: fixtureMode ? true : !realRuntimeMode });
33
45
  const assertions = [];
34
46
  const assert = (condition, message) => assertions.push({ ok: Boolean(condition), message });
35
47
  assert(validateLoopPlan(plan).ok, 'loop plan validates');
@@ -40,6 +52,9 @@ export async function runLoopDirectiveCheck(id) {
40
52
  }
41
53
  else if (id === 'loop:artifact-paths') {
42
54
  assert(loopRoot(root, missionId).includes('.sneakoscope/missions'), 'artifact root layout matches directive');
55
+ assert(throws(() => loopRoot(root, '../../escape')), 'loop artifact root rejects mission traversal');
56
+ assert(throws(() => loopRoot(root, 'bad/mission')), 'loop artifact root rejects path separators in mission id');
57
+ assert(throws(() => loopStatePath(root, missionId, '../loop-escape')), 'loop node artifact path rejects loop traversal');
43
58
  }
44
59
  else if (id === 'loop:state') {
45
60
  assert(await exists(loopStatePath(root, missionId, 'loop-zellij')), 'loop state exists');
@@ -47,6 +62,8 @@ export async function runLoopDirectiveCheck(id) {
47
62
  else if (id === 'loop:planner') {
48
63
  assert(byId.has('loop-integration'), 'integration loop always created');
49
64
  assert(plan.graph.nodes.length >= 2, 'planner creates action plus integration loops');
65
+ assert(plan.graph.nodes.some((node) => node.route !== '$Integration' && node.maker.worker_count > 2), 'planner scales maker workers above the old hardcoded two');
66
+ assert(plan.graph.nodes.some((node) => node.route !== '$Integration' && node.checker.worker_count > 1), 'planner scales checker reviewers above the old hardcoded one');
50
67
  }
51
68
  else if (id === 'loop:decomposer') {
52
69
  const domains = decomposeRequestIntoLoopDomains(request);
@@ -68,6 +85,118 @@ export async function runLoopDirectiveCheck(id) {
68
85
  assert(result.ok, 'loop runtime produces ok graph result');
69
86
  assert(await exists(loopGraphProofPath(root, missionId)), 'graph proof exists');
70
87
  }
88
+ else if (id === 'loop:fixture-safety') {
89
+ const runtimeSource = await fs.readFile(path.join(process.cwd(), 'src/core/loops/loop-runtime.ts'), 'utf8');
90
+ const workerSource = await fs.readFile(path.join(process.cwd(), 'src/core/loops/loop-worker-runtime.ts'), 'utf8');
91
+ assert(!/noMutation\s*\?\s*\{\s*fixture:\s*true\s*\}/.test(runtimeSource), 'noMutation must not force fixture mode');
92
+ assert(workerSource.includes('decideLoopFixturePolicy'), 'fixture runtime has an explicit shared test-context policy guard');
93
+ assert(workerSource.includes('loop_fixture_runtime_forbidden'), 'fixture runtime fails closed outside test context');
94
+ assert(workerSource.includes("process.env.SKS_LOOP_RUNTIME_FIXTURE === '1'"), 'fixture runtime remains opt-in through SKS_LOOP_RUNTIME_FIXTURE');
95
+ assert(!workerSource.includes('visualLaneCount: Math.min(4'), 'zellij visual lane count must use the configurable pane cap');
96
+ const negative = await productionFixtureNegativeCheck();
97
+ assert(negative.code === 0 && negative.stdout.includes('loop_fixture_runtime_forbidden'), 'production fixture request is blocked at runtime');
98
+ }
99
+ else if (id === 'loop:worker-runtime') {
100
+ const proof = await readJson(loopProofPath(root, missionId, 'loop-zellij'));
101
+ assert(proof.maker_result.backend === 'deterministic-fixture' || proof.maker_result.backend === 'native-agent-orchestrator', 'maker backend recorded');
102
+ assert(proof.checker_result.backend === 'deterministic-fixture' || proof.checker_result.backend === 'native-agent-orchestrator', 'checker backend recorded');
103
+ assert(proof.maker_result.runtime_proof_path, 'maker runtime proof path recorded');
104
+ assert(proof.checker_result.runtime_proof_path, 'checker runtime proof path recorded');
105
+ }
106
+ else if (id === 'loop:worker-prompts') {
107
+ const prompts = await import('../core/loops/loop-worker-prompts.js');
108
+ const node = byId.get('loop-zellij');
109
+ assert(prompts.buildLoopMakerPrompt({ plan, node }).includes('Do not mutate outside the owner scope'), 'maker prompt constrains owner scope');
110
+ assert(prompts.buildLoopCheckerPrompt({ plan, node, makerArtifacts: ['maker.json'] }).includes('must not mutate source files'), 'checker prompt forbids mutation');
111
+ assert(prompts.buildLoopCheckerPrompt({ plan, node, makerArtifacts: ['maker.json'] }).includes('fresh session'), 'checker prompt requires fresh session');
112
+ }
113
+ else if (id === 'loop:runtime-real-workers' || id === 'loop:maker-checker-real') {
114
+ const proof = await readJson(loopProofPath(root, missionId, 'loop-zellij'));
115
+ assert(proof.maker_result.artifacts.length > 0, 'maker worker runtime artifacts exist');
116
+ assert(proof.checker_result.artifacts.length > 0, 'checker worker runtime artifacts exist');
117
+ assert(!proof.maker_result.artifacts.includes('fresh-checker-session'), 'placeholder checker string is not used');
118
+ }
119
+ else if (id === 'loop:checker-freshness') {
120
+ const proof = await readJson(loopProofPath(root, missionId, 'loop-zellij'));
121
+ const checker = await readJson(proof.checker_result.checker_findings[0]);
122
+ assert(checker.fresh_session === true, 'checker artifact proves fresh session');
123
+ assert(Array.isArray(checker.reviewed_maker_artifacts), 'checker reviewed maker artifacts');
124
+ assert(proof.checker_result.fresh_session === true, 'loop proof records checker freshness');
125
+ }
126
+ else if (id === 'loop:gate-registry') {
127
+ const registry = await import('../core/loops/loop-gate-registry.js');
128
+ const defs = await registry.listLoopGateDefinitions(process.cwd());
129
+ assert(defs.some((gate) => gate.id === 'gpt:final-arbiter' && gate.source === 'builtin-pseudo'), 'gpt final pseudo gate registered');
130
+ assert(await registry.resolveLoopGate(process.cwd(), 'definitely:unknown') === null, 'unknown gate does not resolve');
131
+ }
132
+ else if (id === 'loop:gate-runner-real' || id === 'loop:gate-artifacts') {
133
+ const proof = await readJson(loopProofPath(root, missionId, 'loop-zellij'));
134
+ assert(proof.gate_result.selected_gates.length > 0, 'gates selected');
135
+ assert(proof.gate_result.passed_gates.length > 0 || proof.gate_result.failed_gates.length > 0, 'gate outcomes recorded');
136
+ assert(await exists(path.join(loopRoot(root, missionId), 'loop-zellij', 'gates')), 'gate artifact directory exists');
137
+ }
138
+ else if (id === 'loop:worktree-runtime') {
139
+ assert(await exists(path.join(loopRoot(root, missionId), 'loop-zellij', 'worktree.json')), 'worktree record exists');
140
+ }
141
+ else if (id === 'loop:worktree-diff-scope') {
142
+ const mod = await import('../core/loops/loop-worktree-runtime.js');
143
+ assert(mod.enforceLoopOwnerScope(['src/core/zellij/zellij-slot-pane-renderer.ts'], byId.get('loop-zellij').owner_scope).length === 0, 'owner-scoped file passes');
144
+ assert(mod.enforceLoopOwnerScope(['README.md'], byId.get('loop-zellij').owner_scope).length > 0, 'outside owner scope blocks');
145
+ }
146
+ else if (id === 'loop:integration-merge') {
147
+ assert(await exists(path.join(loopRoot(root, missionId), 'integration-merge.json')), 'integration merge artifact exists');
148
+ }
149
+ else if (id === 'loop:integration-finalizer-real') {
150
+ const graph = await readJson(loopGraphProofPath(root, missionId));
151
+ assert(graph.integration_merge && typeof graph.integration_merge.ok === 'boolean', 'graph proof includes integration merge');
152
+ }
153
+ else if (id === 'file-lock:atomic') {
154
+ const lock = await import('../core/locks/file-lock.js');
155
+ let count = 0;
156
+ await lock.withFileLock({ lockPath: path.join(root, '.sneakoscope/locks/test.lock'), timeoutMs: 1000, staleMs: 10000 }, async () => { count += 1; });
157
+ assert(count === 1, 'file lock executes critical section');
158
+ }
159
+ else if (id === 'loop:lease-atomic') {
160
+ const node = byId.get('loop-zellij');
161
+ const lease = await acquireLoopLease(root, plan, node);
162
+ assert(lease.status === 'active' || lease.status === 'conflict', 'atomic lease returns status');
163
+ }
164
+ else if (id === 'loop:gpt-final-arbiter' || id === 'loop:integration-gpt-final') {
165
+ const mod = await import('../core/loops/loop-gpt-final-arbiter.js');
166
+ const arbiter = await mod.runLoopGptFinalArbiter({ root, plan, proofs: result.proofs, integrationMerge: { schema: 'sks.loop-integration-merge.v1', ok: true, applied_loops: [], conflict_loops: [], changed_files: ['src/core/loops/loop-runtime.ts'], blockers: [] }, forceVerdict: 'approve' });
167
+ assert(arbiter.ok && arbiter.verdict === 'approve', 'loop GPT final arbiter can approve');
168
+ }
169
+ else if (id === 'loop:checkpoint') {
170
+ assert(await exists(path.join(loopRoot(root, missionId), 'loop-zellij', 'checkpoint-latest.json')), 'latest checkpoint exists');
171
+ }
172
+ else if (id === 'loop:kill-resume' || id === 'loop:cli-kill-resume') {
173
+ const control = await import('../core/loops/loop-runtime-control.js');
174
+ await control.writeLoopKillRequest(root, missionId, 'loop-zellij');
175
+ assert(await control.shouldKillLoop(root, missionId, 'loop-zellij'), 'kill request targets loop');
176
+ }
177
+ else if (id === 'loop:real-maker-checker-blackbox') {
178
+ const proof = await readJson(loopProofPath(root, missionId, 'loop-zellij'));
179
+ assert(proof.maker_result.worker_count > 0 && proof.checker_result.worker_count > 0, 'maker/checker worker counts recorded');
180
+ assert(proof.checker_result.checker_findings.length > 0, 'checker findings artifact exists');
181
+ }
182
+ else if (id === 'naruto:loop-mesh-real-blackbox') {
183
+ assert(plan.graph.nodes.length >= 5, 'at least four domain loops plus integration are planned');
184
+ assert(result.proofs.every((proof) => proof.maker_result.artifacts.length && proof.checker_result.artifacts.length), 'worker runtime artifacts exist for every loop');
185
+ assert(result.graph_proof.integration_merge, 'integration finalizer ran');
186
+ }
187
+ else if (id === 'goal:loop-runtime-real-blackbox') {
188
+ const goalPlan = await compileGoalToLoopPlan({ root, missionId: `${missionId}-goal-real`, goalText: 'fix release cache', legacyGoalOptions: {} });
189
+ const goalResult = await runLoopPlan({ root, plan: goalPlan, parallelism: 'balanced', noMutation: true });
190
+ assert(await exists(path.join(root, '.sneakoscope', 'missions', `${missionId}-goal-real`, 'goal-compat.json')), 'goal compat artifact exists');
191
+ assert(goalResult.proofs.some((proof) => proof.maker_result.artifacts.length), 'goal loop worker runtime artifacts exist');
192
+ assert(await exists(loopGraphProofPath(root, `${missionId}-goal-real`)), 'goal graph proof exists');
193
+ }
194
+ else if (id === 'loop:status-ux') {
195
+ assert(await exists(loopGraphProofPath(root, missionId)), 'status has graph proof source');
196
+ }
197
+ else if (id === 'loop:zellij-real-runtime-ui') {
198
+ assert(renderZellijSlotPane({ slotId: 'slot-003', generationIndex: 1, loopId: 'loop-zellij', loopRole: 'maker', loopGate: 'loop:test', backend: 'fixture', patchStatus: 'fixture', verifyStatus: 'pass' }).includes('fixture loop proof'), 'zellij marks fixture proof');
199
+ }
71
200
  else if (id === 'loop:proof') {
72
201
  assert(await exists(loopProofPath(root, missionId, 'loop-zellij')), 'loop proof exists');
73
202
  }
@@ -85,6 +214,48 @@ export async function runLoopDirectiveCheck(id) {
85
214
  const node = byId.get('loop-zellij');
86
215
  const gates = await runLoopGates({ root, missionId, node, gates: node.gates });
87
216
  assert(gates.skipped_gates.includes('release:check') === false, 'gate runner avoids full release check inside loop');
217
+ const checkerDir = path.join(root, '.sneakoscope', 'missions', missionId, 'agents', 'sessions');
218
+ await fs.mkdir(checkerDir, { recursive: true });
219
+ await fs.writeFile(path.join(checkerDir, 'checker-findings.json'), JSON.stringify({ fresh_session: true, approved: true }));
220
+ const checkerGate = await runLoopGates({
221
+ root,
222
+ missionId,
223
+ node,
224
+ gates: { triage: [], local: [], checker: ['loop:checker-fresh-session'], integration: [], final: [] },
225
+ checkerArtifacts: ['sessions/checker-findings.json']
226
+ });
227
+ assert(checkerGate.ok, 'builtin checker gate resolves mission-ledger relative artifacts');
228
+ const foreignChecker = path.join(path.dirname(root), `${missionId}-foreign-checker-findings.json`);
229
+ await fs.writeFile(foreignChecker, JSON.stringify({ fresh_session: true, approved: true }));
230
+ const foreignRelative = path.relative(path.join(root, '.sneakoscope', 'missions', missionId, 'agents'), foreignChecker);
231
+ const unsafeCheckerGate = await runLoopGates({
232
+ root,
233
+ missionId,
234
+ node,
235
+ gates: { triage: [], local: [], checker: ['loop:checker-fresh-session'], integration: [], final: [] },
236
+ checkerArtifacts: [foreignRelative, foreignChecker]
237
+ });
238
+ assert(!unsafeCheckerGate.ok && unsafeCheckerGate.blockers.includes('loop_checker_fresh_session_missing'), 'builtin checker gate rejects foreign absolute and traversal artifacts');
239
+ const repoLocalChecker = path.join(root, 'repo-local-checker-findings.json');
240
+ await fs.writeFile(repoLocalChecker, JSON.stringify({ fresh_session: true, approved: true }));
241
+ const repoLocalCheckerGate = await runLoopGates({
242
+ root,
243
+ missionId,
244
+ node,
245
+ gates: { triage: [], local: [], checker: ['loop:checker-fresh-session'], integration: [], final: [] },
246
+ checkerArtifacts: ['repo-local-checker-findings.json', repoLocalChecker]
247
+ });
248
+ assert(!repoLocalCheckerGate.ok && repoLocalCheckerGate.blockers.includes('loop_checker_fresh_session_missing'), 'builtin checker gate rejects repo-local non-mission artifacts');
249
+ const symlinkChecker = path.join(checkerDir, 'checker-findings-symlink.json');
250
+ await fs.symlink(repoLocalChecker, symlinkChecker);
251
+ const symlinkCheckerGate = await runLoopGates({
252
+ root,
253
+ missionId,
254
+ node,
255
+ gates: { triage: [], local: [], checker: ['loop:checker-fresh-session'], integration: [], final: [] },
256
+ checkerArtifacts: ['sessions/checker-findings-symlink.json']
257
+ });
258
+ assert(!symlinkCheckerGate.ok && symlinkCheckerGate.blockers.includes('loop_checker_fresh_session_missing'), 'builtin checker gate rejects mission-local symlinks that escape the mission root');
88
259
  }
89
260
  else if (id === 'loop:gate-ladder') {
90
261
  const node = byId.get('loop-zellij');
@@ -108,7 +279,7 @@ export async function runLoopDirectiveCheck(id) {
108
279
  assert(docsA.status === 'active' && docsB.status === 'active', 'docs overlap is allowed when non-exclusive');
109
280
  }
110
281
  else if (id === 'naruto:loop-mesh' || id === 'naruto:loop-maker-checker') {
111
- const mesh = await runNarutoLoopMesh({ root, plan, parallelism: 'balanced' });
282
+ const mesh = await runNarutoLoopMesh({ root, plan, parallelism: 'balanced', noMutation: fixtureMode ? true : !realRuntimeMode });
112
283
  assert(mesh.proofs.every((proof) => proof.maker_result.worker_count > 0 && proof.checker_result.worker_count > 0), 'maker/checker artifacts exist for each loop');
113
284
  }
114
285
  else if (id === 'naruto:loop-worker-router') {
@@ -159,7 +330,59 @@ async function exists(file) {
159
330
  return false;
160
331
  }
161
332
  }
333
+ function throws(fn) {
334
+ try {
335
+ fn();
336
+ return false;
337
+ }
338
+ catch {
339
+ return true;
340
+ }
341
+ }
162
342
  async function readJson(file) {
163
343
  return JSON.parse(await fs.readFile(file, 'utf8'));
164
344
  }
345
+ async function productionFixtureNegativeCheck() {
346
+ const code = `
347
+ import { runLoopMakerWorkers } from './dist/core/loops/loop-worker-runtime.js';
348
+ const node = {
349
+ mission_id: 'M-production-fixture-negative',
350
+ loop_id: 'loop-production-fixture-negative',
351
+ owner_scope: { files: ['README.md'], directories: [], package_scripts: [], release_gate_ids: [], exclusive: true, collision_policy: 'handoff' },
352
+ maker: { worker_count: 1 },
353
+ checker: { worker_count: 1 },
354
+ risk: { requires_gpt_final: false },
355
+ worktree: { required: false }
356
+ };
357
+ const plan = { mission_id: 'M-production-fixture-negative' };
358
+ try {
359
+ await runLoopMakerWorkers({ root: process.cwd(), plan, node, fixture: true });
360
+ console.error('fixture unexpectedly allowed outside test context');
361
+ process.exit(1);
362
+ } catch (err) {
363
+ const message = err instanceof Error ? err.message : String(err);
364
+ if (!message.includes('loop_fixture_runtime_forbidden')) {
365
+ console.error(message);
366
+ process.exit(2);
367
+ }
368
+ console.log(message);
369
+ }
370
+ `;
371
+ return runProcess('/usr/bin/env', [
372
+ '-u', 'NODE_ENV',
373
+ '-u', 'SKS_TEST_RUNTIME_FIXTURE_ALLOWED',
374
+ '-u', 'VITEST_WORKER_ID',
375
+ '-u', 'JEST_WORKER_ID',
376
+ '-u', 'NODE_V8_COVERAGE',
377
+ 'SKS_LOOP_RUNTIME_FIXTURE=1',
378
+ process.execPath,
379
+ '--input-type=module',
380
+ '-e',
381
+ code
382
+ ], {
383
+ cwd: process.cwd(),
384
+ timeoutMs: 30000,
385
+ maxOutputBytes: 8192
386
+ });
387
+ }
165
388
  //# sourceMappingURL=loop-directive-check-lib.js.map