bosun 0.31.6 → 0.31.8
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 +31 -14
- package/bosun.schema.json +99 -5
- package/config-doctor.mjs +21 -0
- package/config.mjs +279 -3
- package/container-runner.mjs +16 -3
- package/copilot-shell.mjs +27 -19
- package/github-app-auth.mjs +463 -0
- package/github-auth-manager.mjs +265 -0
- package/github-oauth-portal.mjs +949 -0
- package/monitor.mjs +378 -64
- package/package.json +10 -2
- package/primary-agent.mjs +119 -0
- package/sync-engine.mjs +14 -7
- package/task-complexity.mjs +80 -0
- package/task-executor.mjs +171 -6
- package/telegram-bot.mjs +8 -3
- package/ui/app.js +119 -97
- package/ui/components/chat-view.js +37 -6
- package/ui/components/forms.js +9 -6
- package/ui/components/shared.js +5 -11
- package/ui/demo.html +58 -0
- package/ui/modules/settings-schema.js +2 -1
- package/ui/modules/state.js +21 -13
- package/ui/modules/telegram.js +136 -63
- package/ui/styles/layout.css +8 -0
- package/ui/styles/sessions.css +10 -3
- package/ui/tabs/dashboard.js +8 -1
- package/ui/tabs/settings.js +46 -26
- package/ui/tabs/tasks.js +366 -6
- package/ui-server.mjs +379 -1
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
|
|
|
@@ -345,31 +345,42 @@ TELEGRAM_MINIAPP_ENABLED=false
|
|
|
345
345
|
# OAuth Client Secret (only needed for callback-based OAuth, not for Device Flow):
|
|
346
346
|
# BOSUN_GITHUB_CLIENT_SECRET=
|
|
347
347
|
#
|
|
348
|
-
# Webhook secret (
|
|
348
|
+
# Webhook secret (VirtEngine relay signs forwarded events with this — leave blank
|
|
349
|
+
# until VirtEngine’s relay server is live; Bosun polls GitHub API in the meantime):
|
|
349
350
|
# BOSUN_GITHUB_WEBHOOK_SECRET=
|
|
350
351
|
#
|
|
351
352
|
# Path to the PEM private key downloaded from App settings → Generate a private key:
|
|
352
353
|
# BOSUN_GITHUB_PRIVATE_KEY_PATH=/path/to/bosun-botswain.pem
|
|
353
354
|
#
|
|
355
|
+
# ─── GitHub App Settings (enable all three in https://github.com/settings/apps/bosun-botswain) ────
|
|
356
|
+
# ✅ Callback URL → http://127.0.0.1:54317/github/callback (set this FIRST, then Save)
|
|
357
|
+
# ✅ "Request user authorization (OAuth) during installation" → ON
|
|
358
|
+
# GitHub does OAuth at install time, redirecting to the Callback URL with
|
|
359
|
+
# installation_id + setup_action=install. Setup URL is DISABLED — that's fine.
|
|
360
|
+
# ✅ "Enable Device Flow" → ON (only available AFTER Callback URL is saved)
|
|
361
|
+
# Allows CLI/terminal auth without a public URL (like VS Code / Roo Code)
|
|
362
|
+
# ✕ Setup URL → leave BLANK (GitHub disables this field when OAuth-at-install is ON)
|
|
363
|
+
# ✕ "Redirect on update" → leave OFF (disabled alongside Setup URL)
|
|
364
|
+
#
|
|
354
365
|
# ─── Authentication Method ───────────────────────────────────────────────
|
|
355
366
|
# RECOMMENDED: Device Flow (like VS Code / Roo Code — no public URL needed!)
|
|
356
367
|
# 1. Set BOSUN_GITHUB_CLIENT_ID above
|
|
357
|
-
# 2. Enable
|
|
358
|
-
# 3. Go to Settings → GitHub in the Bosun UI and click
|
|
359
|
-
# 4. That
|
|
368
|
+
# 2. Enable “Device Flow” in GitHub App settings (only clickable after Callback URL is saved)
|
|
369
|
+
# 3. Go to Settings → GitHub in the Bosun UI and click “Sign in with GitHub”
|
|
370
|
+
# 4. That’s it — no webhook URL, no tunnel, no public server needed
|
|
360
371
|
#
|
|
361
|
-
# ALTERNATIVE: OAuth Callback
|
|
372
|
+
# ALTERNATIVE: OAuth Callback
|
|
362
373
|
# Set BOSUN_GITHUB_CLIENT_ID + BOSUN_GITHUB_CLIENT_SECRET
|
|
363
|
-
# Register callback URL
|
|
364
|
-
# https://<your-bosun-public-url>/api/github/callback
|
|
374
|
+
# Register callback URL: http://127.0.0.1:54317/github/callback
|
|
365
375
|
#
|
|
366
|
-
#
|
|
367
|
-
#
|
|
368
|
-
#
|
|
369
|
-
#
|
|
376
|
+
# NOTE on webhooks:
|
|
377
|
+
# Real-time GitHub events (PR comments, issue mentions) are received via
|
|
378
|
+
# VirtEngine’s relay server and forwarded to your Bosun instance.
|
|
379
|
+
# Until the relay is live, Bosun polls the GitHub API every few minutes instead.
|
|
380
|
+
# Users do NOT need to configure a webhook URL or run any tunnel.
|
|
370
381
|
#
|
|
371
382
|
# Leave BOSUN_GITHUB_APP_ID unset to disable co-author trailer injection.
|
|
372
|
-
#
|
|
383
|
+
# (App ID and Client ID are already filled in above — no need to set them again.)
|
|
373
384
|
|
|
374
385
|
# ─── Kanban Backend ──────────────────────────────────────────────────────────
|
|
375
386
|
# Task-board backend:
|
|
@@ -897,6 +908,12 @@ COPILOT_CLOUD_DISABLED=true
|
|
|
897
908
|
# Task planner status stream interval (milliseconds). Default: 1800000 (30 min)
|
|
898
909
|
# DEVMODE_TASK_PLANNER_STATUS_INTERVAL_MS=1800000
|
|
899
910
|
|
|
911
|
+
# ─── Trigger-Based Task System ───────────────────────────────────────────────
|
|
912
|
+
# Enable configurable trigger templates (disabled by default for safety).
|
|
913
|
+
# Built-in templates ship disabled: task-planner, daily-review-digest,
|
|
914
|
+
# stale-task-followup. Configure/enable in bosun.config.json under triggerSystem.
|
|
915
|
+
# TASK_TRIGGER_SYSTEM_ENABLED=false
|
|
916
|
+
|
|
900
917
|
# ─── GitHub Issue Reconciler ─────────────────────────────────────────────────
|
|
901
918
|
# Periodically reconciles open GitHub issues against open/merged PRs.
|
|
902
919
|
# 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": {
|
|
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": {
|
|
330
|
+
"name": {
|
|
331
|
+
"type": "string",
|
|
332
|
+
"description": "Repository directory name"
|
|
333
|
+
},
|
|
253
334
|
"url": { "type": "string", "description": "Git clone URL" },
|
|
254
|
-
"slug": {
|
|
255
|
-
|
|
256
|
-
|
|
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-doctor.mjs
CHANGED
|
@@ -340,6 +340,27 @@ export function runConfigDoctor(options = {}) {
|
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
if (backend === "jira") {
|
|
344
|
+
const missing = [];
|
|
345
|
+
if (!effective.JIRA_BASE_URL) missing.push("JIRA_BASE_URL");
|
|
346
|
+
if (!effective.JIRA_EMAIL) missing.push("JIRA_EMAIL");
|
|
347
|
+
if (!effective.JIRA_API_TOKEN) missing.push("JIRA_API_TOKEN");
|
|
348
|
+
const hasProjectKey = Boolean(
|
|
349
|
+
effective.JIRA_PROJECT_KEY || effective.KANBAN_PROJECT_ID,
|
|
350
|
+
);
|
|
351
|
+
if (!hasProjectKey) {
|
|
352
|
+
missing.push("JIRA_PROJECT_KEY (or KANBAN_PROJECT_ID)");
|
|
353
|
+
}
|
|
354
|
+
if (missing.length > 0) {
|
|
355
|
+
issues.errors.push({
|
|
356
|
+
code: "JIRA_BACKEND_REQUIRED",
|
|
357
|
+
message: `KANBAN_BACKEND=jira is missing required config: ${missing.join(", ")}`,
|
|
358
|
+
fix:
|
|
359
|
+
"Set required JIRA_* variables (and project key), or switch KANBAN_BACKEND=internal.",
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
343
364
|
const vkNeeded = backend === "vk" || mode === "vk" || mode === "hybrid";
|
|
344
365
|
if (vkNeeded) {
|
|
345
366
|
const vkBaseUrl = effective.VK_BASE_URL || "";
|
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
|
|
|
@@ -158,6 +162,80 @@ function loadDotEnvFile(envPath, options = {}) {
|
|
|
158
162
|
}
|
|
159
163
|
}
|
|
160
164
|
|
|
165
|
+
function readEnvValueFromFile(envPath, key) {
|
|
166
|
+
if (!envPath || !existsSync(envPath)) return undefined;
|
|
167
|
+
const lines = readFileSync(envPath, "utf8").split("\n");
|
|
168
|
+
let found;
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
const trimmed = line.trim();
|
|
171
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
172
|
+
const eqIdx = trimmed.indexOf("=");
|
|
173
|
+
if (eqIdx === -1) continue;
|
|
174
|
+
const parsedKey = trimmed.slice(0, eqIdx).trim();
|
|
175
|
+
if (parsedKey !== key) continue;
|
|
176
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
177
|
+
if (
|
|
178
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
179
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
180
|
+
) {
|
|
181
|
+
value = value.slice(1, -1);
|
|
182
|
+
}
|
|
183
|
+
found = value;
|
|
184
|
+
}
|
|
185
|
+
return found;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function resolveKanbanBackendSource({ envPaths = [], configFilePath, configData }) {
|
|
189
|
+
const key = "KANBAN_BACKEND";
|
|
190
|
+
let source = "default";
|
|
191
|
+
let sourcePath = null;
|
|
192
|
+
|
|
193
|
+
if (process.env[key] != null && String(process.env[key]).trim() !== "") {
|
|
194
|
+
let envFileMatch = null;
|
|
195
|
+
for (const envPath of envPaths) {
|
|
196
|
+
const value = readEnvValueFromFile(envPath, key);
|
|
197
|
+
if (value != null && String(value).trim() !== "") {
|
|
198
|
+
envFileMatch = envPath;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (envFileMatch) {
|
|
202
|
+
source = "env-file";
|
|
203
|
+
sourcePath = envFileMatch;
|
|
204
|
+
} else {
|
|
205
|
+
source = "process-env";
|
|
206
|
+
}
|
|
207
|
+
} else if (configData?.kanban?.backend != null) {
|
|
208
|
+
source = "config-file";
|
|
209
|
+
sourcePath = configFilePath || null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return Object.freeze({
|
|
213
|
+
key,
|
|
214
|
+
rawValue:
|
|
215
|
+
process.env[key] || configData?.kanban?.backend || "internal",
|
|
216
|
+
source,
|
|
217
|
+
sourcePath,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function validateKanbanBackendConfig({ kanbanBackend, kanban, jira }) {
|
|
222
|
+
if (kanbanBackend !== "jira") return;
|
|
223
|
+
const missing = [];
|
|
224
|
+
if (!jira?.baseUrl) missing.push("JIRA_BASE_URL");
|
|
225
|
+
if (!jira?.email) missing.push("JIRA_EMAIL");
|
|
226
|
+
if (!jira?.apiToken) missing.push("JIRA_API_TOKEN");
|
|
227
|
+
const hasProjectKey = Boolean(jira?.projectKey || kanban?.projectId);
|
|
228
|
+
if (!hasProjectKey) {
|
|
229
|
+
missing.push("JIRA_PROJECT_KEY (or KANBAN_PROJECT_ID)");
|
|
230
|
+
}
|
|
231
|
+
if (missing.length > 0) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`[config] KANBAN_BACKEND=jira requires ${missing.join(", ")}. ` +
|
|
234
|
+
`Either configure Jira credentials/project key or switch KANBAN_BACKEND=internal.`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
161
239
|
function loadConfigFile(configDir) {
|
|
162
240
|
for (const name of CONFIG_FILES) {
|
|
163
241
|
const p = resolve(configDir, name);
|
|
@@ -268,6 +346,173 @@ function isEnvEnabled(value, defaultValue = false) {
|
|
|
268
346
|
return parseEnvBoolean(value, defaultValue);
|
|
269
347
|
}
|
|
270
348
|
|
|
349
|
+
function parseListValue(value) {
|
|
350
|
+
if (Array.isArray(value)) {
|
|
351
|
+
return value
|
|
352
|
+
.map((item) => String(item || "").trim())
|
|
353
|
+
.filter(Boolean);
|
|
354
|
+
}
|
|
355
|
+
return String(value || "")
|
|
356
|
+
.split(/[,|]/)
|
|
357
|
+
.map((item) => item.trim())
|
|
358
|
+
.filter(Boolean);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function normalizeExecutorModels(executor, models) {
|
|
362
|
+
const normalizedExecutor = normalizeExecutorKey(executor);
|
|
363
|
+
if (!normalizedExecutor) return [];
|
|
364
|
+
const input = parseListValue(models);
|
|
365
|
+
const known = new Set(getModelsForExecutor(normalizedExecutor));
|
|
366
|
+
if (input.length === 0) {
|
|
367
|
+
return [...known];
|
|
368
|
+
}
|
|
369
|
+
return input.filter((model) => known.has(model));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function normalizeExecutorEntry(entry, index = 0, total = 1) {
|
|
373
|
+
if (!entry || typeof entry !== "object") return null;
|
|
374
|
+
const executorType = String(entry.executor || "").trim().toUpperCase();
|
|
375
|
+
if (!executorType) return null;
|
|
376
|
+
const variant = String(entry.variant || "DEFAULT").trim() || "DEFAULT";
|
|
377
|
+
const normalized = normalizeExecutorKey(executorType) || "codex";
|
|
378
|
+
const weight = Number(entry.weight);
|
|
379
|
+
const safeWeight = Number.isFinite(weight) ? weight : Math.floor(100 / Math.max(1, total));
|
|
380
|
+
const role =
|
|
381
|
+
String(entry.role || "").trim() ||
|
|
382
|
+
(index === 0 ? "primary" : index === 1 ? "backup" : `executor-${index + 1}`);
|
|
383
|
+
const name =
|
|
384
|
+
String(entry.name || "").trim() ||
|
|
385
|
+
`${normalized}-${String(variant || "default").toLowerCase()}`;
|
|
386
|
+
const models = normalizeExecutorModels(executorType, entry.models);
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
name,
|
|
390
|
+
executor: executorType,
|
|
391
|
+
variant,
|
|
392
|
+
weight: safeWeight,
|
|
393
|
+
role,
|
|
394
|
+
enabled: entry.enabled !== false,
|
|
395
|
+
models,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function buildDefaultTriggerTemplates({
|
|
400
|
+
plannerMode,
|
|
401
|
+
plannerPerCapitaThreshold,
|
|
402
|
+
plannerIdleSlotThreshold,
|
|
403
|
+
plannerDedupHours,
|
|
404
|
+
} = {}) {
|
|
405
|
+
return [
|
|
406
|
+
{
|
|
407
|
+
id: "task-planner",
|
|
408
|
+
name: "Task Planner",
|
|
409
|
+
description: "Create planning tasks when backlog/slot metrics indicate replenishment.",
|
|
410
|
+
enabled: false,
|
|
411
|
+
action: "task-planner",
|
|
412
|
+
trigger: {
|
|
413
|
+
anyOf: [
|
|
414
|
+
{
|
|
415
|
+
kind: "metric",
|
|
416
|
+
metric: "backlogPerCapita",
|
|
417
|
+
operator: "lt",
|
|
418
|
+
value: plannerPerCapitaThreshold,
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
kind: "metric",
|
|
422
|
+
metric: "idleSlots",
|
|
423
|
+
operator: "gte",
|
|
424
|
+
value: plannerIdleSlotThreshold,
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
kind: "metric",
|
|
428
|
+
metric: "backlogRemaining",
|
|
429
|
+
operator: "eq",
|
|
430
|
+
value: 0,
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
},
|
|
434
|
+
minIntervalMinutes: Math.max(1, Number(plannerDedupHours || 6) * 60),
|
|
435
|
+
config: {
|
|
436
|
+
plannerMode,
|
|
437
|
+
defaultTaskCount: Number(process.env.TASK_PLANNER_DEFAULT_COUNT || "30"),
|
|
438
|
+
executor: "auto",
|
|
439
|
+
model: "auto",
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
id: "daily-review-digest",
|
|
444
|
+
name: "Daily Review Digest",
|
|
445
|
+
description: "Create a daily review task for fleet health and backlog quality.",
|
|
446
|
+
enabled: false,
|
|
447
|
+
action: "create-task",
|
|
448
|
+
trigger: {
|
|
449
|
+
anyOf: [
|
|
450
|
+
{
|
|
451
|
+
kind: "interval",
|
|
452
|
+
minutes: 24 * 60,
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
},
|
|
456
|
+
minIntervalMinutes: 24 * 60,
|
|
457
|
+
config: {
|
|
458
|
+
title: "[m] Daily review digest",
|
|
459
|
+
description:
|
|
460
|
+
"Review active backlog, blocked tasks, and stale work. Capture next actions and priority adjustments.",
|
|
461
|
+
priority: "medium",
|
|
462
|
+
executor: "auto",
|
|
463
|
+
model: "auto",
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
id: "stale-task-followup",
|
|
468
|
+
name: "Stale Task Follow-up",
|
|
469
|
+
description: "Create a follow-up task when stale in-progress work accumulates.",
|
|
470
|
+
enabled: false,
|
|
471
|
+
action: "create-task",
|
|
472
|
+
trigger: {
|
|
473
|
+
anyOf: [
|
|
474
|
+
{
|
|
475
|
+
kind: "metric",
|
|
476
|
+
metric: "staleInProgressCount",
|
|
477
|
+
operator: "gte",
|
|
478
|
+
value: 1,
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
},
|
|
482
|
+
minIntervalMinutes: 60,
|
|
483
|
+
config: {
|
|
484
|
+
title: "[m] Follow up stale in-progress tasks",
|
|
485
|
+
description:
|
|
486
|
+
"Audit stale in-progress tasks, unblock owners, or split work to recover flow.",
|
|
487
|
+
priority: "high",
|
|
488
|
+
staleHours: Number(process.env.STALE_TASK_AGE_HOURS || "24"),
|
|
489
|
+
executor: "auto",
|
|
490
|
+
model: "auto",
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
];
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function resolveTriggerSystemConfig(configData, defaults) {
|
|
497
|
+
const configTrigger =
|
|
498
|
+
configData && typeof configData.triggerSystem === "object"
|
|
499
|
+
? configData.triggerSystem
|
|
500
|
+
: configData && typeof configData.triggers === "object"
|
|
501
|
+
? configData.triggers
|
|
502
|
+
: {};
|
|
503
|
+
const templates = Array.isArray(configTrigger.templates)
|
|
504
|
+
? configTrigger.templates
|
|
505
|
+
: defaults.templates;
|
|
506
|
+
return Object.freeze({
|
|
507
|
+
enabled: isEnvEnabled(
|
|
508
|
+
process.env.TASK_TRIGGER_SYSTEM_ENABLED ?? configTrigger.enabled,
|
|
509
|
+
false,
|
|
510
|
+
),
|
|
511
|
+
templates,
|
|
512
|
+
defaults: defaults.defaults,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
271
516
|
// ── Git helpers ──────────────────────────────────────────────────────────────
|
|
272
517
|
|
|
273
518
|
function detectRepoSlug(repoRoot = "") {
|
|
@@ -421,7 +666,7 @@ const DEFAULT_EXECUTORS = {
|
|
|
421
666
|
};
|
|
422
667
|
|
|
423
668
|
function parseExecutorsFromEnv() {
|
|
424
|
-
// EXECUTORS=CODEX:DEFAULT:100
|
|
669
|
+
// EXECUTORS=CODEX:DEFAULT:100:gpt-5.2-codex|gpt-5.1-codex-mini
|
|
425
670
|
const raw = process.env.EXECUTORS;
|
|
426
671
|
if (!raw) return null;
|
|
427
672
|
const entries = raw.split(",").map((e) => e.trim());
|
|
@@ -430,13 +675,16 @@ function parseExecutorsFromEnv() {
|
|
|
430
675
|
for (let i = 0; i < entries.length; i++) {
|
|
431
676
|
const parts = entries[i].split(":");
|
|
432
677
|
if (parts.length < 2) continue;
|
|
678
|
+
const executorType = parts[0].toUpperCase();
|
|
679
|
+
const models = normalizeExecutorModels(executorType, parts[3] || "");
|
|
433
680
|
executors.push({
|
|
434
681
|
name: `${parts[0].toLowerCase()}-${parts[1].toLowerCase()}`,
|
|
435
|
-
executor:
|
|
682
|
+
executor: executorType,
|
|
436
683
|
variant: parts[1],
|
|
437
684
|
weight: parts[2] ? Number(parts[2]) : Math.floor(100 / entries.length),
|
|
438
685
|
role: roles[i] || `executor-${i + 1}`,
|
|
439
686
|
enabled: true,
|
|
687
|
+
models,
|
|
440
688
|
});
|
|
441
689
|
}
|
|
442
690
|
return executors.length ? executors : null;
|
|
@@ -522,8 +770,11 @@ function loadExecutorConfig(configDir, configData) {
|
|
|
522
770
|
}
|
|
523
771
|
}
|
|
524
772
|
|
|
525
|
-
const
|
|
773
|
+
const baseExecutors =
|
|
526
774
|
fromEnv || fromFile?.executors || DEFAULT_EXECUTORS.executors;
|
|
775
|
+
const executors = (Array.isArray(baseExecutors) ? baseExecutors : [])
|
|
776
|
+
.map((entry, index, arr) => normalizeExecutorEntry(entry, index, arr.length))
|
|
777
|
+
.filter(Boolean);
|
|
527
778
|
const failover = fromFile?.failover || {
|
|
528
779
|
strategy:
|
|
529
780
|
process.env.FAILOVER_STRATEGY || DEFAULT_EXECUTORS.failover.strategy,
|
|
@@ -970,6 +1221,11 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
970
1221
|
resolve(configDir, ".env"),
|
|
971
1222
|
resolve(repoRoot, ".env"),
|
|
972
1223
|
].filter((p, i, arr) => arr.indexOf(p) === i);
|
|
1224
|
+
const kanbanSource = resolveKanbanBackendSource({
|
|
1225
|
+
envPaths,
|
|
1226
|
+
configFilePath: configFile.path,
|
|
1227
|
+
configData,
|
|
1228
|
+
});
|
|
973
1229
|
|
|
974
1230
|
// ── Project identity ─────────────────────────────────────
|
|
975
1231
|
const projectName =
|
|
@@ -1284,6 +1540,7 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
1284
1540
|
"",
|
|
1285
1541
|
}),
|
|
1286
1542
|
});
|
|
1543
|
+
validateKanbanBackendConfig({ kanbanBackend, kanban, jira });
|
|
1287
1544
|
|
|
1288
1545
|
const internalExecutorConfig = configData.internalExecutor || {};
|
|
1289
1546
|
const projectRequirements = {
|
|
@@ -1483,6 +1740,23 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
1483
1740
|
? plannerDedupHours * 60 * 60 * 1000
|
|
1484
1741
|
: 24 * 60 * 60 * 1000;
|
|
1485
1742
|
|
|
1743
|
+
const triggerSystemDefaults = Object.freeze({
|
|
1744
|
+
templates: buildDefaultTriggerTemplates({
|
|
1745
|
+
plannerMode,
|
|
1746
|
+
plannerPerCapitaThreshold,
|
|
1747
|
+
plannerIdleSlotThreshold,
|
|
1748
|
+
plannerDedupHours,
|
|
1749
|
+
}),
|
|
1750
|
+
defaults: Object.freeze({
|
|
1751
|
+
executor: "auto",
|
|
1752
|
+
model: "auto",
|
|
1753
|
+
}),
|
|
1754
|
+
});
|
|
1755
|
+
const triggerSystem = resolveTriggerSystemConfig(
|
|
1756
|
+
configData,
|
|
1757
|
+
triggerSystemDefaults,
|
|
1758
|
+
);
|
|
1759
|
+
|
|
1486
1760
|
// ── GitHub Reconciler ───────────────────────────────────
|
|
1487
1761
|
const ghReconcileEnabled = isEnvEnabled(
|
|
1488
1762
|
process.env.GH_RECONCILE_ENABLED ?? configData.ghReconcileEnabled,
|
|
@@ -1706,6 +1980,7 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
1706
1980
|
internalExecutor,
|
|
1707
1981
|
executorMode: internalExecutor.mode,
|
|
1708
1982
|
kanban,
|
|
1983
|
+
kanbanSource,
|
|
1709
1984
|
githubProjectSync,
|
|
1710
1985
|
jira,
|
|
1711
1986
|
projectRequirements,
|
|
@@ -1749,6 +2024,7 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
1749
2024
|
plannerIdleSlotThreshold,
|
|
1750
2025
|
plannerDedupHours,
|
|
1751
2026
|
plannerDedupMs,
|
|
2027
|
+
triggerSystem,
|
|
1752
2028
|
|
|
1753
2029
|
// GitHub Reconciler
|
|
1754
2030
|
githubReconcile: {
|
package/container-runner.mjs
CHANGED
|
@@ -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", {
|
|
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`, {
|
|
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", {
|
|
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 {
|