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.
- package/README.md +6 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/.sks-build-stamp.json +4 -4
- package/dist/bin/sks.js +1 -1
- package/dist/build-manifest.json +32 -8
- package/dist/core/agents/agent-orchestrator.js +138 -3
- package/dist/core/agents/agent-patch-schema.js +16 -2
- package/dist/core/agents/agent-proof-evidence.js +3 -0
- package/dist/core/agents/native-cli-session-swarm.js +25 -4
- package/dist/core/agents/native-cli-worker.js +28 -1
- package/dist/core/codex-control/python-codex-sdk-adapter.js +28 -4
- package/dist/core/commands/naruto-command.js +39 -10
- package/dist/core/feature-registry.js +2 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/git/git-integration-worktree.js +15 -0
- package/dist/core/git/git-repo-detection.js +72 -0
- package/dist/core/git/git-worktree-cache-policy.js +36 -0
- package/dist/core/git/git-worktree-capability.js +54 -0
- package/dist/core/git/git-worktree-cleanup.js +51 -0
- package/dist/core/git/git-worktree-conflict-resolver.js +13 -0
- package/dist/core/git/git-worktree-diff.js +50 -0
- package/dist/core/git/git-worktree-manager.js +86 -0
- package/dist/core/git/git-worktree-merge-queue.js +55 -0
- package/dist/core/git/git-worktree-patch-envelope.js +35 -0
- package/dist/core/git/git-worktree-pool.js +23 -0
- package/dist/core/git/git-worktree-root.js +52 -0
- package/dist/core/git/git-worktree-runner.js +40 -0
- package/dist/core/naruto/naruto-active-pool.js +35 -0
- package/dist/core/naruto/naruto-gpt-final-pack.js +2 -0
- package/dist/core/naruto/naruto-work-graph.js +16 -1
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-naruto-dashboard.js +10 -1
- package/dist/core/zellij/zellij-worker-pane-manager.js +6 -4
- package/dist/scripts/git-worktree-cache-performance-check.js +25 -0
- package/dist/scripts/git-worktree-capability-check.js +27 -0
- package/dist/scripts/git-worktree-cleanup-check.js +27 -0
- package/dist/scripts/git-worktree-diff-export-check.js +43 -0
- package/dist/scripts/git-worktree-manager-check.js +37 -0
- package/dist/scripts/git-worktree-merge-queue-check.js +30 -0
- package/dist/scripts/git-worktree-pool-performance-check.js +20 -0
- package/dist/scripts/lib/git-worktree-fixture.js +33 -0
- package/dist/scripts/naruto-shadow-clone-swarm-check.js +9 -5
- package/dist/scripts/naruto-worktree-coding-check.js +44 -0
- package/dist/scripts/naruto-worktree-gpt-final-check.js +45 -0
- package/dist/scripts/naruto-worktree-zellij-ui-check.js +28 -0
- package/dist/scripts/release-parallel-check.js +1 -1
- package/package.json +14 -3
- package/schemas/git/git-worktree-capability.schema.json +19 -0
- 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("
|
|
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 {
|
|
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 =
|
|
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:
|
|
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
|
|
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.
|
|
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
|