bosun 0.41.4 → 0.41.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/infra/monitor.mjs +10 -0
- package/package.json +1 -1
- package/ui/demo-defaults.js +4 -2
- package/workflow/project-detection.mjs +68 -3
- package/workflow/workflow-engine.mjs +68 -6
- package/workflow/workflow-nodes.mjs +36 -6
- package/workflow/workflow-templates.mjs +39 -9
- package/workflow-templates/task-lifecycle.mjs +2 -2
package/infra/monitor.mjs
CHANGED
|
@@ -663,6 +663,7 @@ async function ensureWorkflowAutomationEngine() {
|
|
|
663
663
|
"template-task-lifecycle",
|
|
664
664
|
"template-task-finalization-guard",
|
|
665
665
|
"template-agent-session-monitor",
|
|
666
|
+
"template-github-kanban-sync",
|
|
666
667
|
],
|
|
667
668
|
});
|
|
668
669
|
if (Number(reconcile?.autoUpdated || 0) > 0) {
|
|
@@ -673,6 +674,15 @@ async function ensureWorkflowAutomationEngine() {
|
|
|
673
674
|
: ""),
|
|
674
675
|
);
|
|
675
676
|
}
|
|
677
|
+
if (
|
|
678
|
+
typeof engine.load === "function" &&
|
|
679
|
+
(Number(reconcile?.autoUpdated || 0) > 0 ||
|
|
680
|
+
Number(reconcile?.metadataUpdated || 0) > 0 ||
|
|
681
|
+
(Array.isArray(reconcile?.updatedWorkflowIds) &&
|
|
682
|
+
reconcile.updatedWorkflowIds.length > 0))
|
|
683
|
+
) {
|
|
684
|
+
engine.load();
|
|
685
|
+
}
|
|
676
686
|
}
|
|
677
687
|
for (const summary of engine.list?.() || []) {
|
|
678
688
|
const installedFrom = String(summary?.metadata?.installedFrom || "").trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.6",
|
|
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",
|
package/ui/demo-defaults.js
CHANGED
|
@@ -18432,7 +18432,7 @@
|
|
|
18432
18432
|
"defaultTargetBranch": "origin/main",
|
|
18433
18433
|
"taskTimeoutMs": 21600000,
|
|
18434
18434
|
"prePrValidationEnabled": true,
|
|
18435
|
-
"prePrValidationCommand": "
|
|
18435
|
+
"prePrValidationCommand": "auto",
|
|
18436
18436
|
"autoMergeOnCreate": false,
|
|
18437
18437
|
"autoMergeMethod": "squash",
|
|
18438
18438
|
"maxRetries": 2,
|
|
@@ -18836,6 +18836,7 @@
|
|
|
18836
18836
|
"label": "Pre-PR Validation",
|
|
18837
18837
|
"config": {
|
|
18838
18838
|
"command": "{{prePrValidationCommand}}",
|
|
18839
|
+
"commandType": "qualityGate",
|
|
18839
18840
|
"cwd": "{{worktreePath}}",
|
|
18840
18841
|
"failOnError": false
|
|
18841
18842
|
},
|
|
@@ -38231,7 +38232,7 @@
|
|
|
38231
38232
|
"defaultTargetBranch": "origin/main",
|
|
38232
38233
|
"taskTimeoutMs": 21600000,
|
|
38233
38234
|
"prePrValidationEnabled": true,
|
|
38234
|
-
"prePrValidationCommand": "
|
|
38235
|
+
"prePrValidationCommand": "auto",
|
|
38235
38236
|
"autoMergeOnCreate": false,
|
|
38236
38237
|
"autoMergeMethod": "squash",
|
|
38237
38238
|
"maxRetries": 2,
|
|
@@ -38604,6 +38605,7 @@
|
|
|
38604
38605
|
"label": "Pre-PR Validation",
|
|
38605
38606
|
"config": {
|
|
38606
38607
|
"command": "{{prePrValidationCommand}}",
|
|
38608
|
+
"commandType": "qualityGate",
|
|
38607
38609
|
"cwd": "{{worktreePath}}",
|
|
38608
38610
|
"failOnError": false
|
|
38609
38611
|
},
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* project-detection.mjs — Auto-detect project type, build tools, and commands.
|
|
3
3
|
*
|
|
4
4
|
* Scans a directory for manifest files and infers the project's language,
|
|
5
|
-
* package manager, and standard commands (test, build, lint, syntax-check
|
|
5
|
+
* package manager, and standard commands (test, build, lint, syntax-check,
|
|
6
|
+
* quality gate).
|
|
6
7
|
* Handles mono-repos by detecting multiple stacks in a single root.
|
|
7
8
|
*/
|
|
8
9
|
|
|
@@ -360,6 +361,58 @@ function markerExists(rootDir, marker) {
|
|
|
360
361
|
return existsSync(resolve(rootDir, marker));
|
|
361
362
|
}
|
|
362
363
|
|
|
364
|
+
function buildPackageScriptCommand(packageManager, scriptName) {
|
|
365
|
+
const pm = String(packageManager || "npm").trim().toLowerCase();
|
|
366
|
+
if (pm === "yarn") return `yarn ${scriptName}`;
|
|
367
|
+
if (pm === "pnpm") return `pnpm ${scriptName}`;
|
|
368
|
+
if (pm === "bun") return `bun run ${scriptName}`;
|
|
369
|
+
return `npm run ${scriptName}`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function readMakefile(rootDir) {
|
|
373
|
+
for (const name of ["Makefile", "makefile", "GNUmakefile"]) {
|
|
374
|
+
try {
|
|
375
|
+
return readFileSync(resolve(rootDir, name), "utf8");
|
|
376
|
+
} catch {}
|
|
377
|
+
}
|
|
378
|
+
return "";
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function escapeRegExp(value) {
|
|
382
|
+
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function findMakeTarget(rootDir, targetNames = []) {
|
|
386
|
+
const makefile = readMakefile(rootDir);
|
|
387
|
+
if (!makefile) return "";
|
|
388
|
+
for (const name of targetNames) {
|
|
389
|
+
const pattern = new RegExp(`^${escapeRegExp(name)}\\s*:`, "m");
|
|
390
|
+
if (pattern.test(makefile)) return name;
|
|
391
|
+
}
|
|
392
|
+
return "";
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function detectQualityGateCommand(rootDir, commands = {}, options = {}) {
|
|
396
|
+
const packageManager = String(options.packageManager || "").trim().toLowerCase();
|
|
397
|
+
const scripts = readPackageJsonScripts(rootDir);
|
|
398
|
+
for (const scriptName of ["prepush:check", "prepush-check", "prepush", "pre-push", "verify", "validate", "check"]) {
|
|
399
|
+
if (typeof scripts[scriptName] === "string" && scripts[scriptName].trim()) {
|
|
400
|
+
return buildPackageScriptCommand(packageManager, scriptName);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (existsSync(resolve(rootDir, ".githooks", "pre-push"))) {
|
|
405
|
+
return "bash .githooks/pre-push";
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const makeTarget = findMakeTarget(rootDir, ["prepush", "pre-push", "verify", "validate", "check"]);
|
|
409
|
+
if (makeTarget) {
|
|
410
|
+
return `make ${makeTarget}`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return commands.test || commands.lint || commands.build || commands.syntaxCheck || "";
|
|
414
|
+
}
|
|
415
|
+
|
|
363
416
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
364
417
|
|
|
365
418
|
/**
|
|
@@ -388,6 +441,7 @@ function markerExists(rootDir, marker) {
|
|
|
388
441
|
* @property {string} lint - Lint command
|
|
389
442
|
* @property {string} syntaxCheck - Syntax/compile check command
|
|
390
443
|
* @property {string} [typeCheck] - Type-check command
|
|
444
|
+
* @property {string} [qualityGate] - Pre-push / pre-PR validation command
|
|
391
445
|
* @property {string} [testFramework] - Detected test framework name
|
|
392
446
|
*/
|
|
393
447
|
export function detectProjectStack(rootDir) {
|
|
@@ -402,6 +456,7 @@ export function detectProjectStack(rootDir) {
|
|
|
402
456
|
|
|
403
457
|
const pm = def.detectPackageManager(rootDir);
|
|
404
458
|
const commands = def.detectCommands(rootDir);
|
|
459
|
+
commands.qualityGate = detectQualityGateCommand(rootDir, commands, { packageManager: pm });
|
|
405
460
|
const frameworks = def.detectFrameworks(rootDir);
|
|
406
461
|
stacks.push({
|
|
407
462
|
id: def.id,
|
|
@@ -438,6 +493,7 @@ export function getCommandPresets(detected) {
|
|
|
438
493
|
build: [],
|
|
439
494
|
lint: [],
|
|
440
495
|
syntaxCheck: [],
|
|
496
|
+
qualityGate: [],
|
|
441
497
|
};
|
|
442
498
|
|
|
443
499
|
// If we have a detected stack, put its commands first as "Detected" options
|
|
@@ -448,6 +504,7 @@ export function getCommandPresets(detected) {
|
|
|
448
504
|
if (cmds.build) presets.build.push({ label: `${label}: ${cmds.build}`, value: cmds.build, detected: true });
|
|
449
505
|
if (cmds.lint) presets.lint.push({ label: `${label}: ${cmds.lint}`, value: cmds.lint, detected: true });
|
|
450
506
|
if (cmds.syntaxCheck) presets.syntaxCheck.push({ label: `${label}: ${cmds.syntaxCheck}`, value: cmds.syntaxCheck, detected: true });
|
|
507
|
+
if (cmds.qualityGate) presets.qualityGate.push({ label: `${label}: ${cmds.qualityGate}`, value: cmds.qualityGate, detected: true });
|
|
451
508
|
}
|
|
452
509
|
|
|
453
510
|
// Add additional detected stacks (monorepo)
|
|
@@ -459,6 +516,7 @@ export function getCommandPresets(detected) {
|
|
|
459
516
|
if (cmds.build) presets.build.push({ label: `${label}: ${cmds.build}`, value: cmds.build, detected: true });
|
|
460
517
|
if (cmds.lint) presets.lint.push({ label: `${label}: ${cmds.lint}`, value: cmds.lint, detected: true });
|
|
461
518
|
if (cmds.syntaxCheck) presets.syntaxCheck.push({ label: `${label}: ${cmds.syntaxCheck}`, value: cmds.syntaxCheck, detected: true });
|
|
519
|
+
if (cmds.qualityGate) presets.qualityGate.push({ label: `${label}: ${cmds.qualityGate}`, value: cmds.qualityGate, detected: true });
|
|
462
520
|
}
|
|
463
521
|
}
|
|
464
522
|
|
|
@@ -521,6 +579,13 @@ export function getCommandPresets(detected) {
|
|
|
521
579
|
{ label: "Ruby — ruby -c", value: "ruby -c" },
|
|
522
580
|
{ label: "PHP — php -l", value: "php -l" },
|
|
523
581
|
],
|
|
582
|
+
qualityGate: [
|
|
583
|
+
{ label: "Node.js — npm run prepush:check", value: "npm run prepush:check" },
|
|
584
|
+
{ label: "Node.js — pnpm prepush:check", value: "pnpm prepush:check" },
|
|
585
|
+
{ label: "Repository hook — bash .githooks/pre-push", value: "bash .githooks/pre-push" },
|
|
586
|
+
{ label: "Go — go test ./...", value: "go test ./..." },
|
|
587
|
+
{ label: "Make — make test", value: "make test" },
|
|
588
|
+
],
|
|
524
589
|
};
|
|
525
590
|
|
|
526
591
|
// Merge: skip any universal presets whose value already appears in detected
|
|
@@ -541,7 +606,7 @@ export function getCommandPresets(detected) {
|
|
|
541
606
|
* If the value isn't "auto", returns it unchanged.
|
|
542
607
|
*
|
|
543
608
|
* @param {string} value - The command value (may be "auto" or an actual command)
|
|
544
|
-
* @param {string} commandType - One of "test", "build", "lint", "syntaxCheck"
|
|
609
|
+
* @param {string} commandType - One of "test", "build", "lint", "syntaxCheck", "qualityGate"
|
|
545
610
|
* @param {string} rootDir - Project root for detection
|
|
546
611
|
* @returns {string} The resolved command
|
|
547
612
|
*/
|
|
@@ -552,7 +617,7 @@ export function resolveAutoCommand(value, commandType, rootDir) {
|
|
|
552
617
|
}
|
|
553
618
|
|
|
554
619
|
function emptyCommands() {
|
|
555
|
-
return { test: "", build: "", lint: "", syntaxCheck: "", typeCheck: "" };
|
|
620
|
+
return { test: "", build: "", lint: "", syntaxCheck: "", typeCheck: "", qualityGate: "" };
|
|
556
621
|
}
|
|
557
622
|
|
|
558
623
|
// Re-export the stack definitions for introspection
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
ensureTestRuntimeSandbox,
|
|
42
42
|
resolvePathForTestRuntime,
|
|
43
43
|
} from "../infra/test-runtime.mjs";
|
|
44
|
+
import { getTemplate } from "./workflow-templates.mjs";
|
|
44
45
|
|
|
45
46
|
// Lazy-loaded workspace manager for workspace-aware scheduling
|
|
46
47
|
let _workspaceManagerMod = null;
|
|
@@ -744,6 +745,34 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
744
745
|
return normalized || "";
|
|
745
746
|
}
|
|
746
747
|
|
|
748
|
+
_resolveTemplateDefaultVariable(def, key) {
|
|
749
|
+
if (!def || typeof def !== "object") return undefined;
|
|
750
|
+
if (def?.metadata?.templateState?.isCustomized === true) return undefined;
|
|
751
|
+
const templateId = String(def?.metadata?.installedFrom || "").trim();
|
|
752
|
+
if (!templateId) return undefined;
|
|
753
|
+
const template = getTemplate(templateId);
|
|
754
|
+
if (!template?.variables || typeof template.variables !== "object") return undefined;
|
|
755
|
+
return Object.prototype.hasOwnProperty.call(template.variables, key)
|
|
756
|
+
? template.variables[key]
|
|
757
|
+
: undefined;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
_applyResumeInputMigrations(def, data = {}) {
|
|
761
|
+
if (!data || typeof data !== "object") return data;
|
|
762
|
+
const next = { ...data };
|
|
763
|
+
const templateId = String(def?.metadata?.installedFrom || "").trim();
|
|
764
|
+
if (templateId === "template-task-lifecycle") {
|
|
765
|
+
const currentValue = next.prePrValidationCommand;
|
|
766
|
+
const nextDefault =
|
|
767
|
+
this._resolveTemplateDefaultVariable(def, "prePrValidationCommand")
|
|
768
|
+
?? def?.variables?.prePrValidationCommand;
|
|
769
|
+
if (currentValue === "npm run prepush:check" && nextDefault === "auto") {
|
|
770
|
+
next.prePrValidationCommand = nextDefault;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return next;
|
|
774
|
+
}
|
|
775
|
+
|
|
747
776
|
_resolveTaskTraceContext(ctx, node = null, result = null) {
|
|
748
777
|
const nodeTaskIdCandidate = (() => {
|
|
749
778
|
if (!node?.config || typeof node.config !== "object") return "";
|
|
@@ -1220,10 +1249,13 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
1220
1249
|
* @private
|
|
1221
1250
|
*/
|
|
1222
1251
|
async _executeInner(def, workflowId, inputData, opts) {
|
|
1223
|
-
|
|
1224
|
-
const ctx = new WorkflowContext({
|
|
1252
|
+
const initialData = this._applyResumeInputMigrations(def, {
|
|
1225
1253
|
...def.variables,
|
|
1226
1254
|
...inputData,
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
const ctx = new WorkflowContext({
|
|
1258
|
+
...initialData,
|
|
1227
1259
|
_workflowId: workflowId,
|
|
1228
1260
|
_workflowName: def.name,
|
|
1229
1261
|
});
|
|
@@ -1347,6 +1379,7 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
1347
1379
|
const originalData = { ...(originalRun.detail?.data || {}) };
|
|
1348
1380
|
delete originalData._workflowId;
|
|
1349
1381
|
delete originalData._workflowName;
|
|
1382
|
+
const retryData = this._applyResumeInputMigrations(def, originalData);
|
|
1350
1383
|
|
|
1351
1384
|
this.emit("run:retry", {
|
|
1352
1385
|
originalRunId: runId,
|
|
@@ -1356,7 +1389,7 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
1356
1389
|
});
|
|
1357
1390
|
|
|
1358
1391
|
if (mode === "from_scratch") {
|
|
1359
|
-
const ctx = await this.execute(workflowId,
|
|
1392
|
+
const ctx = await this.execute(workflowId, retryData, {
|
|
1360
1393
|
...retryOpts,
|
|
1361
1394
|
_isRetry: true,
|
|
1362
1395
|
_originalRunId: runId,
|
|
@@ -1373,7 +1406,7 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
1373
1406
|
// Build a fresh context but pre-seed completed node outputs.
|
|
1374
1407
|
const ctx = new WorkflowContext({
|
|
1375
1408
|
...def.variables,
|
|
1376
|
-
...
|
|
1409
|
+
...retryData,
|
|
1377
1410
|
_workflowId: workflowId,
|
|
1378
1411
|
_workflowName: def.name,
|
|
1379
1412
|
_retryOf: runId,
|
|
@@ -3240,7 +3273,9 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
3240
3273
|
const runDetailCache = new Map(); // runId → parsed detail
|
|
3241
3274
|
const latestByTaskId = new Map(); // taskId → run entry (highest startedAt)
|
|
3242
3275
|
|
|
3243
|
-
|
|
3276
|
+
const allRuns = this._readRunIndex();
|
|
3277
|
+
|
|
3278
|
+
for (const run of allRuns) {
|
|
3244
3279
|
const dp = resolve(this.runsDir, `${run.runId}.json`);
|
|
3245
3280
|
if (!existsSync(dp)) continue;
|
|
3246
3281
|
try {
|
|
@@ -3435,7 +3470,34 @@ export function getWorkflowEngine(opts = {}) {
|
|
|
3435
3470
|
_defaultEngine = new WorkflowEngine(engineOpts);
|
|
3436
3471
|
_defaultEngine.load();
|
|
3437
3472
|
} else if (opts && typeof opts === "object") {
|
|
3438
|
-
|
|
3473
|
+
const workflowDir = typeof opts.workflowDir === "string" && opts.workflowDir
|
|
3474
|
+
? resolve(opts.workflowDir)
|
|
3475
|
+
: null;
|
|
3476
|
+
const runsDir = typeof opts.runsDir === "string" && opts.runsDir
|
|
3477
|
+
? resolve(opts.runsDir)
|
|
3478
|
+
: null;
|
|
3479
|
+
const configDir = typeof opts.configDir === "string" && opts.configDir
|
|
3480
|
+
? resolve(opts.configDir)
|
|
3481
|
+
: null;
|
|
3482
|
+
const shouldReinitialize =
|
|
3483
|
+
(workflowDir && workflowDir !== _defaultEngine.workflowDir) ||
|
|
3484
|
+
(runsDir && runsDir !== _defaultEngine.runsDir) ||
|
|
3485
|
+
(configDir && configDir !== resolve(_defaultEngine._configDir || process.cwd()));
|
|
3486
|
+
|
|
3487
|
+
if (shouldReinitialize) {
|
|
3488
|
+
const engineOpts = {
|
|
3489
|
+
...opts,
|
|
3490
|
+
services: mergeWorkflowServices(_defaultEngine.services, opts.services || {}),
|
|
3491
|
+
};
|
|
3492
|
+
if (
|
|
3493
|
+
engineOpts.detectInterruptedRuns === undefined &&
|
|
3494
|
+
shouldDisableDefaultInterruptedRunDetection(engineOpts)
|
|
3495
|
+
) {
|
|
3496
|
+
engineOpts.detectInterruptedRuns = false;
|
|
3497
|
+
}
|
|
3498
|
+
_defaultEngine = new WorkflowEngine(engineOpts);
|
|
3499
|
+
_defaultEngine.load();
|
|
3500
|
+
} else if (opts.services && typeof opts.services === "object") {
|
|
3439
3501
|
_defaultEngine.services = mergeWorkflowServices(
|
|
3440
3502
|
_defaultEngine.services,
|
|
3441
3503
|
opts.services,
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
loadWorkflowContract,
|
|
45
45
|
validateWorkflowContract,
|
|
46
46
|
} from "./workflow-contract.mjs";
|
|
47
|
+
import { resolveAutoCommand } from "./project-detection.mjs";
|
|
47
48
|
import { loadConfig } from "../config/config.mjs";
|
|
48
49
|
import { fixGitConfigCorruption } from "../workspace/worktree-manager.mjs";
|
|
49
50
|
import { clearBlockedWorktreeIdentity, normalizeBaseBranch } from "../git/git-safety.mjs";
|
|
@@ -2509,10 +2510,15 @@ registerBuiltinNodeType("action.run_agent", {
|
|
|
2509
2510
|
) || "",
|
|
2510
2511
|
).trim();
|
|
2511
2512
|
const sessionId = resolvedSessionId || null;
|
|
2513
|
+
const storedSessionOwnerNodeId = String(ctx.data?._agentSessionNodeId || "").trim() || null;
|
|
2514
|
+
const hasExplicitSessionOverride =
|
|
2515
|
+
options.sessionId != null
|
|
2516
|
+
|| node.config?.sessionId != null
|
|
2517
|
+
|| (sessionId && !storedSessionOwnerNodeId);
|
|
2518
|
+
const canContinueStoredSession =
|
|
2519
|
+
!!sessionId && (hasExplicitSessionOverride || storedSessionOwnerNodeId === node.id);
|
|
2512
2520
|
const explicitTaskKey = String(ctx.resolve(node.config?.taskKey || "") || "").trim();
|
|
2513
|
-
const fallbackTaskKey =
|
|
2514
|
-
sessionId ||
|
|
2515
|
-
`${ctx.data?._workflowId || "workflow"}:${ctx.id}:${node.id}`;
|
|
2521
|
+
const fallbackTaskKey = `${ctx.data?._workflowId || "workflow"}:${ctx.id}:${node.id}`;
|
|
2516
2522
|
const recoveryTaskKey = options.taskKey || explicitTaskKey || fallbackTaskKey;
|
|
2517
2523
|
const autoRecover = options.autoRecover ?? (node.config?.autoRecover !== false);
|
|
2518
2524
|
const continueOnSession =
|
|
@@ -2612,7 +2618,7 @@ registerBuiltinNodeType("action.run_agent", {
|
|
|
2612
2618
|
if (
|
|
2613
2619
|
autoRecover &&
|
|
2614
2620
|
continueOnSession &&
|
|
2615
|
-
|
|
2621
|
+
canContinueStoredSession &&
|
|
2616
2622
|
typeof agentPool.continueSession === "function"
|
|
2617
2623
|
) {
|
|
2618
2624
|
ctx.log(node.id, `${passLabel} Recovery: continuing existing session ${sessionId}`.trim());
|
|
@@ -2706,6 +2712,8 @@ registerBuiltinNodeType("action.run_agent", {
|
|
|
2706
2712
|
if (persistSession && threadId) {
|
|
2707
2713
|
ctx.data.sessionId = threadId;
|
|
2708
2714
|
ctx.data.threadId = threadId;
|
|
2715
|
+
ctx.data._agentSessionNodeId = node.id;
|
|
2716
|
+
ctx.data._agentSessionTaskKey = recoveryTaskKey;
|
|
2709
2717
|
}
|
|
2710
2718
|
const digest = buildAgentExecutionDigest(result, streamLines, maxRetainedEvents);
|
|
2711
2719
|
|
|
@@ -2988,6 +2996,11 @@ registerBuiltinNodeType("action.run_command", {
|
|
|
2988
2996
|
env: { type: "object", description: "Environment variables passed to the command (supports templates)", additionalProperties: true },
|
|
2989
2997
|
timeoutMs: { type: "number", default: 300000 },
|
|
2990
2998
|
shell: { type: "string", default: "auto", enum: ["auto", "bash", "pwsh", "cmd"] },
|
|
2999
|
+
commandType: {
|
|
3000
|
+
type: "string",
|
|
3001
|
+
enum: ["test", "build", "lint", "syntaxCheck", "qualityGate"],
|
|
3002
|
+
description: "Optional auto-resolution category when command is set to 'auto'",
|
|
3003
|
+
},
|
|
2991
3004
|
captureOutput: { type: "boolean", default: true },
|
|
2992
3005
|
parseJson: { type: "boolean", default: false, description: "Parse JSON output automatically" },
|
|
2993
3006
|
failOnError: { type: "boolean", default: false, description: "Throw on non-zero exit status (enables workflow retries)" },
|
|
@@ -2996,8 +3009,13 @@ registerBuiltinNodeType("action.run_command", {
|
|
|
2996
3009
|
},
|
|
2997
3010
|
async execute(node, ctx) {
|
|
2998
3011
|
const resolvedCommand = ctx.resolve(node.config?.command || "");
|
|
2999
|
-
const command = normalizeLegacyWorkflowCommand(resolvedCommand);
|
|
3000
3012
|
const cwd = ctx.resolve(node.config?.cwd || ctx.data?.worktreePath || process.cwd());
|
|
3013
|
+
const commandType = typeof node.config?.commandType === "string" ? node.config.commandType.trim() : "";
|
|
3014
|
+
const autoCommandRoot = ctx.resolve(ctx.data?.repoRoot || cwd);
|
|
3015
|
+
const autoResolvedCommand = commandType
|
|
3016
|
+
? resolveAutoCommand(String(resolvedCommand || ""), commandType, autoCommandRoot)
|
|
3017
|
+
: resolvedCommand;
|
|
3018
|
+
const command = normalizeLegacyWorkflowCommand(autoResolvedCommand);
|
|
3001
3019
|
const resolvedEnvConfig = resolveWorkflowNodeValue(node.config?.env ?? {}, ctx);
|
|
3002
3020
|
const commandEnv = { ...process.env };
|
|
3003
3021
|
if (resolvedEnvConfig && typeof resolvedEnvConfig === "object" && !Array.isArray(resolvedEnvConfig)) {
|
|
@@ -3040,7 +3058,19 @@ registerBuiltinNodeType("action.run_command", {
|
|
|
3040
3058
|
};
|
|
3041
3059
|
const usedArgv = commandArgs.length > 0;
|
|
3042
3060
|
|
|
3043
|
-
if (command
|
|
3061
|
+
if (!command.trim()) {
|
|
3062
|
+
const reason =
|
|
3063
|
+
String(resolvedCommand || "").trim().toLowerCase() === "auto" && commandType
|
|
3064
|
+
? `No ${commandType} command detected for ${autoCommandRoot || cwd}`
|
|
3065
|
+
: "No command configured";
|
|
3066
|
+
ctx.log(node.id, reason);
|
|
3067
|
+
return { success: false, output: "", stderr: "", exitCode: null, error: reason };
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
if (autoResolvedCommand !== resolvedCommand) {
|
|
3071
|
+
ctx.log(node.id, `Resolved auto ${commandType} command: ${autoResolvedCommand}`);
|
|
3072
|
+
}
|
|
3073
|
+
if (command !== autoResolvedCommand) {
|
|
3044
3074
|
ctx.log(node.id, `Normalized legacy command for portability: ${command}`);
|
|
3045
3075
|
}
|
|
3046
3076
|
ctx.log(node.id, `Running: ${usedArgv ? `${command} ${commandArgs.join(" ")}`.trim() : command}`);
|
|
@@ -454,13 +454,32 @@ function createWorkflowTemplateState({ getTemplate, cloneTemplateDefinition }) {
|
|
|
454
454
|
return def;
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
-
function
|
|
457
|
+
function applyLegacyTemplateVariableMigrations(existing, template, variables, opts = {}) {
|
|
458
|
+
if (!variables || typeof variables !== "object") return variables;
|
|
459
|
+
const assumeUncustomized = opts.assumeUncustomized === true;
|
|
460
|
+
if (!assumeUncustomized && existing?.metadata?.templateState?.isCustomized === true) {
|
|
461
|
+
return variables;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const templateId = String(template?.id || existing?.metadata?.installedFrom || "").trim();
|
|
465
|
+
if (templateId === "template-task-lifecycle") {
|
|
466
|
+
const currentValue = existing?.variables?.prePrValidationCommand;
|
|
467
|
+
const nextDefault = template?.variables?.prePrValidationCommand;
|
|
468
|
+
if (currentValue === "npm run prepush:check" && nextDefault === "auto") {
|
|
469
|
+
variables.prePrValidationCommand = nextDefault;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return variables;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function makeUpdatedWorkflowFromTemplate(existing, template, mode = "replace", opts = {}) {
|
|
458
477
|
const templateClone = cloneTemplateDefinition(template);
|
|
459
478
|
const nowIso = new Date().toISOString();
|
|
460
|
-
const mergedVariables = {
|
|
479
|
+
const mergedVariables = applyLegacyTemplateVariableMigrations(existing, templateClone, {
|
|
461
480
|
...(templateClone.variables || {}),
|
|
462
481
|
...(existing.variables || {}),
|
|
463
|
-
};
|
|
482
|
+
}, opts);
|
|
464
483
|
const next = {
|
|
465
484
|
...templateClone,
|
|
466
485
|
id: mode === "copy" ? randomUUID() : existing.id,
|
|
@@ -500,7 +519,9 @@ function createWorkflowTemplateState({ getTemplate, cloneTemplateDefinition }) {
|
|
|
500
519
|
throw new Error("Workflow has custom changes; pass force=true to replace it");
|
|
501
520
|
}
|
|
502
521
|
|
|
503
|
-
const next = makeUpdatedWorkflowFromTemplate(hydrated, template, mode
|
|
522
|
+
const next = makeUpdatedWorkflowFromTemplate(hydrated, template, mode, {
|
|
523
|
+
assumeUncustomized: opts.assumeUncustomized === true,
|
|
524
|
+
});
|
|
504
525
|
return engine.save(next);
|
|
505
526
|
}
|
|
506
527
|
|
|
@@ -563,8 +584,13 @@ function createWorkflowTemplateState({ getTemplate, cloneTemplateDefinition }) {
|
|
|
563
584
|
|
|
564
585
|
const templateId = String(state.templateId || "").trim();
|
|
565
586
|
const shouldForceUpdate = templateId && forceUpdateTemplateIds.has(templateId);
|
|
587
|
+
const assumeUncustomized = previousState?.isCustomized !== true;
|
|
566
588
|
if (shouldForceUpdate) {
|
|
567
|
-
const saved = updateWorkflowFromTemplate(engine, def.id, {
|
|
589
|
+
const saved = updateWorkflowFromTemplate(engine, def.id, {
|
|
590
|
+
mode: "replace",
|
|
591
|
+
force: true,
|
|
592
|
+
assumeUncustomized,
|
|
593
|
+
});
|
|
568
594
|
result.autoUpdated += 1;
|
|
569
595
|
result.updatedWorkflowIds.push(saved.id);
|
|
570
596
|
result.forceUpdated.push(saved.id);
|
|
@@ -573,7 +599,11 @@ function createWorkflowTemplateState({ getTemplate, cloneTemplateDefinition }) {
|
|
|
573
599
|
|
|
574
600
|
const wasCustomized = previousState?.isCustomized === true;
|
|
575
601
|
if (autoUpdateUnmodified && state.updateAvailable === true && !wasCustomized) {
|
|
576
|
-
const saved = updateWorkflowFromTemplate(engine, def.id, {
|
|
602
|
+
const saved = updateWorkflowFromTemplate(engine, def.id, {
|
|
603
|
+
mode: "replace",
|
|
604
|
+
force: true,
|
|
605
|
+
assumeUncustomized: true,
|
|
606
|
+
});
|
|
577
607
|
result.autoUpdated += 1;
|
|
578
608
|
result.updatedWorkflowIds.push(saved.id);
|
|
579
609
|
}
|
|
@@ -967,7 +997,7 @@ export function expandTemplateGroups(templateIds) {
|
|
|
967
997
|
*/
|
|
968
998
|
/**
|
|
969
999
|
* Classify a variable key into a command category if it matches.
|
|
970
|
-
* Returns "test" | "build" | "lint" | "syntaxCheck" | null.
|
|
1000
|
+
* Returns "test" | "build" | "lint" | "syntaxCheck" | "qualityGate" | null.
|
|
971
1001
|
*/
|
|
972
1002
|
function classifyCommandVariable(key) {
|
|
973
1003
|
const k = String(key || "").toLowerCase();
|
|
@@ -975,6 +1005,7 @@ function classifyCommandVariable(key) {
|
|
|
975
1005
|
if (k.includes("buildcommand") || k.includes("build_command")) return "build";
|
|
976
1006
|
if (k.includes("lintcommand") || k.includes("lint_command") || k.includes("lintcmd")) return "lint";
|
|
977
1007
|
if (k.includes("syntaxcheck") || k.includes("syntax_check") || k.includes("typecheckcommand") || k.includes("type_check")) return "syntaxCheck";
|
|
1008
|
+
if (k.includes("preprvalidationcommand") || k.includes("pre_pr_validation_command") || k.includes("qualitygatecommand") || k.includes("quality_gate_command")) return "qualityGate";
|
|
978
1009
|
return null;
|
|
979
1010
|
}
|
|
980
1011
|
|
|
@@ -1058,6 +1089,7 @@ function inferVariableDescription(key, defaultValue) {
|
|
|
1058
1089
|
if (classifyCommandVariable(normalized) === "build") return "Build command for your project. Auto-detected from project files when available.";
|
|
1059
1090
|
if (classifyCommandVariable(normalized) === "lint") return "Lint/style check command. Auto-detected from project files when available.";
|
|
1060
1091
|
if (classifyCommandVariable(normalized) === "syntaxCheck") return "Syntax/compile check command. Auto-detected from project files when available.";
|
|
1092
|
+
if (classifyCommandVariable(normalized) === "qualityGate") return "Pre-PR validation command. Auto-detected from project files and repo hooks when available.";
|
|
1061
1093
|
if (typeof defaultValue === "boolean") return "Toggle this setting on or off.";
|
|
1062
1094
|
if (typeof defaultValue === "number") return "Numeric workflow setting.";
|
|
1063
1095
|
return "";
|
|
@@ -1395,5 +1427,3 @@ export function installRecommendedTemplates(engine, overridesById = {}) {
|
|
|
1395
1427
|
|
|
1396
1428
|
|
|
1397
1429
|
|
|
1398
|
-
|
|
1399
|
-
|
|
@@ -66,7 +66,7 @@ export const TASK_LIFECYCLE_TEMPLATE = {
|
|
|
66
66
|
defaultTargetBranch: "origin/main",
|
|
67
67
|
taskTimeoutMs: 21600000, // 6 hours
|
|
68
68
|
prePrValidationEnabled: true,
|
|
69
|
-
prePrValidationCommand: "
|
|
69
|
+
prePrValidationCommand: "auto",
|
|
70
70
|
autoMergeOnCreate: false,
|
|
71
71
|
autoMergeMethod: "squash",
|
|
72
72
|
maxRetries: 2,
|
|
@@ -235,6 +235,7 @@ export const TASK_LIFECYCLE_TEMPLATE = {
|
|
|
235
235
|
// ── SUCCESS PATH: Local quality gate before push/PR ──────────────────
|
|
236
236
|
node("pre-pr-validation", "action.run_command", "Pre-PR Validation", {
|
|
237
237
|
command: "{{prePrValidationCommand}}",
|
|
238
|
+
commandType: "qualityGate",
|
|
238
239
|
cwd: "{{worktreePath}}",
|
|
239
240
|
failOnError: false,
|
|
240
241
|
}, { x: -120, y: 1940 }),
|
|
@@ -826,4 +827,3 @@ export const VE_ORCHESTRATOR_LITE_TEMPLATE = {
|
|
|
826
827
|
},
|
|
827
828
|
},
|
|
828
829
|
};
|
|
829
|
-
|