@fieldwangai/agentflow 0.1.29 → 0.1.30
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/agents/agentflow-node-executor-code.md +3 -2
- package/agents/agentflow-node-executor-planning.md +3 -2
- package/agents/agentflow-node-executor-requirement.md +3 -2
- package/agents/agentflow-node-executor-test.md +3 -2
- package/agents/agentflow-node-executor-ui.md +3 -2
- package/agents/agentflow-node-executor.md +3 -2
- package/agents/en/agentflow-node-executor.md +3 -2
- package/agents/zh/agentflow-node-executor.md +3 -2
- package/bin/lib/agent-runners.mjs +38 -13
- package/bin/lib/api-runner.mjs +6 -3
- package/bin/lib/auth.mjs +240 -0
- package/bin/lib/catalog-agents.mjs +2 -2
- package/bin/lib/catalog-flows.mjs +192 -16
- package/bin/lib/composer-agent.mjs +21 -1
- package/bin/lib/composer-skill-router.mjs +10 -78
- package/bin/lib/flow-import.mjs +2 -2
- package/bin/lib/flow-write.mjs +20 -20
- package/bin/lib/help.mjs +2 -2
- package/bin/lib/locales/en.json +25 -1
- package/bin/lib/locales/zh.json +25 -1
- package/bin/lib/main.mjs +6 -1
- package/bin/lib/node-exec-context.mjs +5 -5
- package/bin/lib/node-execute.mjs +14 -9
- package/bin/lib/paths.mjs +64 -13
- package/bin/lib/recent-runs.mjs +2 -2
- package/bin/lib/run-node-statuses-from-disk.mjs +3 -3
- package/bin/lib/runtime-context.mjs +225 -0
- package/bin/lib/scheduler.mjs +41 -38
- package/bin/lib/skill-registry.mjs +145 -0
- package/bin/lib/ui-server.mjs +902 -57
- package/bin/lib/workspace-tree.mjs +4 -3
- package/bin/lib/workspace.mjs +9 -11
- package/bin/pipeline/build-node-prompt.mjs +29 -4
- package/bin/pipeline/get-exec-id.mjs +2 -2
- package/bin/pipeline/get-resolved-values.mjs +1 -0
- package/bin/pipeline/pre-process-node.mjs +306 -6
- package/bin/pipeline/validate-flow.mjs +2 -0
- package/builtin/nodes/agent_subAgent.md +7 -1
- package/builtin/nodes/control_cd_workspace.md +43 -0
- package/builtin/nodes/control_load_skills.md +48 -0
- package/builtin/nodes/display_ascii.md +17 -0
- package/builtin/nodes/display_markdown.md +17 -0
- package/builtin/nodes/display_mermaid.md +17 -0
- package/builtin/nodes/tool_git_checkout.md +54 -0
- package/builtin/nodes/tool_nodejs.md +8 -1
- package/builtin/nodes/tool_print.md +4 -1
- package/builtin/web-ui/dist/assets/index-NdVOJLL9.js +196 -0
- package/builtin/web-ui/dist/assets/index-naVI6LZj.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-flow-recipes/SKILL.md +24 -0
- package/skills/agentflow-flow-recipes/references/recipes.md +63 -0
- package/skills/agentflow-node-reference/SKILL.md +25 -0
- package/skills/agentflow-node-reference/references/builtin-nodes.md +210 -0
- package/skills/agentflow-placeholder-reference/SKILL.md +24 -0
- package/skills/agentflow-placeholder-reference/references/placeholders.md +20 -0
- package/skills/agentflow-runtime-reference/SKILL.md +25 -0
- package/skills/agentflow-runtime-reference/references/runtime.md +64 -0
- package/builtin/web-ui/dist/assets/index-0vJxkTJz.css +0 -1
- package/builtin/web-ui/dist/assets/index-h69bpxLI.js +0 -190
|
@@ -221,13 +221,14 @@ function readFilesRecursive(dir, baseDir, maxDepth = 2, currentDepth = 0) {
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
export function getPipelineFiles(workspaceRoot, flowId, flowSource, archived = false) {
|
|
224
|
+
export function getPipelineFiles(workspaceRoot, flowId, flowSource, archived = false, opts = {}) {
|
|
225
225
|
const root = path.resolve(workspaceRoot);
|
|
226
226
|
let pipelineDir = null;
|
|
227
|
+
const userPipelinesRoot = getUserPipelinesRoot(opts.userId);
|
|
227
228
|
|
|
228
229
|
if (archived) {
|
|
229
230
|
if (flowSource === "user") {
|
|
230
|
-
pipelineDir = path.join(
|
|
231
|
+
pipelineDir = path.join(userPipelinesRoot, ARCHIVED_PIPELINES_DIR_NAME, flowId);
|
|
231
232
|
} else if (flowSource === "workspace") {
|
|
232
233
|
pipelineDir = path.join(root, PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowId);
|
|
233
234
|
if (!fs.existsSync(pipelineDir)) {
|
|
@@ -239,7 +240,7 @@ export function getPipelineFiles(workspaceRoot, flowId, flowSource, archived = f
|
|
|
239
240
|
if (flowSource === "builtin") {
|
|
240
241
|
pipelineDir = path.join(PACKAGE_BUILTIN_PIPELINES_DIR, flowId);
|
|
241
242
|
} else if (flowSource === "user") {
|
|
242
|
-
pipelineDir = path.join(
|
|
243
|
+
pipelineDir = path.join(userPipelinesRoot, flowId);
|
|
243
244
|
if (!fs.existsSync(pipelineDir)) {
|
|
244
245
|
const alt = path.join(root, PIPELINES_DIR, flowId);
|
|
245
246
|
if (fs.existsSync(alt)) pipelineDir = alt;
|
package/bin/lib/workspace.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getReferenceRootAbs,
|
|
10
10
|
getWorkspaceRunBuildRoot,
|
|
11
11
|
getUserPipelinesRoot,
|
|
12
|
+
resolveUniqueUserPipelineDir,
|
|
12
13
|
ARCHIVED_PIPELINES_DIR_NAME,
|
|
13
14
|
} from "./paths.mjs";
|
|
14
15
|
|
|
@@ -23,7 +24,7 @@ export { getRunDir } from "./paths.mjs";
|
|
|
23
24
|
* - legacyUserRoot: ~/agentflow/runBuild/<name>/<uuid>
|
|
24
25
|
* 后两者仅作为向下兼容读取;新写入只走 user/workspace。
|
|
25
26
|
*/
|
|
26
|
-
export function listAllRunDirs(workspaceRoot) {
|
|
27
|
+
export function listAllRunDirs(workspaceRoot, opts = {}) {
|
|
27
28
|
const root = path.resolve(workspaceRoot);
|
|
28
29
|
const out = [];
|
|
29
30
|
const seen = new Set();
|
|
@@ -62,7 +63,7 @@ export function listAllRunDirs(workspaceRoot) {
|
|
|
62
63
|
};
|
|
63
64
|
|
|
64
65
|
// 新位置(优先)
|
|
65
|
-
scanPipelinesDir(getUserPipelinesRoot(), "user");
|
|
66
|
+
scanPipelinesDir(getUserPipelinesRoot(opts.userId), "user");
|
|
66
67
|
scanPipelinesDir(path.join(root, PIPELINES_DIR), "workspace");
|
|
67
68
|
|
|
68
69
|
// 旧位置(兼容读)
|
|
@@ -138,25 +139,22 @@ export function listRunsWithLogs(workspaceRoot) {
|
|
|
138
139
|
return list;
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
/**
|
|
142
|
-
export function getFlowDir(workspaceRoot, flowName) {
|
|
142
|
+
/** 解析活跃 flow 目录:user → workspace → legacy workspace → builtin。归档 flow 必须走显式 archived resolver。 */
|
|
143
|
+
export function getFlowDir(workspaceRoot, flowName, opts = {}) {
|
|
143
144
|
const root = path.resolve(workspaceRoot);
|
|
144
145
|
const hasFlow = (dir) => fs.existsSync(dir) && fs.existsSync(path.join(dir, "flow.yaml"));
|
|
145
146
|
|
|
146
147
|
// user pipelines
|
|
147
|
-
const userRoot = getUserPipelinesRoot();
|
|
148
|
+
const userRoot = getUserPipelinesRoot(opts.userId);
|
|
148
149
|
const userFlowDir = path.join(userRoot, flowName);
|
|
149
150
|
if (hasFlow(userFlowDir)) return userFlowDir;
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
151
|
+
|
|
152
|
+
const inferredUserFlowDir = resolveUniqueUserPipelineDir(flowName);
|
|
153
|
+
if (inferredUserFlowDir) return inferredUserFlowDir;
|
|
153
154
|
|
|
154
155
|
// workspace pipelines
|
|
155
156
|
const wsFlowDir = path.join(root, PIPELINES_DIR, flowName);
|
|
156
157
|
if (hasFlow(wsFlowDir)) return wsFlowDir;
|
|
157
|
-
// workspace archived
|
|
158
|
-
const wsArchivedDir = path.join(root, PIPELINES_DIR, ARCHIVED_PIPELINES_DIR_NAME, flowName);
|
|
159
|
-
if (hasFlow(wsArchivedDir)) return wsArchivedDir;
|
|
160
158
|
|
|
161
159
|
// legacy
|
|
162
160
|
const legacyFlowDir = path.join(root, LEGACY_PIPELINES_DIR, flowName);
|
|
@@ -16,6 +16,7 @@ import { loadFlowDefinition } from "./parse-flow.mjs";
|
|
|
16
16
|
import { getResolvedValues, getOutputPathForSlot } from "./get-resolved-values.mjs";
|
|
17
17
|
import { loadExecId } from "./get-exec-id.mjs";
|
|
18
18
|
import { intermediatePromptBasename, intermediateDirForNode } from "./get-exec-id.mjs";
|
|
19
|
+
import { normalizeSkillsContext, normalizeWorkspaceContext, renderSkillsContextForPrompt } from "../lib/runtime-context.mjs";
|
|
19
20
|
|
|
20
21
|
function shellQuote(s) {
|
|
21
22
|
if (s == null) return "''";
|
|
@@ -116,7 +117,7 @@ function marketplaceRuntimeCommand(marketplaceNode, resolvedInputs, resolvedOutp
|
|
|
116
117
|
* @param {number} [execId] - 本轮 execId,缺省则从 memory 读取
|
|
117
118
|
* @returns {{ ok: boolean, promptPath?: string, nodeContext?: string, taskBody?: string, error?: string }}
|
|
118
119
|
*/
|
|
119
|
-
export function buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execId) {
|
|
120
|
+
export function buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execId, opts = {}) {
|
|
120
121
|
const runDir = getRunDir(workspaceRoot, flowName, uuid);
|
|
121
122
|
const flowJsonPath = path.join(runDir, "intermediate", "flow.json");
|
|
122
123
|
let flowDir = getFlowDir(workspaceRoot, flowName) || path.join(workspaceRoot, PIPELINES_DIR, flowName);
|
|
@@ -154,7 +155,26 @@ export function buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execI
|
|
|
154
155
|
: "";
|
|
155
156
|
|
|
156
157
|
const { resolvedInputs = {}, resolvedOutputs = {}, systemPrompt = "" } = data;
|
|
157
|
-
const
|
|
158
|
+
const workspaceContext = normalizeWorkspaceContext(opts.workspaceContext || resolvedInputs.workspaceContext, workspaceRoot, flowName, { flowDir });
|
|
159
|
+
const skillsContext = normalizeSkillsContext(opts.skillsContext || resolvedInputs.skillsContext);
|
|
160
|
+
if (workspaceContext?.workspaceRoot) {
|
|
161
|
+
resolvedInputs.workspaceRoot = workspaceContext.workspaceRoot;
|
|
162
|
+
resolvedInputs.cwd = workspaceContext.cwd || workspaceContext.workspaceRoot;
|
|
163
|
+
resolvedInputs.pipelineWorkspace = workspaceContext.pipelineWorkspace || path.resolve(workspaceRoot);
|
|
164
|
+
}
|
|
165
|
+
const resolveOpts = {
|
|
166
|
+
instanceId,
|
|
167
|
+
currentExecId: e,
|
|
168
|
+
runDir,
|
|
169
|
+
workspaceRoot: workspaceContext?.workspaceRoot || workspaceRoot,
|
|
170
|
+
extra: {
|
|
171
|
+
workspaceRoot: workspaceContext?.workspaceRoot || path.resolve(workspaceRoot),
|
|
172
|
+
cwd: workspaceContext?.cwd || workspaceContext?.workspaceRoot || path.resolve(workspaceRoot),
|
|
173
|
+
pipelineWorkspace: workspaceContext?.pipelineWorkspace || path.resolve(workspaceRoot),
|
|
174
|
+
flowDir: path.resolve(flowDir),
|
|
175
|
+
runDir,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
158
178
|
const taskBody = resolvePlaceholdersInText(
|
|
159
179
|
instanceBody,
|
|
160
180
|
resolvedInputs,
|
|
@@ -168,9 +188,12 @@ export function buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execI
|
|
|
168
188
|
? marketplaceRuntimeCommand(marketplaceNode, resolvedInputs, resolvedOutputs, resolveOpts)
|
|
169
189
|
: "";
|
|
170
190
|
|
|
191
|
+
const skillsPrompt = renderSkillsContextForPrompt(skillsContext);
|
|
192
|
+
const contextBlocks = [systemPrompt || "(无)", skillsPrompt].filter((x) => x && String(x).trim());
|
|
193
|
+
|
|
171
194
|
const content = `## 节点上下文
|
|
172
195
|
|
|
173
|
-
${
|
|
196
|
+
${contextBlocks.join("\n\n")}
|
|
174
197
|
|
|
175
198
|
## 执行任务
|
|
176
199
|
|
|
@@ -191,9 +214,11 @@ ${taskBody || "(无)"}
|
|
|
191
214
|
return {
|
|
192
215
|
ok: true,
|
|
193
216
|
promptPath: relativePath.replace(/\\/g, "/"),
|
|
194
|
-
nodeContext:
|
|
217
|
+
nodeContext: contextBlocks.join("\n\n") || "",
|
|
195
218
|
taskBody: taskBody || "",
|
|
196
219
|
script: resolvedScript || "",
|
|
220
|
+
workspaceContext,
|
|
221
|
+
skillsContext,
|
|
197
222
|
};
|
|
198
223
|
}
|
|
199
224
|
|
|
@@ -53,8 +53,8 @@ export function loadExecId(workspaceRoot, flowName, uuid, instanceId) {
|
|
|
53
53
|
/**
|
|
54
54
|
* 从 memory 读取所有 order 中节点的 execId。
|
|
55
55
|
*/
|
|
56
|
-
export function loadAllExecIds(workspaceRoot, flowName, uuid, order) {
|
|
57
|
-
const runDir = getRunDir(workspaceRoot, flowName, uuid);
|
|
56
|
+
export function loadAllExecIds(workspaceRoot, flowName, uuid, order, opts = {}) {
|
|
57
|
+
const runDir = getRunDir(workspaceRoot, flowName, uuid, opts);
|
|
58
58
|
const memoryPath = path.join(runDir, MEMORY_FILENAME);
|
|
59
59
|
const map = fs.existsSync(memoryPath)
|
|
60
60
|
? parseMemory(fs.readFileSync(memoryPath, "utf-8"))
|
|
@@ -233,6 +233,7 @@ export function getResolvedValues(workspaceRoot, flowName, uuid, instanceId) {
|
|
|
233
233
|
// 运行时常量放在后面,确保不会被 input 槽位的空值覆盖
|
|
234
234
|
const runtimeConstants = {
|
|
235
235
|
workspaceRoot: path.resolve(workspaceRoot),
|
|
236
|
+
pipelineWorkspace: path.resolve(workspaceRoot),
|
|
236
237
|
flowName,
|
|
237
238
|
runDir: runDirRel,
|
|
238
239
|
flowDir: path.resolve(flowDir),
|
|
@@ -36,7 +36,16 @@ import { parseBool, getFirstBoolInputValue } from "./parse-bool.mjs";
|
|
|
36
36
|
import { writeResult } from "./write-result.mjs";
|
|
37
37
|
import { intermediateResultBasename, intermediateCacheBasename, intermediateDirForNode, outputNodeBasename, outputDirForNode } from "./get-exec-id.mjs";
|
|
38
38
|
import { logToRunTag } from "./run-log.mjs";
|
|
39
|
-
import { getRunDir } from "../lib/paths.mjs";
|
|
39
|
+
import { getRunDir, sanitizeAgentflowUserId } from "../lib/paths.mjs";
|
|
40
|
+
import {
|
|
41
|
+
buildSkillsContext,
|
|
42
|
+
buildSkillsContextFromRegistry,
|
|
43
|
+
expandRuntimePlaceholders,
|
|
44
|
+
normalizeSkillsContext,
|
|
45
|
+
normalizeWorkspaceContext,
|
|
46
|
+
parseSkillKeyList,
|
|
47
|
+
resolveWorkspaceTarget,
|
|
48
|
+
} from "../lib/runtime-context.mjs";
|
|
40
49
|
|
|
41
50
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
42
51
|
|
|
@@ -124,6 +133,12 @@ function bashSingleQuote(s) {
|
|
|
124
133
|
return "'" + String(s).replace(/'/g, "'\\''") + "'";
|
|
125
134
|
}
|
|
126
135
|
|
|
136
|
+
function agentflowCommand() {
|
|
137
|
+
const userId = sanitizeAgentflowUserId(process.env.AGENTFLOW_USER_ID);
|
|
138
|
+
if (!userId) return "agentflow";
|
|
139
|
+
return `AGENTFLOW_USER_ID=${bashSingleQuote(userId)} agentflow`;
|
|
140
|
+
}
|
|
141
|
+
|
|
127
142
|
function parseDurationMs(raw) {
|
|
128
143
|
const text = String(raw || "").trim();
|
|
129
144
|
if (!text) throw new Error("duration is required");
|
|
@@ -209,6 +224,256 @@ function writeOutputSlot(runDir, instanceId, execId, slotName, value) {
|
|
|
209
224
|
fs.writeFileSync(p, String(value ?? "") + "\n", "utf-8");
|
|
210
225
|
}
|
|
211
226
|
|
|
227
|
+
function readFlowJsonObject(workspaceRoot, flowName, uuid) {
|
|
228
|
+
const flowJsonPath = path.join(getRunDir(workspaceRoot, flowName, uuid), "intermediate", "flow.json");
|
|
229
|
+
if (!fs.existsSync(flowJsonPath)) return null;
|
|
230
|
+
try {
|
|
231
|
+
const flow = JSON.parse(fs.readFileSync(flowJsonPath, "utf-8"));
|
|
232
|
+
return flow && typeof flow === "object" ? flow : null;
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId) {
|
|
239
|
+
const flowJson = readFlowJsonObject(workspaceRoot, flowName, uuid);
|
|
240
|
+
const data = getResolvedValues(workspaceRoot, flowName, uuid, instanceId);
|
|
241
|
+
const inputs = data.ok ? (data.resolvedInputs || {}) : {};
|
|
242
|
+
const workspaceContext = normalizeWorkspaceContext(inputs.workspaceContext, workspaceRoot, flowName, flowJson);
|
|
243
|
+
const skillsContext = normalizeSkillsContext(inputs.skillsContext);
|
|
244
|
+
return { inputs, workspaceContext, skillsContext, flowJson };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function sanitizeRepoDirName(repoUrl) {
|
|
248
|
+
const raw = String(repoUrl || "").trim().replace(/\.git$/i, "");
|
|
249
|
+
const last = raw.split(/[/:]/).filter(Boolean).pop() || "repo";
|
|
250
|
+
return last.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function runGit(args, cwd) {
|
|
254
|
+
return spawnSync("git", args, {
|
|
255
|
+
cwd,
|
|
256
|
+
encoding: "utf-8",
|
|
257
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isTruthyInput(value) {
|
|
262
|
+
const text = String(value ?? "").trim().toLowerCase();
|
|
263
|
+
return text === "true" || text === "1" || text === "yes" || text === "y" || text === "on";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function emitGitCheckoutNode(workspaceRoot, flowName, uuid, instanceId, execId, resultPathRel) {
|
|
267
|
+
const runDir = getRunDir(workspaceRoot, flowName, uuid);
|
|
268
|
+
const { inputs, workspaceContext } = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
|
|
269
|
+
const repoUrl = String(inputs.repoUrl || inputs.url || "").trim();
|
|
270
|
+
if (!repoUrl) throw new Error("tool_git_checkout: repoUrl is required");
|
|
271
|
+
const branch = String(inputs.branch || "").trim();
|
|
272
|
+
const pullIfExists = String(inputs.pullIfExists ?? "true").trim().toLowerCase() !== "false";
|
|
273
|
+
const includeSubmodules = isTruthyInput(inputs.includeSubmodules ?? inputs.submodules ?? inputs.pullSubmodules);
|
|
274
|
+
const defaultDir = path.join(workspaceContext.pipelineWorkspace || workspaceRoot, ".workspace", "agentflow", "git-repos", sanitizeRepoDirName(repoUrl));
|
|
275
|
+
const targetRaw = String(inputs.targetDir || "").trim();
|
|
276
|
+
const targetDir = targetRaw
|
|
277
|
+
? resolveWorkspaceTarget(targetRaw, workspaceContext, { repoName: sanitizeRepoDirName(repoUrl) })
|
|
278
|
+
: defaultDir;
|
|
279
|
+
|
|
280
|
+
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
|
|
281
|
+
let changed = false;
|
|
282
|
+
let action = "clone";
|
|
283
|
+
if (fs.existsSync(path.join(targetDir, ".git"))) {
|
|
284
|
+
action = pullIfExists ? "pull" : "exists";
|
|
285
|
+
if (pullIfExists) {
|
|
286
|
+
const fetch = runGit(["fetch", "--all", "--prune"], targetDir);
|
|
287
|
+
if (fetch.status !== 0) throw new Error(`git fetch failed: ${fetch.stderr || fetch.stdout}`);
|
|
288
|
+
if (branch) {
|
|
289
|
+
const checkout = runGit(["checkout", branch], targetDir);
|
|
290
|
+
if (checkout.status !== 0) throw new Error(`git checkout failed: ${checkout.stderr || checkout.stdout}`);
|
|
291
|
+
}
|
|
292
|
+
const before = runGit(["rev-parse", "HEAD"], targetDir).stdout.trim();
|
|
293
|
+
const pull = runGit(["pull", "--ff-only"], targetDir);
|
|
294
|
+
if (pull.status !== 0) throw new Error(`git pull failed: ${pull.stderr || pull.stdout}`);
|
|
295
|
+
const after = runGit(["rev-parse", "HEAD"], targetDir).stdout.trim();
|
|
296
|
+
changed = before !== after;
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
const args = ["clone"];
|
|
300
|
+
if (includeSubmodules) args.push("--recurse-submodules");
|
|
301
|
+
if (branch) args.push("--branch", branch);
|
|
302
|
+
args.push(repoUrl, targetDir);
|
|
303
|
+
const clone = runGit(args, workspaceContext.cwd || workspaceRoot);
|
|
304
|
+
if (clone.status !== 0) throw new Error(`git clone failed: ${clone.stderr || clone.stdout}`);
|
|
305
|
+
changed = true;
|
|
306
|
+
}
|
|
307
|
+
if (includeSubmodules) {
|
|
308
|
+
const submodule = runGit(["submodule", "update", "--init", "--recursive"], targetDir);
|
|
309
|
+
if (submodule.status !== 0) throw new Error(`git submodule update failed: ${submodule.stderr || submodule.stdout}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const currentBranch = runGit(["rev-parse", "--abbrev-ref", "HEAD"], targetDir).stdout.trim();
|
|
313
|
+
const commit = runGit(["rev-parse", "HEAD"], targetDir).stdout.trim();
|
|
314
|
+
const outWorkspaceContext = {
|
|
315
|
+
version: 1,
|
|
316
|
+
label: inputs.label || sanitizeRepoDirName(repoUrl),
|
|
317
|
+
cwd: path.resolve(targetDir),
|
|
318
|
+
workspaceRoot: path.resolve(targetDir),
|
|
319
|
+
pipelineWorkspace: workspaceContext.pipelineWorkspace || path.resolve(workspaceRoot),
|
|
320
|
+
flowDir: workspaceContext.flowDir,
|
|
321
|
+
previous: workspaceContext,
|
|
322
|
+
};
|
|
323
|
+
writeOutputSlot(runDir, instanceId, execId, "repoPath", targetDir);
|
|
324
|
+
writeOutputSlot(runDir, instanceId, execId, "branch", currentBranch);
|
|
325
|
+
writeOutputSlot(runDir, instanceId, execId, "commit", commit);
|
|
326
|
+
writeOutputSlot(runDir, instanceId, execId, "changed", changed ? "true" : "false");
|
|
327
|
+
writeOutputSlot(runDir, instanceId, execId, "workspaceContext", JSON.stringify(outWorkspaceContext));
|
|
328
|
+
writeResult(workspaceRoot, flowName, uuid, instanceId, { status: "success", message: `git ${action}: ${currentBranch}@${commit.slice(0, 8)}` }, { execId });
|
|
329
|
+
return emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "git-checkout", `Git checkout completed: ${targetDir}\n`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function emitCdWorkspaceNode(workspaceRoot, flowName, uuid, instanceId, execId) {
|
|
333
|
+
const runDir = getRunDir(workspaceRoot, flowName, uuid);
|
|
334
|
+
const { inputs, workspaceContext } = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
|
|
335
|
+
const mode = String(inputs.mode || "set").trim().toLowerCase();
|
|
336
|
+
let next;
|
|
337
|
+
if (mode === "pop") {
|
|
338
|
+
next = normalizeWorkspaceContext(workspaceContext.previous, workspaceRoot, flowName);
|
|
339
|
+
} else {
|
|
340
|
+
const target = resolveWorkspaceTarget(inputs.target || inputs.path || inputs.repoPath || "", workspaceContext);
|
|
341
|
+
if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
|
|
342
|
+
throw new Error(`control_cd_workspace: target directory not found: ${target}`);
|
|
343
|
+
}
|
|
344
|
+
next = {
|
|
345
|
+
version: 1,
|
|
346
|
+
label: String(inputs.label || path.basename(target) || "workspace").trim(),
|
|
347
|
+
cwd: path.resolve(target),
|
|
348
|
+
workspaceRoot: path.resolve(target),
|
|
349
|
+
pipelineWorkspace: workspaceContext.pipelineWorkspace || path.resolve(workspaceRoot),
|
|
350
|
+
flowDir: workspaceContext.flowDir,
|
|
351
|
+
previous: mode === "push" ? workspaceContext : workspaceContext.previous || null,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
writeOutputSlot(runDir, instanceId, execId, "workspaceContext", JSON.stringify(next));
|
|
355
|
+
writeOutputSlot(runDir, instanceId, execId, "cwd", next.cwd);
|
|
356
|
+
writeOutputSlot(runDir, instanceId, execId, "previous", next.previous ? JSON.stringify(next.previous) : "");
|
|
357
|
+
writeResult(workspaceRoot, flowName, uuid, instanceId, { status: "success", message: `cwd=${next.cwd}` }, { execId });
|
|
358
|
+
return emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "cd-workspace", `Workspace context switched to: ${next.cwd}\n`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function emitLoadSkillsNode(workspaceRoot, flowName, uuid, instanceId, execId) {
|
|
362
|
+
const runDir = getRunDir(workspaceRoot, flowName, uuid);
|
|
363
|
+
const { inputs, workspaceContext, skillsContext: existingSkills } = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
|
|
364
|
+
const mergeMode = String(inputs.mergeMode || "replace").trim();
|
|
365
|
+
const skillKeys = parseSkillKeyList(inputs.skillKeys || inputs.skills || inputs.keys || "");
|
|
366
|
+
let source = "public-registry";
|
|
367
|
+
let loaded;
|
|
368
|
+
if (skillKeys.length > 0) {
|
|
369
|
+
loaded = buildSkillsContextFromRegistry({ workspaceContext, skillKeys, mergeMode });
|
|
370
|
+
} else {
|
|
371
|
+
source = String(inputs.source || "current-workspace").trim();
|
|
372
|
+
const paths = String(inputs.paths || "")
|
|
373
|
+
.split(/\r?\n|,/)
|
|
374
|
+
.map((x) => expandRuntimePlaceholders(x, workspaceContext).trim())
|
|
375
|
+
.filter(Boolean);
|
|
376
|
+
const include = String(inputs.include || "").split(/[\s,]+/).map((x) => x.trim()).filter(Boolean);
|
|
377
|
+
const exclude = String(inputs.exclude || "").split(/[\s,]+/).map((x) => x.trim()).filter(Boolean);
|
|
378
|
+
loaded = buildSkillsContext({ workspaceContext, source, paths, include, exclude, mergeMode });
|
|
379
|
+
}
|
|
380
|
+
let next = loaded;
|
|
381
|
+
if (existingSkills && mergeMode !== "replace") {
|
|
382
|
+
const existingBodies = Array.isArray(existingSkills.skillBodies) ? existingSkills.skillBodies : [];
|
|
383
|
+
const loadedBodies = Array.isArray(loaded.skillBodies) ? loaded.skillBodies : [];
|
|
384
|
+
const skillBodies = mergeMode === "prepend" ? [...loadedBodies, ...existingBodies] : [...existingBodies, ...loadedBodies];
|
|
385
|
+
const seen = new Set();
|
|
386
|
+
next = {
|
|
387
|
+
...loaded,
|
|
388
|
+
skillBodies: skillBodies.filter((s) => {
|
|
389
|
+
const key = s.key || s.name;
|
|
390
|
+
if (seen.has(key)) return false;
|
|
391
|
+
seen.add(key);
|
|
392
|
+
return true;
|
|
393
|
+
}),
|
|
394
|
+
};
|
|
395
|
+
next.skills = next.skillBodies.map(({ body, ...meta }) => meta);
|
|
396
|
+
next.skillKeys = next.skills.map((s) => s.key);
|
|
397
|
+
next.loadedCount = next.skills.length;
|
|
398
|
+
}
|
|
399
|
+
writeOutputSlot(runDir, instanceId, execId, "skillsContext", JSON.stringify(next));
|
|
400
|
+
writeOutputSlot(runDir, instanceId, execId, "loadedCount", String(next.loadedCount || 0));
|
|
401
|
+
writeOutputSlot(runDir, instanceId, execId, "summary", `${next.loadedCount || 0} skills loaded from ${source}`);
|
|
402
|
+
writeResult(workspaceRoot, flowName, uuid, instanceId, { status: "success", message: `加载 ${next.loadedCount || 0} 个 skills` }, { execId });
|
|
403
|
+
return emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "load-skills", `Loaded ${next.loadedCount || 0} skills.\n`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function readPrintableValue(value, runDir) {
|
|
407
|
+
const text = String(value ?? "").trim();
|
|
408
|
+
if (!text) return "";
|
|
409
|
+
const candidates = [];
|
|
410
|
+
if (path.isAbsolute(text)) candidates.push(text);
|
|
411
|
+
candidates.push(path.join(runDir, text));
|
|
412
|
+
for (const candidate of candidates) {
|
|
413
|
+
try {
|
|
414
|
+
if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) continue;
|
|
415
|
+
const stat = fs.statSync(candidate);
|
|
416
|
+
if (stat.size > 1024 * 1024) {
|
|
417
|
+
return fs.readFileSync(candidate, "utf-8").slice(0, 1024 * 1024) + "\n\n[内容超过 1MB,已截断]";
|
|
418
|
+
}
|
|
419
|
+
return fs.readFileSync(candidate, "utf-8").trim();
|
|
420
|
+
} catch (_) {}
|
|
421
|
+
}
|
|
422
|
+
return text;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function readResultBody(runDir, sourceInstanceId) {
|
|
426
|
+
const id = String(sourceInstanceId || "").trim();
|
|
427
|
+
if (!id) return "";
|
|
428
|
+
const resultPath = path.join(runDir, intermediateDirForNode(id), intermediateResultBasename(id, 1));
|
|
429
|
+
try {
|
|
430
|
+
if (!fs.existsSync(resultPath)) return "";
|
|
431
|
+
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
432
|
+
const match = raw.match(/---\s*\r?\n[\s\S]*?\r?\n---\s*\r?\n([\s\S]*)$/);
|
|
433
|
+
return (match ? match[1] : raw).trim();
|
|
434
|
+
} catch (_) {
|
|
435
|
+
return "";
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function emitToolPrintNode(workspaceRoot, flowName, uuid, instanceId, execId) {
|
|
440
|
+
const runDir = getRunDir(workspaceRoot, flowName, uuid);
|
|
441
|
+
const flowJson = readFlowJsonObject(workspaceRoot, flowName, uuid);
|
|
442
|
+
const data = getResolvedValues(workspaceRoot, flowName, uuid, instanceId);
|
|
443
|
+
const inputs = data.ok ? (data.resolvedInputs || {}) : {};
|
|
444
|
+
const skipNames = new Set(["prev", "next", "workspaceContext", "skillsContext", "workspaceRoot", "pipelineWorkspace", "flowName", "runDir", "flowDir", "cwd"]);
|
|
445
|
+
|
|
446
|
+
let content = readPrintableValue(inputs.content, runDir);
|
|
447
|
+
if (!content) {
|
|
448
|
+
const parts = [];
|
|
449
|
+
for (const [name, value] of Object.entries(inputs)) {
|
|
450
|
+
if (skipNames.has(name)) continue;
|
|
451
|
+
const v = readPrintableValue(value, runDir);
|
|
452
|
+
if (!v) continue;
|
|
453
|
+
parts.push(name === "content" ? v : `## ${name}\n\n${v}`);
|
|
454
|
+
}
|
|
455
|
+
content = parts.join("\n\n").trim();
|
|
456
|
+
}
|
|
457
|
+
if (!content && inputs.prev) {
|
|
458
|
+
content = readResultBody(runDir, inputs.prev);
|
|
459
|
+
}
|
|
460
|
+
if (!content && flowJson?.nodes) {
|
|
461
|
+
const node = flowJson.nodes.find((n) => n.id === instanceId);
|
|
462
|
+
content = String(node?.body || "").trim();
|
|
463
|
+
}
|
|
464
|
+
if (!content) content = "(tool_print 没有可展示内容:请填写 content 输入,或连接上游输出到 content。)";
|
|
465
|
+
|
|
466
|
+
writeResult(
|
|
467
|
+
workspaceRoot,
|
|
468
|
+
flowName,
|
|
469
|
+
uuid,
|
|
470
|
+
instanceId,
|
|
471
|
+
{ status: "success", message: "Print 输出" },
|
|
472
|
+
{ execId, preserveBody: false, body: content },
|
|
473
|
+
);
|
|
474
|
+
return emitLocalNoopPrompt(workspaceRoot, runDir, instanceId, "tool-print", `Printed content for ${instanceId}.\n`);
|
|
475
|
+
}
|
|
476
|
+
|
|
212
477
|
function writeWaitState(runDir, state) {
|
|
213
478
|
const legacyPath = path.join(runDir, "wait-state.json");
|
|
214
479
|
const registryPath = path.join(runDir, "wait-states.json");
|
|
@@ -300,16 +565,17 @@ function emitLoadSaveKeyOptionalPrompt(workspaceRoot, flowName, uuid, instanceId
|
|
|
300
565
|
const rootArg = workspaceRoot;
|
|
301
566
|
const q = bashSingleQuote;
|
|
302
567
|
const keyQ = q(key);
|
|
568
|
+
const af = agentflowCommand();
|
|
303
569
|
const directCommand =
|
|
304
570
|
definitionId === "tool_get_env"
|
|
305
|
-
?
|
|
571
|
+
? `${af} apply -ai get-env ${q(rootArg)} ${q(flowName)} ${q(uuid)} ${q(instanceId)} ${q(String(execId))} ${keyQ}`
|
|
306
572
|
: (() => {
|
|
307
573
|
const scriptArgs =
|
|
308
574
|
definitionId === "tool_load_key"
|
|
309
575
|
? `${q(rootArg)} ${q(flowName)} ${q(uuid)} ${keyQ}`
|
|
310
576
|
: `${q(rootArg)} ${q(flowName)} ${q(uuid)} ${keyQ} ${q(value)}`;
|
|
311
577
|
const scriptPath = path.join(__dirname, definitionId === "tool_load_key" ? "load-key.mjs" : "save-key.mjs");
|
|
312
|
-
return
|
|
578
|
+
return `${af} apply -ai run-tool-nodejs ${q(rootArg)} ${q(flowName)} ${q(uuid)} ${q(instanceId)} ${q(String(execId))} -- node ${q(scriptPath)} ${scriptArgs}`;
|
|
313
579
|
})();
|
|
314
580
|
const content = `此节点不调用 subagent,请主 agent 在工作区根目录直接执行以下命令完成该节点。
|
|
315
581
|
|
|
@@ -342,7 +608,7 @@ function emitToolNodejsDirectCommand(workspaceRoot, flowName, uuid, instanceId,
|
|
|
342
608
|
const promptPath = path.join(nodeIntermediateDir, promptFileName);
|
|
343
609
|
|
|
344
610
|
const q = bashSingleQuote;
|
|
345
|
-
const directCommand =
|
|
611
|
+
const directCommand = `${agentflowCommand()} apply -ai run-tool-nodejs ${q(workspaceRoot)} ${q(flowName)} ${q(uuid)} ${q(instanceId)} ${q(String(execId))} -- ${resolvedScript}`;
|
|
346
612
|
const content = `此节点为 tool_nodejs(直接执行模式),不调用 subagent,由流水线直接执行以下命令。
|
|
347
613
|
|
|
348
614
|
\`\`\`bash
|
|
@@ -376,7 +642,7 @@ function emitAnyOneOptionalPrompt(workspaceRoot, flowName, uuid, instanceId, exe
|
|
|
376
642
|
message: "任一前驱已就绪,直接通过",
|
|
377
643
|
execId,
|
|
378
644
|
});
|
|
379
|
-
const directCommand =
|
|
645
|
+
const directCommand = `${agentflowCommand()} apply -ai write-result ${bashSingleQuote(workspaceRoot)} ${bashSingleQuote(flowName)} ${bashSingleQuote(uuid)} ${bashSingleQuote(instanceId)} --json ${bashSingleQuote(jsonPayload)}`;
|
|
380
646
|
const content = `此节点为 control_anyOne,不调用 subagent。请主 agent 在工作区根目录直接执行以下命令将该节点标记为 success。
|
|
381
647
|
|
|
382
648
|
\`\`\`bash
|
|
@@ -715,7 +981,39 @@ function main() {
|
|
|
715
981
|
return;
|
|
716
982
|
}
|
|
717
983
|
|
|
718
|
-
|
|
984
|
+
if (definitionId === "tool_git_checkout" || definitionId === "control_cd_workspace" || definitionId === "control_load_skills" || definitionId === "tool_print") {
|
|
985
|
+
try {
|
|
986
|
+
const promptPath =
|
|
987
|
+
definitionId === "tool_git_checkout"
|
|
988
|
+
? emitGitCheckoutNode(workspaceRoot, flowName, uuid, instanceId, execId, resultPathRel)
|
|
989
|
+
: definitionId === "control_cd_workspace"
|
|
990
|
+
? emitCdWorkspaceNode(workspaceRoot, flowName, uuid, instanceId, execId)
|
|
991
|
+
: definitionId === "control_load_skills"
|
|
992
|
+
? emitLoadSkillsNode(workspaceRoot, flowName, uuid, instanceId, execId)
|
|
993
|
+
: emitToolPrintNode(workspaceRoot, flowName, uuid, instanceId, execId);
|
|
994
|
+
writeCacheJsonForNode(workspaceRoot, flowName, uuid, instanceId, execId);
|
|
995
|
+
logToRunTag(workspaceRoot, flowName, uuid, "pre-process", { event: "runtime-context-node", instanceId, definitionId });
|
|
996
|
+
console.log(JSON.stringify({
|
|
997
|
+
ok: true,
|
|
998
|
+
promptPath,
|
|
999
|
+
resultPath: resultPathRel,
|
|
1000
|
+
execId,
|
|
1001
|
+
subagent: "agentflow-node-executor",
|
|
1002
|
+
optionalPromptPath: promptPath,
|
|
1003
|
+
definitionId,
|
|
1004
|
+
}));
|
|
1005
|
+
return;
|
|
1006
|
+
} catch (e) {
|
|
1007
|
+
console.error(JSON.stringify({ ok: false, error: `${definitionId}: ${e.message || e}` }));
|
|
1008
|
+
process.exit(1);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const runtimeContexts = resolveNodeRuntimeContexts(workspaceRoot, flowName, uuid, instanceId);
|
|
1013
|
+
const data = buildNodePrompt(workspaceRoot, flowName, uuid, instanceId, execId, {
|
|
1014
|
+
workspaceContext: runtimeContexts.workspaceContext,
|
|
1015
|
+
skillsContext: runtimeContexts.skillsContext,
|
|
1016
|
+
});
|
|
719
1017
|
if (!data.ok) {
|
|
720
1018
|
console.error(JSON.stringify({ ok: false, error: data.error || "build-node-prompt failed" }));
|
|
721
1019
|
process.exit(1);
|
|
@@ -746,6 +1044,8 @@ function main() {
|
|
|
746
1044
|
definitionId,
|
|
747
1045
|
role,
|
|
748
1046
|
};
|
|
1047
|
+
if (data.workspaceContext) output.workspaceContext = data.workspaceContext;
|
|
1048
|
+
if (data.skillsContext) output.skillsContext = data.skillsContext;
|
|
749
1049
|
if (model) output.model = model;
|
|
750
1050
|
if (data.optionalPromptPath) {
|
|
751
1051
|
output.optionalPromptPath = data.optionalPromptPath;
|
|
@@ -17,6 +17,7 @@ import { getFlowDir } from "../lib/workspace.mjs";
|
|
|
17
17
|
/** 槽位合法 type 集合(英文为 builtin/nodes 标准;中文为遗留兼容,新写代码统一英文) */
|
|
18
18
|
const VALID_SLOT_TYPES = new Set(["node", "text", "file", "bool", "节点", "文本", "文件", "布尔"]);
|
|
19
19
|
const CANONICAL_SLOT_TYPES = "node|text|file|bool";
|
|
20
|
+
const RUNTIME_CONTEXT_INPUT_NAMES = new Set(["workspaceContext", "skillsContext"]);
|
|
20
21
|
|
|
21
22
|
/** 与前端 flowFormat.VALID_ROLES + 内置 id 一致 */
|
|
22
23
|
const VALID_ROLE_KEYS = ["requirement", "planning", "code", "test", "normal"];
|
|
@@ -470,6 +471,7 @@ function checkFlowCore(nodes, edges, flowDir, nodeIdToSlots, getNodeBody, instan
|
|
|
470
471
|
const slotName = (slot && slot.name != null ? String(slot.name).trim() : "");
|
|
471
472
|
const slotType = normType((slot && slot.type != null ? String(slot.type).trim() : ""));
|
|
472
473
|
if (!slotName || slotType === "node") continue;
|
|
474
|
+
if (RUNTIME_CONTEXT_INPUT_NAMES.has(slotName)) continue;
|
|
473
475
|
if (!phSet.has(slotName) && !phSet.has(`input.${slotName}`)) {
|
|
474
476
|
errors.push(
|
|
475
477
|
`节点 "${n.id}"(${defId})script 未引用 input 引脚 "${slotName}"(type: ${slotType}),` +
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
# 内置节点:SubAgent
|
|
3
|
-
description: 利用SubAgent
|
|
3
|
+
description: 利用 SubAgent 执行任务;可接收 workspaceContext 切换执行工作区,并接收 skillsContext 注入已加载 skills。
|
|
4
4
|
displayName: SubAgent
|
|
5
5
|
input:
|
|
6
6
|
- type: node
|
|
7
7
|
name: prev
|
|
8
8
|
default: ""
|
|
9
|
+
- type: text
|
|
10
|
+
name: workspaceContext
|
|
11
|
+
default: ""
|
|
12
|
+
- type: text
|
|
13
|
+
name: skillsContext
|
|
14
|
+
default: ""
|
|
9
15
|
output:
|
|
10
16
|
- type: node
|
|
11
17
|
name: next
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
# 内置节点:CD Workspace
|
|
3
|
+
description: |
|
|
4
|
+
Switch the runtime workspace context for downstream nodes without changing the AgentFlow pipeline workspace.
|
|
5
|
+
|
|
6
|
+
Modes:
|
|
7
|
+
- `set`: switch to target, keep previous stack unchanged.
|
|
8
|
+
- `push`: switch to target and save the incoming context as previous.
|
|
9
|
+
- `pop`: restore the previous context.
|
|
10
|
+
|
|
11
|
+
`target` supports `${workspaceRoot}`, `${pipelineWorkspace}`, `${flowDir}`, absolute paths, and paths relative to current workspace context.
|
|
12
|
+
displayName: CD Workspace
|
|
13
|
+
input:
|
|
14
|
+
- type: node
|
|
15
|
+
name: prev
|
|
16
|
+
default: ""
|
|
17
|
+
- type: text
|
|
18
|
+
name: target
|
|
19
|
+
default: "${pipelineWorkspace}"
|
|
20
|
+
- type: text
|
|
21
|
+
name: mode
|
|
22
|
+
default: "set"
|
|
23
|
+
- type: text
|
|
24
|
+
name: label
|
|
25
|
+
default: ""
|
|
26
|
+
- type: text
|
|
27
|
+
name: workspaceContext
|
|
28
|
+
default: ""
|
|
29
|
+
output:
|
|
30
|
+
- type: node
|
|
31
|
+
name: next
|
|
32
|
+
default: ""
|
|
33
|
+
- type: text
|
|
34
|
+
name: workspaceContext
|
|
35
|
+
default: ""
|
|
36
|
+
- type: file
|
|
37
|
+
name: cwd
|
|
38
|
+
default: ""
|
|
39
|
+
- type: text
|
|
40
|
+
name: previous
|
|
41
|
+
default: ""
|
|
42
|
+
---
|
|
43
|
+
Switch downstream execution to `${target}` using mode `${mode}`.
|