bosun 0.34.3 → 0.34.5
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/agent-endpoint.mjs +2 -2
- package/agent-event-bus.mjs +3 -3
- package/agent-pool.mjs +30 -10
- package/agent-work-report.mjs +1 -1
- package/bosun.schema.json +5 -0
- package/desktop/main.mjs +37 -1
- package/git-commit-helpers.mjs +83 -0
- package/kanban-adapter.mjs +97 -11
- package/maintenance.mjs +73 -6
- package/monitor.mjs +363 -80
- package/package.json +2 -1
- package/primary-agent.mjs +2 -0
- package/setup-web-server.mjs +88 -22
- package/setup.mjs +31 -22
- package/task-executor.mjs +7 -10
- package/task-store.mjs +43 -10
- package/telegram-bot.mjs +91 -23
- package/ui/app.js +2 -2
- package/ui/app.monolith.js +1 -0
- package/ui/components/agent-selector.js +181 -135
- package/ui/components/charts.js +10 -11
- package/ui/modules/agent-display.js +1 -1
- package/ui/modules/settings-schema.js +9 -0
- package/ui/modules/streaming.js +28 -14
- package/ui/setup.html +5 -6
- package/ui/styles/components.css +15 -0
- package/ui/styles/kanban.css +2 -2
- package/ui/styles/layout.css +12 -3
- package/ui/styles/sessions.css +24 -0
- package/ui/styles/variables.css +375 -0
- package/ui/tabs/chat.js +2 -2
- package/ui/tabs/control.js +3 -2
- package/ui/tabs/settings.js +21 -3
- package/ui/tabs/workflows.js +234 -20
- package/ui-server.mjs +410 -106
- package/workflow-templates/security.mjs +241 -241
- package/workspace-manager.mjs +18 -6
package/agent-endpoint.mjs
CHANGED
|
@@ -30,7 +30,7 @@ const TAG = "[agent-endpoint]";
|
|
|
30
30
|
const DEFAULT_PORT = 18432;
|
|
31
31
|
const MAX_BODY_SIZE = 1024 * 1024; // 1 MB
|
|
32
32
|
const REQUEST_TIMEOUT_MS = 30_000; // 30 seconds
|
|
33
|
-
const ACCESS_DENIED_COOLDOWN_MS = 10 * 60 * 1000; // 10 minutes
|
|
33
|
+
const ACCESS_DENIED_COOLDOWN_MS = 10 * 60 * 1000; // 10 minutes
|
|
34
34
|
const BOSUN_ROOT_HINT = __dirname.toLowerCase().replace(/\\/g, '/');
|
|
35
35
|
|
|
36
36
|
// Valid status transitions when an agent self-reports
|
|
@@ -1387,4 +1387,4 @@ export class AgentEndpoint {
|
|
|
1387
1387
|
export function createAgentEndpoint(options) {
|
|
1388
1388
|
return new AgentEndpoint(options);
|
|
1389
1389
|
}
|
|
1390
|
-
|
|
1390
|
+
|
package/agent-event-bus.mjs
CHANGED
|
@@ -112,8 +112,8 @@ export class AgentEventBus {
|
|
|
112
112
|
options.staleThresholdMs || DEFAULTS.staleThresholdMs;
|
|
113
113
|
this._staleCheckIntervalMs =
|
|
114
114
|
options.staleCheckIntervalMs || DEFAULTS.staleCheckIntervalMs;
|
|
115
|
-
this._maxAutoRetries =
|
|
116
|
-
options.maxAutoRetries ?? DEFAULTS.maxAutoRetries;
|
|
115
|
+
this._maxAutoRetries =
|
|
116
|
+
options.maxAutoRetries ?? DEFAULTS.maxAutoRetries;
|
|
117
117
|
this._dedupeWindowMs = options.dedupeWindowMs || DEFAULTS.dedupeWindowMs;
|
|
118
118
|
|
|
119
119
|
/** @type {Array<{type: string, taskId: string, payload: object, ts: number}>} ring buffer */
|
|
@@ -203,7 +203,7 @@ export class AgentEventBus {
|
|
|
203
203
|
|
|
204
204
|
// ── Dedup
|
|
205
205
|
const key = `${type}:${taskId}`;
|
|
206
|
-
const last = this._recentEmits.get(key);
|
|
206
|
+
const last = this._recentEmits.get(key);
|
|
207
207
|
if (typeof last === "number" && ts - last < this._dedupeWindowMs) return;
|
|
208
208
|
this._recentEmits.set(key, ts);
|
|
209
209
|
if (this._recentEmits.size > 200) {
|
package/agent-pool.mjs
CHANGED
|
@@ -75,6 +75,25 @@ const HARD_TIMEOUT_BUFFER_MS = 5 * 60_000; // 5 minutes
|
|
|
75
75
|
|
|
76
76
|
/** Tag for console logging */
|
|
77
77
|
const TAG = "[agent-pool]";
|
|
78
|
+
const MAX_PROMPT_BYTES = 180_000;
|
|
79
|
+
|
|
80
|
+
function sanitizeAndBoundPrompt(text) {
|
|
81
|
+
if (typeof text !== "string") return "";
|
|
82
|
+
// eslint-disable-next-line no-control-regex
|
|
83
|
+
const sanitized = text.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
|
|
84
|
+
const bytes = Buffer.byteLength(sanitized, "utf8");
|
|
85
|
+
if (bytes <= MAX_PROMPT_BYTES) return sanitized;
|
|
86
|
+
const buf = Buffer.from(sanitized, "utf8").slice(0, MAX_PROMPT_BYTES);
|
|
87
|
+
const truncated = buf.toString("utf8");
|
|
88
|
+
const removedBytes = bytes - MAX_PROMPT_BYTES;
|
|
89
|
+
console.warn(
|
|
90
|
+
`${TAG} prompt truncated: ${bytes} → ${MAX_PROMPT_BYTES} bytes (removed ${removedBytes})`,
|
|
91
|
+
);
|
|
92
|
+
return (
|
|
93
|
+
truncated +
|
|
94
|
+
`\n\n[...prompt truncated — ${removedBytes} bytes removed to stay within API limits]`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
78
97
|
|
|
79
98
|
function envFlagEnabled(value) {
|
|
80
99
|
const raw = String(value ?? "")
|
|
@@ -243,13 +262,8 @@ function hasSdkPrerequisites(name) {
|
|
|
243
262
|
return { ok: true, reason: null };
|
|
244
263
|
}
|
|
245
264
|
if (name === "copilot") {
|
|
246
|
-
// Copilot
|
|
247
|
-
|
|
248
|
-
// Copilot also works from VS Code extension context — check for common indicators
|
|
249
|
-
const hasVsCode = process.env.VSCODE_PID || process.env.COPILOT_AGENT_HOST;
|
|
250
|
-
if (!hasToken && !hasVsCode) {
|
|
251
|
-
return { ok: false, reason: "no COPILOT_CLI_TOKEN or GITHUB_TOKEN" };
|
|
252
|
-
}
|
|
265
|
+
// Copilot auth can come from multiple sources (OAuth manager, gh auth,
|
|
266
|
+
// VS Code Copilot login, env tokens). Don't block execution here.
|
|
253
267
|
return { ok: true, reason: null };
|
|
254
268
|
}
|
|
255
269
|
if (name === "claude") {
|
|
@@ -775,7 +789,11 @@ async function launchCodexThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
775
789
|
// Codex steering: send a follow-up message to the live thread
|
|
776
790
|
// Note: the thread is consumed in the streaming loop below, so
|
|
777
791
|
// additional runStreamed calls are queued by the SDK
|
|
778
|
-
thread
|
|
792
|
+
thread
|
|
793
|
+
.runStreamed(sanitizeAndBoundPrompt(steerPrompt), {
|
|
794
|
+
signal: controller?.signal,
|
|
795
|
+
})
|
|
796
|
+
.catch(() => {});
|
|
779
797
|
});
|
|
780
798
|
}
|
|
781
799
|
|
|
@@ -791,7 +809,8 @@ async function launchCodexThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
791
809
|
|
|
792
810
|
// ── 4. Stream the turn ───────────────────────────────────────────────────
|
|
793
811
|
try {
|
|
794
|
-
const
|
|
812
|
+
const safePrompt = sanitizeAndBoundPrompt(prompt);
|
|
813
|
+
const turn = await thread.runStreamed(safePrompt, {
|
|
795
814
|
signal: controller.signal,
|
|
796
815
|
});
|
|
797
816
|
|
|
@@ -2237,7 +2256,8 @@ async function resumeCodexThread(threadId, prompt, cwd, timeoutMs, extra = {}) {
|
|
|
2237
2256
|
let hardTimer;
|
|
2238
2257
|
|
|
2239
2258
|
try {
|
|
2240
|
-
const
|
|
2259
|
+
const safePrompt = sanitizeAndBoundPrompt(prompt);
|
|
2260
|
+
const turn = await thread.runStreamed(safePrompt, {
|
|
2241
2261
|
signal: controller.signal,
|
|
2242
2262
|
});
|
|
2243
2263
|
let finalResponse = "";
|
package/agent-work-report.mjs
CHANGED
package/bosun.schema.json
CHANGED
|
@@ -49,6 +49,11 @@
|
|
|
49
49
|
"type": "string",
|
|
50
50
|
"enum": ["internal", "vk", "github", "jira"]
|
|
51
51
|
},
|
|
52
|
+
"syncPolicy": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"enum": ["internal-primary", "bidirectional"],
|
|
55
|
+
"default": "internal-primary"
|
|
56
|
+
},
|
|
52
57
|
"projectId": { "type": "string" },
|
|
53
58
|
"github": {
|
|
54
59
|
"type": "object",
|
package/desktop/main.mjs
CHANGED
|
@@ -41,6 +41,38 @@ function parseBoolEnv(value, fallback) {
|
|
|
41
41
|
return fallback;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function isWslInteropRuntime() {
|
|
45
|
+
return Boolean(
|
|
46
|
+
process.env.WSL_DISTRO_NAME
|
|
47
|
+
|| process.env.WSL_INTEROP
|
|
48
|
+
|| (process.platform === "win32"
|
|
49
|
+
&& String(process.env.HOME || "")
|
|
50
|
+
.trim()
|
|
51
|
+
.startsWith("/home/")),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveDesktopConfigDir() {
|
|
56
|
+
if (process.env.BOSUN_HOME) return resolve(process.env.BOSUN_HOME);
|
|
57
|
+
if (process.env.BOSUN_DIR) return resolve(process.env.BOSUN_DIR);
|
|
58
|
+
|
|
59
|
+
const preferWindowsDirs = process.platform === "win32" && !isWslInteropRuntime();
|
|
60
|
+
const baseDir = preferWindowsDirs
|
|
61
|
+
? process.env.APPDATA
|
|
62
|
+
|| process.env.LOCALAPPDATA
|
|
63
|
+
|| process.env.USERPROFILE
|
|
64
|
+
|| process.env.HOME
|
|
65
|
+
|| homedir()
|
|
66
|
+
: process.env.HOME
|
|
67
|
+
|| process.env.XDG_CONFIG_HOME
|
|
68
|
+
|| process.env.USERPROFILE
|
|
69
|
+
|| process.env.APPDATA
|
|
70
|
+
|| process.env.LOCALAPPDATA
|
|
71
|
+
|| homedir();
|
|
72
|
+
|
|
73
|
+
return resolve(baseDir, "bosun");
|
|
74
|
+
}
|
|
75
|
+
|
|
44
76
|
function isProcessAlive(pid) {
|
|
45
77
|
try {
|
|
46
78
|
process.kill(pid, 0);
|
|
@@ -223,7 +255,11 @@ async function ensureDaemonRunning() {
|
|
|
223
255
|
async function startUiServer() {
|
|
224
256
|
if (uiServerStarted) return;
|
|
225
257
|
const api = await loadUiServerModule();
|
|
226
|
-
const server = await api.startTelegramUiServer({
|
|
258
|
+
const server = await api.startTelegramUiServer({
|
|
259
|
+
dependencies: {
|
|
260
|
+
configDir: resolveDesktopConfigDir(),
|
|
261
|
+
},
|
|
262
|
+
});
|
|
227
263
|
if (!server) {
|
|
228
264
|
throw new Error("Failed to start Telegram UI server.");
|
|
229
265
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git-commit-helpers.mjs — Git commit utilities for Bosun
|
|
3
|
+
*
|
|
4
|
+
* Ensures Bosun Bot is credited in commits and PRs via GitHub's
|
|
5
|
+
* Co-authored-by trailer convention.
|
|
6
|
+
*
|
|
7
|
+
* GitHub App bot user ID: 262908237
|
|
8
|
+
* Noreply email: 262908237+bosun-ve[bot]@users.noreply.github.com
|
|
9
|
+
* GitHub appearance: https://github.com/apps/bosun-ve
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const BOSUN_BOT_TRAILER =
|
|
13
|
+
"Co-authored-by: bosun-ve[bot] <262908237+bosun-ve[bot]@users.noreply.github.com>";
|
|
14
|
+
|
|
15
|
+
const BOSUN_PR_CREDIT =
|
|
16
|
+
"\n\n---\n*Created by [Bosun Bot](https://github.com/apps/bosun-ve)*";
|
|
17
|
+
|
|
18
|
+
// ── Commit message helpers ────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Appends the Bosun bot Co-authored-by trailer to a commit message.
|
|
22
|
+
*
|
|
23
|
+
* GitHub displays the bot in the Contributors graph when this trailer is present.
|
|
24
|
+
* The Co-authored-by line must be separated from the message body by a blank line.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} message - original commit message
|
|
27
|
+
* @returns {string} commit message with trailer appended
|
|
28
|
+
*/
|
|
29
|
+
export function appendBosunCoAuthor(message) {
|
|
30
|
+
if (message.includes("Co-authored-by: bosun-ve")) return message;
|
|
31
|
+
const trimmed = message.trimEnd();
|
|
32
|
+
return `${trimmed}\n\n${BOSUN_BOT_TRAILER}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builds a complete commit message with an optional Bosun bot credit trailer.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} title - commit title (first line / summary)
|
|
39
|
+
* @param {string} [body] - commit body (optional extended description)
|
|
40
|
+
* @param {Object} [opts]
|
|
41
|
+
* @param {boolean} [opts.addBosunCredit=true] - whether to append the co-author trailer
|
|
42
|
+
* @returns {string} full commit message
|
|
43
|
+
*/
|
|
44
|
+
export function buildCommitMessage(title, body = "", { addBosunCredit = true } = {}) {
|
|
45
|
+
const parts = [title.trimEnd()];
|
|
46
|
+
if (body && body.trim()) {
|
|
47
|
+
parts.push(""); // blank line
|
|
48
|
+
parts.push(body.trimEnd());
|
|
49
|
+
}
|
|
50
|
+
const base = parts.join("\n");
|
|
51
|
+
return addBosunCredit ? appendBosunCoAuthor(base) : base;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── PR body helpers ───────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Appends the Bosun Bot credit footer to a PR body.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} body - original PR description
|
|
60
|
+
* @returns {string} PR body with Bosun Bot credit appended
|
|
61
|
+
*/
|
|
62
|
+
export function appendBosunPrCredit(body) {
|
|
63
|
+
if (body.includes("Bosun Bot") || body.includes("bosun-ve")) return body;
|
|
64
|
+
return body.trimEnd() + BOSUN_PR_CREDIT;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns the Bosun bot Co-authored-by trailer string (for direct use).
|
|
69
|
+
*
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
export function getBosunCoAuthorTrailer() {
|
|
73
|
+
return BOSUN_BOT_TRAILER;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Returns the Bosun bot PR credit footer markdown (for direct use).
|
|
78
|
+
*
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
export function getBosunPrCredit() {
|
|
82
|
+
return BOSUN_PR_CREDIT;
|
|
83
|
+
}
|
package/kanban-adapter.mjs
CHANGED
|
@@ -1280,6 +1280,8 @@ class GitHubIssuesAdapter {
|
|
|
1280
1280
|
/** @type {Map<string, {fields: any, time: number}>} projectNumber → {fields, time} */
|
|
1281
1281
|
this._projectFieldsCache = new Map();
|
|
1282
1282
|
this._projectFieldsCacheTTL = 300_000; // 5 minutes
|
|
1283
|
+
/** @type {Set<string>} projects already warned for missing Status field */
|
|
1284
|
+
this._projectStatusFieldWarned = new Set();
|
|
1283
1285
|
this._repositoryNodeId = null;
|
|
1284
1286
|
|
|
1285
1287
|
// Auto-sync toggle: set GITHUB_PROJECT_AUTO_SYNC=false to disable project sync
|
|
@@ -1309,6 +1311,46 @@ class GitHubIssuesAdapter {
|
|
|
1309
1311
|
this._issueListCache = new Map();
|
|
1310
1312
|
/** @type {Map<string, {data: object|null, ts: number}>} issueNum → {data, ts} */
|
|
1311
1313
|
this._sharedStateCache = new Map();
|
|
1314
|
+
this._lastKnownTasks = [];
|
|
1315
|
+
this._taskListBackoffUntil = 0;
|
|
1316
|
+
this._taskListBackoffMs = parseDelayMs(
|
|
1317
|
+
process.env.GH_TASK_LIST_BACKOFF_MS,
|
|
1318
|
+
90_000,
|
|
1319
|
+
5_000,
|
|
1320
|
+
);
|
|
1321
|
+
this._taskListBackoffWarnAt = 0;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
_cacheLastKnownTasks(tasks) {
|
|
1325
|
+
if (!Array.isArray(tasks)) return;
|
|
1326
|
+
this._lastKnownTasks = tasks.map((task) => ({
|
|
1327
|
+
...task,
|
|
1328
|
+
meta: task?.meta ? { ...task.meta } : {},
|
|
1329
|
+
}));
|
|
1330
|
+
this._taskListBackoffUntil = 0;
|
|
1331
|
+
this._taskListBackoffWarnAt = 0;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
_setTaskListBackoff() {
|
|
1335
|
+
const now = Date.now();
|
|
1336
|
+
this._taskListBackoffUntil = Math.max(
|
|
1337
|
+
this._taskListBackoffUntil || 0,
|
|
1338
|
+
now + this._taskListBackoffMs,
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
_getBackoffTasks(filters = {}) {
|
|
1343
|
+
let fallback = this._lastKnownTasks.map((task) => ({
|
|
1344
|
+
...task,
|
|
1345
|
+
meta: task?.meta ? { ...task.meta } : {},
|
|
1346
|
+
}));
|
|
1347
|
+
|
|
1348
|
+
if (filters.status) {
|
|
1349
|
+
const normalizedFilter = normaliseStatus(filters.status);
|
|
1350
|
+
fallback = fallback.filter((task) => task.status === normalizedFilter);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
return fallback;
|
|
1312
1354
|
}
|
|
1313
1355
|
|
|
1314
1356
|
/**
|
|
@@ -1341,7 +1383,15 @@ class GitHubIssuesAdapter {
|
|
|
1341
1383
|
"json",
|
|
1342
1384
|
]);
|
|
1343
1385
|
|
|
1344
|
-
|
|
1386
|
+
const normalizedFields = Array.isArray(fields)
|
|
1387
|
+
? fields
|
|
1388
|
+
: Array.isArray(fields?.fields)
|
|
1389
|
+
? fields.fields
|
|
1390
|
+
: Array.isArray(fields?.data)
|
|
1391
|
+
? fields.data
|
|
1392
|
+
: null;
|
|
1393
|
+
|
|
1394
|
+
if (!Array.isArray(normalizedFields)) {
|
|
1345
1395
|
console.warn(
|
|
1346
1396
|
`${TAG} project field-list returned non-array for project ${projectNumber}`,
|
|
1347
1397
|
);
|
|
@@ -1349,7 +1399,7 @@ class GitHubIssuesAdapter {
|
|
|
1349
1399
|
}
|
|
1350
1400
|
|
|
1351
1401
|
// Find the Status field
|
|
1352
|
-
const statusField =
|
|
1402
|
+
const statusField = normalizedFields.find(
|
|
1353
1403
|
(f) =>
|
|
1354
1404
|
f.name === "Status" &&
|
|
1355
1405
|
(f.type === "SINGLE_SELECT" || f.data_type === "SINGLE_SELECT"),
|
|
@@ -1366,7 +1416,7 @@ class GitHubIssuesAdapter {
|
|
|
1366
1416
|
// Cache the result (also cache the raw fields array for getProjectFields)
|
|
1367
1417
|
this._projectFieldsCache.set(cacheKey, {
|
|
1368
1418
|
fields: result,
|
|
1369
|
-
rawFields:
|
|
1419
|
+
rawFields: normalizedFields,
|
|
1370
1420
|
time: now,
|
|
1371
1421
|
});
|
|
1372
1422
|
|
|
@@ -1950,10 +2000,9 @@ class GitHubIssuesAdapter {
|
|
|
1950
2000
|
|
|
1951
2001
|
return tasks;
|
|
1952
2002
|
} catch (err) {
|
|
1953
|
-
|
|
1954
|
-
|
|
2003
|
+
throw new Error(
|
|
2004
|
+
`project ${projectNumber} list failed: ${err?.message || err}`,
|
|
1955
2005
|
);
|
|
1956
|
-
return [];
|
|
1957
2006
|
}
|
|
1958
2007
|
}
|
|
1959
2008
|
|
|
@@ -1981,9 +2030,16 @@ class GitHubIssuesAdapter {
|
|
|
1981
2030
|
// Get project fields
|
|
1982
2031
|
const fields = await this._getProjectFields(projectNumber);
|
|
1983
2032
|
if (!fields || !fields.statusFieldId) {
|
|
1984
|
-
|
|
2033
|
+
const projectKey = String(projectNumber || "");
|
|
2034
|
+
if (!this._projectStatusFieldWarned.has(projectKey)) {
|
|
2035
|
+
this._projectStatusFieldWarned.add(projectKey);
|
|
2036
|
+
console.warn(
|
|
2037
|
+
`${TAG} cannot sync to project ${projectKey}: no Status field found`,
|
|
2038
|
+
);
|
|
2039
|
+
}
|
|
1985
2040
|
return false;
|
|
1986
2041
|
}
|
|
2042
|
+
this._projectStatusFieldWarned.delete(String(projectNumber || ""));
|
|
1987
2043
|
|
|
1988
2044
|
// Map codex status to project status option using configurable mapping
|
|
1989
2045
|
const targetStatusName = this._normalizeProjectStatus(status, true);
|
|
@@ -2256,6 +2312,24 @@ class GitHubIssuesAdapter {
|
|
|
2256
2312
|
}
|
|
2257
2313
|
|
|
2258
2314
|
async listTasks(_projectId, filters = {}) {
|
|
2315
|
+
const now = Date.now();
|
|
2316
|
+
if (this._taskListBackoffUntil > now) {
|
|
2317
|
+
const fallback = this._getBackoffTasks(filters);
|
|
2318
|
+
if (fallback.length > 0) {
|
|
2319
|
+
if (now - this._taskListBackoffWarnAt > 15_000) {
|
|
2320
|
+
this._taskListBackoffWarnAt = now;
|
|
2321
|
+
const sec = Math.max(
|
|
2322
|
+
1,
|
|
2323
|
+
Math.ceil((this._taskListBackoffUntil - now) / 1000),
|
|
2324
|
+
);
|
|
2325
|
+
console.warn(
|
|
2326
|
+
`${TAG} task list in backoff (${sec}s remaining) — serving ${fallback.length} cached task(s)`,
|
|
2327
|
+
);
|
|
2328
|
+
}
|
|
2329
|
+
return fallback;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2259
2333
|
// If project mode is enabled, read from project board
|
|
2260
2334
|
if (this._projectMode === "kanban" && this._projectNumber) {
|
|
2261
2335
|
const projectNumber = await this._resolveProjectNumber();
|
|
@@ -2296,6 +2370,7 @@ class GitHubIssuesAdapter {
|
|
|
2296
2370
|
}
|
|
2297
2371
|
}
|
|
2298
2372
|
|
|
2373
|
+
this._cacheLastKnownTasks(filtered);
|
|
2299
2374
|
return filtered;
|
|
2300
2375
|
} catch (err) {
|
|
2301
2376
|
console.warn(
|
|
@@ -2345,8 +2420,20 @@ class GitHubIssuesAdapter {
|
|
|
2345
2420
|
} else {
|
|
2346
2421
|
args.push("--state", "open");
|
|
2347
2422
|
}
|
|
2348
|
-
|
|
2349
|
-
|
|
2423
|
+
try {
|
|
2424
|
+
rawIssues = await this._gh(args);
|
|
2425
|
+
this._issueListCache.set(listCacheKey, { data: rawIssues, ts: nowMs });
|
|
2426
|
+
} catch (err) {
|
|
2427
|
+
this._setTaskListBackoff();
|
|
2428
|
+
const fallback = this._getBackoffTasks(filters);
|
|
2429
|
+
if (fallback.length > 0) {
|
|
2430
|
+
console.warn(
|
|
2431
|
+
`${TAG} failed to list issues (${err.message}); serving ${fallback.length} cached task(s)`,
|
|
2432
|
+
);
|
|
2433
|
+
return fallback;
|
|
2434
|
+
}
|
|
2435
|
+
throw err;
|
|
2436
|
+
}
|
|
2350
2437
|
}
|
|
2351
2438
|
|
|
2352
2439
|
let normalized = (Array.isArray(rawIssues) ? rawIssues : []).map((i) =>
|
|
@@ -2390,6 +2477,7 @@ class GitHubIssuesAdapter {
|
|
|
2390
2477
|
}
|
|
2391
2478
|
}
|
|
2392
2479
|
|
|
2480
|
+
this._cacheLastKnownTasks(normalized);
|
|
2393
2481
|
return normalized;
|
|
2394
2482
|
}
|
|
2395
2483
|
|
|
@@ -5209,5 +5297,3 @@ export async function unmarkTaskIgnored(taskId) {
|
|
|
5209
5297
|
);
|
|
5210
5298
|
return false;
|
|
5211
5299
|
}
|
|
5212
|
-
|
|
5213
|
-
|
package/maintenance.mjs
CHANGED
|
@@ -888,26 +888,93 @@ export function syncLocalTrackingBranches(repoRoot, branches) {
|
|
|
888
888
|
);
|
|
889
889
|
if (remoteCheck.status !== 0) continue;
|
|
890
890
|
|
|
891
|
-
//
|
|
891
|
+
// Measure divergence in both directions up front
|
|
892
892
|
const behindCheck = spawnSync(
|
|
893
893
|
"git",
|
|
894
894
|
["rev-list", "--count", `${branch}..${remoteRef}`],
|
|
895
895
|
{ cwd: repoRoot, encoding: "utf8", timeout: 5000, windowsHide: true },
|
|
896
896
|
);
|
|
897
897
|
const behind = parseInt(behindCheck.stdout?.trim(), 10) || 0;
|
|
898
|
-
if (behind === 0) continue; // Already up to date
|
|
899
898
|
|
|
900
|
-
// Check if local has commits not in remote (diverged)
|
|
901
899
|
const aheadCheck = spawnSync(
|
|
902
900
|
"git",
|
|
903
901
|
["rev-list", "--count", `${remoteRef}..${branch}`],
|
|
904
902
|
{ cwd: repoRoot, encoding: "utf8", timeout: 5000, windowsHide: true },
|
|
905
903
|
);
|
|
906
904
|
const ahead = parseInt(aheadCheck.stdout?.trim(), 10) || 0;
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
905
|
+
|
|
906
|
+
if (behind === 0 && ahead === 0) continue; // Already in sync
|
|
907
|
+
|
|
908
|
+
// Local is ahead of remote but not behind — try a plain push
|
|
909
|
+
if (behind === 0 && ahead > 0) {
|
|
910
|
+
const push = spawnSync(
|
|
911
|
+
"git",
|
|
912
|
+
["push", "origin", `${branch}:refs/heads/${branch}`, "--quiet"],
|
|
913
|
+
{ cwd: repoRoot, encoding: "utf8", timeout: 30_000, windowsHide: true },
|
|
910
914
|
);
|
|
915
|
+
if (push.status === 0) {
|
|
916
|
+
console.log(
|
|
917
|
+
`[maintenance] pushed local '${branch}' to origin (${ahead} commit(s) ahead)`,
|
|
918
|
+
);
|
|
919
|
+
synced++;
|
|
920
|
+
} else {
|
|
921
|
+
console.warn(
|
|
922
|
+
`[maintenance] git push '${branch}' failed: ${(push.stderr || push.stdout || "").toString().trim()}`,
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Truly diverged: local has unique commits AND is missing remote commits.
|
|
929
|
+
// Attempt rebase onto remote then push (checked-out branch only).
|
|
930
|
+
if (ahead > 0) {
|
|
931
|
+
const statusCheck = spawnSync("git", ["status", "--porcelain"], {
|
|
932
|
+
cwd: repoRoot,
|
|
933
|
+
encoding: "utf8",
|
|
934
|
+
timeout: 5000,
|
|
935
|
+
windowsHide: true,
|
|
936
|
+
});
|
|
937
|
+
if (statusCheck.stdout?.trim()) {
|
|
938
|
+
console.warn(
|
|
939
|
+
`[maintenance] local '${branch}' diverged (${ahead}↑ ${behind}↓) but has uncommitted changes — skipping`,
|
|
940
|
+
);
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
if (branch === currentBranch) {
|
|
944
|
+
const rebase = spawnSync(
|
|
945
|
+
"git",
|
|
946
|
+
["rebase", remoteRef, "--quiet"],
|
|
947
|
+
{ cwd: repoRoot, encoding: "utf8", timeout: 60_000, windowsHide: true },
|
|
948
|
+
);
|
|
949
|
+
if (rebase.status !== 0) {
|
|
950
|
+
spawnSync("git", ["rebase", "--abort"], {
|
|
951
|
+
cwd: repoRoot, timeout: 10_000, windowsHide: true,
|
|
952
|
+
});
|
|
953
|
+
console.warn(
|
|
954
|
+
`[maintenance] rebase of '${branch}' onto ${remoteRef} failed (${ahead}↑ ${behind}↓) — skipping`,
|
|
955
|
+
);
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
const push = spawnSync(
|
|
959
|
+
"git",
|
|
960
|
+
["push", "origin", `${branch}:refs/heads/${branch}`, "--quiet"],
|
|
961
|
+
{ cwd: repoRoot, encoding: "utf8", timeout: 30_000, windowsHide: true },
|
|
962
|
+
);
|
|
963
|
+
if (push.status === 0) {
|
|
964
|
+
console.log(
|
|
965
|
+
`[maintenance] rebased and pushed '${branch}' (was ${ahead}↑ ${behind}↓)`,
|
|
966
|
+
);
|
|
967
|
+
synced++;
|
|
968
|
+
} else {
|
|
969
|
+
console.warn(
|
|
970
|
+
`[maintenance] push after rebase of '${branch}' failed: ${(push.stderr || push.stdout || "").toString().trim()}`,
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
} else {
|
|
974
|
+
console.warn(
|
|
975
|
+
`[maintenance] local '${branch}' diverged (${ahead}↑ ${behind}↓) and not checked out — skipping (rebase requires checkout)`,
|
|
976
|
+
);
|
|
977
|
+
}
|
|
911
978
|
continue;
|
|
912
979
|
}
|
|
913
980
|
|