bosun 0.40.3 → 0.40.4
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/README.md +4 -0
- package/cli.mjs +41 -2
- package/config/config.mjs +35 -15
- package/desktop/package.json +2 -2
- package/infra/monitor.mjs +44 -13
- package/infra/session-tracker.mjs +1 -0
- package/infra/sync-engine.mjs +6 -1
- package/infra/update-check.mjs +15 -7
- package/kanban/kanban-adapter.mjs +19 -4
- package/kanban/ve-orchestrator.ps1 +25 -0
- package/package.json +1 -1
- package/server/ui-server.mjs +385 -39
- package/ui/components/kanban-board.js +137 -9
- package/ui/components/shared.js +107 -45
- package/ui/demo-defaults.js +20 -20
- package/ui/modules/mui.js +600 -397
- package/ui/styles/kanban.css +66 -11
- package/ui/styles.monolith.css +89 -0
- package/ui/tabs/agents.js +194 -20
- package/ui/tabs/tasks.js +291 -70
- package/workflow/workflow-engine.mjs +0 -24
- package/workflow/workflow-nodes.mjs +219 -20
- package/workflow/workflow-templates.mjs +1 -1
- package/workflow-templates/task-batch.mjs +10 -10
- package/workspace/workspace-manager.mjs +11 -0
- package/workspace/worktree-manager.mjs +6 -0
|
@@ -25,6 +25,8 @@ import { randomUUID } from "node:crypto";
|
|
|
25
25
|
import { getAgentToolConfig, getEffectiveTools } from "../agent/agent-tool-config.mjs";
|
|
26
26
|
import { getToolsPromptBlock } from "../agent/agent-custom-tools.mjs";
|
|
27
27
|
import { buildRelevantSkillsPromptBlock, findRelevantSkills } from "../agent/bosun-skills.mjs";
|
|
28
|
+
import { getSessionTracker } from "../infra/session-tracker.mjs";
|
|
29
|
+
import { fixGitConfigCorruption } from "../workspace/worktree-manager.mjs";
|
|
28
30
|
|
|
29
31
|
const TAG = "[workflow-nodes]";
|
|
30
32
|
const PORTABLE_WORKTREE_COUNT_COMMAND = "node -e \"const cp=require('node:child_process');const wt=cp.execSync('git worktree list --porcelain',{encoding:'utf8'});const count=(wt.match(/^worktree /gm)||[]).length;process.stdout.write(String(count)+'\\\\n');\"";
|
|
@@ -40,6 +42,29 @@ const WORKFLOW_AGENT_EVENT_PREVIEW_LIMIT = (() => {
|
|
|
40
42
|
return Math.max(20, Math.min(500, Math.trunc(raw)));
|
|
41
43
|
})();
|
|
42
44
|
|
|
45
|
+
function makeIsolatedGitEnv(extra = {}) {
|
|
46
|
+
const env = { ...process.env, ...extra };
|
|
47
|
+
for (const key of [
|
|
48
|
+
"GIT_DIR",
|
|
49
|
+
"GIT_WORK_TREE",
|
|
50
|
+
"GIT_COMMON_DIR",
|
|
51
|
+
"GIT_INDEX_FILE",
|
|
52
|
+
"GIT_OBJECT_DIRECTORY",
|
|
53
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
54
|
+
"GIT_PREFIX",
|
|
55
|
+
]) {
|
|
56
|
+
delete env[key];
|
|
57
|
+
}
|
|
58
|
+
return env;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function execGitSync(command, options = {}) {
|
|
62
|
+
return execSync(command, {
|
|
63
|
+
...options,
|
|
64
|
+
env: makeIsolatedGitEnv(options.env),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
43
68
|
function trimLogText(value, max = 180) {
|
|
44
69
|
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
45
70
|
if (!text) return "";
|
|
@@ -1510,6 +1535,20 @@ registerNodeType("action.run_agent", {
|
|
|
1510
1535
|
const prompt = ctx.resolve(node.config?.prompt || "");
|
|
1511
1536
|
const sdk = node.config?.sdk || "auto";
|
|
1512
1537
|
const cwd = ctx.resolve(node.config?.cwd || ctx.data?.worktreePath || process.cwd());
|
|
1538
|
+
const trackedTaskId = String(
|
|
1539
|
+
ctx.data?.taskId ||
|
|
1540
|
+
ctx.data?.task?.id ||
|
|
1541
|
+
ctx.data?.taskDetail?.id ||
|
|
1542
|
+
ctx.resolve(node.config?.taskId || "") ||
|
|
1543
|
+
"",
|
|
1544
|
+
).trim();
|
|
1545
|
+
const trackedTaskTitle = String(
|
|
1546
|
+
ctx.data?.task?.title ||
|
|
1547
|
+
ctx.data?.taskDetail?.title ||
|
|
1548
|
+
ctx.data?.taskInfo?.title ||
|
|
1549
|
+
trackedTaskId ||
|
|
1550
|
+
"",
|
|
1551
|
+
).trim();
|
|
1513
1552
|
const agentProfileId = String(
|
|
1514
1553
|
ctx.resolve(node.config?.agentProfile || ctx.data?.agentProfile || ""),
|
|
1515
1554
|
).trim();
|
|
@@ -1637,6 +1676,43 @@ registerNodeType("action.run_agent", {
|
|
|
1637
1676
|
const maxRetainedEvents = Number.isFinite(Number(node.config?.maxRetainedEvents))
|
|
1638
1677
|
? Math.max(10, Math.min(500, Math.trunc(Number(node.config.maxRetainedEvents))))
|
|
1639
1678
|
: WORKFLOW_AGENT_EVENT_PREVIEW_LIMIT;
|
|
1679
|
+
const tracker = trackedTaskId ? getSessionTracker() : null;
|
|
1680
|
+
const trackedSessionType = trackedTaskId ? "task" : "flow";
|
|
1681
|
+
|
|
1682
|
+
if (tracker && trackedTaskId) {
|
|
1683
|
+
const existing = tracker.getSessionById(trackedTaskId);
|
|
1684
|
+
if (!existing) {
|
|
1685
|
+
tracker.createSession({
|
|
1686
|
+
id: trackedTaskId,
|
|
1687
|
+
type: "task",
|
|
1688
|
+
taskId: trackedTaskId,
|
|
1689
|
+
metadata: {
|
|
1690
|
+
title: trackedTaskTitle || trackedTaskId,
|
|
1691
|
+
workspaceId: String(ctx.data?.workspaceId || ctx.data?.activeWorkspace || "").trim() || undefined,
|
|
1692
|
+
workspaceDir: String(cwd || "").trim() || undefined,
|
|
1693
|
+
branch:
|
|
1694
|
+
String(
|
|
1695
|
+
ctx.data?.branch ||
|
|
1696
|
+
ctx.data?.task?.branchName ||
|
|
1697
|
+
ctx.data?.taskDetail?.branchName ||
|
|
1698
|
+
"",
|
|
1699
|
+
).trim() || undefined,
|
|
1700
|
+
},
|
|
1701
|
+
});
|
|
1702
|
+
} else {
|
|
1703
|
+
tracker.updateSessionStatus(trackedTaskId, "active");
|
|
1704
|
+
if (trackedTaskTitle) {
|
|
1705
|
+
tracker.renameSession(trackedTaskId, trackedTaskTitle);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
tracker.recordEvent(trackedTaskId, {
|
|
1709
|
+
role: "system",
|
|
1710
|
+
type: "system",
|
|
1711
|
+
content: `Workflow agent run started in ${cwd}`,
|
|
1712
|
+
timestamp: new Date().toISOString(),
|
|
1713
|
+
_sessionType: trackedSessionType,
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1640
1716
|
|
|
1641
1717
|
const launchExtra = {};
|
|
1642
1718
|
if (sessionId) launchExtra.resumeThreadId = sessionId;
|
|
@@ -1644,6 +1720,12 @@ registerNodeType("action.run_agent", {
|
|
|
1644
1720
|
if (modelOverride) launchExtra.model = modelOverride;
|
|
1645
1721
|
launchExtra.onEvent = (event) => {
|
|
1646
1722
|
try {
|
|
1723
|
+
if (tracker && trackedTaskId) {
|
|
1724
|
+
tracker.recordEvent(trackedTaskId, {
|
|
1725
|
+
...(event && typeof event === "object" ? event : { content: String(event || "") }),
|
|
1726
|
+
_sessionType: trackedSessionType,
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1647
1729
|
const line = summarizeAgentStreamEvent(event);
|
|
1648
1730
|
if (!line || line === lastStreamLog) return;
|
|
1649
1731
|
lastStreamLog = line;
|
|
@@ -1711,9 +1793,10 @@ registerNodeType("action.run_agent", {
|
|
|
1711
1793
|
timeoutMs,
|
|
1712
1794
|
maxRetries: sessionRetries,
|
|
1713
1795
|
maxContinues,
|
|
1714
|
-
sessionType:
|
|
1796
|
+
sessionType: trackedSessionType,
|
|
1715
1797
|
sdk: sdkOverride,
|
|
1716
1798
|
model: modelOverride,
|
|
1799
|
+
onEvent: launchExtra.onEvent,
|
|
1717
1800
|
});
|
|
1718
1801
|
}
|
|
1719
1802
|
|
|
@@ -1721,9 +1804,10 @@ registerNodeType("action.run_agent", {
|
|
|
1721
1804
|
ctx.log(node.id, `${passLabel} Recovery: launchOrResumeThread taskKey=${recoveryTaskKey}`.trim());
|
|
1722
1805
|
result = await agentPool.launchOrResumeThread(passPrompt, cwd, timeoutMs, {
|
|
1723
1806
|
taskKey: recoveryTaskKey,
|
|
1724
|
-
sessionType:
|
|
1807
|
+
sessionType: trackedSessionType,
|
|
1725
1808
|
sdk: sdkOverride,
|
|
1726
1809
|
model: modelOverride,
|
|
1810
|
+
onEvent: launchExtra.onEvent,
|
|
1727
1811
|
});
|
|
1728
1812
|
}
|
|
1729
1813
|
|
|
@@ -1736,6 +1820,24 @@ registerNodeType("action.run_agent", {
|
|
|
1736
1820
|
}
|
|
1737
1821
|
ctx.log(node.id, `${passLabel || "Agent"} completed: success=${success} streamEvents=${streamEventCount}`);
|
|
1738
1822
|
|
|
1823
|
+
if (tracker && trackedTaskId) {
|
|
1824
|
+
if (streamEventCount === 0) {
|
|
1825
|
+
const fallbackContent = success
|
|
1826
|
+
? String(result?.output || result?.message || "Agent run completed.").trim()
|
|
1827
|
+
: String(result?.error || "Agent run failed.").trim();
|
|
1828
|
+
if (fallbackContent) {
|
|
1829
|
+
tracker.recordEvent(trackedTaskId, {
|
|
1830
|
+
role: success ? "assistant" : "system",
|
|
1831
|
+
type: success ? "agent_message" : "error",
|
|
1832
|
+
content: fallbackContent,
|
|
1833
|
+
timestamp: new Date().toISOString(),
|
|
1834
|
+
_sessionType: trackedSessionType,
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
tracker.endSession(trackedTaskId, success ? "completed" : "failed");
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1739
1841
|
const threadId = result?.threadId || result?.sessionId || sessionId || null;
|
|
1740
1842
|
if (persistSession && threadId) {
|
|
1741
1843
|
ctx.data.sessionId = threadId;
|
|
@@ -2713,7 +2815,7 @@ registerNodeType("action.update_task_status", {
|
|
|
2713
2815
|
required: ["taskId", "status"],
|
|
2714
2816
|
},
|
|
2715
2817
|
async execute(node, ctx, engine) {
|
|
2716
|
-
|
|
2818
|
+
let taskId = ctx.resolve(node.config?.taskId || "");
|
|
2717
2819
|
const status = node.config?.status;
|
|
2718
2820
|
const kanban = engine.services?.kanban;
|
|
2719
2821
|
const workflowEvent = ctx.resolve(node.config?.workflowEvent || "");
|
|
@@ -2732,6 +2834,29 @@ registerNodeType("action.update_task_status", {
|
|
|
2732
2834
|
if (workflowData) updateOptions.workflowData = workflowData;
|
|
2733
2835
|
if (workflowDedupKey) updateOptions.workflowDedupKey = workflowDedupKey;
|
|
2734
2836
|
|
|
2837
|
+
if (isUnresolvedTemplateToken(taskId)) {
|
|
2838
|
+
const fallbackTaskId =
|
|
2839
|
+
ctx.data?.taskId ||
|
|
2840
|
+
ctx.data?.task?.id ||
|
|
2841
|
+
ctx.data?.task_id ||
|
|
2842
|
+
"";
|
|
2843
|
+
if (fallbackTaskId && !isUnresolvedTemplateToken(fallbackTaskId)) {
|
|
2844
|
+
taskId = String(fallbackTaskId);
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
if (!taskId || isUnresolvedTemplateToken(taskId)) {
|
|
2849
|
+
const unresolvedValue = String(taskId || node.config?.taskId || "(empty)");
|
|
2850
|
+
ctx.log(node.id, `Skipping update_task_status due unresolved taskId: ${unresolvedValue}`);
|
|
2851
|
+
return {
|
|
2852
|
+
success: false,
|
|
2853
|
+
skipped: true,
|
|
2854
|
+
error: "unresolved_task_id",
|
|
2855
|
+
taskId: unresolvedValue,
|
|
2856
|
+
status,
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2735
2860
|
if (kanban?.updateTaskStatus) {
|
|
2736
2861
|
await kanban.updateTaskStatus(taskId, status, updateOptions);
|
|
2737
2862
|
bindTaskContext(ctx, {
|
|
@@ -7004,6 +7129,48 @@ function cfgOrCtx(node, ctx, key, defaultVal = "") {
|
|
|
7004
7129
|
return defaultVal;
|
|
7005
7130
|
}
|
|
7006
7131
|
|
|
7132
|
+
function getWorkflowRuntimeState(ctx) {
|
|
7133
|
+
if (!ctx || typeof ctx !== "object") return {};
|
|
7134
|
+
if (!ctx.__workflowRuntimeState || typeof ctx.__workflowRuntimeState !== "object") {
|
|
7135
|
+
ctx.__workflowRuntimeState = {};
|
|
7136
|
+
}
|
|
7137
|
+
return ctx.__workflowRuntimeState;
|
|
7138
|
+
}
|
|
7139
|
+
|
|
7140
|
+
function isUnresolvedTemplateToken(value) {
|
|
7141
|
+
return /{{[^{}]+}}/.test(String(value || ""));
|
|
7142
|
+
}
|
|
7143
|
+
|
|
7144
|
+
function normalizeGitRefValue(value) {
|
|
7145
|
+
const text = String(value ?? "").trim();
|
|
7146
|
+
if (!text || isUnresolvedTemplateToken(text)) return "";
|
|
7147
|
+
const lowered = text.toLowerCase();
|
|
7148
|
+
if (lowered === "null" || lowered === "undefined") return "";
|
|
7149
|
+
return text;
|
|
7150
|
+
}
|
|
7151
|
+
|
|
7152
|
+
function pickGitRef(...candidates) {
|
|
7153
|
+
for (const candidate of candidates) {
|
|
7154
|
+
const normalized = normalizeGitRefValue(candidate);
|
|
7155
|
+
if (normalized) return normalized;
|
|
7156
|
+
}
|
|
7157
|
+
return "";
|
|
7158
|
+
}
|
|
7159
|
+
|
|
7160
|
+
function formatExecSyncError(err) {
|
|
7161
|
+
if (!err) return "unknown error";
|
|
7162
|
+
const detail = [err?.stderr, err?.stdout, err?.message]
|
|
7163
|
+
.map((entry) => String(entry || "").trim())
|
|
7164
|
+
.filter(Boolean)
|
|
7165
|
+
.join(" | ");
|
|
7166
|
+
return trimLogText(detail || String(err?.message || err), 420);
|
|
7167
|
+
}
|
|
7168
|
+
|
|
7169
|
+
function isExistingBranchWorktreeError(err) {
|
|
7170
|
+
const detail = formatExecSyncError(err).toLowerCase();
|
|
7171
|
+
return detail.includes("already exists") || detail.includes("is already checked out");
|
|
7172
|
+
}
|
|
7173
|
+
|
|
7007
7174
|
/**
|
|
7008
7175
|
* Anti-thrash state — module-scope to survive across workflow runs.
|
|
7009
7176
|
* Mirrors TaskExecutor._noCommitCounts / _skipUntil / _completedWithPR.
|
|
@@ -7544,11 +7711,18 @@ registerNodeType("action.claim_task", {
|
|
|
7544
7711
|
ctx.data._claimToken = token;
|
|
7545
7712
|
ctx.data._claimInstanceId = instanceId;
|
|
7546
7713
|
|
|
7547
|
-
|
|
7548
|
-
|
|
7714
|
+
const runtimeState = getWorkflowRuntimeState(ctx);
|
|
7715
|
+
// Start renewal timer (stored in non-serializable runtime state for cleanup by release_claim)
|
|
7716
|
+
const renewClaimFn =
|
|
7717
|
+
typeof claims.renewTaskClaim === "function"
|
|
7718
|
+
? claims.renewTaskClaim.bind(claims)
|
|
7719
|
+
: typeof claims.renewClaim === "function"
|
|
7720
|
+
? claims.renewClaim.bind(claims)
|
|
7721
|
+
: null;
|
|
7722
|
+
if (renewIntervalMs > 0 && renewClaimFn) {
|
|
7549
7723
|
const renewTimer = setInterval(async () => {
|
|
7550
7724
|
try {
|
|
7551
|
-
await
|
|
7725
|
+
await renewClaimFn({ taskId, claimToken: token, instanceId, ttlMinutes });
|
|
7552
7726
|
} catch (renewErr) {
|
|
7553
7727
|
const msg = renewErr?.message || String(renewErr);
|
|
7554
7728
|
const fatal = ["claimed_by_different_instance", "claim_token_mismatch",
|
|
@@ -7556,6 +7730,7 @@ registerNodeType("action.claim_task", {
|
|
|
7556
7730
|
if (fatal) {
|
|
7557
7731
|
ctx.log(node.id, `Claim renewal fatal: ${msg} — aborting task`);
|
|
7558
7732
|
clearInterval(renewTimer);
|
|
7733
|
+
runtimeState.claimRenewTimer = null;
|
|
7559
7734
|
ctx.data._claimRenewTimer = null;
|
|
7560
7735
|
// Signal abort to downstream nodes via context
|
|
7561
7736
|
ctx.data._claimStolen = true;
|
|
@@ -7566,7 +7741,9 @@ registerNodeType("action.claim_task", {
|
|
|
7566
7741
|
}, renewIntervalMs);
|
|
7567
7742
|
// Prevent timer from keeping the process alive
|
|
7568
7743
|
if (renewTimer.unref) renewTimer.unref();
|
|
7569
|
-
|
|
7744
|
+
runtimeState.claimRenewTimer = renewTimer;
|
|
7745
|
+
// Keep serialized context JSON-safe.
|
|
7746
|
+
ctx.data._claimRenewTimer = null;
|
|
7570
7747
|
}
|
|
7571
7748
|
|
|
7572
7749
|
ctx.log(node.id, `Task "${taskTitle}" claimed (ttl=${ttlMinutes}min, renew=${renewIntervalMs}ms)`);
|
|
@@ -7602,11 +7779,14 @@ registerNodeType("action.release_claim", {
|
|
|
7602
7779
|
const claimToken = cfgOrCtx(node, ctx, "claimToken") || ctx.data?._claimToken || "";
|
|
7603
7780
|
const instanceId = cfgOrCtx(node, ctx, "instanceId") || ctx.data?._claimInstanceId || "";
|
|
7604
7781
|
|
|
7605
|
-
// Always cancel the renewal timer first
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7782
|
+
// Always cancel the renewal timer first.
|
|
7783
|
+
const runtimeState = getWorkflowRuntimeState(ctx);
|
|
7784
|
+
const renewTimer = runtimeState.claimRenewTimer || ctx.data?._claimRenewTimer;
|
|
7785
|
+
if (renewTimer) {
|
|
7786
|
+
try { clearInterval(renewTimer); } catch { /* ok */ }
|
|
7609
7787
|
}
|
|
7788
|
+
runtimeState.claimRenewTimer = null;
|
|
7789
|
+
ctx.data._claimRenewTimer = null;
|
|
7610
7790
|
|
|
7611
7791
|
if (!taskId || !claimToken) {
|
|
7612
7792
|
ctx.log(node.id, `No claim to release for ${taskId || "(unknown)"}`);
|
|
@@ -7622,8 +7802,15 @@ registerNodeType("action.release_claim", {
|
|
|
7622
7802
|
ctx.data._claimInstanceId = null;
|
|
7623
7803
|
return { success: true, taskId, warning: initErr.message };
|
|
7624
7804
|
}
|
|
7805
|
+
const releaseClaimFn =
|
|
7806
|
+
typeof claims.releaseTaskClaim === "function"
|
|
7807
|
+
? claims.releaseTaskClaim.bind(claims)
|
|
7808
|
+
: typeof claims.releaseTask === "function"
|
|
7809
|
+
? claims.releaseTask.bind(claims)
|
|
7810
|
+
: null;
|
|
7625
7811
|
try {
|
|
7626
|
-
|
|
7812
|
+
if (!releaseClaimFn) throw new Error("no claim release function available");
|
|
7813
|
+
await releaseClaimFn({ taskId, claimToken, instanceId });
|
|
7627
7814
|
ctx.data._claimToken = null;
|
|
7628
7815
|
ctx.data._claimInstanceId = null;
|
|
7629
7816
|
ctx.log(node.id, `Claim released for ${taskId}`);
|
|
@@ -7742,7 +7929,9 @@ registerNodeType("action.acquire_worktree", {
|
|
|
7742
7929
|
const taskId = cfgOrCtx(node, ctx, "taskId");
|
|
7743
7930
|
const branch = cfgOrCtx(node, ctx, "branch");
|
|
7744
7931
|
const repoRoot = cfgOrCtx(node, ctx, "repoRoot") || process.cwd();
|
|
7745
|
-
const
|
|
7932
|
+
const baseBranchRaw = cfgOrCtx(node, ctx, "baseBranch", "origin/main");
|
|
7933
|
+
const defaultTargetBranch = cfgOrCtx(node, ctx, "defaultTargetBranch", "origin/main");
|
|
7934
|
+
const baseBranch = pickGitRef(baseBranchRaw, defaultTargetBranch, "origin/main", "main");
|
|
7746
7935
|
const fetchTimeout = node.config?.fetchTimeout ?? 30000;
|
|
7747
7936
|
const worktreeTimeout = node.config?.worktreeTimeout ?? 60000;
|
|
7748
7937
|
|
|
@@ -7799,17 +7988,24 @@ registerNodeType("action.acquire_worktree", {
|
|
|
7799
7988
|
`git worktree add "${worktreePath}" -b "${branch}" "${baseBranch}" 2>&1`,
|
|
7800
7989
|
{ cwd: repoRoot, encoding: "utf8", timeout: worktreeTimeout },
|
|
7801
7990
|
);
|
|
7802
|
-
} catch {
|
|
7803
|
-
|
|
7991
|
+
} catch (createErr) {
|
|
7992
|
+
if (!isExistingBranchWorktreeError(createErr)) {
|
|
7993
|
+
throw new Error(`Worktree creation failed: ${formatExecSyncError(createErr)}`);
|
|
7994
|
+
}
|
|
7995
|
+
// Branch already exists — attach worktree to existing branch.
|
|
7804
7996
|
try {
|
|
7805
7997
|
execSync(
|
|
7806
7998
|
`git worktree add "${worktreePath}" "${branch}" 2>&1`,
|
|
7807
7999
|
{ cwd: repoRoot, encoding: "utf8", timeout: worktreeTimeout },
|
|
7808
8000
|
);
|
|
7809
|
-
} catch (
|
|
7810
|
-
throw new Error(
|
|
8001
|
+
} catch (reuseErr) {
|
|
8002
|
+
throw new Error(
|
|
8003
|
+
`Worktree creation failed: ${formatExecSyncError(createErr)}; ` +
|
|
8004
|
+
`reuse failed: ${formatExecSyncError(reuseErr)}`,
|
|
8005
|
+
);
|
|
7811
8006
|
}
|
|
7812
8007
|
}
|
|
8008
|
+
fixGitConfigCorruption(repoRoot);
|
|
7813
8009
|
|
|
7814
8010
|
ctx.data.worktreePath = worktreePath;
|
|
7815
8011
|
ctx.data._worktreeCreated = true;
|
|
@@ -8204,7 +8400,7 @@ registerNodeType("action.detect_new_commits", {
|
|
|
8204
8400
|
// Get current HEAD
|
|
8205
8401
|
let postExecHead = "";
|
|
8206
8402
|
try {
|
|
8207
|
-
postExecHead =
|
|
8403
|
+
postExecHead = execGitSync("git rev-parse HEAD", {
|
|
8208
8404
|
cwd: worktreePath, encoding: "utf8", timeout: 5000,
|
|
8209
8405
|
}).trim();
|
|
8210
8406
|
} catch (err) {
|
|
@@ -8218,7 +8414,7 @@ registerNodeType("action.detect_new_commits", {
|
|
|
8218
8414
|
let hasUnpushed = false;
|
|
8219
8415
|
let commitCount = 0;
|
|
8220
8416
|
try {
|
|
8221
|
-
const log =
|
|
8417
|
+
const log = execGitSync(`git log --oneline ${baseBranch}..HEAD`, {
|
|
8222
8418
|
cwd: worktreePath, encoding: "utf8", timeout: 10000,
|
|
8223
8419
|
stdio: ["ignore", "pipe", "pipe"],
|
|
8224
8420
|
}).trim();
|
|
@@ -8232,7 +8428,7 @@ registerNodeType("action.detect_new_commits", {
|
|
|
8232
8428
|
let diffStats = null;
|
|
8233
8429
|
if (hasNewCommits || hasUnpushed) {
|
|
8234
8430
|
try {
|
|
8235
|
-
const statOutput =
|
|
8431
|
+
const statOutput = execGitSync(`git diff --stat ${baseBranch}..HEAD`, {
|
|
8236
8432
|
cwd: worktreePath, encoding: "utf8", timeout: 10000,
|
|
8237
8433
|
stdio: ["ignore", "pipe", "pipe"],
|
|
8238
8434
|
}).trim();
|
|
@@ -8602,3 +8798,6 @@ registerNodeType("action.web_search", {
|
|
|
8602
8798
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
8603
8799
|
|
|
8604
8800
|
export { registerNodeType, getNodeType, listNodeTypes } from "./workflow-engine.mjs";
|
|
8801
|
+
|
|
8802
|
+
|
|
8803
|
+
|
|
@@ -575,7 +575,7 @@ export const WORKFLOW_SETUP_PROFILES = Object.freeze({
|
|
|
575
575
|
"template-task-batch-processor",
|
|
576
576
|
]),
|
|
577
577
|
}),
|
|
578
|
-
|
|
578
|
+
workflowfirst: Object.freeze({
|
|
579
579
|
id: "workflowFirst",
|
|
580
580
|
name: "Workflow-First (Full)",
|
|
581
581
|
description:
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - TASK_BATCH_PR_TEMPLATE (batch → agent → PR shortcut)
|
|
11
11
|
*
|
|
12
12
|
* DAG overview:
|
|
13
|
-
* trigger.
|
|
13
|
+
* trigger.task_available
|
|
14
14
|
* → condition.expression (is coordinator or solo?)
|
|
15
15
|
* → action.run_command (list todo tasks)
|
|
16
16
|
* → loop.for_each (fan-out, maxConcurrent tasks at a time)
|
|
@@ -38,9 +38,8 @@ export const TASK_BATCH_PROCESSOR_TEMPLATE = {
|
|
|
38
38
|
category: "lifecycle",
|
|
39
39
|
enabled: true,
|
|
40
40
|
recommended: true,
|
|
41
|
-
trigger: "trigger.
|
|
41
|
+
trigger: "trigger.task_available",
|
|
42
42
|
variables: {
|
|
43
|
-
backlogThreshold: 3,
|
|
44
43
|
maxConcurrent: 3,
|
|
45
44
|
pollStatus: "todo",
|
|
46
45
|
maxBatchSize: 10,
|
|
@@ -48,9 +47,10 @@ export const TASK_BATCH_PROCESSOR_TEMPLATE = {
|
|
|
48
47
|
notifyChannel: "telegram",
|
|
49
48
|
},
|
|
50
49
|
nodes: [
|
|
51
|
-
// ── Trigger:
|
|
52
|
-
node("trigger", "trigger.
|
|
53
|
-
|
|
50
|
+
// ── Trigger: Tasks available for processing ──────────────────────────
|
|
51
|
+
node("trigger", "trigger.task_available", "Tasks Available?", {
|
|
52
|
+
maxParallel: "{{maxConcurrent}}",
|
|
53
|
+
pollIntervalMs: 60000,
|
|
54
54
|
status: "{{pollStatus}}",
|
|
55
55
|
}, { x: 400, y: 50 }),
|
|
56
56
|
|
|
@@ -141,9 +141,8 @@ export const TASK_BATCH_PR_TEMPLATE = {
|
|
|
141
141
|
category: "lifecycle",
|
|
142
142
|
enabled: true,
|
|
143
143
|
recommended: false,
|
|
144
|
-
trigger: "trigger.
|
|
144
|
+
trigger: "trigger.task_available",
|
|
145
145
|
variables: {
|
|
146
|
-
backlogThreshold: 3,
|
|
147
146
|
maxConcurrent: 2,
|
|
148
147
|
pollStatus: "todo",
|
|
149
148
|
maxBatchSize: 5,
|
|
@@ -153,8 +152,9 @@ export const TASK_BATCH_PR_TEMPLATE = {
|
|
|
153
152
|
},
|
|
154
153
|
nodes: [
|
|
155
154
|
// ── Trigger ──────────────────────────────────────────────────────────
|
|
156
|
-
node("trigger", "trigger.
|
|
157
|
-
|
|
155
|
+
node("trigger", "trigger.task_available", "Tasks Available?", {
|
|
156
|
+
maxParallel: "{{maxConcurrent}}",
|
|
157
|
+
pollIntervalMs: 60000,
|
|
158
158
|
status: "{{pollStatus}}",
|
|
159
159
|
}, { x: 400, y: 50 }),
|
|
160
160
|
|
|
@@ -403,6 +403,16 @@ export function listWorkspaces(configDir, opts = {}) {
|
|
|
403
403
|
const standardExists = existsSync(standardPath);
|
|
404
404
|
let effectivePath = standardPath;
|
|
405
405
|
let exists = standardExists;
|
|
406
|
+
const repoUrlRaw = String(repo.url || "").trim();
|
|
407
|
+
const looksLikeRemoteUrl =
|
|
408
|
+
/^https?:\/\//i.test(repoUrlRaw) ||
|
|
409
|
+
/^git@[^:]+:/i.test(repoUrlRaw) ||
|
|
410
|
+
/^ssh:\/\//i.test(repoUrlRaw);
|
|
411
|
+
const repoUrlPath = repoUrlRaw && !looksLikeRemoteUrl ? resolve(repoUrlRaw) : null;
|
|
412
|
+
if (!standardExists && repoUrlPath && existsSync(repoUrlPath)) {
|
|
413
|
+
effectivePath = repoUrlPath;
|
|
414
|
+
exists = true;
|
|
415
|
+
}
|
|
406
416
|
if (!standardExists && repoRootOverride) {
|
|
407
417
|
const altPath = resolve(repoRootOverride, repo.name);
|
|
408
418
|
if (existsSync(altPath)) {
|
|
@@ -1000,3 +1010,4 @@ export function initializeWorkspaces(configDir, opts = {}) {
|
|
|
1000
1010
|
|
|
1001
1011
|
return { workspaces: [], isNew: true };
|
|
1002
1012
|
}
|
|
1013
|
+
|
|
@@ -81,6 +81,12 @@ function fixGitConfigCorruption(repoRoot) {
|
|
|
81
81
|
timeout: 5000,
|
|
82
82
|
env: { ...process.env, ...GIT_ENV },
|
|
83
83
|
});
|
|
84
|
+
spawnSync("git", ["config", "--local", "--unset-all", "core.worktree"], {
|
|
85
|
+
cwd: repoRoot,
|
|
86
|
+
encoding: "utf8",
|
|
87
|
+
timeout: 5000,
|
|
88
|
+
env: { ...process.env, ...GIT_ENV },
|
|
89
|
+
});
|
|
84
90
|
}
|
|
85
91
|
} catch {
|
|
86
92
|
/* best-effort — don't crash on config repair */
|