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

@@ -15,8 +15,16 @@ import {
type BuiltInAgentDefinition,
} from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js'
import { createUserMessage, extractTextContent } from '../../utils/messages.js'
import { getTokenCountFromUsage } from '../../utils/tokens.js'
import { createHash } from 'node:crypto'
import { createAgentId } from '../../utils/uuid.js'
import { logForDebugging } from '../../utils/debug.js'
import { runWithCwdOverride } from '../../utils/cwd.js'
import {
createAgentWorktree,
hasWorktreeChanges,
removeAgentWorktree,
} from '../../utils/worktree.js'
import { logEvent } from '../../services/analytics/index.js'
import type { ModelAlias } from '../../utils/model/aliases.js'
import type { Message } from '../../types/message.js'
@@ -74,6 +82,57 @@ export function extractStructuredOutput(
return null
}
type WorkflowWorktreeInfo = Awaited<ReturnType<typeof createAgentWorktree>>
/**
* 为 workflow agent 的 worktree 隔离生成 slugsha256(runId:agentId) 派生 hex 段,
* 匹配 cleanupStaleAgentWorktrees 的清理正则 `^wf_[0-9a-f]{8}-[0-9a-f]{3}-\d+$`。
* taskId 是 `w`+base36非 UUID不能直接塞 runId 进正则段sha256 是确定性映射,
* agentId 保证同 runId 多 agent 的 slug 唯一(无共享计数器,无线程安全问题)。
*/
function makeWorkflowWorktreeSlug(runId: string, agentId: string): string {
const h = createHash('sha256').update(`${runId}:${agentId}`).digest('hex')
return `wf_${h.slice(0, 8)}-${h.slice(8, 11)}-${parseInt(h.slice(11, 17), 16) % 100000}`
}
/**
* agent 完成后清理 worktreehookBased 保留(无法检测 VCS 变更);否则用
* hasWorktreeChangesfail-closed检测无变更 auto-remove有变更/检测失败保留
* 并 log 路径v1 用日志而非扩 AgentRunResult避免动 journal 序列化)。
*/
async function cleanupWorkflowWorktree(
info: WorkflowWorktreeInfo,
agentType: string,
): Promise<void> {
if (info.hookBased || !info.headCommit) return
let changed = true
try {
changed = await hasWorktreeChanges(info.worktreePath, info.headCommit)
} catch (e) {
logForDebugging(
`workflow worktree change-detect failed (${agentType}): ${(e as Error).message}`,
)
changed = true
}
if (!changed) {
try {
await removeAgentWorktree(
info.worktreePath,
info.worktreeBranch,
info.gitRoot,
)
} catch (e) {
logForDebugging(
`workflow worktree remove failed (${agentType}): ${(e as Error).message}`,
)
}
} else {
logForDebugging(
`workflow worktree retained (has changes, ${agentType}): ${info.worktreePath}`,
)
}
}
/** 深度集成后端:从活会话解析 agent/model/tools委托核心 runAgent。 */
export const claudeCodeBackend: AgentAdapter = {
id: 'claude-code',
@@ -89,6 +148,28 @@ export const claudeCodeBackend: AgentAdapter = {
const model = mapWorkflowModel(params.model)
const agentId = createAgentId()
// isolation:'worktree' — 在独立 git worktree 里跑 agent并发写互不冲突。
let worktreeInfo: WorkflowWorktreeInfo | null = null
if (params.isolation === 'worktree') {
try {
worktreeInfo = await createAgentWorktree(
makeWorkflowWorktreeSlug(ctx.runId, agentId),
)
} catch (e) {
// fail-closed隔离未达成不静默退化为共享 cwd否则并发写数据竞争
logForDebugging(
`workflow worktree creation failed (${agentDef.agentType}): ${(e as Error).message}`,
)
return { kind: 'dead' }
}
}
// runWithCwdOverride 让 agent 内的 Bash/Read 等工具看到 worktree 路径
// AsyncLocalStorage 跨 await 保持runAgent 的 worktreePath 参数仅写 metadata。
const runInCwd = worktreeInfo
? <T>(fn: () => T): T =>
runWithCwdOverride(worktreeInfo!.worktreePath, fn)
: <T>(fn: () => T): T => fn()
const workerPermissionContext = {
...appState.toolPermissionContext,
mode: agentDef.permissionMode ?? 'acceptEdits',
@@ -106,29 +187,54 @@ export const claudeCodeBackend: AgentAdapter = {
const promptMessages = [createUserMessage({ content: promptText })]
const messages: Message[] = []
const startTime = Date.now()
// 运行中进度累计onProgress 推送 → agent_progress 事件 → 面板实时刷新 token/tool
let tokenCount = 0
let toolCount = 0
try {
for await (const msg of runAgent({
agentDefinition: agentDef,
promptMessages,
toolUseContext,
canUseTool,
isAsync: true,
querySource: toolUseContext.options.querySource ?? 'workflow',
availableTools: workerTools,
override: { agentId },
// runAgent 的 model 是顶层 ModelAliasworkflow 的 model 是任意别名串,
// 类型上不兼容,运行时由 provider 层解析。双重断言透传(优于 as any/never
...(model ? { model: model as unknown as ModelAlias } : {}),
})) {
messages.push(msg as Message)
}
await runInCwd(async () => {
for await (const msg of runAgent({
agentDefinition: agentDef,
promptMessages,
toolUseContext,
canUseTool,
isAsync: true,
querySource: toolUseContext.options.querySource ?? 'workflow',
availableTools: workerTools,
override: { agentId },
// runAgent 的 model 是顶层 ModelAliasworkflow 的 model 是任意别名串,
// 类型上不兼容,运行时由 provider 层解析。双重断言透传(优于 as any/never
...(model ? { model: model as unknown as ModelAlias } : {}),
...(worktreeInfo ? { worktreePath: worktreeInfo.worktreePath } : {}),
})) {
messages.push(msg as Message)
// 累计运行中进度assistant message 带 usage累积值→覆盖、content 内 tool_use增量
if (msg.type === 'assistant' && msg.message) {
const usage = msg.message.usage as
| Parameters<typeof getTokenCountFromUsage>[0]
| undefined
if (usage) tokenCount = getTokenCountFromUsage(usage)
const content = msg.message.content as
| Array<{ type: string }>
| undefined
if (content)
toolCount += content.filter(b => b.type === 'tool_use').length
}
ctx.onProgress?.({ tokenCount, toolCount })
}
})
} catch (e) {
logForDebugging(
`workflow sub-agent error (${agentDef.agentType}): ${(e as Error).message}`,
)
logEvent('tengu_workflow_agent', { ok: 0 })
return { kind: 'dead' }
} finally {
if (worktreeInfo) {
const info = worktreeInfo
worktreeInfo = null
await cleanupWorkflowWorktree(info, agentDef.agentType)
}
}
const finalized = finalizeAgentTool(messages, agentId, {
@@ -141,6 +247,10 @@ export const claudeCodeBackend: AgentAdapter = {
})
const outputTokens =
finalized.usage?.output_tokens ?? finalized.totalTokens ?? 0
// 面板展示用:完成时 context 总 token、工具调用次数、解析后 model id。
const finalTokenCount = finalized.totalTokens ?? 0
const finalToolCount = finalized.totalToolUseCount ?? 0
const resolvedModel = model ?? toolUseContext.options.mainLoopModel
logEvent('tengu_workflow_agent', { ok: 1, outputTokens })
if (params.schema) {
@@ -150,9 +260,19 @@ export const claudeCodeBackend: AgentAdapter = {
kind: 'ok',
output: structured as object,
usage: { outputTokens },
model: resolvedModel,
toolCount: finalToolCount,
tokenCount: finalTokenCount,
}
}
const text = extractTextContent(finalized.content, '\n')
return { kind: 'ok', output: text, usage: { outputTokens } }
return {
kind: 'ok',
output: text,
usage: { outputTokens },
model: resolvedModel,
toolCount: finalToolCount,
tokenCount: finalTokenCount,
}
},
}