bosun 0.34.2 → 0.34.4
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-pool.mjs +30 -10
- package/bosun.schema.json +5 -0
- package/cli.mjs +121 -1
- package/desktop/main.mjs +37 -1
- package/git-commit-helpers.mjs +83 -0
- package/kanban-adapter.mjs +97 -11
- package/monitor.mjs +179 -39
- package/package.json +2 -1
- package/setup-web-server.mjs +32 -7
- package/task-executor.mjs +7 -10
- package/task-store.mjs +43 -10
- package/telegram-bot.mjs +96 -23
- package/ui/modules/settings-schema.js +6 -0
- package/ui/tabs/control.js +2 -1
- package/ui/tabs/settings.js +21 -3
- package/ui-server.mjs +106 -12
- package/workflow-nodes.mjs +109 -2
- package/workspace-manager.mjs +18 -6
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/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/cli.mjs
CHANGED
|
@@ -81,6 +81,7 @@ function showHelp() {
|
|
|
81
81
|
--no-auto-update Disable background auto-update polling
|
|
82
82
|
--daemon, -d Run as a background daemon (detached, with PID file)
|
|
83
83
|
--stop-daemon Stop a running daemon process
|
|
84
|
+
--terminate Hard-stop all bosun processes (daemon + monitor + companions)
|
|
84
85
|
--daemon-status Check if daemon is running
|
|
85
86
|
|
|
86
87
|
ORCHESTRATOR
|
|
@@ -674,6 +675,122 @@ function daemonStatus() {
|
|
|
674
675
|
process.exit(0);
|
|
675
676
|
}
|
|
676
677
|
|
|
678
|
+
function findAllBosunProcessPids() {
|
|
679
|
+
const patterns = [
|
|
680
|
+
"cli.mjs",
|
|
681
|
+
"monitor.mjs",
|
|
682
|
+
"telegram-bot.mjs",
|
|
683
|
+
"telegram-sentinel.mjs",
|
|
684
|
+
"ui-server.mjs",
|
|
685
|
+
];
|
|
686
|
+
const joined = patterns.join("|");
|
|
687
|
+
if (process.platform === "win32") {
|
|
688
|
+
try {
|
|
689
|
+
const out = execFileSync(
|
|
690
|
+
"powershell",
|
|
691
|
+
[
|
|
692
|
+
"-NoProfile",
|
|
693
|
+
"-Command",
|
|
694
|
+
`Get-CimInstance Win32_Process | Where-Object { $_.Name -match '^node(\\.exe)?$' -and $_.CommandLine -match '${joined.replace(/\|/g, "|")}' } | Select-Object -ExpandProperty ProcessId`,
|
|
695
|
+
],
|
|
696
|
+
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 4000 },
|
|
697
|
+
).trim();
|
|
698
|
+
if (!out) return [];
|
|
699
|
+
return out
|
|
700
|
+
.split(/\r?\n/)
|
|
701
|
+
.map((s) => Number.parseInt(String(s).trim(), 10))
|
|
702
|
+
.filter((pid) => Number.isFinite(pid) && pid > 0 && pid !== process.pid);
|
|
703
|
+
} catch {
|
|
704
|
+
return [];
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
try {
|
|
708
|
+
const out = execFileSync("pgrep", ["-f", joined], {
|
|
709
|
+
encoding: "utf8",
|
|
710
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
711
|
+
timeout: 4000,
|
|
712
|
+
}).trim();
|
|
713
|
+
if (!out) return [];
|
|
714
|
+
return out
|
|
715
|
+
.split(/\r?\n/)
|
|
716
|
+
.map((s) => Number.parseInt(String(s).trim(), 10))
|
|
717
|
+
.filter((pid) => Number.isFinite(pid) && pid > 0 && pid !== process.pid);
|
|
718
|
+
} catch {
|
|
719
|
+
return [];
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function removeKnownPidFiles() {
|
|
724
|
+
const pidFiles = [
|
|
725
|
+
DAEMON_PID_FILE,
|
|
726
|
+
PID_FILE,
|
|
727
|
+
SENTINEL_PID_FILE,
|
|
728
|
+
SENTINEL_PID_FILE_LEGACY,
|
|
729
|
+
resolve(__dirname, "..", ".cache", "bosun.pid"),
|
|
730
|
+
resolve(process.cwd(), ".cache", "bosun.pid"),
|
|
731
|
+
];
|
|
732
|
+
for (const pidFile of pidFiles) {
|
|
733
|
+
try {
|
|
734
|
+
if (existsSync(pidFile)) unlinkSync(pidFile);
|
|
735
|
+
} catch {
|
|
736
|
+
/* best effort */
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function terminateBosun() {
|
|
742
|
+
const tracked = [
|
|
743
|
+
getDaemonPid(),
|
|
744
|
+
readAlivePid(PID_FILE),
|
|
745
|
+
readAlivePid(SENTINEL_PID_FILE),
|
|
746
|
+
readAlivePid(SENTINEL_PID_FILE_LEGACY),
|
|
747
|
+
].filter((pid) => Number.isFinite(pid) && pid > 0);
|
|
748
|
+
const ghosts = findGhostDaemonPids();
|
|
749
|
+
const scanned = findAllBosunProcessPids();
|
|
750
|
+
const allPids = Array.from(new Set([...tracked, ...ghosts, ...scanned])).filter(
|
|
751
|
+
(pid) => pid !== process.pid,
|
|
752
|
+
);
|
|
753
|
+
if (allPids.length === 0) {
|
|
754
|
+
removeKnownPidFiles();
|
|
755
|
+
console.log(" No running bosun processes found.");
|
|
756
|
+
process.exit(0);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
console.log(` Terminating ${allPids.length} bosun process(es): ${allPids.join(", ")}`);
|
|
761
|
+
for (const pid of allPids) {
|
|
762
|
+
try {
|
|
763
|
+
process.kill(pid, "SIGTERM");
|
|
764
|
+
} catch {
|
|
765
|
+
/* already dead */
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const deadline = Date.now() + 5000;
|
|
770
|
+
let alive = allPids.filter((pid) => isProcessAlive(pid));
|
|
771
|
+
while (alive.length > 0 && Date.now() < deadline) {
|
|
772
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
|
|
773
|
+
alive = alive.filter((pid) => isProcessAlive(pid));
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
for (const pid of alive) {
|
|
777
|
+
try {
|
|
778
|
+
process.kill(pid, "SIGKILL");
|
|
779
|
+
} catch {
|
|
780
|
+
/* already dead */
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
removeKnownPidFiles();
|
|
784
|
+
const killed = allPids.length - alive.length;
|
|
785
|
+
console.log(` ✓ Terminated ${killed}/${allPids.length} process(es).`);
|
|
786
|
+
if (alive.length > 0) {
|
|
787
|
+
console.log(` ⚠️ Still alive: ${alive.join(", ")}`);
|
|
788
|
+
process.exit(1);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
process.exit(0);
|
|
792
|
+
}
|
|
793
|
+
|
|
677
794
|
async function main() {
|
|
678
795
|
// Apply legacy CODEX_MONITOR_* → BOSUN_* env aliases before any config ops
|
|
679
796
|
applyAllCompatibility();
|
|
@@ -793,6 +910,10 @@ async function main() {
|
|
|
793
910
|
stopDaemon();
|
|
794
911
|
return;
|
|
795
912
|
}
|
|
913
|
+
if (args.includes("--terminate")) {
|
|
914
|
+
terminateBosun();
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
796
917
|
if (args.includes("--daemon-status")) {
|
|
797
918
|
daemonStatus();
|
|
798
919
|
return;
|
|
@@ -1500,4 +1621,3 @@ main().catch(async (err) => {
|
|
|
1500
1621
|
await sendCrashNotification(1, null).catch(() => {});
|
|
1501
1622
|
process.exit(1);
|
|
1502
1623
|
});
|
|
1503
|
-
|
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
|
-
|