sneakoscope 2.0.9 → 2.0.11

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 (74) 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 +37 -8
  8. package/dist/cli/command-registry.js +2 -0
  9. package/dist/cli/install-helpers.js +8 -20
  10. package/dist/commands/doctor.js +23 -10
  11. package/dist/commands/zellij-slot-column-anchor.js +23 -0
  12. package/dist/commands/zellij-slot-pane.js +26 -0
  13. package/dist/core/agents/agent-orchestrator.js +255 -16
  14. package/dist/core/agents/agent-patch-schema.js +8 -1
  15. package/dist/core/agents/agent-role-config.js +92 -0
  16. package/dist/core/agents/native-cli-session-swarm.js +186 -71
  17. package/dist/core/commands/naruto-command.js +165 -11
  18. package/dist/core/doctor/doctor-readiness-matrix.js +6 -0
  19. package/dist/core/fsx.js +1 -1
  20. package/dist/core/git/git-worktree-checkpoint.js +52 -0
  21. package/dist/core/git/git-worktree-cross-rebase.js +54 -0
  22. package/dist/core/git/git-worktree-merge-queue.js +69 -0
  23. package/dist/core/git/git-worktree-patch-envelope.js +8 -1
  24. package/dist/core/hooks-runtime.js +4 -0
  25. package/dist/core/init.js +3 -2
  26. package/dist/core/naruto/naruto-active-pool.js +35 -2
  27. package/dist/core/naruto/naruto-allocation-policy.js +99 -0
  28. package/dist/core/naruto/naruto-concurrency-governor.js +1 -1
  29. package/dist/core/naruto/naruto-real-worker-child.js +134 -0
  30. package/dist/core/naruto/naruto-real-worker-runtime.js +121 -0
  31. package/dist/core/naruto/naruto-rebalance-policy.js +36 -0
  32. package/dist/core/naruto/naruto-task-hints.js +71 -0
  33. package/dist/core/pipeline/finalize-pipeline-result.js +3 -1
  34. package/dist/core/pipeline/gpt-final-required.js +22 -2
  35. package/dist/core/version.js +1 -1
  36. package/dist/core/zellij/zellij-right-column-manager.js +111 -9
  37. package/dist/core/zellij/zellij-slot-column-anchor.js +59 -0
  38. package/dist/core/zellij/zellij-slot-pane-renderer.js +82 -0
  39. package/dist/core/zellij/zellij-ui-mode.js +16 -0
  40. package/dist/core/zellij/zellij-worker-pane-manager.js +104 -13
  41. package/dist/scripts/agent-role-config-repair-check.js +33 -0
  42. package/dist/scripts/git-worktree-checkpoint-check.js +20 -0
  43. package/dist/scripts/git-worktree-cross-rebase-check.js +27 -0
  44. package/dist/scripts/git-worktree-integration-primary-check.js +4 -2
  45. package/dist/scripts/git-worktree-integration-primary-runtime-check.js +20 -0
  46. package/dist/scripts/mutation-callsite-coverage-check.js +2 -1
  47. package/dist/scripts/naruto-actual-worker-control-plane-check.js +29 -0
  48. package/dist/scripts/naruto-allocation-policy-check.js +33 -0
  49. package/dist/scripts/naruto-extreme-parallelism-check.js +1 -1
  50. package/dist/scripts/naruto-extreme-parallelism-real-check.js +43 -0
  51. package/dist/scripts/naruto-orchestrator-runtime-source-check.js +11 -0
  52. package/dist/scripts/naruto-real-active-pool-check.js +3 -2
  53. package/dist/scripts/naruto-real-active-pool-runtime-check.js +55 -0
  54. package/dist/scripts/naruto-rebalance-policy-check.js +28 -0
  55. package/dist/scripts/naruto-shadow-clone-swarm-check.js +7 -3
  56. package/dist/scripts/naruto-zellij-dynamic-right-column-check.js +29 -2
  57. package/dist/scripts/readme-architecture-imagegen-official-check.js +4 -3
  58. package/dist/scripts/release-check-dynamic-execute.js +27 -1
  59. package/dist/scripts/release-check-dynamic.js +38 -11
  60. package/dist/scripts/release-check-stamp.js +7 -2
  61. package/dist/scripts/release-dag-full-coverage-check.js +15 -1
  62. package/dist/scripts/release-dynamic-performance-check.js +31 -1
  63. package/dist/scripts/release-gate-existence-audit.js +29 -33
  64. package/dist/scripts/release-readiness-report.js +14 -3
  65. package/dist/scripts/zellij-first-slot-down-stack-check.js +20 -0
  66. package/dist/scripts/zellij-first-slot-down-stack-real-check.js +16 -0
  67. package/dist/scripts/zellij-right-column-geometry-proof.js +155 -22
  68. package/dist/scripts/zellij-right-column-headless-overflow-check.js +22 -0
  69. package/dist/scripts/zellij-right-column-manager-check.js +9 -4
  70. package/dist/scripts/zellij-slot-column-anchor-check.js +24 -0
  71. package/dist/scripts/zellij-slot-only-ui-check.js +24 -0
  72. package/dist/scripts/zellij-slot-pane-renderer-check.js +38 -0
  73. package/dist/scripts/zellij-worker-pane-manager-single-owner-check.js +11 -4
  74. package/package.json +22 -5
@@ -0,0 +1,121 @@
1
+ import fs from 'node:fs';
2
+ import { spawn } from 'node:child_process';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { ensureDir, nowIso, readJson, writeJsonAtomic } from '../fsx.js';
6
+ import { allocateWorkerWorktree } from '../git/git-worktree-manager.js';
7
+ import { cleanupGitWorktree } from '../git/git-worktree-cleanup.js';
8
+ export async function spawnActualNarutoWorker(input) {
9
+ const workerDir = path.join(input.root, '.sneakoscope', 'missions', input.missionId, 'agents', 'naruto-real-workers', input.item.id);
10
+ await ensureDir(workerDir);
11
+ let worktree = null;
12
+ if (input.worktreePolicy?.mode === 'git-worktree' && input.item.write_allowed === true) {
13
+ const allocation = await allocateWorkerWorktree({
14
+ repoRoot: input.worktreePolicy.main_repo_root || input.root,
15
+ missionId: input.missionId,
16
+ workerId: input.item.id,
17
+ slotId: input.item.id.replace(/[^A-Za-z0-9_-]/g, '-'),
18
+ generationIndex: 1
19
+ }).catch((err) => ({ ok: false, blockers: [`git_worktree_allocate_exception:${err?.message || String(err)}`] }));
20
+ await writeJsonAtomic(path.join(workerDir, 'git-worktree-allocation.json'), allocation);
21
+ if (allocation.ok)
22
+ worktree = allocation;
23
+ }
24
+ const resultPath = path.join(workerDir, 'worker-result.json');
25
+ const heartbeatPath = path.join(workerDir, 'worker-heartbeat.jsonl');
26
+ const intakePath = path.join(workerDir, 'worker-intake.json');
27
+ await writeJsonAtomic(intakePath, {
28
+ schema: 'sks.naruto-actual-worker-intake.v1',
29
+ generated_at: nowIso(),
30
+ mission_id: input.missionId,
31
+ item: input.item,
32
+ placement: input.placement,
33
+ backend: input.backend,
34
+ result_path: resultPath,
35
+ heartbeat_path: heartbeatPath,
36
+ worktree_path: worktree?.worktree_path || null,
37
+ zellij_session_name: input.zellijSessionName || null,
38
+ visible_pane_cap: input.visiblePaneCap
39
+ });
40
+ const child = spawn(process.execPath, [actualWorkerEntrypoint(), intakePath], {
41
+ cwd: worktree?.worktree_path || input.root,
42
+ stdio: ['ignore', 'ignore', 'ignore']
43
+ });
44
+ const exit = waitForExit(child, 30000);
45
+ return {
46
+ id: input.item.id,
47
+ item: input.item,
48
+ placement: input.placement,
49
+ started_at: Date.now(),
50
+ pid: child.pid || null,
51
+ child,
52
+ worker_artifact_dir: workerDir,
53
+ result_path: resultPath,
54
+ heartbeat_path: heartbeatPath,
55
+ worktree,
56
+ exit
57
+ };
58
+ }
59
+ export async function collectActualNarutoWorker(handle) {
60
+ const exit = await handle.exit;
61
+ const result = await readJson(handle.result_path, null).catch(() => null);
62
+ const blockers = [
63
+ ...(exit.code === 0 ? [] : [`naruto_actual_worker_exit_${exit.code ?? exit.signal ?? 'unknown'}`]),
64
+ ...(result?.ok === false ? result.blockers || ['naruto_actual_worker_result_not_ok'] : []),
65
+ ...(result ? [] : ['naruto_actual_worker_result_missing'])
66
+ ];
67
+ if (handle.worktree?.worktree_path) {
68
+ const cleanup = await cleanupGitWorktree({
69
+ repoRoot: handle.worktree.main_repo_root || handle.worktree.repo_root || handle.worktree.repoRoot || '',
70
+ worktreePath: handle.worktree.worktree_path,
71
+ branch: handle.worktree.branch,
72
+ deleteBranch: true
73
+ }).catch((err) => ({ ok: false, blockers: [`git_worktree_cleanup_exception:${err?.message || String(err)}`] }));
74
+ await writeJsonAtomic(path.join(handle.worker_artifact_dir, 'git-worktree-cleanup.json'), cleanup);
75
+ blockers.push(...(cleanup.blockers || []));
76
+ }
77
+ return {
78
+ id: handle.id,
79
+ ok: blockers.length === 0,
80
+ item: handle.item,
81
+ placement: handle.placement,
82
+ completed_at: Date.now(),
83
+ pid: handle.pid,
84
+ worker_artifact_dir: handle.worker_artifact_dir,
85
+ blockers
86
+ };
87
+ }
88
+ function actualWorkerEntrypoint() {
89
+ return fileURLToPath(new URL('./naruto-real-worker-child.js', import.meta.url));
90
+ }
91
+ function waitForExit(child, timeoutMs) {
92
+ return new Promise((resolve) => {
93
+ let settled = false;
94
+ let killTimer = null;
95
+ const finish = (code, signal) => {
96
+ if (settled)
97
+ return;
98
+ settled = true;
99
+ clearTimeout(timer);
100
+ if (killTimer)
101
+ clearTimeout(killTimer);
102
+ resolve({ code, signal });
103
+ };
104
+ const timer = setTimeout(() => {
105
+ if (!child.killed)
106
+ child.kill();
107
+ killTimer = setTimeout(() => {
108
+ if (!settled)
109
+ child.kill('SIGKILL');
110
+ finish(null, 'SIGKILL');
111
+ }, 5000);
112
+ }, Math.max(1000, timeoutMs));
113
+ child.on('close', (code, signal) => {
114
+ finish(code, signal);
115
+ });
116
+ child.on('error', () => {
117
+ finish(1, null);
118
+ });
119
+ });
120
+ }
121
+ //# sourceMappingURL=naruto-real-worker-runtime.js.map
@@ -0,0 +1,36 @@
1
+ import { chooseNarutoTaskOwner } from './naruto-allocation-policy.js';
2
+ export function rebalanceNarutoReadyWork(input) {
3
+ const completed = new Set((input.completedTaskIds || []).map(String));
4
+ const reclaimed = new Set((input.reclaimedTaskIds || []).map(String));
5
+ const idle = input.workers.filter((worker) => worker.alive && ['idle', 'done', 'unknown'].includes(worker.state));
6
+ if (!idle.length)
7
+ return [];
8
+ const ready = input.tasks
9
+ .filter((task) => (task.status || 'pending') === 'pending' && !task.owner)
10
+ .filter((task) => task.dependencies.every((dep) => completed.has(dep)))
11
+ .sort((left, right) => {
12
+ const reclaimedOrder = Number(!reclaimed.has(left.id)) - Number(!reclaimed.has(right.id));
13
+ return reclaimedOrder || left.id.localeCompare(right.id);
14
+ });
15
+ const decisions = [];
16
+ const assignments = [...(input.currentAssignments || [])];
17
+ for (const task of ready) {
18
+ const decision = chooseNarutoTaskOwner(task, idle, assignments);
19
+ decisions.push({
20
+ type: 'assign',
21
+ task_id: task.id,
22
+ worker_id: decision.owner,
23
+ reason: `${reclaimed.has(task.id) ? 'reclaimed ready work' : 'idle worker pickup'}; ${decision.reason}`
24
+ });
25
+ assignments.push({
26
+ task_id: task.id,
27
+ owner: decision.owner,
28
+ role: task.required_role,
29
+ paths: decision.hints.paths,
30
+ domains: decision.hints.domains,
31
+ write_paths: decision.hints.writePaths
32
+ });
33
+ }
34
+ return decisions;
35
+ }
36
+ //# sourceMappingURL=naruto-rebalance-policy.js.map
@@ -0,0 +1,71 @@
1
+ import { normalizeNarutoPath } from './naruto-work-item.js';
2
+ const PATH_PATTERN = /(?:^|[\s("'`])((?:src|scripts|schemas|docs|test|tests|packages|crates|bin)\/[A-Za-z0-9._/-]+)/g;
3
+ const DOMAIN_STOP_WORDS = new Set([
4
+ 'the', 'and', 'for', 'with', 'into', 'from', 'task', 'work', 'worker', 'workers',
5
+ 'implement', 'implementation', 'test', 'tests', 'check', 'gate', 'runtime',
6
+ 'naruto', 'sks', 'src', 'core', 'scripts', 'docs', 'file', 'files'
7
+ ]);
8
+ export function extractNarutoTaskHints(task) {
9
+ const record = task;
10
+ const writePaths = normalizePaths(readArray(record, 'write_paths', 'writePaths'));
11
+ const readPaths = normalizePaths([
12
+ ...readArray(record, 'readonly_paths', 'readPaths'),
13
+ ...readArray(record, 'target_paths')
14
+ ]);
15
+ const paths = normalizePaths([
16
+ ...readArray(record, 'target_paths'),
17
+ ...readArray(record, 'readonly_paths'),
18
+ ...readArray(record, 'write_paths'),
19
+ ...extractPathsFromText(`${readString(record, 'title')}\n${readString(record, 'description')}\n${readString(record, 'summary')}`)
20
+ ]);
21
+ const domains = [...new Set([
22
+ ...paths.flatMap(pathDomains),
23
+ ...extractDomainsFromText(`${readString(record, 'kind')} ${readString(record, 'title')} ${readString(record, 'description')}`)
24
+ ])].sort();
25
+ return {
26
+ paths,
27
+ domains,
28
+ role: readString(record, 'required_role') || readString(record, 'role') || null,
29
+ writePaths,
30
+ readPaths
31
+ };
32
+ }
33
+ export function pathPrefix(pathValue) {
34
+ const parts = normalizeNarutoPath(pathValue).split('/').filter(Boolean);
35
+ if (parts.length <= 1)
36
+ return parts[0] || '';
37
+ if (parts[0] === 'src' && parts.length >= 3)
38
+ return parts.slice(0, 3).join('/');
39
+ return parts.slice(0, 2).join('/');
40
+ }
41
+ function normalizePaths(paths) {
42
+ return [...new Set(paths.map((file) => normalizeNarutoPath(String(file || ''))).filter(Boolean))].sort();
43
+ }
44
+ function readArray(record, ...keys) {
45
+ return keys.flatMap((key) => Array.isArray(record[key]) ? record[key].map(String) : []);
46
+ }
47
+ function readString(record, key) {
48
+ const value = record[key];
49
+ return typeof value === 'string' ? value : value == null ? '' : String(value);
50
+ }
51
+ function extractPathsFromText(text) {
52
+ const out = [];
53
+ for (const match of String(text || '').matchAll(PATH_PATTERN)) {
54
+ if (match[1])
55
+ out.push(match[1]);
56
+ }
57
+ return out;
58
+ }
59
+ function extractDomainsFromText(text) {
60
+ return [...new Set(String(text || '').toLowerCase().match(/[a-z][a-z0-9_-]{2,}/g) || [])]
61
+ .filter((word) => !DOMAIN_STOP_WORDS.has(word))
62
+ .sort();
63
+ }
64
+ function pathDomains(pathValue) {
65
+ const normalized = normalizeNarutoPath(pathValue);
66
+ const parts = normalized.split('/').filter(Boolean);
67
+ const file = parts[parts.length - 1] || '';
68
+ const stem = file.replace(/\.[^.]+$/, '');
69
+ return [...new Set([pathPrefix(normalized), parts[0], parts[1], stem].filter((part) => Boolean(part)))];
70
+ }
71
+ //# sourceMappingURL=naruto-task-hints.js.map
@@ -7,7 +7,8 @@ export async function finalizePipelineResult(input) {
7
7
  const root = path.resolve(input.mutationLedgerRoot || path.join(cwd, '.sneakoscope', 'tmp', 'pipeline-finalize', safeName(input.missionId)));
8
8
  const requirement = gptFinalRequiredForPipeline({
9
9
  localParticipated: input.localParticipated,
10
- candidateResults: input.candidateResults
10
+ candidateResults: input.candidateResults,
11
+ candidatePatchEnvelopes: input.candidatePatchEnvelopes
11
12
  });
12
13
  let arbiter = null;
13
14
  let blockers = [];
@@ -36,6 +37,7 @@ export async function finalizePipelineResult(input) {
36
37
  route: input.route,
37
38
  mission_id: input.missionId,
38
39
  local_participated: requirement.local_participated,
40
+ worktree_participated: requirement.worktree_participated,
39
41
  gpt_final_required: requirement.gpt_final_required,
40
42
  gpt_final_arbiter: arbiter,
41
43
  final_status: blockers.length ? 'blocked' : 'accepted',
@@ -2,11 +2,31 @@ import { localCollaborationParticipated } from '../local-llm/local-collaboration
2
2
  export function gptFinalRequiredForPipeline(input) {
3
3
  const localParticipated = input.localParticipated === true
4
4
  || localCollaborationParticipated(Array.isArray(input.candidateResults) ? input.candidateResults : []);
5
+ const worktreeParticipated = worktreeCandidateParticipated(input.candidateResults)
6
+ || worktreeCandidateParticipated(input.candidatePatchEnvelopes);
5
7
  return {
6
8
  schema: 'sks.gpt-final-required.v1',
7
9
  local_participated: localParticipated,
8
- gpt_final_required: localParticipated,
9
- reason: localParticipated ? 'local_llm_outputs_are_drafts' : 'no_local_participation'
10
+ worktree_participated: worktreeParticipated,
11
+ gpt_final_required: localParticipated || worktreeParticipated,
12
+ reason: localParticipated
13
+ ? 'local_llm_outputs_are_drafts'
14
+ : worktreeParticipated
15
+ ? 'worktree_candidate_outputs_require_gpt_final'
16
+ : 'no_local_or_worktree_candidate_participation'
10
17
  };
11
18
  }
19
+ function worktreeCandidateParticipated(values) {
20
+ return (Array.isArray(values) ? values : []).some((value) => {
21
+ if (!value || typeof value !== 'object')
22
+ return false;
23
+ if (value.source === 'git-worktree-diff')
24
+ return true;
25
+ if (value.git_worktree?.worktree_path || value.git_worktree?.checkpoint?.commit_hash)
26
+ return true;
27
+ if (value.git_worktree_diff || value.git_worktree_checkpoint)
28
+ return true;
29
+ return Array.isArray(value.patch_envelopes) && worktreeCandidateParticipated(value.patch_envelopes);
30
+ });
31
+ }
12
32
  //# sourceMappingURL=gpt-final-required.js.map
@@ -1,2 +1,2 @@
1
- export const PACKAGE_VERSION = '2.0.9';
1
+ export const PACKAGE_VERSION = '2.0.11';
2
2
  //# sourceMappingURL=version.js.map
@@ -1,13 +1,21 @@
1
1
  import path from 'node:path';
2
2
  import { appendJsonl, ensureDir, nowIso, readJson, writeJsonAtomic } from '../fsx.js';
3
3
  import { openZellijDashboardPane } from './zellij-dashboard-pane.js';
4
+ import { zellijUiModeCreatesDashboard } from './zellij-ui-mode.js';
4
5
  export const ZELLIJ_RIGHT_COLUMN_STATE_SCHEMA = 'sks.zellij-right-column-state.v1';
5
6
  export async function ensureRightColumn(input) {
6
7
  const paths = resolveRightColumnPaths(input.root, input.missionId, input.projectRoot);
7
8
  await ensureDir(paths.missionDir);
9
+ const uiMode = input.uiMode || 'compact-slots';
10
+ const createDashboard = input.createDashboard ?? zellijUiModeCreatesDashboard(uiMode);
8
11
  const existing = await readRightColumnState(input.root, input.missionId, input.projectRoot);
9
- if (existing?.status === 'active' && existing.dashboard_pane_id) {
10
- return writeRightColumnState(paths.statePath, { ...existing, updated_at: nowIso() });
12
+ if (existing?.status === 'active') {
13
+ return writeRightColumnState(paths.statePath, {
14
+ ...existing,
15
+ updated_at: nowIso(),
16
+ slot_column_anchor_pane_id: existing.slot_column_anchor_pane_id || null,
17
+ ui_mode: existing.ui_mode || uiMode
18
+ });
11
19
  }
12
20
  const creating = await writeRightColumnState(paths.statePath, {
13
21
  schema: ZELLIJ_RIGHT_COLUMN_STATE_SCHEMA,
@@ -18,11 +26,31 @@ export async function ensureRightColumn(input) {
18
26
  status: 'creating',
19
27
  dashboard_pane_id: null,
20
28
  right_anchor_pane_id: null,
29
+ slot_column_anchor_pane_id: null,
30
+ ui_mode: uiMode,
21
31
  visible_worker_panes: [],
22
32
  headless_workers: [],
23
33
  blockers: []
24
34
  });
25
35
  await appendRightColumnEvent(paths.eventsPath, 'right_column_creating', creating, {});
36
+ if (!createDashboard) {
37
+ const active = await writeRightColumnState(paths.statePath, {
38
+ ...creating,
39
+ updated_at: nowIso(),
40
+ status: 'active',
41
+ dashboard_pane_id: null,
42
+ right_anchor_pane_id: null,
43
+ slot_column_anchor_pane_id: null,
44
+ ui_mode: uiMode,
45
+ blockers: []
46
+ });
47
+ await appendRightColumnEvent(paths.eventsPath, 'right_column_created', active, {
48
+ ok: true,
49
+ dashboard_created: false,
50
+ ui_mode: uiMode
51
+ });
52
+ return active;
53
+ }
26
54
  const dashboard = await openZellijDashboardPane({
27
55
  root: paths.projectRoot,
28
56
  missionId: input.missionId,
@@ -45,6 +73,8 @@ export async function ensureRightColumn(input) {
45
73
  status: blockers.length ? 'creating' : 'active',
46
74
  dashboard_pane_id: dashboard.pane_id ? String(dashboard.pane_id) : null,
47
75
  right_anchor_pane_id: dashboard.pane_id ? String(dashboard.pane_id) : null,
76
+ slot_column_anchor_pane_id: null,
77
+ ui_mode: uiMode,
48
78
  blockers
49
79
  });
50
80
  await appendRightColumnEvent(paths.eventsPath, 'right_column_created', active, { ok: active.status === 'active' });
@@ -52,6 +82,9 @@ export async function ensureRightColumn(input) {
52
82
  return active;
53
83
  }
54
84
  export async function prepareWorkerInRightColumn(input) {
85
+ return withRightColumnLock(input.root, input.missionId, async () => prepareWorkerInRightColumnUnlocked(input));
86
+ }
87
+ async function prepareWorkerInRightColumnUnlocked(input) {
55
88
  const paths = resolveRightColumnPaths(input.root, input.missionId, input.projectRoot);
56
89
  const state = await ensureRightColumn({
57
90
  root: input.root,
@@ -59,12 +92,13 @@ export async function prepareWorkerInRightColumn(input) {
59
92
  missionId: input.missionId,
60
93
  sessionName: input.sessionName,
61
94
  cwd: input.cwd,
62
- dashboardSnapshot: input.dashboardSnapshot
95
+ dashboardSnapshot: input.dashboardSnapshot,
96
+ uiMode: input.uiMode || 'compact-slots'
63
97
  });
64
98
  const activeVisible = state.visible_worker_panes.filter((pane) => pane.status === 'launching' || pane.status === 'running');
65
99
  const cap = Math.max(1, Math.floor(Number(input.visiblePaneCap || 1)));
66
100
  if (activeVisible.length >= cap) {
67
- const next = await recordHeadlessWorkerInRightColumn({
101
+ const next = await recordHeadlessWorkerInRightColumnUnlocked({
68
102
  root: input.root,
69
103
  ...(input.projectRoot ? { projectRoot: input.projectRoot } : {}),
70
104
  missionId: input.missionId,
@@ -76,7 +110,7 @@ export async function prepareWorkerInRightColumn(input) {
76
110
  return { state: next, placement: 'headless', focusPaneId: null, yOrder: null };
77
111
  }
78
112
  const lastVisible = activeVisible[activeVisible.length - 1];
79
- const focusPaneId = lastVisible?.pane_id || state.right_anchor_pane_id || state.dashboard_pane_id || null;
113
+ const focusPaneId = lastVisible?.pane_id || state.slot_column_anchor_pane_id || state.right_anchor_pane_id || state.dashboard_pane_id || null;
80
114
  const yOrder = Math.max(1, ...state.visible_worker_panes.map((pane) => Number(pane.y_order || 0) + 1));
81
115
  const next = await writeRightColumnState(paths.statePath, {
82
116
  ...state,
@@ -129,11 +163,52 @@ export async function recordWorkerPaneInRightColumn(input) {
129
163
  pane_id: input.record.pane_id,
130
164
  y_order: yOrder,
131
165
  direction_applied: input.record.direction_applied,
166
+ column_creation_direction_requested: input.record.column_creation_direction_requested || null,
167
+ column_creation_direction_applied: input.record.column_creation_direction_applied || null,
168
+ worker_direction_requested: input.record.worker_direction_requested,
169
+ worker_direction_applied: input.record.worker_direction_applied,
170
+ slot_column_anchor_pane_id: input.record.slot_column_anchor_pane_id || state.slot_column_anchor_pane_id || null,
132
171
  blockers: input.record.blockers
133
172
  });
134
173
  return next;
135
174
  }
175
+ export async function recordSlotColumnAnchorInRightColumn(input) {
176
+ const paths = resolveRightColumnPaths(input.root, input.missionId, input.projectRoot);
177
+ const state = await readRightColumnState(input.root, input.missionId, input.projectRoot) || {
178
+ schema: ZELLIJ_RIGHT_COLUMN_STATE_SCHEMA,
179
+ generated_at: nowIso(),
180
+ updated_at: nowIso(),
181
+ mission_id: input.missionId,
182
+ session_name: input.sessionName,
183
+ status: 'active',
184
+ dashboard_pane_id: null,
185
+ right_anchor_pane_id: null,
186
+ slot_column_anchor_pane_id: null,
187
+ ui_mode: 'compact-slots',
188
+ visible_worker_panes: [],
189
+ headless_workers: [],
190
+ blockers: []
191
+ };
192
+ const paneId = input.paneId ? String(input.paneId) : null;
193
+ const next = await writeRightColumnState(paths.statePath, {
194
+ ...state,
195
+ updated_at: nowIso(),
196
+ status: 'active',
197
+ session_name: input.sessionName || state.session_name,
198
+ right_anchor_pane_id: paneId || state.right_anchor_pane_id,
199
+ slot_column_anchor_pane_id: paneId || state.slot_column_anchor_pane_id
200
+ });
201
+ await appendRightColumnEvent(paths.eventsPath, 'slot_column_anchor_created', next, {
202
+ pane_id: paneId,
203
+ column_creation_direction_requested: 'right',
204
+ column_creation_direction_applied: paneId ? 'right' : 'unknown'
205
+ });
206
+ return next;
207
+ }
136
208
  export async function recordHeadlessWorkerInRightColumn(input) {
209
+ return withRightColumnLock(input.root, input.missionId, async () => recordHeadlessWorkerInRightColumnUnlocked(input));
210
+ }
211
+ async function recordHeadlessWorkerInRightColumnUnlocked(input) {
137
212
  const paths = resolveRightColumnPaths(input.root, input.missionId, input.projectRoot);
138
213
  const state = await readRightColumnState(input.root, input.missionId, input.projectRoot) || {
139
214
  schema: ZELLIJ_RIGHT_COLUMN_STATE_SCHEMA,
@@ -144,13 +219,15 @@ export async function recordHeadlessWorkerInRightColumn(input) {
144
219
  status: 'absent',
145
220
  dashboard_pane_id: null,
146
221
  right_anchor_pane_id: null,
222
+ slot_column_anchor_pane_id: null,
223
+ ui_mode: 'compact-slots',
147
224
  visible_worker_panes: [],
148
225
  headless_workers: [],
149
226
  blockers: []
150
227
  };
151
228
  const headless = [
152
229
  ...state.headless_workers.filter((row) => !(row.slot_id === input.slotId && row.generation_index === input.generationIndex)),
153
- { slot_id: input.slotId, generation_index: input.generationIndex, reason: input.reason }
230
+ { slot_id: input.slotId, generation_index: input.generationIndex, reason: input.reason, status: 'running', closed_at: null }
154
231
  ];
155
232
  const next = await writeRightColumnState(paths.statePath, { ...state, updated_at: nowIso(), headless_workers: headless });
156
233
  await appendRightColumnEvent(paths.eventsPath, 'worker_headless_overflow', next, {
@@ -169,13 +246,19 @@ export async function closeWorkerInRightColumn(input) {
169
246
  const same = pane.slot_id === input.slotId && pane.generation_index === input.generationIndex;
170
247
  return same ? { ...pane, pane_id: input.paneId || pane.pane_id, status: input.status } : pane;
171
248
  });
249
+ const headless = state.headless_workers.map((row) => {
250
+ const same = row.slot_id === input.slotId && row.generation_index === input.generationIndex;
251
+ return same ? { ...row, status: input.status === 'draining' ? 'closed' : input.status, closed_at: nowIso() } : row;
252
+ });
172
253
  const visibleStillActive = panes.filter((pane) => pane.status === 'launching' || pane.status === 'running');
254
+ const headlessStillActive = headless.filter((row) => !row.status || row.status === 'running');
173
255
  const next = await writeRightColumnState(paths.statePath, {
174
256
  ...state,
175
257
  updated_at: nowIso(),
176
- status: visibleStillActive.length || state.headless_workers.length ? 'active' : 'draining',
177
- right_anchor_pane_id: visibleStillActive[visibleStillActive.length - 1]?.pane_id || state.dashboard_pane_id,
178
- visible_worker_panes: panes
258
+ status: visibleStillActive.length || headlessStillActive.length ? 'active' : 'draining',
259
+ right_anchor_pane_id: visibleStillActive[visibleStillActive.length - 1]?.pane_id || state.slot_column_anchor_pane_id || state.dashboard_pane_id,
260
+ visible_worker_panes: panes,
261
+ headless_workers: headless
179
262
  });
180
263
  await appendRightColumnEvent(paths.eventsPath, 'worker_pane_drained', next, {
181
264
  slot_id: input.slotId,
@@ -242,4 +325,23 @@ function inferProjectRoot(root, missionId) {
242
325
  }
243
326
  return root;
244
327
  }
328
+ const rightColumnLocks = new Map();
329
+ async function withRightColumnLock(root, missionId, fn) {
330
+ const key = `${path.resolve(root)}:${missionId}`;
331
+ const previous = rightColumnLocks.get(key) || Promise.resolve();
332
+ let release;
333
+ const current = new Promise((resolve) => {
334
+ release = resolve;
335
+ });
336
+ rightColumnLocks.set(key, previous.then(() => current, () => current));
337
+ await previous.catch(() => undefined);
338
+ try {
339
+ return await fn();
340
+ }
341
+ finally {
342
+ release();
343
+ if (rightColumnLocks.get(key) === current)
344
+ rightColumnLocks.delete(key);
345
+ }
346
+ }
245
347
  //# sourceMappingURL=zellij-right-column-manager.js.map
@@ -0,0 +1,59 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function renderZellijSlotColumnAnchor(input = {}) {
4
+ const active = nonNegativeInt(input.activeWorkers, 0);
5
+ const visible = Math.max(1, nonNegativeInt(input.visiblePaneCap, active || 1));
6
+ const headless = nonNegativeInt(input.headlessWorkers, 0);
7
+ const queue = nonNegativeInt(input.queueDepth, 0);
8
+ return `SLOTS active ${active}/${visible} · headless ${headless} · q ${queue}`;
9
+ }
10
+ export async function renderZellijSlotColumnAnchorFromArtifacts(input) {
11
+ const root = path.resolve(input.artifactRoot);
12
+ const missionDir = inferMissionDir(root, input.missionId);
13
+ const snapshot = await readJson(path.join(missionDir, 'zellij-dashboard-snapshot.json'));
14
+ 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);
16
+ 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);
18
+ const queueDepth = Number(snapshot?.queue_depth ?? 0);
19
+ const anchorInput = { activeWorkers, visiblePaneCap, headlessWorkers, queueDepth };
20
+ if (input.mode !== undefined)
21
+ anchorInput.mode = input.mode;
22
+ return renderZellijSlotColumnAnchor(anchorInput);
23
+ }
24
+ export function buildZellijSlotColumnAnchorCommand(input) {
25
+ const args = [
26
+ input.cliPath,
27
+ 'zellij-slot-column-anchor',
28
+ '--mission', input.missionId,
29
+ '--mode', input.mode,
30
+ '--artifact-root', input.artifactRoot,
31
+ ...(input.watch ? ['--watch'] : [])
32
+ ];
33
+ return [input.nodePath || process.execPath, ...args].map(shellQuote).join(' ');
34
+ }
35
+ function inferMissionDir(root, missionId) {
36
+ if (path.basename(root) === 'agents' && path.basename(path.dirname(root)) === missionId)
37
+ return path.dirname(root);
38
+ if (path.basename(root) === missionId && path.basename(path.dirname(root)) === 'missions')
39
+ return root;
40
+ return path.join(root, '.sneakoscope', 'missions', missionId);
41
+ }
42
+ async function readJson(file) {
43
+ try {
44
+ return JSON.parse(await fs.promises.readFile(file, 'utf8'));
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ function nonNegativeInt(value, fallback) {
51
+ const parsed = Number(value);
52
+ if (!Number.isFinite(parsed) || parsed < 0)
53
+ return fallback;
54
+ return Math.floor(parsed);
55
+ }
56
+ function shellQuote(value) {
57
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
58
+ }
59
+ //# sourceMappingURL=zellij-slot-column-anchor.js.map
@@ -0,0 +1,82 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function renderZellijSlotPane(input) {
4
+ const mode = input.mode || 'compact-slots';
5
+ const maxLines = mode === 'compact-slots' ? 5 : mode === 'dashboard-plus-slots' ? 8 : 20;
6
+ const task = trimInline(input.currentFile || input.currentTask || '-', 56);
7
+ const heartbeat = input.heartbeatAgeMs == null
8
+ ? 'unknown'
9
+ : input.heartbeatAgeMs < 1000
10
+ ? 'now'
11
+ : `${Math.max(1, Math.round(input.heartbeatAgeMs / 1000))}s ago`;
12
+ const rows = [
13
+ `${input.slotId} gen-${Math.max(1, Math.floor(Number(input.generationIndex) || 1))}`,
14
+ `${trimInline(input.role || 'worker', 18)} - ${trimInline(input.backend || 'codex-sdk', 18)} - ${trimInline(input.worktreeId || '-', 18)}`,
15
+ `status: ${trimInline(input.status || 'running', 14)} ${task}`,
16
+ `patch: ${trimInline(input.patchStatus || 'queued', 18)} verify: ${trimInline(input.verifyStatus || 'queued', 18)}`,
17
+ `heartbeat: ${heartbeat}`
18
+ ];
19
+ return rows.slice(0, maxLines).join('\n');
20
+ }
21
+ export async function renderZellijSlotPaneFromArtifacts(input) {
22
+ const artifactDir = path.resolve(input.artifactDir);
23
+ const result = await readJson(path.join(artifactDir, 'worker-result.json'));
24
+ const heartbeatPath = path.join(artifactDir, 'worker-heartbeat.jsonl');
25
+ const heartbeatMtime = await statMtimeMs(heartbeatPath);
26
+ const now = Date.now();
27
+ return renderZellijSlotPane({
28
+ slotId: input.slotId,
29
+ generationIndex: input.generationIndex,
30
+ role: input.role || result?.persona_id || result?.agent_id || null,
31
+ backend: input.backend || result?.backend || null,
32
+ status: result?.status || (heartbeatMtime ? 'running' : 'launching'),
33
+ currentTask: result?.summary || null,
34
+ currentFile: Array.isArray(result?.changed_files) ? result.changed_files[0] : null,
35
+ patchStatus: Array.isArray(result?.patch_envelopes) && result.patch_envelopes.length ? 'candidate' : 'queued',
36
+ verifyStatus: result?.verification?.status || 'queued',
37
+ heartbeatAgeMs: heartbeatMtime ? now - heartbeatMtime : null,
38
+ worktreeId: result?.worktree?.id || null,
39
+ mode: input.mode || 'compact-slots'
40
+ });
41
+ }
42
+ export function buildZellijSlotPaneCommand(input) {
43
+ const args = [
44
+ input.cliPath,
45
+ 'zellij-slot-pane',
46
+ '--mission', input.missionId,
47
+ '--slot', input.slotId,
48
+ '--generation', String(Math.max(1, Math.floor(Number(input.generationIndex) || 1))),
49
+ '--artifact-dir', input.artifactDir,
50
+ '--mode', input.mode || 'compact-slots',
51
+ ...(input.backend ? ['--backend', input.backend] : []),
52
+ ...(input.role ? ['--role', input.role] : []),
53
+ ...(input.watch ? ['--watch'] : [])
54
+ ];
55
+ return [input.nodePath || process.execPath, ...args].map(shellQuote).join(' ');
56
+ }
57
+ async function readJson(file) {
58
+ try {
59
+ return JSON.parse(await fs.promises.readFile(file, 'utf8'));
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ async function statMtimeMs(file) {
66
+ try {
67
+ return (await fs.promises.stat(file)).mtimeMs;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ function trimInline(value, max) {
74
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
75
+ if (text.length <= max)
76
+ return text;
77
+ return text.slice(0, Math.max(1, max - 3)) + '...';
78
+ }
79
+ function shellQuote(value) {
80
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
81
+ }
82
+ //# sourceMappingURL=zellij-slot-pane-renderer.js.map
@@ -0,0 +1,16 @@
1
+ export function resolveZellijUiMode(args = [], env = process.env) {
2
+ const fromEnv = String(env.SKS_ZELLIJ_UI_MODE || '').trim();
3
+ if (fromEnv === 'full-debug')
4
+ return 'full-debug';
5
+ if (fromEnv === 'dashboard-plus-slots')
6
+ return 'dashboard-plus-slots';
7
+ if (args.includes('--zellij-dashboard'))
8
+ return 'dashboard-plus-slots';
9
+ if (args.includes('--zellij-full-debug'))
10
+ return 'full-debug';
11
+ return 'compact-slots';
12
+ }
13
+ export function zellijUiModeCreatesDashboard(mode) {
14
+ return mode !== 'compact-slots';
15
+ }
16
+ //# sourceMappingURL=zellij-ui-mode.js.map