sneakoscope 2.0.9 → 2.0.10

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 (49) hide show
  1. package/README.md +8 -4
  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 +21 -8
  8. package/dist/cli/command-registry.js +1 -0
  9. package/dist/commands/doctor.js +18 -1
  10. package/dist/commands/zellij-slot-pane.js +26 -0
  11. package/dist/core/agents/agent-orchestrator.js +198 -7
  12. package/dist/core/agents/agent-role-config.js +92 -0
  13. package/dist/core/agents/native-cli-session-swarm.js +186 -71
  14. package/dist/core/commands/naruto-command.js +59 -4
  15. package/dist/core/doctor/doctor-readiness-matrix.js +6 -0
  16. package/dist/core/fsx.js +1 -1
  17. package/dist/core/hooks-runtime.js +4 -0
  18. package/dist/core/init.js +1 -0
  19. package/dist/core/naruto/naruto-active-pool.js +35 -2
  20. package/dist/core/naruto/naruto-concurrency-governor.js +1 -1
  21. package/dist/core/naruto/naruto-real-worker-child.js +35 -0
  22. package/dist/core/naruto/naruto-real-worker-runtime.js +121 -0
  23. package/dist/core/version.js +1 -1
  24. package/dist/core/zellij/zellij-right-column-manager.js +66 -7
  25. package/dist/core/zellij/zellij-slot-pane-renderer.js +82 -0
  26. package/dist/core/zellij/zellij-ui-mode.js +16 -0
  27. package/dist/core/zellij/zellij-worker-pane-manager.js +38 -8
  28. package/dist/scripts/agent-role-config-repair-check.js +33 -0
  29. package/dist/scripts/git-worktree-integration-primary-check.js +4 -2
  30. package/dist/scripts/git-worktree-integration-primary-runtime-check.js +20 -0
  31. package/dist/scripts/mutation-callsite-coverage-check.js +2 -1
  32. package/dist/scripts/naruto-extreme-parallelism-check.js +1 -1
  33. package/dist/scripts/naruto-extreme-parallelism-real-check.js +42 -0
  34. package/dist/scripts/naruto-real-active-pool-check.js +3 -2
  35. package/dist/scripts/naruto-real-active-pool-runtime-check.js +53 -0
  36. package/dist/scripts/naruto-zellij-dynamic-right-column-check.js +29 -2
  37. package/dist/scripts/readme-architecture-imagegen-official-check.js +4 -3
  38. package/dist/scripts/release-check-dynamic-execute.js +27 -1
  39. package/dist/scripts/release-check-dynamic.js +38 -11
  40. package/dist/scripts/release-check-stamp.js +7 -2
  41. package/dist/scripts/release-dynamic-performance-check.js +31 -1
  42. package/dist/scripts/release-gate-existence-audit.js +29 -33
  43. package/dist/scripts/release-readiness-report.js +13 -2
  44. package/dist/scripts/zellij-right-column-geometry-proof.js +155 -22
  45. package/dist/scripts/zellij-right-column-headless-overflow-check.js +22 -0
  46. package/dist/scripts/zellij-right-column-manager-check.js +4 -4
  47. package/dist/scripts/zellij-slot-only-ui-check.js +22 -0
  48. package/dist/scripts/zellij-slot-pane-renderer-check.js +38 -0
  49. package/package.json +12 -4
@@ -5,8 +5,10 @@ import { appendJsonl, ensureDir, exists, nowIso, packageRoot, readJson, writeJso
5
5
  import { fastModeEnv } from './fast-mode-policy.js';
6
6
  import { validateAgentWorkerResult } from './agent-worker-pipeline.js';
7
7
  import { closeWorkerPane, openWorkerPane } from '../zellij/zellij-worker-pane-manager.js';
8
- import { recordHeadlessWorkerInRightColumn } from '../zellij/zellij-right-column-manager.js';
8
+ import { closeWorkerInRightColumn, recordHeadlessWorkerInRightColumn } from '../zellij/zellij-right-column-manager.js';
9
9
  import { resolveProviderContext } from '../provider/provider-context.js';
10
+ import { buildZellijSlotPaneCommand } from '../zellij/zellij-slot-pane-renderer.js';
11
+ import { resolveZellijUiMode } from '../zellij/zellij-ui-mode.js';
10
12
  export const NATIVE_CLI_SESSION_SWARM_SCHEMA = 'sks.agent-native-cli-session-swarm.v1';
11
13
  export function createNativeCliSessionSwarmRecorder(root, input) {
12
14
  return new NativeCliSessionSwarmRecorder(root, input);
@@ -19,6 +21,7 @@ class NativeCliSessionSwarmRecorder {
19
21
  maxObserved = 0;
20
22
  writeLock = Promise.resolve();
21
23
  nextPaneToken = -1;
24
+ visibleZellijReservations = new Set();
22
25
  constructor(root, input) {
23
26
  this.root = root;
24
27
  this.input = input;
@@ -103,10 +106,12 @@ class NativeCliSessionSwarmRecorder {
103
106
  const stdout = fs.createWriteStream(path.join(this.root, stdoutRel), { flags: 'a' });
104
107
  const stderr = fs.createWriteStream(path.join(this.root, stderrRel), { flags: 'a' });
105
108
  const placement = String(ctx.opts.workerPlacement || this.input.workerPlacement || (this.input.backend === 'zellij' ? 'zellij-pane' : 'process'));
106
- const useZellijPane = placement === 'zellij-pane'
109
+ const zellijReservation = placement === 'zellij-pane'
107
110
  && ctx.opts.zellijPaneWorker !== false
108
111
  && (ctx.opts.zellijSessionName || this.input.missionId)
109
- && this.visibleZellijPaneCount() < this.zellijVisiblePaneCap(ctx.opts);
112
+ ? this.reserveVisibleZellijPane(ctx.opts, String(ctx.agent.session_id || ctx.agent.id || `${Date.now()}:${Math.random()}`))
113
+ : null;
114
+ const useZellijPane = Boolean(zellijReservation);
110
115
  if (useZellijPane) {
111
116
  stdout.end();
112
117
  stderr.end();
@@ -118,7 +123,8 @@ class NativeCliSessionSwarmRecorder {
118
123
  stdoutRel,
119
124
  stderrRel,
120
125
  heartbeatRel,
121
- workerDirRel
126
+ workerDirRel,
127
+ zellijReservation
122
128
  });
123
129
  }
124
130
  if (placement === 'zellij-pane' && ctx.opts.zellijPaneWorker !== false && !useZellijPane) {
@@ -174,6 +180,17 @@ class NativeCliSessionSwarmRecorder {
174
180
  record.exit_code = exit.code;
175
181
  record.signal = exit.signal;
176
182
  record.status = exit.code === 0 ? 'closed' : 'failed';
183
+ if (record.worker_placement === 'headless') {
184
+ await closeWorkerInRightColumn({
185
+ root: this.root,
186
+ projectRoot: ctx.opts.projectRoot || this.input.projectRoot || ctx.opts.cwd,
187
+ missionId: this.input.missionId,
188
+ slotId: String(ctx.agent.slot_id || ctx.agent.id || 'slot-001'),
189
+ generationIndex: Number(ctx.agent.generation_index || 1),
190
+ paneId: null,
191
+ status: record.status === 'closed' ? 'closed' : 'failed'
192
+ }).catch(() => null);
193
+ }
177
194
  const parsed = await readJson(path.join(this.root, resultRel), null).catch(() => null);
178
195
  if (!parsed) {
179
196
  record.blockers = ['native_cli_worker_result_missing'];
@@ -217,77 +234,102 @@ class NativeCliSessionSwarmRecorder {
217
234
  route: this.input.route,
218
235
  serviceTier: this.input.fastModePolicy.service_tier
219
236
  });
220
- const workerCommand = buildPaneWorkerCommand({
221
- args: input.args,
222
- stdoutPath: path.join(this.root, input.stdoutRel),
223
- stderrPath: path.join(this.root, input.stderrRel),
224
- heartbeatPath: path.join(this.root, input.heartbeatRel),
225
- header: buildPaneWorkerHeader({
237
+ const uiMode = resolveZellijUiMode(Array.isArray(input.ctx.opts.args) ? input.ctx.opts.args : [], process.env);
238
+ const workerEnv = {
239
+ ...(input.ctx.opts.env || {}),
240
+ ...fastModeEnv(this.input.fastModePolicy),
241
+ SKS_AGENT_WORKER: '1',
242
+ SKS_PIPELINE_MODE: 'agent-worker',
243
+ SKS_DISABLE_ROUTE_RECURSION: '1',
244
+ SKS_PARENT_MISSION_ID: this.input.missionId,
245
+ SKS_AGENT_SESSION_ID: String(input.ctx.agent.session_id || ''),
246
+ SKS_AGENT_SLOT_ID: slotId,
247
+ SKS_AGENT_GENERATION_INDEX: String(input.ctx.agent.generation_index || 1),
248
+ ...(input.ctx.opts.ollamaModel ? { SKS_OLLAMA_MODEL: String(input.ctx.opts.ollamaModel) } : {}),
249
+ ...(input.ctx.opts.ollamaBaseUrl ? { SKS_OLLAMA_BASE_URL: String(input.ctx.opts.ollamaBaseUrl) } : {}),
250
+ SKS_ZELLIJ_SESSION_NAME: sessionName
251
+ };
252
+ const role = String(input.ctx.agent.naruto_role || input.ctx.agent.role || input.ctx.agent.persona_id || 'worker');
253
+ const workerCommand = uiMode === 'full-debug'
254
+ ? buildPaneWorkerCommand({
255
+ args: input.args,
256
+ stdoutPath: path.join(this.root, input.stdoutRel),
257
+ stderrPath: path.join(this.root, input.stderrRel),
258
+ heartbeatPath: path.join(this.root, input.heartbeatRel),
259
+ header: buildPaneWorkerHeader({
260
+ slotId,
261
+ generationIndex: Number(input.ctx.agent.generation_index || 1),
262
+ role,
263
+ backend: this.input.backend,
264
+ provider: providerContext.provider,
265
+ serviceTier: this.input.fastModePolicy.service_tier,
266
+ worktree,
267
+ task: input.ctx.slice?.description || input.ctx.slice?.title || input.ctx.slice?.id || ''
268
+ }),
269
+ env: {
270
+ ...workerEnv,
271
+ SKS_ZELLIJ_WORKER_PANE: '1'
272
+ }
273
+ })
274
+ : buildZellijSlotPaneCommand({
275
+ cliPath: String(input.args[0] || ''),
276
+ missionId: this.input.missionId,
226
277
  slotId,
227
278
  generationIndex: Number(input.ctx.agent.generation_index || 1),
228
- role: input.ctx.agent.naruto_role || input.ctx.agent.role || input.ctx.agent.persona_id || 'worker',
279
+ artifactDir: path.join(this.root, input.workerDirRel),
229
280
  backend: this.input.backend,
230
- provider: providerContext.provider,
281
+ role,
282
+ mode: uiMode,
283
+ watch: true
284
+ });
285
+ let paneRecord;
286
+ try {
287
+ paneRecord = await openWorkerPane({
288
+ root: this.root,
289
+ missionId: this.input.missionId,
290
+ sessionName,
291
+ slotId,
292
+ generationIndex: Number(input.ctx.agent.generation_index || 1),
293
+ sessionId: String(input.ctx.agent.session_id || ''),
294
+ workerArtifactDir: input.workerDirRel,
295
+ workerCommand,
296
+ resultPath: input.resultRel,
297
+ heartbeatPath: input.heartbeatRel,
298
+ patchEnvelopePath: input.record.patch_envelope_path,
299
+ stdoutLog: input.stdoutRel,
300
+ stderrLog: input.stderrRel,
301
+ cwd: workerCwd,
302
+ providerContext,
231
303
  serviceTier: this.input.fastModePolicy.service_tier,
232
- worktree,
233
- task: input.ctx.slice?.description || input.ctx.slice?.title || input.ctx.slice?.id || ''
234
- }),
235
- env: {
236
- ...(input.ctx.opts.env || {}),
237
- ...fastModeEnv(this.input.fastModePolicy),
238
- SKS_AGENT_WORKER: '1',
239
- SKS_PIPELINE_MODE: 'agent-worker',
240
- SKS_DISABLE_ROUTE_RECURSION: '1',
241
- SKS_PARENT_MISSION_ID: this.input.missionId,
242
- SKS_AGENT_SESSION_ID: String(input.ctx.agent.session_id || ''),
243
- SKS_AGENT_SLOT_ID: slotId,
244
- SKS_AGENT_GENERATION_INDEX: String(input.ctx.agent.generation_index || 1),
245
- ...(input.ctx.opts.ollamaModel ? { SKS_OLLAMA_MODEL: String(input.ctx.opts.ollamaModel) } : {}),
246
- ...(input.ctx.opts.ollamaBaseUrl ? { SKS_OLLAMA_BASE_URL: String(input.ctx.opts.ollamaBaseUrl) } : {}),
247
- SKS_ZELLIJ_WORKER_PANE: '1',
248
- SKS_ZELLIJ_SESSION_NAME: sessionName
249
- }
250
- });
251
- let paneRecord = await openWorkerPane({
252
- root: this.root,
253
- missionId: this.input.missionId,
254
- sessionName,
255
- slotId,
256
- generationIndex: Number(input.ctx.agent.generation_index || 1),
257
- sessionId: String(input.ctx.agent.session_id || ''),
258
- workerArtifactDir: input.workerDirRel,
259
- workerCommand,
260
- resultPath: input.resultRel,
261
- heartbeatPath: input.heartbeatRel,
262
- patchEnvelopePath: input.record.patch_envelope_path,
263
- stdoutLog: input.stdoutRel,
264
- stderrLog: input.stderrRel,
265
- cwd: workerCwd,
266
- providerContext,
267
- serviceTier: this.input.fastModePolicy.service_tier,
268
- worktree: worktree ? { id: worktree.id, path: worktree.path, branch: worktree.branch } : null,
269
- backend: this.input.backend,
270
- projectRoot: input.ctx.opts.projectRoot || this.input.projectRoot || input.ctx.opts.cwd,
271
- rightColumnMode: 'spawn-on-first-worker',
272
- visiblePaneCap: this.zellijVisiblePaneCap(input.ctx.opts),
273
- dashboardSnapshot: {
274
- mode: this.input.route || '$Agent',
275
- backend_counts: { [this.input.backend]: this.input.targetActiveSlots },
276
- placement_counts: {
277
- 'zellij-pane': this.zellijVisiblePaneCap(input.ctx.opts),
278
- headless: Math.max(0, this.input.targetActiveSlots - this.zellijVisiblePaneCap(input.ctx.opts))
279
- },
280
- active_workers: this.input.targetActiveSlots,
281
- visible_panes: this.zellijVisiblePaneCap(input.ctx.opts),
282
- headless_workers: Math.max(0, this.input.targetActiveSlots - this.zellijVisiblePaneCap(input.ctx.opts)),
283
- queue_depth: Math.max(0, this.input.requestedAgents - this.input.targetActiveSlots),
284
- local_llm: { tps: 0, queue: 0 },
285
- gpt_final_status: 'pending',
286
- gate_progress: 'worker-spawn'
287
- }
288
- });
304
+ worktree: worktree ? { id: worktree.id, path: worktree.path, branch: worktree.branch } : null,
305
+ backend: this.input.backend,
306
+ uiMode,
307
+ projectRoot: input.ctx.opts.projectRoot || this.input.projectRoot || input.ctx.opts.cwd,
308
+ rightColumnMode: 'spawn-on-first-worker',
309
+ visiblePaneCap: this.zellijVisiblePaneCap(input.ctx.opts),
310
+ dashboardSnapshot: {
311
+ mode: this.input.route || '$Agent',
312
+ backend_counts: { [this.input.backend]: this.input.targetActiveSlots },
313
+ placement_counts: {
314
+ 'zellij-pane': this.zellijVisiblePaneCap(input.ctx.opts),
315
+ headless: Math.max(0, this.input.targetActiveSlots - this.zellijVisiblePaneCap(input.ctx.opts))
316
+ },
317
+ active_workers: this.input.targetActiveSlots,
318
+ visible_panes: this.zellijVisiblePaneCap(input.ctx.opts),
319
+ headless_workers: Math.max(0, this.input.targetActiveSlots - this.zellijVisiblePaneCap(input.ctx.opts)),
320
+ queue_depth: Math.max(0, this.input.requestedAgents - this.input.targetActiveSlots),
321
+ local_llm: { tps: 0, queue: 0 },
322
+ gpt_final_status: 'pending',
323
+ gate_progress: 'worker-spawn'
324
+ }
325
+ });
326
+ }
327
+ finally {
328
+ if (input.zellijReservation)
329
+ this.releaseVisibleZellijReservation(input.zellijReservation);
330
+ }
289
331
  const launchBlockers = paneRecord.blockers || [];
290
- input.record.command_line = ['zellij', '--session', sessionName, 'action', 'new-pane', '--direction', paneRecord.direction_applied, '--name', paneRecord.pane_name, '--', 'sh', '-lc', '<native-cli-worker-command>'];
332
+ input.record.command_line = ['zellij', '--session', sessionName, 'action', 'new-pane', '--direction', paneRecord.direction_applied, '--name', paneRecord.pane_name, '--', 'sh', '-lc', uiMode === 'full-debug' ? '<native-cli-worker-command>' : '<zellij-slot-pane-renderer-command>'];
291
333
  input.record.zellij_session_name = sessionName;
292
334
  input.record.zellij_pane_id = paneRecord.pane_id || null;
293
335
  input.record.zellij_pane_id_source = paneRecord.pane_id_source;
@@ -300,6 +342,8 @@ class NativeCliSessionSwarmRecorder {
300
342
  input.record.service_tier = paneRecord.service_tier;
301
343
  input.record.provider_context = paneRecord.provider_context;
302
344
  input.record.worktree = worktree;
345
+ input.record.zellij_ui_mode = uiMode;
346
+ input.record.slot_visualization = uiMode === 'full-debug' ? 'worker-command-pane' : 'zellij-slot-pane-renderer';
303
347
  input.record.status = launchBlockers.length ? 'failed' : 'running';
304
348
  input.record.blockers = launchBlockers;
305
349
  await this.record(input.record);
@@ -325,6 +369,20 @@ class NativeCliSessionSwarmRecorder {
325
369
  goal_mode_ref: input.ctx.agent.goal_mode_ref || null
326
370
  });
327
371
  }
372
+ const processRun = uiMode === 'full-debug'
373
+ ? null
374
+ : await this.spawnCompactSlotWorkerProcess({
375
+ args: input.args,
376
+ cwd: workerCwd,
377
+ env: workerEnv,
378
+ stdoutRel: input.stdoutRel,
379
+ stderrRel: input.stderrRel
380
+ });
381
+ if (processRun?.pid) {
382
+ input.record.pid = processRun.pid;
383
+ input.record.process_id = processRun.pid;
384
+ await this.record(input.record);
385
+ }
328
386
  await waitForWorkerHeartbeat(path.join(this.root, input.heartbeatRel), Number(process.env.SKS_ZELLIJ_WORKER_HEARTBEAT_TIMEOUT_MS || 30000));
329
387
  await appendJsonl(path.join(this.root, input.workerDirRel, 'zellij-worker-pane-events.jsonl'), {
330
388
  schema: 'sks.zellij-worker-pane-event.v1',
@@ -337,6 +395,7 @@ class NativeCliSessionSwarmRecorder {
337
395
  worker_artifact_dir: input.workerDirRel
338
396
  });
339
397
  const parsed = await waitForWorkerResult(path.join(this.root, input.resultRel), Number(process.env.SKS_ZELLIJ_WORKER_RESULT_TIMEOUT_MS || 120000));
398
+ const compactExit = processRun ? await processRun.wait(parsed ? 10000 : 1000) : null;
340
399
  this.active.delete(activeToken);
341
400
  input.record.closed_at = nowIso();
342
401
  const workerProcessReport = await readJson(path.join(this.root, input.workerDirRel, 'worker-process-report.json'), null).catch(() => null);
@@ -368,8 +427,10 @@ class NativeCliSessionSwarmRecorder {
368
427
  result_path: input.resultRel
369
428
  });
370
429
  }
371
- input.record.pid = Number(workerProcessReport?.pid) || null;
430
+ input.record.pid = Number(workerProcessReport?.pid || processRun?.pid) || null;
372
431
  input.record.process_id = input.record.pid;
432
+ input.record.compact_worker_exit_code = compactExit?.code ?? null;
433
+ input.record.compact_worker_signal = compactExit?.signal ?? null;
373
434
  input.record.sdk_thread_id = sdkThreadId;
374
435
  input.record.sdk_run_id = sdkRunId;
375
436
  input.record.stream_event_count = Number(workerProcessReport?.stream_event_count || workerProcessReport?.backend_router_report?.stream_event_count || 0);
@@ -479,6 +540,46 @@ class NativeCliSessionSwarmRecorder {
479
540
  visibleZellijPaneCount() {
480
541
  return this.records.filter((row) => row.scaling_primitive === 'native_cli_process_in_zellij_worker_pane' && (row.status === 'launching' || row.status === 'running')).length;
481
542
  }
543
+ reserveVisibleZellijPane(opts = {}, token) {
544
+ const cap = this.zellijVisiblePaneCap(opts);
545
+ if (this.visibleZellijPaneCount() + this.visibleZellijReservations.size >= cap)
546
+ return null;
547
+ this.visibleZellijReservations.add(token);
548
+ return token;
549
+ }
550
+ releaseVisibleZellijReservation(token) {
551
+ this.visibleZellijReservations.delete(token);
552
+ }
553
+ async spawnCompactSlotWorkerProcess(input) {
554
+ const stdout = fs.createWriteStream(path.join(this.root, input.stdoutRel), { flags: 'a' });
555
+ const stderr = fs.createWriteStream(path.join(this.root, input.stderrRel), { flags: 'a' });
556
+ const child = spawn(process.execPath, input.args, {
557
+ cwd: input.cwd,
558
+ env: {
559
+ ...process.env,
560
+ ...Object.fromEntries(Object.entries(input.env).filter(([, value]) => value != null).map(([key, value]) => [key, String(value)]))
561
+ },
562
+ stdio: ['ignore', 'pipe', 'pipe']
563
+ });
564
+ child.stdout?.pipe(stdout);
565
+ child.stderr?.pipe(stderr);
566
+ const exitPromise = new Promise((resolve) => {
567
+ child.on('close', (code, signal) => {
568
+ stdout.end();
569
+ stderr.end();
570
+ resolve({ code, signal });
571
+ });
572
+ child.on('error', () => {
573
+ stdout.end();
574
+ stderr.end();
575
+ resolve({ code: 1, signal: null });
576
+ });
577
+ });
578
+ return {
579
+ pid: child.pid || null,
580
+ wait: async (timeoutMs) => waitForChildExit(child, exitPromise, timeoutMs)
581
+ };
582
+ }
482
583
  }
483
584
  export function buildPaneWorkerCommand(input) {
484
585
  const envPrefix = Object.entries(input.env)
@@ -555,4 +656,18 @@ function normalizeWorkerWorktree(value) {
555
656
  main_repo_root: value?.main_repo_root == null ? null : String(value.main_repo_root)
556
657
  };
557
658
  }
659
+ async function waitForChildExit(child, exitPromise, timeoutMs) {
660
+ let timer = null;
661
+ const timeout = new Promise((resolve) => {
662
+ timer = setTimeout(() => {
663
+ if (!child.killed)
664
+ child.kill();
665
+ resolve({ code: null, signal: 'SIGTERM' });
666
+ }, Math.max(1000, timeoutMs));
667
+ });
668
+ const result = await Promise.race([exitPromise, timeout]);
669
+ if (timer)
670
+ clearTimeout(timer);
671
+ return result;
672
+ }
558
673
  //# sourceMappingURL=native-cli-session-swarm.js.map
@@ -11,6 +11,7 @@ import { buildNarutoWorkGraph } from '../naruto/naruto-work-graph.js';
11
11
  import { buildNarutoRoleDistribution } from '../naruto/naruto-role-policy.js';
12
12
  import { decideNarutoConcurrency } from '../naruto/naruto-concurrency-governor.js';
13
13
  import { runNarutoActivePool, runNarutoRealActivePool } from '../naruto/naruto-active-pool.js';
14
+ import { collectActualNarutoWorker, spawnActualNarutoWorker } from '../naruto/naruto-real-worker-runtime.js';
14
15
  import { buildNarutoVerificationDag } from '../naruto/naruto-verification-dag.js';
15
16
  import { buildNarutoGptFinalPack } from '../naruto/naruto-gpt-final-pack.js';
16
17
  import { planNarutoZellijDashboard } from '../zellij/zellij-naruto-dashboard.js';
@@ -112,11 +113,29 @@ async function narutoRun(parsed) {
112
113
  const activeSlots = Math.max(1, Math.min(roster.agent_count, parsed.concurrency || Math.max(governor.safe_active_workers, backendMinimum), safe.cap));
113
114
  const zellijVisiblePanes = Math.max(1, Math.min(activeSlots, governor.safe_zellij_visible_panes));
114
115
  const activePool = await runNarutoActivePool({ graph: workGraph, governor: { ...governor, safe_active_workers: activeSlots } });
116
+ const realRuntimeWorktreePolicy = schedulerBackend === 'fake'
117
+ ? {
118
+ mode: 'patch-envelope-only',
119
+ required: false,
120
+ main_repo_root: worktreePolicy.main_repo_root,
121
+ worktree_root: null,
122
+ fallback_reason: 'fake_backend_fixture_skips_real_worktree_cleanup'
123
+ }
124
+ : worktreePolicy;
115
125
  const realActivePool = await runNarutoRealActivePool({
116
126
  graph: workGraph,
117
127
  governor: { ...governor, safe_active_workers: activeSlots },
118
- spawnWorker: async (item, placement) => ({ id: item.id, item, placement, started_at: Date.now() }),
119
- collectWorker: async (handle) => ({ id: handle.id, ok: true, item: handle.item, placement: handle.placement, completed_at: Date.now() }),
128
+ spawnWorker: async (item, placement) => spawnActualNarutoWorker({
129
+ root,
130
+ missionId: mission.id,
131
+ item,
132
+ placement,
133
+ backend: schedulerBackend,
134
+ worktreePolicy: realRuntimeWorktreePolicy,
135
+ zellijSessionName: `sks-${mission.id}`,
136
+ visiblePaneCap: zellijVisiblePanes
137
+ }),
138
+ collectWorker: async (handle) => collectActualNarutoWorker(handle),
120
139
  enqueueVerification: async () => undefined,
121
140
  updateDashboard: async () => undefined
122
141
  });
@@ -256,14 +275,20 @@ async function narutoRun(parsed) {
256
275
  completed_count: activePool.completed_count,
257
276
  real_runtime: {
258
277
  ok: realActivePool.ok,
278
+ active_cap: realActivePool.active_cap,
259
279
  max_observed_active_workers: realActivePool.max_observed_active_workers,
280
+ average_active_workers: realActivePool.average_active_workers,
281
+ active_pool_utilization: realActivePool.active_pool_utilization,
260
282
  refill_latency_ms_p95: realActivePool.refill_latency_ms_p95,
261
- worker_lifecycle: realActivePool.worker_lifecycle
283
+ visible_workers: realActivePool.visible_workers,
284
+ headless_workers: realActivePool.headless_workers,
285
+ worker_lifecycle_count: realActivePool.worker_lifecycle.length,
286
+ worker_lifecycle_sample: realActivePool.worker_lifecycle.slice(0, 5)
262
287
  }
263
288
  },
264
289
  local_worker: localWorkerSummary,
265
290
  proof: result.proof?.status || 'missing',
266
- run: result,
291
+ run: compactNarutoRunResult(result),
267
292
  zellij: null
268
293
  };
269
294
  summary.zellij = liveZellij;
@@ -280,6 +305,36 @@ async function narutoRun(parsed) {
280
305
  console.log('Zellij: optional live panes unavailable (' + ((summary.zellij.warnings || []).join('; ') || summary.zellij.capability?.status || 'unknown') + ')');
281
306
  });
282
307
  }
308
+ function compactNarutoRunResult(result) {
309
+ return {
310
+ schema: result?.schema || 'sks.agent-run.v1',
311
+ ok: result?.ok === true,
312
+ mission_id: result?.mission_id || null,
313
+ route: result?.route || NARUTO_ROUTE,
314
+ backend: result?.backend || null,
315
+ target_active_slots: result?.target_active_slots ?? null,
316
+ proof: result?.proof ? {
317
+ ok: result.proof.ok === true,
318
+ status: result.proof.status || null,
319
+ blockers: result.proof.blockers || []
320
+ } : null,
321
+ scheduler: result?.scheduler ? {
322
+ state: {
323
+ completed_count: result.scheduler.state?.completed_count ?? result.scheduler.completed_count ?? null,
324
+ failed_count: result.scheduler.state?.failed_count ?? result.scheduler.failed_count ?? null,
325
+ blocked_count: result.scheduler.state?.blocked_count ?? result.scheduler.blocked_count ?? null,
326
+ max_observed_active_slots: result.scheduler.state?.max_observed_active_slots ?? result.scheduler.max_observed_active_slots ?? null
327
+ }
328
+ } : null,
329
+ artifacts: {
330
+ ledger_root: result?.ledger_root || null,
331
+ proof: 'agent-proof-evidence.json',
332
+ scheduler: 'agent-scheduler-state.json',
333
+ native_cli_session_swarm: 'agent-native-cli-session-swarm.json',
334
+ naruto_real_active_pool: 'naruto-real-active-pool.json'
335
+ }
336
+ };
337
+ }
283
338
  function summarizeNarutoLocalWorkerResult(localWorker, result) {
284
339
  const backendCounts = {};
285
340
  const rows = Array.isArray(result?.results) ? result.results : [];
@@ -62,6 +62,11 @@ export function buildDoctorReadinessMatrix(input = {}) {
62
62
  warnings.add('local_llm_degraded');
63
63
  if (localModel.enabled === true && localStatus === 'blocked')
64
64
  warnings.add('local_llm_blocked_worker_tier_disabled');
65
+ const agentRoleConfig = input.agent_role_config || {};
66
+ if (agentRoleConfig.ok === false)
67
+ blockers.add('agent_role_config_repair_failed');
68
+ if (Array.isArray(agentRoleConfig.missing) && agentRoleConfig.missing.length && agentRoleConfig.apply !== true)
69
+ warnings.add('agent_role_config_missing_repair_available');
65
70
  const localCollaborationPolicy = resolveLocalCollaborationPolicy({ mode: input.local_collaboration?.mode || null });
66
71
  const gptFinalAvailable = input.local_collaboration?.gpt_final_arbiter_available === undefined
67
72
  ? codexBinOk
@@ -126,6 +131,7 @@ export function buildDoctorReadinessMatrix(input = {}) {
126
131
  worker_tier_enabled: localModel.enabled === true && localStatus === 'verified',
127
132
  blockers: normalizeList(localModel.blockers)
128
133
  },
134
+ agent_role_config: agentRoleConfig,
129
135
  ready: blockers.size === 0 && cliReady,
130
136
  primary_blocker: [...blockers][0] || null,
131
137
  blockers: [...blockers],
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.9';
8
+ export const PACKAGE_VERSION = '2.0.10';
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() {
@@ -25,6 +25,10 @@ const STOP_REPEAT_GUARD_WINDOW_MS = 10 * 60 * 1000;
25
25
  const STOP_REPEAT_GUARD_MAX_ENTRIES = 25;
26
26
  const DEFAULT_STOP_REPEAT_GUARD_LIMIT = 2;
27
27
  const CODEX_GIT_ACTION_STOP_TTL_MS = 15 * 60 * 1000;
28
+ const UPDATE_CHECK_HOOK_INVOCATION_POLICY = 'function-only:no-runSksUpdateCheck-call-in-hooks';
29
+ // Update checks stay function-only in hooks: the policy marker above is checked
30
+ // by release readiness so ordinary Codex hook flow cannot grow a hidden update
31
+ // prompt path.
28
32
  async function loadHookPayload() {
29
33
  const raw = await readStdin();
30
34
  try {
package/dist/core/init.js CHANGED
@@ -1257,6 +1257,7 @@ async function removeDirIfEmpty(dir) {
1257
1257
  }
1258
1258
  async function installCodexAgents(root) {
1259
1259
  const agents = {
1260
+ 'analysis-scout.toml': `name = "analysis_scout"\ndescription = "Read-only SKS analysis scout retained for stale Codex agent-role config repair."\nmodel = "gpt-5.5"\nmodel_reasoning_effort = "medium"\nsandbox_mode = "read-only"\ndeveloper_instructions = """\nYou are an SKS read-only analysis scout.\nDo not edit files.\nInspect only the assigned source paths and return concise source-backed findings.\n"""\n`,
1260
1261
  'native-agent-intake.toml': `name = "native_agent"\ndescription = "Read-only Team native agent. Maps one independent repo/docs/tests/API/risk/user-friction slice and returns TriWiki-ready source-backed findings before debate starts."\nmodel = "gpt-5.5"\nmodel_reasoning_effort = "medium"\nsandbox_mode = "read-only"\ndeveloper_instructions = """\nYou are an SKS Team native agent.\nDo not edit files.\nOwn exactly one investigation slice assigned by the parent orchestrator.\nUse the mission roster or worker inbox reasoning_effort when the host exposes it; simple bounded work can use low, tool-heavy runtime work medium, and knowledge/research/safety/release work high or xhigh. Default to medium only when no assignment is visible.\nMap relevant source files, docs, tests, APIs, DB or safety risks, UX friction, and likely implementation boundaries.\nReturn concise source-backed claims suitable for team-analysis.md and TriWiki ingestion: claim, source path, evidence hash or quoted anchor, risk, confidence, and recommended implementation slice.\nDo not debate the final plan and do not implement code.\nAlso return a concise LIVE_EVENT line that the parent can record with sks team event.\n"""\n`,
1261
1262
  'team-consensus.toml': `name = "team_consensus"\ndescription = "Planning and debate specialist for SKS Team mode. Maps options, constraints, role-persona risks, and proposes the agreed objective before implementation starts."\nmodel = "gpt-5.5"\nmodel_reasoning_effort = "high"\nsandbox_mode = "read-only"\ndeveloper_instructions = """\nYou are the SKS Team consensus specialist.\nDo not edit files.\nUse the mission roster reasoning_effort when the host exposes it; planning has a medium minimum, with high or xhigh for knowledge, safety, release, research, or recovery work.\nMap the affected code paths, viable approaches, constraints, risks, and acceptance criteria.\nRun the debate as role-persona synthesis: final users are low-context, self-interested, stubborn, and inconvenience-averse; executors are capable developers; reviewers are strict.\nArgue for the smallest coherent objective that can be handed to a fresh executor_N development team.\nPlan for at least ${MIN_TEAM_REVIEWER_LANES} independent reviewer/QA validation lanes before integration or final.\nReturn: recommended objective, rejected alternatives, implementation slices, required reviewers, user-friction risks, and unresolved risks.\nAlso return a concise LIVE_EVENT line that the parent can record with sks team event.\n"""\n`,
1262
1263
  'implementation-worker.toml': `name = "implementation_worker"\ndescription = "Implementation specialist for SKS Team mode. Owns one bounded write set and coordinates with other executor_N workers."\nmodel = "gpt-5.5"\nmodel_reasoning_effort = "medium"\nsandbox_mode = "workspace-write"\ndeveloper_instructions = """\nYou are an SKS Team executor/developer in the fresh development bundle.\nYou are not alone in the codebase. Other executor_N workers may be editing disjoint files.\nUse the mission roster or worker inbox reasoning_effort when the host exposes it; simple bounded changes can use low, tool-heavy implementation medium, and safety/release/DB work high.\nOnly edit the files or module slice assigned to you.\nDo not revert or overwrite edits made by others.\nRead local patterns first, make the smallest correct change, avoid adding user friction, run focused verification for your slice, and report changed paths plus evidence.\nDo not create fallback implementation code, substitute behavior, mock behavior, or compatibility shims unless the user or sealed decision contract explicitly requested them.\nRespect all SKS hooks, DB safety rules, no-question run rules, and H-Proof completion gates.\nAlso return concise LIVE_EVENT lines for started, blocked, changed files, verification, and final result so the parent can record them.\n"""\n`,
@@ -29,7 +29,7 @@ export async function runNarutoRealActivePool(input) {
29
29
  visibleRunning += 1;
30
30
  const handle = await input.spawnWorker(item, placement);
31
31
  active.set(handle.id, handle);
32
- lifecycle.push({ work_item_id: item.id, placement: placement.placement, started_at: handle.started_at, completed_at: null, ok: null });
32
+ lifecycle.push({ work_item_id: item.id, placement: placement.placement, pid: handle.pid || null, worker_artifact_dir: handle.worker_artifact_dir || null, started_at: handle.started_at, completed_at: null, ok: null });
33
33
  launched += 1;
34
34
  await input.updateDashboard({ event_type: 'worker_spawned', work_item_id: item.id, active_workers: active.size, pending_workers: pending.length, completed_workers: completed.size, placement });
35
35
  }
@@ -40,7 +40,7 @@ export async function runNarutoRealActivePool(input) {
40
40
  }
41
41
  maxObserved = Math.max(maxObserved, active.size);
42
42
  timeline.push({ tick, active: active.size, pending: pending.length, completed: completed.size, event: launched ? 'refill' : 'wait' });
43
- const done = [...active.values()].slice(0, Math.max(1, Math.ceil(active.size / 2)));
43
+ const done = await nextCollectableWorkers(active);
44
44
  if (!done.length)
45
45
  break;
46
46
  for (const handle of done) {
@@ -53,6 +53,8 @@ export async function runNarutoRealActivePool(input) {
53
53
  if (row) {
54
54
  row.completed_at = result.completed_at;
55
55
  row.ok = result.ok;
56
+ row.pid = result.pid || row.pid || null;
57
+ row.worker_artifact_dir = result.worker_artifact_dir || row.worker_artifact_dir || null;
56
58
  }
57
59
  await input.updateDashboard({ event_type: 'worker_completed', work_item_id: result.item.id, active_workers: active.size, pending_workers: pending.length, completed_workers: completed.size, placement: result.placement });
58
60
  if (result.item.verification_required) {
@@ -66,16 +68,32 @@ export async function runNarutoRealActivePool(input) {
66
68
  }
67
69
  await input.updateDashboard({ event_type: 'pool_drained', active_workers: active.size, pending_workers: pending.length, completed_workers: completed.size });
68
70
  const failedCount = [...completed.values()].filter((result) => !result.ok).length;
71
+ const activeSamples = timeline.map((row) => row.active);
72
+ const averageActiveWorkers = activeSamples.length
73
+ ? activeSamples.reduce((sum, value) => sum + value, 0) / activeSamples.length
74
+ : 0;
75
+ const saturatedSamples = timeline
76
+ .filter((row) => row.pending + row.active >= safeActiveWorkers)
77
+ .map((row) => row.active);
78
+ const averageSaturatedActiveWorkers = saturatedSamples.length
79
+ ? saturatedSamples.reduce((sum, value) => sum + value, 0) / saturatedSamples.length
80
+ : averageActiveWorkers;
81
+ const utilizationDenominator = Math.max(1, safeActiveWorkers);
82
+ const activePoolUtilization = Math.min(1, averageSaturatedActiveWorkers / utilizationDenominator);
83
+ const enoughWorkForUtilization = input.graph.total_work_items >= safeActiveWorkers * 2;
69
84
  const blockers = [
70
85
  ...(pending.length ? ['naruto_real_active_pool_pending_not_drained'] : []),
71
86
  ...(active.size ? ['naruto_real_active_pool_active_not_drained'] : []),
72
87
  ...(maxObserved > safeActiveWorkers ? ['naruto_real_active_pool_exceeded_safe_workers'] : []),
88
+ ...(enoughWorkForUtilization && maxObserved < Math.ceil(safeActiveWorkers * 0.8) ? ['naruto_real_active_pool_underutilized'] : []),
89
+ ...(enoughWorkForUtilization && activePoolUtilization < 0.8 ? ['naruto_real_active_pool_low_sustained_utilization'] : []),
73
90
  ...[...completed.values()].flatMap((result) => result.blockers || [])
74
91
  ];
75
92
  return {
76
93
  schema: 'sks.naruto-active-pool.v1',
77
94
  ok: blockers.length === 0,
78
95
  runtime_mode: 'real-worker-lifecycle',
96
+ active_cap: safeActiveWorkers,
79
97
  safe_active_workers: safeActiveWorkers,
80
98
  total_work_items: input.graph.total_work_items,
81
99
  completed_count: completed.size,
@@ -85,12 +103,27 @@ export async function runNarutoRealActivePool(input) {
85
103
  duplicate_execution_count: 0,
86
104
  conflict_items_enqueued: 0,
87
105
  max_observed_write_lease_conflicts: 0,
106
+ average_active_workers: Number(averageSaturatedActiveWorkers.toFixed(4)),
107
+ active_pool_utilization: Number(activePoolUtilization.toFixed(4)),
108
+ visible_workers: lifecycle.filter((row) => row.placement === 'zellij-pane').length,
109
+ headless_workers: lifecycle.filter((row) => row.placement === 'headless').length,
88
110
  refill_latency_ms_p95: percentile(refillLatencies, 0.95),
89
111
  worker_lifecycle: lifecycle,
90
112
  timeline,
91
113
  blockers
92
114
  };
93
115
  }
116
+ async function nextCollectableWorkers(active) {
117
+ const handles = [...active.values()];
118
+ const handlesWithExit = handles.filter((handle) => typeof handle.exit?.then === 'function');
119
+ if (!handlesWithExit.length)
120
+ return handles.slice(0, Math.max(1, Math.ceil(active.size / 2)));
121
+ const completed = await Promise.race(handlesWithExit.map(async (handle) => {
122
+ await handle.exit.catch(() => undefined);
123
+ return handle;
124
+ }));
125
+ return completed ? [completed] : [];
126
+ }
94
127
  export async function runNarutoActivePool(input) {
95
128
  const base = simulateNarutoActivePool(input);
96
129
  const allocations = [];
@@ -7,7 +7,7 @@ export function decideNarutoConcurrency(input = {}) {
7
7
  const pending = normalizeNonNegativeInt(input.pendingWorkQueueSize, totalWorkItems);
8
8
  const leaseConflicts = normalizeNonNegativeInt(input.activeLeaseConflicts, 0);
9
9
  const hardware = probeHardwareCapacity(input.hardware || {});
10
- const zellijVisiblePaneCap = normalizePositiveInt(input.zellijVisiblePaneCap, Math.min(12, Math.max(4, Math.floor(hardware.terminal_rows / 3))));
10
+ const zellijVisiblePaneCap = normalizePositiveInt(input.zellijVisiblePaneCap, Math.min(8, Math.max(4, Math.floor(hardware.terminal_rows / 5))));
11
11
  const backend = String(input.backend || 'codex-sdk');
12
12
  const freeGb = hardware.free_memory_bytes / (1024 * 1024 * 1024);
13
13
  const totalGb = hardware.total_memory_bytes / (1024 * 1024 * 1024);
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import { nowIso, readJson, writeJsonAtomic } from '../fsx.js';
4
+ async function main() {
5
+ const intakePath = process.argv[2];
6
+ if (!intakePath)
7
+ throw new Error('naruto worker intake path is required');
8
+ const intake = await readJson(intakePath, null);
9
+ if (!intake?.result_path || !intake?.heartbeat_path || !intake?.item?.id) {
10
+ throw new Error('naruto worker intake is invalid');
11
+ }
12
+ await fs.appendFile(intake.heartbeat_path, `${JSON.stringify({
13
+ schema: 'sks.naruto-actual-worker-heartbeat.v1',
14
+ ts: nowIso(),
15
+ item_id: intake.item.id,
16
+ status: 'running'
17
+ })}\n`);
18
+ await new Promise((resolve) => setTimeout(resolve, 25));
19
+ await writeJsonAtomic(intake.result_path, {
20
+ schema: 'sks.naruto-actual-worker-result.v1',
21
+ ok: true,
22
+ generated_at: nowIso(),
23
+ item_id: intake.item.id,
24
+ placement: intake.placement,
25
+ backend: intake.backend,
26
+ worktree_path: intake.worktree_path
27
+ });
28
+ }
29
+ main().then(() => {
30
+ process.exit(0);
31
+ }).catch((err) => {
32
+ console.error(err?.message || String(err));
33
+ process.exit(1);
34
+ });
35
+ //# sourceMappingURL=naruto-real-worker-child.js.map