sneakoscope 2.0.6 → 2.0.8

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 (79) hide show
  1. package/README.md +6 -1
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/.sks-build-stamp.json +4 -4
  6. package/dist/bin/sks.js +1 -1
  7. package/dist/build-manifest.json +56 -8
  8. package/dist/core/agents/agent-command-surface.js +4 -2
  9. package/dist/core/agents/agent-orchestrator.js +140 -4
  10. package/dist/core/agents/agent-patch-schema.js +20 -4
  11. package/dist/core/agents/agent-proof-evidence.js +3 -0
  12. package/dist/core/agents/native-cli-session-swarm.js +31 -5
  13. package/dist/core/agents/native-cli-worker.js +28 -1
  14. package/dist/core/codex-control/python-codex-sdk-adapter.js +28 -4
  15. package/dist/core/commands/mad-sks-command.js +25 -0
  16. package/dist/core/commands/naruto-command.js +68 -10
  17. package/dist/core/feature-registry.js +2 -0
  18. package/dist/core/fsx.js +1 -1
  19. package/dist/core/git/git-integration-worktree.js +15 -0
  20. package/dist/core/git/git-repo-detection.js +79 -0
  21. package/dist/core/git/git-worktree-cache-policy.js +36 -0
  22. package/dist/core/git/git-worktree-capability.js +54 -0
  23. package/dist/core/git/git-worktree-cleanup.js +62 -0
  24. package/dist/core/git/git-worktree-conflict-resolver.js +13 -0
  25. package/dist/core/git/git-worktree-diff.js +55 -0
  26. package/dist/core/git/git-worktree-manager.js +93 -0
  27. package/dist/core/git/git-worktree-merge-queue.js +55 -0
  28. package/dist/core/git/git-worktree-patch-envelope.js +35 -0
  29. package/dist/core/git/git-worktree-pool.js +23 -0
  30. package/dist/core/git/git-worktree-root.js +52 -0
  31. package/dist/core/git/git-worktree-runner.js +40 -0
  32. package/dist/core/naruto/naruto-active-pool.js +35 -0
  33. package/dist/core/naruto/naruto-gpt-final-pack.js +2 -0
  34. package/dist/core/naruto/naruto-work-graph.js +16 -1
  35. package/dist/core/release/release-gate-cache-v2.js +63 -0
  36. package/dist/core/release/release-gate-dag.js +179 -0
  37. package/dist/core/release/release-gate-hermetic-env.js +32 -0
  38. package/dist/core/release/release-gate-node.js +62 -0
  39. package/dist/core/release/release-gate-report.js +11 -0
  40. package/dist/core/release/release-gate-resource-governor.js +54 -0
  41. package/dist/core/release/release-gate-scheduler.js +15 -0
  42. package/dist/core/version.js +1 -1
  43. package/dist/core/zellij/zellij-dashboard-pane.js +71 -0
  44. package/dist/core/zellij/zellij-dashboard-renderer.js +42 -0
  45. package/dist/core/zellij/zellij-naruto-dashboard.js +10 -1
  46. package/dist/core/zellij/zellij-worker-pane-manager.js +68 -6
  47. package/dist/scripts/git-worktree-cache-performance-check.js +25 -0
  48. package/dist/scripts/git-worktree-capability-check.js +27 -0
  49. package/dist/scripts/git-worktree-cleanup-check.js +27 -0
  50. package/dist/scripts/git-worktree-diff-envelope-check.js +17 -0
  51. package/dist/scripts/git-worktree-diff-export-check.js +43 -0
  52. package/dist/scripts/git-worktree-dirty-lock-check.js +17 -0
  53. package/dist/scripts/git-worktree-dirty-main-detection-check.js +14 -0
  54. package/dist/scripts/git-worktree-integration-primary-check.js +22 -0
  55. package/dist/scripts/git-worktree-manager-check.js +37 -0
  56. package/dist/scripts/git-worktree-manifest-append-check.js +18 -0
  57. package/dist/scripts/git-worktree-merge-queue-check.js +30 -0
  58. package/dist/scripts/git-worktree-pool-performance-check.js +20 -0
  59. package/dist/scripts/git-worktree-untracked-diff-check.js +18 -0
  60. package/dist/scripts/lib/git-worktree-fixture.js +33 -0
  61. package/dist/scripts/naruto-shadow-clone-swarm-check.js +9 -5
  62. package/dist/scripts/naruto-worktree-coding-blackbox.js +29 -0
  63. package/dist/scripts/naruto-worktree-coding-check.js +44 -0
  64. package/dist/scripts/naruto-worktree-gpt-final-check.js +45 -0
  65. package/dist/scripts/naruto-worktree-zellij-ui-check.js +28 -0
  66. package/dist/scripts/release-gate-dag-runner-check.js +17 -0
  67. package/dist/scripts/release-gate-dag-runner.js +32 -0
  68. package/dist/scripts/release-gate-worker.js +10 -0
  69. package/dist/scripts/release-metadata-1-19-check.js +8 -2
  70. package/dist/scripts/release-parallel-check.js +1 -1
  71. package/dist/scripts/release-parallel-speed-budget-check.js +25 -0
  72. package/dist/scripts/release-stability-report-check.js +99 -0
  73. package/dist/scripts/zellij-dashboard-pane-check.js +68 -0
  74. package/dist/scripts/zellij-dashboard-watch.js +41 -0
  75. package/dist/scripts/zellij-worker-pane-real-ui-blackbox.js +185 -0
  76. package/package.json +33 -5
  77. package/schemas/git/git-worktree-capability.schema.json +19 -0
  78. package/schemas/git/git-worktree-manifest.schema.json +36 -0
  79. package/schemas/release/release-gate-node.schema.json +52 -0
@@ -26,6 +26,8 @@ class NativeCliSessionSwarmRecorder {
26
26
  await this.persist();
27
27
  }
28
28
  async launchWorker(ctx) {
29
+ const worktree = normalizeWorkerWorktree(ctx.agent?.worktree || ctx.slice?.worktree || ctx.opts?.worktree || null);
30
+ const workerCwd = worktree?.path || ctx.opts.cwd || packageRoot();
29
31
  const workerDirRel = path.join(ctx.agent.session_artifact_dir || path.join('sessions', ctx.agent.id), 'worker');
30
32
  const workerDir = path.join(this.root, workerDirRel);
31
33
  await ensureDir(workerDir);
@@ -45,6 +47,9 @@ class NativeCliSessionSwarmRecorder {
45
47
  backend_explicit: this.input.backendExplicit === true,
46
48
  no_ollama: this.input.noOllama === true || ctx.opts.noOllama === true,
47
49
  agent_root: this.root,
50
+ main_repo_root: worktree?.main_repo_root || ctx.opts.cwd || packageRoot(),
51
+ cwd: workerCwd,
52
+ worktree,
48
53
  agent: ctx.agent,
49
54
  slice: ctx.slice,
50
55
  worker_artifact_dir: workerDirRel,
@@ -89,13 +94,18 @@ class NativeCliSessionSwarmRecorder {
89
94
  patch_envelope_path: patchRel,
90
95
  fast_mode: this.input.fastModePolicy.fast_mode,
91
96
  service_tier: this.input.fastModePolicy.service_tier,
97
+ cwd: workerCwd,
92
98
  status: 'launching',
93
99
  exit_code: null,
94
100
  blockers: []
95
101
  };
96
102
  const stdout = fs.createWriteStream(path.join(this.root, stdoutRel), { flags: 'a' });
97
103
  const stderr = fs.createWriteStream(path.join(this.root, stderrRel), { flags: 'a' });
98
- if (this.input.backend === 'zellij' && ctx.opts.real === true && ctx.opts.zellijPaneWorker !== false) {
104
+ const placement = String(ctx.opts.workerPlacement || this.input.workerPlacement || (this.input.backend === 'zellij' ? 'zellij-pane' : 'process'));
105
+ const useZellijPane = placement === 'zellij-pane'
106
+ && ctx.opts.zellijPaneWorker !== false
107
+ && (ctx.opts.zellijSessionName || this.input.missionId);
108
+ if (useZellijPane) {
99
109
  stdout.end();
100
110
  stderr.end();
101
111
  return this.launchWorkerInZellijPane({
@@ -110,7 +120,7 @@ class NativeCliSessionSwarmRecorder {
110
120
  });
111
121
  }
112
122
  const child = spawn(process.execPath, args, {
113
- cwd: ctx.opts.cwd || packageRoot(),
123
+ cwd: workerCwd,
114
124
  env: {
115
125
  ...process.env,
116
126
  ...(ctx.opts.env || {}),
@@ -179,6 +189,8 @@ class NativeCliSessionSwarmRecorder {
179
189
  async launchWorkerInZellijPane(input) {
180
190
  const sessionName = String(input.ctx.opts.zellijSessionName || (this.input.missionId ? `sks-${this.input.missionId}` : 'sks-agent-runtime'));
181
191
  const slotId = String(input.ctx.agent.slot_id || input.ctx.agent.id || 'slot-001');
192
+ const worktree = normalizeWorkerWorktree(input.ctx.agent?.worktree || input.ctx.slice?.worktree || input.ctx.opts?.worktree || null);
193
+ const workerCwd = worktree?.path || input.ctx.opts.cwd || packageRoot();
182
194
  const activeToken = this.nextPaneToken--;
183
195
  this.active.add(activeToken);
184
196
  this.maxObserved = Math.max(this.maxObserved, this.active.size);
@@ -222,9 +234,11 @@ class NativeCliSessionSwarmRecorder {
222
234
  patchEnvelopePath: input.record.patch_envelope_path,
223
235
  stdoutLog: input.stdoutRel,
224
236
  stderrLog: input.stderrRel,
225
- cwd: input.ctx.opts.cwd || packageRoot(),
237
+ cwd: workerCwd,
226
238
  providerContext,
227
- serviceTier: this.input.fastModePolicy.service_tier
239
+ serviceTier: this.input.fastModePolicy.service_tier,
240
+ worktree: worktree ? { id: worktree.id, path: worktree.path, branch: worktree.branch } : null,
241
+ backend: this.input.backend
228
242
  });
229
243
  const launchBlockers = paneRecord.blockers || [];
230
244
  input.record.command_line = ['zellij', '--session', sessionName, 'action', 'new-pane', '--direction', paneRecord.direction_applied, '--name', paneRecord.pane_name, '--', 'sh', '-lc', '<native-cli-worker-command>'];
@@ -239,6 +253,7 @@ class NativeCliSessionSwarmRecorder {
239
253
  input.record.provider = paneRecord.provider;
240
254
  input.record.service_tier = paneRecord.service_tier;
241
255
  input.record.provider_context = paneRecord.provider_context;
256
+ input.record.worktree = worktree;
242
257
  input.record.status = launchBlockers.length ? 'failed' : 'running';
243
258
  input.record.blockers = launchBlockers;
244
259
  await this.record(input.record);
@@ -323,7 +338,7 @@ class NativeCliSessionSwarmRecorder {
323
338
  paneRecord = await closeWorkerPane({
324
339
  root: this.root,
325
340
  paneRecord,
326
- cwd: input.ctx.opts.cwd || packageRoot(),
341
+ cwd: workerCwd,
327
342
  status: input.record.status === 'closed' ? 'closed' : 'failed',
328
343
  blockers: input.record.blockers,
329
344
  sdkThreadId,
@@ -462,4 +477,15 @@ function redactWorkerArgs(args) {
462
477
  function shellQuote(value) {
463
478
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
464
479
  }
480
+ function normalizeWorkerWorktree(value) {
481
+ const pathValue = value?.path || value?.worktree_path;
482
+ if (!pathValue)
483
+ return null;
484
+ return {
485
+ id: String(value?.id || value?.worktree_id || value?.slot_id || 'worktree'),
486
+ path: String(pathValue),
487
+ branch: String(value?.branch || 'unknown'),
488
+ main_repo_root: value?.main_repo_root == null ? null : String(value.main_repo_root)
489
+ };
490
+ }
465
491
  //# sourceMappingURL=native-cli-session-swarm.js.map
@@ -27,6 +27,8 @@ export async function runNativeCliWorker(input = {}) {
27
27
  };
28
28
  const slice = intake.slice || {};
29
29
  const backend = String(input.backend || intake.backend || 'fake');
30
+ const workerCwd = path.resolve(String(input.cwd || intake.cwd || process.cwd()));
31
+ const worktree = normalizeWorkerWorktree(input.worktree || intake.worktree || null);
30
32
  const policy = resolveFastModePolicy({
31
33
  fastMode: intake.fast_mode ?? input.fastMode,
32
34
  serviceTier: intake.service_tier ?? input.serviceTier
@@ -37,6 +39,10 @@ export async function runNativeCliWorker(input = {}) {
37
39
  const heartbeatRel = String(input.heartbeatPath || intake.heartbeat_path || path.join(workerDirRel, 'worker-heartbeat.jsonl'));
38
40
  const patchRel = String(input.patchEnvelopePath || intake.patch_envelope_path || path.join(workerDirRel, 'worker-patch-envelope.json'));
39
41
  await ensureDir(workerDir);
42
+ try {
43
+ process.chdir(workerCwd);
44
+ }
45
+ catch { }
40
46
  const recursion = scanAgentTextForRecursion(JSON.stringify({ agent, slice, backend }));
41
47
  const guard = {
42
48
  schema: 'sks.native-cli-worker-recursion-guard.v1',
@@ -56,6 +62,9 @@ export async function runNativeCliWorker(input = {}) {
56
62
  persona_id: String(agent.persona_id || input.personaId || ''),
57
63
  lease_id: String(intake.lease_id || input.leaseId || ''),
58
64
  agent_root: agentRoot,
65
+ main_repo_root: String(input.mainRepoRoot || intake.main_repo_root || worktree?.main_repo_root || agentRoot),
66
+ cwd: workerCwd,
67
+ worktree,
59
68
  worker_artifact_dir: workerDirRel,
60
69
  result_path: resultRel,
61
70
  heartbeat_path: heartbeatRel,
@@ -65,7 +74,9 @@ export async function runNativeCliWorker(input = {}) {
65
74
  backend: backend === 'zellij' ? 'codex-sdk' : backend,
66
75
  output_schema_id: 'sks.agent-worker-result.v1',
67
76
  sandbox_policy: Array.isArray(slice.write_paths) && slice.write_paths.length > 0 ? 'workspace-write' : 'read-only',
68
- thread_policy: 'new_thread_per_generation'
77
+ thread_policy: 'new_thread_per_generation',
78
+ worktree,
79
+ cwd: workerCwd
69
80
  },
70
81
  service_tier: policy.service_tier,
71
82
  fast_mode: policy.fast_mode,
@@ -143,6 +154,8 @@ export async function runNativeCliWorker(input = {}) {
143
154
  generated_at: nowIso(),
144
155
  ok: guard.ok,
145
156
  backend,
157
+ cwd: workerCwd,
158
+ worktree,
146
159
  agent_id: agent.id,
147
160
  session_id: agent.session_id,
148
161
  slot_id: agent.slot_id || null,
@@ -221,6 +234,9 @@ export async function runNativeCliWorker(input = {}) {
221
234
  slot_id: agent.slot_id || null,
222
235
  generation_index: agent.generation_index || null,
223
236
  process_id: process.pid,
237
+ main_repo_root: String(input.mainRepoRoot || intake.main_repo_root || worktree?.main_repo_root || agentRoot),
238
+ cwd: workerCwd,
239
+ worktree,
224
240
  artifact_dir: workerDirRel,
225
241
  patch_envelope: patchEnvelopes.length ? patchRel : null,
226
242
  no_patch_reason: patchEnvelopes.length ? null : path.join(workerDirRel, 'worker-no-patch-reason.json'),
@@ -290,4 +306,15 @@ function redactCommandLine(argv) {
290
306
  return part;
291
307
  });
292
308
  }
309
+ function normalizeWorkerWorktree(value) {
310
+ const pathValue = value?.path || value?.worktree_path;
311
+ if (!pathValue)
312
+ return null;
313
+ return {
314
+ id: String(value?.id || value?.worktree_id || value?.slot_id || 'worktree'),
315
+ path: String(pathValue),
316
+ branch: String(value?.branch || 'unknown'),
317
+ main_repo_root: value?.main_repo_root == null ? null : String(value.main_repo_root)
318
+ };
319
+ }
293
320
  //# sourceMappingURL=native-cli-worker.js.map
@@ -9,18 +9,24 @@ export async function detectPythonCodexSdkCapability() {
9
9
  const pyOk = parsePythonVersion(python.versionText) >= 3.10;
10
10
  const probes = [];
11
11
  let detected = null;
12
+ let detectedModulePath = '';
13
+ let detectedModuleParentPath = '';
12
14
  if (pyOk) {
13
15
  for (const candidate of PYTHON_CODEX_SDK_CANDIDATES) {
14
- const importProbe = await runProcess(python.path, ['-c', `import ${candidate.importName}; print("ok")`], { timeoutMs: 5000, maxOutputBytes: 4096 })
16
+ const importProbe = await runProcess(python.path, ['-c', `import ${candidate.importName} as m, os; print(os.path.dirname(os.path.abspath(getattr(m, "__file__", ""))))`], { timeoutMs: 5000, maxOutputBytes: 4096 })
15
17
  .catch((err) => ({ code: 1, stdout: '', stderr: err.message || String(err) }));
18
+ const modulePath = String(importProbe.stdout || '').trim().split(/\r?\n/).filter(Boolean).at(-1) || '';
16
19
  probes.push({
17
20
  package_name: candidate.packageName,
18
21
  import_name: candidate.importName,
19
22
  ok: importProbe.code === 0,
23
+ module_path: modulePath || null,
20
24
  stderr: String(importProbe.stderr || '').slice(-500)
21
25
  });
22
26
  if (importProbe.code === 0) {
23
27
  detected = candidate;
28
+ detectedModulePath = modulePath;
29
+ detectedModuleParentPath = modulePath ? path.dirname(modulePath) : '';
24
30
  break;
25
31
  }
26
32
  }
@@ -29,7 +35,7 @@ export async function detectPythonCodexSdkCapability() {
29
35
  ...(pyOk ? [] : ['python_version_below_3_10']),
30
36
  ...(detected ? [] : ['python_codex_sdk_unavailable'])
31
37
  ];
32
- return capability(blockers.length === 0, python.path, python.versionText, blockers, detected, blockers.length ? setupAction(python.path) : null, probes);
38
+ return capability(blockers.length === 0, python.path, python.versionText, blockers, detected, blockers.length ? setupAction(python.path) : null, probes, detectedModulePath, detectedModuleParentPath);
33
39
  }
34
40
  export async function runPythonCodexSdkTask(input, opts = {}) {
35
41
  const cap = await detectPythonCodexSdkCapability();
@@ -48,7 +54,7 @@ export async function runPythonCodexSdkTask(input, opts = {}) {
48
54
  prompt: input.prompt,
49
55
  output_schema: input.outputSchema || {}
50
56
  };
51
- const events = await runPythonRunner(python, request, opts.env);
57
+ const events = await runPythonRunner(python, request, pythonRunnerEnv(opts.env, cap.module_parent_path));
52
58
  const translatedEvents = translatePythonCodexSdkEvents(events);
53
59
  const last = [...events].reverse().find((event) => event?.event === 'turn_completed');
54
60
  const errors = events.filter((event) => event?.event === 'error').map((event) => String(event.message || 'python_codex_sdk_error'));
@@ -63,6 +69,22 @@ export async function runPythonCodexSdkTask(input, opts = {}) {
63
69
  capability: cap
64
70
  };
65
71
  }
72
+ function pythonRunnerEnv(envOverride, moduleParentPath) {
73
+ const env = {};
74
+ for (const [key, value] of Object.entries(envOverride || process.env)) {
75
+ if (value !== undefined)
76
+ env[key] = String(value);
77
+ }
78
+ const parent = String(moduleParentPath || '').trim();
79
+ if (!parent)
80
+ return env;
81
+ env.PYTHONPATH = prependPath(env.PYTHONPATH, parent);
82
+ return env;
83
+ }
84
+ function prependPath(value, entry) {
85
+ const parts = String(value || '').split(path.delimiter).filter(Boolean);
86
+ return [entry, ...parts.filter((part) => part !== entry)].join(path.delimiter);
87
+ }
66
88
  function runPythonRunner(python, request, envOverride) {
67
89
  const runner = path.join(packageRoot(), 'pytools', 'codex_sdk_runner.py');
68
90
  return new Promise((resolve, reject) => {
@@ -177,7 +199,7 @@ function setupAction(pythonBin) {
177
199
  `If your environment provides the directive package, run \`${pythonBin} -m pip install openai-codex\`.`
178
200
  ].join(' ');
179
201
  }
180
- function capability(ok, pythonBin, versionText, blockers, detected, setupActionValue, probes = []) {
202
+ function capability(ok, pythonBin, versionText, blockers, detected, setupActionValue, probes = [], modulePath = '', moduleParentPath = '') {
181
203
  const selected = detected || PYTHON_CODEX_SDK_CANDIDATES[0] || { packageName: 'codex-app-server', importName: 'codex_app_server', source: 'developers.openai.com/codex/sdk' };
182
204
  return {
183
205
  schema: 'sks.python-codex-sdk-capability.v1',
@@ -189,6 +211,8 @@ function capability(ok, pythonBin, versionText, blockers, detected, setupActionV
189
211
  source: selected.source,
190
212
  supported_packages: PYTHON_CODEX_SDK_CANDIDATES.map((candidate) => candidate.packageName),
191
213
  supported_imports: PYTHON_CODEX_SDK_CANDIDATES.map((candidate) => candidate.importName),
214
+ module_path: modulePath || null,
215
+ module_parent_path: moduleParentPath || null,
192
216
  import_probes: probes,
193
217
  setup_action: setupActionValue,
194
218
  blockers
@@ -7,6 +7,7 @@ import { createMission, setCurrent } from '../mission.js';
7
7
  import { buildMadHighLaunchProfileNoWrite, madHighProfileName } from '../auto-review.js';
8
8
  import { permissionGateSummary } from '../permission-gates.js';
9
9
  import { attachZellijSessionInteractive, launchMadZellijUi, sanitizeZellijSessionName } from '../zellij/zellij-launcher.js';
10
+ import { openZellijDashboardPane } from '../zellij/zellij-dashboard-pane.js';
10
11
  import { createMadSksAuthorizationManifest, validateMadSksAuthorizationManifest } from '../mad-sks/authorization-manifest.js';
11
12
  import { createMadSksAuditLedger, madSksAuditAction, writeMadSksAuditLedger } from '../mad-sks/audit-ledger.js';
12
13
  import { compareProtectedCoreSnapshots, evaluateMadSksWrite, resolveProtectedCore, snapshotProtectedCore } from '../mad-sks/immutable-harness-guard.js';
@@ -113,6 +114,30 @@ export async function madHighCommand(args = [], deps = {}) {
113
114
  console.log(`MAD Zellij action: ${formatMadZellijAction(launch)}`);
114
115
  return launch;
115
116
  }
117
+ launch.dashboard_pane = await openZellijDashboardPane({
118
+ root: madLaunch.root,
119
+ missionId: madLaunch.mission_id,
120
+ sessionName: launch.session_name,
121
+ cwd: process.cwd(),
122
+ snapshot: {
123
+ mode: 'mad-sks',
124
+ backend_counts: { zellij: 1 },
125
+ placement_counts: { 'zellij-pane': 1, headless: 0 },
126
+ active_workers: 1,
127
+ visible_panes: 1,
128
+ headless_workers: 0,
129
+ queue_depth: 0,
130
+ worktrees: { active: 0, completed: 0, retained: 0 },
131
+ local_llm: { tps: 0, queue: 0 },
132
+ gpt_final_status: 'pending',
133
+ gate_progress: 'mad-sks:session-open'
134
+ }
135
+ }).catch((err) => ({
136
+ ok: false,
137
+ pane_kind: 'dashboard',
138
+ worker_pane: false,
139
+ blockers: [`zellij_dashboard_exception:${err?.message || String(err)}`]
140
+ }));
116
141
  const madNativeSwarm = await startMadNativeSwarm(madLaunch.root, madLaunch, args, profile, {
117
142
  env: {
118
143
  ...madSksEnv,
@@ -10,11 +10,13 @@ import { attachZellijSessionInteractive, launchZellijLayout } from '../zellij/ze
10
10
  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
- import { simulateNarutoActivePool } from '../naruto/naruto-active-pool.js';
13
+ import { runNarutoActivePool } from '../naruto/naruto-active-pool.js';
14
14
  import { buildNarutoVerificationDag } from '../naruto/naruto-verification-dag.js';
15
15
  import { buildNarutoGptFinalPack } from '../naruto/naruto-gpt-final-pack.js';
16
16
  import { planNarutoZellijDashboard } from '../zellij/zellij-naruto-dashboard.js';
17
+ import { openZellijDashboardPane } from '../zellij/zellij-dashboard-pane.js';
17
18
  import { checkPromptPlaceholders } from '../prompt/prompt-placeholder-guard.js';
19
+ import { evaluateGitWorktreeCapability } from '../git/git-worktree-capability.js';
18
20
  const NARUTO_RESULT_SCHEMA = 'sks.naruto-command-result.v1';
19
21
  const NARUTO_ROUTE = '$Naruto';
20
22
  // $Naruto — Shadow Clone Swarm (影分身 / Kage Bunshin no Jutsu).
@@ -61,6 +63,25 @@ async function narutoRun(parsed) {
61
63
  readonly: parsed.readonly,
62
64
  maxAgentCount: MAX_NARUTO_AGENT_COUNT
63
65
  });
66
+ const mission = await createMission(root, { mode: 'naruto', prompt: parsed.prompt });
67
+ const gitWorktreeCapability = writeCapable
68
+ ? await evaluateGitWorktreeCapability({ root, missionId: mission.id })
69
+ : null;
70
+ const worktreePolicy = gitWorktreeCapability?.mode === 'git-worktree'
71
+ ? {
72
+ mode: 'git-worktree',
73
+ required: true,
74
+ main_repo_root: gitWorktreeCapability.detection.root,
75
+ worktree_root: gitWorktreeCapability.root_resolution?.root || null,
76
+ fallback_reason: null
77
+ }
78
+ : {
79
+ mode: 'patch-envelope-only',
80
+ required: false,
81
+ main_repo_root: gitWorktreeCapability?.detection.root || null,
82
+ worktree_root: null,
83
+ fallback_reason: writeCapable ? (gitWorktreeCapability?.blockers.join(';') || 'not_git_repo_or_worktree_unavailable') : 'readonly_or_write_disabled'
84
+ };
64
85
  // The clone roster is the full work fan-out; live concurrency is throttled to a
65
86
  // system-safe number so naruto never spawns the whole count at once unless an
66
87
  // explicit operator override asks for a higher target.
@@ -74,7 +95,8 @@ async function narutoRun(parsed) {
74
95
  readonly: parsed.readonly,
75
96
  writeCapable,
76
97
  leaseBasePath: patchEnvelopeBasePath,
77
- maxActiveWorkers: parsed.concurrency || safe.cap
98
+ maxActiveWorkers: parsed.concurrency || safe.cap,
99
+ worktreePolicy
78
100
  });
79
101
  const roleDistribution = buildNarutoRoleDistribution(workGraph.work_items, { readonly: parsed.readonly });
80
102
  const governor = decideNarutoConcurrency({
@@ -86,22 +108,24 @@ async function narutoRun(parsed) {
86
108
  const backendMinimum = schedulerBackend === 'fake' ? roster.agent_count : Math.min(roster.agent_count, 2);
87
109
  const activeSlots = Math.max(1, Math.min(roster.agent_count, parsed.concurrency || Math.max(governor.safe_active_workers, backendMinimum), safe.cap));
88
110
  const zellijVisiblePanes = Math.max(1, Math.min(activeSlots, governor.safe_zellij_visible_panes));
89
- const activePool = simulateNarutoActivePool({ graph: workGraph, governor: { ...governor, safe_active_workers: activeSlots } });
111
+ const activePool = await runNarutoActivePool({ graph: workGraph, governor: { ...governor, safe_active_workers: activeSlots } });
90
112
  const verificationDag = buildNarutoVerificationDag(workGraph, { cwd: root });
91
113
  const gptFinalPack = buildNarutoGptFinalPack({
92
- missionId: 'pending',
114
+ missionId: mission.id,
93
115
  graph: workGraph,
94
116
  roleDistribution,
95
- localLlmMetrics: localWorker
117
+ localLlmMetrics: localWorker,
118
+ worktreePolicy,
119
+ worktreeDiffs: []
96
120
  });
97
121
  const zellijDashboard = planNarutoZellijDashboard({
98
122
  targetActiveWorkers: activeSlots,
99
123
  visiblePaneCap: governor.safe_zellij_visible_panes,
100
124
  backpressure: governor.backpressure,
101
125
  roles: roleDistribution.work_item_roles.map((row) => row.role),
102
- backend: schedulerBackend
126
+ backend: schedulerBackend,
127
+ worktreePolicy
103
128
  });
104
- const mission = await createMission(root, { mode: 'naruto', prompt: parsed.prompt });
105
129
  const ledgerRoot = path.join(mission.dir, 'agents');
106
130
  await writeNarutoArtifacts(ledgerRoot, {
107
131
  workGraph,
@@ -109,9 +133,10 @@ async function narutoRun(parsed) {
109
133
  governor,
110
134
  activePool,
111
135
  verificationDag,
112
- gptFinalPack: { ...gptFinalPack, mission_id: mission.id },
136
+ gptFinalPack,
113
137
  zellijDashboard,
114
- placeholderGuard
138
+ placeholderGuard,
139
+ gitWorktreeCapability
115
140
  });
116
141
  let liveZellij = null;
117
142
  if (!parsed.json && !parsed.mock && !parsed.noOpenZellij) {
@@ -125,6 +150,34 @@ async function narutoRun(parsed) {
125
150
  attach: false
126
151
  });
127
152
  if (liveZellij?.ok && liveZellij.capability?.status === 'ok') {
153
+ liveZellij.dashboard_pane = await openZellijDashboardPane({
154
+ root,
155
+ missionId: mission.id,
156
+ sessionName: liveZellij.session_name,
157
+ cwd: root,
158
+ snapshot: {
159
+ mode: 'naruto',
160
+ backend_counts: { [schedulerBackend]: activeSlots },
161
+ placement_counts: { 'zellij-pane': zellijVisiblePanes, headless: Math.max(0, activeSlots - zellijVisiblePanes) },
162
+ active_workers: activeSlots,
163
+ visible_panes: zellijVisiblePanes,
164
+ headless_workers: Math.max(0, activeSlots - zellijVisiblePanes),
165
+ queue_depth: Math.max(0, workGraph.total_work_items - activeSlots),
166
+ worktrees: {
167
+ active: worktreePolicy.mode === 'git-worktree' ? activeSlots : 0,
168
+ completed: 0,
169
+ retained: 0
170
+ },
171
+ local_llm: { tps: 0, queue: localWorker.auto_select_eligible ? Math.max(0, activeSlots - zellijVisiblePanes) : 0 },
172
+ gpt_final_status: 'pending',
173
+ gate_progress: 'naruto:pre-orchestrator'
174
+ }
175
+ }).catch((err) => ({
176
+ ok: false,
177
+ pane_kind: 'dashboard',
178
+ worker_pane: false,
179
+ blockers: [`zellij_dashboard_exception:${err?.message || String(err)}`]
180
+ }));
128
181
  console.log('Zellij: prepared ' + zellijVisiblePanes + ' visible active clone lane(s) in ' + liveZellij.session_name + ' with ' + Math.max(0, activeSlots - zellijVisiblePanes) + ' headless active worker(s). Attach with: ' + (liveZellij.attach_command_with_env || liveZellij.attach_command));
129
182
  if (parsed.attach)
130
183
  attachZellijSessionInteractive(liveZellij.session_name, { cwd: process.cwd(), configPath: liveZellij.clipboard_config_path });
@@ -165,6 +218,7 @@ async function narutoRun(parsed) {
165
218
  serviceTier: 'fast',
166
219
  noFast: false,
167
220
  writeMode: writeCapable ? parsed.writeMode || 'parallel' : 'off',
221
+ gitWorktreePolicy: worktreePolicy,
168
222
  json: parsed.json
169
223
  });
170
224
  const clones = result.roster?.agent_count ?? roster.agent_count;
@@ -188,8 +242,10 @@ async function narutoRun(parsed) {
188
242
  write_allowed_count: workGraph.write_allowed_count,
189
243
  active_wave_count: workGraph.active_waves.length,
190
244
  parallel_write_wave_count: workGraph.active_waves.filter((wave) => wave.write_paths.length > 1).length,
191
- ok: workGraph.ok
245
+ ok: workGraph.ok,
246
+ worktree_policy: workGraph.worktree_policy
192
247
  },
248
+ git_worktree: gitWorktreeCapability,
193
249
  role_distribution: roleDistribution,
194
250
  concurrency_governor: governor,
195
251
  active_pool: {
@@ -328,6 +384,8 @@ async function writeNarutoArtifacts(ledgerRoot, artifacts) {
328
384
  await writeJsonAtomic(path.join(ledgerRoot, 'naruto-gpt-final-pack.json'), artifacts.gptFinalPack);
329
385
  await writeJsonAtomic(path.join(ledgerRoot, 'naruto-zellij-dashboard.json'), artifacts.zellijDashboard);
330
386
  await writeJsonAtomic(path.join(ledgerRoot, 'prompt-placeholder-guard.json'), artifacts.placeholderGuard);
387
+ if (artifacts.gitWorktreeCapability)
388
+ await writeJsonAtomic(path.join(ledgerRoot, 'git-worktree-capability.json'), artifacts.gitWorktreeCapability);
331
389
  }
332
390
  function clampClones(value) {
333
391
  if (!Number.isFinite(value) || value < 1)
@@ -786,6 +786,8 @@ function isExternalPromptCommandMention(mention) {
786
786
  '$SKS_CODEX_APP_IMAGEGEN_OUTPUT',
787
787
  '$SKS_CODEX_APP_IMAGEGEN_OUTPUT_ID',
788
788
  '$SKS_CODEX_APP_IMAGEGEN_CREATED_AT',
789
+ '$SKS_WORKTREE_ROOT',
790
+ '$XDG_CACHE_HOME',
789
791
  '$IMAGEGEN'
790
792
  ].includes(normalized);
791
793
  }
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.6';
8
+ export const PACKAGE_VERSION = '2.0.8';
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() {
@@ -0,0 +1,15 @@
1
+ import { allocateWorkerWorktree } from './git-worktree-manager.js';
2
+ export async function createGitIntegrationWorktree(input) {
3
+ const allocationInput = {
4
+ repoRoot: input.repoRoot,
5
+ missionId: input.missionId,
6
+ workerId: 'integration',
7
+ slotId: 'integration',
8
+ generationIndex: 1,
9
+ branchPrefix: 'sks-integration'
10
+ };
11
+ if (input.baseRef !== undefined)
12
+ allocationInput.baseRef = input.baseRef;
13
+ return allocateWorkerWorktree(allocationInput);
14
+ }
15
+ //# sourceMappingURL=git-integration-worktree.js.map
@@ -0,0 +1,79 @@
1
+ import path from 'node:path';
2
+ import { which } from '../fsx.js';
3
+ import { gitBlocker, gitOutputLine, runGitCommand } from './git-worktree-runner.js';
4
+ export async function detectGitRepo(root = process.cwd()) {
5
+ const cwd = path.resolve(root);
6
+ const gitBinary = await which('git');
7
+ const blockers = [];
8
+ if (!gitBinary) {
9
+ return baseDetection(cwd, null, false, ['git_binary_missing']);
10
+ }
11
+ const inside = await runGitCommand(cwd, ['rev-parse', '--is-inside-work-tree']);
12
+ if (!inside.ok || gitOutputLine(inside) !== 'true') {
13
+ return baseDetection(cwd, gitBinary, true, []);
14
+ }
15
+ const top = await runGitCommand(cwd, ['rev-parse', '--show-toplevel']);
16
+ const gitDir = await runGitCommand(cwd, ['rev-parse', '--git-dir']);
17
+ const commonDir = await runGitCommand(cwd, ['rev-parse', '--git-common-dir']);
18
+ const bare = await runGitCommand(cwd, ['rev-parse', '--is-bare-repository']);
19
+ const branch = await runGitCommand(cwd, ['branch', '--show-current']);
20
+ const head = await runGitCommand(cwd, ['rev-parse', 'HEAD']);
21
+ const status = await runGitCommand(cwd, ['status', '--porcelain=v1', '--untracked-files=all']);
22
+ if (!top.ok)
23
+ blockers.push(gitBlocker('git_root_unresolved', top));
24
+ if (!gitDir.ok)
25
+ blockers.push(gitBlocker('git_dir_unresolved', gitDir));
26
+ if (!commonDir.ok)
27
+ blockers.push(gitBlocker('git_common_dir_unresolved', commonDir));
28
+ if (!head.ok)
29
+ blockers.push(gitBlocker('git_head_unresolved', head));
30
+ if (!status.ok)
31
+ blockers.push(gitBlocker('git_status_unresolved', status));
32
+ const repoRoot = top.ok ? path.resolve(gitOutputLine(top)) : null;
33
+ const resolvedGitDir = gitDir.ok ? absolutizeGitPath(cwd, gitOutputLine(gitDir)) : null;
34
+ const resolvedCommonDir = commonDir.ok ? absolutizeGitPath(cwd, gitOutputLine(commonDir)) : null;
35
+ return {
36
+ schema: 'sks.git-repo-detection.v1',
37
+ ok: blockers.length === 0,
38
+ cwd,
39
+ git_binary: gitBinary,
40
+ is_git_repo: true,
41
+ inside_work_tree: true,
42
+ bare: gitOutputLine(bare) === 'true',
43
+ root: repoRoot,
44
+ git_dir: resolvedGitDir,
45
+ common_dir: resolvedCommonDir,
46
+ worktree_git_dir: resolvedGitDir && resolvedCommonDir && resolvedGitDir !== resolvedCommonDir ? resolvedGitDir : null,
47
+ branch: gitOutputLine(branch) || null,
48
+ head: gitOutputLine(head) || null,
49
+ main_worktree_dirty: status.ok && status.stdout.trim().length > 0,
50
+ status_porcelain: status.stdout || '',
51
+ blockers
52
+ };
53
+ }
54
+ function baseDetection(cwd, gitBinary, gitAvailable, blockers) {
55
+ return {
56
+ schema: 'sks.git-repo-detection.v1',
57
+ ok: gitAvailable || blockers.length === 0,
58
+ cwd,
59
+ git_binary: gitBinary,
60
+ is_git_repo: false,
61
+ inside_work_tree: false,
62
+ bare: false,
63
+ root: null,
64
+ git_dir: null,
65
+ common_dir: null,
66
+ worktree_git_dir: null,
67
+ branch: null,
68
+ head: null,
69
+ main_worktree_dirty: false,
70
+ status_porcelain: '',
71
+ blockers
72
+ };
73
+ }
74
+ function absolutizeGitPath(cwd, value) {
75
+ if (!value)
76
+ return null;
77
+ return path.isAbsolute(value) ? path.resolve(value) : path.resolve(cwd, value);
78
+ }
79
+ //# sourceMappingURL=git-repo-detection.js.map
@@ -0,0 +1,36 @@
1
+ import { nowIso } from '../fsx.js';
2
+ export function planGitWorktreeCachePolicy(input) {
3
+ const nowMs = Math.max(0, Math.floor(Number(input.nowMs ?? Date.now())));
4
+ const maxEntries = Math.max(1, Math.floor(Number(input.maxEntries ?? 50)));
5
+ const maxBytes = Math.max(1024 * 1024, Math.floor(Number(input.maxBytes ?? 8 * 1024 * 1024 * 1024)));
6
+ const ttlMs = Math.max(60000, Math.floor(Number(input.ttlMs ?? 7 * 24 * 60 * 60 * 1000)));
7
+ const sorted = [...input.entries].sort((a, b) => a.updated_at_ms - b.updated_at_ms);
8
+ const prune = new Set();
9
+ let totalBytes = sorted.reduce((sum, entry) => sum + Math.max(0, entry.bytes), 0);
10
+ for (const entry of sorted) {
11
+ if (entry.dirty)
12
+ continue;
13
+ if (nowMs - entry.updated_at_ms > ttlMs)
14
+ prune.add(entry.path);
15
+ }
16
+ for (const entry of sorted) {
17
+ if (sorted.length - prune.size <= maxEntries && totalBytes <= maxBytes)
18
+ break;
19
+ if (entry.dirty || prune.has(entry.path))
20
+ continue;
21
+ prune.add(entry.path);
22
+ totalBytes -= Math.max(0, entry.bytes);
23
+ }
24
+ return {
25
+ schema: 'sks.git-worktree-cache-policy.v1',
26
+ ok: true,
27
+ generated_at: nowIso(),
28
+ max_entries: maxEntries,
29
+ max_bytes: maxBytes,
30
+ ttl_ms: ttlMs,
31
+ keep: sorted.filter((entry) => !prune.has(entry.path)).map((entry) => entry.path),
32
+ prune: sorted.filter((entry) => prune.has(entry.path)).map((entry) => entry.path),
33
+ dirty_retained: sorted.filter((entry) => entry.dirty === true).map((entry) => entry.path)
34
+ };
35
+ }
36
+ //# sourceMappingURL=git-worktree-cache-policy.js.map
@@ -0,0 +1,54 @@
1
+ import { ensureDir } from '../fsx.js';
2
+ import { detectGitRepo } from './git-repo-detection.js';
3
+ import { gitBlocker, runGitCommand } from './git-worktree-runner.js';
4
+ import { resolveGitWorktreeRoot } from './git-worktree-root.js';
5
+ export async function evaluateGitWorktreeCapability(input = {}) {
6
+ const requireGitWorktree = input.requireGitWorktree === true || process.env.SKS_REQUIRE_GIT_WORKTREE === '1';
7
+ const detection = await detectGitRepo(input.root || process.cwd());
8
+ const blockers = [...detection.blockers];
9
+ const gitAvailable = Boolean(detection.git_binary);
10
+ if (!detection.is_git_repo || !detection.root) {
11
+ if (requireGitWorktree)
12
+ blockers.push('git_worktree_required_but_not_git_repo');
13
+ return {
14
+ schema: 'sks.git-worktree-capability.v1',
15
+ ok: blockers.length === 0,
16
+ mode: 'patch-envelope-only',
17
+ require_git_worktree: requireGitWorktree,
18
+ git_available: gitAvailable,
19
+ is_git_repo: false,
20
+ worktree_supported: false,
21
+ worktree_probe_attempted: false,
22
+ detection,
23
+ root_resolution: null,
24
+ blockers
25
+ };
26
+ }
27
+ const rootResolution = resolveGitWorktreeRoot({
28
+ repoRoot: detection.root,
29
+ missionId: input.missionId || 'capability'
30
+ });
31
+ blockers.push(...rootResolution.blockers);
32
+ if (rootResolution.ok)
33
+ await ensureDir(rootResolution.root);
34
+ const list = await runGitCommand(detection.root, ['worktree', 'list', '--porcelain']);
35
+ const worktreeSupported = list.ok;
36
+ if (!worktreeSupported)
37
+ blockers.push(gitBlocker('git_worktree_list_failed', list));
38
+ if (requireGitWorktree && !worktreeSupported)
39
+ blockers.push('git_worktree_required_but_unsupported');
40
+ return {
41
+ schema: 'sks.git-worktree-capability.v1',
42
+ ok: blockers.length === 0,
43
+ mode: blockers.length === 0 && worktreeSupported ? 'git-worktree' : 'patch-envelope-only',
44
+ require_git_worktree: requireGitWorktree,
45
+ git_available: gitAvailable,
46
+ is_git_repo: true,
47
+ worktree_supported: worktreeSupported,
48
+ worktree_probe_attempted: true,
49
+ detection,
50
+ root_resolution: rootResolution,
51
+ blockers: [...new Set(blockers)]
52
+ };
53
+ }
54
+ //# sourceMappingURL=git-worktree-capability.js.map