bosun 0.41.8 → 0.41.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +1 -1
- package/README.md +23 -1
- package/agent/agent-event-bus.mjs +31 -2
- package/agent/agent-pool.mjs +275 -40
- package/agent/agent-prompts.mjs +9 -1
- package/agent/agent-supervisor.mjs +22 -0
- package/agent/autofix.mjs +1 -1
- package/agent/primary-agent.mjs +115 -5
- package/cli.mjs +3 -2
- package/config/config.mjs +47 -33
- package/config/context-shredding-config.mjs +1 -1
- package/config/repo-root.mjs +41 -33
- package/desktop/main.mjs +350 -25
- package/desktop/preload.cjs +8 -0
- package/desktop/preload.mjs +19 -0
- package/entrypoint.mjs +332 -0
- package/git/sdk-conflict-resolver.mjs +1 -1
- package/infra/health-status.mjs +72 -0
- package/infra/library-manager.mjs +58 -1
- package/infra/maintenance.mjs +1 -2
- package/infra/monitor.mjs +26 -8
- package/infra/session-tracker.mjs +30 -3
- package/package.json +12 -4
- package/server/bosun-mcp-server.mjs +1004 -0
- package/server/setup-web-server.mjs +288 -259
- package/server/ui-server.mjs +1323 -26
- package/shell/claude-shell.mjs +14 -1
- package/shell/codex-config.mjs +1 -1
- package/shell/codex-model-profiles.mjs +170 -30
- package/shell/codex-shell.mjs +63 -18
- package/shell/opencode-providers.mjs +20 -8
- package/task/task-executor.mjs +28 -0
- package/task/task-store.mjs +13 -4
- package/telegram/telegram-sentinel.mjs +54 -3
- package/tools/list-todos.mjs +7 -1
- package/ui/app.js +3 -2
- package/ui/components/agent-selector.js +127 -0
- package/ui/components/session-list.js +15 -10
- package/ui/demo-defaults.js +334 -336
- package/ui/modules/router.js +2 -0
- package/ui/modules/state.js +13 -5
- package/ui/tabs/chat.js +3 -0
- package/ui/tabs/library.js +284 -52
- package/ui/tabs/tasks.js +5 -13
- package/ui/tabs/workflows.js +766 -3
- package/workflow/workflow-engine.mjs +246 -5
- package/workflow/workflow-nodes/definitions.mjs +37 -0
- package/workflow/workflow-nodes.mjs +1014 -184
- package/workflow/workflow-templates.mjs +0 -5
- package/workflow-templates/_helpers.mjs +253 -0
- package/workflow-templates/agents.mjs +199 -226
- package/workflow-templates/github.mjs +106 -16
- package/workflow-templates/sub-workflows.mjs +233 -0
- package/workflow-templates/task-execution.mjs +125 -471
- package/workflow-templates/task-lifecycle.mjs +11 -48
- package/workspace/command-diagnostics.mjs +460 -0
- package/workspace/context-cache.mjs +396 -28
- package/workspace/worktree-manager.mjs +1 -1
|
@@ -535,6 +535,8 @@ export class WorkflowContext {
|
|
|
535
535
|
this.nodeStatusEvents = [];
|
|
536
536
|
this.variables = {};
|
|
537
537
|
this.retryAttempts = new Map();
|
|
538
|
+
this._nodeTimings = Object.create(null);
|
|
539
|
+
this._nodeInputs = Object.create(null);
|
|
538
540
|
}
|
|
539
541
|
|
|
540
542
|
/** Target repo for multi-repo workspaces (convenience accessor) */
|
|
@@ -575,6 +577,31 @@ export class WorkflowContext {
|
|
|
575
577
|
return forked;
|
|
576
578
|
}
|
|
577
579
|
|
|
580
|
+
/** Set a timing field for a node (startedAt / endedAt) */
|
|
581
|
+
setNodeTiming(nodeId, field, value) {
|
|
582
|
+
const key = String(nodeId);
|
|
583
|
+
const fieldKey = String(field);
|
|
584
|
+
if (!this._nodeTimings[key] || typeof this._nodeTimings[key] !== "object") {
|
|
585
|
+
this._nodeTimings[key] = Object.create(null);
|
|
586
|
+
}
|
|
587
|
+
this._nodeTimings[key][fieldKey] = value;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** Get timing data for a node */
|
|
591
|
+
getNodeTiming(nodeId) {
|
|
592
|
+
return this._nodeTimings[String(nodeId)] || null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** Store the resolved input snapshot for a node */
|
|
596
|
+
setNodeInput(nodeId, input) {
|
|
597
|
+
this._nodeInputs[String(nodeId)] = input;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** Get the stored input snapshot for a node */
|
|
601
|
+
getNodeInput(nodeId) {
|
|
602
|
+
return this._nodeInputs[String(nodeId)] || null;
|
|
603
|
+
}
|
|
604
|
+
|
|
578
605
|
/** Set output from a node */
|
|
579
606
|
setNodeOutput(nodeId, output) {
|
|
580
607
|
this.nodeOutputs.set(nodeId, output);
|
|
@@ -665,6 +692,8 @@ export class WorkflowContext {
|
|
|
665
692
|
logs: this.logs,
|
|
666
693
|
errors: this.errors,
|
|
667
694
|
nodeStatusEvents: this.nodeStatusEvents,
|
|
695
|
+
nodeTimings: { ...this._nodeTimings },
|
|
696
|
+
nodeInputs: { ...this._nodeInputs },
|
|
668
697
|
};
|
|
669
698
|
}
|
|
670
699
|
}
|
|
@@ -2051,7 +2080,199 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
2051
2080
|
? detail.data._taskWorkflowEvents
|
|
2052
2081
|
: [];
|
|
2053
2082
|
return events.map((event) => ({ ...event }));
|
|
2054
|
-
}
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
/**
|
|
2086
|
+
* Get detailed forensics for a single node in a run.
|
|
2087
|
+
* @param {string} runId
|
|
2088
|
+
* @param {string} nodeId
|
|
2089
|
+
* @returns {object|null}
|
|
2090
|
+
*/
|
|
2091
|
+
getNodeForensics(runId, nodeId) {
|
|
2092
|
+
const run = this.getRunDetail(runId);
|
|
2093
|
+
if (!run) return null;
|
|
2094
|
+
const detail = run.detail || {};
|
|
2095
|
+
const nodeStatuses = detail.nodeStatuses || {};
|
|
2096
|
+
if (!(nodeId in nodeStatuses)) return null;
|
|
2097
|
+
|
|
2098
|
+
const timings = detail.nodeTimings?.[nodeId] || {};
|
|
2099
|
+
const startedAt = timings.startedAt || null;
|
|
2100
|
+
const endedAt = timings.endedAt || null;
|
|
2101
|
+
const durationMs = startedAt && endedAt ? Math.max(0, endedAt - startedAt) : null;
|
|
2102
|
+
|
|
2103
|
+
return {
|
|
2104
|
+
nodeId,
|
|
2105
|
+
status: nodeStatuses[nodeId] || null,
|
|
2106
|
+
startedAt,
|
|
2107
|
+
endedAt,
|
|
2108
|
+
durationMs,
|
|
2109
|
+
input: detail.nodeInputs?.[nodeId] || null,
|
|
2110
|
+
output: detail.nodeOutputs?.[nodeId] || null,
|
|
2111
|
+
errors: (detail.errors || []).filter((e) => e.nodeId === nodeId),
|
|
2112
|
+
retryAttempts: detail.retryAttempts?.[nodeId] || 0,
|
|
2113
|
+
statusEvents: (detail.nodeStatusEvents || []).filter((e) => e.nodeId === nodeId),
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
/**
|
|
2118
|
+
* Get forensics for all nodes in a run.
|
|
2119
|
+
* @param {string} runId
|
|
2120
|
+
* @returns {object|null}
|
|
2121
|
+
*/
|
|
2122
|
+
getRunForensics(runId) {
|
|
2123
|
+
const run = this.getRunDetail(runId);
|
|
2124
|
+
if (!run) return null;
|
|
2125
|
+
const detail = run.detail || {};
|
|
2126
|
+
const nodeStatuses = detail.nodeStatuses || {};
|
|
2127
|
+
const nodes = {};
|
|
2128
|
+
for (const nodeId of Object.keys(nodeStatuses)) {
|
|
2129
|
+
nodes[nodeId] = this.getNodeForensics(runId, nodeId);
|
|
2130
|
+
}
|
|
2131
|
+
return {
|
|
2132
|
+
runId,
|
|
2133
|
+
status: run.status || null,
|
|
2134
|
+
startedAt: detail.startedAt || null,
|
|
2135
|
+
endedAt: detail.endedAt || null,
|
|
2136
|
+
durationMs: detail.duration || null,
|
|
2137
|
+
nodes,
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
/**
|
|
2142
|
+
* Create a snapshot of a completed run for later restore.
|
|
2143
|
+
* @param {string} runId
|
|
2144
|
+
* @returns {{ snapshotId: string, path: string }|null}
|
|
2145
|
+
*/
|
|
2146
|
+
createRunSnapshot(runId) {
|
|
2147
|
+
const run = this.getRunDetail(runId);
|
|
2148
|
+
if (!run) return null;
|
|
2149
|
+
const detail = run.detail || {};
|
|
2150
|
+
const workflowId = run.workflowId || detail.data?._workflowId;
|
|
2151
|
+
const snapshotsDir = resolve(this.runsDir, "snapshots");
|
|
2152
|
+
mkdirSync(snapshotsDir, { recursive: true });
|
|
2153
|
+
const snapshotId = runId;
|
|
2154
|
+
const snapshotPath = resolve(snapshotsDir, `${snapshotId}.json`);
|
|
2155
|
+
const snapshot = {
|
|
2156
|
+
snapshotId,
|
|
2157
|
+
runId,
|
|
2158
|
+
workflowId,
|
|
2159
|
+
createdAt: Date.now(),
|
|
2160
|
+
nodeStatuses: detail.nodeStatuses || {},
|
|
2161
|
+
nodeOutputs: detail.nodeOutputs || {},
|
|
2162
|
+
nodeTimings: detail.nodeTimings || {},
|
|
2163
|
+
nodeInputs: detail.nodeInputs || {},
|
|
2164
|
+
retryAttempts: detail.retryAttempts || {},
|
|
2165
|
+
variables: detail.data || {},
|
|
2166
|
+
errors: detail.errors || [],
|
|
2167
|
+
};
|
|
2168
|
+
writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
2169
|
+
return { snapshotId, path: snapshotPath };
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/**
|
|
2173
|
+
* Restore a run from a snapshot — creates a new execution pre-seeded
|
|
2174
|
+
* with completed node state from the snapshot.
|
|
2175
|
+
* @param {string} snapshotId
|
|
2176
|
+
* @param {object} [opts]
|
|
2177
|
+
* @param {object} [opts.variables] - Override variables
|
|
2178
|
+
* @returns {Promise<object>}
|
|
2179
|
+
*/
|
|
2180
|
+
async restoreFromSnapshot(snapshotId, opts = {}) {
|
|
2181
|
+
const snapshotPath = resolve(this.runsDir, "snapshots", `${snapshotId}.json`);
|
|
2182
|
+
if (!existsSync(snapshotPath)) {
|
|
2183
|
+
throw new Error(`Snapshot "${snapshotId}" not found`);
|
|
2184
|
+
}
|
|
2185
|
+
const snapshot = JSON.parse(readFileSync(snapshotPath, "utf8"));
|
|
2186
|
+
const workflowId = snapshot.workflowId;
|
|
2187
|
+
const def = this.get(workflowId);
|
|
2188
|
+
if (!def) {
|
|
2189
|
+
throw new Error(`Workflow "${workflowId}" no longer exists — cannot restore`);
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
const inputData = {
|
|
2193
|
+
...def.variables,
|
|
2194
|
+
...(snapshot.variables || {}),
|
|
2195
|
+
...(opts.variables || {}),
|
|
2196
|
+
_workflowId: workflowId,
|
|
2197
|
+
_workflowName: def.name,
|
|
2198
|
+
_restoredFrom: snapshotId,
|
|
2199
|
+
};
|
|
2200
|
+
|
|
2201
|
+
// Remove internal keys that should be regenerated
|
|
2202
|
+
delete inputData._workflowId;
|
|
2203
|
+
delete inputData._workflowName;
|
|
2204
|
+
|
|
2205
|
+
const ctx = new WorkflowContext(inputData);
|
|
2206
|
+
ctx.data._workflowId = workflowId;
|
|
2207
|
+
ctx.data._workflowName = def.name;
|
|
2208
|
+
ctx.data._restoredFrom = snapshotId;
|
|
2209
|
+
ctx.variables = { ...def.variables, ...(opts.variables || {}) };
|
|
2210
|
+
|
|
2211
|
+
// Pre-seed completed nodes from snapshot
|
|
2212
|
+
const nodeStatuses = snapshot.nodeStatuses || {};
|
|
2213
|
+
const nodeOutputs = snapshot.nodeOutputs || {};
|
|
2214
|
+
for (const [nodeId, status] of Object.entries(nodeStatuses)) {
|
|
2215
|
+
if (status === "completed") {
|
|
2216
|
+
ctx.setNodeStatus(nodeId, NodeStatus.COMPLETED);
|
|
2217
|
+
if (nodeOutputs[nodeId] !== undefined) {
|
|
2218
|
+
ctx.setNodeOutput(nodeId, nodeOutputs[nodeId]);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
const retryRunId = ctx.id;
|
|
2224
|
+
this._activeRuns.set(retryRunId, {
|
|
2225
|
+
workflowId,
|
|
2226
|
+
workflowName: def.name,
|
|
2227
|
+
ctx,
|
|
2228
|
+
startedAt: ctx.startedAt,
|
|
2229
|
+
status: WorkflowStatus.RUNNING,
|
|
2230
|
+
});
|
|
2231
|
+
this.emit("run:start", { runId: retryRunId, workflowId, name: def.name, restoredFrom: snapshotId });
|
|
2232
|
+
|
|
2233
|
+
try {
|
|
2234
|
+
const adjacency = this._buildAdjacency(def);
|
|
2235
|
+
const entryNodes = this._findEntryNodes(def);
|
|
2236
|
+
await this._executeDag(def, entryNodes, adjacency, ctx, opts);
|
|
2237
|
+
const finalStatus = ctx.errors.length > 0 ? WorkflowStatus.FAILED : WorkflowStatus.COMPLETED;
|
|
2238
|
+
this._persistRun(retryRunId, workflowId, ctx);
|
|
2239
|
+
this._activeRuns.delete(retryRunId);
|
|
2240
|
+
return { runId: retryRunId, snapshotId, workflowId, ctx, status: finalStatus };
|
|
2241
|
+
} catch (err) {
|
|
2242
|
+
this._persistRun(retryRunId, workflowId, ctx);
|
|
2243
|
+
this._activeRuns.delete(retryRunId);
|
|
2244
|
+
throw err;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
/**
|
|
2249
|
+
* List available snapshots, optionally filtered by workflowId.
|
|
2250
|
+
* @param {string} [workflowId]
|
|
2251
|
+
* @returns {Array<object>}
|
|
2252
|
+
*/
|
|
2253
|
+
listSnapshots(workflowId) {
|
|
2254
|
+
const snapshotsDir = resolve(this.runsDir, "snapshots");
|
|
2255
|
+
if (!existsSync(snapshotsDir)) return [];
|
|
2256
|
+
const files = readdirSync(snapshotsDir).filter((f) => f.endsWith(".json"));
|
|
2257
|
+
const snapshots = [];
|
|
2258
|
+
for (const file of files) {
|
|
2259
|
+
try {
|
|
2260
|
+
const data = JSON.parse(readFileSync(resolve(snapshotsDir, file), "utf8"));
|
|
2261
|
+
if (workflowId && data.workflowId !== workflowId) continue;
|
|
2262
|
+
snapshots.push({
|
|
2263
|
+
snapshotId: data.snapshotId,
|
|
2264
|
+
runId: data.runId,
|
|
2265
|
+
workflowId: data.workflowId,
|
|
2266
|
+
createdAt: data.createdAt,
|
|
2267
|
+
});
|
|
2268
|
+
} catch {
|
|
2269
|
+
// skip corrupt snapshot files
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
return snapshots.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// ── Internal DAG Execution ────────────────────────────────────────────
|
|
2055
2276
|
|
|
2056
2277
|
_buildAdjacency(def) {
|
|
2057
2278
|
const adj = new Map();
|
|
@@ -2639,6 +2860,9 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
2639
2860
|
// Resolve config templates against context
|
|
2640
2861
|
const resolvedConfig = this._resolveConfig(node.config || {}, ctx);
|
|
2641
2862
|
|
|
2863
|
+
// Capture resolved input snapshot for forensics
|
|
2864
|
+
ctx.setNodeInput(node.id, resolvedConfig);
|
|
2865
|
+
|
|
2642
2866
|
// Dry run — skip capability checks and handler execution.
|
|
2643
2867
|
// Services aren't needed for simulation; this keeps dry-run tests fast.
|
|
2644
2868
|
if (opts.dryRun) {
|
|
@@ -2668,6 +2892,7 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
2668
2892
|
// Execute with timeout — clear timer on completion to avoid resource leaks
|
|
2669
2893
|
const timeout = resolveNodeTimeoutMs(node, resolvedConfig);
|
|
2670
2894
|
let timer;
|
|
2895
|
+
ctx.setNodeTiming(node.id, "startedAt", Date.now());
|
|
2671
2896
|
try {
|
|
2672
2897
|
const result = await Promise.race([
|
|
2673
2898
|
handler.execute(
|
|
@@ -2679,7 +2904,11 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
2679
2904
|
timer = setTimeout(() => reject(new Error(`Node "${node.label || node.id}" timed out after ${timeout}ms`)), timeout);
|
|
2680
2905
|
}),
|
|
2681
2906
|
]);
|
|
2907
|
+
ctx.setNodeTiming(node.id, "endedAt", Date.now());
|
|
2682
2908
|
return result;
|
|
2909
|
+
} catch (err) {
|
|
2910
|
+
ctx.setNodeTiming(node.id, "endedAt", Date.now());
|
|
2911
|
+
throw err;
|
|
2683
2912
|
} finally {
|
|
2684
2913
|
clearTimeout(timer);
|
|
2685
2914
|
}
|
|
@@ -2969,6 +3198,16 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
2969
3198
|
normalized.activeNodeCount = 0;
|
|
2970
3199
|
}
|
|
2971
3200
|
if (normalized.status !== WorkflowStatus.RUNNING) {
|
|
3201
|
+
normalized.activeNodeCount = 0;
|
|
3202
|
+
if (!Number.isFinite(Number(normalized.endedAt))) {
|
|
3203
|
+
const fallbackEndedAt = Math.max(
|
|
3204
|
+
Number(normalized.interruptedAt) || 0,
|
|
3205
|
+
Number(normalized.lastProgressAt) || 0,
|
|
3206
|
+
Number(normalized.lastLogAt) || 0,
|
|
3207
|
+
Number(normalized.startedAt) || 0,
|
|
3208
|
+
);
|
|
3209
|
+
normalized.endedAt = fallbackEndedAt > 0 ? fallbackEndedAt : null;
|
|
3210
|
+
}
|
|
2972
3211
|
normalized.isStuck = false;
|
|
2973
3212
|
normalized.stuckMs = 0;
|
|
2974
3213
|
return normalized;
|
|
@@ -3172,6 +3411,10 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
3172
3411
|
summary.status = WorkflowStatus.PAUSED;
|
|
3173
3412
|
summary.resumable = canResume;
|
|
3174
3413
|
summary.interruptedAt = now;
|
|
3414
|
+
summary.activeNodeCount = 0;
|
|
3415
|
+
if (!Number.isFinite(Number(summary.endedAt))) {
|
|
3416
|
+
summary.endedAt = now;
|
|
3417
|
+
}
|
|
3175
3418
|
if (!canResume) summary.resumeResult = "recovery_cap_exceeded";
|
|
3176
3419
|
}
|
|
3177
3420
|
if (canResume && !forceResumable) resumableStaleRunsAssigned += 1;
|
|
@@ -3253,7 +3496,8 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
3253
3496
|
this._resumingRuns = true;
|
|
3254
3497
|
|
|
3255
3498
|
try {
|
|
3256
|
-
const
|
|
3499
|
+
const allRuns = this._readRunIndex();
|
|
3500
|
+
const runs = allRuns.filter(
|
|
3257
3501
|
(r) => r.status === WorkflowStatus.PAUSED && r.resumable,
|
|
3258
3502
|
);
|
|
3259
3503
|
|
|
@@ -3272,9 +3516,6 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
3272
3516
|
// and mark older duplicates as not-resumable before we even try them.
|
|
3273
3517
|
const runDetailCache = new Map(); // runId → parsed detail
|
|
3274
3518
|
const latestByTaskId = new Map(); // taskId → run entry (highest startedAt)
|
|
3275
|
-
|
|
3276
|
-
const allRuns = this._readRunIndex();
|
|
3277
|
-
|
|
3278
3519
|
for (const run of allRuns) {
|
|
3279
3520
|
const dp = resolve(this.runsDir, `${run.runId}.json`);
|
|
3280
3521
|
if (!existsSync(dp)) continue;
|
|
@@ -542,6 +542,43 @@ async function createKanbanTaskWithProject(kanban, taskData = {}, projectIdValue
|
|
|
542
542
|
payload.projectId = resolvedProjectId;
|
|
543
543
|
}
|
|
544
544
|
|
|
545
|
+
const createTaskParamNames = (() => {
|
|
546
|
+
try {
|
|
547
|
+
const inspectTarget =
|
|
548
|
+
typeof kanban.createTask?.getMockImplementation === "function"
|
|
549
|
+
? kanban.createTask.getMockImplementation() || kanban.createTask
|
|
550
|
+
: kanban.createTask;
|
|
551
|
+
const source = Function.prototype.toString.call(inspectTarget);
|
|
552
|
+
const parenMatch = source.match(/^[^(]*\(([^)]*)\)/s);
|
|
553
|
+
if (parenMatch) {
|
|
554
|
+
return String(parenMatch[1] || "")
|
|
555
|
+
.split(",")
|
|
556
|
+
.map((entry) =>
|
|
557
|
+
String(entry || "")
|
|
558
|
+
.trim()
|
|
559
|
+
.replace(/^\.{3}/, "")
|
|
560
|
+
.replace(/\s*=.*$/s, "")
|
|
561
|
+
.trim(),
|
|
562
|
+
)
|
|
563
|
+
.filter(Boolean);
|
|
564
|
+
}
|
|
565
|
+
const arrowMatch = source.match(/^(?:async\s+)?([A-Za-z_$][\w$]*)\s*=>/);
|
|
566
|
+
if (arrowMatch?.[1]) return [arrowMatch[1]];
|
|
567
|
+
} catch {
|
|
568
|
+
// Fall back to the project-aware signature when adapter source is opaque.
|
|
569
|
+
}
|
|
570
|
+
return [];
|
|
571
|
+
})();
|
|
572
|
+
const firstParamName = String(createTaskParamNames[0] || "").toLowerCase();
|
|
573
|
+
const payloadOnlyCreateTask =
|
|
574
|
+
createTaskParamNames.length === 1 &&
|
|
575
|
+
/(task|payload|spec|data)/i.test(firstParamName) &&
|
|
576
|
+
!/project/i.test(firstParamName);
|
|
577
|
+
|
|
578
|
+
if (payloadOnlyCreateTask) {
|
|
579
|
+
return kanban.createTask(payload);
|
|
580
|
+
}
|
|
581
|
+
|
|
545
582
|
const taskPayload = { ...payload };
|
|
546
583
|
delete taskPayload.projectId;
|
|
547
584
|
return kanban.createTask(resolvedProjectId, taskPayload);
|