feat(workflow): 复刻 ultracode 手册并修复 worktree/inline/opt-in 三处缺口

围绕 ultracode skill 审查 agent 系统一致性后:
- ultracode.ts: 用系统提示版完整 Workflow 编排手册替换中文精简版
- HIGH#1 isolation:'worktree': claudeCodeBackend.run() 用 createAgentWorktree +
  runWithCwdOverride 包裹 runAgent + finally 清理实现真正的 cwd 隔离;slug 用
  sha256(runId:agentId) 派生以匹配 cleanupStaleAgentWorktrees 清理正则
  (修 runId 为 w+base36 非 UUID 导致的泄漏盲区);worktree.ts 注释同步修正
- HIGH#2 inline 持久化: 新增 persistInlineScript,WorkflowTool + service 两条
  inline 路径对称持久化到 .claude/workflow-runs/<runId>/script.js,返回可复用
  scriptPath(闭环 inline→编辑→scriptPath 重提迭代循环)
- HIGH#3 opt-in 分工: ultracode/WorkflowTool/effort 注明 session reminder 由
  harness 注入,repo 内无 ultracode 信号,保持 feature('WORKFLOW_SCRIPTS') +
  isEnabled 两层 gate,不自造注入
- 测试: 新增 persistInline.test.ts;扩展 claudeCodeBackend(isolation 4 用例)/
  WorkflowTool(inline)/service(scriptPath)/ultracode(harness)

含配套 workflow engine/panel 完善与 run-state-persistence design doc。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-06-13 23:04:33 +08:00
parent d236880bc3
commit 54d2bf6f12
32 changed files with 2253 additions and 196 deletions

View File

@@ -1,6 +1,7 @@
import {
listNamedWorkflows,
parseScript,
persistInlineScript,
resolveNamedWorkflow,
runWorkflow,
WORKFLOW_DIR_NAME,
@@ -49,7 +50,7 @@ export type WorkflowService = {
>,
toolUseContext: ToolUseContext,
canUseTool: CanUseToolFn,
): Promise<{ runId: string }>
): Promise<{ runId: string; scriptPath?: string }>
kill(runId: string): void
/**
* 进程退出 / 配置卸载时清理:杀掉所有 running run避免孤儿 task。
@@ -86,6 +87,7 @@ export function getWorkflowService(): WorkflowService {
export function makeService(
ports: WorkflowPorts,
store: ProgressStore,
cwdOverride?: string,
): WorkflowService {
const buildHost = (
toolUseContext: ToolUseContext,
@@ -94,7 +96,8 @@ export function makeService(
handle: makeHostHandle(buildHostBundle(toolUseContext, canUseTool)),
// 用 projectRoot 与 ports.ts hostFactory / journalStore 保持同根;
// 进入 worktree/子目录时不会让命名 workflow 解析与 journal 落盘不同步。
cwd: getProjectRoot(),
// cwdOverride 仅供测试注入临时目录(避免 inline 持久化写真实项目目录)。
cwd: cwdOverride ?? getProjectRoot(),
budgetTotal: null, // turn 级预算注入点(未来从 settings 读)
toolUseId: toolUseContext.toolUseId,
})
@@ -158,6 +161,23 @@ export function makeService(
host.handle,
)
// inline 入口持久化脚本到 run 目录(与 WorkflowTool 对称),返回可复用路径。
// 写盘失败降级log不阻断 runscript 已在内存)。
let persistedScriptPath: string | undefined
if (!workflowFile && input.script) {
try {
persistedScriptPath = await persistInlineScript(
input.script,
runId,
host.cwd,
)
} catch (e) {
logForDebugging(
`workflow inline script persist failed: ${(e as Error).message}`,
)
}
}
// detached不 await让调用方立即拿到 runId结束路由到 registrar。
void runWorkflow({
script,
@@ -183,7 +203,10 @@ export function makeService(
.catch(e => ports.taskRegistrar.fail(runId, (e as Error).message))
logForDebugging(`workflow launched: ${runId} (${workflowName})`)
return { runId }
return {
runId,
...(persistedScriptPath ? { scriptPath: persistedScriptPath } : {}),
}
},
kill(runId) {
@@ -193,8 +216,17 @@ export function makeService(
shutdown() {
// 仅杀 running已完成/失败的 run taskRegistrar 已回收 bindingkill 是 no-op。
// taskRegistrar.kill 对未知 runId 安全 no-op因此幂等——多次 shutdown 不重复抛错。
// 每个 kill 单独 try/catchkill 内部走 setAppState进程 exit 阶段触发 React 重渲染
// 可能抛错render 已卸载等);单个失败不应阻断其他 run 的清理。
for (const run of store.list()) {
if (run.status === 'running') ports.taskRegistrar.kill(run.runId)
if (run.status !== 'running') continue
try {
ports.taskRegistrar.kill(run.runId)
} catch (e) {
logForDebugging(
`workflow shutdown: kill ${run.runId} failed: ${(e as Error).message}`,
)
}
}
},