bosun 0.40.4 → 0.40.6
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/.env.example +13 -0
- package/README.md +9 -2
- package/agent/agent-pool.mjs +16 -7
- package/agent/agent-supervisor.mjs +81 -28
- package/agent/autofix.mjs +50 -36
- package/agent/fleet-coordinator.mjs +2 -1
- package/agent/hook-profiles.mjs +1 -1
- package/agent/review-agent.mjs +48 -19
- package/bosun.schema.json +51 -0
- package/config/context-shredding-config.mjs +143 -0
- package/infra/library-manager.mjs +717 -9
- package/infra/maintenance.mjs +9 -4
- package/infra/monitor.mjs +55 -8
- package/kanban/ve-kanban.mjs +12 -2
- package/kanban/vk-log-stream.mjs +4 -1
- package/package.json +3 -2
- package/server/setup-web-server.mjs +112 -10
- package/server/ui-server.mjs +146 -14
- package/setup.mjs +25 -16
- package/shell/codex-config.mjs +19 -8
- package/shell/copilot-shell.mjs +55 -7
- package/task/task-assessment.mjs +17 -9
- package/task/task-executor.mjs +37 -0
- package/telegram/telegram-bot.mjs +5 -4
- package/ui/demo-defaults.js +146 -24
- package/ui/demo.html +73 -2
- package/ui/modules/settings-schema.js +9 -0
- package/ui/setup.html +95 -0
- package/ui/tabs/library.js +31 -6
- package/ui/tabs/tasks.js +615 -85
- package/ui/tabs/telemetry.js +72 -22
- package/voice/voice-agents-sdk.mjs +5 -1
- package/workflow/workflow-engine.mjs +100 -16
- package/workflow/workflow-nodes.mjs +146 -9
- package/workflow-templates/github.mjs +11 -8
- package/workflow-templates/task-batch.mjs +1 -1
- package/workflow-templates/task-lifecycle.mjs +16 -3
- package/workspace/context-cache.mjs +399 -6
- package/workspace/workspace-manager.mjs +22 -16
- package/workspace/worktree-manager.mjs +7 -7
package/.env.example
CHANGED
|
@@ -196,6 +196,19 @@ VOICE_FALLBACK_MODE=browser
|
|
|
196
196
|
# Executor used by voice tool delegations for complex requests
|
|
197
197
|
VOICE_DELEGATE_EXECUTOR=codex-sdk
|
|
198
198
|
|
|
199
|
+
# ─── Context Shredding / Live Tool Compaction ───────────────────────────────
|
|
200
|
+
# Traditional shredding trims older context turns. Live tool compaction runs
|
|
201
|
+
# earlier: it summarizes large, noisy command outputs before they ever land in
|
|
202
|
+
# the active turn, while preserving a `bosun --tool-log <id>` retrieval path.
|
|
203
|
+
# CONTEXT_SHREDDING_ENABLED=true
|
|
204
|
+
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_ENABLED=false
|
|
205
|
+
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_MODE=auto
|
|
206
|
+
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_MIN_CHARS=4000
|
|
207
|
+
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_TARGET_CHARS=1800
|
|
208
|
+
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_MIN_SAVINGS_PCT=15
|
|
209
|
+
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_MIN_RUNTIME_MS=2000
|
|
210
|
+
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_BLOCK_STRUCTURED_OUTPUT=true
|
|
211
|
+
# CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_ALLOW_COMMANDS=grep,rg,find,findstr,select-string,ag,ack,sift,fd,where,which,ls,dir,tree,git,go,npm,pnpm,yarn,npx,bun,node,python,python3,pytest,pip,pip3,poetry,docker,kubectl,helm,terraform,ansible,ansible-playbook,journalctl,tail,get-content,cargo,gradle,maven,mvn,javac,tsc,jest,vitest,deno,make,cmake,bazel,buck,nx,turbo,rush,composer,bundle
|
|
199
212
|
# ─── Desktop Portal ────────────────────────────────────────────────────────
|
|
200
213
|
# Auto-start bosun daemon when the desktop portal launches (default: true)
|
|
201
214
|
# BOSUN_DESKTOP_AUTO_START_DAEMON=true
|
package/README.md
CHANGED
|
@@ -183,8 +183,15 @@ npm run hooks:install
|
|
|
183
183
|
|
|
184
184
|
If you find this project useful or would like to stay up to date with new releases, a star is appreciated!
|
|
185
185
|
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
## Star History
|
|
187
|
+
|
|
188
|
+
<a href="https://www.star-history.com/?repos=VirtEngine%2FBosun&type=date&legend=top-left">
|
|
189
|
+
<picture>
|
|
190
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=VirtEngine/Bosun&type=date&theme=dark&legend=top-left" />
|
|
191
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=VirtEngine/Bosun&type=date&legend=top-left" />
|
|
192
|
+
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=VirtEngine/Bosun&type=date&legend=top-left" />
|
|
193
|
+
</picture>
|
|
194
|
+
</a>
|
|
188
195
|
---
|
|
189
196
|
|
|
190
197
|
## License
|
package/agent/agent-pool.mjs
CHANGED
|
@@ -44,6 +44,7 @@ import { fileURLToPath } from "node:url";
|
|
|
44
44
|
import { loadConfig } from "../config/config.mjs";
|
|
45
45
|
import { resolveRepoRoot, resolveAgentRepoRoot } from "../config/repo-root.mjs";
|
|
46
46
|
import { resolveCodexProfileRuntime } from "../shell/codex-model-profiles.mjs";
|
|
47
|
+
import { resolveCopilotCliLaunchConfig } from "../shell/copilot-shell.mjs";
|
|
47
48
|
import { getGitHubToken } from "../github/github-auth-manager.mjs";
|
|
48
49
|
import {
|
|
49
50
|
isTransientStreamError,
|
|
@@ -489,6 +490,10 @@ function shouldFallbackForSdkError(error) {
|
|
|
489
490
|
if (!error) return false;
|
|
490
491
|
const message = String(error).toLowerCase();
|
|
491
492
|
if (!message) return false;
|
|
493
|
+
if (message.includes("protocol version mismatch")) return true;
|
|
494
|
+
if (message.includes("sdk expects version") && message.includes("server reports version")) {
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
492
497
|
// SDK not installed / not found
|
|
493
498
|
if (message.includes("not available")) return true;
|
|
494
499
|
// Missing finish_reason (incomplete response)
|
|
@@ -808,6 +813,10 @@ function shouldApplySdkCooldown(error) {
|
|
|
808
813
|
if (!error) return false;
|
|
809
814
|
const message = String(error).toLowerCase();
|
|
810
815
|
if (!message) return false;
|
|
816
|
+
if (message.includes("protocol version mismatch")) return true;
|
|
817
|
+
if (message.includes("sdk expects version") && message.includes("server reports version")) {
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
811
820
|
if (message.includes("timeout")) return true;
|
|
812
821
|
if (message.includes("rate limit") || message.includes("429")) return true;
|
|
813
822
|
if (message.includes("service unavailable") || message.includes("503")) {
|
|
@@ -1448,22 +1457,22 @@ async function launchCopilotThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1448
1457
|
console.warn(`${TAG} copilot MCP config write failed (non-fatal): ${mcpErr.message}`);
|
|
1449
1458
|
}
|
|
1450
1459
|
}
|
|
1451
|
-
const
|
|
1460
|
+
const cliLaunch = resolveCopilotCliLaunchConfig({
|
|
1461
|
+
env: runtimeEnv,
|
|
1462
|
+
repoRoot: REPO_ROOT,
|
|
1463
|
+
cliArgs: buildPoolCopilotCliArgs(mcpConfigPath),
|
|
1464
|
+
});
|
|
1452
1465
|
clientOpts = {
|
|
1453
1466
|
cwd,
|
|
1454
1467
|
env: clientEnv,
|
|
1455
|
-
cliArgs,
|
|
1468
|
+
cliArgs: cliLaunch.cliArgs,
|
|
1456
1469
|
useStdio: true,
|
|
1457
1470
|
};
|
|
1458
1471
|
if (token) {
|
|
1459
1472
|
clientOpts.githubToken = token;
|
|
1460
1473
|
clientOpts.token = token;
|
|
1461
1474
|
}
|
|
1462
|
-
|
|
1463
|
-
process.env.COPILOT_CLI_PATH ||
|
|
1464
|
-
process.env.GITHUB_COPILOT_CLI_PATH ||
|
|
1465
|
-
undefined;
|
|
1466
|
-
if (cliPath) clientOpts.cliPath = cliPath;
|
|
1475
|
+
if (cliLaunch.cliPath) clientOpts.cliPath = cliLaunch.cliPath;
|
|
1467
1476
|
}
|
|
1468
1477
|
client = new CopilotClientClass(clientOpts);
|
|
1469
1478
|
await client.start();
|
|
@@ -901,6 +901,16 @@ export class AgentSupervisor {
|
|
|
901
901
|
const progress = signals.sessionProgress;
|
|
902
902
|
const analysis = signals.sessionAnalysis;
|
|
903
903
|
const anomalies = signals.activeAnomalies || [];
|
|
904
|
+
const includesAny = (text, needles) => needles.some((needle) => text.includes(needle));
|
|
905
|
+
const includesOrdered = (text, parts) => {
|
|
906
|
+
let cursor = 0;
|
|
907
|
+
for (const part of parts) {
|
|
908
|
+
const idx = text.indexOf(part, cursor);
|
|
909
|
+
if (idx === -1) return false;
|
|
910
|
+
cursor = idx + part.length;
|
|
911
|
+
}
|
|
912
|
+
return true;
|
|
913
|
+
};
|
|
904
914
|
|
|
905
915
|
// ── Agent completely dead ──
|
|
906
916
|
if (
|
|
@@ -914,7 +924,13 @@ export class AgentSupervisor {
|
|
|
914
924
|
// ── External errors (check first — these aren't the agent's fault) ──
|
|
915
925
|
if (context.error || signals.error) {
|
|
916
926
|
const errText = context.error || signals.error || "";
|
|
917
|
-
|
|
927
|
+
const errLower = String(errText).toLowerCase();
|
|
928
|
+
if (
|
|
929
|
+
errLower.includes("429") ||
|
|
930
|
+
includesOrdered(errLower, ["rate", "limit"]) ||
|
|
931
|
+
errLower.includes("too many requests") ||
|
|
932
|
+
errLower.includes("quota exceeded")
|
|
933
|
+
) {
|
|
918
934
|
// Check for flood
|
|
919
935
|
if (signals.errorPatterns?.length >= 3) {
|
|
920
936
|
const recent = signals.errorPatterns.slice(-5);
|
|
@@ -923,47 +939,83 @@ export class AgentSupervisor {
|
|
|
923
939
|
}
|
|
924
940
|
return SITUATION.RATE_LIMITED;
|
|
925
941
|
}
|
|
926
|
-
if (
|
|
942
|
+
if (
|
|
943
|
+
includesAny(errLower, ["econnrefused", "etimedout", "fetch failed"]) ||
|
|
944
|
+
includesAny(errLower, [" 500", "500 ", "500internal", "500internalservererror"]) ||
|
|
945
|
+
includesAny(errLower, [" 502", "502 ", "502badgateway", "502bad gateway"]) ||
|
|
946
|
+
errLower.includes(" 503")
|
|
947
|
+
) {
|
|
927
948
|
return SITUATION.API_ERROR;
|
|
928
949
|
}
|
|
929
|
-
if (
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
950
|
+
if (
|
|
951
|
+
(errLower.includes("context") && includesAny(errLower, ["too long", "exceeded", "overflow"])) ||
|
|
952
|
+
includesOrdered(errLower, ["max", "token"]) ||
|
|
953
|
+
includesAny(errLower, ["context_length_exceeded", "prompt_too_long", "this model's maximum context length", "turn_limit_reached", "string_above_max_length"]) ||
|
|
954
|
+
includesOrdered(errLower, ["prompt", "too", "long"]) ||
|
|
955
|
+
includesOrdered(errLower, ["token_budget", "exceeded"]) ||
|
|
956
|
+
includesOrdered(errLower, ["token", "budget"]) ||
|
|
957
|
+
includesOrdered(errLower, ["conversation", "too", "long"]) ||
|
|
958
|
+
includesOrdered(errLower, ["maximum", "number", "tokens"])
|
|
959
|
+
) {
|
|
935
960
|
return SITUATION.TOKEN_OVERFLOW;
|
|
936
961
|
}
|
|
937
|
-
if (
|
|
962
|
+
if (
|
|
963
|
+
includesOrdered(errLower, ["model", "not", "supported"]) ||
|
|
964
|
+
includesOrdered(errLower, ["model", "error"]) ||
|
|
965
|
+
includesOrdered(errLower, ["invalid", "model"]) ||
|
|
966
|
+
includesOrdered(errLower, ["model", "not", "found"]) ||
|
|
967
|
+
includesOrdered(errLower, ["model", "deprecated"])
|
|
968
|
+
) {
|
|
938
969
|
return SITUATION.MODEL_ERROR;
|
|
939
970
|
}
|
|
940
971
|
// ── Auth failures — MUST come BEFORE session_expired (which has 'unauthorized')
|
|
941
|
-
if (
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
972
|
+
if (
|
|
973
|
+
includesAny(errLower, ["invalid api key", "invalid-api-key", "invalid_api_key", "authentication_error", "permission_error", "401 unauthorized", "403 forbidden", "billing_hard_limit", "insufficient_quota"]) ||
|
|
974
|
+
includesOrdered(errLower, ["invalid", "credentials"]) ||
|
|
975
|
+
includesOrdered(errLower, ["access", "denied"]) ||
|
|
976
|
+
includesOrdered(errLower, ["not", "authorized"])
|
|
977
|
+
) {
|
|
945
978
|
return SITUATION.AUTH_FAILURE;
|
|
946
979
|
}
|
|
947
980
|
// ── Session expired (after auth check since 'unauthorized' overlaps)
|
|
948
|
-
if (
|
|
949
|
-
|
|
981
|
+
if (
|
|
982
|
+
includesOrdered(errLower, ["session", "expired"]) ||
|
|
983
|
+
includesOrdered(errLower, ["thread", "not", "found"]) ||
|
|
984
|
+
includesOrdered(errLower, ["invalid", "session"]) ||
|
|
985
|
+
includesOrdered(errLower, ["invalid", "token"])
|
|
986
|
+
) {
|
|
950
987
|
return SITUATION.SESSION_EXPIRED;
|
|
951
988
|
}
|
|
952
989
|
// ── Content policy (safety filter)
|
|
953
|
-
if (
|
|
954
|
-
|
|
990
|
+
if (
|
|
991
|
+
includesAny(errLower, ["content_policy", "content filter", "content-filter", "content_filter", "safety_system"]) ||
|
|
992
|
+
includesOrdered(errLower, ["safety", "filter"]) ||
|
|
993
|
+
includesOrdered(errLower, ["flagged", "content"]) ||
|
|
994
|
+
includesOrdered(errLower, ["output", "blocked"]) ||
|
|
995
|
+
includesOrdered(errLower, ["response", "blocked"])
|
|
996
|
+
) {
|
|
955
997
|
return SITUATION.CONTENT_POLICY;
|
|
956
998
|
}
|
|
957
999
|
// ── Codex sandbox/CLI errors
|
|
958
|
-
if (
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1000
|
+
if (
|
|
1001
|
+
includesOrdered(errLower, ["sandbox", "fail"]) ||
|
|
1002
|
+
includesOrdered(errLower, ["bwrap", "error"]) ||
|
|
1003
|
+
errLower.includes("bubblewrap") ||
|
|
1004
|
+
includesOrdered(errLower, ["eperm", "operation", "not", "permitted"]) ||
|
|
1005
|
+
errLower.includes("writable_roots") ||
|
|
1006
|
+
includesOrdered(errLower, ["namespace", "error"]) ||
|
|
1007
|
+
(errLower.includes("codex") && includesAny(errLower, ["segfault", "killed", "crash"]))
|
|
1008
|
+
) {
|
|
962
1009
|
return SITUATION.CODEX_SANDBOX;
|
|
963
1010
|
}
|
|
964
1011
|
// ── Invalid config (catch-all for config-related errors)
|
|
965
|
-
if (
|
|
966
|
-
|
|
1012
|
+
if (
|
|
1013
|
+
includesOrdered(errLower, ["config", "invalid"]) ||
|
|
1014
|
+
includesOrdered(errLower, ["config", "missing"]) ||
|
|
1015
|
+
errLower.includes("misconfigured") ||
|
|
1016
|
+
includesOrdered(errLower, ["openai_api_key", "not", "set"]) ||
|
|
1017
|
+
includesOrdered(errLower, ["anthropic_api_key", "not", "set"])
|
|
1018
|
+
) {
|
|
967
1019
|
return SITUATION.INVALID_CONFIG;
|
|
968
1020
|
}
|
|
969
1021
|
}
|
|
@@ -1009,11 +1061,11 @@ export class AgentSupervisor {
|
|
|
1009
1061
|
// ── Build/test/lint failures from context ──
|
|
1010
1062
|
if (context.error) {
|
|
1011
1063
|
const err = context.error.toLowerCase();
|
|
1012
|
-
if (
|
|
1013
|
-
if (
|
|
1014
|
-
if (
|
|
1015
|
-
if (
|
|
1016
|
-
if (
|
|
1064
|
+
if (includesOrdered(err, ["pre-push hook", "fail"])) return SITUATION.PRE_PUSH_FAILURE;
|
|
1065
|
+
if (includesOrdered(err, ["git push", "fail"]) || includesOrdered(err, ["rejected", "push"])) return SITUATION.PUSH_FAILURE;
|
|
1066
|
+
if (includesOrdered(err, ["go build", "fail"]) || err.includes("compilation error") || err.includes("cannot find")) return SITUATION.BUILD_FAILURE;
|
|
1067
|
+
if ((err.includes("fail") && err.includes("test")) || err.includes("fail ")) return SITUATION.TEST_FAILURE;
|
|
1068
|
+
if (err.includes("golangci-lint") || includesOrdered(err, ["lint", "error"])) return SITUATION.LINT_FAILURE;
|
|
1017
1069
|
if (/merge conflict|CONFLICT/i.test(err)) return SITUATION.GIT_CONFLICT;
|
|
1018
1070
|
}
|
|
1019
1071
|
|
|
@@ -1234,3 +1286,4 @@ export class AgentSupervisor {
|
|
|
1234
1286
|
export function createAgentSupervisor(opts) {
|
|
1235
1287
|
return new AgentSupervisor(opts);
|
|
1236
1288
|
}
|
|
1289
|
+
|
package/agent/autofix.mjs
CHANGED
|
@@ -93,7 +93,6 @@ export function extractErrors(logText) {
|
|
|
93
93
|
const atLineHeader = /^At\s+([A-Za-z]:\\[^\n:]+\.ps1):(\d+)\s+char:(\d+)/;
|
|
94
94
|
|
|
95
95
|
// Pattern C: TerminatingError(X): "message"
|
|
96
|
-
const terminatingPattern = /TerminatingError\(([^)]+)\):\s*"(.+?)"/;
|
|
97
96
|
|
|
98
97
|
for (let i = 0; i < lines.length; i++) {
|
|
99
98
|
const line = lines[i];
|
|
@@ -147,16 +146,24 @@ export function extractErrors(logText) {
|
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
// ── Check Pattern C (TerminatingError) ──────────────────────────
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
149
|
+
const marker = "TerminatingError(";
|
|
150
|
+
const markerPos = line.indexOf(marker);
|
|
151
|
+
if (markerPos !== -1) {
|
|
152
|
+
const typeStart = markerPos + marker.length;
|
|
153
|
+
const typeEnd = line.indexOf(")", typeStart);
|
|
154
|
+
const colonPos = typeEnd >= 0 ? line.indexOf(":", typeEnd + 1) : -1;
|
|
155
|
+
const errorType = typeEnd >= 0 ? line.slice(typeStart, typeEnd).trim() : "";
|
|
156
|
+
const detail = colonPos >= 0 ? line.slice(colonPos + 1).trim().replace(/^"|"$/g, "") : "";
|
|
157
|
+
if (errorType && detail) {
|
|
158
|
+
addError({
|
|
159
|
+
errorType: "TerminatingError",
|
|
160
|
+
file: "unknown",
|
|
161
|
+
line: 0,
|
|
162
|
+
column: null,
|
|
163
|
+
message: `${errorType}: ${detail}`,
|
|
164
|
+
signature: `TerminatingError:${errorType}`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
160
167
|
}
|
|
161
168
|
}
|
|
162
169
|
|
|
@@ -185,18 +192,22 @@ function parseLineBlock(lines, startIdx) {
|
|
|
185
192
|
}
|
|
186
193
|
|
|
187
194
|
// Capture code line: " NNN | code..."
|
|
188
|
-
if (i < lines.length
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
195
|
+
if (i < lines.length) {
|
|
196
|
+
const raw = String(lines[i] || "");
|
|
197
|
+
const sep = raw.indexOf("|");
|
|
198
|
+
if (sep !== -1 && /^\s*\d+\s*$/.test(raw.slice(0, sep))) {
|
|
199
|
+
codeLine = raw.slice(sep + 1).trim();
|
|
200
|
+
i++;
|
|
201
|
+
}
|
|
192
202
|
}
|
|
193
203
|
|
|
194
204
|
// Skip underline and intermediate "| ..." lines, capture last "| message" line
|
|
195
205
|
let lastPipeMessage = "";
|
|
196
206
|
while (i < lines.length) {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
207
|
+
const raw = String(lines[i] || "");
|
|
208
|
+
const sep = raw.indexOf("|");
|
|
209
|
+
if (sep === -1 || raw.slice(0, sep).trim().length > 0) break;
|
|
210
|
+
const content = raw.slice(sep + 1).trim();
|
|
200
211
|
// Skip underline-only lines (~~~~) and empty lines
|
|
201
212
|
if (content && !/^~+$/.test(content)) {
|
|
202
213
|
lastPipeMessage = content;
|
|
@@ -225,29 +236,32 @@ function parsePlusBlock(lines, startIdx) {
|
|
|
225
236
|
let i = startIdx;
|
|
226
237
|
|
|
227
238
|
// First "+ " line is usually the code
|
|
228
|
-
if (i < lines.length
|
|
229
|
-
const
|
|
230
|
-
if (
|
|
231
|
-
const content =
|
|
239
|
+
if (i < lines.length) {
|
|
240
|
+
const raw = String(lines[i] || "").trimStart();
|
|
241
|
+
if (raw.startsWith("+")) {
|
|
242
|
+
const content = raw.slice(1).trim();
|
|
232
243
|
if (!/^~+$/.test(content)) codeLine = content;
|
|
244
|
+
i++;
|
|
233
245
|
}
|
|
234
|
-
i++;
|
|
235
246
|
}
|
|
236
247
|
|
|
237
248
|
// Subsequent "+ " lines — skip underlines, capture error type + message
|
|
238
|
-
while (i < lines.length
|
|
239
|
-
const
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
249
|
+
while (i < lines.length) {
|
|
250
|
+
const raw = String(lines[i] || "").trimStart();
|
|
251
|
+
if (!raw.startsWith("+")) break;
|
|
252
|
+
const content = raw.slice(1).trim();
|
|
253
|
+
if (/^~+$/.test(content)) {
|
|
254
|
+
i++;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
// Check for "ErrorType: message"
|
|
258
|
+
const colonPos = content.indexOf(":");
|
|
259
|
+
if (colonPos > 0 && colonPos < content.length - 1) {
|
|
260
|
+
const maybeType = content.slice(0, colonPos).trim();
|
|
261
|
+
const maybeMessage = content.slice(colonPos + 1).trim();
|
|
262
|
+
if (/^\w[\w.-]+$/.test(maybeType) && maybeMessage) {
|
|
263
|
+
errorType = maybeType;
|
|
264
|
+
message = maybeMessage;
|
|
251
265
|
}
|
|
252
266
|
}
|
|
253
267
|
i++;
|
|
@@ -160,7 +160,7 @@ export function normalizeGitUrl(url) {
|
|
|
160
160
|
s = s.replace(/\.git$/, "");
|
|
161
161
|
|
|
162
162
|
// Strip trailing slashes
|
|
163
|
-
s = s.
|
|
163
|
+
while (s.endsWith("/")) s = s.slice(0, -1);
|
|
164
164
|
|
|
165
165
|
return s.toLowerCase();
|
|
166
166
|
}
|
|
@@ -857,3 +857,4 @@ export function formatFleetSummary() {
|
|
|
857
857
|
|
|
858
858
|
return lines.join("\n");
|
|
859
859
|
}
|
|
860
|
+
|
package/agent/hook-profiles.mjs
CHANGED
|
@@ -187,7 +187,7 @@ function normalizeOverrideCommands(rawValue) {
|
|
|
187
187
|
return [];
|
|
188
188
|
}
|
|
189
189
|
const commands = raw
|
|
190
|
-
.split(/\
|
|
190
|
+
.split(/\r?\n/).flatMap((part) => part.split(";;"))
|
|
191
191
|
.map((part) => part.trim())
|
|
192
192
|
.filter(Boolean);
|
|
193
193
|
return commands;
|
package/agent/review-agent.mjs
CHANGED
|
@@ -122,24 +122,36 @@ function extractRepoSlug(prUrl) {
|
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
124
|
* Get the PR diff using `gh pr diff` or `git diff`.
|
|
125
|
-
* @param {{ prUrl?: string, branchName?: string }} opts
|
|
125
|
+
* @param {{ prUrl?: string, prNumber?: number|string, repoSlug?: string, branchName?: string, cwd?: string|null }} opts
|
|
126
126
|
* @returns {{ diff: string, truncated: boolean }}
|
|
127
127
|
*/
|
|
128
|
-
function getPrDiff({ prUrl, branchName }) {
|
|
128
|
+
function getPrDiff({ prUrl, prNumber, repoSlug, branchName, cwd }) {
|
|
129
129
|
let diff = "";
|
|
130
|
+
const commandOptions = {
|
|
131
|
+
encoding: "utf8",
|
|
132
|
+
timeout: 30_000,
|
|
133
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
134
|
+
...(cwd ? { cwd } : {}),
|
|
135
|
+
};
|
|
130
136
|
|
|
131
137
|
// Strategy 1: gh pr diff
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
const resolvedPrNumber = Number.isFinite(Number(prNumber))
|
|
139
|
+
? Number(prNumber)
|
|
140
|
+
: extractPrNumber(prUrl);
|
|
141
|
+
const resolvedRepoSlug = String(repoSlug || "").trim() || extractRepoSlug(prUrl);
|
|
142
|
+
if (resolvedPrNumber) {
|
|
135
143
|
try {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
["pr", "diff", String(
|
|
139
|
-
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
const attempts = [];
|
|
145
|
+
if (resolvedRepoSlug) {
|
|
146
|
+
attempts.push(["pr", "diff", String(resolvedPrNumber), "--repo", resolvedRepoSlug]);
|
|
147
|
+
}
|
|
148
|
+
attempts.push(["pr", "diff", String(resolvedPrNumber)]);
|
|
149
|
+
for (const args of attempts) {
|
|
150
|
+
const result = spawnSync("gh", args, commandOptions);
|
|
151
|
+
if (result.status === 0 && result.stdout?.trim()) {
|
|
152
|
+
diff = result.stdout;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
143
155
|
}
|
|
144
156
|
} catch {
|
|
145
157
|
/* fall through to git diff */
|
|
@@ -149,13 +161,16 @@ function getPrDiff({ prUrl, branchName }) {
|
|
|
149
161
|
// Strategy 2: git diff main...<branch>
|
|
150
162
|
if (!diff && branchName) {
|
|
151
163
|
try {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
164
|
+
const attempts = [
|
|
165
|
+
["diff", `origin/main...${branchName}`],
|
|
166
|
+
["diff", `main...${branchName}`],
|
|
167
|
+
];
|
|
168
|
+
for (const args of attempts) {
|
|
169
|
+
const result = spawnSync("git", args, commandOptions);
|
|
170
|
+
if (result.status === 0 && result.stdout?.trim()) {
|
|
171
|
+
diff = result.stdout;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
159
174
|
}
|
|
160
175
|
} catch {
|
|
161
176
|
/* ignore */
|
|
@@ -343,6 +358,17 @@ export class ReviewAgent {
|
|
|
343
358
|
console.warn(`${TAG} queueReview called without task id — skipping`);
|
|
344
359
|
return;
|
|
345
360
|
}
|
|
361
|
+
const hasReviewReference = Boolean(
|
|
362
|
+
String(task.prUrl || "").trim() ||
|
|
363
|
+
String(task.prNumber || "").trim() ||
|
|
364
|
+
String(task.branchName || "").trim(),
|
|
365
|
+
);
|
|
366
|
+
if (!hasReviewReference) {
|
|
367
|
+
console.warn(
|
|
368
|
+
`${TAG} queueReview skipped for ${task.id}: no prUrl, prNumber, or branchName`,
|
|
369
|
+
);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
346
372
|
|
|
347
373
|
if (this.#seen.has(task.id)) {
|
|
348
374
|
console.log(`${TAG} task ${task.id} already queued/in-flight — skipping`);
|
|
@@ -463,7 +489,10 @@ export class ReviewAgent {
|
|
|
463
489
|
// 1. Get PR diff
|
|
464
490
|
const { diff, truncated } = getPrDiff({
|
|
465
491
|
prUrl: task.prUrl,
|
|
492
|
+
prNumber: task.prNumber,
|
|
493
|
+
repoSlug: task.repoSlug,
|
|
466
494
|
branchName: task.branchName,
|
|
495
|
+
cwd: task.worktreePath || null,
|
|
467
496
|
});
|
|
468
497
|
|
|
469
498
|
if (!diff) {
|
package/bosun.schema.json
CHANGED
|
@@ -1199,6 +1199,56 @@
|
|
|
1199
1199
|
"default": 1,
|
|
1200
1200
|
"description": "Turns during which the full user prompt is preserved."
|
|
1201
1201
|
},
|
|
1202
|
+
"liveToolCompactionEnabled": {
|
|
1203
|
+
"type": "boolean",
|
|
1204
|
+
"default": false,
|
|
1205
|
+
"description": "Enable command-aware live compaction of large tool outputs before they are stored in the active turn."
|
|
1206
|
+
},
|
|
1207
|
+
"liveToolCompactionMode": {
|
|
1208
|
+
"type": "string",
|
|
1209
|
+
"enum": ["off", "auto", "aggressive"],
|
|
1210
|
+
"default": "auto",
|
|
1211
|
+
"description": "off disables live compaction, auto compacts only when pressure or signal justify it, and aggressive favors stronger reduction for noisy command families."
|
|
1212
|
+
},
|
|
1213
|
+
"liveToolCompactionMinChars": {
|
|
1214
|
+
"type": "integer",
|
|
1215
|
+
"minimum": 500,
|
|
1216
|
+
"maximum": 500000,
|
|
1217
|
+
"default": 4000,
|
|
1218
|
+
"description": "Minimum output size before live compaction is considered."
|
|
1219
|
+
},
|
|
1220
|
+
"liveToolCompactionTargetChars": {
|
|
1221
|
+
"type": "integer",
|
|
1222
|
+
"minimum": 200,
|
|
1223
|
+
"maximum": 50000,
|
|
1224
|
+
"default": 1800,
|
|
1225
|
+
"description": "Target upper bound for compacted live command output before retrieval hints and metadata."
|
|
1226
|
+
},
|
|
1227
|
+
"liveToolCompactionMinSavingsPct": {
|
|
1228
|
+
"type": "integer",
|
|
1229
|
+
"minimum": 0,
|
|
1230
|
+
"maximum": 95,
|
|
1231
|
+
"default": 15,
|
|
1232
|
+
"description": "Discard compacted output if it does not save at least this much space."
|
|
1233
|
+
},
|
|
1234
|
+
"liveToolCompactionMinRuntimeMs": {
|
|
1235
|
+
"type": "integer",
|
|
1236
|
+
"minimum": 0,
|
|
1237
|
+
"maximum": 3600000,
|
|
1238
|
+
"default": 2000,
|
|
1239
|
+
"description": "When command duration metadata is present, require at least this runtime before compacting in auto mode."
|
|
1240
|
+
},
|
|
1241
|
+
"liveToolCompactionBlockStructured": {
|
|
1242
|
+
"type": "boolean",
|
|
1243
|
+
"default": true,
|
|
1244
|
+
"description": "Avoid live compaction for likely JSON or other structured outputs where exact bytes matter more than size."
|
|
1245
|
+
},
|
|
1246
|
+
"liveToolCompactionAllowCommands": {
|
|
1247
|
+
"type": "array",
|
|
1248
|
+
"items": { "type": "string" },
|
|
1249
|
+
"default": ["grep", "rg", "find", "git", "go", "npm", "pnpm", "yarn", "node", "python", "python3", "pytest", "docker", "kubectl", "cargo", "gradle", "maven", "mvn", "javac", "tsc", "jest", "vitest", "deno"],
|
|
1250
|
+
"description": "Command or tool families eligible for live compaction."
|
|
1251
|
+
},
|
|
1202
1252
|
"perType": {
|
|
1203
1253
|
"type": "object",
|
|
1204
1254
|
"description": "Per-interaction-type config overrides. Keys: task, manual, primary, chat, voice, flow.",
|
|
@@ -1237,3 +1287,4 @@
|
|
|
1237
1287
|
}
|
|
1238
1288
|
}
|
|
1239
1289
|
}
|
|
1290
|
+
|