bosun 0.31.6 → 0.31.7

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
@@ -244,8 +244,8 @@ TELEGRAM_MINIAPP_ENABLED=false
244
244
 
245
245
  # ─── Executor Configuration ──────────────────────────────────────────────────
246
246
  # Define AI executors that work on tasks.
247
- # Format: EXECUTOR_TYPE:VARIANT:WEIGHT,EXECUTOR_TYPE:VARIANT:WEIGHT
248
- # Example: COPILOT:CLAUDE_OPUS_4_6:50,CODEX:DEFAULT:50
247
+ # Format: EXECUTOR_TYPE:VARIANT:WEIGHT[:MODEL|MODEL],EXECUTOR_TYPE:VARIANT:WEIGHT[:MODEL|MODEL]
248
+ # Example: COPILOT:CLAUDE_OPUS_4_6:50:claude-opus-4.6,CODEX:DEFAULT:50:gpt-5.2-codex|gpt-5.1-codex-mini
249
249
  # For full config, use bosun.config.json instead.
250
250
  # EXECUTORS=CODEX:DEFAULT:100
251
251
 
@@ -897,6 +897,12 @@ COPILOT_CLOUD_DISABLED=true
897
897
  # Task planner status stream interval (milliseconds). Default: 1800000 (30 min)
898
898
  # DEVMODE_TASK_PLANNER_STATUS_INTERVAL_MS=1800000
899
899
 
900
+ # ─── Trigger-Based Task System ───────────────────────────────────────────────
901
+ # Enable configurable trigger templates (disabled by default for safety).
902
+ # Built-in templates ship disabled: task-planner, daily-review-digest,
903
+ # stale-task-followup. Configure/enable in bosun.config.json under triggerSystem.
904
+ # TASK_TRIGGER_SYSTEM_ENABLED=false
905
+
900
906
  # ─── GitHub Issue Reconciler ─────────────────────────────────────────────────
901
907
  # Periodically reconciles open GitHub issues against open/merged PRs.
902
908
  # Hybrid close policy:
package/bosun.schema.json CHANGED
@@ -88,7 +88,10 @@
88
88
  "description": "Map internal statuses to project board column names",
89
89
  "properties": {
90
90
  "todo": { "type": "string", "default": "Todo" },
91
- "inprogress": { "type": "string", "default": "In Progress" },
91
+ "inprogress": {
92
+ "type": "string",
93
+ "default": "In Progress"
94
+ },
92
95
  "inreview": { "type": "string", "default": "In Review" },
93
96
  "done": { "type": "string", "default": "Done" },
94
97
  "cancelled": { "type": "string", "default": "Cancelled" }
@@ -221,6 +224,81 @@
221
224
  "type": "string",
222
225
  "enum": ["codex-sdk", "kanban", "disabled"]
223
226
  },
227
+ "triggerSystem": {
228
+ "type": "object",
229
+ "additionalProperties": true,
230
+ "properties": {
231
+ "enabled": { "type": "boolean", "default": false },
232
+ "templates": {
233
+ "type": "array",
234
+ "items": {
235
+ "type": "object",
236
+ "additionalProperties": true,
237
+ "properties": {
238
+ "id": { "type": "string" },
239
+ "name": { "type": "string" },
240
+ "description": { "type": "string" },
241
+ "enabled": { "type": "boolean", "default": false },
242
+ "action": {
243
+ "type": "string",
244
+ "enum": ["task-planner", "create-task"]
245
+ },
246
+ "minIntervalMinutes": { "type": "number", "minimum": 1 },
247
+ "trigger": {
248
+ "type": "object",
249
+ "additionalProperties": true,
250
+ "properties": {
251
+ "anyOf": {
252
+ "type": "array",
253
+ "items": {
254
+ "type": "object",
255
+ "additionalProperties": true,
256
+ "properties": {
257
+ "kind": {
258
+ "type": "string",
259
+ "enum": ["metric", "interval"]
260
+ },
261
+ "metric": { "type": "string" },
262
+ "operator": {
263
+ "type": "string",
264
+ "enum": ["lt", "lte", "gt", "gte", "eq", "neq"]
265
+ },
266
+ "value": { "type": ["number", "string", "boolean"] },
267
+ "minutes": { "type": "number", "minimum": 1 }
268
+ }
269
+ }
270
+ }
271
+ }
272
+ },
273
+ "config": {
274
+ "type": "object",
275
+ "additionalProperties": true,
276
+ "properties": {
277
+ "plannerMode": {
278
+ "type": "string",
279
+ "enum": ["codex-sdk", "kanban", "disabled"]
280
+ },
281
+ "defaultTaskCount": { "type": "number", "minimum": 1 },
282
+ "title": { "type": "string" },
283
+ "description": { "type": "string" },
284
+ "priority": { "type": "string" },
285
+ "executor": { "type": "string" },
286
+ "model": { "type": "string" }
287
+ }
288
+ }
289
+ }
290
+ }
291
+ },
292
+ "defaults": {
293
+ "type": "object",
294
+ "additionalProperties": false,
295
+ "properties": {
296
+ "executor": { "type": "string" },
297
+ "model": { "type": "string" }
298
+ }
299
+ }
300
+ }
301
+ },
224
302
  "activeWorkspace": {
225
303
  "type": "string",
226
304
  "description": "ID of the currently active workspace"
@@ -249,11 +327,23 @@
249
327
  "additionalProperties": true,
250
328
  "required": ["name"],
251
329
  "properties": {
252
- "name": { "type": "string", "description": "Repository directory name" },
330
+ "name": {
331
+ "type": "string",
332
+ "description": "Repository directory name"
333
+ },
253
334
  "url": { "type": "string", "description": "Git clone URL" },
254
- "slug": { "type": "string", "description": "GitHub slug (org/repo)" },
255
- "primary": { "type": "boolean", "description": "Whether this is the primary repo" },
256
- "branch": { "type": "string", "description": "Default branch to track" }
335
+ "slug": {
336
+ "type": "string",
337
+ "description": "GitHub slug (org/repo)"
338
+ },
339
+ "primary": {
340
+ "type": "boolean",
341
+ "description": "Whether this is the primary repo"
342
+ },
343
+ "branch": {
344
+ "type": "string",
345
+ "description": "Default branch to track"
346
+ }
257
347
  }
258
348
  }
259
349
  },
@@ -399,6 +489,10 @@
399
489
  "name": { "type": "string" },
400
490
  "executor": { "type": "string" },
401
491
  "variant": { "type": "string" },
492
+ "models": {
493
+ "type": "array",
494
+ "items": { "type": "string" }
495
+ },
402
496
  "weight": { "type": "number" },
403
497
  "role": { "type": "string" },
404
498
  "enabled": { "type": "boolean" }
package/config.mjs CHANGED
@@ -26,6 +26,10 @@ import {
26
26
  } from "./agent-prompts.mjs";
27
27
  import { resolveAgentRepoRoot } from "./repo-root.mjs";
28
28
  import { applyAllCompatibility } from "./compat.mjs";
29
+ import {
30
+ normalizeExecutorKey,
31
+ getModelsForExecutor,
32
+ } from "./task-complexity.mjs";
29
33
 
30
34
  const __dirname = dirname(fileURLToPath(import.meta.url));
31
35
 
@@ -268,6 +272,173 @@ function isEnvEnabled(value, defaultValue = false) {
268
272
  return parseEnvBoolean(value, defaultValue);
269
273
  }
270
274
 
275
+ function parseListValue(value) {
276
+ if (Array.isArray(value)) {
277
+ return value
278
+ .map((item) => String(item || "").trim())
279
+ .filter(Boolean);
280
+ }
281
+ return String(value || "")
282
+ .split(/[,|]/)
283
+ .map((item) => item.trim())
284
+ .filter(Boolean);
285
+ }
286
+
287
+ function normalizeExecutorModels(executor, models) {
288
+ const normalizedExecutor = normalizeExecutorKey(executor);
289
+ if (!normalizedExecutor) return [];
290
+ const input = parseListValue(models);
291
+ const known = new Set(getModelsForExecutor(normalizedExecutor));
292
+ if (input.length === 0) {
293
+ return [...known];
294
+ }
295
+ return input.filter((model) => known.has(model));
296
+ }
297
+
298
+ function normalizeExecutorEntry(entry, index = 0, total = 1) {
299
+ if (!entry || typeof entry !== "object") return null;
300
+ const executorType = String(entry.executor || "").trim().toUpperCase();
301
+ if (!executorType) return null;
302
+ const variant = String(entry.variant || "DEFAULT").trim() || "DEFAULT";
303
+ const normalized = normalizeExecutorKey(executorType) || "codex";
304
+ const weight = Number(entry.weight);
305
+ const safeWeight = Number.isFinite(weight) ? weight : Math.floor(100 / Math.max(1, total));
306
+ const role =
307
+ String(entry.role || "").trim() ||
308
+ (index === 0 ? "primary" : index === 1 ? "backup" : `executor-${index + 1}`);
309
+ const name =
310
+ String(entry.name || "").trim() ||
311
+ `${normalized}-${String(variant || "default").toLowerCase()}`;
312
+ const models = normalizeExecutorModels(executorType, entry.models);
313
+
314
+ return {
315
+ name,
316
+ executor: executorType,
317
+ variant,
318
+ weight: safeWeight,
319
+ role,
320
+ enabled: entry.enabled !== false,
321
+ models,
322
+ };
323
+ }
324
+
325
+ function buildDefaultTriggerTemplates({
326
+ plannerMode,
327
+ plannerPerCapitaThreshold,
328
+ plannerIdleSlotThreshold,
329
+ plannerDedupHours,
330
+ } = {}) {
331
+ return [
332
+ {
333
+ id: "task-planner",
334
+ name: "Task Planner",
335
+ description: "Create planning tasks when backlog/slot metrics indicate replenishment.",
336
+ enabled: false,
337
+ action: "task-planner",
338
+ trigger: {
339
+ anyOf: [
340
+ {
341
+ kind: "metric",
342
+ metric: "backlogPerCapita",
343
+ operator: "lt",
344
+ value: plannerPerCapitaThreshold,
345
+ },
346
+ {
347
+ kind: "metric",
348
+ metric: "idleSlots",
349
+ operator: "gte",
350
+ value: plannerIdleSlotThreshold,
351
+ },
352
+ {
353
+ kind: "metric",
354
+ metric: "backlogRemaining",
355
+ operator: "eq",
356
+ value: 0,
357
+ },
358
+ ],
359
+ },
360
+ minIntervalMinutes: Math.max(1, Number(plannerDedupHours || 6) * 60),
361
+ config: {
362
+ plannerMode,
363
+ defaultTaskCount: Number(process.env.TASK_PLANNER_DEFAULT_COUNT || "30"),
364
+ executor: "auto",
365
+ model: "auto",
366
+ },
367
+ },
368
+ {
369
+ id: "daily-review-digest",
370
+ name: "Daily Review Digest",
371
+ description: "Create a daily review task for fleet health and backlog quality.",
372
+ enabled: false,
373
+ action: "create-task",
374
+ trigger: {
375
+ anyOf: [
376
+ {
377
+ kind: "interval",
378
+ minutes: 24 * 60,
379
+ },
380
+ ],
381
+ },
382
+ minIntervalMinutes: 24 * 60,
383
+ config: {
384
+ title: "[m] Daily review digest",
385
+ description:
386
+ "Review active backlog, blocked tasks, and stale work. Capture next actions and priority adjustments.",
387
+ priority: "medium",
388
+ executor: "auto",
389
+ model: "auto",
390
+ },
391
+ },
392
+ {
393
+ id: "stale-task-followup",
394
+ name: "Stale Task Follow-up",
395
+ description: "Create a follow-up task when stale in-progress work accumulates.",
396
+ enabled: false,
397
+ action: "create-task",
398
+ trigger: {
399
+ anyOf: [
400
+ {
401
+ kind: "metric",
402
+ metric: "staleInProgressCount",
403
+ operator: "gte",
404
+ value: 1,
405
+ },
406
+ ],
407
+ },
408
+ minIntervalMinutes: 60,
409
+ config: {
410
+ title: "[m] Follow up stale in-progress tasks",
411
+ description:
412
+ "Audit stale in-progress tasks, unblock owners, or split work to recover flow.",
413
+ priority: "high",
414
+ staleHours: Number(process.env.STALE_TASK_AGE_HOURS || "24"),
415
+ executor: "auto",
416
+ model: "auto",
417
+ },
418
+ },
419
+ ];
420
+ }
421
+
422
+ function resolveTriggerSystemConfig(configData, defaults) {
423
+ const configTrigger =
424
+ configData && typeof configData.triggerSystem === "object"
425
+ ? configData.triggerSystem
426
+ : configData && typeof configData.triggers === "object"
427
+ ? configData.triggers
428
+ : {};
429
+ const templates = Array.isArray(configTrigger.templates)
430
+ ? configTrigger.templates
431
+ : defaults.templates;
432
+ return Object.freeze({
433
+ enabled: isEnvEnabled(
434
+ process.env.TASK_TRIGGER_SYSTEM_ENABLED ?? configTrigger.enabled,
435
+ false,
436
+ ),
437
+ templates,
438
+ defaults: defaults.defaults,
439
+ });
440
+ }
441
+
271
442
  // ── Git helpers ──────────────────────────────────────────────────────────────
272
443
 
273
444
  function detectRepoSlug(repoRoot = "") {
@@ -421,7 +592,7 @@ const DEFAULT_EXECUTORS = {
421
592
  };
422
593
 
423
594
  function parseExecutorsFromEnv() {
424
- // EXECUTORS=CODEX:DEFAULT:100
595
+ // EXECUTORS=CODEX:DEFAULT:100:gpt-5.2-codex|gpt-5.1-codex-mini
425
596
  const raw = process.env.EXECUTORS;
426
597
  if (!raw) return null;
427
598
  const entries = raw.split(",").map((e) => e.trim());
@@ -430,13 +601,16 @@ function parseExecutorsFromEnv() {
430
601
  for (let i = 0; i < entries.length; i++) {
431
602
  const parts = entries[i].split(":");
432
603
  if (parts.length < 2) continue;
604
+ const executorType = parts[0].toUpperCase();
605
+ const models = normalizeExecutorModels(executorType, parts[3] || "");
433
606
  executors.push({
434
607
  name: `${parts[0].toLowerCase()}-${parts[1].toLowerCase()}`,
435
- executor: parts[0].toUpperCase(),
608
+ executor: executorType,
436
609
  variant: parts[1],
437
610
  weight: parts[2] ? Number(parts[2]) : Math.floor(100 / entries.length),
438
611
  role: roles[i] || `executor-${i + 1}`,
439
612
  enabled: true,
613
+ models,
440
614
  });
441
615
  }
442
616
  return executors.length ? executors : null;
@@ -522,8 +696,11 @@ function loadExecutorConfig(configDir, configData) {
522
696
  }
523
697
  }
524
698
 
525
- const executors =
699
+ const baseExecutors =
526
700
  fromEnv || fromFile?.executors || DEFAULT_EXECUTORS.executors;
701
+ const executors = (Array.isArray(baseExecutors) ? baseExecutors : [])
702
+ .map((entry, index, arr) => normalizeExecutorEntry(entry, index, arr.length))
703
+ .filter(Boolean);
527
704
  const failover = fromFile?.failover || {
528
705
  strategy:
529
706
  process.env.FAILOVER_STRATEGY || DEFAULT_EXECUTORS.failover.strategy,
@@ -1483,6 +1660,23 @@ export function loadConfig(argv = process.argv, options = {}) {
1483
1660
  ? plannerDedupHours * 60 * 60 * 1000
1484
1661
  : 24 * 60 * 60 * 1000;
1485
1662
 
1663
+ const triggerSystemDefaults = Object.freeze({
1664
+ templates: buildDefaultTriggerTemplates({
1665
+ plannerMode,
1666
+ plannerPerCapitaThreshold,
1667
+ plannerIdleSlotThreshold,
1668
+ plannerDedupHours,
1669
+ }),
1670
+ defaults: Object.freeze({
1671
+ executor: "auto",
1672
+ model: "auto",
1673
+ }),
1674
+ });
1675
+ const triggerSystem = resolveTriggerSystemConfig(
1676
+ configData,
1677
+ triggerSystemDefaults,
1678
+ );
1679
+
1486
1680
  // ── GitHub Reconciler ───────────────────────────────────
1487
1681
  const ghReconcileEnabled = isEnvEnabled(
1488
1682
  process.env.GH_RECONCILE_ENABLED ?? configData.ghReconcileEnabled,
@@ -1749,6 +1943,7 @@ export function loadConfig(argv = process.argv, options = {}) {
1749
1943
  plannerIdleSlotThreshold,
1750
1944
  plannerDedupHours,
1751
1945
  plannerDedupMs,
1946
+ triggerSystem,
1752
1947
 
1753
1948
  // GitHub Reconciler
1754
1949
  githubReconcile: {
@@ -31,6 +31,10 @@ const containerTimeout = parseInt(
31
31
  process.env.CONTAINER_TIMEOUT_MS || "1800000",
32
32
  10,
33
33
  ); // 30 min default
34
+ const containerRuntimeCheckTimeout = Math.max(
35
+ 500,
36
+ parseInt(process.env.CONTAINER_RUNTIME_CHECK_TIMEOUT_MS || "3000", 10),
37
+ );
34
38
  const containerMaxOutput = parseInt(
35
39
  process.env.CONTAINER_MAX_OUTPUT_SIZE || "10485760",
36
40
  10,
@@ -84,11 +88,17 @@ export function checkContainerRuntime() {
84
88
  try {
85
89
  if (containerRuntime === "container") {
86
90
  // macOS Apple Container
87
- execSync("container system status", { stdio: "pipe" });
91
+ execSync("container system status", {
92
+ stdio: "pipe",
93
+ timeout: containerRuntimeCheckTimeout,
94
+ });
88
95
  return { available: true, runtime: "container", platform: "macos" };
89
96
  }
90
97
  // Docker or Podman
91
- execSync(`${containerRuntime} info`, { stdio: "pipe", timeout: 10000 });
98
+ execSync(`${containerRuntime} info`, {
99
+ stdio: "pipe",
100
+ timeout: containerRuntimeCheckTimeout,
101
+ });
92
102
  return {
93
103
  available: true,
94
104
  runtime: containerRuntime,
@@ -110,7 +120,10 @@ export function ensureContainerRuntime() {
110
120
  if (containerRuntime === "container") {
111
121
  // macOS Apple Container — may need explicit start
112
122
  try {
113
- execSync("container system status", { stdio: "pipe" });
123
+ execSync("container system status", {
124
+ stdio: "pipe",
125
+ timeout: containerRuntimeCheckTimeout,
126
+ });
114
127
  } catch {
115
128
  console.log("[container] Starting Apple Container system...");
116
129
  try {