bosun 0.36.2 → 0.36.3
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/analyze-agent-work-helpers.mjs +308 -0
- package/analyze-agent-work.mjs +926 -0
- package/autofix.mjs +2 -0
- package/codex-shell.mjs +85 -10
- package/git-editor-fix.mjs +273 -0
- package/mcp-registry.mjs +579 -0
- package/meeting-workflow-service.mjs +631 -0
- package/monitor.mjs +18 -103
- package/package.json +13 -2
- package/primary-agent.mjs +32 -12
- package/session-tracker.mjs +68 -0
- package/stream-resilience.mjs +17 -7
- package/ui/app.js +19 -4
- package/ui/components/chat-view.js +108 -5
- package/ui/components/session-list.js +1 -1
- package/ui/components/shared.js +188 -15
- package/ui/modules/icons.js +13 -0
- package/ui/modules/utils.js +44 -0
- package/ui/modules/voice.js +15 -6
- package/ui/styles/components.css +99 -3
- package/ui/styles/sessions.css +84 -12
- package/ui/tabs/chat.js +5 -1
- package/ui/tabs/control.js +16 -22
- package/ui/tabs/dashboard.js +85 -8
- package/ui/tabs/library.js +113 -17
- package/ui/tabs/settings.js +116 -2
- package/ui/tabs/tasks.js +388 -39
- package/ui/tabs/telemetry.js +0 -1
- package/ui/tabs/workflows.js +4 -0
- package/ui-server.mjs +193 -19
- package/update-check.mjs +41 -13
- package/voice-relay.mjs +816 -0
- package/voice-tools.mjs +679 -0
- package/workflow-templates/agents.mjs +6 -2
- package/workflow-templates/github.mjs +154 -12
- package/workflow-templates.mjs +3 -0
- package/github-reconciler.mjs +0 -506
- package/merge-strategy.mjs +0 -1210
- package/pr-cleanup-daemon.mjs +0 -992
- package/workspace-reaper.mjs +0 -405
package/autofix.mjs
CHANGED
|
@@ -688,6 +688,7 @@ function detectChangedFiles(repoRoot) {
|
|
|
688
688
|
cwd: repoRoot,
|
|
689
689
|
encoding: "utf8",
|
|
690
690
|
timeout: 10_000,
|
|
691
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
691
692
|
});
|
|
692
693
|
return output
|
|
693
694
|
.split(/\r?\n/)
|
|
@@ -708,6 +709,7 @@ function getChangeSummary(repoRoot, files) {
|
|
|
708
709
|
cwd: repoRoot,
|
|
709
710
|
encoding: "utf8",
|
|
710
711
|
timeout: 10_000,
|
|
712
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
711
713
|
});
|
|
712
714
|
return diff.trim() || files.join(", ");
|
|
713
715
|
} catch {
|
package/codex-shell.mjs
CHANGED
|
@@ -177,6 +177,7 @@ function sanitizeAndTruncatePrompt(text) {
|
|
|
177
177
|
return truncated + `\n\n[...prompt truncated — ${removedBytes} bytes removed to stay within API limits]`;
|
|
178
178
|
}
|
|
179
179
|
const REPO_ROOT = resolveRepoRoot();
|
|
180
|
+
const DEFAULT_WORKING_DIRECTORY = REPO_ROOT;
|
|
180
181
|
|
|
181
182
|
// ── State ────────────────────────────────────────────────────────────────────
|
|
182
183
|
|
|
@@ -187,6 +188,7 @@ let activeThreadId = null; // Thread ID for resume
|
|
|
187
188
|
let activeTurn = null; // Whether a turn is in-flight
|
|
188
189
|
let turnCount = 0; // Number of turns in this thread
|
|
189
190
|
let currentSessionId = null; // Active session identifier
|
|
191
|
+
let activeWorkingDirectory = DEFAULT_WORKING_DIRECTORY; // Session/thread cwd
|
|
190
192
|
let threadNeedsPriming = false; // True when a fresh thread needs the system prompt on next turn
|
|
191
193
|
let codexRuntimeCaps = {
|
|
192
194
|
hasSteeringApi: false,
|
|
@@ -200,6 +202,20 @@ function timestamp() {
|
|
|
200
202
|
return new Date().toISOString();
|
|
201
203
|
}
|
|
202
204
|
|
|
205
|
+
function normalizeWorkingDirectory(input) {
|
|
206
|
+
const raw = String(input || "").trim();
|
|
207
|
+
if (!raw) return null;
|
|
208
|
+
try {
|
|
209
|
+
return resolve(raw);
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function getWorkingDirectory() {
|
|
216
|
+
return normalizeWorkingDirectory(activeWorkingDirectory) || DEFAULT_WORKING_DIRECTORY;
|
|
217
|
+
}
|
|
218
|
+
|
|
203
219
|
function resolveCodexTransport() {
|
|
204
220
|
const raw = String(process.env.CODEX_TRANSPORT || "auto")
|
|
205
221
|
.trim()
|
|
@@ -250,13 +266,17 @@ async function loadState() {
|
|
|
250
266
|
activeThreadId = data.threadId || null;
|
|
251
267
|
turnCount = data.turnCount || 0;
|
|
252
268
|
currentSessionId = data.currentSessionId || null;
|
|
269
|
+
activeWorkingDirectory =
|
|
270
|
+
normalizeWorkingDirectory(data.workingDirectory) ||
|
|
271
|
+
DEFAULT_WORKING_DIRECTORY;
|
|
253
272
|
console.log(
|
|
254
|
-
`[codex-shell] loaded state: threadId=${activeThreadId}, turns=${turnCount}, session=${currentSessionId}`,
|
|
273
|
+
`[codex-shell] loaded state: threadId=${activeThreadId}, turns=${turnCount}, session=${currentSessionId}, cwd=${getWorkingDirectory()}`,
|
|
255
274
|
);
|
|
256
275
|
} catch {
|
|
257
276
|
activeThreadId = null;
|
|
258
277
|
turnCount = 0;
|
|
259
278
|
currentSessionId = null;
|
|
279
|
+
activeWorkingDirectory = DEFAULT_WORKING_DIRECTORY;
|
|
260
280
|
}
|
|
261
281
|
}
|
|
262
282
|
|
|
@@ -270,6 +290,7 @@ async function saveState() {
|
|
|
270
290
|
threadId: activeThreadId,
|
|
271
291
|
turnCount,
|
|
272
292
|
currentSessionId,
|
|
293
|
+
workingDirectory: getWorkingDirectory(),
|
|
273
294
|
updatedAt: timestamp(),
|
|
274
295
|
},
|
|
275
296
|
null,
|
|
@@ -311,6 +332,7 @@ async function saveCurrentSession() {
|
|
|
311
332
|
await saveSessionData(currentSessionId, {
|
|
312
333
|
threadId: activeThreadId,
|
|
313
334
|
turnCount,
|
|
335
|
+
workingDirectory: getWorkingDirectory(),
|
|
314
336
|
createdAt: (await loadSessionData(currentSessionId))?.createdAt || timestamp(),
|
|
315
337
|
lastActiveAt: timestamp(),
|
|
316
338
|
});
|
|
@@ -325,12 +347,16 @@ async function loadSession(sessionId) {
|
|
|
325
347
|
turnCount = data.turnCount || 0;
|
|
326
348
|
activeThread = null; // will be re-created/resumed via getThread()
|
|
327
349
|
currentSessionId = sessionId;
|
|
328
|
-
|
|
350
|
+
activeWorkingDirectory =
|
|
351
|
+
normalizeWorkingDirectory(data.workingDirectory) ||
|
|
352
|
+
DEFAULT_WORKING_DIRECTORY;
|
|
353
|
+
console.log(`[codex-shell] loaded session ${sessionId}: threadId=${activeThreadId}, turns=${turnCount}, cwd=${getWorkingDirectory()}`);
|
|
329
354
|
} else {
|
|
330
355
|
activeThread = null;
|
|
331
356
|
activeThreadId = null;
|
|
332
357
|
turnCount = 0;
|
|
333
358
|
currentSessionId = sessionId;
|
|
359
|
+
activeWorkingDirectory = DEFAULT_WORKING_DIRECTORY;
|
|
334
360
|
console.log(`[codex-shell] created new session ${sessionId}`);
|
|
335
361
|
}
|
|
336
362
|
await saveState();
|
|
@@ -366,9 +392,8 @@ Key files:
|
|
|
366
392
|
AGENTS.md — Repo guide for agents
|
|
367
393
|
`;
|
|
368
394
|
|
|
369
|
-
const
|
|
395
|
+
const THREAD_BASE_OPTIONS = {
|
|
370
396
|
sandboxMode: process.env.CODEX_SANDBOX || "workspace-write",
|
|
371
|
-
workingDirectory: REPO_ROOT,
|
|
372
397
|
skipGitRepoCheck: true,
|
|
373
398
|
webSearchMode: "live",
|
|
374
399
|
approvalPolicy: "never",
|
|
@@ -377,6 +402,13 @@ const THREAD_OPTIONS = {
|
|
|
377
402
|
// codex-config.mjs ensureFeatureFlags() handles this during setup.
|
|
378
403
|
};
|
|
379
404
|
|
|
405
|
+
function buildThreadOptions() {
|
|
406
|
+
return {
|
|
407
|
+
...THREAD_BASE_OPTIONS,
|
|
408
|
+
workingDirectory: getWorkingDirectory(),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
380
412
|
/**
|
|
381
413
|
* Get or create a thread.
|
|
382
414
|
* Uses fresh-thread mode by default to avoid context bloat.
|
|
@@ -384,6 +416,7 @@ const THREAD_OPTIONS = {
|
|
|
384
416
|
*/
|
|
385
417
|
async function getThread() {
|
|
386
418
|
if (activeThread) return activeThread;
|
|
419
|
+
const threadOptions = buildThreadOptions();
|
|
387
420
|
|
|
388
421
|
const { env: resolvedEnv } = resolveCodexProfileRuntime(process.env);
|
|
389
422
|
Object.assign(process.env, resolvedEnv);
|
|
@@ -391,8 +424,21 @@ async function getThread() {
|
|
|
391
424
|
if (!codexInstance) {
|
|
392
425
|
const Cls = await loadCodexSdk();
|
|
393
426
|
if (!Cls) throw new Error("Codex SDK not available");
|
|
394
|
-
|
|
395
|
-
//
|
|
427
|
+
|
|
428
|
+
// Inject stream resilience settings via --config overrides so they apply
|
|
429
|
+
// even if config.toml hasn't been patched by codex-config.mjs yet.
|
|
430
|
+
// This is the most reliable path for Azure/Foundry deployments where
|
|
431
|
+
// dropped SSE streams ("response.failed") are the dominant failure mode.
|
|
432
|
+
const providerName = resolvedEnv.OPENAI_BASE_URL?.toLowerCase().includes(".openai.azure.com")
|
|
433
|
+
? "azure"
|
|
434
|
+
: "openai";
|
|
435
|
+
const STREAM_IDLE_TIMEOUT_MS = 3_600_000; // 60 min — matches Azure max stream lifetime
|
|
436
|
+
const streamProviderOverrides = {
|
|
437
|
+
stream_idle_timeout_ms: STREAM_IDLE_TIMEOUT_MS,
|
|
438
|
+
stream_max_retries: 15,
|
|
439
|
+
request_max_retries: 6,
|
|
440
|
+
};
|
|
441
|
+
|
|
396
442
|
codexInstance = new Cls({
|
|
397
443
|
config: {
|
|
398
444
|
features: {
|
|
@@ -402,8 +448,13 @@ async function getThread() {
|
|
|
402
448
|
undo: true,
|
|
403
449
|
steer: true,
|
|
404
450
|
},
|
|
451
|
+
model_providers: {
|
|
452
|
+
[providerName]: streamProviderOverrides,
|
|
453
|
+
},
|
|
405
454
|
},
|
|
406
455
|
});
|
|
456
|
+
|
|
457
|
+
console.log(`[codex-shell] created Codex instance (provider=${providerName}, stream_idle_timeout=${STREAM_IDLE_TIMEOUT_MS}ms, stream_max_retries=${streamProviderOverrides.stream_max_retries})`);
|
|
407
458
|
}
|
|
408
459
|
|
|
409
460
|
const transport = resolveCodexTransport();
|
|
@@ -414,7 +465,7 @@ async function getThread() {
|
|
|
414
465
|
try {
|
|
415
466
|
activeThread = codexInstance.resumeThread(
|
|
416
467
|
activeThreadId,
|
|
417
|
-
|
|
468
|
+
threadOptions,
|
|
418
469
|
);
|
|
419
470
|
if (activeThread) {
|
|
420
471
|
detectThreadCapabilities(activeThread);
|
|
@@ -446,16 +497,16 @@ async function getThread() {
|
|
|
446
497
|
// the priming turn is STREAMED (runStreamed) instead of blocking (run).
|
|
447
498
|
// This eliminates the 2-5 minute silent delay the chat UI suffered because
|
|
448
499
|
// the old `thread.run(SYSTEM_PROMPT)` call produced zero streaming events.
|
|
449
|
-
activeThread = codexInstance.startThread(
|
|
500
|
+
activeThread = codexInstance.startThread(threadOptions);
|
|
450
501
|
detectThreadCapabilities(activeThread);
|
|
451
502
|
threadNeedsPriming = true;
|
|
452
503
|
|
|
453
504
|
if (activeThread.id) {
|
|
454
505
|
activeThreadId = activeThread.id;
|
|
455
506
|
await saveState();
|
|
456
|
-
console.log(`[codex-shell] new thread started: ${activeThreadId} (priming deferred to first user turn)`);
|
|
507
|
+
console.log(`[codex-shell] new thread started: ${activeThreadId} (priming deferred to first user turn, cwd=${threadOptions.workingDirectory})`);
|
|
457
508
|
} else {
|
|
458
|
-
console.log(
|
|
509
|
+
console.log(`[codex-shell] new thread started (priming deferred to first user turn, cwd=${threadOptions.workingDirectory})`);
|
|
459
510
|
}
|
|
460
511
|
|
|
461
512
|
return activeThread;
|
|
@@ -627,6 +678,7 @@ export async function execCodexPrompt(userMessage, options = {}) {
|
|
|
627
678
|
persistent = false,
|
|
628
679
|
sessionId = null,
|
|
629
680
|
mode = null,
|
|
681
|
+
cwd = null,
|
|
630
682
|
} = options;
|
|
631
683
|
|
|
632
684
|
agentSdk = resolveAgentSdkConfig({ reload: true });
|
|
@@ -651,9 +703,13 @@ export async function execCodexPrompt(userMessage, options = {}) {
|
|
|
651
703
|
|
|
652
704
|
try {
|
|
653
705
|
const streamSafety = resolveCodexStreamSafety(timeoutMs);
|
|
706
|
+
const requestedWorkingDirectory = normalizeWorkingDirectory(cwd);
|
|
707
|
+
|
|
654
708
|
if (!persistent) {
|
|
655
709
|
// Task executor path — keep existing fresh-thread behavior
|
|
656
710
|
activeThread = null;
|
|
711
|
+
activeWorkingDirectory =
|
|
712
|
+
requestedWorkingDirectory || DEFAULT_WORKING_DIRECTORY;
|
|
657
713
|
} else if (sessionId && sessionId !== currentSessionId) {
|
|
658
714
|
// Switching to a different persistent session
|
|
659
715
|
await loadSession(sessionId);
|
|
@@ -669,6 +725,24 @@ export async function execCodexPrompt(userMessage, options = {}) {
|
|
|
669
725
|
}
|
|
670
726
|
// else: persistent && same session && under limit → reuse activeThread
|
|
671
727
|
|
|
728
|
+
if (
|
|
729
|
+
requestedWorkingDirectory &&
|
|
730
|
+
requestedWorkingDirectory !== getWorkingDirectory()
|
|
731
|
+
) {
|
|
732
|
+
activeWorkingDirectory = requestedWorkingDirectory;
|
|
733
|
+
activeThread = null;
|
|
734
|
+
activeThreadId = null;
|
|
735
|
+
turnCount = 0;
|
|
736
|
+
threadNeedsPriming = false;
|
|
737
|
+
await saveState();
|
|
738
|
+
if (persistent && currentSessionId) {
|
|
739
|
+
await saveCurrentSession();
|
|
740
|
+
}
|
|
741
|
+
console.log(
|
|
742
|
+
`[codex-shell] switched working directory to ${requestedWorkingDirectory} for session ${currentSessionId || "(ephemeral)"}`,
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
672
746
|
// ── Mode detection ───────────────────────────────────────────────────
|
|
673
747
|
// "ask" mode should be lightweight — no heavy executor framing that
|
|
674
748
|
// instructs the agent to run commands and read files. The mode is
|
|
@@ -960,6 +1034,7 @@ export async function resetThread() {
|
|
|
960
1034
|
turnCount = 0;
|
|
961
1035
|
activeTurn = null;
|
|
962
1036
|
currentSessionId = null;
|
|
1037
|
+
activeWorkingDirectory = DEFAULT_WORKING_DIRECTORY;
|
|
963
1038
|
threadNeedsPriming = false;
|
|
964
1039
|
await saveState();
|
|
965
1040
|
console.log("[codex-shell] thread reset");
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* git-editor-fix.mjs — Prevent agents from opening interactive editors
|
|
4
|
+
*
|
|
5
|
+
* Problem: Agents inherit user's git config which uses VSCode (`code --wait`)
|
|
6
|
+
* Result: Git operations block waiting for editor, freezing agents
|
|
7
|
+
*
|
|
8
|
+
* Solution: Set GIT_EDITOR=true (or GIT_EDITOR=:) for non-interactive mode
|
|
9
|
+
*
|
|
10
|
+
* This script ensures all agent workspaces have non-blocking git config.
|
|
11
|
+
* Covers: main repo, tmpclaude-* workspaces, git worktrees (ve/*),
|
|
12
|
+
* and VK task worktrees under $TEMP/vibe-kanban/worktrees/.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execSync } from "child_process";
|
|
16
|
+
import { existsSync, readdirSync } from "fs";
|
|
17
|
+
import { resolve, basename } from "path";
|
|
18
|
+
import { tmpdir } from "os";
|
|
19
|
+
import { fileURLToPath } from "url";
|
|
20
|
+
import { resolveRepoRoot } from "./repo-root.mjs";
|
|
21
|
+
|
|
22
|
+
const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
|
23
|
+
const REPO_ROOT = resolveRepoRoot();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Configure git to never open interactive editors
|
|
27
|
+
* @param {string} workspacePath - Path to workspace directory
|
|
28
|
+
* @returns {boolean} true if configured successfully
|
|
29
|
+
*/
|
|
30
|
+
function configureNonInteractiveGit(workspacePath) {
|
|
31
|
+
const gitDir = resolve(workspacePath, ".git");
|
|
32
|
+
|
|
33
|
+
// .git can be a file (worktree link) or a directory — both are valid
|
|
34
|
+
if (!existsSync(gitDir)) {
|
|
35
|
+
console.warn(`[git-editor-fix] No .git entry at ${workspacePath}`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Set local git config for this workspace
|
|
41
|
+
// Use ':' (colon) as no-op editor — POSIX standard that always succeeds
|
|
42
|
+
execSync("git config --local core.editor :", {
|
|
43
|
+
cwd: workspacePath,
|
|
44
|
+
stdio: "pipe",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Also disable merge commit editor prompts
|
|
48
|
+
execSync("git config --local merge.commit.autoEdit no", {
|
|
49
|
+
cwd: workspacePath,
|
|
50
|
+
stdio: "pipe",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
console.log(
|
|
54
|
+
`[git-editor-fix] ✓ Configured ${workspacePath} for non-interactive git`,
|
|
55
|
+
);
|
|
56
|
+
return true;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error(
|
|
59
|
+
`[git-editor-fix] Failed to configure ${workspacePath}:`,
|
|
60
|
+
err.message,
|
|
61
|
+
);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Workspace discovery helpers ──────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Collect tmpclaude-* directories under REPO_ROOT
|
|
70
|
+
* @returns {string[]}
|
|
71
|
+
*/
|
|
72
|
+
function findTmpclaudeWorkspaces() {
|
|
73
|
+
/** @type {string[]} */
|
|
74
|
+
const results = [];
|
|
75
|
+
try {
|
|
76
|
+
const entries = readdirSync(REPO_ROOT, { withFileTypes: true });
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (entry.isDirectory() && entry.name.startsWith("tmpclaude-")) {
|
|
79
|
+
results.push(resolve(REPO_ROOT, entry.name));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error(
|
|
84
|
+
"[git-editor-fix] Failed to scan tmpclaude workspaces:",
|
|
85
|
+
err.message,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Parse `git worktree list --porcelain` and return paths of ve/* worktrees
|
|
93
|
+
* @returns {string[]}
|
|
94
|
+
*/
|
|
95
|
+
function findGitWorktrees() {
|
|
96
|
+
/** @type {string[]} */
|
|
97
|
+
const results = [];
|
|
98
|
+
try {
|
|
99
|
+
const raw = execSync("git worktree list --porcelain", {
|
|
100
|
+
cwd: REPO_ROOT,
|
|
101
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
102
|
+
encoding: "utf-8",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Porcelain output has blocks separated by blank lines.
|
|
106
|
+
// Each block starts with "worktree <path>".
|
|
107
|
+
// We also look for "branch refs/heads/ve/..." to identify VK worktrees,
|
|
108
|
+
// but we include ALL worktrees — they all need the fix.
|
|
109
|
+
for (const line of raw.split("\n")) {
|
|
110
|
+
const match = line.match(/^worktree\s+(.+)$/);
|
|
111
|
+
if (match) {
|
|
112
|
+
const wtPath = match[1].trim();
|
|
113
|
+
// Skip the bare repo entry if present
|
|
114
|
+
if (existsSync(resolve(wtPath, ".git"))) {
|
|
115
|
+
results.push(wtPath);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
// git worktree list fails if the repo has no worktrees — that's fine
|
|
121
|
+
if (!String(err.message).includes("is not a git repository")) {
|
|
122
|
+
console.warn(
|
|
123
|
+
"[git-editor-fix] Could not enumerate git worktrees:",
|
|
124
|
+
err.message,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return results;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Scan $TEMP/vibe-kanban/worktrees/ for VK task worktree directories
|
|
133
|
+
* @returns {string[]}
|
|
134
|
+
*/
|
|
135
|
+
function findVKWorktrees() {
|
|
136
|
+
/** @type {string[]} */
|
|
137
|
+
const results = [];
|
|
138
|
+
const vkBase = resolve(tmpdir(), "vibe-kanban", "worktrees");
|
|
139
|
+
|
|
140
|
+
if (!existsSync(vkBase)) {
|
|
141
|
+
return results;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const entries = readdirSync(vkBase, { withFileTypes: true });
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (entry.isDirectory()) {
|
|
148
|
+
const candidate = resolve(vkBase, entry.name);
|
|
149
|
+
if (existsSync(resolve(candidate, ".git"))) {
|
|
150
|
+
results.push(candidate);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error("[git-editor-fix] Failed to scan VK worktrees:", err.message);
|
|
156
|
+
}
|
|
157
|
+
return results;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Main functions ───────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Scan for all agent workspaces and fix git config.
|
|
164
|
+
* Sources:
|
|
165
|
+
* 1. Main repo root (REPO_ROOT)
|
|
166
|
+
* 2. tmpclaude-* directories
|
|
167
|
+
* 3. git worktrees (parsed from `git worktree list --porcelain`)
|
|
168
|
+
* 4. VK task worktrees under $TEMP/vibe-kanban/worktrees/
|
|
169
|
+
*
|
|
170
|
+
* Paths are deduplicated before configuration.
|
|
171
|
+
*/
|
|
172
|
+
function fixAllWorkspaces() {
|
|
173
|
+
console.log("[git-editor-fix] Scanning for agent workspaces...");
|
|
174
|
+
|
|
175
|
+
/** @type {Set<string>} */
|
|
176
|
+
const seen = new Set();
|
|
177
|
+
|
|
178
|
+
/** @param {string} p */
|
|
179
|
+
const add = (p) => {
|
|
180
|
+
const normalized = resolve(p);
|
|
181
|
+
seen.add(normalized);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// 1. Main repo root
|
|
185
|
+
add(REPO_ROOT);
|
|
186
|
+
|
|
187
|
+
// 2. tmpclaude-* directories
|
|
188
|
+
for (const ws of findTmpclaudeWorkspaces()) {
|
|
189
|
+
add(ws);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 3. Git worktrees (includes ve/* branches)
|
|
193
|
+
for (const ws of findGitWorktrees()) {
|
|
194
|
+
add(ws);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 4. VK worktrees under $TEMP
|
|
198
|
+
for (const ws of findVKWorktrees()) {
|
|
199
|
+
add(ws);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const workspaces = [...seen];
|
|
203
|
+
console.log(
|
|
204
|
+
`[git-editor-fix] Found ${workspaces.length} workspace(s) to configure`,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
let fixed = 0;
|
|
208
|
+
for (const ws of workspaces) {
|
|
209
|
+
if (configureNonInteractiveGit(ws)) {
|
|
210
|
+
fixed++;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
console.log(
|
|
215
|
+
`[git-editor-fix] ✓ Fixed ${fixed}/${workspaces.length} workspaces`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Convenience wrapper: configure the main repo and all discoverable worktrees
|
|
221
|
+
* in a single call. Suitable for use from other modules.
|
|
222
|
+
* @returns {{ fixed: number, total: number }}
|
|
223
|
+
*/
|
|
224
|
+
function configureRepoAndWorktrees() {
|
|
225
|
+
console.log("[git-editor-fix] Configuring repo and all worktrees...");
|
|
226
|
+
|
|
227
|
+
/** @type {Set<string>} */
|
|
228
|
+
const seen = new Set();
|
|
229
|
+
|
|
230
|
+
const add = (/** @type {string} */ p) => seen.add(resolve(p));
|
|
231
|
+
|
|
232
|
+
add(REPO_ROOT);
|
|
233
|
+
findTmpclaudeWorkspaces().forEach(add);
|
|
234
|
+
findGitWorktrees().forEach(add);
|
|
235
|
+
findVKWorktrees().forEach(add);
|
|
236
|
+
|
|
237
|
+
const workspaces = [...seen];
|
|
238
|
+
let fixed = 0;
|
|
239
|
+
for (const ws of workspaces) {
|
|
240
|
+
if (configureNonInteractiveGit(ws)) {
|
|
241
|
+
fixed++;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
console.log(
|
|
246
|
+
`[git-editor-fix] ✓ Configured ${fixed}/${workspaces.length} workspace(s)`,
|
|
247
|
+
);
|
|
248
|
+
return { fixed, total: workspaces.length };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── CLI Entry Point ──────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
const isMainModule = () => {
|
|
254
|
+
try {
|
|
255
|
+
const modulePath = fileURLToPath(import.meta.url);
|
|
256
|
+
return process.argv[1] === modulePath;
|
|
257
|
+
} catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
if (isMainModule()) {
|
|
263
|
+
fixAllWorkspaces();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export {
|
|
267
|
+
configureNonInteractiveGit,
|
|
268
|
+
fixAllWorkspaces,
|
|
269
|
+
configureRepoAndWorktrees,
|
|
270
|
+
findGitWorktrees,
|
|
271
|
+
findVKWorktrees,
|
|
272
|
+
findTmpclaudeWorkspaces,
|
|
273
|
+
};
|