c0de-agent 1.4.0 → 1.6.0

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 (44) hide show
  1. package/dist/core/agent.js +4 -0
  2. package/dist/core/config.js +1 -1
  3. package/dist/core/index.d.ts +3 -3
  4. package/dist/core/index.js +1 -1
  5. package/dist/core/loop.js +9 -0
  6. package/dist/core/prompt-registry.d.ts +2 -2
  7. package/dist/core/prompt-registry.js +42 -3
  8. package/dist/core/slash.js +102 -6
  9. package/dist/core/types.d.ts +10 -1
  10. package/dist/core/workflow.d.ts +1 -1
  11. package/dist/core/workflow.js +56 -5
  12. package/dist/core/workflows/discovery.d.ts +23 -2
  13. package/dist/core/workflows/discovery.js +38 -2
  14. package/dist/core/workflows/index.d.ts +3 -2
  15. package/dist/core/workflows/index.js +2 -2
  16. package/dist/core/workflows/registry.d.ts +8 -1
  17. package/dist/core/workflows/registry.js +21 -1
  18. package/dist/core/workflows/runtime.d.ts +3 -0
  19. package/dist/core/workflows/runtime.js +2 -2
  20. package/dist/project/resolve.d.ts +75 -0
  21. package/dist/project/resolve.js +253 -1
  22. package/dist/server/app.js +3 -0
  23. package/dist/server/context.js +2 -1
  24. package/dist/server/dev.js +3 -1
  25. package/dist/server/routes/chat.js +12 -0
  26. package/dist/server/routes/commands.js +1 -0
  27. package/dist/server/routes/files.js +252 -4
  28. package/dist/server/routes/terminal.js +2 -1
  29. package/dist/server/routes/todo.d.ts +4 -0
  30. package/dist/server/routes/todo.js +107 -0
  31. package/dist/server/routes/workflows.js +83 -10
  32. package/dist/server/server.d.ts +8 -1
  33. package/dist/server/server.js +113 -23
  34. package/dist/server/terminal/pty-manager.d.ts +14 -0
  35. package/dist/server/terminal/pty-manager.js +105 -7
  36. package/dist/shared/types/agent.d.ts +9 -0
  37. package/dist/shared/types/config.d.ts +6 -0
  38. package/dist/shared/types/tool.d.ts +13 -0
  39. package/dist/tools/builtin/todo.d.ts +67 -0
  40. package/dist/tools/builtin/todo.js +517 -0
  41. package/dist/tools/index.d.ts +2 -0
  42. package/dist/tools/index.js +3 -0
  43. package/dist/tools/types.d.ts +32 -1
  44. package/package.json +2 -1
@@ -2,6 +2,7 @@ import { appendMessage, getMessages } from '../session/message.js';
2
2
  import { getLLMSegments } from '../session/session.js';
3
3
  import { generateId } from '../shared/index.js';
4
4
  import { listTools } from '../tools/registry.js';
5
+ import { getLatestTodoPhasesFromMessages } from '../tools/builtin/todo.js';
5
6
  import { estimateBudget } from './context.js';
6
7
  import { createTokenBudget } from './context.js';
7
8
  import { agentLoop } from './loop.js';
@@ -39,6 +40,9 @@ async function createAgent(session, config, deps) {
39
40
  segments,
40
41
  tokenBudget: { ...tokenBudget, used },
41
42
  calibrationFactor: 1.0,
43
+ // 从历史 tool result 恢复 todo 状态(跨 session resume / compaction 后重建)。
44
+ // 遍历所有消息找最后一条带 phases metadata 的 todo result。
45
+ todoPhases: getLatestTodoPhasesFromMessages(messages),
42
46
  // 压缩模型覆盖:从全局配置映射到 agent state,compactContext 读取后用于
43
47
  // 创建 summarizer。未配置时为 undefined → 回退到会话主模型。
44
48
  ...(deps.config.compaction.compactionModel
@@ -16,7 +16,7 @@ const DEFAULT_CONFIG = {
16
16
  keepRecentTokens: 4000,
17
17
  midTurnEnabled: false,
18
18
  },
19
- tools: { enabled: ['read', 'write', 'edit', 'glob', 'grep', 'bash', 'websearch'], disabled: [] },
19
+ tools: { enabled: ['read', 'write', 'edit', 'glob', 'grep', 'bash', 'todo', 'websearch'], disabled: [] },
20
20
  plugins: { enabled: [] },
21
21
  mcpServers: [],
22
22
  slashCommands: { enabled: ['/compact', '/model', '/clear', '/help', '/fork', '/config'] },
@@ -10,7 +10,7 @@ export { builtinCommands, createSlashRegistry, parseSlashInput } from './slash.j
10
10
  export { clearSteering, drainSteering, injectSteering } from './steering.js';
11
11
  export type { CollectedToolCall, ToolCallResult } from './tool-exec.js';
12
12
  export { executeToolCall, executeToolCalls, partitionByConflict } from './tool-exec.js';
13
- export type { AgentConfig, AgentDependencies, AgentError, AgentEvent, AgentState, AgentStatus, CommandContext, CommandResult, LLMSegment, PendingToolCall, ProjectInfo, PromptContext, SlashCommand, TokenBudget, } from './types.js';
13
+ export type { AgentConfig, AgentDependencies, AgentError, AgentEvent, AgentState, AgentStatus, CommandContext, CommandResult, LLMSegment, PendingToolCall, ProjectInfo, PromptContext, SlashCommand, SubcommandDef, TokenBudget, } from './types.js';
14
14
  export { buildWorkflowNotice, containsWorkflow, WORKFLOW_NOTICE } from './workflow.js';
15
- export type { WorkflowAgentResult, WorkflowContext, WorkflowEntry, WorkflowMeta, WorkflowRegistry, WorkflowResult, WorkflowUtils, } from './workflows/index.js';
16
- export { BUILTIN_WORKFLOWS, buildWorkflowContext, createAndPopulateRegistry, createBuiltinWorkflows, createWorkflowRegistry, discoverWorkflows, executeWorkflow, } from './workflows/index.js';
15
+ export type { SaveResult, SaveTarget, WorkflowAgentResult, WorkflowContext, WorkflowEntry, WorkflowMeta, WorkflowRegistry, WorkflowResult, WorkflowUtils, } from './workflows/index.js';
16
+ export { BUILTIN_WORKFLOWS, buildWorkflowContext, createAndPopulateRegistry, createBuiltinWorkflows, createWorkflowRegistry, discoverWorkflows, executeWorkflow, reloadRegistry, saveWorkflow, } from './workflows/index.js';
@@ -10,4 +10,4 @@ export { builtinCommands, createSlashRegistry, parseSlashInput } from './slash.j
10
10
  export { clearSteering, drainSteering, injectSteering } from './steering.js';
11
11
  export { executeToolCall, executeToolCalls, partitionByConflict } from './tool-exec.js';
12
12
  export { buildWorkflowNotice, containsWorkflow, WORKFLOW_NOTICE } from './workflow.js';
13
- export { BUILTIN_WORKFLOWS, buildWorkflowContext, createAndPopulateRegistry, createBuiltinWorkflows, createWorkflowRegistry, discoverWorkflows, executeWorkflow, } from './workflows/index.js';
13
+ export { BUILTIN_WORKFLOWS, buildWorkflowContext, createAndPopulateRegistry, createBuiltinWorkflows, createWorkflowRegistry, discoverWorkflows, executeWorkflow, reloadRegistry, saveWorkflow, } from './workflows/index.js';
package/dist/core/loop.js CHANGED
@@ -598,6 +598,15 @@ async function* persistAssistantAndTools(state, deps, collectedText, validCalls)
598
598
  ...(deps.debugSpawn ? { debugSpawn: deps.debugSpawn } : {}),
599
599
  runSubAgent: (req) => runSubAgent({ ...deps, _subagentEventSink: eventSink }, state, req),
600
600
  ...(deps._subagentYieldCollector ? { collectYield: deps._subagentYieldCollector } : {}),
601
+ // todo 工具状态通过 dependency-reversal hook 注入:get/set 直接读写
602
+ // state.todoPhases(in-memory),tool result 的 metadata.phases 充当
603
+ // 持久化层——createAgent 时从历史消息恢复。
604
+ todoState: {
605
+ get: () => state.todoPhases,
606
+ set: (phases) => {
607
+ state.todoPhases = phases;
608
+ },
609
+ },
601
610
  }, validCalls, deps.hookRunner);
602
611
  const toolLatency = Date.now() - toolExecStart;
603
612
  const metricsEnabled = deps.config.toolMetrics.enabled;
@@ -3,14 +3,14 @@ declare const ROLE_DESCRIPTION = "You are c0de-agent, an open-source AI coding a
3
3
  declare const ENGINEERING_PRINCIPLES = "# Engineering Principles\n- Optimize for correctness first, then for the next maintainer six months out.\n- You have agency and taste: delete code that isn't pulling its weight, refuse unnecessary abstractions, prefer boring when it's called for.\n- Consider what code compiles to. Never allocate avoidably; no needless copies or computation.\n- You are not alone in this repo. Treat unexpected changes as the user's work and adapt \u2014 never revert edits you did not make unless explicitly asked.";
4
4
  declare const TOOL_USAGE = "# Tool Usage\nYou have dedicated tools \u2014 prefer them over shell commands for file operations:\n- Reading a file or listing a directory \u2192 `read` (NOT `cat`, `head`, `tail`).\n- Finding files by name or pattern \u2192 `glob` (NOT `find`, `ls -R`).\n- Searching file contents \u2192 `grep` (NOT shell `grep`/`rg`/`ack`, NOT `awk`/`sed`).\n- Modifying files \u2192 `edit`/`write` (NOT `sed`, `echo >`, heredocs).\n\nReserve `bash` for genuine command execution: builds, tests, git, or short pipelines that compute a fact (`wc -l`, `git status`, `diff`, a checksum). Never explore a codebase with `find`/`ls`/`cat` when `read`/`glob`/`grep` can.\n\nBatch independent tool calls in a single response \u2014 parallelize file reads and independent lookups. Only sequence when one call's result informs the next.\n\nWhen referencing code, use the `file_path:line_number` pattern so the user can navigate directly.";
5
5
  declare const CODEBASE = "# Working with the Codebase\nBefore changing files, understand the existing conventions. Mimic code style, reuse existing utilities, and follow established patterns.\n- NEVER assume a library is available, even if well known. Check package.json (or equivalent) and neighboring files first.\n- When creating a new component, look at existing ones first for naming, typing, and framework conventions.\n- When editing, read the surrounding context (especially imports) to make the change idiomatic. Never introduce code that exposes or logs secrets.";
6
- declare const EXECUTION_WORKFLOW = "# Execution Workflow\n1. Scope \u2014 plan before touching files; research existing code and conventions.\n2. Research \u2014 read sections, not snippets. Reuse existing patterns; a second convention beside an existing one is prohibited.\n3. Decompose \u2014 break multi-step work into steps and track them; skip for trivial requests. Plan only what makes the request work.\n4. Implement \u2014 fix problems at the source. Remove obsolete code \u2014 no leftover comments, aliases, or re-exports. Prefer editing existing files over new ones.\n5. Verify \u2014 never yield non-trivial work without proof: run the relevant tests. Prefer testing behavior, not plumbing. Don't test defaults.\n6. Cleanup \u2014 changelog, tests, docs, and removing scaffolding are the LAST phase, gated on the request demonstrably working. Never pre-plan cleanup before the request works.";
6
+ declare const EXECUTION_WORKFLOW = "# Execution Workflow\n1. Scope \u2014 plan before touching files; research existing code and conventions.\n2. Research \u2014 read sections, not snippets. Reuse existing patterns; a second convention beside an existing one is prohibited.\n3. Decompose \u2014 break multi-step work into steps and track them with the `todo` tool; skip for trivial requests. Plan only what makes the request work.\n4. Implement \u2014 fix problems at the source. Remove obsolete code \u2014 no leftover comments, aliases, or re-exports. Prefer editing existing files over new ones.\n5. Verify \u2014 never yield non-trivial work without proof: run the relevant tests. Prefer testing behavior, not plumbing. Don't test defaults.\n6. Cleanup \u2014 changelog, tests, docs, and removing scaffolding are the LAST phase, gated on the request demonstrably working. Never pre-plan cleanup before the request works.";
7
7
  declare const VERIFICATION = "# Verification & Evidence\n- Never yield non-trivial work without proof: tests, builds, or QA.\n- Run lint and typecheck after changes if the project provides them.\n- Every claim about code, tools, or tests must be grounded. Mark anything not directly observed as [INFERENCE].\n- Verification claims must match what was actually exercised. A passing typecheck does not prove an integration.";
8
8
  declare const DELIVERY_CONTRACT = "# Delivery Contract\n- \"Done\" means the deliverable behaves as specified end-to-end \u2014 not that a scaffold compiles.\n- Never yield unless complete. A phase boundary is never a yield point.\n- Never suppress tests to make code pass. Never fabricate outputs.\n- Never substitute an easier problem: don't infer extra scope, don't treat the symptom unless asked.\n- Never ship stubs, placeholders, mocks, no-ops, or fake fallbacks as finished work.\n- Default to clean cutover: migrate every caller; leave no shims or aliases.";
9
9
  declare const GIT_SAFETY = "# Git & Safety\n- NEVER commit unless the user explicitly asks. Committing is too proactive.\n- NEVER use `git reset --hard` or `git checkout --` unless explicitly approved.\n- Do not amend a commit unless asked.\n- You may be in a dirty worktree. NEVER revert changes you did not make; if unrelated changes conflict with your task, stop and ask.";
10
10
  declare const TONE = "# Tone & Output\n- Be concise and direct. Lead with the conclusion, then the evidence.\n- No preamble or postamble (\"The answer is\u2026\", \"Here is what I'll do next\u2026\").\n- Use GitHub-flavored markdown. Only use emojis if explicitly asked.\n- Don't hide uncertainty: state it at the specific claim, name the tradeoff.\n- For a simple question, a one-liner is best.";
11
11
  export { CODEBASE, DELIVERY_CONTRACT, ENGINEERING_PRINCIPLES, EXECUTION_WORKFLOW, GIT_SAFETY, ROLE_DESCRIPTION, TONE, TOOL_USAGE, VERIFICATION, };
12
12
  /** Built-in section ids in priority order. */
13
- export declare const BUILTIN_SECTION_IDS: readonly ["role", "systemPrompt", "engineering", "tool-usage", "codebase", "constraints", "execution-workflow", "verification", "delivery-contract", "git-safety", "tone", "project", "skills", "agents", "slash-commands"];
13
+ export declare const BUILTIN_SECTION_IDS: readonly ["role", "systemPrompt", "engineering", "tool-usage", "codebase", "constraints", "execution-workflow", "verification", "delivery-contract", "git-safety", "tone", "project", "skills", "agents", "slash-commands", "workflow-format"];
14
14
  /** Create a registry pre-loaded with the built-in sections. */
15
15
  declare function createPromptRegistry(): PromptRegistry;
16
16
  /** Register a section. A later registration with the same id overrides the earlier
@@ -37,7 +37,7 @@ This project follows a strict data + functions paradigm:
37
37
  const EXECUTION_WORKFLOW = `# Execution Workflow
38
38
  1. Scope — plan before touching files; research existing code and conventions.
39
39
  2. Research — read sections, not snippets. Reuse existing patterns; a second convention beside an existing one is prohibited.
40
- 3. Decompose — break multi-step work into steps and track them; skip for trivial requests. Plan only what makes the request work.
40
+ 3. Decompose — break multi-step work into steps and track them with the \`todo\` tool; skip for trivial requests. Plan only what makes the request work.
41
41
  4. Implement — fix problems at the source. Remove obsolete code — no leftover comments, aliases, or re-exports. Prefer editing existing files over new ones.
42
42
  5. Verify — never yield non-trivial work without proof: run the relevant tests. Prefer testing behavior, not plumbing. Don't test defaults.
43
43
  6. Cleanup — changelog, tests, docs, and removing scaffolding are the LAST phase, gated on the request demonstrably working. Never pre-plan cleanup before the request works.`;
@@ -83,6 +83,7 @@ export const BUILTIN_SECTION_IDS = [
83
83
  'skills',
84
84
  'agents',
85
85
  'slash-commands',
86
+ 'workflow-format',
86
87
  ];
87
88
  /** Built-in sections. Dynamic bodies (tool list, project info, skills) use `render`. */
88
89
  function builtinSections() {
@@ -164,8 +165,46 @@ function builtinSections() {
164
165
  priority: 120,
165
166
  render: () => {
166
167
  const lines = ['## Slash Commands'];
167
- for (const cmd of builtinCommands)
168
- lines.push(`- /${cmd.name}: ${cmd.description}`);
168
+ for (const cmd of builtinCommands) {
169
+ let line = `- /${cmd.name}: ${cmd.description}`;
170
+ if (cmd.subcommands) {
171
+ const subs = cmd.subcommands.map((s) => s.name).join('|');
172
+ line += ` (${subs})`;
173
+ }
174
+ lines.push(line);
175
+ }
176
+ return lines.join('\n');
177
+ },
178
+ },
179
+ // 工作流脚本格式提示:让模型知道工作流脚本是 JS 格式,放在 .c0de/workflows/。
180
+ {
181
+ id: 'workflow-format',
182
+ content: '',
183
+ priority: 121,
184
+ render: () => {
185
+ const lines = [
186
+ '## Dynamic Workflows',
187
+ 'When the user asks to create/save a workflow (e.g. "workflowz 创建工作流"), you MUST generate a **JavaScript** file (NOT bash/shell/python) with this structure:',
188
+ '```js',
189
+ '// File: .c0de/workflows/<name>.js',
190
+ 'export const meta = {',
191
+ " name: '<name>', // lowercase, kebab-case",
192
+ " description: '<description>',",
193
+ " phases: ['phase1', 'phase2'],",
194
+ '}',
195
+ '',
196
+ 'export default async function workflow(ctx) {',
197
+ ' const { runSubagents, runSubagent, utils, progress, project, args } = ctx',
198
+ ' // runSubagents(type, tasks[]) → parallel fan-out',
199
+ ' // runSubagent(type, { assignment, description? }) → single dispatch',
200
+ ' // utils: { glob, grep, read, splitByDirectory }',
201
+ ' // progress(message, { phase? }) → SSE progress',
202
+ " return { output: 'summary', data: {} }",
203
+ '}',
204
+ '```',
205
+ 'Save to `<project>/.c0de/workflows/<name>.js`. Available subagent types: general, coder, researcher, reviewer.',
206
+ 'Run with `/workflow run <name>` or POST /api/workflows/:name/run. NEVER create bash/shell scripts for workflows.',
207
+ ];
169
208
  return lines.join('\n');
170
209
  },
171
210
  },
@@ -1,6 +1,6 @@
1
1
  import { createSession } from '../session/session.js';
2
2
  import { createAgent } from './agent.js';
3
- import { BUILTIN_WORKFLOWS, createWorkflowRegistry, executeWorkflow } from './workflows/index.js';
3
+ import { BUILTIN_WORKFLOWS, createWorkflowRegistry, discoverWorkflows, executeWorkflow, reloadRegistry, saveWorkflow, } from './workflows/index.js';
4
4
  function parseSlashInput(input) {
5
5
  const trimmed = input.trim();
6
6
  if (!trimmed.startsWith('/'))
@@ -109,7 +109,14 @@ const configCommand = {
109
109
  const workflowCommand = {
110
110
  name: 'workflow',
111
111
  description: 'Manage and run workflows',
112
- argsHint: '[run|show|list] [name] [args]',
112
+ argsHint: '[list|run|show|create|edit] [name] [args]',
113
+ subcommands: [
114
+ { name: 'list', description: 'List available workflows' },
115
+ { name: 'run', description: 'Run a workflow', usage: '<name> [args]' },
116
+ { name: 'show', description: 'Show workflow source', usage: '<name>' },
117
+ { name: 'create', description: 'Create a workflow from file', usage: '<name> --file <path>' },
118
+ { name: 'edit', description: 'Edit workflow source', usage: '<name>' },
119
+ ],
113
120
  execute: async (args, ctx) => {
114
121
  const parts = args.split(/\s+/).filter(Boolean);
115
122
  const subcommand = parts[0] ?? 'list';
@@ -121,22 +128,35 @@ const workflowCommand = {
121
128
  registry.register(wf);
122
129
  }
123
130
  }
131
+ // 项目级工作流:从 agent cwd(= project worktree)动态发现。
132
+ // registry 是 server 单例(含 builtin + global + server-cwd),不含其他项目的工作流。
133
+ const projectWorkflows = await discoverWorkflows(ctx.cwd);
134
+ const projectByName = new Map(projectWorkflows.map((w) => [w.meta.name, w]));
135
+ const resolveEntry = (name) => registry.get(name) ?? projectByName.get(name);
124
136
  if (subcommand === 'list') {
125
- const workflows = registry.list();
137
+ // 合并 registry + 项目级(去重:同名项目级覆盖)
138
+ const byName = new Map(registry.list().map((w) => [w.meta.name, w]));
139
+ for (const wf of projectWorkflows)
140
+ byName.set(wf.meta.name, wf);
141
+ const workflows = Array.from(byName.values());
126
142
  const lines = ['Available workflows:'];
127
143
  for (const wf of workflows) {
128
144
  const phases = wf.meta.phases ? ` [${wf.meta.phases.join('→')}]` : '';
129
145
  lines.push(` /${wf.meta.name}${phases} — ${wf.meta.description} (${wf.source})`);
130
146
  }
131
147
  lines.push('');
132
- lines.push('Usage: /workflow run <name> [args]');
148
+ lines.push('Usage:');
149
+ lines.push(' /workflow run <name> [args] — 执行工作流');
150
+ lines.push(' /workflow create <name> --file <path> — 从文件创建工作流');
151
+ lines.push(' /workflow edit <name> — 编辑工作流源码');
152
+ lines.push(' /workflow show <name> — 查看工作流源码');
133
153
  return { _tag: 'text', text: lines.join('\n') };
134
154
  }
135
155
  if (subcommand === 'show') {
136
156
  const name = parts[1];
137
157
  if (!name)
138
158
  return { _tag: 'error', message: 'Usage: /workflow show <name>' };
139
- const wf = registry.get(name);
159
+ const wf = resolveEntry(name);
140
160
  if (!wf) {
141
161
  return { _tag: 'error', message: `Unknown workflow: ${name}` };
142
162
  }
@@ -146,11 +166,86 @@ const workflowCommand = {
146
166
  text: `// ${wf.meta.name}: ${wf.meta.description}\n\n${code}`,
147
167
  };
148
168
  }
169
+ if (subcommand === 'create') {
170
+ const name = parts[1];
171
+ if (!name)
172
+ return { _tag: 'error', message: 'Usage: /workflow create <name> --file <path>' };
173
+ // 解析 --file <path> 参数
174
+ const fileIdx = parts.indexOf('--file');
175
+ if (fileIdx === -1 || !parts[fileIdx + 1]) {
176
+ return {
177
+ _tag: 'error',
178
+ message: 'Usage: /workflow create <name> --file <path>\nTip: 也可通过 REST API POST /api/workflows { name, source } 创建',
179
+ };
180
+ }
181
+ const filePath = parts[fileIdx + 1] ?? '';
182
+ let source;
183
+ try {
184
+ source = await import('node:fs/promises').then((fs) => fs.readFile(filePath, 'utf-8'));
185
+ }
186
+ catch {
187
+ return { _tag: 'error', message: `Cannot read file: ${filePath}` };
188
+ }
189
+ const result = await saveWorkflow(name, source, 'project', ctx.cwd);
190
+ if (!result.ok) {
191
+ return { _tag: 'error', message: result.error };
192
+ }
193
+ // 热重载注册表
194
+ if (ctx.workflowRegistry) {
195
+ await reloadRegistry(ctx.workflowRegistry, ctx.cwd);
196
+ }
197
+ return {
198
+ _tag: 'success',
199
+ message: `Workflow "${name}" saved to ${result.filePath}\n现在可以用 /workflow run ${name} 执行,或在对话中输入 /${name} 调用。`,
200
+ };
201
+ }
202
+ if (subcommand === 'edit') {
203
+ const name = parts[1];
204
+ if (!name)
205
+ return { _tag: 'error', message: 'Usage: /workflow edit <name>' };
206
+ const wf = resolveEntry(name);
207
+ if (!wf) {
208
+ return { _tag: 'error', message: `Unknown workflow: ${name}` };
209
+ }
210
+ if (wf.source === 'builtin') {
211
+ return {
212
+ _tag: 'error',
213
+ message: 'Cannot edit builtin workflow. Fork it first: /workflow create <new-name> --file <path>',
214
+ };
215
+ }
216
+ if (!wf.filePath) {
217
+ return { _tag: 'error', message: `Workflow file path not available for "${name}"` };
218
+ }
219
+ const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
220
+ try {
221
+ const { spawnSync } = await import('node:child_process');
222
+ spawnSync(editor, [wf.filePath], { stdio: 'inherit' });
223
+ }
224
+ catch {
225
+ return { _tag: 'error', message: `Failed to launch editor: ${editor}` };
226
+ }
227
+ // 编辑后热重载
228
+ if (ctx.workflowRegistry) {
229
+ await reloadRegistry(ctx.workflowRegistry, ctx.cwd);
230
+ }
231
+ return { _tag: 'success', message: `Workflow "${name}" reloaded after edit.` };
232
+ }
149
233
  if (subcommand === 'run') {
150
234
  const name = parts[1];
151
235
  if (!name)
152
236
  return { _tag: 'error', message: 'Usage: /workflow run <name> [args]' };
153
237
  const wfArgs = parts.slice(2).join(' ');
238
+ const entry = resolveEntry(name);
239
+ if (!entry) {
240
+ const available = [
241
+ ...registry.list().map((e) => e.meta.name),
242
+ ...projectWorkflows.map((e) => e.meta.name),
243
+ ].join(', ');
244
+ return {
245
+ _tag: 'error',
246
+ message: `Unknown workflow: "${name}". Available: ${available || '(none)'}`,
247
+ };
248
+ }
154
249
  const agentConfig = {
155
250
  provider: ctx.config.defaultProvider,
156
251
  model: ctx.config.defaultModel,
@@ -163,6 +258,7 @@ const workflowCommand = {
163
258
  return executeWorkflow({
164
259
  registry,
165
260
  name,
261
+ entry,
166
262
  args: wfArgs,
167
263
  deps: ctx.deps,
168
264
  parent,
@@ -170,7 +266,7 @@ const workflowCommand = {
170
266
  }
171
267
  return {
172
268
  _tag: 'error',
173
- message: `Unknown subcommand: ${subcommand}. Use: list, run, show`,
269
+ message: `Unknown subcommand: ${subcommand}. Use: list, run, show, create, edit`,
174
270
  };
175
271
  },
176
272
  };
@@ -89,10 +89,19 @@ type CommandContext = {
89
89
  deps: AgentDependencies;
90
90
  workflowRegistry?: import('./workflows/registry.js').WorkflowRegistry;
91
91
  };
92
+ /** Declarative subcommand definition for commands like /workflow. */
93
+ type SubcommandDef = {
94
+ name: string;
95
+ description: string;
96
+ /** Usage hint shown in popover, e.g. "<name> [args]". */
97
+ usage?: string;
98
+ };
92
99
  type SlashCommand = {
93
100
  name: string;
94
101
  description: string;
95
102
  argsHint?: string;
103
+ /** Subcommands for dropdown completion (e.g. /workflow list, /workflow run). */
104
+ subcommands?: SubcommandDef[];
96
105
  execute: (args: string, ctx: CommandContext) => Promise<CommandResult>;
97
106
  };
98
- export type { AgentConfig, AgentDependencies, AgentError, AgentEvent, AgentState, AgentStatus, ChatTool, CommandContext, CommandResult, HookRunner, LLMSegment, PendingToolCall, ProjectInfo, PromptBuildContext, PromptContext, PromptRegistry, PromptSection, SlashCommand, TokenBudget, };
107
+ export type { AgentConfig, AgentDependencies, AgentError, AgentEvent, AgentState, AgentStatus, ChatTool, CommandContext, CommandResult, HookRunner, LLMSegment, PendingToolCall, ProjectInfo, PromptBuildContext, PromptContext, PromptRegistry, PromptSection, SlashCommand, SubcommandDef, TokenBudget, };
@@ -25,7 +25,7 @@ export declare function containsWorkflow(text: string): boolean;
25
25
  *
26
26
  * 适配 c0de-agent 的 task 工具 schema(subagent_type + context + tasks[])。
27
27
  */
28
- export declare const WORKFLOW_NOTICE = "<workflow-notice>\nThe user's message contains the **workflowz** keyword: you MUST drive this task as a deterministic multi-subagent workflow using the `task` tool for batched fan-out. This is not optional \u2014 the keyword signals the user explicitly wants parallel sub-agent decomposition, not inline work. This overrides any default tendency to do the whole task inline.\n\n<when>\nThe user typed workflowz intentionally \u2014 they want fan-out. You may do a brief inline scout (1-2 tool calls to list files/scope the work), but your NEXT action after scouting MUST be a `task` tool call to fan out. Do NOT spend the entire turn reading files inline \u2014 that defeats the purpose. Common shapes:\n- **Understand** \u2014 parallel readers over subsystems \u2192 structured map.\n- **Design** \u2014 independent approaches \u2192 scored synthesis.\n- **Review** \u2014 split dimensions \u2192 find per dimension \u2192 adversarially verify each finding.\n- **Research** \u2014 multi-modal sweep \u2192 deep-read the hits \u2192 synthesize.\n- **Migrate** \u2014 discover sites \u2192 transform each \u2192 verify.\n</when>\n\n<task-contract>\nCall `task` once per independent fan-out batch using the batch form:\n\n task({\n subagent_type: \"coder\",\n context: \"shared background all subagents need\",\n tasks: [\n { role: \"coder\", assignment: \"specific assignment for agent 1\", description: \"short label\" },\n { role: \"reviewer\", assignment: \"specific assignment for agent 2\", description: \"short label\" },\n ]\n })\n\nAvailable subagent types: `general` (full tools, recursive), `coder` (implementation), `researcher` (read-only scout), `reviewer` (code review). Pick the type that matches each task's intent.\n\n`context` carries shared background prepended to every subagent's prompt \u2014 put the shared contract, conventions, and coordination rules here.\n\nEach task in `tasks[]` must be self-contained:\n- `role`: specialist type for this sub-task (e.g. coder, reviewer, researcher)\n- `assignment`: exact target (files, symbols, subsystem) + what to do + acceptance criteria\n- `description`: short label for the UI\n\nEach subagent runs in an isolated session and returns its result via the `yield` tool. Subagents skip formatters, linters, and project-wide tests \u2014 the parent runs shared proof once after all results return.\n</task-contract>\n\n<structure>\nDecompose first, then batch the independent leaves:\n\n task({\n subagent_type: \"coder\",\n context: \"# Goal\nImplement feature X across the codebase\n# Constraints\nFollow existing patterns...\n# Contract\nReturn findings as structured data...\",\n tasks: [\n { role: \"coder\", assignment: \"# Target\nsrc/auth/login.ts\n# Change\nAdd rate limiting to login endpoint\n# Acceptance\nRate limiter works, tests pass\", description: \"Login rate limiting\" },\n { role: \"coder\", assignment: \"# Target\nsrc/auth/signup.ts\n# Change\nAdd input validation\n# Acceptance\nValidation blocks invalid emails\", description: \"Signup validation\" },\n ]\n })\n\nPrefer one wide batch over serial calls when work items do not share files. If tasks overlap, have agents coordinate before editing.\n</structure>\n\n<patterns>\n- **Adversarial verify** \u2014 dispatch skeptical reviewers with distinct targets, then keep only findings you can verify against source.\n- **Perspective-diverse review** \u2014 use separate correctness, security, performance roles instead of identical reviewers.\n- **Completeness critic** \u2014 after the first batch, dispatch one read-only critic that asks what was missed.\n- **No silent caps** \u2014 if you bound coverage (top-N, sampling), state what was dropped and why.\n- **Parent owns closure** \u2014 subagents return evidence; the parent reads it, resolves contradictions, runs proof, and makes the final decision.\n</patterns>\n\n<execution>\n- **You MUST call the `task` tool** \u2014 this is the defining action of a workflowz request. A response that only does inline read/glob/edit without dispatching sub-agents is a failure.\n- Capture multi-phase workflow state in the visible todo system when available.\n- Batch independent subagents in one `task` call.\n- Give every subagent a narrow target, explicit non-goals, and a concrete return packet.\n- After fan-out returns, read the results, patch or decide, and run the shared gate.\n- Keep going until the task is closed \u2014 returned fan-out is a step, not a stopping point.\n</execution>\n</workflow-notice>";
28
+ export declare const WORKFLOW_NOTICE = "<workflow-notice>\nThe user's message contains the **workflowz** keyword. This triggers the Dynamic Workflow system. You MUST first determine the user's intent:\n\n<intent-detection>\n- **Create/save a workflow** \u2014 the user says \"workflowz \u521B\u5EFA/\u5EFA/save/make a workflow\" \u2014 they want you to GENERATE a reusable JavaScript workflow script and save it. Jump to <create-workflow> below.\n- **Execute a workflow** \u2014 the user says \"workflowz \u626B\u63CF/\u5BA1\u67E5/\u91CD\u6784/\u6D4B\u8BD5...\" \u2014 they want you to drive a deterministic multi-subagent fan-out using the `task` tool. Jump to <task-contract> below.\n- **Both** \u2014 \"workflowz \u521B\u5EFA\u4E00\u4E2A\u626B\u63CF\u5DE5\u4F5C\u6D41\" means create the workflow script first, then optionally run it.\n</intent-detection>\n\n<create-workflow>\nWhen the intent is to CREATE a workflow, you MUST generate a **JavaScript** file (NOT bash/shell/python) with this exact structure:\n\n```js\n// File: .c0de/workflows/<name>.js\nexport const meta = {\n name: '<name>', // lowercase, kebab-case, matches filename\n description: '<description>',\n argsHint: '[optional args]',\n phases: ['phase1', 'phase2', ...],\n timeout: 300, // optional, seconds\n}\n\nexport default async function workflow(ctx) {\n const { runSubagents, runSubagent, utils, progress, project, args } = ctx\n\n // Phase 1: progress('starting...', { phase: 'phase1' })\n // Call runSubagents for parallel fan-out:\n const results = await runSubagents('researcher', [\n { assignment: '...', description: 'task 1' },\n { assignment: '...', description: 'task 2' },\n ])\n // results: [{ ok: true, output: '...' }, { ok: false, error: '...' }]\n\n // Phase 2: verify / cross-check results\n // Phase 3: return summary\n return { output: 'summary text', data: { ... } }\n}\n```\n\n**Rules:**\n1. The file MUST be `.js` with ES module exports (`export const meta` + `export default async function`).\n2. Save to `<project_root>/.c0de/workflows/<name>.js` \u2014 use the `write` tool.\n3. Use `ctx.runSubagents(type, tasks[])` for parallel dispatch, `ctx.runSubagent(type, params)` for single dispatch.\n4. Available subagent types: `general`, `coder`, `researcher`, `reviewer`.\n5. Use `ctx.utils` for file ops: `glob(pattern)`, `grep(pattern, path?)`, `read(path, range?)`, `splitByDirectory(dir, {depth, ignore})`.\n6. Use `ctx.progress(message, { phase })` to report progress.\n7. Parse subagent output with `JSON.parse(r.output)` \u2014 subagents return text.\n8. Return `{ output: 'summary', data: {...} }` as the final result.\n9. NEVER generate bash/shell scripts, python scripts, or any non-JS format for workflows.\n10. After writing the file, tell the user they can run it with `/workflow run <name>` or via POST /api/workflows/:name/run.\n</create-workflow>\n\n<when>\nWhen the intent is to EXECUTE a workflow (not create), you may do a brief inline scout (1-2 tool calls to list files/scope the work), but your NEXT action after scouting MUST be a `task` tool call to fan out. Do NOT spend the entire turn reading files inline \u2014 that defeats the purpose. Common shapes:\n- **Understand** \u2014 parallel readers over subsystems \u2192 structured map.\n- **Design** \u2014 independent approaches \u2192 scored synthesis.\n- **Review** \u2014 split dimensions \u2192 find per dimension \u2192 adversarially verify each finding.\n- **Research** \u2014 multi-modal sweep \u2192 deep-read the hits \u2192 synthesize.\n- **Migrate** \u2014 discover sites \u2192 transform each \u2192 verify.\n</when>\n\n<task-contract>\nCall `task` once per independent fan-out batch using the batch form:\n\n task({\n subagent_type: \"coder\",\n context: \"shared background all subagents need\",\n tasks: [\n { role: \"coder\", assignment: \"specific assignment for agent 1\", description: \"short label\" },\n { role: \"reviewer\", assignment: \"specific assignment for agent 2\", description: \"short label\" },\n ]\n })\n\nAvailable subagent types: `general` (full tools, recursive), `coder` (implementation), `researcher` (read-only scout), `reviewer` (code review). Pick the type that matches each task's intent.\n\n`context` carries shared background prepended to every subagent's prompt \u2014 put the shared contract, conventions, and coordination rules here.\n\nEach task in `tasks[]` must be self-contained:\n- `role`: specialist type for this sub-task (e.g. coder, reviewer, researcher)\n- `assignment`: exact target (files, symbols, subsystem) + what to do + acceptance criteria\n- `description`: short label for the UI\n\nEach subagent runs in an isolated session and returns its result via the `yield` tool. Subagents skip formatters, linters, and project-wide tests \u2014 the parent runs shared proof once after all results return.\n</task-contract>\n\n<structure>\nDecompose first, then batch the independent leaves:\n\n task({\n subagent_type: \"coder\",\n context: \"# Goal\nImplement feature X across the codebase\n# Constraints\nFollow existing patterns...\n# Contract\nReturn findings as structured data...\",\n tasks: [\n { role: \"coder\", assignment: \"# Target\nsrc/auth/login.ts\n# Change\nAdd rate limiting to login endpoint\n# Acceptance\nRate limiter works, tests pass\", description: \"Login rate limiting\" },\n { role: \"coder\", assignment: \"# Target\nsrc/auth/signup.ts\n# Change\nAdd input validation\n# Acceptance\nValidation blocks invalid emails\", description: \"Signup validation\" },\n ]\n })\n\nPrefer one wide batch over serial calls when work items do not share files. If tasks overlap, have agents coordinate before editing.\n</structure>\n\n<patterns>\n- **Adversarial verify** \u2014 dispatch skeptical reviewers with distinct targets, then keep only findings you can verify against source.\n- **Perspective-diverse review** \u2014 use separate correctness, security, performance roles instead of identical reviewers.\n- **Completeness critic** \u2014 after the first batch, dispatch one read-only critic that asks what was missed.\n- **No silent caps** \u2014 if you bound coverage (top-N, sampling), state what was dropped and why.\n- **Parent owns closure** \u2014 subagents return evidence; the parent reads it, resolves contradictions, runs proof, and makes the final decision.\n</patterns>\n\n<execution>\n- **You MUST call the `task` tool** \u2014 this is the defining action of a workflowz request. A response that only does inline read/glob/edit without dispatching sub-agents is a failure.\n- Capture multi-phase workflow state in the visible todo system when available.\n- Batch independent subagents in one `task` call.\n- Give every subagent a narrow target, explicit non-goals, and a concrete return packet.\n- After fan-out returns, read the results, patch or decide, and run the shared gate.\n- Keep going until the task is closed \u2014 returned fan-out is a step, not a stopping point.\n</execution>\n</workflow-notice>";
29
29
  /**
30
30
  * 构建工作流 steering 通知:基础通知 + 可选的已注册工作流列表。
31
31
  * 无工作流时退化为纯基础通知(向后兼容)。
@@ -37,10 +37,59 @@ export function containsWorkflow(text) {
37
37
  * 适配 c0de-agent 的 task 工具 schema(subagent_type + context + tasks[])。
38
38
  */
39
39
  export const WORKFLOW_NOTICE = `<workflow-notice>
40
- The user's message contains the **workflowz** keyword: you MUST drive this task as a deterministic multi-subagent workflow using the \`task\` tool for batched fan-out. This is not optional — the keyword signals the user explicitly wants parallel sub-agent decomposition, not inline work. This overrides any default tendency to do the whole task inline.
40
+ The user's message contains the **workflowz** keyword. This triggers the Dynamic Workflow system. You MUST first determine the user's intent:
41
+
42
+ <intent-detection>
43
+ - **Create/save a workflow** — the user says "workflowz 创建/建/save/make a workflow" — they want you to GENERATE a reusable JavaScript workflow script and save it. Jump to <create-workflow> below.
44
+ - **Execute a workflow** — the user says "workflowz 扫描/审查/重构/测试..." — they want you to drive a deterministic multi-subagent fan-out using the \`task\` tool. Jump to <task-contract> below.
45
+ - **Both** — "workflowz 创建一个扫描工作流" means create the workflow script first, then optionally run it.
46
+ </intent-detection>
47
+
48
+ <create-workflow>
49
+ When the intent is to CREATE a workflow, you MUST generate a **JavaScript** file (NOT bash/shell/python) with this exact structure:
50
+
51
+ \`\`\`js
52
+ // File: .c0de/workflows/<name>.js
53
+ export const meta = {
54
+ name: '<name>', // lowercase, kebab-case, matches filename
55
+ description: '<description>',
56
+ argsHint: '[optional args]',
57
+ phases: ['phase1', 'phase2', ...],
58
+ timeout: 300, // optional, seconds
59
+ }
60
+
61
+ export default async function workflow(ctx) {
62
+ const { runSubagents, runSubagent, utils, progress, project, args } = ctx
63
+
64
+ // Phase 1: progress('starting...', { phase: 'phase1' })
65
+ // Call runSubagents for parallel fan-out:
66
+ const results = await runSubagents('researcher', [
67
+ { assignment: '...', description: 'task 1' },
68
+ { assignment: '...', description: 'task 2' },
69
+ ])
70
+ // results: [{ ok: true, output: '...' }, { ok: false, error: '...' }]
71
+
72
+ // Phase 2: verify / cross-check results
73
+ // Phase 3: return summary
74
+ return { output: 'summary text', data: { ... } }
75
+ }
76
+ \`\`\`
77
+
78
+ **Rules:**
79
+ 1. The file MUST be \`.js\` with ES module exports (\`export const meta\` + \`export default async function\`).
80
+ 2. Save to \`<project_root>/.c0de/workflows/<name>.js\` — use the \`write\` tool.
81
+ 3. Use \`ctx.runSubagents(type, tasks[])\` for parallel dispatch, \`ctx.runSubagent(type, params)\` for single dispatch.
82
+ 4. Available subagent types: \`general\`, \`coder\`, \`researcher\`, \`reviewer\`.
83
+ 5. Use \`ctx.utils\` for file ops: \`glob(pattern)\`, \`grep(pattern, path?)\`, \`read(path, range?)\`, \`splitByDirectory(dir, {depth, ignore})\`.
84
+ 6. Use \`ctx.progress(message, { phase })\` to report progress.
85
+ 7. Parse subagent output with \`JSON.parse(r.output)\` — subagents return text.
86
+ 8. Return \`{ output: 'summary', data: {...} }\` as the final result.
87
+ 9. NEVER generate bash/shell scripts, python scripts, or any non-JS format for workflows.
88
+ 10. After writing the file, tell the user they can run it with \`/workflow run <name>\` or via POST /api/workflows/:name/run.
89
+ </create-workflow>
41
90
 
42
91
  <when>
43
- The user typed workflowz intentionally they want fan-out. You may do a brief inline scout (1-2 tool calls to list files/scope the work), but your NEXT action after scouting MUST be a \`task\` tool call to fan out. Do NOT spend the entire turn reading files inline — that defeats the purpose. Common shapes:
92
+ When the intent is to EXECUTE a workflow (not create), you may do a brief inline scout (1-2 tool calls to list files/scope the work), but your NEXT action after scouting MUST be a \`task\` tool call to fan out. Do NOT spend the entire turn reading files inline — that defeats the purpose. Common shapes:
44
93
  - **Understand** — parallel readers over subsystems → structured map.
45
94
  - **Design** — independent approaches → scored synthesis.
46
95
  - **Review** — split dimensions → find per dimension → adversarially verify each finding.
@@ -111,10 +160,12 @@ Prefer one wide batch over serial calls when work items do not share files. If t
111
160
  export function buildWorkflowNotice(workflows) {
112
161
  const registeredSection = workflows && workflows.length > 0
113
162
  ? `\n<registered-workflows>
114
- Available workflow templates you can invoke with the task tool's workflow parameter:
163
+ Available saved workflow scripts (run with /workflow run <name> or POST /api/workflows/:name/run):
115
164
  ${workflows.map((w) => `- ${w.name}: ${w.description}`).join('\n')}
116
- If none fit, orchestrate inline using runSubagent fan-out as described below.
165
+ To create a new workflow, generate a .js file with export const meta + export default async function(ctx) and save it to .c0de/workflows/<name>.js.
117
166
  </registered-workflows>`
118
- : '';
167
+ : `\n<registered-workflows>
168
+ No saved workflow scripts yet. When the user asks to create a workflow, generate a .js file (export const meta + export default async function) and save it to .c0de/workflows/<name>.js.
169
+ </registered-workflows>`;
119
170
  return WORKFLOW_NOTICE + registeredSection;
120
171
  }
@@ -1,4 +1,4 @@
1
- import type { WorkflowEntry } from './types.js';
1
+ import type { WorkflowEntry, WorkflowMeta } from './types.js';
2
2
  /**
3
3
  * 扫描项目目录下的 `.c0de/workflows/*.js` 文件,source 标记为 'project'。
4
4
  */
@@ -8,4 +8,25 @@ declare function discoverWorkflows(projectDir: string): Promise<WorkflowEntry[]>
8
8
  * 目录不存在时返回空数组(与项目级一致)。
9
9
  */
10
10
  declare function discoverGlobalWorkflows(): Promise<WorkflowEntry[]>;
11
- export { discoverGlobalWorkflows, discoverWorkflows };
11
+ /** saveWorkflow 的目标层级。 */
12
+ type SaveTarget = 'project' | 'user';
13
+ /** saveWorkflow 的结果。 */
14
+ type SaveResult = {
15
+ ok: true;
16
+ filePath: string;
17
+ meta: WorkflowMeta;
18
+ } | {
19
+ ok: false;
20
+ error: string;
21
+ };
22
+ /**
23
+ * 将工作流源码保存到磁盘并验证可加载。
24
+ *
25
+ * - name 必须匹配 `[a-z0-9-]+`
26
+ * - 写入 `<projectDir>/.c0de/workflows/<name>.js`(project)或 `~/.c0de/workflows/<name>.js`(user)
27
+ * - 写入后 dynamic import 验证 meta + default 导出存在;验证失败则删除文件
28
+ * - 返回 { ok, filePath, meta } 或 { ok: false, error }
29
+ */
30
+ declare function saveWorkflow(name: string, source: string, target?: SaveTarget, projectDir?: string): Promise<SaveResult>;
31
+ export { discoverGlobalWorkflows, discoverWorkflows, saveWorkflow };
32
+ export type { SaveResult, SaveTarget };
@@ -1,4 +1,4 @@
1
- import { readdir, readFile } from 'node:fs/promises';
1
+ import { mkdir, readdir, readFile, unlink, writeFile } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import { basename, join } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
@@ -66,4 +66,40 @@ async function discoverWorkflows(projectDir) {
66
66
  async function discoverGlobalWorkflows() {
67
67
  return discoverFromDir(join(homedir(), GLOBAL_WORKFLOWS_DIR), 'user');
68
68
  }
69
- export { discoverGlobalWorkflows, discoverWorkflows };
69
+ /** 合法工作流名称:仅小写字母、数字、连字符。 */
70
+ const WORKFLOW_NAME_RE = /^[a-z0-9-]+$/;
71
+ /**
72
+ * 将工作流源码保存到磁盘并验证可加载。
73
+ *
74
+ * - name 必须匹配 `[a-z0-9-]+`
75
+ * - 写入 `<projectDir>/.c0de/workflows/<name>.js`(project)或 `~/.c0de/workflows/<name>.js`(user)
76
+ * - 写入后 dynamic import 验证 meta + default 导出存在;验证失败则删除文件
77
+ * - 返回 { ok, filePath, meta } 或 { ok: false, error }
78
+ */
79
+ async function saveWorkflow(name, source, target = 'project', projectDir) {
80
+ if (!WORKFLOW_NAME_RE.test(name)) {
81
+ return { ok: false, error: `Invalid workflow name "${name}": must match [a-z0-9-]+` };
82
+ }
83
+ const dir = target === 'project'
84
+ ? join(projectDir ?? '.', PROJECT_WORKFLOWS_DIR)
85
+ : join(homedir(), GLOBAL_WORKFLOWS_DIR);
86
+ await mkdir(dir, { recursive: true });
87
+ const filePath = join(dir, `${name}.js`);
88
+ await writeFile(filePath, source, 'utf-8');
89
+ // 验证:dynamic import 检查 meta + default
90
+ try {
91
+ const fileUrl = pathToFileURL(filePath).href;
92
+ const mod = (await import(`${fileUrl}#${Date.now()}`));
93
+ if (!mod.meta || typeof mod.default !== 'function') {
94
+ await unlink(filePath);
95
+ return { ok: false, error: 'Workflow source missing `export const meta` or default export' };
96
+ }
97
+ const meta = { ...mod.meta, name: mod.meta.name ?? name };
98
+ return { ok: true, filePath, meta };
99
+ }
100
+ catch (e) {
101
+ await unlink(filePath);
102
+ return { ok: false, error: `Failed to load workflow: ${e instanceof Error ? e.message : String(e)}` };
103
+ }
104
+ }
105
+ export { discoverGlobalWorkflows, discoverWorkflows, saveWorkflow };
@@ -1,7 +1,8 @@
1
1
  export { BUILTIN_WORKFLOWS, createBuiltinWorkflows } from './builtins.js';
2
2
  export { buildWorkflowContext } from './context.js';
3
- export { discoverGlobalWorkflows, discoverWorkflows } from './discovery.js';
3
+ export { discoverGlobalWorkflows, discoverWorkflows, saveWorkflow } from './discovery.js';
4
+ export type { SaveResult, SaveTarget } from './discovery.js';
4
5
  export type { WorkflowRegistry } from './registry.js';
5
- export { createAndPopulateRegistry, createWorkflowRegistry } from './registry.js';
6
+ export { createAndPopulateRegistry, createWorkflowRegistry, reloadRegistry } from './registry.js';
6
7
  export { executeWorkflow } from './runtime.js';
7
8
  export type { WorkflowAgentResult, WorkflowContext, WorkflowEntry, WorkflowMeta, WorkflowModule, WorkflowResult, WorkflowSource, WorkflowUtils, } from './types.js';
@@ -1,5 +1,5 @@
1
1
  export { BUILTIN_WORKFLOWS, createBuiltinWorkflows } from './builtins.js';
2
2
  export { buildWorkflowContext } from './context.js';
3
- export { discoverGlobalWorkflows, discoverWorkflows } from './discovery.js';
4
- export { createAndPopulateRegistry, createWorkflowRegistry } from './registry.js';
3
+ export { discoverGlobalWorkflows, discoverWorkflows, saveWorkflow } from './discovery.js';
4
+ export { createAndPopulateRegistry, createWorkflowRegistry, reloadRegistry } from './registry.js';
5
5
  export { executeWorkflow } from './runtime.js';
@@ -6,6 +6,8 @@ declare function createWorkflowRegistry(): {
6
6
  list(): WorkflowEntry[];
7
7
  has(name: string): boolean;
8
8
  delete(name: string): boolean;
9
+ /** 清空注册表(热重载前调用)。 */
10
+ clear(): void;
9
11
  };
10
12
  type WorkflowRegistry = ReturnType<typeof createWorkflowRegistry>;
11
13
  /**
@@ -16,5 +18,10 @@ type WorkflowRegistry = ReturnType<typeof createWorkflowRegistry>;
16
18
  * 覆盖优先级:project > user > builtin。
17
19
  */
18
20
  declare function createAndPopulateRegistry(projectDir: string): Promise<WorkflowRegistry>;
21
+ /**
22
+ * 重新填充已有注册表:清空 → 三级发现 → 注册。
23
+ * 用于 create/save 后热重载,保留同一 registry 引用。
24
+ */
25
+ declare function reloadRegistry(registry: WorkflowRegistry, projectDir: string): Promise<void>;
19
26
  export type { WorkflowRegistry };
20
- export { createAndPopulateRegistry, createWorkflowRegistry };
27
+ export { createAndPopulateRegistry, createWorkflowRegistry, reloadRegistry };
@@ -19,6 +19,10 @@ function createWorkflowRegistry() {
19
19
  delete(name) {
20
20
  return entries.delete(name);
21
21
  },
22
+ /** 清空注册表(热重载前调用)。 */
23
+ clear() {
24
+ entries.clear();
25
+ },
22
26
  };
23
27
  }
24
28
  /**
@@ -46,4 +50,20 @@ async function createAndPopulateRegistry(projectDir) {
46
50
  }
47
51
  return registry;
48
52
  }
49
- export { createAndPopulateRegistry, createWorkflowRegistry };
53
+ /**
54
+ * 重新填充已有注册表:清空 → 三级发现 → 注册。
55
+ * 用于 create/save 后热重载,保留同一 registry 引用。
56
+ */
57
+ async function reloadRegistry(registry, projectDir) {
58
+ registry.clear();
59
+ for (const wf of await createBuiltinWorkflows()) {
60
+ registry.register(wf);
61
+ }
62
+ for (const wf of await discoverGlobalWorkflows()) {
63
+ registry.register(wf);
64
+ }
65
+ for (const wf of await discoverWorkflows(projectDir)) {
66
+ registry.register(wf);
67
+ }
68
+ }
69
+ export { createAndPopulateRegistry, createWorkflowRegistry, reloadRegistry };