mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 22:05:50 +00:00
feat: 添加 /goal 命令,支持长时间运行任务的目标管理 (#1222)
* feat: 添加 /goal 命令,支持长时间运行任务的目标管理 从 Codex 项目移植 /goal 命令到 Claude Code,实现: - Goal 状态管理模块(active/paused/budget_limited/complete) - /goal 斜杠命令(set/clear/pause/resume/complete) - Goal 模型工具(get/set/complete) - Continuation prompt 自动注入系统提示 - Token 用量自动追踪 Co-Authored-By: mimo-v2.5-pro <XiaomiMiMo@claude-code-best.win> * fix: goal 状态改为 session-scoped,避免多会话泄漏 将 currentGoal 单例替换为 Map<string, GoalState>,按 sessionId 隔离, 遵循 sessionIngress.ts 的模式。所有函数支持可选 sessionId 参数。 Co-Authored-By: mimo-v2.5-pro <XiaomiMiMo@claude-code-best.win> * fix: 对 goal 的 tokenBudget/tokensUsed 添加数值校验 setGoal 中 tokenBudget 非 finite 或负数时归零; updateGoalTokens 中 usage 非 finite 或负数时归零。 Co-Authored-By: mimo-v2.5-pro <XiaomiMiMo@claude-code-best.win> * fix: 暂停期间 goal 时间不再继续计数 新增 pausedAt/accumulatedActiveMs 字段,pauseGoal 累积已活跃时间, resumeGoal 重置 startTime,计时统一使用 getActiveElapsedMs()。 Co-Authored-By: mimo-v2.5-pro <XiaomiMiMo@claude-code-best.win> --------- Co-authored-by: mimo-v2.5-pro <XiaomiMiMo@claude-code-best.win>
This commit is contained in:
156
src/services/goal/goalState.ts
Normal file
156
src/services/goal/goalState.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { getSessionId } from '../../bootstrap/state.js'
|
||||
|
||||
export type GoalStatus = 'active' | 'paused' | 'budget_limited' | 'complete'
|
||||
|
||||
export type GoalState = {
|
||||
objective: string
|
||||
status: GoalStatus
|
||||
tokenBudget: number | null
|
||||
tokensUsed: number
|
||||
startTime: number
|
||||
pausedAt: number | null
|
||||
accumulatedActiveMs: number
|
||||
}
|
||||
|
||||
const goals: Map<string, GoalState> = new Map()
|
||||
|
||||
export function getGoal(sessionId?: string): GoalState | null {
|
||||
return goals.get(sessionId ?? getSessionId()) ?? null
|
||||
}
|
||||
|
||||
export function setGoal(
|
||||
objective: string,
|
||||
tokenBudget?: number,
|
||||
sessionId?: string,
|
||||
): GoalState {
|
||||
const validBudget =
|
||||
tokenBudget !== undefined &&
|
||||
Number.isFinite(tokenBudget) &&
|
||||
tokenBudget >= 0
|
||||
? tokenBudget
|
||||
: null
|
||||
const state: GoalState = {
|
||||
objective,
|
||||
status: 'active',
|
||||
tokenBudget: validBudget,
|
||||
tokensUsed: 0,
|
||||
startTime: Date.now(),
|
||||
pausedAt: null,
|
||||
accumulatedActiveMs: 0,
|
||||
}
|
||||
goals.set(sessionId ?? getSessionId(), state)
|
||||
return state
|
||||
}
|
||||
|
||||
export function clearGoal(sessionId?: string): void {
|
||||
goals.delete(sessionId ?? getSessionId())
|
||||
}
|
||||
|
||||
export function pauseGoal(sessionId?: string): boolean {
|
||||
const goal = getGoal(sessionId)
|
||||
if (!goal || goal.status !== 'active') return false
|
||||
goal.accumulatedActiveMs += Date.now() - goal.startTime
|
||||
goal.pausedAt = Date.now()
|
||||
goal.status = 'paused'
|
||||
return true
|
||||
}
|
||||
|
||||
export function resumeGoal(sessionId?: string): boolean {
|
||||
const goal = getGoal(sessionId)
|
||||
if (!goal || goal.status !== 'paused') return false
|
||||
goal.pausedAt = null
|
||||
goal.startTime = Date.now()
|
||||
goal.status = 'active'
|
||||
return true
|
||||
}
|
||||
|
||||
export function completeGoal(sessionId?: string): boolean {
|
||||
const goal = getGoal(sessionId)
|
||||
if (!goal) return false
|
||||
goal.status = 'complete'
|
||||
return true
|
||||
}
|
||||
|
||||
export function updateGoalTokens(usage: number, sessionId?: string): void {
|
||||
const goal = getGoal(sessionId)
|
||||
if (!goal || goal.status !== 'active') return
|
||||
const validUsage = Number.isFinite(usage) && usage >= 0 ? usage : 0
|
||||
goal.tokensUsed += validUsage
|
||||
if (goal.tokenBudget !== null && goal.tokensUsed >= goal.tokenBudget) {
|
||||
goal.status = 'budget_limited'
|
||||
}
|
||||
}
|
||||
|
||||
export function getActiveElapsedMs(goal: GoalState): number {
|
||||
const ongoing =
|
||||
goal.status === 'active' && goal.pausedAt === null
|
||||
? Date.now() - goal.startTime
|
||||
: 0
|
||||
return goal.accumulatedActiveMs + ongoing
|
||||
}
|
||||
|
||||
export function getGoalContinuationPrompt(sessionId?: string): string | null {
|
||||
const goal = getGoal(sessionId)
|
||||
if (!goal || goal.status !== 'active') return null
|
||||
|
||||
const elapsedSeconds = Math.floor(getActiveElapsedMs(goal) / 1000)
|
||||
const budgetDisplay =
|
||||
goal.tokenBudget !== null ? `${goal.tokenBudget}` : 'unlimited'
|
||||
const remainingDisplay =
|
||||
goal.tokenBudget !== null
|
||||
? `${Math.max(0, goal.tokenBudget - goal.tokensUsed)}`
|
||||
: 'unlimited'
|
||||
|
||||
return `Continue working toward the active goal.
|
||||
|
||||
<objective>
|
||||
${goal.objective}
|
||||
</objective>
|
||||
|
||||
Budget:
|
||||
- Time spent: ${elapsedSeconds} seconds
|
||||
- Tokens used: ${goal.tokensUsed}
|
||||
- Token budget: ${budgetDisplay}
|
||||
- Tokens remaining: ${remainingDisplay}
|
||||
|
||||
Avoid repeating work that is already done. Choose the next concrete action toward the objective.
|
||||
|
||||
Before deciding that the goal is achieved, perform a completion audit:
|
||||
- Restate the objective as concrete deliverables or success criteria.
|
||||
- Inspect relevant files, command output, test results, or other real evidence.
|
||||
- Do not accept proxy signals as completion by themselves.
|
||||
- Treat uncertainty as not achieved; do more verification or continue the work.
|
||||
- Only mark the goal achieved when the objective has actually been achieved and no required work remains.
|
||||
|
||||
If the objective is achieved, call the goal tool with action "complete" so usage accounting is preserved.`
|
||||
}
|
||||
|
||||
export function formatGoalStatus(sessionId?: string): string {
|
||||
const goal = getGoal(sessionId)
|
||||
if (!goal) return 'No active goal.'
|
||||
|
||||
const elapsed = Math.floor(getActiveElapsedMs(goal) / 1000)
|
||||
const minutes = Math.floor(elapsed / 60)
|
||||
const seconds = elapsed % 60
|
||||
const timeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`
|
||||
|
||||
const statusLabel: Record<GoalStatus, string> = {
|
||||
active: 'Active',
|
||||
paused: 'Paused',
|
||||
budget_limited: 'Budget Limited',
|
||||
complete: 'Complete',
|
||||
}
|
||||
|
||||
const lines = [
|
||||
`Goal: ${goal.objective}`,
|
||||
`Status: ${statusLabel[goal.status]}`,
|
||||
`Time: ${timeStr}`,
|
||||
`Tokens: ${goal.tokensUsed}${goal.tokenBudget !== null ? ` / ${goal.tokenBudget}` : ''}`,
|
||||
]
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
export function clearAllGoals(): void {
|
||||
goals.clear()
|
||||
}
|
||||
Reference in New Issue
Block a user