bosun 0.41.9 → 0.41.10

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 CHANGED
@@ -201,7 +201,7 @@ VOICE_DELEGATE_EXECUTOR=codex-sdk
201
201
  # earlier: it summarizes large, noisy command outputs before they ever land in
202
202
  # the active turn, while preserving a `bosun --tool-log <id>` retrieval path.
203
203
  # CONTEXT_SHREDDING_ENABLED=true
204
- # CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_ENABLED=false
204
+ # CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_ENABLED=true
205
205
  # CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_MODE=auto
206
206
  # CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_MIN_CHARS=4000
207
207
  # CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_TARGET_CHARS=1800
@@ -55,8 +55,7 @@ import {
55
55
  MAX_STREAM_RETRIES,
56
56
  } from "../infra/stream-resilience.mjs";
57
57
  import { ensureTestRuntimeSandbox } from "../infra/test-runtime.mjs";
58
- import { compressAllItems, estimateSavings, estimateContextUsagePct, recordShreddingEvent } from "../workspace/context-cache.mjs";
59
- import { resolveContextShreddingOptions } from "../config/context-shredding-config.mjs";
58
+ import { maybeCompressSessionItems } from "../workspace/context-cache.mjs";
60
59
 
61
60
  // Lazy-load MCP registry to avoid circular dependencies.
62
61
  // Cached at module scope per AGENTS.md hard rules.
@@ -110,7 +109,15 @@ function hasOptionalModule(specifier) {
110
109
  require.resolve(specifier);
111
110
  ok = true;
112
111
  } catch {
113
- ok = false;
112
+ // ESM-only packages have no CJS "require" export so require.resolve
113
+ // throws even when the package is installed. Fall back to checking
114
+ // whether the package directory exists on disk.
115
+ try {
116
+ const pkgDir = resolve(__dirname, "..", "node_modules", ...specifier.split("/"));
117
+ ok = existsSync(resolve(pkgDir, "package.json"));
118
+ } catch {
119
+ ok = false;
120
+ }
114
121
  }
115
122
  MODULE_PRESENCE_CACHE.set(specifier, ok);
116
123
  return ok;
@@ -244,29 +251,12 @@ async function maybeCompressResultItems(
244
251
 
245
252
  const resolvedSessionType = normalizeSessionType(sessionType, "task");
246
253
  const agentType = normalizeSdkForShredding(sdk);
247
- const shreddingOpts = resolveContextShreddingOptions(
248
- resolvedSessionType,
254
+ return maybeCompressSessionItems(items, {
255
+ sessionType: resolvedSessionType,
249
256
  agentType,
250
- );
251
- if (shreddingOpts?._skip === true) return items;
252
-
253
- const usagePct = estimateContextUsagePct(items);
254
- const threshold = Number.isFinite(shreddingOpts?.contextUsageThreshold)
255
- ? Number(shreddingOpts.contextUsageThreshold)
256
- : 0.5;
257
- if (usagePct < threshold) return items;
258
-
259
- shreddingOpts.contextUsagePct = usagePct;
260
- const compressedItems = await compressAllItems(items, shreddingOpts);
261
- try {
262
- const savings = estimateSavings(items, compressedItems);
263
- if (savings.savedChars > 0) {
264
- recordShreddingEvent({ ...savings, agentType: agentType || sdk });
265
- }
266
- } catch {
267
- /* non-fatal */
268
- }
269
- return compressedItems;
257
+ force: forceCompression,
258
+ skip: skipCompression,
259
+ });
270
260
  }
271
261
 
272
262
  function resolveCodexStreamSafety(totalTimeoutMs) {
@@ -762,13 +752,14 @@ async function withTemporaryEnv(overrides, fn) {
762
752
  * Otherwise strips OPENAI_BASE_URL so the SDK uses its default auth.
763
753
  */
764
754
  function buildCodexSdkOptions(envInput = process.env) {
765
- const { env: resolvedEnv } = resolveCodexProfileRuntime(envInput);
755
+ const resolved = resolveCodexProfileRuntime(envInput);
756
+ const { env: resolvedEnv, configProvider } = resolved;
766
757
  const baseUrl = resolvedEnv.OPENAI_BASE_URL || "";
767
758
  const isAzure = (() => {
768
759
  try {
769
760
  const parsed = new URL(baseUrl);
770
761
  const host = String(parsed.hostname || "").toLowerCase();
771
- return host === "openai.azure.com" || host.endsWith(".openai.azure.com");
762
+ return host === "openai.azure.com" || host.endsWith(".openai.azure.com") || host.endsWith(".cognitiveservices.azure.com");
772
763
  } catch {
773
764
  return false;
774
765
  }
@@ -783,16 +774,20 @@ function buildCodexSdkOptions(envInput = process.env) {
783
774
  if (env.OPENAI_API_KEY && !env.AZURE_OPENAI_API_KEY) {
784
775
  env.AZURE_OPENAI_API_KEY = env.OPENAI_API_KEY;
785
776
  }
777
+ // Use the config.toml provider section name and env_key when available,
778
+ // so the SDK config override is consistent with the user's config.toml.
779
+ const providerSectionName = configProvider?.name || "azure";
780
+ const providerEnvKey = configProvider?.envKey || "AZURE_OPENAI_API_KEY";
786
781
  const azureModel = env.CODEX_MODEL || undefined;
787
782
  return {
788
783
  env,
789
784
  config: {
790
- model_provider: "azure",
785
+ model_provider: providerSectionName,
791
786
  model_providers: {
792
- azure: {
787
+ [providerSectionName]: {
793
788
  name: "Azure OpenAI",
794
789
  base_url: baseUrl,
795
- env_key: "AZURE_OPENAI_API_KEY",
790
+ env_key: providerEnvKey,
796
791
  wire_api: "responses",
797
792
  },
798
793
  },
@@ -10,8 +10,12 @@ function normalizeTemplateValue(value) {
10
10
  if (value == null) return "";
11
11
  if (typeof value === "string") {
12
12
  const text = value.trim();
13
- if (/^\{\{\s*[\w.-]+\s*\}\}$/.test(text)) return "";
14
- return value;
13
+ if (!text) return "";
14
+ const sanitized = text
15
+ .replace(/\{\{\s*[\w.-]+\s*\}\}/g, " ")
16
+ .replace(/[ \t]{2,}/g, " ")
17
+ .trim();
18
+ return sanitized;
15
19
  }
16
20
  if (typeof value === "number" || typeof value === "boolean") {
17
21
  return String(value);
package/agent/autofix.mjs CHANGED
@@ -545,7 +545,7 @@ export function runCodexExec(
545
545
  try {
546
546
  const parsed = new URL(baseUrl);
547
547
  const host = String(parsed.hostname || "").toLowerCase();
548
- return host === "openai.azure.com" || host.endsWith(".openai.azure.com");
548
+ return host === "openai.azure.com" || host.endsWith(".openai.azure.com") || host.endsWith(".cognitiveservices.azure.com");
549
549
  } catch {
550
550
  return false;
551
551
  }
package/config/config.mjs CHANGED
@@ -711,9 +711,15 @@ function detectRepoSlug(repoRoot = "") {
711
711
  const direct = tryResolve(process.cwd());
712
712
  if (direct) return direct;
713
713
 
714
- // Fall back to detected repo root if provided (or detectable)
715
- const root = repoRoot || detectRepoRoot();
716
- if (root) {
714
+ // Fall back to detected repo root if provided
715
+ if (repoRoot) {
716
+ const viaRoot = tryResolve(repoRoot);
717
+ if (viaRoot) return viaRoot;
718
+ }
719
+
720
+ // Last resort — use cached detectRepoRoot (avoids redundant subprocess calls)
721
+ const root = detectRepoRoot();
722
+ if (root && root !== process.cwd()) {
717
723
  const viaRoot = tryResolve(root);
718
724
  if (viaRoot) return viaRoot;
719
725
  }
@@ -721,7 +727,16 @@ function detectRepoSlug(repoRoot = "") {
721
727
  return null;
722
728
  }
723
729
 
730
+ let _detectRepoRootCache = null;
731
+
724
732
  function detectRepoRoot() {
733
+ if (_detectRepoRootCache) return _detectRepoRootCache;
734
+ const result = _detectRepoRootUncached();
735
+ _detectRepoRootCache = result;
736
+ return result;
737
+ }
738
+
739
+ function _detectRepoRootUncached() {
725
740
  const gitExecOptions = {
726
741
  encoding: "utf8",
727
742
  stdio: ["pipe", "pipe", "ignore"],
@@ -734,7 +749,30 @@ function detectRepoRoot() {
734
749
  if (existsSync(envRoot)) return envRoot;
735
750
  }
736
751
 
737
- // 2. Try git from cwd
752
+ // 2. Check bosun config for workspace repos FIRST — this is the primary
753
+ // source of truth and works regardless of whether cwd is inside a git repo.
754
+ const configDirs = getConfigSearchDirs();
755
+ let fallbackRepo = null;
756
+ for (const cfgName of CONFIG_FILES) {
757
+ for (const configDir of configDirs) {
758
+ const cfgPath = resolve(configDir, cfgName);
759
+ if (!existsSync(cfgPath)) continue;
760
+ try {
761
+ const cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
762
+ const repoPaths = collectRepoPathsFromConfig(cfg, configDir);
763
+ for (const repoPath of repoPaths) {
764
+ if (!repoPath || !existsSync(repoPath)) continue;
765
+ if (existsSync(resolve(repoPath, ".git"))) return repoPath;
766
+ fallbackRepo ??= repoPath;
767
+ }
768
+ } catch {
769
+ /* invalid config */
770
+ }
771
+ }
772
+ }
773
+ if (fallbackRepo) return fallbackRepo;
774
+
775
+ // 3. Try git from cwd
738
776
  try {
739
777
  const gitRoot = execSync("git rev-parse --show-toplevel", {
740
778
  ...gitExecOptions,
@@ -744,7 +782,7 @@ function detectRepoRoot() {
744
782
  // not in a git repo from cwd
745
783
  }
746
784
 
747
- // 3. Bosun package directory may be inside a repo (common: scripts/bosun/ within a project)
785
+ // 4. Bosun package directory may be inside a repo (common: scripts/bosun/ within a project)
748
786
  try {
749
787
  const gitRoot = execSync("git rev-parse --show-toplevel", {
750
788
  cwd: __dirname,
@@ -755,7 +793,7 @@ function detectRepoRoot() {
755
793
  // bosun installed standalone, not in a repo
756
794
  }
757
795
 
758
- // 4. Module root detection — when bosun is installed as a standalone npm package,
796
+ // 5. Module root detection — when bosun is installed as a standalone npm package,
759
797
  // use the module root directory as a stable base for config resolution.
760
798
  const moduleRoot = detectBosunModuleRoot();
761
799
  if (moduleRoot && moduleRoot !== process.cwd()) {
@@ -770,33 +808,9 @@ function detectRepoRoot() {
770
808
  }
771
809
  }
772
810
 
773
- // 5. Check bosun config for workspace repos
774
- const configDirs = getConfigSearchDirs();
775
- let fallbackRepo = null;
776
- for (const cfgName of CONFIG_FILES) {
777
- for (const configDir of configDirs) {
778
- const cfgPath = resolve(configDir, cfgName);
779
- if (!existsSync(cfgPath)) continue;
780
- try {
781
- const cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
782
- const repoPaths = collectRepoPathsFromConfig(cfg, configDir);
783
- for (const repoPath of repoPaths) {
784
- if (!repoPath || !existsSync(repoPath)) continue;
785
- if (existsSync(resolve(repoPath, ".git"))) return repoPath;
786
- fallbackRepo ??= repoPath;
787
- }
788
- } catch {
789
- /* invalid config */
790
- }
791
- }
792
- }
793
- if (fallbackRepo) return fallbackRepo;
794
-
795
- // 6. Final fallback — warn and return cwd.
796
- // git repo (e.g. when the daemon spawns with cwd=homedir), but returning
797
- // null would crash downstream callers like resolve(repoRoot). The warning
798
- // helps diagnose "not a git repository" errors from child processes.
799
- console.warn("[config] detectRepoRoot: no git repository found — falling back to cwd:", process.cwd());
811
+ // 6. Final fallback — return cwd silently. The config system resolves
812
+ // repos from BOSUN_HOME workspaces, so a missing git repo in cwd is
813
+ // expected for globally-installed usage and daemon spawns.
800
814
  return process.cwd();
801
815
  }
802
816
 
@@ -147,7 +147,7 @@ export const DEFAULT_SHREDDING_CONFIG = Object.freeze({
147
147
  userMsgFullTurns: 1, // only the current turn keeps the full user prompt
148
148
 
149
149
  // ── Live command/tool compaction ─────────────────────────────
150
- liveToolCompactionEnabled: false,
150
+ liveToolCompactionEnabled: true,
151
151
  liveToolCompactionMode: "auto",
152
152
  liveToolCompactionMinChars: 4000,
153
153
  liveToolCompactionTargetChars: 1800,
@@ -98,40 +98,9 @@ export function resolveRepoRoot(options = {}) {
98
98
 
99
99
  const cwd = options.cwd || process.cwd();
100
100
 
101
- // Try git from cwd
102
- try {
103
- const gitRoot = execSync("git rev-parse --show-toplevel", {
104
- encoding: "utf8",
105
- cwd,
106
- stdio: ["ignore", "pipe", "ignore"],
107
- }).trim();
108
- if (gitRoot) return gitRoot;
109
- } catch {
110
- // ignore - fall back
111
- }
112
-
113
- // Try git from the bosun package directory (may be inside a repo)
114
- try {
115
- const gitRoot = execSync("git rev-parse --show-toplevel", {
116
- encoding: "utf8",
117
- cwd: __dirname,
118
- stdio: ["ignore", "pipe", "ignore"],
119
- }).trim();
120
- if (gitRoot) return gitRoot;
121
- } catch {
122
- // bosun installed standalone
123
- }
124
-
125
- // Check if __dirname itself is the bosun module root (installed as npm package)
126
- const moduleRoot = detectBosunModuleRoot();
127
- if (moduleRoot && moduleRoot !== cwd && existsSync(resolve(moduleRoot, "package.json"))) {
128
- // When bosun is installed globally/locally outside a git repo, use its directory
129
- // as a reference point if no git root was found above.
130
- // Only use it if it's a valid directory on disk (don't return the fallback as a repo root).
131
- }
132
-
133
- // Check bosun config for workspace repos
101
+ // Check bosun config for workspace repos FIRST — works regardless of cwd
134
102
  const configDirs = [...getConfigSearchDirs(), __dirname];
103
+ if (process.env.BOSUN_HOME) configDirs.unshift(resolve(process.env.BOSUN_HOME));
135
104
  let fallbackRepo = null;
136
105
  for (const cfgName of CONFIG_FILES) {
137
106
  for (const dir of configDirs) {
@@ -139,6 +108,21 @@ export function resolveRepoRoot(options = {}) {
139
108
  if (!existsSync(cfgPath)) continue;
140
109
  try {
141
110
  const cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
111
+ // Check workspace repos (higher priority — explicit workspace configuration)
112
+ const workspaces = Array.isArray(cfg.workspaces) ? cfg.workspaces : [];
113
+ for (const ws of workspaces) {
114
+ const wsRepos = ws?.repos || ws?.repositories || [];
115
+ if (!Array.isArray(wsRepos)) continue;
116
+ const wsId = ws?.id || "default";
117
+ for (const repo of wsRepos) {
118
+ const name = typeof repo === "string" ? repo.split("/").pop() : (repo?.name || repo?.id || "");
119
+ if (!name) continue;
120
+ const repoPath = resolve(dir, "workspaces", wsId, name);
121
+ if (existsSync(resolve(repoPath, ".git"))) return repoPath;
122
+ if (existsSync(repoPath)) fallbackRepo ??= repoPath;
123
+ }
124
+ }
125
+ // Check flat repositories
142
126
  const repos = cfg.repositories || cfg.repos || [];
143
127
  if (Array.isArray(repos) && repos.length > 0) {
144
128
  const primary = repos.find((r) => r.primary) || repos[0];
@@ -155,6 +139,30 @@ export function resolveRepoRoot(options = {}) {
155
139
  }
156
140
  if (fallbackRepo) return fallbackRepo;
157
141
 
142
+ // Try git from cwd
143
+ try {
144
+ const gitRoot = execSync("git rev-parse --show-toplevel", {
145
+ encoding: "utf8",
146
+ cwd,
147
+ stdio: ["ignore", "pipe", "ignore"],
148
+ }).trim();
149
+ if (gitRoot) return gitRoot;
150
+ } catch {
151
+ // ignore - fall back
152
+ }
153
+
154
+ // Try git from the bosun package directory (may be inside a repo)
155
+ try {
156
+ const gitRoot = execSync("git rev-parse --show-toplevel", {
157
+ encoding: "utf8",
158
+ cwd: __dirname,
159
+ stdio: ["ignore", "pipe", "ignore"],
160
+ }).trim();
161
+ if (gitRoot) return gitRoot;
162
+ } catch {
163
+ // bosun installed standalone
164
+ }
165
+
158
166
  return resolve(cwd);
159
167
  }
160
168
 
@@ -827,7 +827,7 @@ function launchCodexExec(prompt, cwd, timeoutMs) {
827
827
  try {
828
828
  const parsed = new URL(baseUrl);
829
829
  const host = String(parsed.hostname || "").toLowerCase();
830
- return host === "openai.azure.com" || host.endsWith(".openai.azure.com");
830
+ return host === "openai.azure.com" || host.endsWith(".openai.azure.com") || host.endsWith(".cognitiveservices.azure.com");
831
831
  } catch {
832
832
  return false;
833
833
  }
package/infra/monitor.mjs CHANGED
@@ -2794,7 +2794,7 @@ function buildCodexSdkOptionsForMonitor() {
2794
2794
  try {
2795
2795
  const parsed = new URL(baseUrl);
2796
2796
  const host = String(parsed.hostname || "").toLowerCase();
2797
- return host === "openai.azure.com" || host.endsWith(".openai.azure.com");
2797
+ return host === "openai.azure.com" || host.endsWith(".openai.azure.com") || host.endsWith(".cognitiveservices.azure.com");
2798
2798
  } catch {
2799
2799
  return false;
2800
2800
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.41.9",
3
+ "version": "0.41.10",
4
4
  "description": "Bosun Autonomous Engineering — manages AI agent executors with failover, extremely powerful workflow builder, and a massive amount of included default workflow templates for autonomous engineering, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -190,6 +190,7 @@
190
190
  "config/workspace-health.mjs",
191
191
  "git/conflict-resolver.mjs",
192
192
  "workspace/context-cache.mjs",
193
+ "workspace/command-diagnostics.mjs",
193
194
  "workspace/context-indexer.mjs",
194
195
  "config/context-shredding-config.mjs",
195
196
  "shell/copilot-shell.mjs",
@@ -330,6 +331,7 @@
330
331
  "workflow/pipeline.mjs",
331
332
  "workflow-templates/_helpers.mjs",
332
333
  "workflow-templates/agents.mjs",
334
+ "workflow-templates/sub-workflows.mjs",
333
335
  "workflow-templates/ci-cd.mjs",
334
336
  "workflow-templates/coverage.mjs",
335
337
  "workflow-templates/github.mjs",
@@ -43,7 +43,7 @@ function isAzureOpenAIHost(value) {
43
43
  try {
44
44
  const parsed = value instanceof URL ? value : new URL(String(value || "").trim());
45
45
  const host = String(parsed.hostname || "").toLowerCase();
46
- return host === "openai.azure.com" || host.endsWith(".openai.azure.com");
46
+ return host === "openai.azure.com" || host.endsWith(".openai.azure.com") || host.endsWith(".cognitiveservices.azure.com");
47
47
  } catch {
48
48
  return false;
49
49
  }