sneakoscope 2.0.6 → 2.0.7

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 (51) 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 +32 -8
  8. package/dist/core/agents/agent-orchestrator.js +138 -3
  9. package/dist/core/agents/agent-patch-schema.js +16 -2
  10. package/dist/core/agents/agent-proof-evidence.js +3 -0
  11. package/dist/core/agents/native-cli-session-swarm.js +25 -4
  12. package/dist/core/agents/native-cli-worker.js +28 -1
  13. package/dist/core/codex-control/python-codex-sdk-adapter.js +28 -4
  14. package/dist/core/commands/naruto-command.js +39 -10
  15. package/dist/core/feature-registry.js +2 -0
  16. package/dist/core/fsx.js +1 -1
  17. package/dist/core/git/git-integration-worktree.js +15 -0
  18. package/dist/core/git/git-repo-detection.js +72 -0
  19. package/dist/core/git/git-worktree-cache-policy.js +36 -0
  20. package/dist/core/git/git-worktree-capability.js +54 -0
  21. package/dist/core/git/git-worktree-cleanup.js +51 -0
  22. package/dist/core/git/git-worktree-conflict-resolver.js +13 -0
  23. package/dist/core/git/git-worktree-diff.js +50 -0
  24. package/dist/core/git/git-worktree-manager.js +86 -0
  25. package/dist/core/git/git-worktree-merge-queue.js +55 -0
  26. package/dist/core/git/git-worktree-patch-envelope.js +35 -0
  27. package/dist/core/git/git-worktree-pool.js +23 -0
  28. package/dist/core/git/git-worktree-root.js +52 -0
  29. package/dist/core/git/git-worktree-runner.js +40 -0
  30. package/dist/core/naruto/naruto-active-pool.js +35 -0
  31. package/dist/core/naruto/naruto-gpt-final-pack.js +2 -0
  32. package/dist/core/naruto/naruto-work-graph.js +16 -1
  33. package/dist/core/version.js +1 -1
  34. package/dist/core/zellij/zellij-naruto-dashboard.js +10 -1
  35. package/dist/core/zellij/zellij-worker-pane-manager.js +6 -4
  36. package/dist/scripts/git-worktree-cache-performance-check.js +25 -0
  37. package/dist/scripts/git-worktree-capability-check.js +27 -0
  38. package/dist/scripts/git-worktree-cleanup-check.js +27 -0
  39. package/dist/scripts/git-worktree-diff-export-check.js +43 -0
  40. package/dist/scripts/git-worktree-manager-check.js +37 -0
  41. package/dist/scripts/git-worktree-merge-queue-check.js +30 -0
  42. package/dist/scripts/git-worktree-pool-performance-check.js +20 -0
  43. package/dist/scripts/lib/git-worktree-fixture.js +33 -0
  44. package/dist/scripts/naruto-shadow-clone-swarm-check.js +9 -5
  45. package/dist/scripts/naruto-worktree-coding-check.js +44 -0
  46. package/dist/scripts/naruto-worktree-gpt-final-check.js +45 -0
  47. package/dist/scripts/naruto-worktree-zellij-ui-check.js +28 -0
  48. package/dist/scripts/release-parallel-check.js +1 -1
  49. package/package.json +14 -3
  50. package/schemas/git/git-worktree-capability.schema.json +19 -0
  51. package/schemas/git/git-worktree-manifest.schema.json +36 -0
@@ -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
@@ -10,11 +10,12 @@ 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
17
  import { checkPromptPlaceholders } from '../prompt/prompt-placeholder-guard.js';
18
+ import { evaluateGitWorktreeCapability } from '../git/git-worktree-capability.js';
18
19
  const NARUTO_RESULT_SCHEMA = 'sks.naruto-command-result.v1';
19
20
  const NARUTO_ROUTE = '$Naruto';
20
21
  // $Naruto — Shadow Clone Swarm (影分身 / Kage Bunshin no Jutsu).
@@ -61,6 +62,25 @@ async function narutoRun(parsed) {
61
62
  readonly: parsed.readonly,
62
63
  maxAgentCount: MAX_NARUTO_AGENT_COUNT
63
64
  });
65
+ const mission = await createMission(root, { mode: 'naruto', prompt: parsed.prompt });
66
+ const gitWorktreeCapability = writeCapable
67
+ ? await evaluateGitWorktreeCapability({ root, missionId: mission.id })
68
+ : null;
69
+ const worktreePolicy = gitWorktreeCapability?.mode === 'git-worktree'
70
+ ? {
71
+ mode: 'git-worktree',
72
+ required: true,
73
+ main_repo_root: gitWorktreeCapability.detection.root,
74
+ worktree_root: gitWorktreeCapability.root_resolution?.root || null,
75
+ fallback_reason: null
76
+ }
77
+ : {
78
+ mode: 'patch-envelope-only',
79
+ required: false,
80
+ main_repo_root: gitWorktreeCapability?.detection.root || null,
81
+ worktree_root: null,
82
+ fallback_reason: writeCapable ? (gitWorktreeCapability?.blockers.join(';') || 'not_git_repo_or_worktree_unavailable') : 'readonly_or_write_disabled'
83
+ };
64
84
  // The clone roster is the full work fan-out; live concurrency is throttled to a
65
85
  // system-safe number so naruto never spawns the whole count at once unless an
66
86
  // explicit operator override asks for a higher target.
@@ -74,7 +94,8 @@ async function narutoRun(parsed) {
74
94
  readonly: parsed.readonly,
75
95
  writeCapable,
76
96
  leaseBasePath: patchEnvelopeBasePath,
77
- maxActiveWorkers: parsed.concurrency || safe.cap
97
+ maxActiveWorkers: parsed.concurrency || safe.cap,
98
+ worktreePolicy
78
99
  });
79
100
  const roleDistribution = buildNarutoRoleDistribution(workGraph.work_items, { readonly: parsed.readonly });
80
101
  const governor = decideNarutoConcurrency({
@@ -86,22 +107,24 @@ async function narutoRun(parsed) {
86
107
  const backendMinimum = schedulerBackend === 'fake' ? roster.agent_count : Math.min(roster.agent_count, 2);
87
108
  const activeSlots = Math.max(1, Math.min(roster.agent_count, parsed.concurrency || Math.max(governor.safe_active_workers, backendMinimum), safe.cap));
88
109
  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 } });
110
+ const activePool = await runNarutoActivePool({ graph: workGraph, governor: { ...governor, safe_active_workers: activeSlots } });
90
111
  const verificationDag = buildNarutoVerificationDag(workGraph, { cwd: root });
91
112
  const gptFinalPack = buildNarutoGptFinalPack({
92
- missionId: 'pending',
113
+ missionId: mission.id,
93
114
  graph: workGraph,
94
115
  roleDistribution,
95
- localLlmMetrics: localWorker
116
+ localLlmMetrics: localWorker,
117
+ worktreePolicy,
118
+ worktreeDiffs: []
96
119
  });
97
120
  const zellijDashboard = planNarutoZellijDashboard({
98
121
  targetActiveWorkers: activeSlots,
99
122
  visiblePaneCap: governor.safe_zellij_visible_panes,
100
123
  backpressure: governor.backpressure,
101
124
  roles: roleDistribution.work_item_roles.map((row) => row.role),
102
- backend: schedulerBackend
125
+ backend: schedulerBackend,
126
+ worktreePolicy
103
127
  });
104
- const mission = await createMission(root, { mode: 'naruto', prompt: parsed.prompt });
105
128
  const ledgerRoot = path.join(mission.dir, 'agents');
106
129
  await writeNarutoArtifacts(ledgerRoot, {
107
130
  workGraph,
@@ -109,9 +132,10 @@ async function narutoRun(parsed) {
109
132
  governor,
110
133
  activePool,
111
134
  verificationDag,
112
- gptFinalPack: { ...gptFinalPack, mission_id: mission.id },
135
+ gptFinalPack,
113
136
  zellijDashboard,
114
- placeholderGuard
137
+ placeholderGuard,
138
+ gitWorktreeCapability
115
139
  });
116
140
  let liveZellij = null;
117
141
  if (!parsed.json && !parsed.mock && !parsed.noOpenZellij) {
@@ -165,6 +189,7 @@ async function narutoRun(parsed) {
165
189
  serviceTier: 'fast',
166
190
  noFast: false,
167
191
  writeMode: writeCapable ? parsed.writeMode || 'parallel' : 'off',
192
+ gitWorktreePolicy: worktreePolicy,
168
193
  json: parsed.json
169
194
  });
170
195
  const clones = result.roster?.agent_count ?? roster.agent_count;
@@ -188,8 +213,10 @@ async function narutoRun(parsed) {
188
213
  write_allowed_count: workGraph.write_allowed_count,
189
214
  active_wave_count: workGraph.active_waves.length,
190
215
  parallel_write_wave_count: workGraph.active_waves.filter((wave) => wave.write_paths.length > 1).length,
191
- ok: workGraph.ok
216
+ ok: workGraph.ok,
217
+ worktree_policy: workGraph.worktree_policy
192
218
  },
219
+ git_worktree: gitWorktreeCapability,
193
220
  role_distribution: roleDistribution,
194
221
  concurrency_governor: governor,
195
222
  active_pool: {
@@ -328,6 +355,8 @@ async function writeNarutoArtifacts(ledgerRoot, artifacts) {
328
355
  await writeJsonAtomic(path.join(ledgerRoot, 'naruto-gpt-final-pack.json'), artifacts.gptFinalPack);
329
356
  await writeJsonAtomic(path.join(ledgerRoot, 'naruto-zellij-dashboard.json'), artifacts.zellijDashboard);
330
357
  await writeJsonAtomic(path.join(ledgerRoot, 'prompt-placeholder-guard.json'), artifacts.placeholderGuard);
358
+ if (artifacts.gitWorktreeCapability)
359
+ await writeJsonAtomic(path.join(ledgerRoot, 'git-worktree-capability.json'), artifacts.gitWorktreeCapability);
331
360
  }
332
361
  function clampClones(value) {
333
362
  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.7';
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,72 @@
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
+ if (!top.ok)
22
+ blockers.push(gitBlocker('git_root_unresolved', top));
23
+ if (!gitDir.ok)
24
+ blockers.push(gitBlocker('git_dir_unresolved', gitDir));
25
+ if (!commonDir.ok)
26
+ blockers.push(gitBlocker('git_common_dir_unresolved', commonDir));
27
+ if (!head.ok)
28
+ blockers.push(gitBlocker('git_head_unresolved', head));
29
+ const repoRoot = top.ok ? path.resolve(gitOutputLine(top)) : null;
30
+ const resolvedGitDir = gitDir.ok ? absolutizeGitPath(cwd, gitOutputLine(gitDir)) : null;
31
+ const resolvedCommonDir = commonDir.ok ? absolutizeGitPath(cwd, gitOutputLine(commonDir)) : null;
32
+ return {
33
+ schema: 'sks.git-repo-detection.v1',
34
+ ok: blockers.length === 0,
35
+ cwd,
36
+ git_binary: gitBinary,
37
+ is_git_repo: true,
38
+ inside_work_tree: true,
39
+ bare: gitOutputLine(bare) === 'true',
40
+ root: repoRoot,
41
+ git_dir: resolvedGitDir,
42
+ common_dir: resolvedCommonDir,
43
+ worktree_git_dir: resolvedGitDir && resolvedCommonDir && resolvedGitDir !== resolvedCommonDir ? resolvedGitDir : null,
44
+ branch: gitOutputLine(branch) || null,
45
+ head: gitOutputLine(head) || null,
46
+ blockers
47
+ };
48
+ }
49
+ function baseDetection(cwd, gitBinary, gitAvailable, blockers) {
50
+ return {
51
+ schema: 'sks.git-repo-detection.v1',
52
+ ok: gitAvailable || blockers.length === 0,
53
+ cwd,
54
+ git_binary: gitBinary,
55
+ is_git_repo: false,
56
+ inside_work_tree: false,
57
+ bare: false,
58
+ root: null,
59
+ git_dir: null,
60
+ common_dir: null,
61
+ worktree_git_dir: null,
62
+ branch: null,
63
+ head: null,
64
+ blockers
65
+ };
66
+ }
67
+ function absolutizeGitPath(cwd, value) {
68
+ if (!value)
69
+ return null;
70
+ return path.isAbsolute(value) ? path.resolve(value) : path.resolve(cwd, value);
71
+ }
72
+ //# 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
@@ -0,0 +1,51 @@
1
+ import path from 'node:path';
2
+ import { nowIso, writeJsonAtomic } from '../fsx.js';
3
+ import { runGitCommand } from './git-worktree-runner.js';
4
+ export async function cleanupGitWorktree(input) {
5
+ const repoRoot = path.resolve(input.repoRoot);
6
+ const worktreePath = path.resolve(input.worktreePath);
7
+ const status = await runGitCommand(worktreePath, ['status', '--porcelain=v1', '--untracked-files=all']);
8
+ const clean = status.ok && status.stdout.trim().length === 0;
9
+ if (!clean) {
10
+ const lockPath = `${worktreePath}.retained.json`;
11
+ await writeJsonAtomic(lockPath, {
12
+ schema: 'sks.git-worktree-retention-lock.v1',
13
+ generated_at: nowIso(),
14
+ repo_root: repoRoot,
15
+ worktree_path: worktreePath,
16
+ branch: input.branch || null,
17
+ reason: status.ok ? 'dirty_worktree_retained' : 'status_failed_retained',
18
+ status_porcelain: status.stdout || null
19
+ });
20
+ return {
21
+ schema: 'sks.git-worktree-cleanup.v1',
22
+ ok: true,
23
+ generated_at: nowIso(),
24
+ repo_root: repoRoot,
25
+ worktree_path: worktreePath,
26
+ branch: input.branch || null,
27
+ clean: false,
28
+ action: 'retained_dirty',
29
+ retention_lock_path: lockPath,
30
+ blockers: []
31
+ };
32
+ }
33
+ const remove = await runGitCommand(repoRoot, ['worktree', 'remove', worktreePath]);
34
+ const blockers = remove.ok ? [] : ['git_worktree_remove_failed'];
35
+ if (remove.ok && input.deleteBranch === true && input.branch) {
36
+ await runGitCommand(repoRoot, ['branch', '-D', input.branch]);
37
+ }
38
+ return {
39
+ schema: 'sks.git-worktree-cleanup.v1',
40
+ ok: blockers.length === 0,
41
+ generated_at: nowIso(),
42
+ repo_root: repoRoot,
43
+ worktree_path: worktreePath,
44
+ branch: input.branch || null,
45
+ clean: true,
46
+ action: remove.ok ? 'removed' : 'remove_failed',
47
+ retention_lock_path: null,
48
+ blockers
49
+ };
50
+ }
51
+ //# sourceMappingURL=git-worktree-cleanup.js.map
@@ -0,0 +1,13 @@
1
+ export function summarizeGitWorktreeConflict(input) {
2
+ const stderr = String(input.stderr || '');
3
+ return {
4
+ schema: 'sks.git-worktree-conflict.v1',
5
+ ok: false,
6
+ worker_id: input.workerId,
7
+ changed_files: input.changedFiles,
8
+ stderr_tail: stderr.slice(-4000),
9
+ conflict_markers_possible: /conflict|patch failed|does not apply/i.test(stderr),
10
+ blockers: ['git_worktree_diff_apply_failed']
11
+ };
12
+ }
13
+ //# sourceMappingURL=git-worktree-conflict-resolver.js.map
@@ -0,0 +1,50 @@
1
+ import path from 'node:path';
2
+ import { nowIso } from '../fsx.js';
3
+ import { gitOutputLine, runGitCommand } from './git-worktree-runner.js';
4
+ export async function exportGitWorktreeDiff(input) {
5
+ const worktreePath = path.resolve(input.worktreePath);
6
+ const blockers = [];
7
+ const branch = await runGitCommand(worktreePath, ['branch', '--show-current']);
8
+ const head = await runGitCommand(worktreePath, ['rev-parse', 'HEAD']);
9
+ const status = await runGitCommand(worktreePath, ['status', '--porcelain=v1', '--untracked-files=all']);
10
+ const diff = await runGitCommand(worktreePath, ['diff', '--binary', '--full-index', 'HEAD']);
11
+ const names = await runGitCommand(worktreePath, ['diff', '--name-only', 'HEAD']);
12
+ const untracked = await runGitCommand(worktreePath, ['ls-files', '--others', '--exclude-standard']);
13
+ if (!status.ok)
14
+ blockers.push('git_worktree_status_failed');
15
+ if (!diff.ok)
16
+ blockers.push('git_worktree_diff_failed');
17
+ const untrackedFiles = lines(untracked.stdout);
18
+ const trackedChanged = lines(names.stdout);
19
+ const changedFiles = [...new Set([...trackedChanged, ...untrackedFiles, ...statusFiles(status.stdout)])];
20
+ return {
21
+ schema: 'sks.git-worktree-diff.v1',
22
+ ok: blockers.length === 0,
23
+ generated_at: nowIso(),
24
+ mission_id: input.missionId,
25
+ worker_id: input.workerId,
26
+ main_repo_root: path.resolve(input.mainRepoRoot),
27
+ worktree_path: worktreePath,
28
+ branch: gitOutputLine(branch) || null,
29
+ base_head: null,
30
+ worktree_head: gitOutputLine(head) || null,
31
+ status_porcelain: status.stdout,
32
+ changed_files: changedFiles,
33
+ untracked_files: untrackedFiles,
34
+ diff: diff.stdout,
35
+ diff_bytes: Buffer.byteLength(diff.stdout),
36
+ clean: changedFiles.length === 0 && status.stdout.trim().length === 0,
37
+ blockers
38
+ };
39
+ }
40
+ function lines(text) {
41
+ return String(text || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
42
+ }
43
+ function statusFiles(text) {
44
+ return lines(text).map((line) => {
45
+ const match = line.match(/^.{2}\s+(.*)$/) || line.match(/^\S+\s+(.*)$/);
46
+ const file = (match?.[1] || line).trim();
47
+ return file.includes(' -> ') ? file.split(' -> ').pop()?.trim() || file : file;
48
+ }).filter(Boolean);
49
+ }
50
+ //# sourceMappingURL=git-worktree-diff.js.map
@@ -0,0 +1,86 @@
1
+ import path from 'node:path';
2
+ import fsp from 'node:fs/promises';
3
+ import { ensureDir, nowIso, writeJsonAtomic } from '../fsx.js';
4
+ import { evaluateGitWorktreeCapability } from './git-worktree-capability.js';
5
+ import { gitBlocker, gitOutputLine, runGitCommand } from './git-worktree-runner.js';
6
+ import { sanitizePathPart } from './git-worktree-root.js';
7
+ export async function allocateWorkerWorktree(input) {
8
+ const capability = await evaluateGitWorktreeCapability({
9
+ root: input.repoRoot || process.cwd(),
10
+ missionId: input.missionId,
11
+ requireGitWorktree: true
12
+ });
13
+ const repoRoot = capability.detection.root || path.resolve(input.repoRoot || process.cwd());
14
+ const root = capability.root_resolution?.root || path.join(repoRoot, '.sneakoscope', 'blocked-worktrees');
15
+ const workerId = sanitizePathPart(input.workerId);
16
+ const slotId = sanitizePathPart(input.slotId || workerId);
17
+ const generationIndex = Math.max(1, Math.floor(Number(input.generationIndex || 1)));
18
+ const baseRef = input.baseRef || capability.detection.head || 'HEAD';
19
+ const branchPrefix = sanitizeBranchPart(input.branchPrefix || 'sks');
20
+ const branch = `${branchPrefix}/${sanitizeBranchPart(input.missionId)}/${sanitizeBranchPart(slotId)}-gen-${generationIndex}-${workerId}`;
21
+ const worktreePath = path.join(root, `${slotId}-gen-${generationIndex}-${workerId}`);
22
+ const blockers = [...capability.blockers];
23
+ let baseHead = capability.detection.head;
24
+ if (capability.ok) {
25
+ await ensureDir(root);
26
+ let add = null;
27
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
28
+ if (attempt > 1) {
29
+ await sleep(250 * attempt);
30
+ await runGitCommand(repoRoot, ['worktree', 'prune'], { timeoutMs: 30000 }).catch(() => null);
31
+ await fsp.rm(worktreePath, { recursive: true, force: true }).catch(() => null);
32
+ }
33
+ const existingBranch = await runGitCommand(repoRoot, ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`]);
34
+ const args = existingBranch.ok
35
+ ? ['worktree', 'add', worktreePath, branch]
36
+ : ['worktree', 'add', '-b', branch, worktreePath, baseRef];
37
+ add = await runGitCommand(repoRoot, args, { timeoutMs: 120000 });
38
+ if (add.ok)
39
+ break;
40
+ }
41
+ if (!add?.ok)
42
+ blockers.push(gitBlocker('git_worktree_add_failed', add));
43
+ if (add?.ok) {
44
+ const head = await runGitCommand(worktreePath, ['rev-parse', 'HEAD']);
45
+ baseHead = head.ok ? gitOutputLine(head) : baseHead;
46
+ }
47
+ }
48
+ const allocation = {
49
+ schema: 'sks.git-worktree-allocation.v1',
50
+ ok: blockers.length === 0,
51
+ created_at: nowIso(),
52
+ mission_id: input.missionId,
53
+ worker_id: workerId,
54
+ slot_id: slotId,
55
+ generation_index: generationIndex,
56
+ repo_root: repoRoot,
57
+ main_repo_root: repoRoot,
58
+ worktree_path: worktreePath,
59
+ branch,
60
+ base_ref: baseRef,
61
+ base_head: baseHead,
62
+ manifest_path: path.join(root, 'git-worktree-manifest.json'),
63
+ capability,
64
+ blockers: [...new Set(blockers)]
65
+ };
66
+ await appendWorktreeManifest(allocation);
67
+ return allocation;
68
+ }
69
+ async function appendWorktreeManifest(allocation) {
70
+ const manifest = {
71
+ schema: 'sks.git-worktree-manifest.v1',
72
+ updated_at: nowIso(),
73
+ mission_id: allocation.mission_id,
74
+ repo_root: allocation.repo_root,
75
+ root: path.dirname(allocation.worktree_path),
76
+ allocations: [allocation]
77
+ };
78
+ await writeJsonAtomic(allocation.manifest_path, manifest);
79
+ }
80
+ function sanitizeBranchPart(value) {
81
+ return sanitizePathPart(value).replace(/\./g, '-').slice(0, 48) || 'item';
82
+ }
83
+ function sleep(ms) {
84
+ return new Promise((resolve) => setTimeout(resolve, ms));
85
+ }
86
+ //# sourceMappingURL=git-worktree-manager.js.map
@@ -0,0 +1,55 @@
1
+ import { nowIso } from '../fsx.js';
2
+ import { runGitCommand } from './git-worktree-runner.js';
3
+ import { summarizeGitWorktreeConflict } from './git-worktree-conflict-resolver.js';
4
+ export async function applyGitWorktreeMergeQueue(input) {
5
+ const conflicts = [];
6
+ const changedFiles = new Set();
7
+ let appliedCount = 0;
8
+ let skippedCleanCount = 0;
9
+ for (const diff of input.diffs) {
10
+ for (const file of diff.changed_files)
11
+ changedFiles.add(file);
12
+ if (diff.clean || !diff.diff.trim()) {
13
+ skippedCleanCount += 1;
14
+ continue;
15
+ }
16
+ const check = await runGitCommand(input.integrationWorktreePath, ['apply', '--3way', '--check', '-'], {
17
+ input: diff.diff,
18
+ timeoutMs: 30000
19
+ });
20
+ if (!check.ok) {
21
+ conflicts.push(summarizeGitWorktreeConflict({
22
+ workerId: diff.worker_id,
23
+ changedFiles: diff.changed_files,
24
+ stderr: check.stderr || check.stdout
25
+ }));
26
+ continue;
27
+ }
28
+ const apply = await runGitCommand(input.integrationWorktreePath, ['apply', '--3way', '-'], {
29
+ input: diff.diff,
30
+ timeoutMs: 30000
31
+ });
32
+ if (apply.ok)
33
+ appliedCount += 1;
34
+ else {
35
+ conflicts.push(summarizeGitWorktreeConflict({
36
+ workerId: diff.worker_id,
37
+ changedFiles: diff.changed_files,
38
+ stderr: apply.stderr || apply.stdout
39
+ }));
40
+ }
41
+ }
42
+ const blockers = conflicts.length ? ['git_worktree_merge_queue_conflicts'] : [];
43
+ return {
44
+ schema: 'sks.git-worktree-merge-queue.v1',
45
+ ok: blockers.length === 0,
46
+ generated_at: nowIso(),
47
+ integration_worktree_path: input.integrationWorktreePath,
48
+ applied_count: appliedCount,
49
+ skipped_clean_count: skippedCleanCount,
50
+ conflicts,
51
+ changed_files: [...changedFiles],
52
+ blockers
53
+ };
54
+ }
55
+ //# sourceMappingURL=git-worktree-merge-queue.js.map