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.
- package/README.md +12 -4
- 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 +37 -8
- package/dist/cli/install-helpers.js +23 -0
- package/dist/commands/codex-app.js +25 -3
- package/dist/commands/doctor.js +19 -4
- package/dist/commands/mad-sks.js +2 -2
- package/dist/core/agents/agent-orchestrator.js +160 -6
- package/dist/core/agents/agent-patch-schema.js +16 -2
- package/dist/core/agents/agent-proof-evidence.js +27 -2
- package/dist/core/agents/agent-worker-pipeline.js +9 -1
- package/dist/core/agents/native-cli-session-swarm.js +25 -4
- package/dist/core/agents/native-cli-worker.js +28 -1
- package/dist/core/agents/native-worker-backend-router.js +19 -1
- package/dist/core/codex-app.js +124 -2
- package/dist/core/codex-control/python-codex-sdk-adapter.js +28 -4
- package/dist/core/commands/naruto-command.js +48 -14
- 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/hooks-runtime.js +2 -233
- package/dist/core/init.js +8 -8
- package/dist/core/naruto/naruto-active-pool.js +55 -4
- package/dist/core/naruto/naruto-gpt-final-pack.js +2 -0
- package/dist/core/naruto/naruto-work-graph.js +16 -1
- package/dist/core/pipeline-internals/runtime-core.js +1 -1
- package/dist/core/ppt.js +31 -8
- package/dist/core/product-design-app-server.js +410 -0
- package/dist/core/product-design-plugin.js +139 -0
- package/dist/core/routes.js +8 -8
- 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-active-pool-check.js +13 -1
- package/dist/scripts/naruto-readonly-routing-check.js +116 -0
- package/dist/scripts/naruto-shadow-clone-swarm-check.js +16 -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/product-design-auto-install-check.js +119 -0
- package/dist/scripts/product-design-plugin-routing-check.js +101 -0
- package/dist/scripts/release-parallel-check.js +16 -2
- package/dist/scripts/release-provenance-check.js +21 -0
- package/package.json +17 -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).
|
|
@@ -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 ? [
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|