sneakoscope 2.0.11 → 2.0.12

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 (44) hide show
  1. package/README.md +5 -3
  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 +11 -8
  8. package/dist/core/agents/agent-orchestrator.js +279 -1
  9. package/dist/core/agents/agent-scheduler.js +12 -1
  10. package/dist/core/agents/agent-slot-pane-binding-proof.js +3 -3
  11. package/dist/core/agents/agent-work-queue.js +26 -2
  12. package/dist/core/agents/agent-worker-pipeline.js +2 -0
  13. package/dist/core/agents/native-cli-session-swarm.js +2 -2
  14. package/dist/core/commands/naruto-command.js +104 -51
  15. package/dist/core/fsx.js +1 -1
  16. package/dist/core/git/git-worktree-merge-queue.js +34 -14
  17. package/dist/core/naruto/naruto-rebalance-policy.js +15 -3
  18. package/dist/core/naruto/naruto-work-graph.js +13 -0
  19. package/dist/core/version.js +1 -1
  20. package/dist/core/zellij/zellij-slot-column-anchor.js +163 -4
  21. package/dist/core/zellij/zellij-worker-pane-manager.js +13 -7
  22. package/dist/scripts/agent-real-codex-in-zellij-worker-pane-check.js +8 -2
  23. package/dist/scripts/agent-slot-pane-binding-proof-check.js +4 -4
  24. package/dist/scripts/codex-sdk-release-review-pipeline-check.js +2 -1
  25. package/dist/scripts/codex-sdk-zellij-pane-binding-check.js +2 -2
  26. package/dist/scripts/git-worktree-cross-rebase-check.js +13 -1
  27. package/dist/scripts/git-worktree-merge-queue-check.js +1 -0
  28. package/dist/scripts/local-collab-worktree-gpt-final-apply-policy-check.js +63 -0
  29. package/dist/scripts/naruto-actual-worker-control-plane-check.js +30 -3
  30. package/dist/scripts/naruto-allocation-runtime-wiring-check.js +92 -0
  31. package/dist/scripts/naruto-orchestrator-runtime-source-check.js +65 -6
  32. package/dist/scripts/naruto-rebalance-policy-check.js +15 -2
  33. package/dist/scripts/naruto-shadow-clone-swarm-check.js +1 -1
  34. package/dist/scripts/release-dag-full-coverage-check.js +4 -0
  35. package/dist/scripts/release-real-check.js +258 -77
  36. package/dist/scripts/zellij-first-slot-down-stack-check.js +1 -1
  37. package/dist/scripts/zellij-first-slot-down-stack-real-check.js +344 -4
  38. package/dist/scripts/zellij-right-column-manager-check.js +1 -1
  39. package/dist/scripts/zellij-slot-column-anchor-check.js +23 -2
  40. package/dist/scripts/zellij-slot-only-ui-check.js +3 -1
  41. package/dist/scripts/zellij-slot-renderer-proof-semantics-check.js +59 -0
  42. package/dist/scripts/zellij-worker-pane-manager-check.js +23 -1
  43. package/dist/scripts/zellij-worker-pane-real-ui-blackbox.js +21 -4
  44. package/package.json +5 -2
@@ -94,7 +94,7 @@ async function narutoRun(parsed) {
94
94
  const localWorker = await resolveNarutoLocalWorkerMode(parsed);
95
95
  const schedulerBackend = localWorker.auto_select_eligible ? 'ollama' : parsed.backend;
96
96
  const safe = systemSafeNarutoConcurrency({ backend: schedulerBackend });
97
- const workGraph = buildNarutoWorkGraph({
97
+ const baseWorkGraph = buildNarutoWorkGraph({
98
98
  prompt: parsed.prompt,
99
99
  requestedClones: roster.agent_count,
100
100
  totalWorkItems: parsed.workItems,
@@ -104,9 +104,21 @@ async function narutoRun(parsed) {
104
104
  maxActiveWorkers: parsed.concurrency || safe.cap,
105
105
  worktreePolicy
106
106
  });
107
+ const baseRoleDistribution = buildNarutoRoleDistribution(baseWorkGraph.work_items, { readonly: parsed.readonly });
108
+ const allocationWorkers = buildNarutoAllocationWorkers(baseWorkGraph, baseRoleDistribution, roster);
109
+ const allocationAssignments = allocateNarutoTasksToWorkers(baseWorkGraph.work_items, allocationWorkers);
110
+ const workGraph = buildNarutoWorkGraph({
111
+ prompt: parsed.prompt,
112
+ requestedClones: roster.agent_count,
113
+ totalWorkItems: parsed.workItems,
114
+ readonly: parsed.readonly,
115
+ writeCapable,
116
+ leaseBasePath: patchEnvelopeBasePath,
117
+ maxActiveWorkers: parsed.concurrency || safe.cap,
118
+ worktreePolicy,
119
+ allocationAssignments
120
+ });
107
121
  const roleDistribution = buildNarutoRoleDistribution(workGraph.work_items, { readonly: parsed.readonly });
108
- const allocationWorkers = buildNarutoAllocationWorkers(workGraph, roleDistribution, roster);
109
- const allocationAssignments = allocateNarutoTasksToWorkers(workGraph.work_items, allocationWorkers);
110
122
  const allocationPolicy = {
111
123
  schema: 'sks.naruto-allocation-policy.v1',
112
124
  generated_at: nowIso(),
@@ -135,7 +147,7 @@ async function narutoRun(parsed) {
135
147
  blockers: allocationWorkers.length ? [] : ['naruto_allocation_workers_missing']
136
148
  };
137
149
  const rebalanceDecisions = rebalanceNarutoReadyWork({
138
- tasks: workGraph.work_items.map((item) => ({ ...item, owner: null, status: 'pending' })),
150
+ tasks: workGraph.work_items.map((item) => ({ ...item, status: 'pending' })),
139
151
  workers: allocationWorkers.map((worker) => ({ ...worker, alive: true, state: 'idle' })),
140
152
  completedTaskIds: [],
141
153
  reclaimedTaskIds: []
@@ -159,52 +171,38 @@ async function narutoRun(parsed) {
159
171
  const activeSlots = Math.max(1, Math.min(roster.agent_count, parsed.concurrency || Math.max(governor.safe_active_workers, backendMinimum), safe.cap));
160
172
  const zellijVisiblePanes = Math.max(1, Math.min(activeSlots, governor.safe_zellij_visible_panes));
161
173
  const activePool = await runNarutoActivePool({ graph: workGraph, governor: { ...governor, safe_active_workers: activeSlots } });
162
- const realRuntimeSmokeGraph = buildNarutoWorkGraph({
163
- prompt: parsed.prompt,
164
- requestedClones: Math.min(2, roster.agent_count),
165
- totalWorkItems: Math.min(2, workGraph.total_work_items),
166
- readonly: true,
167
- writeCapable: false,
168
- leaseBasePath: patchEnvelopeBasePath,
169
- maxActiveWorkers: Math.min(2, activeSlots),
170
- worktreePolicy: {
171
- mode: 'patch-envelope-only',
172
- required: false,
173
- main_repo_root: worktreePolicy.main_repo_root,
174
- worktree_root: null,
175
- fallback_reason: 'pre_run_smoke_never_owns_production_runtime'
176
- }
177
- });
178
- const realRuntimeWorktreePolicy = {
179
- mode: 'patch-envelope-only',
180
- required: false,
181
- main_repo_root: worktreePolicy.main_repo_root,
182
- worktree_root: null,
183
- fallback_reason: 'pre_run_smoke_never_owns_production_runtime'
184
- };
185
- const realActivePool = await runNarutoRealActivePool({
186
- graph: realRuntimeSmokeGraph,
187
- governor: { ...governor, safe_active_workers: Math.min(2, activeSlots), safe_zellij_visible_panes: Math.min(1, zellijVisiblePanes) },
188
- spawnWorker: async (item, placement) => spawnActualNarutoWorker({
174
+ const runPreRunSmoke = parsed.smoke === true || process.env.SKS_NARUTO_PRE_RUN_SMOKE === '1';
175
+ const realActivePoolSmoke = runPreRunSmoke
176
+ ? await runNarutoControlPlaneSmoke({
189
177
  root,
190
178
  missionId: mission.id,
191
- item,
192
- placement,
193
- backend: 'fake',
194
- worktreePolicy: realRuntimeWorktreePolicy,
195
- zellijSessionName: `sks-${mission.id}`,
196
- visiblePaneCap: zellijVisiblePanes
197
- }),
198
- collectWorker: async (handle) => collectActualNarutoWorker(handle),
199
- enqueueVerification: async () => undefined,
200
- updateDashboard: async () => undefined
201
- });
202
- const realActivePoolSmoke = {
203
- ...realActivePool,
204
- runtime_source_of_truth: 'pre_run_smoke_only',
205
- production_runtime_source_of_truth: 'agent-orchestrator-scheduler',
206
- smoke_graph_total_work_items: realRuntimeSmokeGraph.total_work_items
207
- };
179
+ prompt: parsed.prompt,
180
+ rosterCount: roster.agent_count,
181
+ totalWorkItems: workGraph.total_work_items,
182
+ patchEnvelopeBasePath,
183
+ worktreePolicy,
184
+ governor,
185
+ activeSlots,
186
+ zellijVisiblePanes
187
+ })
188
+ : {
189
+ schema: 'sks.naruto-active-pool.v1',
190
+ ok: true,
191
+ status: 'skipped',
192
+ runtime_source_of_truth: 'agent-orchestrator-scheduler',
193
+ production_runtime_source_of_truth: 'agent-orchestrator-scheduler',
194
+ fallback_reason: 'pre_run_smoke_never_owns_production_runtime',
195
+ reason: 'pre_run_smoke_disabled_for_production',
196
+ active_cap: 0,
197
+ max_observed_active_workers: 0,
198
+ average_active_workers: 0,
199
+ active_pool_utilization: 0,
200
+ refill_latency_ms_p95: 0,
201
+ visible_workers: 0,
202
+ headless_workers: 0,
203
+ worker_lifecycle: [],
204
+ smoke_graph_total_work_items: 0
205
+ };
208
206
  const verificationDag = buildNarutoVerificationDag(workGraph, { cwd: root });
209
207
  const gptFinalPack = buildNarutoGptFinalPack({
210
208
  missionId: mission.id,
@@ -284,7 +282,8 @@ async function narutoRun(parsed) {
284
282
  concurrency: activeSlots,
285
283
  targetActiveSlots: activeSlots,
286
284
  visualLaneCount: zellijVisiblePanes,
287
- desiredWorkItemCount: parsed.workItems,
285
+ desiredWorkItemCount: workGraph.total_work_items,
286
+ minimumWorkItems: workGraph.total_work_items,
288
287
  maxAgentCount: MAX_NARUTO_AGENT_COUNT,
289
288
  narutoMode: true,
290
289
  clones: roster.agent_count,
@@ -307,6 +306,9 @@ async function narutoRun(parsed) {
307
306
  noFast: false,
308
307
  writeMode: writeCapable ? parsed.writeMode || 'parallel' : 'off',
309
308
  gitWorktreePolicy: worktreePolicy,
309
+ narutoWorkGraph: workGraph,
310
+ narutoAllocationPolicy: allocationPolicy,
311
+ narutoRebalancePolicy: rebalancePolicy,
310
312
  json: parsed.json
311
313
  });
312
314
  const clones = result.roster?.agent_count ?? roster.agent_count;
@@ -323,7 +325,7 @@ async function narutoRun(parsed) {
323
325
  concurrency: result.target_active_slots ?? activeSlots,
324
326
  target_active_slots: result.target_active_slots ?? activeSlots,
325
327
  runtime_source_of_truth: 'agent-orchestrator-scheduler',
326
- pre_run_real_active_pool_source: 'smoke_only',
328
+ pre_run_real_active_pool_source: runPreRunSmoke ? 'smoke_only' : 'skipped',
327
329
  concurrency_capped: clones > (result.target_active_slots ?? activeSlots),
328
330
  system: { cores: safe.cores, free_gb: safe.free_gb, safe_concurrency: safe.cap, heavy_backend: safe.heavy },
329
331
  work_graph: {
@@ -409,6 +411,56 @@ function compactNarutoRunResult(result) {
409
411
  }
410
412
  };
411
413
  }
414
+ async function runNarutoControlPlaneSmoke(input) {
415
+ const smokeGraph = buildNarutoWorkGraph({
416
+ prompt: input.prompt,
417
+ requestedClones: Math.min(2, input.rosterCount),
418
+ totalWorkItems: Math.min(2, input.totalWorkItems),
419
+ readonly: true,
420
+ writeCapable: false,
421
+ leaseBasePath: input.patchEnvelopeBasePath,
422
+ maxActiveWorkers: Math.min(2, input.activeSlots),
423
+ worktreePolicy: {
424
+ mode: 'patch-envelope-only',
425
+ required: false,
426
+ main_repo_root: input.worktreePolicy.main_repo_root,
427
+ worktree_root: null,
428
+ fallback_reason: 'pre_run_smoke_never_owns_production_runtime'
429
+ }
430
+ });
431
+ const smokeWorktreePolicy = {
432
+ mode: 'patch-envelope-only',
433
+ required: false,
434
+ main_repo_root: input.worktreePolicy.main_repo_root,
435
+ worktree_root: null,
436
+ fallback_reason: 'pre_run_smoke_never_owns_production_runtime'
437
+ };
438
+ const realActivePool = await runNarutoRealActivePool({
439
+ graph: smokeGraph,
440
+ governor: { ...input.governor, safe_active_workers: Math.min(2, input.activeSlots), safe_zellij_visible_panes: Math.min(1, input.zellijVisiblePanes) },
441
+ spawnWorker: async (item, placement) => spawnActualNarutoWorker({
442
+ root: input.root,
443
+ missionId: input.missionId,
444
+ item,
445
+ placement,
446
+ backend: 'fake',
447
+ worktreePolicy: smokeWorktreePolicy,
448
+ zellijSessionName: `sks-${input.missionId}`,
449
+ visiblePaneCap: input.zellijVisiblePanes
450
+ }),
451
+ collectWorker: async (handle) => collectActualNarutoWorker(handle),
452
+ enqueueVerification: async () => undefined,
453
+ updateDashboard: async () => undefined
454
+ });
455
+ return {
456
+ ...realActivePool,
457
+ status: 'smoke_completed',
458
+ runtime_source_of_truth: 'pre_run_smoke_only',
459
+ production_runtime_source_of_truth: 'agent-orchestrator-scheduler',
460
+ fallback_reason: 'pre_run_smoke_never_owns_production_runtime',
461
+ smoke_graph_total_work_items: smokeGraph.total_work_items
462
+ };
463
+ }
412
464
  function buildNarutoAllocationWorkers(workGraph, roleDistribution, roster) {
413
465
  const workItems = Array.isArray(workGraph?.work_items) ? workGraph.work_items : [];
414
466
  const roleByWorkItem = new Map((roleDistribution?.work_item_roles || []).map((row) => [String(row.work_item_id), String(row.role || '')]));
@@ -582,9 +634,10 @@ function parseNarutoArgs(args = []) {
582
634
  const ollamaBaseUrl = String(readOption(args, '--ollama-base-url', readOption(args, '--local-model-base-url', '')) || '') || null;
583
635
  const noOpenZellij = hasFlag(args, '--no-open-zellij') || hasFlag(args, '--no-zellij');
584
636
  const attach = hasFlag(args, '--attach');
637
+ const smoke = hasFlag(args, '--smoke');
585
638
  const valueFlags = new Set(['--clones', '--agents', '--work-items', '--concurrency', '--target-active-slots', '--backend', '--write-mode', '--mission', '--mission-id', '--ollama-model', '--local-model-model', '--ollama-base-url', '--local-model-base-url']);
586
639
  const prompt = positionalArgs(rest, valueFlags).join(' ').trim() || 'Naruto shadow clone swarm run';
587
- return { action, prompt, clones, workItems, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, json, missionId, noOpenZellij, attach };
640
+ return { action, prompt, clones, workItems, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, json, missionId, noOpenZellij, attach, smoke };
588
641
  }
589
642
  async function writeNarutoArtifacts(ledgerRoot, artifacts) {
590
643
  await writeJsonAtomic(path.join(ledgerRoot, 'naruto-work-graph.json'), artifacts.workGraph);
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.11';
8
+ export const PACKAGE_VERSION = '2.0.12';
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() {
@@ -54,14 +54,34 @@ export async function applyGitWorktreeMergeQueue(input) {
54
54
  input: diff.diff,
55
55
  timeoutMs: 30000
56
56
  });
57
- if (apply.ok)
57
+ if (apply.ok) {
58
58
  appliedCount += 1;
59
+ strategyResults.push({
60
+ ok: true,
61
+ worker_id: diff.worker_id,
62
+ strategy: 'diff-apply-3way',
63
+ commit_hash: null,
64
+ conflict_files: [],
65
+ changed_files: diff.changed_files,
66
+ blockers: []
67
+ });
68
+ }
59
69
  else {
60
- conflicts.push(summarizeGitWorktreeConflict({
70
+ const conflict = summarizeGitWorktreeConflict({
61
71
  workerId: diff.worker_id,
62
72
  changedFiles: diff.changed_files,
63
73
  stderr: apply.stderr || apply.stdout
64
- }));
74
+ });
75
+ strategyResults.push({
76
+ ok: false,
77
+ worker_id: diff.worker_id,
78
+ strategy: 'diff-apply-3way',
79
+ commit_hash: null,
80
+ conflict_files: conflict.conflict_files || diff.changed_files,
81
+ changed_files: diff.changed_files,
82
+ blockers: conflict.blockers || ['git_worktree_diff_apply_failed']
83
+ });
84
+ conflicts.push(conflict);
65
85
  }
66
86
  }
67
87
  const blockers = conflicts.length ? ['git_worktree_merge_queue_conflicts'] : [];
@@ -80,44 +100,44 @@ export async function applyGitWorktreeMergeQueue(input) {
80
100
  };
81
101
  }
82
102
  async function applyCheckpointCommit(integrationWorktreePath, checkpoint) {
83
- const merge = await runGitCommand(integrationWorktreePath, ['merge', '--no-ff', '--no-edit', checkpoint.commit_hash || ''], {
103
+ const cherryPick = await runGitCommand(integrationWorktreePath, ['cherry-pick', '--allow-empty', '-X', 'theirs', checkpoint.commit_hash || ''], {
84
104
  timeoutMs: 120000
85
105
  });
86
- if (merge.ok) {
106
+ if (cherryPick.ok) {
87
107
  return {
88
108
  ok: true,
89
109
  worker_id: checkpoint.worker_id,
90
- strategy: 'merge',
110
+ strategy: 'checkpoint-cherry-pick',
91
111
  commit_hash: checkpoint.commit_hash,
92
112
  conflict_files: [],
93
113
  blockers: []
94
114
  };
95
115
  }
96
- await runGitCommand(integrationWorktreePath, ['merge', '--abort'], { timeoutMs: 30000 }).catch(() => null);
97
- const cherryPick = await runGitCommand(integrationWorktreePath, ['cherry-pick', checkpoint.commit_hash || ''], {
116
+ await runGitCommand(integrationWorktreePath, ['cherry-pick', '--abort'], { timeoutMs: 30000 }).catch(() => null);
117
+ const merge = await runGitCommand(integrationWorktreePath, ['merge', '--no-ff', '--no-edit', '-X', 'theirs', checkpoint.commit_hash || ''], {
98
118
  timeoutMs: 120000
99
119
  });
100
- if (cherryPick.ok) {
120
+ if (merge.ok) {
101
121
  return {
102
122
  ok: true,
103
123
  worker_id: checkpoint.worker_id,
104
- strategy: 'cherry-pick',
124
+ strategy: 'checkpoint-merge',
105
125
  commit_hash: checkpoint.commit_hash,
106
126
  conflict_files: [],
107
127
  blockers: []
108
128
  };
109
129
  }
110
130
  const conflictFiles = await runGitCommand(integrationWorktreePath, ['diff', '--name-only', '--diff-filter=U'], { timeoutMs: 30000 }).catch(() => null);
111
- await runGitCommand(integrationWorktreePath, ['cherry-pick', '--abort'], { timeoutMs: 30000 }).catch(() => null);
131
+ await runGitCommand(integrationWorktreePath, ['merge', '--abort'], { timeoutMs: 30000 }).catch(() => null);
112
132
  return {
113
133
  ok: false,
114
134
  worker_id: checkpoint.worker_id,
115
- strategy: 'merge_then_cherry-pick',
135
+ strategy: 'checkpoint-cherry-pick-then-merge',
116
136
  commit_hash: checkpoint.commit_hash,
117
137
  conflict_files: String(conflictFiles?.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean),
118
138
  blockers: [
119
- `git_worktree_checkpoint_merge_failed:${merge.stderr_tail || merge.stdout_tail}`,
120
- `git_worktree_checkpoint_cherry_pick_failed:${cherryPick.stderr_tail || cherryPick.stdout_tail}`
139
+ `git_worktree_checkpoint_cherry_pick_failed:${cherryPick.stderr_tail || cherryPick.stdout_tail}`,
140
+ `git_worktree_checkpoint_merge_failed:${merge.stderr_tail || merge.stdout_tail}`
121
141
  ]
122
142
  };
123
143
  }
@@ -3,11 +3,14 @@ export function rebalanceNarutoReadyWork(input) {
3
3
  const completed = new Set((input.completedTaskIds || []).map(String));
4
4
  const reclaimed = new Set((input.reclaimedTaskIds || []).map(String));
5
5
  const idle = input.workers.filter((worker) => worker.alive && ['idle', 'done', 'unknown'].includes(worker.state));
6
+ const activeWorkerIds = new Set(input.workers.filter((worker) => worker.alive).map((worker) => worker.id));
7
+ const activeWritePaths = new Set([...(input.activeWritePaths || []), ...(input.currentAssignments || []).flatMap((row) => row.write_paths || [])].map(normalizePath));
6
8
  if (!idle.length)
7
9
  return [];
8
10
  const ready = input.tasks
9
- .filter((task) => (task.status || 'pending') === 'pending' && !task.owner)
11
+ .filter((task) => (task.status || 'pending') === 'pending')
10
12
  .filter((task) => task.dependencies.every((dep) => completed.has(dep)))
13
+ .filter((task) => task.write_paths.every((file) => !activeWritePaths.has(normalizePath(file))))
11
14
  .sort((left, right) => {
12
15
  const reclaimedOrder = Number(!reclaimed.has(left.id)) - Number(!reclaimed.has(right.id));
13
16
  return reclaimedOrder || left.id.localeCompare(right.id);
@@ -15,12 +18,18 @@ export function rebalanceNarutoReadyWork(input) {
15
18
  const decisions = [];
16
19
  const assignments = [...(input.currentAssignments || [])];
17
20
  for (const task of ready) {
18
- const decision = chooseNarutoTaskOwner(task, idle, assignments);
21
+ const requestedOwner = task.owner ? String(task.owner) : '';
22
+ const ownerActive = requestedOwner && activeWorkerIds.has(requestedOwner);
23
+ const ownerIdle = ownerActive ? idle.some((worker) => worker.id === requestedOwner) : false;
24
+ if (requestedOwner && ownerActive && !ownerIdle)
25
+ continue;
26
+ const candidateWorkers = ownerIdle ? idle.filter((worker) => worker.id === requestedOwner) : idle;
27
+ const decision = chooseNarutoTaskOwner({ ...task, owner: null }, candidateWorkers, assignments);
19
28
  decisions.push({
20
29
  type: 'assign',
21
30
  task_id: task.id,
22
31
  worker_id: decision.owner,
23
- reason: `${reclaimed.has(task.id) ? 'reclaimed ready work' : 'idle worker pickup'}; ${decision.reason}`
32
+ reason: `${reclaimed.has(task.id) ? 'reclaimed ready work' : requestedOwner && !ownerActive ? `owner inactive:${requestedOwner}` : 'idle worker pickup'}; ${decision.reason}`
24
33
  });
25
34
  assignments.push({
26
35
  task_id: task.id,
@@ -33,4 +42,7 @@ export function rebalanceNarutoReadyWork(input) {
33
42
  }
34
43
  return decisions;
35
44
  }
45
+ function normalizePath(file) {
46
+ return String(file || '').replace(/\\/g, '/').replace(/^\.\/+/, '').replace(/\/+$/, '');
47
+ }
36
48
  //# sourceMappingURL=naruto-rebalance-policy.js.map
@@ -42,6 +42,12 @@ export function buildNarutoWorkGraph(input = {}) {
42
42
  fallback_reason: writeCapable ? 'git_capability_not_evaluated' : 'readonly_or_write_disabled'
43
43
  };
44
44
  const workItems = [];
45
+ const assignmentById = new Map();
46
+ for (const row of input.allocationAssignments || []) {
47
+ const id = String(row.task_id || row.id || '');
48
+ if (id)
49
+ assignmentById.set(id, row);
50
+ }
45
51
  for (let index = 0; index < totalWorkItems; index += 1) {
46
52
  const id = `NW-${String(index + 1).padStart(6, '0')}`;
47
53
  const kind = kindCycle[index % kindCycle.length] || 'verification';
@@ -53,6 +59,8 @@ export function buildNarutoWorkGraph(input = {}) {
53
59
  ...writePaths.map((file) => ({ path: file, kind: 'write' })),
54
60
  ...readPaths.map((file) => ({ path: file, kind: 'read' }))
55
61
  ];
62
+ const assignment = assignmentById.get(id);
63
+ const allocationHints = assignment?.allocation_hints || assignment?.hints || null;
56
64
  workItems.push({
57
65
  id,
58
66
  kind,
@@ -73,6 +81,11 @@ export function buildNarutoWorkGraph(input = {}) {
73
81
  requires_verification: kind !== 'research' && kind !== 'final_review_input_pack',
74
82
  requires_gpt_final: writePaths.length > 0 || kind === 'final_review_input_pack'
75
83
  },
84
+ owner: assignment?.owner ?? null,
85
+ allocation_reason: assignment?.allocation_reason ?? null,
86
+ allocation_score: assignment?.allocation_score ?? null,
87
+ allocation_hints: allocationHints,
88
+ lane: assignment?.owner ?? null,
76
89
  ...(writePaths.length > 0 ? {
77
90
  worktree: {
78
91
  mode: worktreePolicy.mode,
@@ -1,2 +1,2 @@
1
- export const PACKAGE_VERSION = '2.0.11';
1
+ export const PACKAGE_VERSION = '2.0.12';
2
2
  //# sourceMappingURL=version.js.map
@@ -5,18 +5,32 @@ export function renderZellijSlotColumnAnchor(input = {}) {
5
5
  const visible = Math.max(1, nonNegativeInt(input.visiblePaneCap, active || 1));
6
6
  const headless = nonNegativeInt(input.headlessWorkers, 0);
7
7
  const queue = nonNegativeInt(input.queueDepth, 0);
8
- return `SLOTS active ${active}/${visible} · headless ${headless} · q ${queue}`;
8
+ const header = `SLOTS active ${active}/${visible} · headless ${headless} · q ${queue}`;
9
+ const workers = Array.isArray(input.workerRows) ? input.workerRows : [];
10
+ if (!workers.length)
11
+ return header;
12
+ const maxRows = Math.max(1, nonNegativeInt(input.maxWorkerRows, input.mode === 'full-debug' ? 24 : 12));
13
+ const visibleRows = workers.slice(0, maxRows);
14
+ const hidden = Math.max(0, workers.length - visibleRows.length);
15
+ return [
16
+ header,
17
+ ...visibleRows.map((row, index) => renderWorkerRow(row, index + 1)),
18
+ ...(hidden ? [`+${hidden} more worker${hidden === 1 ? '' : 's'}`] : [])
19
+ ].join('\n');
9
20
  }
10
21
  export async function renderZellijSlotColumnAnchorFromArtifacts(input) {
11
22
  const root = path.resolve(input.artifactRoot);
12
23
  const missionDir = inferMissionDir(root, input.missionId);
13
24
  const snapshot = await readJson(path.join(missionDir, 'zellij-dashboard-snapshot.json'));
14
25
  const rightColumn = await readJson(path.join(missionDir, 'zellij-right-column-state.json'));
15
- const activeWorkers = Number(snapshot?.active_workers ?? rightColumn?.visible_worker_panes?.filter((row) => row?.status === 'running' || row?.status === 'launching').length ?? 0);
26
+ const swarm = await readJson(path.join(root, 'agent-native-cli-session-swarm.json'))
27
+ || await readJson(path.join(missionDir, 'agents', 'agent-native-cli-session-swarm.json'));
28
+ const workerRows = await buildWorkerRows(root, missionDir, rightColumn, swarm);
29
+ const activeWorkers = Number(snapshot?.active_workers ?? workerRows.filter((row) => row.status === 'running' || row.status === 'launching').length ?? 0);
16
30
  const visiblePaneCap = Number(snapshot?.visible_panes ?? Math.max(1, rightColumn?.visible_worker_panes?.length || activeWorkers || 1));
17
- const headlessWorkers = Number(snapshot?.headless_workers ?? rightColumn?.headless_workers?.filter((row) => !row?.status || row?.status === 'running').length ?? 0);
31
+ const headlessWorkers = Number(snapshot?.headless_workers ?? workerRows.filter((row) => row.placement === 'headless' && (!row.status || row.status === 'running')).length ?? 0);
18
32
  const queueDepth = Number(snapshot?.queue_depth ?? 0);
19
- const anchorInput = { activeWorkers, visiblePaneCap, headlessWorkers, queueDepth };
33
+ const anchorInput = { activeWorkers, visiblePaneCap, headlessWorkers, queueDepth, workerRows };
20
34
  if (input.mode !== undefined)
21
35
  anchorInput.mode = input.mode;
22
36
  return renderZellijSlotColumnAnchor(anchorInput);
@@ -47,12 +61,157 @@ async function readJson(file) {
47
61
  return null;
48
62
  }
49
63
  }
64
+ async function buildWorkerRows(root, missionDir, rightColumn, swarm) {
65
+ const byKey = new Map();
66
+ const records = Array.isArray(swarm?.records) ? swarm.records : [];
67
+ const recordByKey = new Map();
68
+ for (const record of records) {
69
+ const key = workerKey(record?.slot_id, record?.generation_index);
70
+ if (key)
71
+ recordByKey.set(key, record);
72
+ }
73
+ for (const pane of Array.isArray(rightColumn?.visible_worker_panes) ? rightColumn.visible_worker_panes : []) {
74
+ const key = workerKey(pane?.slot_id, pane?.generation_index);
75
+ if (!key)
76
+ continue;
77
+ const record = recordByKey.get(key);
78
+ byKey.set(key, await hydrateWorkerRow(root, missionDir, {
79
+ slotId: String(pane.slot_id),
80
+ generationIndex: Number(pane.generation_index || 1),
81
+ placement: 'zellij-pane',
82
+ status: pane.status || record?.status || 'running',
83
+ paneId: pane.pane_id || record?.zellij_pane_id || null,
84
+ yOrder: Number(pane.y_order || 0)
85
+ }, record));
86
+ }
87
+ for (const row of Array.isArray(rightColumn?.headless_workers) ? rightColumn.headless_workers : []) {
88
+ const key = workerKey(row?.slot_id, row?.generation_index);
89
+ if (!key)
90
+ continue;
91
+ const record = recordByKey.get(key);
92
+ byKey.set(key, await hydrateWorkerRow(root, missionDir, {
93
+ slotId: String(row.slot_id),
94
+ generationIndex: Number(row.generation_index || 1),
95
+ placement: 'headless',
96
+ status: row.status || record?.status || 'running',
97
+ reason: row.reason || record?.headless_reason || null,
98
+ yOrder: 9000
99
+ }, record));
100
+ }
101
+ for (const record of records) {
102
+ const key = workerKey(record?.slot_id, record?.generation_index);
103
+ if (!key || byKey.has(key))
104
+ continue;
105
+ byKey.set(key, await hydrateWorkerRow(root, missionDir, {
106
+ slotId: String(record.slot_id || record.agent_id || 'slot-?'),
107
+ generationIndex: Number(record.generation_index || 1),
108
+ placement: record.worker_placement || (record.zellij_pane_id ? 'zellij-pane' : 'process'),
109
+ status: record.status || 'running',
110
+ paneId: record.zellij_pane_id || null,
111
+ yOrder: 5000
112
+ }, record));
113
+ }
114
+ return [...byKey.values()].sort((a, b) => {
115
+ const statusDelta = statusWeight(a.status) - statusWeight(b.status);
116
+ if (statusDelta)
117
+ return statusDelta;
118
+ const yDelta = Number(a.yOrder || 0) - Number(b.yOrder || 0);
119
+ if (yDelta)
120
+ return yDelta;
121
+ return String(a.slotId).localeCompare(String(b.slotId));
122
+ });
123
+ }
124
+ async function hydrateWorkerRow(root, missionDir, base, record) {
125
+ const artifactDir = resolveArtifactDir(root, missionDir, record?.worker_artifact_dir);
126
+ const result = artifactDir ? await readJson(path.join(artifactDir, 'worker-result.json')) : null;
127
+ const intake = artifactDir ? await readJson(path.join(artifactDir, 'worker-intake.json')) : null;
128
+ const heartbeatPath = artifactDir ? path.join(artifactDir, 'worker-heartbeat.jsonl') : null;
129
+ return {
130
+ ...base,
131
+ status: result?.status || base.status || record?.status || 'running',
132
+ backend: result?.backend || record?.backend || intake?.backend || null,
133
+ role: result?.persona_id || intake?.agent?.naruto_role || intake?.agent?.role || intake?.agent?.persona_id || null,
134
+ task: firstText([
135
+ result?.summary,
136
+ Array.isArray(result?.changed_files) ? result.changed_files[0] : null,
137
+ intake?.slice?.description,
138
+ intake?.slice?.title,
139
+ intake?.slice?.id,
140
+ base.reason
141
+ ]),
142
+ worktreeId: result?.worktree?.id || record?.worktree?.id || intake?.worktree?.id || null,
143
+ heartbeatAgeMs: heartbeatPath ? await heartbeatAgeMs(heartbeatPath) : null
144
+ };
145
+ }
146
+ function renderWorkerRow(row, index) {
147
+ const slot = `${trimInline(row.slotId || 'slot-?', 12)} g${Math.max(1, Math.floor(Number(row.generationIndex) || 1))}`;
148
+ const status = trimInline(row.status || 'running', 9);
149
+ const backend = trimInline(row.backend || row.placement || '-', 12);
150
+ const worktree = trimInline(row.worktreeId || row.role || '-', 10);
151
+ const task = trimInline(row.task || row.reason || '-', 38);
152
+ return `${String(index).padStart(2, '0')} ${slot} ${status} ${backend} ${worktree} · ${task} · hb ${formatHeartbeat(row.heartbeatAgeMs)}`;
153
+ }
154
+ function resolveArtifactDir(root, missionDir, value) {
155
+ if (!value)
156
+ return null;
157
+ const text = String(value);
158
+ if (path.isAbsolute(text))
159
+ return text;
160
+ return path.join(root, text);
161
+ }
162
+ async function heartbeatAgeMs(file) {
163
+ try {
164
+ return Date.now() - (await fs.promises.stat(file)).mtimeMs;
165
+ }
166
+ catch {
167
+ return null;
168
+ }
169
+ }
170
+ function formatHeartbeat(ageMs) {
171
+ if (ageMs == null)
172
+ return '?';
173
+ if (ageMs < 1000)
174
+ return 'now';
175
+ return `${Math.max(1, Math.round(ageMs / 1000))}s`;
176
+ }
177
+ function firstText(values) {
178
+ for (const value of values) {
179
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
180
+ if (text)
181
+ return text;
182
+ }
183
+ return null;
184
+ }
185
+ function workerKey(slotId, generationIndex) {
186
+ const slot = String(slotId || '').trim();
187
+ if (!slot)
188
+ return null;
189
+ return `${slot}:g${Math.max(1, Math.floor(Number(generationIndex) || 1))}`;
190
+ }
191
+ function statusWeight(status) {
192
+ const text = String(status || '').toLowerCase();
193
+ if (text === 'running' || text === 'launching')
194
+ return 0;
195
+ if (text === 'failed')
196
+ return 1;
197
+ if (text === 'draining')
198
+ return 2;
199
+ if (text === 'closed')
200
+ return 3;
201
+ return 4;
202
+ }
50
203
  function nonNegativeInt(value, fallback) {
51
204
  const parsed = Number(value);
52
205
  if (!Number.isFinite(parsed) || parsed < 0)
53
206
  return fallback;
54
207
  return Math.floor(parsed);
55
208
  }
209
+ function trimInline(value, max) {
210
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
211
+ if (text.length <= max)
212
+ return text;
213
+ return text.slice(0, Math.max(1, max - 3)) + '...';
214
+ }
56
215
  function shellQuote(value) {
57
216
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
58
217
  }