sneakoscope 2.0.7 → 2.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -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-command-surface.js +4 -2
- package/dist/core/agents/agent-orchestrator.js +2 -1
- package/dist/core/agents/agent-patch-schema.js +4 -2
- package/dist/core/agents/native-cli-session-swarm.js +7 -2
- package/dist/core/commands/mad-sks-command.js +25 -0
- package/dist/core/commands/naruto-command.js +29 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/git/git-repo-detection.js +7 -0
- package/dist/core/git/git-worktree-cleanup.js +14 -3
- package/dist/core/git/git-worktree-diff.js +7 -2
- package/dist/core/git/git-worktree-manager.js +9 -2
- package/dist/core/git/git-worktree-patch-envelope.js +5 -5
- package/dist/core/release/release-gate-cache-v2.js +63 -0
- package/dist/core/release/release-gate-dag.js +179 -0
- package/dist/core/release/release-gate-hermetic-env.js +32 -0
- package/dist/core/release/release-gate-node.js +62 -0
- package/dist/core/release/release-gate-report.js +11 -0
- package/dist/core/release/release-gate-resource-governor.js +54 -0
- package/dist/core/release/release-gate-scheduler.js +15 -0
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-dashboard-pane.js +71 -0
- package/dist/core/zellij/zellij-dashboard-renderer.js +42 -0
- package/dist/core/zellij/zellij-worker-pane-manager.js +63 -3
- package/dist/scripts/git-worktree-diff-envelope-check.js +17 -0
- package/dist/scripts/git-worktree-dirty-lock-check.js +17 -0
- package/dist/scripts/git-worktree-dirty-main-detection-check.js +14 -0
- package/dist/scripts/git-worktree-integration-primary-check.js +22 -0
- package/dist/scripts/git-worktree-manifest-append-check.js +18 -0
- package/dist/scripts/git-worktree-untracked-diff-check.js +18 -0
- package/dist/scripts/naruto-worktree-coding-blackbox.js +29 -0
- package/dist/scripts/release-gate-dag-runner-check.js +17 -0
- package/dist/scripts/release-gate-dag-runner.js +32 -0
- package/dist/scripts/release-gate-worker.js +10 -0
- package/dist/scripts/release-metadata-1-19-check.js +8 -2
- package/dist/scripts/release-parallel-speed-budget-check.js +25 -0
- package/dist/scripts/release-stability-report-check.js +99 -0
- package/dist/scripts/zellij-dashboard-pane-check.js +68 -0
- package/dist/scripts/zellij-dashboard-watch.js +41 -0
- package/dist/scripts/zellij-worker-pane-real-ui-blackbox.js +185 -0
- package/package.json +22 -5
- package/schemas/release/release-gate-node.schema.json +52 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { createReleaseGateHermeticEnv } from './release-gate-hermetic-env.js';
|
|
5
|
+
import { appendReleaseGateJsonl, writeReleaseGateJson } from './release-gate-report.js';
|
|
6
|
+
import { findReadyReleaseGateNodes, findReleaseGatesBlockedByFailedDeps, pickReadyLaunchableReleaseGates } from './release-gate-scheduler.js';
|
|
7
|
+
import { readReleaseGateCacheHit, writeReleaseGateCacheHit } from './release-gate-cache-v2.js';
|
|
8
|
+
import { RELEASE_GATE_NODE_SCHEMA, validateReleaseGateManifest } from './release-gate-node.js';
|
|
9
|
+
import { countReleaseGateResources, defaultReleaseGateBudget, summarizeReleaseGateBudget } from './release-gate-resource-governor.js';
|
|
10
|
+
export function loadReleaseGateManifest(root, file = 'release-gates.v2.json') {
|
|
11
|
+
const manifestPath = path.join(root, file);
|
|
12
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
13
|
+
const validation = validateReleaseGateManifest(parsed);
|
|
14
|
+
if (!validation.ok || !validation.manifest) {
|
|
15
|
+
throw new Error(`invalid ${file}: ${validation.errors.join(', ')}`);
|
|
16
|
+
}
|
|
17
|
+
return validation.manifest;
|
|
18
|
+
}
|
|
19
|
+
export async function runReleaseGateDag(input) {
|
|
20
|
+
const root = path.resolve(input.root);
|
|
21
|
+
const preset = input.preset || 'release';
|
|
22
|
+
const manifest = loadReleaseGateManifest(root);
|
|
23
|
+
const selected = selectPreset(manifest, preset);
|
|
24
|
+
const runId = `rg-${new Date().toISOString().replace(/[:.]/g, '-')}-${process.pid}`;
|
|
25
|
+
const reportDir = path.join(root, '.sneakoscope', 'reports', 'release-gates', runId);
|
|
26
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
27
|
+
const timeline = path.join(reportDir, 'timeline.jsonl');
|
|
28
|
+
const started = Date.now();
|
|
29
|
+
const pending = new Map(selected.map((gate) => [gate.id, gate]));
|
|
30
|
+
const running = new Map();
|
|
31
|
+
const completed = new Map();
|
|
32
|
+
const failed = new Map();
|
|
33
|
+
const budget = defaultReleaseGateBudget();
|
|
34
|
+
const peakResources = {};
|
|
35
|
+
let cached = 0;
|
|
36
|
+
let sumGateMs = 0;
|
|
37
|
+
let peakRunning = 0;
|
|
38
|
+
if (input.explain) {
|
|
39
|
+
writeReleaseGateJson(path.join(reportDir, 'explain.json'), { schema: RELEASE_GATE_NODE_SCHEMA, preset, budget, gates: selected.map((gate) => ({ id: gate.id, deps: gate.deps, resource: gate.resource, command: gate.command })) });
|
|
40
|
+
}
|
|
41
|
+
while (pending.size || running.size) {
|
|
42
|
+
const ready = findReadyReleaseGateNodes({ pending, completed, failed });
|
|
43
|
+
const launchable = pickReadyLaunchableReleaseGates({ ready, running: [...running.values()].map((row) => row.gate) });
|
|
44
|
+
let progressed = false;
|
|
45
|
+
for (const gate of launchable) {
|
|
46
|
+
pending.delete(gate.id);
|
|
47
|
+
const cacheHit = !input.noCache && gate.cache.enabled && readReleaseGateCacheHit(root, gate);
|
|
48
|
+
if (cacheHit) {
|
|
49
|
+
const result = { id: gate.id, ok: true, exit_code: 0, duration_ms: 0, cached: true, stderr_tail: '' };
|
|
50
|
+
completed.set(gate.id, result);
|
|
51
|
+
cached += 1;
|
|
52
|
+
progressed = true;
|
|
53
|
+
appendReleaseGateJsonl(timeline, { event: 'cache_hit', gate_id: gate.id, at: new Date().toISOString() });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
appendReleaseGateJsonl(timeline, { event: 'start', gate_id: gate.id, resource: gate.resource, at: new Date().toISOString() });
|
|
57
|
+
running.set(gate.id, { gate, promise: runGate(root, runId, reportDir, gate) });
|
|
58
|
+
peakRunning = Math.max(peakRunning, running.size);
|
|
59
|
+
const used = countReleaseGateResources([...running.values()].map((row) => row.gate));
|
|
60
|
+
for (const [resource, count] of Object.entries(used)) {
|
|
61
|
+
peakResources[resource] = Math.max(peakResources[resource] || 0, Number(count) || 0);
|
|
62
|
+
}
|
|
63
|
+
progressed = true;
|
|
64
|
+
}
|
|
65
|
+
if (!running.size) {
|
|
66
|
+
const blockedByFailedDeps = findReleaseGatesBlockedByFailedDeps({ pending, failed });
|
|
67
|
+
if (blockedByFailedDeps.length) {
|
|
68
|
+
for (const gate of blockedByFailedDeps) {
|
|
69
|
+
pending.delete(gate.id);
|
|
70
|
+
const result = {
|
|
71
|
+
id: gate.id,
|
|
72
|
+
ok: false,
|
|
73
|
+
exit_code: null,
|
|
74
|
+
duration_ms: 0,
|
|
75
|
+
cached: false,
|
|
76
|
+
stderr_tail: `blocked by failed dependency: ${gate.deps.filter((dep) => failed.has(dep)).join(', ')}`
|
|
77
|
+
};
|
|
78
|
+
failed.set(gate.id, result);
|
|
79
|
+
appendReleaseGateJsonl(timeline, { event: 'blocked_by_failed_dependency', gate_id: gate.id, deps: gate.deps.filter((dep) => failed.has(dep)), at: new Date().toISOString() });
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (progressed)
|
|
84
|
+
continue;
|
|
85
|
+
const blocked = [...pending.keys()];
|
|
86
|
+
throw new Error(`release gate DAG stalled: ${blocked.join(', ')}`);
|
|
87
|
+
}
|
|
88
|
+
const result = await Promise.race([...running.values()].map((row) => row.promise));
|
|
89
|
+
running.delete(result.id);
|
|
90
|
+
sumGateMs += result.duration_ms;
|
|
91
|
+
if (result.ok) {
|
|
92
|
+
completed.set(result.id, result);
|
|
93
|
+
const gate = selected.find((row) => row.id === result.id);
|
|
94
|
+
if (gate?.cache.enabled && !input.noCache)
|
|
95
|
+
writeReleaseGateCacheHit(root, gate);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
failed.set(result.id, result);
|
|
99
|
+
if (input.failFast) {
|
|
100
|
+
for (const id of [...pending.keys()])
|
|
101
|
+
pending.delete(id);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
appendReleaseGateJsonl(timeline, { event: result.ok ? 'pass' : 'fail', gate_id: result.id, duration_ms: result.duration_ms, at: new Date().toISOString() });
|
|
105
|
+
}
|
|
106
|
+
const wallMs = Date.now() - started;
|
|
107
|
+
const failures = [...failed.values()].map((row) => ({ id: row.id, exit_code: row.exit_code, stderr_tail: row.stderr_tail }));
|
|
108
|
+
const result = {
|
|
109
|
+
schema: 'sks.release-gate-dag-run.v1',
|
|
110
|
+
ok: failures.length === 0,
|
|
111
|
+
run_id: runId,
|
|
112
|
+
selected_preset: preset,
|
|
113
|
+
total_gates: manifest.gates.length,
|
|
114
|
+
selected_gates: selected.length,
|
|
115
|
+
completed: completed.size,
|
|
116
|
+
failed: failed.size,
|
|
117
|
+
cached,
|
|
118
|
+
wall_ms: wallMs,
|
|
119
|
+
sum_gate_ms: sumGateMs,
|
|
120
|
+
cpu_time_saved_ms: Math.max(0, sumGateMs - wallMs),
|
|
121
|
+
parallelism_gain: wallMs > 0 ? Number((sumGateMs / wallMs).toFixed(2)) : 1,
|
|
122
|
+
critical_path_ms: estimateCriticalPath(selected, completed),
|
|
123
|
+
peak_running: peakRunning,
|
|
124
|
+
peak_resources: peakResources,
|
|
125
|
+
budget_snapshot: budget,
|
|
126
|
+
budget_summary: summarizeReleaseGateBudget(budget),
|
|
127
|
+
report_dir: reportDir,
|
|
128
|
+
failures
|
|
129
|
+
};
|
|
130
|
+
writeReleaseGateJson(path.join(reportDir, 'summary.json'), result);
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
function selectPreset(manifest, preset) {
|
|
134
|
+
return manifest.gates.filter((gate) => gate.preset.includes(preset));
|
|
135
|
+
}
|
|
136
|
+
function runGate(root, runId, reportRoot, gate) {
|
|
137
|
+
const started = Date.now();
|
|
138
|
+
const hermetic = createReleaseGateHermeticEnv({ root, runId, gate, reportRoot });
|
|
139
|
+
const stdoutFile = path.join(hermetic.report_dir, 'stdout.log');
|
|
140
|
+
const stderrFile = path.join(hermetic.report_dir, 'stderr.log');
|
|
141
|
+
const out = fs.createWriteStream(stdoutFile);
|
|
142
|
+
const err = fs.createWriteStream(stderrFile);
|
|
143
|
+
return new Promise((resolve) => {
|
|
144
|
+
const child = spawn(gate.command, { cwd: root, shell: true, env: hermetic.env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
145
|
+
const timer = setTimeout(() => child.kill('SIGTERM'), gate.timeout_ms);
|
|
146
|
+
child.stdout.pipe(out);
|
|
147
|
+
child.stderr.pipe(err);
|
|
148
|
+
child.on('close', (code) => {
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
out.end();
|
|
151
|
+
err.end();
|
|
152
|
+
const durationMs = Date.now() - started;
|
|
153
|
+
const stderrTail = tail(fs.existsSync(stderrFile) ? fs.readFileSync(stderrFile, 'utf8') : '');
|
|
154
|
+
const result = { id: gate.id, ok: code === 0, exit_code: code, duration_ms: durationMs, cached: false, stderr_tail: stderrTail };
|
|
155
|
+
writeReleaseGateJson(path.join(hermetic.report_dir, 'result.json'), { schema: 'sks.release-gate-result.v1', ...result, stdout_log: stdoutFile, stderr_log: stderrFile });
|
|
156
|
+
resolve(result);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function estimateCriticalPath(gates, completed) {
|
|
161
|
+
const byId = new Map(gates.map((gate) => [gate.id, gate]));
|
|
162
|
+
const memo = new Map();
|
|
163
|
+
const visit = (id) => {
|
|
164
|
+
if (memo.has(id))
|
|
165
|
+
return memo.get(id);
|
|
166
|
+
const gate = byId.get(id);
|
|
167
|
+
if (!gate)
|
|
168
|
+
return 0;
|
|
169
|
+
const own = completed.get(id)?.duration_ms || 0;
|
|
170
|
+
const dep = Math.max(0, ...gate.deps.map(visit));
|
|
171
|
+
memo.set(id, own + dep);
|
|
172
|
+
return own + dep;
|
|
173
|
+
};
|
|
174
|
+
return Math.max(0, ...gates.map((gate) => visit(gate.id)));
|
|
175
|
+
}
|
|
176
|
+
function tail(value, limit = 1200) {
|
|
177
|
+
return value.length > limit ? value.slice(-limit) : value;
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=release-gate-dag.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
export function createReleaseGateHermeticEnv(input) {
|
|
5
|
+
const safeId = input.gate.id.replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
6
|
+
const tmpRoot = path.join(os.tmpdir(), 'sks-gate', input.runId, safeId);
|
|
7
|
+
const home = path.join(tmpRoot, 'home');
|
|
8
|
+
const codexHome = path.join(tmpRoot, 'codex-home');
|
|
9
|
+
const cacheHome = path.join(tmpRoot, 'xdg-cache');
|
|
10
|
+
const reportDir = path.join(input.reportRoot, safeId);
|
|
11
|
+
fs.mkdirSync(home, { recursive: true });
|
|
12
|
+
fs.mkdirSync(codexHome, { recursive: true });
|
|
13
|
+
fs.mkdirSync(cacheHome, { recursive: true });
|
|
14
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
15
|
+
return {
|
|
16
|
+
tmp_dir: tmpRoot,
|
|
17
|
+
report_dir: reportDir,
|
|
18
|
+
env: {
|
|
19
|
+
...process.env,
|
|
20
|
+
SKS_GATE_ID: input.gate.id,
|
|
21
|
+
SKS_GATE_RUN_ID: input.runId,
|
|
22
|
+
SKS_REPORT_DIR: reportDir,
|
|
23
|
+
SKS_TMP_DIR: tmpRoot,
|
|
24
|
+
HOME: input.gate.isolation.home === 'temp' ? home : process.env.HOME,
|
|
25
|
+
CODEX_HOME: input.gate.isolation.codex_home === 'temp' ? codexHome : process.env.CODEX_HOME,
|
|
26
|
+
XDG_CACHE_HOME: cacheHome,
|
|
27
|
+
SKS_DISABLE_REAL_MODEL_CALLS: input.gate.preset.includes('real-check') ? process.env.SKS_DISABLE_REAL_MODEL_CALLS || '0' : '1',
|
|
28
|
+
SKS_DISABLE_GLOBAL_CONFIG_MUTATION: '1'
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=release-gate-hermetic-env.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const RELEASE_GATE_NODE_SCHEMA = 'sks.release-gates.v2';
|
|
2
|
+
export const RELEASE_GATE_RESOURCE_CLASSES = [
|
|
3
|
+
'cpu-light',
|
|
4
|
+
'cpu-heavy',
|
|
5
|
+
'io-light',
|
|
6
|
+
'io-heavy',
|
|
7
|
+
'git',
|
|
8
|
+
'git-worktree',
|
|
9
|
+
'zellij-real',
|
|
10
|
+
'local-llm-real',
|
|
11
|
+
'remote-model-real',
|
|
12
|
+
'python',
|
|
13
|
+
'network',
|
|
14
|
+
'global-config',
|
|
15
|
+
'publish',
|
|
16
|
+
'fs-read'
|
|
17
|
+
];
|
|
18
|
+
export function validateReleaseGateManifest(input) {
|
|
19
|
+
const errors = [];
|
|
20
|
+
if (input?.schema !== RELEASE_GATE_NODE_SCHEMA)
|
|
21
|
+
errors.push('schema_mismatch');
|
|
22
|
+
if (!Array.isArray(input?.gates))
|
|
23
|
+
errors.push('gates_missing');
|
|
24
|
+
const ids = new Set();
|
|
25
|
+
const resources = new Set(RELEASE_GATE_RESOURCE_CLASSES);
|
|
26
|
+
for (const gate of Array.isArray(input?.gates) ? input.gates : []) {
|
|
27
|
+
if (!gate?.id)
|
|
28
|
+
errors.push('gate_id_missing');
|
|
29
|
+
if (gate?.id && ids.has(gate.id))
|
|
30
|
+
errors.push(`gate_duplicate:${gate.id}`);
|
|
31
|
+
if (gate?.id)
|
|
32
|
+
ids.add(gate.id);
|
|
33
|
+
if (!gate?.command)
|
|
34
|
+
errors.push(`gate_command_missing:${gate?.id || 'unknown'}`);
|
|
35
|
+
if (!Array.isArray(gate?.deps))
|
|
36
|
+
errors.push(`gate_deps_missing:${gate?.id || 'unknown'}`);
|
|
37
|
+
if (!Array.isArray(gate?.resource) || !gate.resource.length)
|
|
38
|
+
errors.push(`gate_resource_missing:${gate?.id || 'unknown'}`);
|
|
39
|
+
for (const resource of Array.isArray(gate?.resource) ? gate.resource : []) {
|
|
40
|
+
if (!resources.has(resource))
|
|
41
|
+
errors.push(`gate_unknown_resource:${gate?.id || 'unknown'}:${resource}`);
|
|
42
|
+
}
|
|
43
|
+
if (gate?.side_effect !== 'hermetic' && gate?.side_effect !== 'real-env')
|
|
44
|
+
errors.push(`gate_side_effect_invalid:${gate?.id || 'unknown'}`);
|
|
45
|
+
if (!Number.isFinite(Number(gate?.timeout_ms)) || Number(gate.timeout_ms) <= 0)
|
|
46
|
+
errors.push(`gate_timeout_missing:${gate?.id || 'unknown'}`);
|
|
47
|
+
if (!gate?.cache || typeof gate.cache.enabled !== 'boolean' || !Array.isArray(gate.cache.inputs))
|
|
48
|
+
errors.push(`gate_cache_missing:${gate?.id || 'unknown'}`);
|
|
49
|
+
if (!gate?.isolation || gate.isolation.report_dir !== 'per-gate')
|
|
50
|
+
errors.push(`gate_isolation_missing:${gate?.id || 'unknown'}`);
|
|
51
|
+
if (!Array.isArray(gate?.preset))
|
|
52
|
+
errors.push(`gate_preset_missing:${gate?.id || 'unknown'}`);
|
|
53
|
+
}
|
|
54
|
+
for (const gate of Array.isArray(input?.gates) ? input.gates : []) {
|
|
55
|
+
for (const dep of Array.isArray(gate?.deps) ? gate.deps : []) {
|
|
56
|
+
if (!ids.has(dep))
|
|
57
|
+
errors.push(`gate_unknown_dep:${gate.id}:${dep}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return errors.length ? { ok: false, errors } : { ok: true, manifest: input, errors };
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=release-gate-node.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function writeReleaseGateJson(file, value) {
|
|
4
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
5
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
6
|
+
}
|
|
7
|
+
export function appendReleaseGateJsonl(file, value) {
|
|
8
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
9
|
+
fs.appendFileSync(file, `${JSON.stringify(value)}\n`);
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=release-gate-report.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
export function defaultReleaseGateBudget() {
|
|
3
|
+
const cores = Math.max(1, os.cpus().length || 1);
|
|
4
|
+
return {
|
|
5
|
+
'cpu-light': Math.min(32, cores * 4),
|
|
6
|
+
'cpu-heavy': Math.max(1, cores - 1),
|
|
7
|
+
'io-light': Math.min(64, cores * 8),
|
|
8
|
+
'io-heavy': Math.min(8, cores),
|
|
9
|
+
git: Math.min(8, cores),
|
|
10
|
+
'git-worktree': Math.min(6, cores),
|
|
11
|
+
python: Math.min(8, cores),
|
|
12
|
+
network: 8,
|
|
13
|
+
'zellij-real': 1,
|
|
14
|
+
'local-llm-real': Math.max(1, Number(process.env.SKS_LOCAL_LLM_MAX_PARALLEL || 1)),
|
|
15
|
+
'remote-model-real': 4,
|
|
16
|
+
'global-config': 1,
|
|
17
|
+
publish: 1,
|
|
18
|
+
'fs-read': Math.min(64, cores * 8)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function summarizeReleaseGateBudget(budget = defaultReleaseGateBudget()) {
|
|
22
|
+
return Object.entries(budget)
|
|
23
|
+
.filter(([, value]) => Number(value) > 0)
|
|
24
|
+
.map(([resource, value]) => `${resource}=${value}`)
|
|
25
|
+
.join(' ');
|
|
26
|
+
}
|
|
27
|
+
export function countReleaseGateResources(running) {
|
|
28
|
+
return usedResources(running);
|
|
29
|
+
}
|
|
30
|
+
export function pickLaunchableReleaseGates(input) {
|
|
31
|
+
const budget = input.budget || defaultReleaseGateBudget();
|
|
32
|
+
const used = usedResources(input.running);
|
|
33
|
+
const launchable = [];
|
|
34
|
+
for (const gate of input.ready) {
|
|
35
|
+
if (fits(gate, used, budget)) {
|
|
36
|
+
launchable.push(gate);
|
|
37
|
+
for (const resource of gate.resource)
|
|
38
|
+
used[resource] = (used[resource] || 0) + 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return launchable;
|
|
42
|
+
}
|
|
43
|
+
function usedResources(running) {
|
|
44
|
+
const used = {};
|
|
45
|
+
for (const gate of running) {
|
|
46
|
+
for (const resource of gate.resource)
|
|
47
|
+
used[resource] = (used[resource] || 0) + 1;
|
|
48
|
+
}
|
|
49
|
+
return used;
|
|
50
|
+
}
|
|
51
|
+
function fits(gate, used, budget) {
|
|
52
|
+
return gate.resource.every((resource) => (used[resource] || 0) < budget[resource]);
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=release-gate-resource-governor.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defaultReleaseGateBudget, pickLaunchableReleaseGates } from './release-gate-resource-governor.js';
|
|
2
|
+
export function findReadyReleaseGateNodes(input) {
|
|
3
|
+
return [...input.pending.values()].filter((gate) => gate.deps.every((dep) => input.completed.has(dep)) && !gate.deps.some((dep) => input.failed.has(dep)));
|
|
4
|
+
}
|
|
5
|
+
export function findReleaseGatesBlockedByFailedDeps(input) {
|
|
6
|
+
return [...input.pending.values()].filter((gate) => gate.deps.some((dep) => input.failed.has(dep)));
|
|
7
|
+
}
|
|
8
|
+
export function pickReadyLaunchableReleaseGates(input) {
|
|
9
|
+
return pickLaunchableReleaseGates({
|
|
10
|
+
ready: input.ready,
|
|
11
|
+
running: input.running,
|
|
12
|
+
budget: defaultReleaseGateBudget()
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=release-gate-scheduler.js.map
|
package/dist/core/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '2.0.
|
|
1
|
+
export const PACKAGE_VERSION = '2.0.8';
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { ensureDir, nowIso, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { runZellij } from './zellij-command.js';
|
|
4
|
+
import { extractZellijPaneIdFromOutput } from './zellij-lane-runtime.js';
|
|
5
|
+
import { buildZellijDashboardSnapshot } from './zellij-dashboard-renderer.js';
|
|
6
|
+
export const ZELLIJ_DASHBOARD_PANE_SCHEMA = 'sks.zellij-dashboard-pane.v1';
|
|
7
|
+
export async function openZellijDashboardPane(input) {
|
|
8
|
+
const root = path.resolve(input.root);
|
|
9
|
+
const cwd = input.cwd || root;
|
|
10
|
+
const paneTitle = `dashboard · ${input.missionId}`;
|
|
11
|
+
const dashboardDir = path.join(root, '.sneakoscope', 'missions', input.missionId);
|
|
12
|
+
await ensureDir(dashboardDir);
|
|
13
|
+
const snapshot = buildZellijDashboardSnapshot({ ...(input.snapshot || {}), mission_id: input.missionId });
|
|
14
|
+
const snapshotPath = path.join(dashboardDir, 'zellij-dashboard-snapshot.json');
|
|
15
|
+
await writeJsonAtomic(snapshotPath, snapshot);
|
|
16
|
+
const watchScript = path.join(root, 'dist', 'scripts', 'zellij-dashboard-watch.js');
|
|
17
|
+
const command = `${shellQuote(process.execPath)} ${shellQuote(watchScript)} --snapshot ${shellQuote(snapshotPath)} --interval-ms 1000`;
|
|
18
|
+
const createSession = await runZellij(['attach', '--create-background', input.sessionName], { cwd, timeoutMs: 5000, optional: true });
|
|
19
|
+
const launch = await runZellij(['--session', input.sessionName, 'action', 'new-pane', '--direction', 'right', '--name', paneTitle, '--', 'sh', '-lc', command], {
|
|
20
|
+
cwd,
|
|
21
|
+
timeoutMs: 5000,
|
|
22
|
+
optional: false
|
|
23
|
+
});
|
|
24
|
+
const stdoutPaneId = launch.ok ? extractZellijPaneIdFromOutput(launch.stdout_tail) : null;
|
|
25
|
+
const listed = await runZellij(['--session', input.sessionName, 'action', 'list-panes', '--json', '--all'], { cwd, timeoutMs: 5000, optional: true });
|
|
26
|
+
const rows = parseRows(listed.stdout_tail);
|
|
27
|
+
const pane = stdoutPaneId ? null : rows.find((row) => String(row.title || row.name || '').includes(paneTitle));
|
|
28
|
+
const paneId = stdoutPaneId || pane?.pane_id || pane?.paneId || pane?.id || null;
|
|
29
|
+
const dump = await runZellij(['--session', input.sessionName, 'action', 'dump-screen'], { cwd, timeoutMs: 5000, optional: true });
|
|
30
|
+
const source = stdoutPaneId ? 'zellij_dashboard_new_pane_stdout' : paneId ? 'zellij_dashboard_list_panes' : 'zellij_dashboard_missing';
|
|
31
|
+
const blockers = [
|
|
32
|
+
...(createSession.ok || /Session already exists/i.test(createSession.stderr_tail || '') ? [] : createSession.blockers.map((blocker) => `zellij_dashboard_session_${blocker}`)),
|
|
33
|
+
...(launch.ok ? [] : launch.blockers.map((blocker) => `zellij_dashboard_pane_${blocker}`)),
|
|
34
|
+
...(paneId ? [] : ['zellij_dashboard_pane_id_missing'])
|
|
35
|
+
];
|
|
36
|
+
const record = {
|
|
37
|
+
schema: ZELLIJ_DASHBOARD_PANE_SCHEMA,
|
|
38
|
+
generated_at: nowIso(),
|
|
39
|
+
ok: blockers.length === 0,
|
|
40
|
+
mission_id: input.missionId,
|
|
41
|
+
session_name: input.sessionName,
|
|
42
|
+
pane_title: paneTitle,
|
|
43
|
+
pane_id: paneId == null ? null : String(paneId),
|
|
44
|
+
pane_id_source: source,
|
|
45
|
+
pane_kind: 'dashboard',
|
|
46
|
+
worker_pane: false,
|
|
47
|
+
command,
|
|
48
|
+
launch,
|
|
49
|
+
list_panes: listed,
|
|
50
|
+
dump_screen: dump,
|
|
51
|
+
snapshot,
|
|
52
|
+
blockers
|
|
53
|
+
};
|
|
54
|
+
await writeJsonAtomic(path.join(dashboardDir, 'zellij-dashboard-pane.json'), record);
|
|
55
|
+
return record;
|
|
56
|
+
}
|
|
57
|
+
function parseRows(text) {
|
|
58
|
+
if (!String(text || '').trim())
|
|
59
|
+
return [];
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(String(text));
|
|
62
|
+
return Array.isArray(parsed) ? parsed : Array.isArray(parsed?.panes) ? parsed.panes : [];
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function shellQuote(value) {
|
|
69
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=zellij-dashboard-pane.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { nowIso } from '../fsx.js';
|
|
2
|
+
export const ZELLIJ_DASHBOARD_RENDER_SCHEMA = 'sks.zellij-dashboard-render.v1';
|
|
3
|
+
export function buildZellijDashboardSnapshot(input) {
|
|
4
|
+
return {
|
|
5
|
+
schema: ZELLIJ_DASHBOARD_RENDER_SCHEMA,
|
|
6
|
+
generated_at: nowIso(),
|
|
7
|
+
mission_id: input.mission_id,
|
|
8
|
+
mode: input.mode || 'naruto',
|
|
9
|
+
backend_counts: input.backend_counts || { 'codex-sdk': 1 },
|
|
10
|
+
placement_counts: input.placement_counts || { 'zellij-pane': 1 },
|
|
11
|
+
active_workers: Number(input.active_workers || 0),
|
|
12
|
+
visible_panes: Number(input.visible_panes || 0),
|
|
13
|
+
headless_workers: Number(input.headless_workers || 0),
|
|
14
|
+
queue_depth: Number(input.queue_depth || 0),
|
|
15
|
+
worktrees: input.worktrees || { active: 0, completed: 0, retained: 0 },
|
|
16
|
+
local_llm: input.local_llm || { tps: 0, queue: 0 },
|
|
17
|
+
gpt_final_status: input.gpt_final_status || 'not_started',
|
|
18
|
+
gate_progress: input.gate_progress || 'not_release',
|
|
19
|
+
latest_blockers: input.latest_blockers || []
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function renderZellijDashboardText(snapshot) {
|
|
23
|
+
const backendCounts = Object.entries(snapshot.backend_counts).map(([key, value]) => `${key}=${value}`).join(' ');
|
|
24
|
+
const placementCounts = Object.entries(snapshot.placement_counts).map(([key, value]) => `${key}=${value}`).join(' ');
|
|
25
|
+
return [
|
|
26
|
+
'SKS Dashboard',
|
|
27
|
+
`Mission: ${snapshot.mission_id}`,
|
|
28
|
+
`Mode: ${snapshot.mode}`,
|
|
29
|
+
`Backend counts: ${backendCounts || 'none'}`,
|
|
30
|
+
`Placement counts: ${placementCounts || 'none'}`,
|
|
31
|
+
`Active workers: ${snapshot.active_workers}`,
|
|
32
|
+
`Visible panes: ${snapshot.visible_panes}`,
|
|
33
|
+
`Headless workers: ${snapshot.headless_workers}`,
|
|
34
|
+
`Queue depth: ${snapshot.queue_depth}`,
|
|
35
|
+
`Worktrees active/completed/retained: ${snapshot.worktrees.active}/${snapshot.worktrees.completed}/${snapshot.worktrees.retained}`,
|
|
36
|
+
`Local LLM TPS / queue: ${snapshot.local_llm.tps}/${snapshot.local_llm.queue}`,
|
|
37
|
+
`GPT final status: ${snapshot.gpt_final_status}`,
|
|
38
|
+
`Gate progress: ${snapshot.gate_progress}`,
|
|
39
|
+
`Latest blockers: ${snapshot.latest_blockers.length ? snapshot.latest_blockers.join(', ') : 'none'}`
|
|
40
|
+
].join('\n');
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=zellij-dashboard-renderer.js.map
|
|
@@ -23,7 +23,7 @@ export function buildWorkerPaneArtifact(input) {
|
|
|
23
23
|
const paneIdSource = input.paneIdSource || 'zellij_worker_pane_launch_failed';
|
|
24
24
|
const blockers = input.blockers || [];
|
|
25
25
|
const providerContext = normalizePaneProviderContext(input.providerContext, input.serviceTier);
|
|
26
|
-
const paneTitle = buildWorkerPaneTitle(input.slotId, input.generationIndex, providerContext, input.serviceTier, input.backend, input.
|
|
26
|
+
const paneTitle = buildWorkerPaneTitle(input.slotId, input.generationIndex, providerContext, input.serviceTier, input.backend, input.statusLabel || input.status, input.worktree || null);
|
|
27
27
|
return {
|
|
28
28
|
schema: ZELLIJ_WORKER_PANE_SCHEMA,
|
|
29
29
|
generated_at: now,
|
|
@@ -79,11 +79,12 @@ export async function openWorkerPane(input) {
|
|
|
79
79
|
const workerDir = path.join(root, input.workerArtifactDir);
|
|
80
80
|
await ensureDir(workerDir);
|
|
81
81
|
await appendWorkerPaneEvent(root, 'session_launch_started', input, {});
|
|
82
|
-
const
|
|
82
|
+
const createSessionRaw = await runZellij(['attach', '--create-background', input.sessionName], {
|
|
83
83
|
cwd,
|
|
84
84
|
timeoutMs: 5000,
|
|
85
85
|
optional: false
|
|
86
86
|
});
|
|
87
|
+
const createSession = normalizeExistingZellijSession(input.sessionName, createSessionRaw);
|
|
87
88
|
const paneName = buildWorkerPaneTitle(input.slotId, input.generationIndex, providerContext, input.serviceTier, input.backend, input.statusLabel || 'running', input.worktree || null);
|
|
88
89
|
let launch = createSession.ok
|
|
89
90
|
? await runZellij(['--session', input.sessionName, 'action', 'new-pane', '--direction', 'right', '--name', paneName, '--', 'sh', '-lc', input.workerCommand], {
|
|
@@ -107,6 +108,7 @@ export async function openWorkerPane(input) {
|
|
|
107
108
|
const stdoutPaneId = launch?.ok ? extractZellijPaneIdFromOutput(launch.stdout_tail) : null;
|
|
108
109
|
const reconciledPane = stdoutPaneId ? null : launch?.ok ? await reconcileZellijWorkerPaneId(input.sessionName, paneName, path.join(root, input.resultPath), cwd) : null;
|
|
109
110
|
const paneId = stdoutPaneId || reconciledPane?.pane_id || null;
|
|
111
|
+
const renamePane = paneId ? await renameZellijPaneById(input.sessionName, paneId, paneName, cwd) : null;
|
|
110
112
|
const paneIdSource = stdoutPaneId
|
|
111
113
|
? 'zellij_worker_new_pane_stdout'
|
|
112
114
|
: reconciledPane?.pane_id
|
|
@@ -125,7 +127,7 @@ export async function openWorkerPane(input) {
|
|
|
125
127
|
paneIdSource,
|
|
126
128
|
createSession,
|
|
127
129
|
launch,
|
|
128
|
-
paneReconciliation: reconciledPane,
|
|
130
|
+
paneReconciliation: { ...(reconciledPane || {}), rename_pane: renamePane },
|
|
129
131
|
directionApplied,
|
|
130
132
|
status: blockers.length ? 'failed' : 'running',
|
|
131
133
|
providerContext,
|
|
@@ -262,6 +264,11 @@ async function reconcileZellijWorkerPaneId(sessionName, paneName, resultPath, cw
|
|
|
262
264
|
timeoutMs: 5000,
|
|
263
265
|
optional: true
|
|
264
266
|
});
|
|
267
|
+
const screen = await runZellij(['--session', sessionName, 'action', 'dump-screen'], {
|
|
268
|
+
cwd,
|
|
269
|
+
timeoutMs: 5000,
|
|
270
|
+
optional: true
|
|
271
|
+
});
|
|
265
272
|
const rows = parsePaneRows(listed.stdout_tail);
|
|
266
273
|
const pane = rows.find((row) => {
|
|
267
274
|
const title = String(row.title || row.name || row.pane_name || '');
|
|
@@ -280,6 +287,7 @@ async function reconcileZellijWorkerPaneId(sessionName, paneName, resultPath, cw
|
|
|
280
287
|
pane_id: paneId == null ? null : String(paneId),
|
|
281
288
|
listed_count: rows.length,
|
|
282
289
|
command: listed,
|
|
290
|
+
dump_screen: screen,
|
|
283
291
|
blockers: paneId == null ? ['zellij_worker_pane_id_not_reconciled'] : []
|
|
284
292
|
};
|
|
285
293
|
}
|
|
@@ -330,4 +338,56 @@ function parsePaneRows(text) {
|
|
|
330
338
|
return [];
|
|
331
339
|
}
|
|
332
340
|
}
|
|
341
|
+
async function renameZellijPaneById(sessionName, paneId, paneName, cwd) {
|
|
342
|
+
const numeric = Number.parseInt(paneId.replace(/^terminal_/, ''), 10);
|
|
343
|
+
const numericCandidates = Number.isFinite(numeric)
|
|
344
|
+
? [numeric, numeric + 1, numeric - 1].filter((value) => value >= 0).map(String)
|
|
345
|
+
: [];
|
|
346
|
+
const candidates = [...new Set([paneId, paneId.replace(/^terminal_/, ''), ...numericCandidates].filter(Boolean))];
|
|
347
|
+
let last = null;
|
|
348
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
349
|
+
if (attempt > 0)
|
|
350
|
+
await sleep(200 * attempt);
|
|
351
|
+
for (const candidate of candidates) {
|
|
352
|
+
last = await runZellij(['--session', sessionName, 'action', 'rename-pane', '--pane-id', candidate, paneName], {
|
|
353
|
+
cwd,
|
|
354
|
+
timeoutMs: 5000,
|
|
355
|
+
optional: true
|
|
356
|
+
});
|
|
357
|
+
if (last.ok)
|
|
358
|
+
return last;
|
|
359
|
+
const focus = await runZellij(['--session', sessionName, 'action', 'focus-pane-id', candidate], {
|
|
360
|
+
cwd,
|
|
361
|
+
timeoutMs: 5000,
|
|
362
|
+
optional: true
|
|
363
|
+
});
|
|
364
|
+
if (focus.ok) {
|
|
365
|
+
last = await runZellij(['--session', sessionName, 'action', 'rename-pane', paneName], {
|
|
366
|
+
cwd,
|
|
367
|
+
timeoutMs: 5000,
|
|
368
|
+
optional: true
|
|
369
|
+
});
|
|
370
|
+
if (last.ok)
|
|
371
|
+
return last;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return last;
|
|
376
|
+
}
|
|
377
|
+
function sleep(ms) {
|
|
378
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
379
|
+
}
|
|
380
|
+
function normalizeExistingZellijSession(sessionName, result) {
|
|
381
|
+
if (result.ok)
|
|
382
|
+
return result;
|
|
383
|
+
if (/Session already exists/i.test(result.stderr_tail || '')) {
|
|
384
|
+
return {
|
|
385
|
+
...result,
|
|
386
|
+
ok: true,
|
|
387
|
+
blockers: [],
|
|
388
|
+
warnings: [...result.warnings, `zellij_session_already_exists:${sessionName}`]
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
return result;
|
|
392
|
+
}
|
|
333
393
|
//# sourceMappingURL=zellij-worker-pane-manager.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import { assertGate, emitGate, importDist } from './sks-1-18-gate-lib.js';
|
|
4
|
+
const envelopeMod = await importDist('core/git/git-worktree-patch-envelope.js');
|
|
5
|
+
const schemaMod = await importDist('core/agents/agent-patch-schema.js');
|
|
6
|
+
const envelope = envelopeMod.buildGitWorktreePatchEnvelope({
|
|
7
|
+
diff: { schema: 'sks.git-worktree-diff.v1', mission_id: 'M-env', worker_id: 'worker-1', main_repo_root: '.', worktree_path: '.', branch: null, base_head: null, worktree_head: null, changed_files: ['a.txt', 'b.txt'], diff_bytes: 12, diff: 'diff --git a/a.txt b/a.txt\n', clean: false },
|
|
8
|
+
agentId: 'agent-1',
|
|
9
|
+
sessionId: 'session-1',
|
|
10
|
+
slotId: 'slot-001',
|
|
11
|
+
generationIndex: 1
|
|
12
|
+
});
|
|
13
|
+
const validation = schemaMod.validateAgentPatchEnvelope(schemaMod.normalizeAgentPatchEnvelope(envelope));
|
|
14
|
+
assertGate(envelope.operations.length === 1 && envelope.operations[0].op === 'git_apply_patch', 'git worktree envelope must use one git_apply_patch operation', envelope);
|
|
15
|
+
assertGate(validation.ok === true, 'git_apply_patch envelope must validate', validation);
|
|
16
|
+
emitGate('git:worktree-diff-envelope', { operations: envelope.operations.length, op: envelope.operations[0].op });
|
|
17
|
+
//# sourceMappingURL=git-worktree-diff-envelope-check.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { assertGate, emitGate, importDist } from './sks-1-18-gate-lib.js';
|
|
7
|
+
import { makeGitFixture } from './lib/git-worktree-fixture.js';
|
|
8
|
+
const managerMod = await importDist('core/git/git-worktree-manager.js');
|
|
9
|
+
const cleanupMod = await importDist('core/git/git-worktree-cleanup.js');
|
|
10
|
+
const repo = makeGitFixture('dirty-lock');
|
|
11
|
+
process.env.SKS_WORKTREE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), 'sks-wt-lock-'));
|
|
12
|
+
const allocation = await managerMod.allocateWorkerWorktree({ repoRoot: repo, missionId: 'M-lock', workerId: 'worker-1', slotId: 'slot-001' });
|
|
13
|
+
fs.writeFileSync(path.join(allocation.worktree_path, 'a.txt'), 'dirty\n');
|
|
14
|
+
const cleanup = await cleanupMod.cleanupGitWorktree({ repoRoot: repo, worktreePath: allocation.worktree_path, branch: allocation.branch });
|
|
15
|
+
assertGate(cleanup.action === 'retained_dirty' && cleanup.git_locked === true, 'dirty retained worktree must be git locked', cleanup);
|
|
16
|
+
emitGate('git:worktree-dirty-lock', { git_locked: cleanup.git_locked, unlock_command: cleanup.unlock_command });
|
|
17
|
+
//# sourceMappingURL=git-worktree-dirty-lock-check.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { assertGate, emitGate, importDist } from './sks-1-18-gate-lib.js';
|
|
6
|
+
import { makeGitFixture } from './lib/git-worktree-fixture.js';
|
|
7
|
+
const detectionMod = await importDist('core/git/git-repo-detection.js');
|
|
8
|
+
const repo = makeGitFixture('dirty-main-detection');
|
|
9
|
+
fs.writeFileSync(path.join(repo, 'dirty.txt'), 'dirty\n');
|
|
10
|
+
const detection = await detectionMod.detectGitRepo(repo);
|
|
11
|
+
assertGate(detection.main_worktree_dirty === true, 'dirty main worktree must be detected', detection);
|
|
12
|
+
assertGate(String(detection.status_porcelain).includes('dirty.txt'), 'status_porcelain must include dirty file', detection);
|
|
13
|
+
emitGate('git:worktree-dirty-main-detection', { dirty: detection.main_worktree_dirty });
|
|
14
|
+
//# sourceMappingURL=git-worktree-dirty-main-detection-check.js.map
|