sneakoscope 2.0.5 → 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 (70) hide show
  1. package/README.md +12 -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/install-helpers.js +23 -0
  9. package/dist/commands/codex-app.js +25 -3
  10. package/dist/commands/doctor.js +19 -4
  11. package/dist/commands/mad-sks.js +2 -2
  12. package/dist/core/agents/agent-orchestrator.js +160 -6
  13. package/dist/core/agents/agent-patch-schema.js +16 -2
  14. package/dist/core/agents/agent-proof-evidence.js +27 -2
  15. package/dist/core/agents/agent-worker-pipeline.js +9 -1
  16. package/dist/core/agents/native-cli-session-swarm.js +25 -4
  17. package/dist/core/agents/native-cli-worker.js +28 -1
  18. package/dist/core/agents/native-worker-backend-router.js +19 -1
  19. package/dist/core/codex-app.js +124 -2
  20. package/dist/core/codex-control/python-codex-sdk-adapter.js +28 -4
  21. package/dist/core/commands/naruto-command.js +48 -14
  22. package/dist/core/feature-registry.js +2 -0
  23. package/dist/core/fsx.js +1 -1
  24. package/dist/core/git/git-integration-worktree.js +15 -0
  25. package/dist/core/git/git-repo-detection.js +72 -0
  26. package/dist/core/git/git-worktree-cache-policy.js +36 -0
  27. package/dist/core/git/git-worktree-capability.js +54 -0
  28. package/dist/core/git/git-worktree-cleanup.js +51 -0
  29. package/dist/core/git/git-worktree-conflict-resolver.js +13 -0
  30. package/dist/core/git/git-worktree-diff.js +50 -0
  31. package/dist/core/git/git-worktree-manager.js +86 -0
  32. package/dist/core/git/git-worktree-merge-queue.js +55 -0
  33. package/dist/core/git/git-worktree-patch-envelope.js +35 -0
  34. package/dist/core/git/git-worktree-pool.js +23 -0
  35. package/dist/core/git/git-worktree-root.js +52 -0
  36. package/dist/core/git/git-worktree-runner.js +40 -0
  37. package/dist/core/hooks-runtime.js +2 -233
  38. package/dist/core/init.js +8 -8
  39. package/dist/core/naruto/naruto-active-pool.js +55 -4
  40. package/dist/core/naruto/naruto-gpt-final-pack.js +2 -0
  41. package/dist/core/naruto/naruto-work-graph.js +16 -1
  42. package/dist/core/pipeline-internals/runtime-core.js +1 -1
  43. package/dist/core/ppt.js +31 -8
  44. package/dist/core/product-design-app-server.js +410 -0
  45. package/dist/core/product-design-plugin.js +139 -0
  46. package/dist/core/routes.js +8 -8
  47. package/dist/core/version.js +1 -1
  48. package/dist/core/zellij/zellij-naruto-dashboard.js +10 -1
  49. package/dist/core/zellij/zellij-worker-pane-manager.js +6 -4
  50. package/dist/scripts/git-worktree-cache-performance-check.js +25 -0
  51. package/dist/scripts/git-worktree-capability-check.js +27 -0
  52. package/dist/scripts/git-worktree-cleanup-check.js +27 -0
  53. package/dist/scripts/git-worktree-diff-export-check.js +43 -0
  54. package/dist/scripts/git-worktree-manager-check.js +37 -0
  55. package/dist/scripts/git-worktree-merge-queue-check.js +30 -0
  56. package/dist/scripts/git-worktree-pool-performance-check.js +20 -0
  57. package/dist/scripts/lib/git-worktree-fixture.js +33 -0
  58. package/dist/scripts/naruto-active-pool-check.js +13 -1
  59. package/dist/scripts/naruto-readonly-routing-check.js +116 -0
  60. package/dist/scripts/naruto-shadow-clone-swarm-check.js +16 -5
  61. package/dist/scripts/naruto-worktree-coding-check.js +44 -0
  62. package/dist/scripts/naruto-worktree-gpt-final-check.js +45 -0
  63. package/dist/scripts/naruto-worktree-zellij-ui-check.js +28 -0
  64. package/dist/scripts/product-design-auto-install-check.js +119 -0
  65. package/dist/scripts/product-design-plugin-routing-check.js +101 -0
  66. package/dist/scripts/release-parallel-check.js +16 -2
  67. package/dist/scripts/release-provenance-check.js +21 -0
  68. package/package.json +17 -3
  69. package/schemas/git/git-worktree-capability.schema.json +19 -0
  70. 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).
@@ -34,10 +35,11 @@ export async function narutoCommand(commandOrArgs = 'naruto', maybeArgs = []) {
34
35
  async function narutoRun(parsed) {
35
36
  const root = await sksRoot();
36
37
  const writeCapable = parsed.readonly !== true && parsed.writeMode !== 'off';
38
+ const patchEnvelopeBasePath = '.sneakoscope/naruto/patch-envelopes';
37
39
  const placeholderGuard = checkPromptPlaceholders({
38
40
  prompt: parsed.prompt,
39
41
  writeCapable,
40
- targetPaths: writeCapable ? ['.sneakoscope/naruto/patch-envelopes'] : []
42
+ targetPaths: writeCapable ? [patchEnvelopeBasePath] : []
41
43
  });
42
44
  if (!placeholderGuard.ok) {
43
45
  return emit(parsed, {
@@ -60,6 +62,25 @@ async function narutoRun(parsed) {
60
62
  readonly: parsed.readonly,
61
63
  maxAgentCount: MAX_NARUTO_AGENT_COUNT
62
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
+ };
63
84
  // The clone roster is the full work fan-out; live concurrency is throttled to a
64
85
  // system-safe number so naruto never spawns the whole count at once unless an
65
86
  // explicit operator override asks for a higher target.
@@ -72,8 +93,9 @@ async function narutoRun(parsed) {
72
93
  totalWorkItems: parsed.workItems,
73
94
  readonly: parsed.readonly,
74
95
  writeCapable,
75
- targetPaths: ['.sneakoscope/naruto/patch-envelopes'],
76
- maxActiveWorkers: parsed.concurrency || safe.cap
96
+ leaseBasePath: patchEnvelopeBasePath,
97
+ maxActiveWorkers: parsed.concurrency || safe.cap,
98
+ worktreePolicy
77
99
  });
78
100
  const roleDistribution = buildNarutoRoleDistribution(workGraph.work_items, { readonly: parsed.readonly });
79
101
  const governor = decideNarutoConcurrency({
@@ -85,22 +107,24 @@ async function narutoRun(parsed) {
85
107
  const backendMinimum = schedulerBackend === 'fake' ? roster.agent_count : Math.min(roster.agent_count, 2);
86
108
  const activeSlots = Math.max(1, Math.min(roster.agent_count, parsed.concurrency || Math.max(governor.safe_active_workers, backendMinimum), safe.cap));
87
109
  const zellijVisiblePanes = Math.max(1, Math.min(activeSlots, governor.safe_zellij_visible_panes));
88
- const activePool = simulateNarutoActivePool({ graph: workGraph, governor: { ...governor, safe_active_workers: activeSlots } });
110
+ const activePool = await runNarutoActivePool({ graph: workGraph, governor: { ...governor, safe_active_workers: activeSlots } });
89
111
  const verificationDag = buildNarutoVerificationDag(workGraph, { cwd: root });
90
112
  const gptFinalPack = buildNarutoGptFinalPack({
91
- missionId: 'pending',
113
+ missionId: mission.id,
92
114
  graph: workGraph,
93
115
  roleDistribution,
94
- localLlmMetrics: localWorker
116
+ localLlmMetrics: localWorker,
117
+ worktreePolicy,
118
+ worktreeDiffs: []
95
119
  });
96
120
  const zellijDashboard = planNarutoZellijDashboard({
97
121
  targetActiveWorkers: activeSlots,
98
122
  visiblePaneCap: governor.safe_zellij_visible_panes,
99
123
  backpressure: governor.backpressure,
100
124
  roles: roleDistribution.work_item_roles.map((row) => row.role),
101
- backend: schedulerBackend
125
+ backend: schedulerBackend,
126
+ worktreePolicy
102
127
  });
103
- const mission = await createMission(root, { mode: 'naruto', prompt: parsed.prompt });
104
128
  const ledgerRoot = path.join(mission.dir, 'agents');
105
129
  await writeNarutoArtifacts(ledgerRoot, {
106
130
  workGraph,
@@ -108,9 +132,10 @@ async function narutoRun(parsed) {
108
132
  governor,
109
133
  activePool,
110
134
  verificationDag,
111
- gptFinalPack: { ...gptFinalPack, mission_id: mission.id },
135
+ gptFinalPack,
112
136
  zellijDashboard,
113
- placeholderGuard
137
+ placeholderGuard,
138
+ gitWorktreeCapability
114
139
  });
115
140
  let liveZellij = null;
116
141
  if (!parsed.json && !parsed.mock && !parsed.noOpenZellij) {
@@ -163,7 +188,8 @@ async function narutoRun(parsed) {
163
188
  fastMode: true,
164
189
  serviceTier: 'fast',
165
190
  noFast: false,
166
- ...(parsed.writeMode ? { writeMode: parsed.writeMode } : {}),
191
+ writeMode: writeCapable ? parsed.writeMode || 'parallel' : 'off',
192
+ gitWorktreePolicy: worktreePolicy,
167
193
  json: parsed.json
168
194
  });
169
195
  const clones = result.roster?.agent_count ?? roster.agent_count;
@@ -185,8 +211,12 @@ async function narutoRun(parsed) {
185
211
  total_work_items: workGraph.total_work_items,
186
212
  mixed_work_kinds: workGraph.mixed_work_kinds,
187
213
  write_allowed_count: workGraph.write_allowed_count,
188
- ok: workGraph.ok
214
+ active_wave_count: workGraph.active_waves.length,
215
+ parallel_write_wave_count: workGraph.active_waves.filter((wave) => wave.write_paths.length > 1).length,
216
+ ok: workGraph.ok,
217
+ worktree_policy: workGraph.worktree_policy
189
218
  },
219
+ git_worktree: gitWorktreeCapability,
190
220
  role_distribution: roleDistribution,
191
221
  concurrency_governor: governor,
192
222
  active_pool: {
@@ -251,7 +281,9 @@ async function narutoStatus(parsed) {
251
281
  work_graph: workGraph ? {
252
282
  total_work_items: workGraph.total_work_items,
253
283
  mixed_work_kinds: workGraph.mixed_work_kinds,
254
- write_allowed_count: workGraph.write_allowed_count
284
+ write_allowed_count: workGraph.write_allowed_count,
285
+ active_wave_count: Array.isArray(workGraph.active_waves) ? workGraph.active_waves.length : null,
286
+ parallel_write_wave_count: Array.isArray(workGraph.active_waves) ? workGraph.active_waves.filter((wave) => Array.isArray(wave.write_paths) && wave.write_paths.length > 1).length : null
255
287
  } : null,
256
288
  concurrency_governor: governor
257
289
  };
@@ -323,6 +355,8 @@ async function writeNarutoArtifacts(ledgerRoot, artifacts) {
323
355
  await writeJsonAtomic(path.join(ledgerRoot, 'naruto-gpt-final-pack.json'), artifacts.gptFinalPack);
324
356
  await writeJsonAtomic(path.join(ledgerRoot, 'naruto-zellij-dashboard.json'), artifacts.zellijDashboard);
325
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);
326
360
  }
327
361
  function clampClones(value) {
328
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.5';
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