@fieldwangai/agentflow 0.1.160 → 0.1.163

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.
Files changed (111) hide show
  1. package/bin/lib/auth.mjs +231 -14
  2. package/bin/lib/catalog-flows.mjs +2 -0
  3. package/bin/lib/cli-auth-page.mjs +103 -0
  4. package/bin/lib/flow-dsl/codegen.mjs +23 -2
  5. package/bin/lib/flow-dsl/lint.mjs +44 -0
  6. package/bin/lib/flow-dsl/parser.mjs +68 -1
  7. package/bin/lib/marketplace-usage.mjs +218 -0
  8. package/bin/lib/marketplace.mjs +183 -3
  9. package/bin/lib/node-package-manifest.mjs +1 -1
  10. package/bin/lib/skill-runtime.mjs +6 -0
  11. package/bin/lib/spaces.mjs +200 -0
  12. package/bin/lib/table.mjs +2 -3
  13. package/bin/lib/ui-server.mjs +471 -84
  14. package/bin/lib/workspace-draft.mjs +74 -0
  15. package/bin/lib/workspace-routes.mjs +484 -4
  16. package/bin/lib/workspace-server.mjs +373 -30
  17. package/bin/lib/workspace-state.mjs +2 -1
  18. package/builtin/nodes/agent_subAgent.md +5 -1
  19. package/builtin/nodes/context_bundle.md +44 -0
  20. package/builtin/nodes/context_knowledge.md +29 -0
  21. package/builtin/nodes/context_skills.md +29 -0
  22. package/builtin/nodes/context_workspace.md +36 -0
  23. package/builtin/nodes/control_while.md +12 -5
  24. package/builtin/nodes/tool_git_worktree_load.md +4 -3
  25. package/builtin/pipelines/new/workspace.flow.js +1 -1
  26. package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-Bfo9Ythw.js → WorkflowAssistantThread-DmhIgoD7.js} +1 -1
  27. package/builtin/web-ui/dist/assets/index-CI9J6Unt.js +873 -0
  28. package/builtin/web-ui/dist/assets/index-DY5vE7v1.css +1 -0
  29. package/builtin/web-ui/dist/index.html +2 -2
  30. package/package.json +5 -1
  31. package/shared/slot-types.js +1 -0
  32. package/skills/agentflow-author-flow/SKILL.md +81 -8
  33. package/skills/agentflow-author-flow/agents/openai.yaml +3 -3
  34. package/skills/agentflow-cli/SKILL.md +179 -27
  35. package/skills/agentflow-cli/agents/openai.yaml +2 -2
  36. package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +14879 -0
  37. package/skills/agentflow-cli/runtime/builtin/nodes/agent_subAgent.md +38 -0
  38. package/skills/agentflow-cli/runtime/builtin/nodes/context_bundle.md +44 -0
  39. package/skills/agentflow-cli/runtime/builtin/nodes/context_knowledge.md +29 -0
  40. package/skills/agentflow-cli/runtime/builtin/nodes/context_skills.md +29 -0
  41. package/skills/agentflow-cli/runtime/builtin/nodes/context_workspace.md +36 -0
  42. package/skills/agentflow-cli/runtime/builtin/nodes/control_agent_toBool.md +26 -0
  43. package/skills/agentflow-cli/runtime/builtin/nodes/control_anyOne.md +19 -0
  44. package/skills/agentflow-cli/runtime/builtin/nodes/control_cancelled.md +26 -0
  45. package/skills/agentflow-cli/runtime/builtin/nodes/control_cd_workspace.md +50 -0
  46. package/skills/agentflow-cli/runtime/builtin/nodes/control_delay.md +31 -0
  47. package/skills/agentflow-cli/runtime/builtin/nodes/control_end.md +13 -0
  48. package/skills/agentflow-cli/runtime/builtin/nodes/control_if.md +21 -0
  49. package/skills/agentflow-cli/runtime/builtin/nodes/control_interval_loop.md +55 -0
  50. package/skills/agentflow-cli/runtime/builtin/nodes/control_load_mcp.md +24 -0
  51. package/skills/agentflow-cli/runtime/builtin/nodes/control_load_skills.md +35 -0
  52. package/skills/agentflow-cli/runtime/builtin/nodes/control_parse_json.md +25 -0
  53. package/skills/agentflow-cli/runtime/builtin/nodes/control_start.md +13 -0
  54. package/skills/agentflow-cli/runtime/builtin/nodes/control_subflow_call.md +27 -0
  55. package/skills/agentflow-cli/runtime/builtin/nodes/control_toBool.md +27 -0
  56. package/skills/agentflow-cli/runtime/builtin/nodes/control_user_workspace.md +24 -0
  57. package/skills/agentflow-cli/runtime/builtin/nodes/control_wait_until.md +34 -0
  58. package/skills/agentflow-cli/runtime/builtin/nodes/control_while.md +131 -0
  59. package/skills/agentflow-cli/runtime/builtin/nodes/display_ascii.md +24 -0
  60. package/skills/agentflow-cli/runtime/builtin/nodes/display_chart.md +32 -0
  61. package/skills/agentflow-cli/runtime/builtin/nodes/display_code.md +39 -0
  62. package/skills/agentflow-cli/runtime/builtin/nodes/display_html.md +32 -0
  63. package/skills/agentflow-cli/runtime/builtin/nodes/display_image.md +36 -0
  64. package/skills/agentflow-cli/runtime/builtin/nodes/display_markdown.md +24 -0
  65. package/skills/agentflow-cli/runtime/builtin/nodes/display_mermaid.md +24 -0
  66. package/skills/agentflow-cli/runtime/builtin/nodes/display_react_app.md +53 -0
  67. package/skills/agentflow-cli/runtime/builtin/nodes/display_table.md +32 -0
  68. package/skills/agentflow-cli/runtime/builtin/nodes/provide_bool.md +14 -0
  69. package/skills/agentflow-cli/runtime/builtin/nodes/provide_file.md +14 -0
  70. package/skills/agentflow-cli/runtime/builtin/nodes/provide_json.md +14 -0
  71. package/skills/agentflow-cli/runtime/builtin/nodes/provide_password.md +14 -0
  72. package/skills/agentflow-cli/runtime/builtin/nodes/provide_str.md +14 -0
  73. package/skills/agentflow-cli/runtime/builtin/nodes/tool_display_share_link.md +47 -0
  74. package/skills/agentflow-cli/runtime/builtin/nodes/tool_get_env.md +20 -0
  75. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_checkout.md +70 -0
  76. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_load.md +74 -0
  77. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_unload.md +52 -0
  78. package/skills/agentflow-cli/runtime/builtin/nodes/tool_gitlab_create_mr.md +114 -0
  79. package/skills/agentflow-cli/runtime/builtin/nodes/tool_jenkins_build.md +67 -0
  80. package/skills/agentflow-cli/runtime/builtin/nodes/tool_load_key.md +26 -0
  81. package/skills/agentflow-cli/runtime/builtin/nodes/tool_nodejs.md +52 -0
  82. package/skills/agentflow-cli/runtime/builtin/nodes/tool_print.md +21 -0
  83. package/skills/agentflow-cli/runtime/builtin/nodes/tool_save_key.md +26 -0
  84. package/skills/agentflow-cli/runtime/builtin/nodes/tool_set_run_env.md +38 -0
  85. package/skills/agentflow-cli/runtime/builtin/nodes/tool_user_ask.md +27 -0
  86. package/skills/agentflow-cli/runtime/builtin/nodes/tool_user_check.md +26 -0
  87. package/skills/agentflow-cli/runtime/builtin/nodes/tool_wecom_send_app_markdown.md +57 -0
  88. package/skills/agentflow-cli/runtime/builtin/nodes/tool_wecom_send_group_markdown.md +45 -0
  89. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_one_click_task.md +44 -0
  90. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_run.md +16 -0
  91. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_scheduled_run.md +16 -0
  92. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_subflow_input.md +14 -0
  93. package/skills/agentflow-cli/runtime/package.json +6 -0
  94. package/skills/agentflow-cli/scripts/agentflow-auth-store.mjs +113 -0
  95. package/skills/agentflow-cli/scripts/agentflow-cli.mjs +457 -28
  96. package/skills/agentflow-cli/scripts/agentflow-runtime.mjs +11 -4
  97. package/skills/agentflow-flow-add-instances/SKILL.md +2 -2
  98. package/skills/agentflow-flow-dsl/SKILL.md +73 -8
  99. package/skills/agentflow-flow-dsl/agents/openai.yaml +2 -2
  100. package/skills/agentflow-flow-dsl/references/node-calls.md +6 -2
  101. package/skills/agentflow-flow-dsl/references/subflow-authoring.md +34 -7
  102. package/skills/agentflow-flow-edit-node-fields/SKILL.md +3 -3
  103. package/skills/agentflow-flow-recipes/SKILL.md +1 -1
  104. package/skills/agentflow-flow-recipes/references/recipes.md +1 -2
  105. package/skills/agentflow-flow-sync-ui/SKILL.md +1 -1
  106. package/skills/agentflow-node-dsl/SKILL.md +5 -13
  107. package/skills/agentflow-node-reference/references/builtin-nodes.md +37 -5
  108. package/skills/agentflow-workspace-markdown/SKILL.md +1 -1
  109. package/builtin/web-ui/dist/assets/index-XbeI5foV.js +0 -872
  110. package/builtin/web-ui/dist/assets/index-e1omCEau.css +0 -1
  111. package/skills/agentflow-workspace-graph/SKILL.md +0 -85
@@ -33,6 +33,7 @@ import { t } from "./i18n.mjs";
33
33
  import { advanceJenkinsBuild, createJenkinsHttpInvoker, jenkinsBuildStatePath, normalizeJenkinsBuildConfig, readJenkinsBuildState, writeJenkinsBuildState } from "./jenkins.mjs";
34
34
  import { log } from "./log.mjs";
35
35
  import { resolveMarketplaceNodePackage } from "./marketplace.mjs";
36
+ import { marketplaceResourcesForRun, recordMarketplaceRunUsage } from "./marketplace-usage.mjs";
36
37
  import { PACKAGE_ROOT, getAgentflowDataRoot, getAgentflowUserDataRoot, listAgentflowUserIds } from "./paths.mjs";
37
38
  import { appendRunLedgerEvent, readRunLedgerEvents, runLedgerId } from "./run-ledger.mjs";
38
39
  import { computeNextRunAt } from "./schedule-config.mjs";
@@ -44,6 +45,7 @@ import { FLOW_SOURCE_FILENAME, WORKSPACE_GRAPH_FILENAME, WorkspaceFlowParseError
44
45
  import { createWorkspaceRunController, terminateWorkspaceChild } from "./workspace-run-controller.mjs";
45
46
  import { appendWorkspaceRunLogEvent, createWorkspaceRunLogSession, finishWorkspaceRunLogSession } from "./workspace-run-logs.mjs";
46
47
  import { splitWorkspaceGraph } from "./workspace-state.mjs";
48
+ import { isWorkspaceDraftDir } from "./workspace-draft.mjs";
47
49
  import { getPipelineFiles } from "./workspace-tree.mjs";
48
50
  import { spawn } from "child_process";
49
51
  import crypto from "crypto";
@@ -795,6 +797,71 @@ export function readWorkspacesFromPath(p, userCtx = {}) {
795
797
  }
796
798
  }
797
799
 
800
+ function legacyUserWorkspacesPath(userCtx = {}) {
801
+ return path.join(getAgentflowUserDataRoot(userCtx.userId || ""), USER_WORKSPACES_FILENAME);
802
+ }
803
+
804
+ function readLegacyAdminWorkspaces(userCtx = {}) {
805
+ const users = readAuthUsers();
806
+ const candidates = [];
807
+ for (const [userId, user] of Object.entries(users || {})) {
808
+ if (user?.isAdmin) candidates.push(String(userId || ""));
809
+ }
810
+ if (userCtx?.isAdmin && userCtx.userId) candidates.unshift(String(userCtx.userId));
811
+ const seenPaths = new Set();
812
+ const seenEntries = new Set();
813
+ const out = [];
814
+ for (const userId of candidates) {
815
+ const p = legacyUserWorkspacesPath({ userId });
816
+ const resolved = path.resolve(p);
817
+ if (seenPaths.has(resolved) || resolved === path.resolve(workspacesPath())) continue;
818
+ seenPaths.add(resolved);
819
+ for (const entry of readWorkspacesFromPath(p, { userId })) {
820
+ const key = entry.id || entry.path || entry.repoUrl;
821
+ if (seenEntries.has(key)) continue;
822
+ seenEntries.add(key);
823
+ out.push(entry);
824
+ }
825
+ }
826
+ return out;
827
+ }
828
+
829
+ /** The same authenticated Workspace catalog used by GET /api/workspaces. */
830
+ export function readUserWorkspaces(userCtx = {}) {
831
+ const globalPath = workspacesPath();
832
+ const globalWorkspaces = fs.existsSync(globalPath) ? readWorkspacesFromPath(globalPath, userCtx) : [];
833
+ const adminLegacy = readLegacyAdminWorkspaces(userCtx);
834
+ if (globalWorkspaces.length || adminLegacy.length) {
835
+ const seen = new Set();
836
+ const out = [];
837
+ for (const entry of [...globalWorkspaces, ...adminLegacy]) {
838
+ const key = entry.id || entry.path || entry.repoUrl;
839
+ if (seen.has(key)) continue;
840
+ seen.add(key);
841
+ out.push(entry);
842
+ }
843
+ return out;
844
+ }
845
+ return readWorkspacesFromPath(legacyUserWorkspacesPath(userCtx), userCtx);
846
+ }
847
+
848
+ export function listConfiguredWorkspaces(root, scopedRoot, userCtx = {}) {
849
+ const currentRoot = path.resolve(scopedRoot || root);
850
+ const homeRoot = path.resolve(os.homedir());
851
+ const builtins = [
852
+ { id: "current", label: "当前流程工作区", kind: "local", path: currentRoot, builtin: true, exists: fs.existsSync(currentRoot) && fs.statSync(currentRoot).isDirectory(), type: "flow", enabled: true },
853
+ { id: "home", label: "用户 Home", kind: "local", path: homeRoot, builtin: true, exists: fs.existsSync(homeRoot) && fs.statSync(homeRoot).isDirectory(), type: "local", enabled: true },
854
+ ];
855
+ const custom = readUserWorkspaces(userCtx).filter((entry) => entry.enabled !== false).map((entry) => ({ ...entry, builtin: false }));
856
+ const seen = new Set();
857
+ return [...builtins, ...custom].filter((entry) => {
858
+ const key = path.resolve(entry.path);
859
+ if (seen.has(key)) return false;
860
+ seen.add(key);
861
+ return true;
862
+ });
863
+ }
864
+
798
865
  export function workspaceRepoUrlWithCredential(repoUrl = "", credential = "") {
799
866
  const token = String(credential || "").trim();
800
867
  if (!token) return String(repoUrl || "").trim();
@@ -855,6 +922,10 @@ export function appendWorkspaceRunFinished(record, status) {
855
922
  durationMs: Math.max(0, Number(record.durationMs || (Number(record.endedAt || Date.now()) - Number(record.startedAt || record.at || Date.now())))),
856
923
  status,
857
924
  });
925
+ recordMarketplaceRunUsage(record.workspaceRoot || record.root || "", record.marketplaceResources || [], {
926
+ ...record,
927
+ status,
928
+ });
858
929
  }
859
930
 
860
931
  function normalizeWorkspaceUsageRecord(parsed, source = "workspace-run") {
@@ -1033,7 +1104,7 @@ function normalizeDisplayShareLayout(layout, fallback = "canvas") {
1033
1104
  return ["canvas", "gallery", "slides", "document", "single"].includes(text) ? text : fallback;
1034
1105
  }
1035
1106
 
1036
- export function createDisplayShareRecord({ userId, flowId, flowSource, archived, title, layout, nodeIds, expiresMode, expiresInDays, permanent, expiresAt }) {
1107
+ export function createDisplayShareRecord({ userId, flowId, flowSource, archived, title, layout, nodeIds, expiresMode, expiresInDays, permanent, expiresAt, visibility = "public" }) {
1037
1108
  const shares = readDisplayShares();
1038
1109
  let id = createDisplayShareId();
1039
1110
  while (shares[id]) id = createDisplayShareId();
@@ -1049,6 +1120,7 @@ export function createDisplayShareRecord({ userId, flowId, flowSource, archived,
1049
1120
  title: String(title || "").trim() || "AgentFlow Display",
1050
1121
  layout: normalizeDisplayShareLayout(layout, "canvas"),
1051
1122
  nodeIds: Array.isArray(nodeIds) ? nodeIds : [],
1123
+ visibility: String(visibility || "").trim().toLowerCase() === "private" ? "private" : "public",
1052
1124
  createdAt: now,
1053
1125
  updatedAt: now,
1054
1126
  expiresAt: expiry.expiresAt,
@@ -1491,6 +1563,7 @@ export function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {})
1491
1563
  requestedFlowSource: flowSource,
1492
1564
  workspaceId: collaboration?.id || workspaceId,
1493
1565
  archived,
1566
+ draft: isWorkspaceDraftDir(result.path),
1494
1567
  collaboration,
1495
1568
  collaborationAccess: workspaceCollaborationAccess(collaboration, opts.userId),
1496
1569
  };
@@ -3173,7 +3246,7 @@ function workspaceTargetSlotForEdge(graph, edge) {
3173
3246
  function isWorkspaceSemanticInputSlot(slot) {
3174
3247
  const name = String(slot?.name || "");
3175
3248
  const type = String(slot?.type || "");
3176
- return type === "node" || name === "prev" || name === "next" || name === "skillsContext" || name === "mcpContext" || name === "knowledgeContext" || name === "workspaceContext" || name === "gitContext";
3249
+ return type === "node" || type === "context" || name === "prev" || name === "next" || name === "context" || name === "skillsContext" || name === "mcpContext" || name === "knowledgeContext" || name === "workspaceContext" || name === "gitContext";
3177
3250
  }
3178
3251
 
3179
3252
  function workspaceAgentInputBlock(inputValues = {}, inputMounts = {}) {
@@ -3228,8 +3301,9 @@ function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames =
3228
3301
  return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge, scopedRoot);
3229
3302
  }
3230
3303
 
3231
- function workspaceInputValues(graph, nodeId, outputs, scopedRoot = "") {
3304
+ function workspaceInputValues(graph, nodeId, outputs, scopedRoot = "", options = {}) {
3232
3305
  const values = {};
3306
+ const includeContext = options?.includeContext === true;
3233
3307
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
3234
3308
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
3235
3309
  const target = instances[String(nodeId || "")] || {};
@@ -3239,13 +3313,13 @@ function workspaceInputValues(graph, nodeId, outputs, scopedRoot = "") {
3239
3313
  const index = workspaceHandleIndex(edge?.targetHandle, "input");
3240
3314
  const slot = inputSlots[index] || null;
3241
3315
  const name = String(slot?.name || "").trim();
3242
- if (!name || isWorkspaceSemanticInputSlot(slot)) continue;
3316
+ if (!name || (isWorkspaceSemanticInputSlot(slot) && !(includeContext && name === "context"))) continue;
3243
3317
  const value = workspaceOutputSlotValueForEdge(graph, outputs, edge, scopedRoot);
3244
3318
  if (String(value || "").trim()) values[name] = String(value);
3245
3319
  }
3246
3320
  for (const slot of inputSlots) {
3247
3321
  const name = String(slot?.name || "").trim();
3248
- if (!name || isWorkspaceSemanticInputSlot(slot) || Object.prototype.hasOwnProperty.call(values, name)) continue;
3322
+ if (!name || (isWorkspaceSemanticInputSlot(slot) && !(includeContext && name === "context")) || Object.prototype.hasOwnProperty.call(values, name)) continue;
3249
3323
  const value = workspaceSlotValue(slot);
3250
3324
  if (String(value || "").trim()) values[name] = String(value);
3251
3325
  }
@@ -3942,7 +4016,9 @@ function selectedSkillKeysFromInstance(instance) {
3942
4016
 
3943
4017
  function selectedSkillKeysFromConfigSlots(instance) {
3944
4018
  const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
3945
- const slot = slots.find((item) => item?.name === "skillsContext") || slots.find((item) => item?.name === "skillKeys");
4019
+ const slot = slots.find((item) => item?.name === "skills") ||
4020
+ slots.find((item) => item?.name === "skillsContext") ||
4021
+ slots.find((item) => item?.name === "skillKeys");
3946
4022
  return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
3947
4023
  }
3948
4024
 
@@ -3996,6 +4072,25 @@ function workspaceSemanticInputText(graph, nodeId, outputs, name, scopedRoot = "
3996
4072
  return workspaceSlotValue(workspaceSlotByName(instance, targetName));
3997
4073
  }
3998
4074
 
4075
+ function workspaceContextBundleFromText(text) {
4076
+ const parsed = parseJsonText(String(text || "").trim(), null);
4077
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
4078
+ if (Number(parsed.version || 1) !== 1) return {};
4079
+ const out = {};
4080
+ for (const name of ["knowledgeContext", "skillsContext", "workspaceContext", "mcpContext"]) {
4081
+ if (parsed[name] !== undefined && parsed[name] !== null) {
4082
+ out[name] = typeof parsed[name] === "string" ? parsed[name] : JSON.stringify(parsed[name]);
4083
+ }
4084
+ }
4085
+ return out;
4086
+ }
4087
+
4088
+ function workspaceNodeContextBundle(graph, nodeId, outputs, scopedRoot = "") {
4089
+ return workspaceContextBundleFromText(
4090
+ workspaceSemanticInputText(graph, nodeId, outputs, "context", scopedRoot),
4091
+ );
4092
+ }
4093
+
3999
4094
  function workspaceContextObjectFromText(text, baseCwd, scopedRoot) {
4000
4095
  const raw = String(text || "").trim();
4001
4096
  if (!raw) return null;
@@ -4022,20 +4117,30 @@ function workspaceLooksLikeKnowledgePath(value) {
4022
4117
  }
4023
4118
 
4024
4119
  function workspaceKnowledgeSourceFromObject(source = {}, baseCwd = "", scopedRoot = "") {
4120
+ if (typeof source === "string") {
4121
+ const ref = source.trim();
4122
+ if (!ref) return null;
4123
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(ref)) {
4124
+ return { id: ref, label: ref, kind: "binding", type: "", path: "", repoPath: "", mountPath: "", repoUrl: "", branch: "", ref, readonly: true };
4125
+ }
4126
+ source = { path: ref };
4127
+ }
4025
4128
  if (!source || typeof source !== "object" || Array.isArray(source)) return null;
4129
+ const ref = String(source.ref || source.binding || "").trim();
4026
4130
  const rawPath = String(source.path || source.repoPath || source.cwd || source.workspaceRoot || "").trim();
4027
- if (!workspaceLooksLikeKnowledgePath(rawPath)) return null;
4028
- const resolvedPath = workspaceResolvePath(baseCwd || scopedRoot, rawPath) || rawPath;
4131
+ if (!workspaceLooksLikeKnowledgePath(rawPath) && !ref) return null;
4132
+ const resolvedPath = rawPath ? (workspaceResolvePath(baseCwd || scopedRoot, rawPath) || rawPath) : "";
4029
4133
  return {
4030
- id: String(source.id || source.mountPath || source.label || path.basename(resolvedPath) || "").trim(),
4031
- label: String(source.label || source.id || source.mountPath || path.basename(resolvedPath) || "知识库").trim(),
4032
- kind: String(source.kind || (source.repoUrl ? "git" : "local")).trim() || "local",
4134
+ id: String(source.id || source.mountPath || source.label || path.basename(resolvedPath) || ref || "").trim(),
4135
+ label: String(source.label || source.id || source.mountPath || path.basename(resolvedPath) || ref || "知识库").trim(),
4136
+ kind: String(source.kind || (source.repoUrl ? "git" : (ref ? "binding" : "local"))).trim() || "local",
4033
4137
  type: String(source.type || "").trim(),
4034
4138
  path: resolvedPath,
4035
4139
  repoPath: resolvedPath,
4036
4140
  mountPath: String(source.mountPath || "").trim(),
4037
4141
  repoUrl: String(source.repoUrl || "").trim(),
4038
4142
  branch: String(source.branch || "").trim(),
4143
+ ref,
4039
4144
  readonly: source.readonly !== false,
4040
4145
  };
4041
4146
  }
@@ -4071,7 +4176,7 @@ export function workspaceKnowledgeSourcesFromText(text, baseCwd = "", scopedRoot
4071
4176
  }
4072
4177
 
4073
4178
  function workspaceKnowledgeContextBlockFromSources(sources = []) {
4074
- const valid = Array.isArray(sources) ? sources.filter((source) => source?.path || source?.repoPath) : [];
4179
+ const valid = Array.isArray(sources) ? sources.filter((source) => source?.path || source?.repoPath || source?.ref) : [];
4075
4180
  if (!valid.length) return "";
4076
4181
  const lines = [
4077
4182
  "## 知识库上下文",
@@ -4085,6 +4190,7 @@ function workspaceKnowledgeContextBlockFromSources(sources = []) {
4085
4190
  lines.push(`${index + 1}. ${label}`);
4086
4191
  if (source.kind) lines.push(` - 类型:${source.kind}${source.type ? `/${source.type}` : ""}`);
4087
4192
  if (sourcePath) lines.push(` - 路径:\`${sourcePath}\``);
4193
+ if (source.ref) lines.push(` - 绑定:\`${source.ref}\``);
4088
4194
  if (source.mountPath) lines.push(` - 挂载目录:${source.mountPath}`);
4089
4195
  if (source.repoUrl) lines.push(` - Git URL:${source.repoUrl}`);
4090
4196
  if (source.branch) lines.push(` - 分支:${source.branch}`);
@@ -4098,10 +4204,11 @@ function workspaceDedupeKnowledgeSources(sources = []) {
4098
4204
  for (const source of Array.isArray(sources) ? sources : []) {
4099
4205
  if (!source || typeof source !== "object") continue;
4100
4206
  const key = [
4101
- path.resolve(String(source.path || source.repoPath || "")),
4207
+ source.path || source.repoPath ? path.resolve(String(source.path || source.repoPath)) : "",
4102
4208
  String(source.mountPath || ""),
4103
4209
  String(source.repoUrl || ""),
4104
4210
  String(source.branch || ""),
4211
+ String(source.ref || ""),
4105
4212
  ].join("\n");
4106
4213
  if (seen.has(key)) continue;
4107
4214
  seen.add(key);
@@ -4147,16 +4254,18 @@ function workspaceGlobalKnowledgeSources(graph, scopedRoot = "", logicalCwd = ""
4147
4254
  );
4148
4255
  }
4149
4256
 
4150
- function workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot = "", logicalCwd = "") {
4257
+ function workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot = "", logicalCwd = "", contextBundle = {}) {
4151
4258
  const root = scopedRoot ? path.resolve(scopedRoot) : "";
4152
4259
  const cwd = logicalCwd ? path.resolve(logicalCwd) : root;
4153
- const knowledgeText = workspaceSemanticInputText(graph, nodeId, outputs, "knowledgeContext", scopedRoot);
4260
+ const knowledgeText = workspaceSemanticInputText(graph, nodeId, outputs, "knowledgeContext", scopedRoot)
4261
+ || String(contextBundle?.knowledgeContext || "");
4154
4262
  let knowledgeSources = workspaceKnowledgeSourcesFromText(knowledgeText, cwd || root, scopedRoot);
4155
4263
  knowledgeSources = workspaceDedupeKnowledgeSources([
4156
4264
  ...workspaceGlobalKnowledgeSources(graph, scopedRoot, logicalCwd, nodeId),
4157
4265
  ...knowledgeSources,
4158
4266
  ]);
4159
- const workspaceText = workspaceSemanticInputText(graph, nodeId, outputs, "workspaceContext", scopedRoot);
4267
+ const workspaceText = workspaceSemanticInputText(graph, nodeId, outputs, "workspaceContext", scopedRoot)
4268
+ || String(contextBundle?.workspaceContext || "");
4160
4269
  let workspaceContext = workspaceContextObjectFromText(workspaceText, cwd || root, scopedRoot);
4161
4270
  if (!knowledgeSources.length && workspaceContext?.cwd) {
4162
4271
  knowledgeSources = workspaceKnowledgeSourcesFromText(JSON.stringify([workspaceContext]), cwd || root, scopedRoot);
@@ -4382,7 +4491,7 @@ function workspaceDefaultGitRepoRoot(scopedRoot, _userCtx = {}) {
4382
4491
  return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "git-repos");
4383
4492
  }
4384
4493
 
4385
- function workspaceDefaultWorktreePath(runTmpRoot, nodeId, repoPath, branch = "") {
4494
+ function workspaceDefaultWorktreePath(scopedRoot, runId, nodeId, repoPath, branch = "") {
4386
4495
  const repoRoot = path.resolve(repoPath);
4387
4496
  const repoName = sanitizeWorktreeName(path.basename(repoRoot));
4388
4497
  const branchName = String(branch || "").trim();
@@ -4400,9 +4509,12 @@ function workspaceDefaultWorktreePath(runTmpRoot, nodeId, repoPath, branch = "")
4400
4509
  : "HEAD";
4401
4510
  }
4402
4511
  return path.join(
4403
- path.resolve(runTmpRoot),
4404
- "worktrees",
4512
+ path.resolve(scopedRoot),
4513
+ ".workspace",
4514
+ "agentflow",
4515
+ "run-workspaces",
4405
4516
  workspaceSanitizeTmpSegment(nodeId, "node"),
4517
+ workspaceSanitizeTmpSegment(runId, "run"),
4406
4518
  repoName,
4407
4519
  sanitizeWorktreeName(refLabel),
4408
4520
  );
@@ -4442,15 +4554,18 @@ function workspaceMarkAutoWorktreeCleaned(graph, entry) {
4442
4554
  return true;
4443
4555
  }
4444
4556
 
4445
- function workspaceCleanupAutoWorktrees(list, graph, emit) {
4557
+ function workspaceCleanupAutoWorktrees(list, graph, emit, { force = false } = {}) {
4558
+ const cleaned = [];
4559
+ const preserved = [];
4446
4560
  for (const entry of [...list].reverse()) {
4447
4561
  try {
4448
4562
  const result = unloadGitWorktree({
4449
4563
  repoPath: entry.repoPath,
4450
4564
  worktreePath: entry.worktreePath,
4451
- force: true,
4565
+ force,
4452
4566
  prune: true,
4453
4567
  });
4568
+ cleaned.push(result.worktreePath);
4454
4569
  emit({
4455
4570
  type: "natural",
4456
4571
  kind: "status",
@@ -4461,15 +4576,74 @@ function workspaceCleanupAutoWorktrees(list, graph, emit) {
4461
4576
  emit({ type: "graph", nodeId: entry.nodeId, graph });
4462
4577
  }
4463
4578
  } catch (e) {
4579
+ preserved.push({ ...entry, reason: e?.message || String(e) });
4464
4580
  emit({
4465
4581
  type: "natural",
4466
4582
  kind: "warning",
4467
4583
  nodeId: entry.nodeId,
4468
- text: `临时 worktree 未自动清理:${entry.worktreePath}\n原因:${e?.message || String(e)}`,
4584
+ text: `运行 worktree 已保留:${entry.worktreePath}\n原因:${e?.message || String(e)}`,
4469
4585
  });
4470
4586
  }
4471
4587
  }
4472
4588
  list.splice(0, list.length);
4589
+ return { cleaned, preserved };
4590
+ }
4591
+
4592
+ function workspaceRunManifestPath(scopedRoot, runId) {
4593
+ const id = workspaceSanitizeTmpSegment(runId, "run");
4594
+ return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "run-manifests", `${id}.json`);
4595
+ }
4596
+
4597
+ function workspaceWriteRunManifest(scopedRoot, runId, value = {}) {
4598
+ const filePath = workspaceRunManifestPath(scopedRoot, runId);
4599
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
4600
+ const previous = (() => {
4601
+ try {
4602
+ return fs.existsSync(filePath) ? JSON.parse(fs.readFileSync(filePath, "utf-8")) : {};
4603
+ } catch {
4604
+ return {};
4605
+ }
4606
+ })();
4607
+ const next = {
4608
+ version: 1,
4609
+ ...previous,
4610
+ ...value,
4611
+ runId: String(runId || previous.runId || ""),
4612
+ updatedAt: new Date().toISOString(),
4613
+ };
4614
+ const tempPath = `${filePath}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
4615
+ fs.writeFileSync(tempPath, JSON.stringify(next, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
4616
+ fs.renameSync(tempPath, filePath);
4617
+ return next;
4618
+ }
4619
+
4620
+ export function cleanupWorkspaceRunResources(scopedRoot, runId, { force = false, status = "stopped", emit = () => {} } = {}) {
4621
+ const filePath = workspaceRunManifestPath(scopedRoot, runId);
4622
+ if (!fs.existsSync(filePath)) return { cleaned: [], preserved: [], manifestPath: filePath };
4623
+ let manifest = {};
4624
+ try {
4625
+ manifest = JSON.parse(fs.readFileSync(filePath, "utf-8"));
4626
+ } catch {
4627
+ return { cleaned: [], preserved: [], manifestPath: filePath };
4628
+ }
4629
+ const resources = Array.isArray(manifest.worktrees) ? manifest.worktrees : [];
4630
+ const pending = resources.filter((entry) => entry?.repoPath && entry?.worktreePath && entry.removed !== true);
4631
+ const result = workspaceCleanupAutoWorktrees(pending.map((entry) => ({ ...entry })), null, emit, { force });
4632
+ const cleanedSet = new Set(result.cleaned.map((item) => path.resolve(item)));
4633
+ const preservedByPath = new Map(result.preserved.map((item) => [path.resolve(item.worktreePath), item]));
4634
+ const worktrees = resources.map((entry) => {
4635
+ const target = entry?.worktreePath ? path.resolve(entry.worktreePath) : "";
4636
+ if (target && cleanedSet.has(target)) return { ...entry, removed: true, removedAt: new Date().toISOString(), reason: "" };
4637
+ if (target && preservedByPath.has(target)) return { ...entry, removed: false, reason: preservedByPath.get(target).reason };
4638
+ return entry;
4639
+ });
4640
+ workspaceWriteRunManifest(scopedRoot, runId, {
4641
+ ...manifest,
4642
+ status: result.preserved.length ? `${status}:resources-preserved` : status,
4643
+ worktrees,
4644
+ finishedAt: new Date().toISOString(),
4645
+ });
4646
+ return { ...result, manifestPath: filePath };
4473
4647
  }
4474
4648
 
4475
4649
  function workspaceSanitizeTmpSegment(value, fallback = "node") {
@@ -5108,10 +5282,27 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5108
5282
  const runtimeEnv = (extra = {}) => runtimeEnvForUser(userCtx, { ...runEnv, ...(extra || {}) });
5109
5283
  const autoCleanupWorktrees = [];
5110
5284
  const runTmpRoot = workspaceCreateRunTmpRoot(scopedRoot, runNodeId);
5285
+ const runtimeRunId = String(opts.runId || payload?.runId || "").trim() || runLedgerId("workspace-execution");
5286
+ const ownsRunManifest = !(Array.isArray(opts?.subflowCallStack) && opts.subflowCallStack.length);
5287
+ const persistRunManifest = (status, extra = {}) => {
5288
+ if (!ownsRunManifest) return null;
5289
+ return workspaceWriteRunManifest(scopedRoot, runtimeRunId, {
5290
+ flowId: String(payload?.flowId || ""),
5291
+ flowSource: String(payload?.flowSource || "user"),
5292
+ runNodeId,
5293
+ status,
5294
+ runtimeRoot: runTmpRoot,
5295
+ artifactRoot: path.join(path.resolve(scopedRoot), "outputs"),
5296
+ worktrees: autoCleanupWorktrees.map((entry) => ({ ...entry, removed: false })),
5297
+ ...extra,
5298
+ });
5299
+ };
5300
+ persistRunManifest("running", { startedAt: new Date().toISOString() });
5111
5301
  const controlBranches = new Map();
5112
5302
  const skippedNodes = new Set();
5113
5303
  const runtimePauseNodeIds = [];
5114
5304
  let deferred = null;
5305
+ let runFailure = null;
5115
5306
  const incomingControlEdgesByTarget = new Map();
5116
5307
  for (const edge of Array.isArray(graph?.edges) ? graph.edges : []) {
5117
5308
  const target = String(edge?.target || "");
@@ -5175,11 +5366,87 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5175
5366
  continue;
5176
5367
  }
5177
5368
 
5369
+ if (defId === "context_knowledge") {
5370
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
5371
+ const workspaceIds = parseWorkspaceSkillKeys(inputValues.workspaceIds || "[]");
5372
+ if (!workspaceIds.length) throw new Error(`context.knowledge ${nodeId} requires at least one Workspace ID`);
5373
+ const catalog = new Map(listConfiguredWorkspaces(root, scopedRoot, userCtx).map((entry) => [String(entry.id || ""), entry]));
5374
+ const missing = workspaceIds.filter((id) => !catalog.has(id));
5375
+ if (missing.length) throw new Error(`context.knowledge ${nodeId} cannot resolve Workspace IDs: ${missing.join(", ")}`);
5376
+ const unavailable = workspaceIds.filter((id) => catalog.get(id)?.exists === false);
5377
+ if (unavailable.length) throw new Error(`context.knowledge ${nodeId} Workspace paths are not ready: ${unavailable.join(", ")}`);
5378
+ const sources = workspaceIds
5379
+ .map((id, index) => workspaceKnowledgeSourceFromObject({ ...catalog.get(id), role: index === 0 ? "primary" : "context" }, cwd || scopedRoot, scopedRoot))
5380
+ .filter(Boolean);
5381
+ const value = JSON.stringify({ version: 1, sources });
5382
+ graph.instances[nodeId] = workspaceSetOutputSlot(instance, "knowledgeContext", value);
5383
+ publishNodeOutput(nodeId, value, { emitGraph: true });
5384
+ emit({ type: "node-done", nodeId, definitionId: defId, sourceCount: sources.length, workspaceIds });
5385
+ continue;
5386
+ }
5387
+
5388
+ if (defId === "context_skills") {
5389
+ const keys = selectedSkillKeysFromConfigSlots(instance);
5390
+ if (!keys.length) throw new Error(`context.skills ${nodeId} requires at least one skill`);
5391
+ const value = loadSkillsBlockForKeys(keys);
5392
+ graph.instances[nodeId] = workspaceSetOutputSlot(instance, "skillsContext", value);
5393
+ publishNodeOutput(nodeId, value, { emitGraph: true });
5394
+ emit({ type: "node-done", nodeId, definitionId: defId, skillCount: keys.length });
5395
+ continue;
5396
+ }
5397
+
5398
+ if (defId === "context_workspace") {
5399
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
5400
+ const workspaceId = String(inputValues.workspaceId || "current").trim();
5401
+ const access = String(inputValues.access || "read-write").trim().toLowerCase();
5402
+ if (!["read-only", "read-write"].includes(access)) {
5403
+ throw new Error(`context.workspace ${nodeId} access must be read-only or read-write`);
5404
+ }
5405
+ const catalog = new Map(listConfiguredWorkspaces(root, scopedRoot, userCtx).map((entry) => [String(entry.id || ""), entry]));
5406
+ const selected = catalog.get(workspaceId);
5407
+ if (!selected) throw new Error(`context.workspace ${nodeId} cannot resolve Workspace ID ${workspaceId || "(empty)"}`);
5408
+ if (selected.exists === false) throw new Error(`context.workspace ${nodeId} Workspace path is not ready: ${workspaceId}`);
5409
+ const workspaceRoot = path.resolve(selected.path);
5410
+ const value = JSON.stringify({
5411
+ version: 1,
5412
+ workspaceId,
5413
+ access,
5414
+ label: String(selected.label || selected.id || path.basename(workspaceRoot)),
5415
+ cwd: workspaceRoot,
5416
+ workspaceRoot,
5417
+ pipelineWorkspace: path.resolve(scopedRoot),
5418
+ previous: null,
5419
+ });
5420
+ let nextInstance = workspaceSetOutputSlot(instance, "workspaceContext", value);
5421
+ graph.instances[nodeId] = nextInstance;
5422
+ publishNodeOutput(nodeId, value, { emitGraph: true });
5423
+ emit({ type: "node-done", nodeId, definitionId: defId, workspaceId, access });
5424
+ continue;
5425
+ }
5426
+
5427
+ if (defId === "context_bundle") {
5428
+ const bundle = {
5429
+ version: 1,
5430
+ knowledgeContext: workspaceSemanticInputText(graph, nodeId, outputs, "knowledgeContext", scopedRoot),
5431
+ skillsContext: workspaceSemanticInputText(graph, nodeId, outputs, "skillsContext", scopedRoot),
5432
+ workspaceContext: workspaceSemanticInputText(graph, nodeId, outputs, "workspaceContext", scopedRoot),
5433
+ mcpContext: workspaceSemanticInputText(graph, nodeId, outputs, "mcpContext", scopedRoot),
5434
+ };
5435
+ if (![bundle.knowledgeContext, bundle.skillsContext, bundle.workspaceContext, bundle.mcpContext].some((item) => String(item || "").trim())) {
5436
+ throw new Error(`context.bundle ${nodeId} requires at least one connected Context resource`);
5437
+ }
5438
+ const value = JSON.stringify(bundle);
5439
+ graph.instances[nodeId] = workspaceSetOutputSlot(instance, "context", value);
5440
+ publishNodeOutput(nodeId, value, { emitGraph: true });
5441
+ emit({ type: "node-done", nodeId, definitionId: defId });
5442
+ continue;
5443
+ }
5444
+
5178
5445
  if (defId === "control_subflow_call") {
5179
5446
  const subflowId = String(instance.subflowId || "").trim();
5180
5447
  const subflow = graph?.subflows?.[subflowId];
5181
5448
  if (!subflow) throw new Error(`flow.call ${nodeId} references missing subflow ${subflowId || "(empty)"}`);
5182
- const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
5449
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot, { includeContext: true });
5183
5450
  const { resultValues, callFrameId } = await workspaceRunSubflowFrame({
5184
5451
  root,
5185
5452
  scopedRoot,
@@ -5295,6 +5562,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5295
5562
 
5296
5563
  if (defId === "control_while") {
5297
5564
  const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
5565
+ const loopContext = workspaceSemanticInputText(graph, nodeId, outputs, "context", scopedRoot);
5298
5566
  const config = normalizeControlWhileConfig(inputValues);
5299
5567
  const stepScript = String(instance.script || instance.body || "").trim();
5300
5568
  const stepScriptRef = String(instance.scriptRef || "").trim();
@@ -5390,6 +5658,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5390
5658
  parentDefinitionId: defId,
5391
5659
  subflowId: whileSubflows.conditionId,
5392
5660
  inputValues: {
5661
+ ...(loopContext && whileSubflows.condition.inputs?.context ? { context: loopContext } : {}),
5393
5662
  state: stateText,
5394
5663
  iteration: String(iteration),
5395
5664
  idempotencyKey,
@@ -5415,6 +5684,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5415
5684
  parentDefinitionId: defId,
5416
5685
  subflowId: whileSubflows.bodyId,
5417
5686
  inputValues: {
5687
+ ...(loopContext && whileSubflows.body.inputs?.context ? { context: loopContext } : {}),
5418
5688
  state: stateText,
5419
5689
  iteration: String(iteration),
5420
5690
  idempotencyKey,
@@ -5809,9 +6079,17 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5809
6079
  const worktreeInputSlot = (Array.isArray(instance.input) ? instance.input : [])
5810
6080
  .find((slot) => String(slot?.name || "") === "worktreePath") || null;
5811
6081
  const rawWorktreePath = workspaceSlotValue(worktreeInputSlot || workspaceSlotByName(instance, "worktreePath")).trim();
6082
+ const retainedWorktreePath = workspaceSlotValue(
6083
+ (Array.isArray(instance.output) ? instance.output : [])
6084
+ .find((slot) => String(slot?.name || "") === "worktreePath"),
6085
+ ).trim();
5812
6086
  const worktreePath = rawWorktreePath
5813
6087
  ? workspaceResolvePath(cwd, rawWorktreePath)
5814
- : (gitContext?.worktreePath ? path.resolve(gitContext.worktreePath) : workspaceDefaultWorktreePath(runTmpRoot, nodeId, repoPath, branch));
6088
+ : (gitContext?.worktreePath
6089
+ ? path.resolve(gitContext.worktreePath)
6090
+ : (retainedWorktreePath
6091
+ ? path.resolve(retainedWorktreePath)
6092
+ : workspaceDefaultWorktreePath(scopedRoot, runtimeRunId, nodeId, repoPath, branch)));
5815
6093
  const previousCwd = cwd;
5816
6094
  const force = ["true", "1", "yes", "on"].includes(workspaceSlotValue(workspaceSlotByName(instance, "force")).trim().toLowerCase());
5817
6095
  const pruneMissingRaw = workspaceSlotValue(workspaceSlotByName(instance, "pruneMissing")).trim().toLowerCase();
@@ -5823,6 +6101,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5823
6101
  repoPath: result.repoRoot,
5824
6102
  worktreePath: result.worktreePath,
5825
6103
  });
6104
+ persistRunManifest("running");
5826
6105
  }
5827
6106
  const outGitContext = buildGitContext({
5828
6107
  repoPath: result.repoRoot,
@@ -6065,10 +6344,17 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
6065
6344
  const relevantInputs = workspaceRelevantInputValues(instance.body || "", inputValues);
6066
6345
  workspaceAssertRequiredInputs(instance.body || "", inputValues, nodeId);
6067
6346
  const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders, scopedRoot);
6068
- const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
6347
+ const contextBundle = workspaceNodeContextBundle(graph, nodeId, outputs, scopedRoot);
6348
+ const upstreamSkillBlocks = mergeWorkspaceSkillBlocks(
6349
+ workspaceUpstreamSkillBlocks(graph, nodeId, outputs),
6350
+ String(contextBundle.skillsContext || ""),
6351
+ );
6069
6352
  const ownSkillBlock = isContextRunNode ? loadSkillsBlockForKeys(selectedSkillKeysFromConfigSlots(instance)) : "";
6070
6353
  const promptSkillsBlock = mergeWorkspaceSkillBlocks(ownSkillBlock, upstreamSkillBlocks);
6071
- const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
6354
+ const promptMcpBlock = mergeWorkspaceSkillBlocks(
6355
+ workspaceUpstreamMcpBlocks(graph, nodeId, outputs),
6356
+ String(contextBundle.mcpContext || ""),
6357
+ );
6072
6358
  const resultOutputSpec = workspaceResultOutputSpec(graph, nodeId);
6073
6359
  const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
6074
6360
  scopedRoot,
@@ -6093,7 +6379,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
6093
6379
  // Best-effort debug artifact only.
6094
6380
  }
6095
6381
  const historyBlock = workspaceNodeHistoryBlock(nodeId, scopedRoot, runPackage);
6096
- let workspaceContextBlock = workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot, cwd);
6382
+ let workspaceContextBlock = workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot, cwd, contextBundle);
6097
6383
  if (!isContextRunNode && workspaceBoolSlot(instance, "includeWorkspaceContext", true)) {
6098
6384
  const defaultWorkspaceBlock = workspaceDefaultWorkspaceContextBlock(scopedRoot, cwd);
6099
6385
  if (!workspaceContextBlock) {
@@ -6227,9 +6513,47 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
6227
6513
  outputFiles: normalizedAgentOutput.outputFiles || [],
6228
6514
  });
6229
6515
  }
6516
+ } catch (error) {
6517
+ runFailure = error;
6518
+ throw error;
6230
6519
  } finally {
6231
- workspaceCleanupAutoWorktrees(autoCleanupWorktrees, graph, emit);
6232
- workspaceCleanupTmpRoot(runTmpRoot, userCtx, emit);
6520
+ const retainForResume = Boolean(deferred || runtimePauseNodeIds.length);
6521
+ const trackedWorktrees = autoCleanupWorktrees.map((entry) => ({ ...entry }));
6522
+ let cleanup = { cleaned: [], preserved: [] };
6523
+ if (retainForResume) {
6524
+ emit({
6525
+ type: "status",
6526
+ line: `Run workspace retained for resume${trackedWorktrees.length ? ` (${trackedWorktrees.length} worktree)` : ""}`,
6527
+ runId: runtimeRunId,
6528
+ });
6529
+ persistRunManifest("waiting", {
6530
+ worktrees: trackedWorktrees.map((entry) => ({ ...entry, removed: false })),
6531
+ waitingAt: new Date().toISOString(),
6532
+ });
6533
+ autoCleanupWorktrees.splice(0, autoCleanupWorktrees.length);
6534
+ } else {
6535
+ cleanup = workspaceCleanupAutoWorktrees(autoCleanupWorktrees, graph, emit, { force: false });
6536
+ const cleanedSet = new Set(cleanup.cleaned.map((item) => path.resolve(item)));
6537
+ const preservedByPath = new Map(cleanup.preserved.map((item) => [path.resolve(item.worktreePath), item]));
6538
+ persistRunManifest(runFailure ? "failed" : "completed", {
6539
+ worktrees: trackedWorktrees.map((entry) => {
6540
+ const target = path.resolve(entry.worktreePath);
6541
+ if (cleanedSet.has(target)) return { ...entry, removed: true, removedAt: new Date().toISOString(), reason: "" };
6542
+ if (preservedByPath.has(target)) return { ...entry, removed: false, reason: preservedByPath.get(target).reason };
6543
+ return entry;
6544
+ }),
6545
+ finishedAt: new Date().toISOString(),
6546
+ error: runFailure ? (runFailure?.message || String(runFailure)) : "",
6547
+ resourcesPreserved: cleanup.preserved.length > 0,
6548
+ });
6549
+ }
6550
+ const protectedWorktrees = retainForResume ? trackedWorktrees : cleanup.preserved;
6551
+ const protectsRunTmpRoot = protectedWorktrees.some((entry) => workspacePathInside(runTmpRoot, entry.worktreePath));
6552
+ if (protectsRunTmpRoot) {
6553
+ emit({ type: "status", line: `Workspace tmp kept because it contains a retained worktree: ${runTmpRoot}` });
6554
+ } else {
6555
+ workspaceCleanupTmpRoot(runTmpRoot, userCtx, emit);
6556
+ }
6233
6557
  }
6234
6558
  const finalPauseNodeIds = Array.from(new Set([...pauseNodeIds, ...runtimePauseNodeIds]));
6235
6559
  if (!deferred && finalPauseNodeIds.length > 0) {
@@ -6442,6 +6766,14 @@ export function upsertWorkspaceDeferredRun(meta = {}, deferred = {}) {
6442
6766
  nodeId: String(deferred.nodeId || meta.nodeId || previous.nodeId || ""),
6443
6767
  label: String(meta.label || previous.label || "Workspace Run"),
6444
6768
  plannedNodeIds: Array.isArray(meta.plannedNodeIds) ? meta.plannedNodeIds.map(String) : (previous.plannedNodeIds || []),
6769
+ workspaceRoot: String(meta.workspaceRoot || previous.workspaceRoot || ""),
6770
+ marketplaceResources: Array.isArray(meta.marketplaceResources)
6771
+ ? meta.marketplaceResources.map((item) => ({
6772
+ kind: String(item?.kind || ""),
6773
+ id: String(item?.id || ""),
6774
+ version: String(item?.version || ""),
6775
+ })).filter((item) => item.kind && item.id && item.version)
6776
+ : (previous.marketplaceResources || []),
6445
6777
  startedAt: Number(meta.startedAt || previous.startedAt || now),
6446
6778
  scheduled: meta.scheduled === true || previous.scheduled === true,
6447
6779
  scheduleKey: String(meta.scheduleKey || previous.scheduleKey || ""),
@@ -6726,6 +7058,10 @@ export function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, us
6726
7058
  );
6727
7059
  if (sameSharedFlow || key.startsWith(prefix)) delete schedules[key];
6728
7060
  }
7061
+ if (isWorkspaceDraftDir(scoped?.root || "")) {
7062
+ writeWorkspaceScheduleRegistry({ version: 1, schedules });
7063
+ return [];
7064
+ }
6729
7065
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
6730
7066
  for (const [scheduleNodeId, instance] of Object.entries(instances)) {
6731
7067
  if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
@@ -6937,6 +7273,8 @@ export async function runWorkspaceScheduledEntry(root, entry) {
6937
7273
  plannedNodeIds,
6938
7274
  startedAt: Date.now(),
6939
7275
  scheduled: true,
7276
+ workspaceRoot: root,
7277
+ marketplaceResources: marketplaceResourcesForRun(scoped.root, graph, plannedNodeIds),
6940
7278
  };
6941
7279
  activeWorkspaceRuns.set(runKey, runEntry);
6942
7280
  appendWorkspaceRunStarted(runEntry);
@@ -6991,7 +7329,12 @@ export async function runWorkspaceScheduledEntry(root, entry) {
6991
7329
  return;
6992
7330
  }
6993
7331
  const endedAt = Date.now();
6994
- appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "success");
7332
+ appendWorkspaceRunFinished({
7333
+ ...runEntry,
7334
+ endedAt,
7335
+ durationMs: endedAt - runEntry.startedAt,
7336
+ marketplaceResources: marketplaceResourcesForRun(scoped.root, graph, result.order || Array.from(touchedIds)),
7337
+ }, "success");
6995
7338
  finishWorkspaceRunLogSession(runLog.runId, "success", {
6996
7339
  endedAt,
6997
7340
  durationMs: endedAt - runEntry.startedAt,