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

@@ -62,13 +62,15 @@ export { TerminalCaptureTool } from './tools/TerminalCaptureTool/TerminalCapture
export { VerifyPlanExecutionTool } from './tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.js'
export { WebBrowserTool } from './tools/WebBrowserTool/WebBrowserTool.js'
// WorkflowTool 实现已迁移到 @claude-code-best/workflow-engine独立包端口适配
// 这里仅 re-export 工厂与常量,保持向后兼容
// 注意:本 commit 移除了 builtin-tools 的 WorkflowTool 值导出和 getWorkflowCommands
// - WorkflowTool 工厂:改由 @claude-code-best/workflow-engine 的 createWorkflowTool 提供
// - getWorkflowCommands已移除功能迁至 src/workflow/namedWorkflowCommands.ts
// 第三方若从本包 import 这两个符号,需切换到新路径。
export {
createWorkflowTool,
WORKFLOW_TOOL_NAME,
type WorkflowToolDescriptor,
} from '@claude-code-best/workflow-engine'
export { initBundledWorkflows } from './tools/WorkflowTool/bundled/index.js'
// Constants
export {

View File

@@ -1,15 +0,0 @@
// Bundled workflow initialization.
// Called by tools.ts when WORKFLOW_SCRIPTS feature flag is enabled.
// Sets up any pre-bundled workflow scripts that ship with the CLI.
/**
* Initialize bundled workflows. Called once at startup when the
* WORKFLOW_SCRIPTS feature flag is active. This is the hook point
* for registering any workflow scripts that are compiled into the
* binary (as opposed to user-authored ones in .claude/workflows/).
*/
export function initBundledWorkflows(): void {
// Bundled workflows are registered here at startup.
// Currently a no-op — all workflows are user-authored in .claude/workflows/.
// This function exists as the extension point for future built-in workflows.
}

View File

@@ -1,5 +1,5 @@
import { expect, test } from 'bun:test'
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { createWorkflowTool } from '../tool/WorkflowTool.js'
@@ -74,6 +74,34 @@ test('call 返回 launch 消息并在后台完成', async () => {
}
})
test('inline script 持久化到 run 目录,返回真实 scriptPath', async () => {
const dir = await mkdtemp(join(tmpdir(), 'wf-tool-'))
try {
const { ports } = mockPorts(
dir,
new Map([['x', { kind: 'ok', output: 'x', usage: { outputTokens: 1 } }]]),
)
const tool = createWorkflowTool(ports)
const res = await tool.call(
{ script: `return agent('x')` },
undefined,
undefined,
undefined,
)
const expectedPath = join(
dir,
'.claude',
'workflow-runs',
'run-x',
'script.js',
)
expect(res.data.output).toContain(expectedPath)
expect(await readFile(expectedPath, 'utf-8')).toBe(`return agent('x')`)
} finally {
await rm(dir, { recursive: true, force: true })
}
})
test('缺少 script/name/scriptPath → 返回错误(不进后台)', async () => {
const dir = await mkdtemp(join(tmpdir(), 'wf-tool-'))
try {

View File

@@ -0,0 +1,41 @@
import { expect, test } from 'bun:test'
import { mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { persistInlineScript } from '../tool/persistInline.js'
test('持久化到 <cwd>/.claude/workflow-runs/<runId>/script.js 并返回路径', async () => {
const dir = await mkdtemp(join(tmpdir(), 'wf-pi-'))
try {
const path = await persistInlineScript('return 1', 'r1', dir)
expect(path).toBe(join(dir, '.claude', 'workflow-runs', 'r1', 'script.js'))
expect(await readFile(path, 'utf-8')).toBe('return 1')
} finally {
await rm(dir, { recursive: true, force: true })
}
})
test('同 runId 重复写覆盖mkdir 幂等,不抛错)', async () => {
const dir = await mkdtemp(join(tmpdir(), 'wf-pi-'))
try {
await persistInlineScript('first', 'r2', dir)
const path = await persistInlineScript('second', 'r2', dir)
expect(await readFile(path, 'utf-8')).toBe('second')
} finally {
await rm(dir, { recursive: true, force: true })
}
})
test('不同 runId 互不干扰(各自独立子目录)', async () => {
const dir = await mkdtemp(join(tmpdir(), 'wf-pi-'))
try {
const p1 = await persistInlineScript('a', 'run-a', dir)
const p2 = await persistInlineScript('b', 'run-b', dir)
expect(p1).not.toBe(p2)
expect(await readFile(p1, 'utf-8')).toBe('a')
expect(await readFile(p2, 'utf-8')).toBe('b')
} finally {
await rm(dir, { recursive: true, force: true })
}
})

View File

@@ -1,6 +1,10 @@
// Agent 后端适配器抽象。引擎通过 registry 取 adapter 再调 run不关心具体实现
// Anthropic SDK / 核心 runAgent / OpenAI / 本地模型 / mock 均为 adapter 的实现)。
import type { AgentRunParams, AgentRunResult } from './types.js'
import type {
AgentProgressUpdate,
AgentRunParams,
AgentRunResult,
} from './types.js'
import type { HostHandle } from './ports.js'
/** adapter 能力声明。引擎/脚本据此降级(如后端不支持 schema 则改文本 + 解析)。 */
@@ -21,6 +25,11 @@ export type AgentAdapterContext = {
signal: AbortSignal
/** 当前 workflow runId日志/追踪用)。 */
runId: string
/**
* 运行中进度上报(后端循环累计 token/tool 时调用)。可选:独立后端可不实现;
* 引擎据此发 agent_progress 事件(闭包带 agentId/runId 关联),面板实时刷新。
*/
onProgress?: (update: AgentProgressUpdate) => void
}
/**

View File

@@ -1,5 +1,6 @@
import { MAX_ITEMS_PER_CALL, MAX_TOTAL_AGENTS } from '../constants.js'
import type {
AgentProgressUpdate,
AgentRunParams,
AgentRunResult,
JournalEntry,
@@ -29,6 +30,14 @@ type HookProgressInit =
phase?: string
result: AgentRunResult
}
| {
type: 'agent_progress'
agentId: number
label?: string
phase?: string
tokenCount: number
toolCount: number
}
| { type: 'log'; message: string }
export function makeHooks(
@@ -104,11 +113,16 @@ export function makeHooks(
ctx.resources.agentCountBox.value++
emit({ type: 'agent_started', agentId, label, phase })
const registry = ctx.ports.agentAdapterRegistry
// onProgress 闭包:后端循环累计 token/tool → 发 agent_progress 事件(带 agentId 关联)
const onProgress = (update: AgentProgressUpdate): void => {
emit({ type: 'agent_progress', agentId, label, phase, ...update })
}
const result = registry
? await registry.resolve(params).run(params, {
host: ctx.host,
signal: ctx.signal,
runId: ctx.runId,
onProgress,
})
: await ctx.ports.agentRunner.runAgentToResult(params, ctx.host)
if (result.kind === 'ok') {

View File

@@ -21,4 +21,5 @@ export {
type WorkflowToolDescriptor,
} from './tool/WorkflowTool.js'
export { workflowInputSchema, type WorkflowInput } from './tool/schema.js'
export { persistInlineScript } from './tool/persistInline.js'
export { WORKFLOW_TOOL_NAME } from './tool/constants.js'

View File

@@ -9,6 +9,7 @@ import { containsPath, sanitizeWorkflowName } from '../engine/paths.js'
import type { WorkflowPorts } from '../ports.js'
import type { WorkflowRunResult } from '../types.js'
import { workflowInputSchema, type WorkflowInput } from './schema.js'
import { persistInlineScript } from './persistInline.js'
/** 自包含工具描述符(核心 wiring 用 buildTool 包装它)。零核心层依赖。 */
export type WorkflowToolDescriptor = {
@@ -55,6 +56,10 @@ export function createWorkflowTool(
return {
name: WORKFLOW_TOOL_NAME,
inputSchema: workflowInputSchema,
// No per-session runtime opt-in gate here: the "ultracode is on for the
// session" signal is injected by the harness (claude.ai/client), not held
// in any repo state. This tool is compiled in/out via feature('WORKFLOW_SCRIPTS')
// in src/tools.ts; beyond that it is always enabled when present.
isEnabled: () => true,
isReadOnly: () => false,
@@ -109,6 +114,23 @@ export function createWorkflowTool(
host.handle,
)
// inline 入口持久化脚本到 run 目录返回可复用路径ultracode skill 承诺的
// inline → 持久化 → 编辑 → scriptPath 重提迭代循环)。写盘失败降级为占位符
// + warn不阻断 runscript 已在内存)。
if (!workflowFile && input.script) {
try {
workflowFile = await persistInlineScript(
input.script,
runId,
host.cwd,
)
} catch (e) {
ports.logger.warn?.(
`inline script persist failed: ${(e as Error).message}`,
)
}
}
// detached 执行
void runWorkflow({
script,

View File

@@ -0,0 +1,28 @@
import { mkdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { WORKFLOW_RUNS_DIR } from '../constants.js'
/**
* Persist an inline workflow script to the run directory so the caller can
* iterate via `scriptPath` + `resumeFromRunId` without resending the full script
* (the round-trip the ultracode skill promises for the inline entry path).
*
* Mirrors engine/journal.ts: writes directly via node:fs/promises (no port) to
* `<cwd>/<WORKFLOW_RUNS_DIR>/<runId>/script.js` — the same directory as
* journal.jsonl, so journalStore.truncate(runId) cleans it up alongside the journal.
*
* Fixed filename `script.js`: parseScript ignores the extension and the runId
* already makes the directory unique, so a stable name aids muscle memory.
*/
export async function persistInlineScript(
script: string,
runId: string,
cwd: string,
): Promise<string> {
const dir = join(cwd, WORKFLOW_RUNS_DIR, runId)
await mkdir(dir, { recursive: true })
const filePath = join(dir, 'script.js')
await writeFile(filePath, script, 'utf-8')
return filePath
}

View File

@@ -27,9 +27,25 @@ export type AgentRunParams = {
phase?: string
}
/** AgentRunner 返回。 */
/** agent 运行中进度快照onProgress 回调载荷;后端循环累计 token/tool。 */
export type AgentProgressUpdate = {
tokenCount: number
toolCount: number
}
/** AgentRunner 返回。ok 变体携带 model/toolCount 供面板展示(可选,独立后端可不填)。 */
export type AgentRunResult =
| { kind: 'ok'; output: string | object; usage: { outputTokens: number } }
| {
kind: 'ok'
output: string | object
usage: { outputTokens: number }
/** 实际解析后的 model id展示用。 */
model?: string
/** agent 运行期间工具调用次数。 */
toolCount?: number
/** 完成时的 context 总 token 数(展示用;与 agent_progress 实时口径一致)。 */
tokenCount?: number
}
| { kind: 'skipped' }
| { kind: 'dead' }
@@ -66,6 +82,15 @@ export type ProgressEvent =
phase?: string
result: AgentRunResult
}
| {
type: 'agent_progress'
runId: string
agentId: number
label?: string
phase?: string
tokenCount: number
toolCount: number
}
| { type: 'log'; runId: string; message: string }
| {
type: 'run_done'