bosun 0.43.0 → 0.43.1
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 +12 -0
- package/package.json +4 -4
- package/server/ui-server.mjs +326 -14
- package/ui/components/kanban-board.js +57 -5
- package/ui/demo-defaults.js +12 -12
- package/ui/modules/task-hierarchy.js +2 -2
- package/ui/tabs/agents.js +2 -2
- package/ui/tabs/tasks.js +7 -2
- package/workflow/workflow-engine.mjs +15 -3
- package/workflow/workflow-nodes/actions.mjs +35 -3
- package/workflow/workflow-nodes/agent.mjs +50 -8
- package/workflow-templates/agents.mjs +21 -3
- package/workflow-templates/planning.mjs +3 -4
- package/workflow-templates/task-batch.mjs +1 -1
package/infra/monitor.mjs
CHANGED
|
@@ -3188,6 +3188,18 @@ function isFlowReviewGateEnabled() {
|
|
|
3188
3188
|
return !isFalsyFlag(flowRequireReviewDefault);
|
|
3189
3189
|
}
|
|
3190
3190
|
|
|
3191
|
+
function isMonitorMonitorEnabled() {
|
|
3192
|
+
if (isMonitorTestRuntime) return false;
|
|
3193
|
+
|
|
3194
|
+
const explicit =
|
|
3195
|
+
process.env.DEVMODE_MONITOR_MONITOR_ENABLED ??
|
|
3196
|
+
process.env.DEVMODE_AUTO_CODE_FIX_ENABLED;
|
|
3197
|
+
if (explicit !== undefined && String(explicit).trim() !== "") {
|
|
3198
|
+
return !isFalsyFlag(explicit);
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
return false;
|
|
3202
|
+
}
|
|
3191
3203
|
|
|
3192
3204
|
function isSelfRestartWatcherEnabled() {
|
|
3193
3205
|
const devMode = isDevMode();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.1",
|
|
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 GitHub/Jira APIs, 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",
|
|
@@ -550,12 +550,12 @@
|
|
|
550
550
|
"@toast-ui/editor": "^3.2.2",
|
|
551
551
|
"@whiskeysockets/baileys": "^7.0.0-rc.9",
|
|
552
552
|
"ajv": "^8.18.0",
|
|
553
|
-
"dompurify": "^3.4.
|
|
553
|
+
"dompurify": "^3.4.2",
|
|
554
554
|
"es-module-shims": "^2.8.0",
|
|
555
555
|
"express": "^5.1.0",
|
|
556
|
-
"express-rate-limit": "^8.
|
|
556
|
+
"express-rate-limit": "^8.5.1",
|
|
557
557
|
"figures": "^6.1.0",
|
|
558
|
-
"hono": "^4.12.
|
|
558
|
+
"hono": "^4.12.18",
|
|
559
559
|
"htm": "3.1.1",
|
|
560
560
|
"ink": "^5.0.0",
|
|
561
561
|
"ink-text-input": "^6.0.0",
|
package/server/ui-server.mjs
CHANGED
|
@@ -2892,6 +2892,29 @@ function sanitizeTaskDiagnosticText(value, maxLength = 240) {
|
|
|
2892
2892
|
return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
|
|
2893
2893
|
}
|
|
2894
2894
|
|
|
2895
|
+
const LOW_SIGNAL_BLOCKED_REASONS = new Set([
|
|
2896
|
+
"ok",
|
|
2897
|
+
"okay",
|
|
2898
|
+
"success",
|
|
2899
|
+
"succeeded",
|
|
2900
|
+
"passed",
|
|
2901
|
+
"pass",
|
|
2902
|
+
"true",
|
|
2903
|
+
"done",
|
|
2904
|
+
"completed",
|
|
2905
|
+
"complete",
|
|
2906
|
+
"none",
|
|
2907
|
+
"null",
|
|
2908
|
+
"undefined",
|
|
2909
|
+
]);
|
|
2910
|
+
|
|
2911
|
+
function normalizeBlockedReasonForDisplay(value, maxLength = 280) {
|
|
2912
|
+
const text = sanitizeTaskDiagnosticText(value, maxLength);
|
|
2913
|
+
if (!text) return "";
|
|
2914
|
+
if (LOW_SIGNAL_BLOCKED_REASONS.has(text.toLowerCase())) return "";
|
|
2915
|
+
return text;
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2895
2918
|
const TASK_LOG_DIAGNOSTICS_TAIL_BYTES = 256 * 1024;
|
|
2896
2919
|
const TASK_LOG_DIAGNOSTICS_CACHE_MS = 5000;
|
|
2897
2920
|
|
|
@@ -2983,7 +3006,7 @@ function collectTaskWorkflowRunDiagnostics(task, limit = 8) {
|
|
|
2983
3006
|
|| worktreeFailure?.failureKind
|
|
2984
3007
|
|| "",
|
|
2985
3008
|
).trim();
|
|
2986
|
-
const errorText =
|
|
3009
|
+
const errorText = normalizeBlockedReasonForDisplay(
|
|
2987
3010
|
worktreeFailure?.error
|
|
2988
3011
|
|| meta?.error
|
|
2989
3012
|
|| meta?.blockedReason
|
|
@@ -2991,10 +3014,21 @@ function collectTaskWorkflowRunDiagnostics(task, limit = 8) {
|
|
|
2991
3014
|
|| "",
|
|
2992
3015
|
320,
|
|
2993
3016
|
);
|
|
2994
|
-
const blockedReason =
|
|
2995
|
-
worktreeFailure?.blockedReason
|
|
3017
|
+
const blockedReason = normalizeBlockedReasonForDisplay(
|
|
3018
|
+
worktreeFailure?.blockedReason
|
|
3019
|
+
|| meta?.blockedReason
|
|
3020
|
+
|| run?.blockedReason
|
|
3021
|
+
|| "",
|
|
2996
3022
|
280,
|
|
2997
3023
|
);
|
|
3024
|
+
const status = String(run?.status || "").trim().toLowerCase();
|
|
3025
|
+
const diagnosticText = [
|
|
3026
|
+
failureKind,
|
|
3027
|
+
errorText,
|
|
3028
|
+
blockedReason,
|
|
3029
|
+
run?.summary,
|
|
3030
|
+
run?.message,
|
|
3031
|
+
].filter(Boolean).join("\n");
|
|
2998
3032
|
const isWorktreeFailure =
|
|
2999
3033
|
failureKind === "branch_refresh_conflict"
|
|
3000
3034
|
|| failureKind === "worktree_acquisition_failed"
|
|
@@ -3002,17 +3036,29 @@ function collectTaskWorkflowRunDiagnostics(task, limit = 8) {
|
|
|
3002
3036
|
|| /worktree/i.test(errorText)
|
|
3003
3037
|
|| /refresh conflict/i.test(blockedReason)
|
|
3004
3038
|
|| /managed worktree/i.test(errorText);
|
|
3005
|
-
|
|
3039
|
+
const isWorkflowFailure =
|
|
3040
|
+
isWorktreeFailure
|
|
3041
|
+
|| ["failed", "blocked", "error", "cancelled", "canceled"].includes(status)
|
|
3042
|
+
|| /pre-?pr validation failed/i.test(diagnosticText)
|
|
3043
|
+
|| /prepush/i.test(diagnosticText)
|
|
3044
|
+
|| /failed to push/i.test(diagnosticText)
|
|
3045
|
+
|| /push failed/i.test(diagnosticText)
|
|
3046
|
+
|| /create-pr/i.test(diagnosticText)
|
|
3047
|
+
|| /retry-pr-ok/i.test(diagnosticText)
|
|
3048
|
+
|| /vitest-pool/i.test(diagnosticText)
|
|
3049
|
+
|| /Worker exited unexpectedly/i.test(diagnosticText);
|
|
3050
|
+
if (!isWorkflowFailure) continue;
|
|
3006
3051
|
relevant.push({
|
|
3007
3052
|
source: "workflow-run",
|
|
3008
3053
|
runId: String(run?.runId || "").trim() || null,
|
|
3009
3054
|
workflowId: String(run?.workflowId || "").trim() || null,
|
|
3010
3055
|
workflowName: String(run?.workflowName || "").trim() || null,
|
|
3011
|
-
message: errorText || blockedReason ||
|
|
3056
|
+
message: errorText || blockedReason || `Workflow run ${status || "failed"}.`,
|
|
3012
3057
|
reason: blockedReason || null,
|
|
3013
3058
|
failureKind: failureKind || null,
|
|
3014
3059
|
timestamp: run?.endedAt || run?.startedAt || null,
|
|
3015
3060
|
kind: "workflow-run",
|
|
3061
|
+
diagnosticCategory: isWorktreeFailure ? "worktree_failure" : "workflow_failure",
|
|
3016
3062
|
retryable: worktreeFailure?.retryable ?? null,
|
|
3017
3063
|
retryAt: worktreeFailure?.retryAt || null,
|
|
3018
3064
|
recordedAt: worktreeFailure?.recordedAt || null,
|
|
@@ -3037,10 +3083,14 @@ async function collectTaskLogDiagnostics(task, workspaceDir = "", limit = 8) {
|
|
|
3037
3083
|
if (!candidate || !existsSync(candidate) || logPaths.includes(candidate)) return;
|
|
3038
3084
|
logPaths.push(candidate);
|
|
3039
3085
|
};
|
|
3086
|
+
pushLogPath(resolve(__dirname, "..", ".bosun", "logs", "monitor-error.log"));
|
|
3087
|
+
pushLogPath(resolve(__dirname, "..", ".bosun", "logs", "monitor.log"));
|
|
3040
3088
|
if (workspaceDir) {
|
|
3041
3089
|
pushLogPath(resolve(workspaceDir, ".bosun", "logs", "monitor-error.log"));
|
|
3042
3090
|
pushLogPath(resolve(workspaceDir, ".bosun", "logs", "monitor.log"));
|
|
3043
3091
|
}
|
|
3092
|
+
pushLogPath(resolve(process.cwd(), ".bosun", "logs", "monitor-error.log"));
|
|
3093
|
+
pushLogPath(resolve(process.cwd(), ".bosun", "logs", "monitor.log"));
|
|
3044
3094
|
pushLogPath(resolve(repoRoot, ".bosun", "logs", "monitor-error.log"));
|
|
3045
3095
|
pushLogPath(resolve(repoRoot, ".bosun", "logs", "monitor.log"));
|
|
3046
3096
|
|
|
@@ -3117,7 +3167,7 @@ async function buildTaskBlockedContext(task, options = {}) {
|
|
|
3117
3167
|
? options.canStart
|
|
3118
3168
|
: null;
|
|
3119
3169
|
const normalizedStatus = normalizeTaskStatusKey(currentTask?.status);
|
|
3120
|
-
const explicitReason =
|
|
3170
|
+
const explicitReason = normalizeBlockedReasonForDisplay(
|
|
3121
3171
|
currentTask?.blockedReason
|
|
3122
3172
|
|| currentTask?.meta?.worktreeFailure?.blockedReason
|
|
3123
3173
|
|| currentTask?.meta?.autoRecovery?.error
|
|
@@ -3133,7 +3183,7 @@ async function buildTaskBlockedContext(task, options = {}) {
|
|
|
3133
3183
|
headline: "This task cannot start because one or more dependencies are not done yet.",
|
|
3134
3184
|
summary: "Bosun will not dispatch this task until every blocking dependency below is resolved.",
|
|
3135
3185
|
recommendation: "Complete or unblock the listed dependencies, then dispatch this task again.",
|
|
3136
|
-
reason: explicitReason ||
|
|
3186
|
+
reason: explicitReason || normalizeBlockedReasonForDisplay(canStart?.reason || ""),
|
|
3137
3187
|
workflowRunCount: 0,
|
|
3138
3188
|
prePrValidationFailureCount: 0,
|
|
3139
3189
|
worktreeFailureCount: 0,
|
|
@@ -3163,6 +3213,7 @@ async function buildTaskBlockedContext(task, options = {}) {
|
|
|
3163
3213
|
6,
|
|
3164
3214
|
);
|
|
3165
3215
|
const hasPlannerCorruption = /planner payload corrupted/i.test(explicitReason);
|
|
3216
|
+
const latestWorkflowEvidence = workflowRunEvidence[workflowRunEvidence.length - 1] || null;
|
|
3166
3217
|
const repairArtifacts =
|
|
3167
3218
|
currentTask?.meta?.worktreeFailure?.repairArtifacts
|
|
3168
3219
|
&& typeof currentTask.meta.worktreeFailure.repairArtifacts === "object"
|
|
@@ -3170,7 +3221,7 @@ async function buildTaskBlockedContext(task, options = {}) {
|
|
|
3170
3221
|
: null;
|
|
3171
3222
|
const hasWorktreeFailure =
|
|
3172
3223
|
Boolean(currentTask?.meta?.worktreeFailure) ||
|
|
3173
|
-
workflowRunEvidence.
|
|
3224
|
+
workflowRunEvidence.some((entry) => entry?.diagnosticCategory === "worktree_failure") ||
|
|
3174
3225
|
logDiagnostics.counts.worktreeFailed > 0 ||
|
|
3175
3226
|
timelineEvidence.some((entry) => /worktree failed/i.test(String(entry?.message || "")));
|
|
3176
3227
|
|
|
@@ -3200,12 +3251,16 @@ async function buildTaskBlockedContext(task, options = {}) {
|
|
|
3200
3251
|
} else if (normalizedStatus === "blocked") {
|
|
3201
3252
|
category = "blocked";
|
|
3202
3253
|
headline = "This task is blocked.";
|
|
3203
|
-
summary = explicitReason
|
|
3254
|
+
summary = explicitReason
|
|
3255
|
+
|| (latestWorkflowEvidence?.message
|
|
3256
|
+
? `Latest workflow failure: ${latestWorkflowEvidence.message}`
|
|
3257
|
+
: "Bosun marked this task as blocked, but the original blocked reason was not persisted.");
|
|
3204
3258
|
recommendation = "Review the recent workflow evidence below. After the underlying issue is fixed, move the task back to todo to clear the block and retry it.";
|
|
3205
3259
|
} else if (canStart?.canStart === false) {
|
|
3206
3260
|
category = "start_guard_blocked";
|
|
3207
3261
|
headline = "This task is currently not startable.";
|
|
3208
|
-
summary =
|
|
3262
|
+
summary = normalizeBlockedReasonForDisplay(canStart?.reason || "")
|
|
3263
|
+
|| "Bosun start guards rejected dispatch for this task.";
|
|
3209
3264
|
recommendation = "Resolve the blocking condition below before dispatching the task.";
|
|
3210
3265
|
} else {
|
|
3211
3266
|
return null;
|
|
@@ -3217,10 +3272,13 @@ async function buildTaskBlockedContext(task, options = {}) {
|
|
|
3217
3272
|
headline,
|
|
3218
3273
|
summary,
|
|
3219
3274
|
recommendation,
|
|
3220
|
-
reason: explicitReason ||
|
|
3275
|
+
reason: explicitReason || normalizeBlockedReasonForDisplay(canStart?.reason || ""),
|
|
3221
3276
|
workflowRunCount: workflowRuns.length,
|
|
3222
3277
|
prePrValidationFailureCount: logDiagnostics.counts.prePrValidationFailed,
|
|
3223
|
-
worktreeFailureCount: Math.max(
|
|
3278
|
+
worktreeFailureCount: Math.max(
|
|
3279
|
+
logDiagnostics.counts.worktreeFailed,
|
|
3280
|
+
workflowRunEvidence.filter((entry) => entry?.diagnosticCategory === "worktree_failure").length,
|
|
3281
|
+
),
|
|
3224
3282
|
blockedTransitionCount: logDiagnostics.counts.blockedTransitions,
|
|
3225
3283
|
createPrFailureCount: logDiagnostics.counts.createPrFailed,
|
|
3226
3284
|
blockedBy: Array.isArray(canStart?.blockedBy) ? canStart.blockedBy : [],
|
|
@@ -3232,6 +3290,10 @@ async function buildTaskBlockedContext(task, options = {}) {
|
|
|
3232
3290
|
};
|
|
3233
3291
|
}
|
|
3234
3292
|
|
|
3293
|
+
export async function _testBuildTaskBlockedContext(task, options = {}) {
|
|
3294
|
+
return buildTaskBlockedContext(task, options);
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3235
3297
|
function buildTaskMetaPatch(previousMeta, metadataPatchMeta, options = {}) {
|
|
3236
3298
|
const clearBlockedState = options.clearBlockedState === true;
|
|
3237
3299
|
const nextMeta = previousMeta && typeof previousMeta === "object"
|
|
@@ -5254,10 +5316,103 @@ function extractImportedTaskList(body = null) {
|
|
|
5254
5316
|
return null;
|
|
5255
5317
|
}
|
|
5256
5318
|
|
|
5319
|
+
function isPlainObject(value) {
|
|
5320
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
5321
|
+
}
|
|
5322
|
+
|
|
5323
|
+
function mergeImportedTaskCollection(baseEntries = [], incomingEntries = [], keyBuilder) {
|
|
5324
|
+
const merged = [];
|
|
5325
|
+
const indexByKey = new Map();
|
|
5326
|
+
const appendEntry = (entry) => {
|
|
5327
|
+
if (!entry || typeof entry !== "object") return;
|
|
5328
|
+
const key = typeof keyBuilder === "function" ? String(keyBuilder(entry) || "").trim() : "";
|
|
5329
|
+
if (key && indexByKey.has(key)) {
|
|
5330
|
+
merged[indexByKey.get(key)] = { ...merged[indexByKey.get(key)], ...entry };
|
|
5331
|
+
return;
|
|
5332
|
+
}
|
|
5333
|
+
merged.push(entry);
|
|
5334
|
+
if (key) indexByKey.set(key, merged.length - 1);
|
|
5335
|
+
};
|
|
5336
|
+
for (const entry of Array.isArray(baseEntries) ? baseEntries : []) appendEntry(entry);
|
|
5337
|
+
for (const entry of Array.isArray(incomingEntries) ? incomingEntries : []) appendEntry(entry);
|
|
5338
|
+
return merged;
|
|
5339
|
+
}
|
|
5340
|
+
|
|
5341
|
+
function mergeImportedTaskTimeline(baseEntries = [], incomingEntries = []) {
|
|
5342
|
+
return mergeImportedTaskCollection(baseEntries, incomingEntries, (entry) => (
|
|
5343
|
+
entry?.id
|
|
5344
|
+
|| [
|
|
5345
|
+
entry?.type,
|
|
5346
|
+
entry?.source,
|
|
5347
|
+
entry?.at,
|
|
5348
|
+
entry?.status,
|
|
5349
|
+
entry?.fromStatus,
|
|
5350
|
+
entry?.toStatus,
|
|
5351
|
+
entry?.message,
|
|
5352
|
+
].map((value) => String(value || "").trim()).join("|")
|
|
5353
|
+
));
|
|
5354
|
+
}
|
|
5355
|
+
|
|
5356
|
+
function mergeImportedTaskStatusHistory(baseEntries = [], incomingEntries = []) {
|
|
5357
|
+
return mergeImportedTaskCollection(baseEntries, incomingEntries, (entry) => [
|
|
5358
|
+
entry?.status,
|
|
5359
|
+
entry?.timestamp,
|
|
5360
|
+
entry?.source,
|
|
5361
|
+
].map((value) => String(value || "").trim()).join("|"));
|
|
5362
|
+
}
|
|
5363
|
+
|
|
5364
|
+
function mergeImportedTaskMeta(currentMeta = {}, incomingMeta = {}) {
|
|
5365
|
+
const base = isPlainObject(currentMeta) ? currentMeta : {};
|
|
5366
|
+
const incoming = isPlainObject(incomingMeta) ? incomingMeta : {};
|
|
5367
|
+
const merged = { ...base };
|
|
5368
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
5369
|
+
if (isPlainObject(value) && isPlainObject(base[key])) {
|
|
5370
|
+
merged[key] = mergeImportedTaskMeta(base[key], value);
|
|
5371
|
+
} else {
|
|
5372
|
+
merged[key] = value;
|
|
5373
|
+
}
|
|
5374
|
+
}
|
|
5375
|
+
return merged;
|
|
5376
|
+
}
|
|
5377
|
+
|
|
5378
|
+
function buildImportedTaskPatch(currentTask = {}, importedTask = {}, mode = "merge") {
|
|
5379
|
+
const patch = importedTask && typeof importedTask === "object" ? { ...importedTask } : {};
|
|
5380
|
+
if (mode !== "merge") return patch;
|
|
5381
|
+
|
|
5382
|
+
if (Object.prototype.hasOwnProperty.call(importedTask, "workflowRuns")
|
|
5383
|
+
&& Array.isArray(importedTask.workflowRuns)) {
|
|
5384
|
+
patch.workflowRuns = mergeTaskWorkflowRuns(
|
|
5385
|
+
Array.isArray(currentTask?.workflowRuns) ? currentTask.workflowRuns : [],
|
|
5386
|
+
importedTask.workflowRuns,
|
|
5387
|
+
200,
|
|
5388
|
+
);
|
|
5389
|
+
}
|
|
5390
|
+
if (Object.prototype.hasOwnProperty.call(importedTask, "timeline")
|
|
5391
|
+
&& Array.isArray(importedTask.timeline)) {
|
|
5392
|
+
patch.timeline = mergeImportedTaskTimeline(
|
|
5393
|
+
Array.isArray(currentTask?.timeline) ? currentTask.timeline : [],
|
|
5394
|
+
importedTask.timeline,
|
|
5395
|
+
);
|
|
5396
|
+
}
|
|
5397
|
+
if (Object.prototype.hasOwnProperty.call(importedTask, "statusHistory")
|
|
5398
|
+
&& Array.isArray(importedTask.statusHistory)) {
|
|
5399
|
+
patch.statusHistory = mergeImportedTaskStatusHistory(
|
|
5400
|
+
Array.isArray(currentTask?.statusHistory) ? currentTask.statusHistory : [],
|
|
5401
|
+
importedTask.statusHistory,
|
|
5402
|
+
);
|
|
5403
|
+
}
|
|
5404
|
+
if (Object.prototype.hasOwnProperty.call(importedTask, "meta")
|
|
5405
|
+
&& isPlainObject(importedTask.meta)) {
|
|
5406
|
+
patch.meta = mergeImportedTaskMeta(currentTask?.meta, importedTask.meta);
|
|
5407
|
+
}
|
|
5408
|
+
return patch;
|
|
5409
|
+
}
|
|
5410
|
+
|
|
5257
5411
|
async function importInternalTaskStateSnapshot(body = {}) {
|
|
5258
5412
|
const taskStore = await ensureTaskStoreApi();
|
|
5259
5413
|
const addTaskFn = typeof taskStore?.addTask === "function" ? taskStore.addTask : null;
|
|
5260
5414
|
const updateTaskFn = typeof taskStore?.updateTask === "function" ? taskStore.updateTask : null;
|
|
5415
|
+
const getTaskFn = typeof taskStore?.getTask === "function" ? taskStore.getTask : null;
|
|
5261
5416
|
if (!addTaskFn || !updateTaskFn) {
|
|
5262
5417
|
throw new Error("Internal task store import is unavailable");
|
|
5263
5418
|
}
|
|
@@ -5301,12 +5456,19 @@ async function importInternalTaskStateSnapshot(body = {}) {
|
|
|
5301
5456
|
|
|
5302
5457
|
try {
|
|
5303
5458
|
if (existingById.has(taskId)) {
|
|
5304
|
-
|
|
5459
|
+
const currentTask = existingById.get(taskId) || {};
|
|
5460
|
+
const patch = buildImportedTaskPatch(currentTask, task, mode);
|
|
5461
|
+
updateTaskFn(taskId, patch);
|
|
5462
|
+
if (getTaskFn) {
|
|
5463
|
+
existingById.set(taskId, getTaskFn(taskId) || { ...currentTask, ...patch });
|
|
5464
|
+
} else {
|
|
5465
|
+
existingById.set(taskId, { ...currentTask, ...patch });
|
|
5466
|
+
}
|
|
5305
5467
|
summary.updated += 1;
|
|
5306
5468
|
results.push({ id: taskId, status: "updated" });
|
|
5307
5469
|
} else {
|
|
5308
5470
|
addTaskFn(task);
|
|
5309
|
-
existingById.set(taskId, task);
|
|
5471
|
+
existingById.set(taskId, getTaskFn ? (getTaskFn(taskId) || task) : task);
|
|
5310
5472
|
summary.created += 1;
|
|
5311
5473
|
results.push({ id: taskId, status: "created" });
|
|
5312
5474
|
}
|
|
@@ -19798,6 +19960,156 @@ async function handleApi(req, res, url) {
|
|
|
19798
19960
|
return;
|
|
19799
19961
|
}
|
|
19800
19962
|
|
|
19963
|
+
if (path === "/api/executor/dispatch" && req.method === "POST") {
|
|
19964
|
+
try {
|
|
19965
|
+
const body = await readJsonBody(req);
|
|
19966
|
+
const prompt = String(body?.prompt || "").trim();
|
|
19967
|
+
let taskId = String(body?.taskId || body?.id || "").trim();
|
|
19968
|
+
const sdk = typeof body?.sdk === "string" ? body.sdk.trim() : "";
|
|
19969
|
+
const model = typeof body?.model === "string" ? body.model.trim() : "";
|
|
19970
|
+
const executor = uiDeps.getInternalExecutor?.();
|
|
19971
|
+
if (!executor) {
|
|
19972
|
+
jsonResponse(res, 400, {
|
|
19973
|
+
ok: false,
|
|
19974
|
+
error: "Internal executor not enabled. Set EXECUTOR_MODE=internal.",
|
|
19975
|
+
});
|
|
19976
|
+
return;
|
|
19977
|
+
}
|
|
19978
|
+
if (!taskId && !prompt) {
|
|
19979
|
+
jsonResponse(res, 400, { ok: false, error: "taskId or prompt is required" });
|
|
19980
|
+
return;
|
|
19981
|
+
}
|
|
19982
|
+
|
|
19983
|
+
const adapter = getKanbanAdapter();
|
|
19984
|
+
let task = taskId && typeof adapter.getTask === "function"
|
|
19985
|
+
? await adapter.getTask(taskId)
|
|
19986
|
+
: null;
|
|
19987
|
+
if (taskId && !task) {
|
|
19988
|
+
jsonResponse(res, 404, { ok: false, error: "Task not found." });
|
|
19989
|
+
return;
|
|
19990
|
+
}
|
|
19991
|
+
|
|
19992
|
+
let createdFromPrompt = false;
|
|
19993
|
+
if (!taskId) {
|
|
19994
|
+
const activeWorkspace = getActiveManagedWorkspace(resolveUiConfigDir());
|
|
19995
|
+
const defaultRepository =
|
|
19996
|
+
activeWorkspace?.activeRepo ||
|
|
19997
|
+
activeWorkspace?.repos?.find((repo) => repo.primary)?.name ||
|
|
19998
|
+
activeWorkspace?.repos?.[0]?.name ||
|
|
19999
|
+
"";
|
|
20000
|
+
const titleLine = prompt.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || "Fleet prompt dispatch";
|
|
20001
|
+
const taskData = {
|
|
20002
|
+
title: titleLine.length > 96 ? `${titleLine.slice(0, 93)}...` : titleLine,
|
|
20003
|
+
description: prompt,
|
|
20004
|
+
status: "todo",
|
|
20005
|
+
...(activeWorkspace?.id ? { workspace: activeWorkspace.id } : {}),
|
|
20006
|
+
...(defaultRepository ? { repository: defaultRepository } : {}),
|
|
20007
|
+
meta: {
|
|
20008
|
+
...(activeWorkspace?.id ? { workspace: activeWorkspace.id } : {}),
|
|
20009
|
+
...(defaultRepository ? { repository: defaultRepository } : {}),
|
|
20010
|
+
source: "fleet-dispatch",
|
|
20011
|
+
},
|
|
20012
|
+
};
|
|
20013
|
+
const createdRaw = await adapter.createTask("", taskData);
|
|
20014
|
+
task = withTaskMetadataTopLevel(createdRaw);
|
|
20015
|
+
taskId = String(task?.id || "").trim();
|
|
20016
|
+
createdFromPrompt = true;
|
|
20017
|
+
}
|
|
20018
|
+
|
|
20019
|
+
const canStart = await evaluateTaskCanStart({
|
|
20020
|
+
taskId,
|
|
20021
|
+
task,
|
|
20022
|
+
adapter,
|
|
20023
|
+
forceStart: body?.force === true || body?.forceStart === true,
|
|
20024
|
+
manualOverride: body?.manualOverride === true || body?.overrideStartGuard === true,
|
|
20025
|
+
});
|
|
20026
|
+
if (!canStart.canStart) {
|
|
20027
|
+
jsonResponse(res, 409, {
|
|
20028
|
+
ok: false,
|
|
20029
|
+
taskId,
|
|
20030
|
+
error: "Task cannot be dispatched",
|
|
20031
|
+
canStart,
|
|
20032
|
+
data: task ? withTaskRuntimeSnapshot(task) : null,
|
|
20033
|
+
});
|
|
20034
|
+
return;
|
|
20035
|
+
}
|
|
20036
|
+
|
|
20037
|
+
const status = executor.getStatus?.() || {};
|
|
20038
|
+
const activeSlots = Array.isArray(status?.slots) ? status.slots : [];
|
|
20039
|
+
const maxParallel = Number(status?.maxParallel || 0);
|
|
20040
|
+
const activeCount = Number(status?.activeSlots || activeSlots.length || 0);
|
|
20041
|
+
const hasFreeSlot = maxParallel <= 0 || activeCount < maxParallel;
|
|
20042
|
+
if (!hasFreeSlot) {
|
|
20043
|
+
const queuedTask = await persistTaskExecutionMeta(adapter, taskId, {
|
|
20044
|
+
queued: true,
|
|
20045
|
+
queueState: "queued",
|
|
20046
|
+
requestedAt: new Date().toISOString(),
|
|
20047
|
+
});
|
|
20048
|
+
jsonResponse(res, 202, {
|
|
20049
|
+
ok: true,
|
|
20050
|
+
taskId,
|
|
20051
|
+
queued: true,
|
|
20052
|
+
started: false,
|
|
20053
|
+
createdFromPrompt,
|
|
20054
|
+
reason: "No free slots",
|
|
20055
|
+
canStart,
|
|
20056
|
+
data: withTaskRuntimeSnapshot(queuedTask || task),
|
|
20057
|
+
});
|
|
20058
|
+
broadcastUiEvent(["tasks", "overview", "executor", "agents"], "invalidate", {
|
|
20059
|
+
reason: "task-queued",
|
|
20060
|
+
taskId,
|
|
20061
|
+
});
|
|
20062
|
+
return;
|
|
20063
|
+
}
|
|
20064
|
+
|
|
20065
|
+
let startedTask = await persistTaskStatusForExecution(adapter, taskId, "inprogress", "api.executor.dispatch") || task;
|
|
20066
|
+
startedTask = await persistTaskExecutionMeta(adapter, taskId, {
|
|
20067
|
+
queued: false,
|
|
20068
|
+
queueState: null,
|
|
20069
|
+
}) || startedTask;
|
|
20070
|
+
applyInternalLifecycleTransition(taskId, "start", {
|
|
20071
|
+
source: "api.executor.dispatch",
|
|
20072
|
+
actor: "ui",
|
|
20073
|
+
force: body?.force === true || body?.forceStart === true || body?.manualOverride === true || body?.overrideStartGuard === true,
|
|
20074
|
+
reason: createdFromPrompt ? "fleet prompt dispatch" : "fleet task dispatch",
|
|
20075
|
+
});
|
|
20076
|
+
|
|
20077
|
+
traceHttpServerAction(req, {
|
|
20078
|
+
route: "/api/executor/dispatch",
|
|
20079
|
+
taskId,
|
|
20080
|
+
}, () => executor.executeTask(startedTask, {
|
|
20081
|
+
...(sdk ? { sdk } : {}),
|
|
20082
|
+
...(model ? { model } : {}),
|
|
20083
|
+
force: body?.force === true || body?.forceStart === true || body?.manualOverride === true || body?.overrideStartGuard === true,
|
|
20084
|
+
})).catch((error) => {
|
|
20085
|
+
console.warn(`[telegram-ui] fleet dispatch failed for ${taskId}: ${error.message}`);
|
|
20086
|
+
void persistTaskStatusForExecution(
|
|
20087
|
+
adapter,
|
|
20088
|
+
taskId,
|
|
20089
|
+
resolveFallbackStatusAfterFailedDispatch(task?.status, { started: false }),
|
|
20090
|
+
"api.executor.dispatch.failed",
|
|
20091
|
+
);
|
|
20092
|
+
});
|
|
20093
|
+
|
|
20094
|
+
jsonResponse(res, 200, {
|
|
20095
|
+
ok: true,
|
|
20096
|
+
taskId,
|
|
20097
|
+
queued: false,
|
|
20098
|
+
started: true,
|
|
20099
|
+
createdFromPrompt,
|
|
20100
|
+
canStart,
|
|
20101
|
+
data: withTaskRuntimeSnapshot(startedTask),
|
|
20102
|
+
});
|
|
20103
|
+
broadcastUiEvent(["tasks", "overview", "executor", "agents"], "invalidate", {
|
|
20104
|
+
reason: "task-dispatched",
|
|
20105
|
+
taskId,
|
|
20106
|
+
});
|
|
20107
|
+
} catch (err) {
|
|
20108
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
20109
|
+
}
|
|
20110
|
+
return;
|
|
20111
|
+
}
|
|
20112
|
+
|
|
19801
20113
|
if (path === "/api/executor") {
|
|
19802
20114
|
const executor = uiDeps.getInternalExecutor?.();
|
|
19803
20115
|
const mode = uiDeps.getExecutorMode?.() || "internal";
|
|
@@ -393,6 +393,8 @@ export function buildKanbanColumnItems(tasks = [], hierarchyView = null, hierarc
|
|
|
393
393
|
const groups = new Map();
|
|
394
394
|
const taskById = hierarchyModel?.taskById || new Map();
|
|
395
395
|
const epicGroupById = new Map((hierarchyView?.epicGroups || []).map((entry) => [String(entry.id || ""), entry]));
|
|
396
|
+
const nodeStateById = hierarchyView?.nodeStateById || new Map();
|
|
397
|
+
const hasSearch = nodeStateById.size > 0;
|
|
396
398
|
rows.sort((left, right) => getHierarchySortOrder(hierarchyModel, left?.id) - getHierarchySortOrder(hierarchyModel, right?.id));
|
|
397
399
|
|
|
398
400
|
const ensureGroup = (key, create) => {
|
|
@@ -402,7 +404,8 @@ export function buildKanbanColumnItems(tasks = [], hierarchyView = null, hierarc
|
|
|
402
404
|
|
|
403
405
|
for (const task of rows) {
|
|
404
406
|
const taskId = String(task?.id || "");
|
|
405
|
-
|
|
407
|
+
if (hierarchyView?.visibleTaskIds?.size && !hierarchyView.visibleTaskIds.has(taskId)) continue;
|
|
408
|
+
const node = nodeStateById.get?.(taskId) || null;
|
|
406
409
|
const parentId = String(node?.meta?.parentTaskId || "");
|
|
407
410
|
if (parentId && hierarchyView?.nodeStateById?.get?.(parentId)?.isParentNode) {
|
|
408
411
|
const parentNode = hierarchyView.nodeStateById.get(parentId);
|
|
@@ -416,10 +419,14 @@ export function buildKanbanColumnItems(tasks = [], hierarchyView = null, hierarc
|
|
|
416
419
|
children: [],
|
|
417
420
|
}));
|
|
418
421
|
if (parentTask?.id) coveredHeaderTaskIds.add(String(parentTask.id));
|
|
422
|
+
const isDirectMatch = node?.searchMatchState === "direct" || node?.searchMatchState === "self";
|
|
419
423
|
if (taskId === parentId) {
|
|
420
424
|
coveredHeaderTaskIds.add(taskId);
|
|
421
425
|
continue;
|
|
422
426
|
}
|
|
427
|
+
if (hasSearch && parentNode && !isDirectMatch && !parentNode.visibleChildIds?.includes(taskId)) {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
423
430
|
groupedTaskIds.add(taskId);
|
|
424
431
|
group.children.push(task);
|
|
425
432
|
continue;
|
|
@@ -430,20 +437,26 @@ export function buildKanbanColumnItems(tasks = [], hierarchyView = null, hierarc
|
|
|
430
437
|
const taskType = getTaskHierarchyTaskType(task, "task");
|
|
431
438
|
if (epicGroup && epicGroup.visibleTaskCount > 1 && taskType !== "epic") {
|
|
432
439
|
const anchorTask = epicGroup.anchorTaskId ? taskById.get(epicGroup.anchorTaskId) : null;
|
|
440
|
+
const anchorTaskId = String(anchorTask?.id || epicGroup.anchorTaskId || "");
|
|
441
|
+
const anchorNode = anchorTaskId ? nodeStateById.get?.(anchorTaskId) || null : null;
|
|
433
442
|
const group = ensureGroup(`epic:${epicId}`, () => ({
|
|
434
443
|
key: `epic:${epicId}`,
|
|
435
444
|
kind: "epic",
|
|
436
445
|
parentTask: anchorTask,
|
|
437
|
-
parentNode:
|
|
446
|
+
parentNode: anchorNode,
|
|
438
447
|
epicGroup,
|
|
439
448
|
title: epicGroup.label || epicId,
|
|
440
449
|
children: [],
|
|
441
450
|
}));
|
|
442
451
|
if (anchorTask?.id) coveredHeaderTaskIds.add(String(anchorTask.id));
|
|
443
|
-
if (
|
|
452
|
+
if (anchorTaskId && taskId === anchorTaskId) {
|
|
444
453
|
coveredHeaderTaskIds.add(taskId);
|
|
445
454
|
continue;
|
|
446
455
|
}
|
|
456
|
+
const isDirectMatch = node?.searchMatchState === "direct" || node?.searchMatchState === "self";
|
|
457
|
+
if (hasSearch && !isDirectMatch && !epicGroup.visibleTaskIds?.includes(taskId)) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
447
460
|
groupedTaskIds.add(taskId);
|
|
448
461
|
group.children.push(task);
|
|
449
462
|
}
|
|
@@ -465,6 +478,7 @@ export function buildKanbanColumnItems(tasks = [], hierarchyView = null, hierarc
|
|
|
465
478
|
|
|
466
479
|
for (const task of rows) {
|
|
467
480
|
const taskId = String(task?.id || "");
|
|
481
|
+
if (hierarchyView?.visibleTaskIds?.size && !hierarchyView.visibleTaskIds.has(taskId)) continue;
|
|
468
482
|
if (groupedTaskIds.has(taskId) || coveredHeaderTaskIds.has(taskId)) continue;
|
|
469
483
|
items.push({
|
|
470
484
|
kind: "task",
|
|
@@ -477,6 +491,46 @@ export function buildKanbanColumnItems(tasks = [], hierarchyView = null, hierarc
|
|
|
477
491
|
return items;
|
|
478
492
|
}
|
|
479
493
|
|
|
494
|
+
export function groupTasksByStatus(tasks = [], options = {}) {
|
|
495
|
+
const rows = Array.isArray(tasks) ? tasks.filter((task) => task && typeof task === "object") : [];
|
|
496
|
+
const statuses = Array.isArray(options?.statuses) && options.statuses.length > 0
|
|
497
|
+
? options.statuses.map((status) => String(status || "").trim()).filter(Boolean)
|
|
498
|
+
: ["todo", "inprogress", "inreview", "done", "blocked", "draft"];
|
|
499
|
+
const result = Object.create(null);
|
|
500
|
+
for (const status of statuses) result[status] = [];
|
|
501
|
+
|
|
502
|
+
if (options?.hierarchyMode) {
|
|
503
|
+
const hierarchyModel = buildTaskHierarchyModel(rows);
|
|
504
|
+
const hierarchyView = deriveTaskHierarchyView(hierarchyModel);
|
|
505
|
+
for (const status of statuses) {
|
|
506
|
+
const statusRows = rows.filter((task) => String(task?.status || "").trim() === status);
|
|
507
|
+
const grouped = buildKanbanColumnItems(statusRows, hierarchyView, hierarchyModel);
|
|
508
|
+
result[status] = grouped.map((entry) => {
|
|
509
|
+
if (entry?.kind === "group") {
|
|
510
|
+
const parentTask = entry.group?.parentTask || null;
|
|
511
|
+
const childTasks = Array.isArray(entry.group?.children) ? entry.group.children.filter(Boolean) : [];
|
|
512
|
+
if (parentTask && childTasks.length > 0) {
|
|
513
|
+
return {
|
|
514
|
+
...parentTask,
|
|
515
|
+
childTasks,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return entry?.task || entry;
|
|
520
|
+
}).filter(Boolean);
|
|
521
|
+
}
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
for (const task of rows) {
|
|
526
|
+
const normalizedStatus = String(task?.status || "").trim();
|
|
527
|
+
if (!normalizedStatus) continue;
|
|
528
|
+
if (!Array.isArray(result[normalizedStatus])) result[normalizedStatus] = [];
|
|
529
|
+
result[normalizedStatus].push(task);
|
|
530
|
+
}
|
|
531
|
+
return result;
|
|
532
|
+
}
|
|
533
|
+
|
|
480
534
|
function getTaskTypeBadgeLabel(taskType) {
|
|
481
535
|
const normalized = String(taskType || "task").toLowerCase();
|
|
482
536
|
if (normalized === "epic") return "EPIC";
|
|
@@ -1689,5 +1743,3 @@ export function KanbanBoard({ onOpenTask, hasMoreTasks = false, loadingMoreTasks
|
|
|
1689
1743
|
</${Box}>
|
|
1690
1744
|
`;
|
|
1691
1745
|
}
|
|
1692
|
-
|
|
1693
|
-
|
package/ui/demo-defaults.js
CHANGED
|
@@ -7288,7 +7288,7 @@
|
|
|
7288
7288
|
"type": "condition.expression",
|
|
7289
7289
|
"label": "PR Created?",
|
|
7290
7290
|
"config": {
|
|
7291
|
-
"expression": "Boolean($ctx.getNodeOutput(
|
|
7291
|
+
"expression": "Boolean($ctx.getNodeOutput('create-pr')?.prNumber || $ctx.getNodeOutput('create-pr')?.prUrl)"
|
|
7292
7292
|
},
|
|
7293
7293
|
"position": {
|
|
7294
7294
|
"x": 400,
|
|
@@ -7531,7 +7531,7 @@
|
|
|
7531
7531
|
"type": "condition.expression",
|
|
7532
7532
|
"label": "PR Created?",
|
|
7533
7533
|
"config": {
|
|
7534
|
-
"expression": "Boolean($ctx.getNodeOutput(
|
|
7534
|
+
"expression": "Boolean($ctx.getNodeOutput('create-pr-retry')?.prNumber || $ctx.getNodeOutput('create-pr-retry')?.prUrl)"
|
|
7535
7535
|
},
|
|
7536
7536
|
"position": {
|
|
7537
7537
|
"x": 400,
|
|
@@ -7759,7 +7759,7 @@
|
|
|
7759
7759
|
"type": "condition.expression",
|
|
7760
7760
|
"label": "PR Created?",
|
|
7761
7761
|
"config": {
|
|
7762
|
-
"expression": "Boolean($ctx.getNodeOutput(
|
|
7762
|
+
"expression": "Boolean($ctx.getNodeOutput('create-pr-retry2')?.prNumber || $ctx.getNodeOutput('create-pr-retry2')?.prUrl)"
|
|
7763
7763
|
},
|
|
7764
7764
|
"position": {
|
|
7765
7765
|
"x": 400,
|
|
@@ -9950,7 +9950,7 @@
|
|
|
9950
9950
|
"config": {
|
|
9951
9951
|
"plannerNodeId": "run-planner",
|
|
9952
9952
|
"maxTasks": "{{taskCount}}",
|
|
9953
|
-
"status": "
|
|
9953
|
+
"status": "todo",
|
|
9954
9954
|
"dedup": true,
|
|
9955
9955
|
"failOnZero": true,
|
|
9956
9956
|
"minCreated": 1
|
|
@@ -10575,7 +10575,7 @@
|
|
|
10575
10575
|
"type": "action.run_agent",
|
|
10576
10576
|
"label": "Build Follow-up Tasks JSON",
|
|
10577
10577
|
"config": {
|
|
10578
|
-
"prompt": "Convert FOLLOW_UP_ACTION lines below into a single JSON object with shape { \"tasks\": [...] }.\n\nSource:\n{{evaluate-fitness.output}}\n\nStructured context:\n{{fitnessSummaryJson}}\n\nRules:\n- Generate at most {{maxFollowupTasks}} tasks\n- Include fields: title, description, implementation_steps, acceptance_criteria, verification, priority, tags, base_branch, impact, confidence, risk, estimated_effort, repo_areas, why_now, kill_criteria\n- Use trend deltas from the summary artifact to justify urgency and avoid parse errors\n- Keep tasks implementation-ready and avoid duplicates\n- Return only JSON",
|
|
10578
|
+
"prompt": "Convert FOLLOW_UP_ACTION lines below into a single JSON object with shape { \"tasks\": [...] }.\n\nSource:\n{{evaluate-fitness.output}}\n\nStructured context:\n{{fitnessSummaryJson}}\n\nRules:\n- Generate at most {{maxFollowupTasks}} tasks\n- Include fields: task_key, title, description, implementation_steps, files, tests, api_contracts, acceptance_criteria, verification, priority, tags, base_branch, impact, confidence, risk, estimated_effort, repo_areas, why_now, kill_criteria\n- Use sprint_id, epic_id, parent_task_key, and depends_on_task_keys when the work belongs in an existing sprint/epic or has DAG ordering\n- Use trend deltas from the summary artifact to justify urgency and avoid parse errors\n- Keep tasks implementation-ready and avoid duplicates\n- Return only JSON",
|
|
10579
10579
|
"sdk": "auto",
|
|
10580
10580
|
"timeoutMs": 300000
|
|
10581
10581
|
},
|
|
@@ -22958,7 +22958,7 @@
|
|
|
22958
22958
|
"enabled": true,
|
|
22959
22959
|
"trigger": "trigger.task_available",
|
|
22960
22960
|
"variables": {
|
|
22961
|
-
"maxConcurrent":
|
|
22961
|
+
"maxConcurrent": 5,
|
|
22962
22962
|
"pollStatus": "todo",
|
|
22963
22963
|
"maxBatchSize": 10,
|
|
22964
22964
|
"subWorkflow": "template-task-lifecycle",
|
|
@@ -32953,7 +32953,7 @@
|
|
|
32953
32953
|
"type": "condition.expression",
|
|
32954
32954
|
"label": "PR Created?",
|
|
32955
32955
|
"config": {
|
|
32956
|
-
"expression": "Boolean($ctx.getNodeOutput(
|
|
32956
|
+
"expression": "Boolean($ctx.getNodeOutput('create-pr')?.prNumber || $ctx.getNodeOutput('create-pr')?.prUrl)"
|
|
32957
32957
|
},
|
|
32958
32958
|
"position": {
|
|
32959
32959
|
"x": 400,
|
|
@@ -33196,7 +33196,7 @@
|
|
|
33196
33196
|
"type": "condition.expression",
|
|
33197
33197
|
"label": "PR Created?",
|
|
33198
33198
|
"config": {
|
|
33199
|
-
"expression": "Boolean($ctx.getNodeOutput(
|
|
33199
|
+
"expression": "Boolean($ctx.getNodeOutput('create-pr-retry')?.prNumber || $ctx.getNodeOutput('create-pr-retry')?.prUrl)"
|
|
33200
33200
|
},
|
|
33201
33201
|
"position": {
|
|
33202
33202
|
"x": 400,
|
|
@@ -33424,7 +33424,7 @@
|
|
|
33424
33424
|
"type": "condition.expression",
|
|
33425
33425
|
"label": "PR Created?",
|
|
33426
33426
|
"config": {
|
|
33427
|
-
"expression": "Boolean($ctx.getNodeOutput(
|
|
33427
|
+
"expression": "Boolean($ctx.getNodeOutput('create-pr-retry2')?.prNumber || $ctx.getNodeOutput('create-pr-retry2')?.prUrl)"
|
|
33428
33428
|
},
|
|
33429
33429
|
"position": {
|
|
33430
33430
|
"x": 400,
|
|
@@ -35486,7 +35486,7 @@
|
|
|
35486
35486
|
"config": {
|
|
35487
35487
|
"plannerNodeId": "run-planner",
|
|
35488
35488
|
"maxTasks": "{{taskCount}}",
|
|
35489
|
-
"status": "
|
|
35489
|
+
"status": "todo",
|
|
35490
35490
|
"dedup": true,
|
|
35491
35491
|
"failOnZero": true,
|
|
35492
35492
|
"minCreated": 1
|
|
@@ -36069,7 +36069,7 @@
|
|
|
36069
36069
|
"type": "action.run_agent",
|
|
36070
36070
|
"label": "Build Follow-up Tasks JSON",
|
|
36071
36071
|
"config": {
|
|
36072
|
-
"prompt": "Convert FOLLOW_UP_ACTION lines below into a single JSON object with shape { \"tasks\": [...] }.\n\nSource:\n{{evaluate-fitness.output}}\n\nStructured context:\n{{fitnessSummaryJson}}\n\nRules:\n- Generate at most {{maxFollowupTasks}} tasks\n- Include fields: title, description, implementation_steps, acceptance_criteria, verification, priority, tags, base_branch, impact, confidence, risk, estimated_effort, repo_areas, why_now, kill_criteria\n- Use trend deltas from the summary artifact to justify urgency and avoid parse errors\n- Keep tasks implementation-ready and avoid duplicates\n- Return only JSON",
|
|
36072
|
+
"prompt": "Convert FOLLOW_UP_ACTION lines below into a single JSON object with shape { \"tasks\": [...] }.\n\nSource:\n{{evaluate-fitness.output}}\n\nStructured context:\n{{fitnessSummaryJson}}\n\nRules:\n- Generate at most {{maxFollowupTasks}} tasks\n- Include fields: task_key, title, description, implementation_steps, files, tests, api_contracts, acceptance_criteria, verification, priority, tags, base_branch, impact, confidence, risk, estimated_effort, repo_areas, why_now, kill_criteria\n- Use sprint_id, epic_id, parent_task_key, and depends_on_task_keys when the work belongs in an existing sprint/epic or has DAG ordering\n- Use trend deltas from the summary artifact to justify urgency and avoid parse errors\n- Keep tasks implementation-ready and avoid duplicates\n- Return only JSON",
|
|
36073
36073
|
"sdk": "auto",
|
|
36074
36074
|
"timeoutMs": 300000
|
|
36075
36075
|
},
|
|
@@ -47874,7 +47874,7 @@
|
|
|
47874
47874
|
"nodeCount": 9,
|
|
47875
47875
|
"trigger": "trigger.task_available",
|
|
47876
47876
|
"variables": {
|
|
47877
|
-
"maxConcurrent":
|
|
47877
|
+
"maxConcurrent": 5,
|
|
47878
47878
|
"pollStatus": "todo",
|
|
47879
47879
|
"maxBatchSize": 10,
|
|
47880
47880
|
"subWorkflow": "template-task-lifecycle",
|
|
@@ -144,7 +144,7 @@ export function buildTaskHierarchyModel(tasks = [], options = {}) {
|
|
|
144
144
|
taskType,
|
|
145
145
|
epicId,
|
|
146
146
|
parentTaskId,
|
|
147
|
-
collapseKey: getTaskHierarchyCollapseKey(
|
|
147
|
+
collapseKey: getTaskHierarchyCollapseKey(taskType, id),
|
|
148
148
|
epicCollapseKey: epicId ? getTaskHierarchyCollapseKey("epic", epicId) : "",
|
|
149
149
|
});
|
|
150
150
|
}
|
|
@@ -278,7 +278,7 @@ export function deriveTaskHierarchyView(model, options = {}) {
|
|
|
278
278
|
}, 0);
|
|
279
279
|
const descendantMatch = visibleChildIds.length > 0;
|
|
280
280
|
const visible = directMatch || descendantMatch;
|
|
281
|
-
const searchMatchState = directMatch ? "
|
|
281
|
+
const searchMatchState = directMatch ? "direct" : descendantMatch ? "descendant" : "none";
|
|
282
282
|
const state = {
|
|
283
283
|
id,
|
|
284
284
|
task,
|
package/ui/tabs/agents.js
CHANGED
|
@@ -3445,10 +3445,10 @@ function FleetSessionsPanel({ slots, sessions = [], taskFallbackEntries = [], on
|
|
|
3445
3445
|
onClick=${() => setDetailTab("turns")}
|
|
3446
3446
|
>${iconText(":repeat: Turns")}<//>
|
|
3447
3447
|
</div>
|
|
3448
|
-
<div class="fleet-session-body" style=${{ flex: "1 1 auto", minHeight: 0,
|
|
3448
|
+
<div class="fleet-session-body" style=${{ flex: "1 1 auto", minHeight: 0, overflow: "hidden", paddingRight: "4px" }}>
|
|
3449
3449
|
${detailTab === "stream"
|
|
3450
3450
|
? streamSessionId
|
|
3451
|
-
? html`<${ChatView} sessionId=${streamSessionId} readOnly=${true} />`
|
|
3451
|
+
? html`<${ChatView} sessionId=${streamSessionId} readOnly=${true} embedded=${true} />`
|
|
3452
3452
|
: html`
|
|
3453
3453
|
<div class="chat-view chat-empty-state">
|
|
3454
3454
|
<div class="session-empty-icon">${resolveIcon(":chat:")}</div>
|
package/ui/tabs/tasks.js
CHANGED
|
@@ -242,7 +242,8 @@ function normalizeTagInput(input) {
|
|
|
242
242
|
const seen = new Set();
|
|
243
243
|
const tags = [];
|
|
244
244
|
for (const value of values) {
|
|
245
|
-
const
|
|
245
|
+
const raw = typeof value === "string" ? value : value?.name || value?.label || value?.value || "";
|
|
246
|
+
const normalized = String(raw || "")
|
|
246
247
|
.trim()
|
|
247
248
|
.toLowerCase();
|
|
248
249
|
if (!normalized || seen.has(normalized) || SYSTEM_TAGS.has(normalized)) continue;
|
|
@@ -441,6 +442,8 @@ export function normalizeSubtaskRow(entry, fallbackParentTaskId = "") {
|
|
|
441
442
|
status: toText(entry.status || entry.state),
|
|
442
443
|
assignee: toText(entry.assignee || entry.owner),
|
|
443
444
|
taskType: normalizeTaskTypeValue(entry?.taskType || entry?.type || entry?.kind || entry?.meta?.taskType || entry?.meta?.type, "subtask"),
|
|
445
|
+
collapseKey: toText(entry.collapseKey || entry?.meta?.collapseKey),
|
|
446
|
+
epicCollapseKey: toText(entry.epicCollapseKey || entry?.meta?.epicCollapseKey),
|
|
444
447
|
storyPoints: toText(entry.storyPoints || entry.points),
|
|
445
448
|
epicId: toText(entry.epicId || entry.epic || entry.epic_id || entry?.meta?.epicId),
|
|
446
449
|
dueDate: normalizeTaskDueDateInput(entry),
|
|
@@ -484,7 +487,9 @@ function mergeSubtaskLists(...lists) {
|
|
|
484
487
|
}
|
|
485
488
|
|
|
486
489
|
export function buildTaskHierarchyPath(task, hierarchyModel = null) {
|
|
487
|
-
const taskById = hierarchyModel
|
|
490
|
+
const taskById = hierarchyModel instanceof Map
|
|
491
|
+
? hierarchyModel
|
|
492
|
+
: (hierarchyModel?.taskById || new Map());
|
|
488
493
|
const path = [];
|
|
489
494
|
const seen = new Set();
|
|
490
495
|
let cursor = task;
|
|
@@ -5157,13 +5157,14 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
5157
5157
|
}
|
|
5158
5158
|
const createTasksGuard = this._resolvePendingCreateTasksGuard(originalRun, def);
|
|
5159
5159
|
const isInterruptedResume = retryOpts._resumeInterrupted === true;
|
|
5160
|
+
const operatorAction = String(retryOpts.operatorAction || "").trim().toLowerCase();
|
|
5160
5161
|
const isResumeLikeRetry =
|
|
5161
5162
|
(mode === "from_failed" && !isInterruptedResume)
|
|
5162
5163
|
|| mode === "replan_from_failed"
|
|
5163
5164
|
|| mode === "replan_subgraph";
|
|
5164
5165
|
const blocksManualRestart = mode === "from_scratch" && createTasksGuard?.safeResume;
|
|
5165
5166
|
if (createTasksGuard && (isResumeLikeRetry || blocksManualRestart)) {
|
|
5166
|
-
if (isInterruptedResume) {
|
|
5167
|
+
if (isInterruptedResume || operatorAction === "resume") {
|
|
5167
5168
|
if (!createTasksGuard.safeResume) {
|
|
5168
5169
|
throw new Error(`${TAG} ${createTasksGuard.resumeBlockedMessage}`);
|
|
5169
5170
|
}
|
|
@@ -5672,8 +5673,10 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
5672
5673
|
if (!this._loaded) this.load();
|
|
5673
5674
|
|
|
5674
5675
|
const triggered = [];
|
|
5676
|
+
const triggeredWorkflowIds = new Set();
|
|
5675
5677
|
for (const [id, def] of this._workflows) {
|
|
5676
5678
|
if (def.enabled === false) continue;
|
|
5679
|
+
if (triggeredWorkflowIds.has(id)) continue;
|
|
5677
5680
|
|
|
5678
5681
|
// Find trigger nodes
|
|
5679
5682
|
const triggerNodes = (def.nodes || []).filter((n) =>
|
|
@@ -5733,6 +5736,8 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
5733
5736
|
});
|
|
5734
5737
|
if (shouldFire?.triggered) {
|
|
5735
5738
|
triggered.push({ workflowId: id, triggeredBy: tNode.id, eventData });
|
|
5739
|
+
triggeredWorkflowIds.add(id);
|
|
5740
|
+
break;
|
|
5736
5741
|
}
|
|
5737
5742
|
} catch {
|
|
5738
5743
|
// Trigger evaluation errors are non-fatal
|
|
@@ -7109,7 +7114,6 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
7109
7114
|
};
|
|
7110
7115
|
}
|
|
7111
7116
|
|
|
7112
|
-
|
|
7113
7117
|
/**
|
|
7114
7118
|
* Get task-linked workflow trace events for a run.
|
|
7115
7119
|
* Returns [] when run is unknown or has no task trace data.
|
|
@@ -9937,6 +9941,14 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
9937
9941
|
}
|
|
9938
9942
|
}
|
|
9939
9943
|
|
|
9944
|
+
const indexedRunCountsByTaskId = new Map();
|
|
9945
|
+
for (const run of allRuns) {
|
|
9946
|
+
const ident = identityCache.get(run.runId) ?? getIdentity(run.runId);
|
|
9947
|
+
const taskId = this._resolveRunTaskIdentity(run, ident)?.taskId || "";
|
|
9948
|
+
if (!taskId) continue;
|
|
9949
|
+
indexedRunCountsByTaskId.set(taskId, (indexedRunCountsByTaskId.get(taskId) || 0) + 1);
|
|
9950
|
+
}
|
|
9951
|
+
|
|
9940
9952
|
// Mark older duplicate runs as not-resumable before entering the loop.
|
|
9941
9953
|
// This must consider the full paused+resumable set so historical siblings
|
|
9942
9954
|
// outside the current startup cohort are still retired when a newer run wins.
|
|
@@ -10082,7 +10094,7 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
10082
10094
|
: (watchdogDecision || this._chooseRetryModeFromDetail(detail, {
|
|
10083
10095
|
fallbackMode: "from_scratch",
|
|
10084
10096
|
}));
|
|
10085
|
-
const detailedRun = this.getRunDetail(run.runId) || run;
|
|
10097
|
+
const detailedRun = this.getRunDetail(run.runId, { decorate: false }) || { ...run, detail };
|
|
10086
10098
|
const createTasksGuard = this._resolvePendingCreateTasksGuard(detailedRun, def);
|
|
10087
10099
|
if (createTasksGuard && !createTasksGuard.safeResume) {
|
|
10088
10100
|
console.warn(`${TAG} Skipping run ${run.runId}: ${createTasksGuard.resumeBlockedMessage}`);
|
|
@@ -5169,7 +5169,7 @@ registerNodeType("action.run_agent", {
|
|
|
5169
5169
|
for (let idx = 1; idx <= configuredCandidateCount; idx += 1) {
|
|
5170
5170
|
const candidateBranch =
|
|
5171
5171
|
`${safeBranchPart(currentBranch)}-cand-${idx}-${batchToken}`.slice(0, 120);
|
|
5172
|
-
|
|
5172
|
+
execFileSync("git", ["checkout", "-B", candidateBranch, baselineHead], {
|
|
5173
5173
|
cwd,
|
|
5174
5174
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5175
5175
|
encoding: "utf8",
|
|
@@ -5252,7 +5252,7 @@ registerNodeType("action.run_agent", {
|
|
|
5252
5252
|
}
|
|
5253
5253
|
|
|
5254
5254
|
const selectedHead = selected.hasCommit ? selected.head : baselineHead;
|
|
5255
|
-
|
|
5255
|
+
execFileSync("git", ["checkout", "-B", currentBranch, selectedHead], {
|
|
5256
5256
|
cwd,
|
|
5257
5257
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5258
5258
|
encoding: "utf8",
|
|
@@ -5261,7 +5261,7 @@ registerNodeType("action.run_agent", {
|
|
|
5261
5261
|
for (const candidate of candidateRuns) {
|
|
5262
5262
|
if (!candidate?.branch) continue;
|
|
5263
5263
|
try {
|
|
5264
|
-
|
|
5264
|
+
execFileSync("git", ["branch", "-D", candidate.branch], {
|
|
5265
5265
|
cwd,
|
|
5266
5266
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5267
5267
|
encoding: "utf8",
|
|
@@ -7438,6 +7438,16 @@ registerNodeType("action.materialize_planner_tasks", {
|
|
|
7438
7438
|
}
|
|
7439
7439
|
if (Array.isArray(task.tags) && task.tags.length > 0) payload.tags = task.tags;
|
|
7440
7440
|
if (task.baseBranch) payload.baseBranch = task.baseBranch;
|
|
7441
|
+
if (task.sprintId) {
|
|
7442
|
+
payload.sprintId = task.sprintId;
|
|
7443
|
+
}
|
|
7444
|
+
if (task.epicId) {
|
|
7445
|
+
payload.epicId = task.epicId;
|
|
7446
|
+
}
|
|
7447
|
+
if (Array.isArray(task.dependencyTaskIds) && task.dependencyTaskIds.length > 0) {
|
|
7448
|
+
payload.dependencyTaskIds = task.dependencyTaskIds;
|
|
7449
|
+
payload.dependsOn = task.dependencyTaskIds;
|
|
7450
|
+
}
|
|
7441
7451
|
if (task.draft || String(status || "").trim().toLowerCase() === "draft") {
|
|
7442
7452
|
payload.draft = true;
|
|
7443
7453
|
}
|
|
@@ -7472,16 +7482,38 @@ registerNodeType("action.materialize_planner_tasks", {
|
|
|
7472
7482
|
run_id: String(ctx?.runId || ctx?.data?._runId || "").trim() || null,
|
|
7473
7483
|
acceptance_criteria: task.acceptanceCriteria,
|
|
7474
7484
|
verification: task.verification,
|
|
7485
|
+
implementation_steps: Array.isArray(task.implementationSteps) ? task.implementationSteps : [],
|
|
7486
|
+
files: Array.isArray(task.files) ? task.files : [],
|
|
7487
|
+
tests: Array.isArray(task.tests) ? task.tests : [],
|
|
7488
|
+
api_contracts: Array.isArray(task.apiContracts) ? task.apiContracts : [],
|
|
7475
7489
|
task_key: task.taskKey || null,
|
|
7476
7490
|
parent_task_key: task.parentTaskKey || null,
|
|
7477
7491
|
parent_task_id: task.parentTaskId || null,
|
|
7478
7492
|
depends_on_task_keys: Array.isArray(task.dependencyTaskKeys) ? task.dependencyTaskKeys : [],
|
|
7479
7493
|
depends_on_task_ids: Array.isArray(task.dependencyTaskIds) ? task.dependencyTaskIds : [],
|
|
7494
|
+
sprint_id: task.sprintId || null,
|
|
7495
|
+
epic_id: task.epicId || null,
|
|
7480
7496
|
decomposition_kind: task.decompositionKind || null,
|
|
7481
7497
|
spawn_when: task.spawnWhen || null,
|
|
7482
7498
|
merge_back_policy: task.mergeBackPolicy || null,
|
|
7483
7499
|
dedupe_key: plannerDedupeKey,
|
|
7484
7500
|
};
|
|
7501
|
+
const plannerWorkflowRunId = String(ctx?.runId || ctx?.data?.runId || ctx?.data?._runId || "").trim() || null;
|
|
7502
|
+
existingMeta.plannerProvenance = {
|
|
7503
|
+
plannerRunId: plannerWorkflowRunId,
|
|
7504
|
+
workflowRunId: plannerWorkflowRunId,
|
|
7505
|
+
plannerNodeId,
|
|
7506
|
+
sourceNodeId: node.id,
|
|
7507
|
+
sourceNodeType: "action.materialize_planner_tasks",
|
|
7508
|
+
sourceLabels: ["workflow", "planner", "materialize_planner_tasks"],
|
|
7509
|
+
generatedAt:
|
|
7510
|
+
typeof existingMeta.plannerProvenance?.generatedAt === "string"
|
|
7511
|
+
? existingMeta.plannerProvenance.generatedAt
|
|
7512
|
+
: new Date().toISOString(),
|
|
7513
|
+
dedupeKey: plannerDedupeKey,
|
|
7514
|
+
materializationFingerprint,
|
|
7515
|
+
taskIndex: task.index,
|
|
7516
|
+
};
|
|
7485
7517
|
payload.meta = existingMeta;
|
|
7486
7518
|
const existingTaskByProvenance = Array.isArray(existingRows)
|
|
7487
7519
|
? existingRows.find((candidate) => {
|
|
@@ -369,14 +369,22 @@ export const CALIBRATED_MAX_RISK_WITHOUT_HUMAN = "medium";
|
|
|
369
369
|
export const STRICT_TASK_PLANNER_TASK_COUNT = 8;
|
|
370
370
|
const TASK_PLANNER_REQUIRED_TOP_LEVEL_KEYS = ["tasks"];
|
|
371
371
|
const TASK_PLANNER_REQUIRED_TASK_FIELDS = [
|
|
372
|
+
"task_key",
|
|
372
373
|
"title",
|
|
373
374
|
"description",
|
|
375
|
+
"implementation_steps",
|
|
376
|
+
"files",
|
|
377
|
+
"tests",
|
|
378
|
+
"api_contracts",
|
|
374
379
|
"acceptance_criteria",
|
|
375
380
|
"verification",
|
|
376
381
|
"repo_areas",
|
|
377
382
|
"impact",
|
|
378
383
|
"confidence",
|
|
379
384
|
"risk",
|
|
385
|
+
"estimated_effort",
|
|
386
|
+
"why_now",
|
|
387
|
+
"kill_criteria",
|
|
380
388
|
];
|
|
381
389
|
const TASK_PLANNER_ALLOWED_ESTIMATED_EFFORT = ["small", "medium", "large"];
|
|
382
390
|
const PLANNER_SCORE_MODE_RATIO = "ratio";
|
|
@@ -557,10 +565,18 @@ export function normalizePlannerTaskForCreation(task, index, options = {}) {
|
|
|
557
565
|
const strict = options?.strict === true;
|
|
558
566
|
|
|
559
567
|
const normalizeStringList = (value) => {
|
|
560
|
-
if (
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
568
|
+
if (Array.isArray(value)) {
|
|
569
|
+
return value
|
|
570
|
+
.map((entry) => String(entry || "").trim())
|
|
571
|
+
.filter(Boolean);
|
|
572
|
+
}
|
|
573
|
+
if (typeof value === "string") {
|
|
574
|
+
return value
|
|
575
|
+
.split(/\r?\n|,/)
|
|
576
|
+
.map((entry) => entry.replace(/^[-*]\s+/, "").trim())
|
|
577
|
+
.filter(Boolean);
|
|
578
|
+
}
|
|
579
|
+
return [];
|
|
564
580
|
};
|
|
565
581
|
const normalizeRepoAreas = (value) => {
|
|
566
582
|
const list = normalizeStringList(value);
|
|
@@ -590,12 +606,24 @@ export function normalizePlannerTaskForCreation(task, index, options = {}) {
|
|
|
590
606
|
const lines = [];
|
|
591
607
|
const description = String(task.description || "").trim();
|
|
592
608
|
if (description) lines.push(description);
|
|
609
|
+
const implementationSteps = strict
|
|
610
|
+
? validateStrictPlannerRequiredField("implementation_steps", task.implementation_steps || task.implementationSteps, { type: "array" })
|
|
611
|
+
: normalizeStringList(task.implementation_steps || task.implementationSteps);
|
|
593
612
|
const acceptanceCriteria = strict
|
|
594
|
-
? validateStrictPlannerRequiredField("acceptance_criteria", task.acceptance_criteria, { type: "array" })
|
|
595
|
-
: normalizeStringList(task.acceptance_criteria);
|
|
613
|
+
? validateStrictPlannerRequiredField("acceptance_criteria", task.acceptance_criteria || task.acceptanceCriteria, { type: "array" })
|
|
614
|
+
: normalizeStringList(task.acceptance_criteria || task.acceptanceCriteria);
|
|
596
615
|
const verification = strict
|
|
597
616
|
? validateStrictPlannerRequiredField("verification", task.verification, { type: "array" })
|
|
598
617
|
: normalizeStringList(task.verification);
|
|
618
|
+
const files = strict
|
|
619
|
+
? validateStrictPlannerRequiredField("files", task.files || task.file_paths || task.filePaths || task.paths, { type: "array" })
|
|
620
|
+
: normalizeStringList(task.files || task.file_paths || task.filePaths || task.paths);
|
|
621
|
+
const tests = strict
|
|
622
|
+
? validateStrictPlannerRequiredField("tests", task.tests || task.test_commands || task.testCommands, { type: "array" })
|
|
623
|
+
: normalizeStringList(task.tests || task.test_commands || task.testCommands);
|
|
624
|
+
const apiContracts = strict
|
|
625
|
+
? validateStrictPlannerRequiredField("api_contracts", task.api_contracts || task.apiContracts, { type: "array" })
|
|
626
|
+
: normalizeStringList(task.api_contracts || task.apiContracts);
|
|
599
627
|
const repoAreas = normalizeRepoAreas(
|
|
600
628
|
strict
|
|
601
629
|
? validateStrictPlannerRequiredField("repo_areas", task.repo_areas || task.repoAreas, { type: "array" })
|
|
@@ -671,13 +699,18 @@ export function normalizePlannerTaskForCreation(task, index, options = {}) {
|
|
|
671
699
|
for (const item of items) lines.push(`- ${item}`);
|
|
672
700
|
};
|
|
673
701
|
|
|
674
|
-
appendList("Implementation Steps",
|
|
702
|
+
appendList("Implementation Steps", implementationSteps);
|
|
703
|
+
appendList("Files", files);
|
|
704
|
+
appendList("Tests", tests);
|
|
705
|
+
appendList("API Contracts", apiContracts);
|
|
675
706
|
appendList("Acceptance Criteria", acceptanceCriteria);
|
|
676
707
|
appendList("Verification", verification);
|
|
677
708
|
|
|
678
709
|
const baseBranch = String(task.base_branch || "").trim();
|
|
679
710
|
const workspace = String(task.workspace || "").trim();
|
|
680
711
|
const repository = String(task.repository || task.repo || "").trim();
|
|
712
|
+
const sprintId = String(task.sprint_id || task.sprintId || task.sprint || "").trim();
|
|
713
|
+
const epicId = String(task.epic_id || task.epicId || task.epic || "").trim();
|
|
681
714
|
const repositories = Array.isArray(task.repositories)
|
|
682
715
|
? task.repositories.map((entry) => String(entry || "").trim()).filter(Boolean)
|
|
683
716
|
: [];
|
|
@@ -707,6 +740,10 @@ export function normalizePlannerTaskForCreation(task, index, options = {}) {
|
|
|
707
740
|
requestedStatus: requestedStatus || null,
|
|
708
741
|
acceptanceCriteria,
|
|
709
742
|
verification,
|
|
743
|
+
implementationSteps,
|
|
744
|
+
files,
|
|
745
|
+
tests,
|
|
746
|
+
apiContracts,
|
|
710
747
|
repoAreas,
|
|
711
748
|
impact,
|
|
712
749
|
confidence,
|
|
@@ -719,6 +756,8 @@ export function normalizePlannerTaskForCreation(task, index, options = {}) {
|
|
|
719
756
|
parentTaskId,
|
|
720
757
|
dependencyTaskKeys,
|
|
721
758
|
dependencyTaskIds,
|
|
759
|
+
sprintId: sprintId || null,
|
|
760
|
+
epicId: epicId || null,
|
|
722
761
|
decompositionKind,
|
|
723
762
|
spawnWhen,
|
|
724
763
|
mergeBackPolicy,
|
|
@@ -797,7 +836,7 @@ export function validateStrictTaskPlannerPayload(payload, options = {}) {
|
|
|
797
836
|
return { ok: false, error: `${taskLabel}.description must be a non-empty string`, code: "invalid_field", taskIndex: i, field: "description" };
|
|
798
837
|
}
|
|
799
838
|
|
|
800
|
-
const listFields = ["acceptance_criteria", "verification", "repo_areas"];
|
|
839
|
+
const listFields = ["implementation_steps", "files", "tests", "api_contracts", "acceptance_criteria", "verification", "repo_areas", "kill_criteria"];
|
|
801
840
|
for (const field of listFields) {
|
|
802
841
|
const value = task[field];
|
|
803
842
|
if (!Array.isArray(value) || value.length === 0 || value.some((entry) => String(entry || "").trim() === "")) {
|
|
@@ -1731,6 +1770,9 @@ registerNodeType("agent.run_planner", {
|
|
|
1731
1770
|
].filter(Boolean).join("\n\n")}\n\n`
|
|
1732
1771
|
: "\n") +
|
|
1733
1772
|
`Your response MUST be a single fenced JSON block with shape { "tasks": [...] }.\n` +
|
|
1773
|
+
`Each task object MUST include: task_key, title, description, implementation_steps, files, tests, api_contracts, acceptance_criteria, verification, repo_areas, impact, confidence, risk, estimated_effort, why_now, and kill_criteria.\n` +
|
|
1774
|
+
`Use sprint_id and epic_id when the planned work belongs to a sprint or epic. Use parent_task_key and depends_on_task_keys to encode DAG/dependency order between generated tasks.\n` +
|
|
1775
|
+
`Descriptions must be ticket-ready: describe the current problem, exact scope, implementation approach, and user-visible outcome in enough detail for an executor to work without rediscovering intent.\n` +
|
|
1734
1776
|
`Do NOT include status updates, analysis notes, tool commentary, questions, or prose outside the JSON block.\n` +
|
|
1735
1777
|
`The downstream system will parse your output as JSON — any extra text will cause task creation to fail.`;
|
|
1736
1778
|
const promptText = basePrompt
|
|
@@ -503,9 +503,27 @@ export const BACKEND_AGENT_TEMPLATE = (() => {
|
|
|
503
503
|
const mainValidation = embedSubWorkflow(VALIDATION_GATE_SUB, "main-");
|
|
504
504
|
const retryValidation = embedSubWorkflow(VALIDATION_GATE_SUB, "retry-");
|
|
505
505
|
const retry2Validation = embedSubWorkflow(VALIDATION_GATE_SUB, "retry2-");
|
|
506
|
-
const mainPrHandoff = embedSubWorkflow(PR_CHECK_HANDOFF_SUB, "main-"
|
|
507
|
-
|
|
508
|
-
|
|
506
|
+
const mainPrHandoff = embedSubWorkflow(PR_CHECK_HANDOFF_SUB, "main-", {
|
|
507
|
+
nodeOverrides: {
|
|
508
|
+
"pr-ok": {
|
|
509
|
+
expression: "Boolean($ctx.getNodeOutput('create-pr')?.prNumber || $ctx.getNodeOutput('create-pr')?.prUrl)",
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
const retryPrHandoff = embedSubWorkflow(PR_CHECK_HANDOFF_SUB, "retry-", {
|
|
514
|
+
nodeOverrides: {
|
|
515
|
+
"pr-ok": {
|
|
516
|
+
expression: "Boolean($ctx.getNodeOutput('create-pr-retry')?.prNumber || $ctx.getNodeOutput('create-pr-retry')?.prUrl)",
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
const retry2PrHandoff = embedSubWorkflow(PR_CHECK_HANDOFF_SUB, "retry2-", {
|
|
521
|
+
nodeOverrides: {
|
|
522
|
+
"pr-ok": {
|
|
523
|
+
expression: "Boolean($ctx.getNodeOutput('create-pr-retry2')?.prNumber || $ctx.getNodeOutput('create-pr-retry2')?.prUrl)",
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
});
|
|
509
527
|
|
|
510
528
|
return {
|
|
511
529
|
id: "template-backend-agent",
|
|
@@ -76,7 +76,7 @@ export const TASK_PLANNER_TEMPLATE = {
|
|
|
76
76
|
node("materialize-tasks", "action.materialize_planner_tasks", "Create Tasks", {
|
|
77
77
|
plannerNodeId: "run-planner",
|
|
78
78
|
maxTasks: "{{taskCount}}",
|
|
79
|
-
status: "
|
|
79
|
+
status: "todo",
|
|
80
80
|
dedup: true,
|
|
81
81
|
failOnZero: true,
|
|
82
82
|
minCreated: 1,
|
|
@@ -929,7 +929,8 @@ Structured context:
|
|
|
929
929
|
|
|
930
930
|
Rules:
|
|
931
931
|
- Generate at most {{maxFollowupTasks}} tasks
|
|
932
|
-
- Include fields: title, description, implementation_steps, acceptance_criteria, verification, priority, tags, base_branch, impact, confidence, risk, estimated_effort, repo_areas, why_now, kill_criteria
|
|
932
|
+
- Include fields: task_key, title, description, implementation_steps, files, tests, api_contracts, acceptance_criteria, verification, priority, tags, base_branch, impact, confidence, risk, estimated_effort, repo_areas, why_now, kill_criteria
|
|
933
|
+
- Use sprint_id, epic_id, parent_task_key, and depends_on_task_keys when the work belongs in an existing sprint/epic or has DAG ordering
|
|
933
934
|
- Use trend deltas from the summary artifact to justify urgency and avoid parse errors
|
|
934
935
|
- Keep tasks implementation-ready and avoid duplicates
|
|
935
936
|
- Return only JSON`,
|
|
@@ -993,5 +994,3 @@ Rules:
|
|
|
993
994
|
},
|
|
994
995
|
};
|
|
995
996
|
|
|
996
|
-
|
|
997
|
-
|
|
@@ -125,7 +125,7 @@ export const TASK_BATCH_PROCESSOR_TEMPLATE = {
|
|
|
125
125
|
recommended: true,
|
|
126
126
|
trigger: "trigger.task_available",
|
|
127
127
|
variables: {
|
|
128
|
-
maxConcurrent:
|
|
128
|
+
maxConcurrent: 5,
|
|
129
129
|
pollStatus: "todo",
|
|
130
130
|
maxBatchSize: 10,
|
|
131
131
|
subWorkflow: "template-task-lifecycle",
|