bingocode 1.1.183 → 1.1.185
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.
- package/.claude/scheduled_tasks.json +34 -0
- package/package.json +1 -1
- package/src/bootstrap/state.ts +3 -53
- package/src/hooks/useGoalAutoUpdater.ts +187 -0
- package/src/hooks/useGoalEvaluator.ts +71 -37
- package/src/query.ts +21 -0
- package/src/skills/bundled/goal.ts +41 -32
- package/src/types/goal.ts +322 -0
- package/src/utils/__tests__/goalDag.test.ts +177 -0
- package/src/utils/__tests__/goalDrift.test.ts +132 -0
- package/src/utils/__tests__/goalIntegration.test.ts +217 -0
- package/src/utils/__tests__/goalStore.test.ts +592 -0
- package/src/utils/goalDag.ts +408 -0
- package/src/utils/goalDrift.ts +243 -0
- package/src/utils/goalEvaluator.ts +708 -122
- package/src/utils/goalHelpers.ts +155 -0
- package/src/utils/goalPersistence.ts +206 -0
- package/src/utils/goalStore.ts +463 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tasks": [
|
|
3
|
+
{
|
|
4
|
+
"id": "274f11cf",
|
|
5
|
+
"cron": "0 9 * * *",
|
|
6
|
+
"prompt": "test prompt",
|
|
7
|
+
"createdAt": 1782997215925,
|
|
8
|
+
"recurring": true,
|
|
9
|
+
"name": "test-name",
|
|
10
|
+
"description": "test description",
|
|
11
|
+
"folder": "/test/folder",
|
|
12
|
+
"model": "claude-opus-4-7",
|
|
13
|
+
"permissionMode": "ask",
|
|
14
|
+
"worktree": false,
|
|
15
|
+
"frequency": "daily",
|
|
16
|
+
"scheduledTime": "09:00"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "1c4152a1",
|
|
20
|
+
"cron": "0 9 * * *",
|
|
21
|
+
"prompt": "test prompt",
|
|
22
|
+
"createdAt": 1782998699654,
|
|
23
|
+
"recurring": true,
|
|
24
|
+
"name": "test-name",
|
|
25
|
+
"description": "test description",
|
|
26
|
+
"folder": "/test/folder",
|
|
27
|
+
"model": "claude-opus-4-7",
|
|
28
|
+
"permissionMode": "ask",
|
|
29
|
+
"worktree": false,
|
|
30
|
+
"frequency": "daily",
|
|
31
|
+
"scheduledTime": "09:00"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
package/package.json
CHANGED
package/src/bootstrap/state.ts
CHANGED
|
@@ -135,16 +135,7 @@ type State = {
|
|
|
135
135
|
// (useScheduledTasks). Set by cronScheduler.start() when the JSON has
|
|
136
136
|
// entries, or by CronCreateTool. Not persisted.
|
|
137
137
|
scheduledTasksEnabled: boolean
|
|
138
|
-
|
|
139
|
-
goalCondition: string | null
|
|
140
|
-
goalIterationCount: number
|
|
141
|
-
goalMaxIterations: number
|
|
142
|
-
// Goal evaluator history for detecting repeated gaps
|
|
143
|
-
goalEvalHistory: {
|
|
144
|
-
lastGap: string | null
|
|
145
|
-
consecutiveSameGapCount: number
|
|
146
|
-
}
|
|
147
|
-
// Session-only cron tasks created via CronCreate with durable: false.
|
|
138
|
+
// Session-only cron tasks created via CronCreate with durable: false.
|
|
148
139
|
// Fire on schedule like file-backed tasks but are never written to
|
|
149
140
|
// .claude/scheduled_tasks.json — they die with the process. Typed via
|
|
150
141
|
// SessionCronTask below (not importing from cronTasks.ts keeps
|
|
@@ -366,15 +357,7 @@ function getInitialState(): State {
|
|
|
366
357
|
sessionBypassPermissionsMode: false,
|
|
367
358
|
// Scheduled tasks disabled until flag or dialog enables them
|
|
368
359
|
scheduledTasksEnabled: false,
|
|
369
|
-
|
|
370
|
-
goalCondition: null,
|
|
371
|
-
goalIterationCount: 0,
|
|
372
|
-
goalMaxIterations: 20,
|
|
373
|
-
goalEvalHistory: {
|
|
374
|
-
lastGap: null,
|
|
375
|
-
consecutiveSameGapCount: 0,
|
|
376
|
-
},
|
|
377
|
-
sessionCronTasks: [],
|
|
360
|
+
sessionCronTasks: [],
|
|
378
361
|
sessionCreatedTeams: new Set(),
|
|
379
362
|
// Session-only trust flag (not persisted to disk)
|
|
380
363
|
sessionTrustAccepted: false,
|
|
@@ -1793,38 +1776,5 @@ export function setPromptId(id: string | null): void {
|
|
|
1793
1776
|
// /goal session state accessors
|
|
1794
1777
|
// ============================================================================
|
|
1795
1778
|
|
|
1796
|
-
|
|
1797
|
-
return STATE.goalCondition
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
export function setGoalCondition(condition: string | null): void {
|
|
1801
|
-
STATE.goalCondition = condition
|
|
1802
|
-
STATE.goalIterationCount = 0
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
|
-
export function getGoalIterationCount(): number {
|
|
1806
|
-
return STATE.goalIterationCount
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
export function incrementGoalIterationCount(): void {
|
|
1810
|
-
STATE.goalIterationCount++
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
export function getGoalMaxIterations(): number {
|
|
1814
|
-
return STATE.goalMaxIterations
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
// Goal evaluator history accessors
|
|
1818
|
-
export function getGoalEvalHistory() {
|
|
1819
|
-
return STATE.goalEvalHistory
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
export function updateGoalEvalHistory(lastGap: string | null): void {
|
|
1823
|
-
if (lastGap === STATE.goalEvalHistory.lastGap) {
|
|
1824
|
-
STATE.goalEvalHistory.consecutiveSameGapCount++
|
|
1825
|
-
} else {
|
|
1826
|
-
STATE.goalEvalHistory.lastGap = lastGap
|
|
1827
|
-
STATE.goalEvalHistory.consecutiveSameGapCount = 1
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1779
|
+
// ============================================================================
|
|
1830
1780
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal Auto-Updater Hook — bridges agent tool execution with goal state.
|
|
3
|
+
*
|
|
4
|
+
* Monitors PostToolUse events and automatically updates goal state in real-time.
|
|
5
|
+
* This replaces the manual "output EVAL blocks" approach with automatic state
|
|
6
|
+
* tracking based on what the agent actually does.
|
|
7
|
+
*
|
|
8
|
+
* Integration point: called in REPL.tsx after each tool execution completes.
|
|
9
|
+
* Uses the GoalStore to update state, which triggers re-renders in any
|
|
10
|
+
* subscribed components via the signal pattern.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ToolResult, SubGoalStatus } from '../types/goal.js'
|
|
14
|
+
import { getGoalState, addArtifact, updateSubGoalStatus, recalculateProgress } from '../utils/goalStore.js'
|
|
15
|
+
import { asSubGoalId } from '../utils/goalHelpers.js'
|
|
16
|
+
import { assessDrift } from '../utils/goalDrift.js'
|
|
17
|
+
import type { DriftAssessment, SubGoal } from '../types/goal.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Classify a tool execution result into a goal-relevant action category.
|
|
21
|
+
* Returns null if the tool is not relevant to goal progress tracking.
|
|
22
|
+
*/
|
|
23
|
+
function classifyToolAction(
|
|
24
|
+
toolName: string,
|
|
25
|
+
toolInput: Record<string, unknown>,
|
|
26
|
+
toolOutput: string,
|
|
27
|
+
success: boolean,
|
|
28
|
+
): ToolAction | null {
|
|
29
|
+
// Read-only tools (query) → informational, not state-changing
|
|
30
|
+
if (isReadTool(toolName)) return null
|
|
31
|
+
|
|
32
|
+
// Write tools (create/modify)
|
|
33
|
+
if (isCreateTool(toolName)) {
|
|
34
|
+
const filePath = extractFilePath(toolInput)
|
|
35
|
+
return {
|
|
36
|
+
type: 'create',
|
|
37
|
+
filePath,
|
|
38
|
+
subGoalId: inferSubGoal(toolName, filePath),
|
|
39
|
+
description: `Created ${filePath}`,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Build tools (verify)
|
|
44
|
+
if (isVerifyTool(toolName)) {
|
|
45
|
+
return {
|
|
46
|
+
type: 'verify',
|
|
47
|
+
filePath: extractFilePath(toolInput),
|
|
48
|
+
subGoalId: inferSubGoal(toolName, extractFilePath(toolInput)),
|
|
49
|
+
description: `Verified with ${toolName}`,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Default: generic mutation
|
|
54
|
+
return {
|
|
55
|
+
type: 'modify',
|
|
56
|
+
filePath: extractFilePath(toolInput),
|
|
57
|
+
subGoalId: inferSubGoal(toolName, extractFilePath(toolInput)),
|
|
58
|
+
description: `Modified via ${toolName}`,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type ToolAction = {
|
|
63
|
+
type: 'create' | 'modify' | 'verify'
|
|
64
|
+
filePath: string
|
|
65
|
+
subGoalId: string
|
|
66
|
+
description: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Update goal state after a tool execution. Called from REPL.tsx after
|
|
70
|
+
* each PostToolUse event. Also performs drift assessment to detect
|
|
71
|
+
* when agent behavior starts deviating from target goals. */
|
|
72
|
+
export function useGoalAutoUpdater(
|
|
73
|
+
toolResult: ToolResult,
|
|
74
|
+
): void {
|
|
75
|
+
const state = getGoalState()
|
|
76
|
+
if (!state.activeGoalId) return
|
|
77
|
+
if (!state.operationalGoal) return
|
|
78
|
+
|
|
79
|
+
const action = classifyToolAction(
|
|
80
|
+
toolResult.toolName,
|
|
81
|
+
toolResult.toolInput,
|
|
82
|
+
toolResult.toolOutput,
|
|
83
|
+
toolResult.success,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if (!action) return
|
|
87
|
+
|
|
88
|
+
// Phase 1: Update goal state based on action type
|
|
89
|
+
switch (action.type) {
|
|
90
|
+
case 'create':
|
|
91
|
+
addArtifact(action.filePath)
|
|
92
|
+
break
|
|
93
|
+
case 'modify':
|
|
94
|
+
addArtifact(action.filePath)
|
|
95
|
+
break
|
|
96
|
+
case 'verify':
|
|
97
|
+
updateSubGoalStatus(action.subGoalId, 'active')
|
|
98
|
+
break
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Phase 2: Recalculate progress after state mutation
|
|
102
|
+
recalculateProgress()
|
|
103
|
+
|
|
104
|
+
// Phase 3: Assess drift (after state update, before next turn)
|
|
105
|
+
const currentSubGoal = state.dag.nodes.get(action.subGoalId)
|
|
106
|
+
if (currentSubGoal) {
|
|
107
|
+
const drift = assessDrift(
|
|
108
|
+
action.description ?? `${toolResult.toolName}: ${toolResult.toolOutput.slice(0, 100)}`,
|
|
109
|
+
currentSubGoal,
|
|
110
|
+
state.operationalGoal.text,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
// Log drift warnings for debugging — future: escalate to enqueue()
|
|
114
|
+
if (drift.warnings.length > 0) {
|
|
115
|
+
const highWarnings = drift.warnings.filter(w => w.severity === 'high')
|
|
116
|
+
if (highWarnings.length > 0) {
|
|
117
|
+
// Drift is severe — agent should replan
|
|
118
|
+
// TODO: wire into enqueue() for user-visible warning
|
|
119
|
+
console.warn(`[GoalDrift] ${drift.warnings.length} warning(s): score ${drift.consistencyScore}%, trend ${drift.trend}`)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// Helpers
|
|
127
|
+
// ============================================================================
|
|
128
|
+
|
|
129
|
+
/** Check if a tool is read-only (doesn't modify state). */
|
|
130
|
+
function isReadTool(toolName: string): boolean {
|
|
131
|
+
const readTools = new Set([
|
|
132
|
+
'Read',
|
|
133
|
+
'Glob',
|
|
134
|
+
'Grep',
|
|
135
|
+
'LS',
|
|
136
|
+
'WebSearch',
|
|
137
|
+
'WebFetch',
|
|
138
|
+
'TaskOutput',
|
|
139
|
+
'TaskStatus',
|
|
140
|
+
])
|
|
141
|
+
return readTools.has(toolName)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Check if a tool creates new files. */
|
|
145
|
+
function isCreateTool(toolName: string): boolean {
|
|
146
|
+
const createTools = new Set([
|
|
147
|
+
'Write',
|
|
148
|
+
'Edit', // can create or modify, but classified as create for new files
|
|
149
|
+
'NotebookEdit',
|
|
150
|
+
])
|
|
151
|
+
return createTools.has(toolName)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Check if a tool verifies existing work (tests, builds). */
|
|
155
|
+
function isVerifyTool(toolName: string): boolean {
|
|
156
|
+
const verifyTools = new Set([
|
|
157
|
+
'Bash', // often used for test/build commands
|
|
158
|
+
])
|
|
159
|
+
return verifyTools.has(toolName)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Extract file path from tool input. Handles different tool input shapes. */
|
|
163
|
+
function extractFilePath(input: Record<string, unknown>): string {
|
|
164
|
+
if (typeof input.file_path === 'string') return input.file_path
|
|
165
|
+
if (typeof input.path === 'string') return input.path
|
|
166
|
+
if (typeof input.notebook_path === 'string') return input.notebook_path
|
|
167
|
+
return ''
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Infer which sub-goal this tool action likely belongs to.
|
|
171
|
+
* Uses file path heuristics to match against known sub-goals. */
|
|
172
|
+
function inferSubGoal(toolName: string, filePath: string): string {
|
|
173
|
+
// For now, use a simple heuristic — match by file path pattern
|
|
174
|
+
// This will be enhanced in Phase 6 with actual DAG integration
|
|
175
|
+
if (!filePath) return 'unknown'
|
|
176
|
+
|
|
177
|
+
// Strip the path and use the filename as a sub-goal identifier
|
|
178
|
+
// In practice, the real mapping will come from the DAG's SubGoal nodes
|
|
179
|
+
const fileName = filePath.split('/').pop()?.split('\\').pop() ?? filePath
|
|
180
|
+
return `task-${fileName}`
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// Re-export for convenience
|
|
185
|
+
// ============================================================================
|
|
186
|
+
|
|
187
|
+
export { getGoalState as useGoalStore }
|
|
@@ -1,15 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal evaluator hook — post-turn goal assessment.
|
|
3
|
+
*
|
|
4
|
+
* Fires after each turn completes (triggered by lastQueryCompletionTime change).
|
|
5
|
+
* Evaluates progress toward active goal using env-aware evaluator v2.
|
|
6
|
+
*
|
|
7
|
+
* Migrated from old state.ts flat fields to the new GoalStore API.
|
|
8
|
+
* All state access goes through GoalStore — no more STATE.goalCondition fallbacks.
|
|
9
|
+
*/
|
|
10
|
+
|
|
1
11
|
import { useEffect, useRef } from 'react'
|
|
2
12
|
import type { MutableRefObject } from 'react'
|
|
3
13
|
import type { MessageType } from '../components/messages.js'
|
|
4
14
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} from '../
|
|
15
|
+
getGoalState,
|
|
16
|
+
recalculateProgress,
|
|
17
|
+
incrementIteration,
|
|
18
|
+
recordEvalGap,
|
|
19
|
+
getActiveGoalId,
|
|
20
|
+
getReadySubGoals,
|
|
21
|
+
getBlockedSubGoals,
|
|
22
|
+
} from '../utils/goalStore.js'
|
|
13
23
|
import { enqueue } from '../utils/messageQueueManager.js'
|
|
14
24
|
import { evaluateGoal } from '../utils/goalEvaluator.js'
|
|
15
25
|
|
|
@@ -20,16 +30,15 @@ type UseGoalEvaluatorParams = {
|
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
/**
|
|
23
|
-
* React hook
|
|
33
|
+
* React hook — fires after each turn. Triggered by lastQueryCompletionTime.
|
|
34
|
+
* Uses messagesRef (sync ref, not React state) to avoid batching issues.
|
|
24
35
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* On
|
|
31
|
-
* so useQueueProcessor picks it up immediately for the next turn.
|
|
32
|
-
* On goal satisfied or max iterations reached: clears the goal condition.
|
|
36
|
+
* Flow:
|
|
37
|
+
* 1. Check max iterations reached → if so, stop
|
|
38
|
+
* 2. Call env-aware evaluator v2 (rule engine + Haiku + main model)
|
|
39
|
+
* 3. On satisfied → clear goal, enqueue success message
|
|
40
|
+
* 4. On repeated gap (3x) → circuit breaker, clear goal
|
|
41
|
+
* 5. On not satisfied → enqueue continuation message
|
|
33
42
|
*/
|
|
34
43
|
export function useGoalEvaluator({
|
|
35
44
|
lastQueryCompletionTime,
|
|
@@ -40,7 +49,8 @@ export function useGoalEvaluator({
|
|
|
40
49
|
const evaluating = useRef(false)
|
|
41
50
|
|
|
42
51
|
useEffect(() => {
|
|
43
|
-
const
|
|
52
|
+
const goalState = getGoalState()
|
|
53
|
+
const condition = goalState.userGoal?.text ?? null
|
|
44
54
|
if (!condition) return
|
|
45
55
|
if (isQueryActive) return
|
|
46
56
|
if (lastQueryCompletionTime === 0) return
|
|
@@ -52,49 +62,71 @@ export function useGoalEvaluator({
|
|
|
52
62
|
|
|
53
63
|
void (async () => {
|
|
54
64
|
try {
|
|
55
|
-
const iterCount =
|
|
56
|
-
const maxIter =
|
|
65
|
+
const iterCount = goalState.metrics.iterationCount
|
|
66
|
+
const maxIter = goalState.metrics.maxIterations
|
|
57
67
|
|
|
68
|
+
// Max iterations check — circuit breaker
|
|
58
69
|
if (iterCount >= maxIter) {
|
|
59
|
-
|
|
70
|
+
// Clear goal from store (delegates to clear() which resets engine too)
|
|
71
|
+
const { clear } = require('../utils/goalStore.js')
|
|
72
|
+
clear()
|
|
60
73
|
enqueue({
|
|
61
|
-
value:
|
|
74
|
+
value: `Goal not achieved after ${maxIter} iterations. Goal was: "${condition}"`,
|
|
62
75
|
mode: 'task-notification',
|
|
63
76
|
priority: 'now',
|
|
64
77
|
})
|
|
65
78
|
return
|
|
66
79
|
}
|
|
67
80
|
|
|
68
|
-
//
|
|
81
|
+
// Get messages + current sub-goal for evaluation context
|
|
69
82
|
const messages = messagesRef.current
|
|
70
|
-
const
|
|
83
|
+
const currentSubGoal = goalState.immediateGoal
|
|
84
|
+
? goalState.dag.nodes.get(goalState.immediateGoal.subGoalId)
|
|
85
|
+
: undefined
|
|
86
|
+
|
|
87
|
+
// Run environment-aware evaluation (rule engine + semantic + final)
|
|
88
|
+
const result = await evaluateGoal(condition, messages, currentSubGoal)
|
|
71
89
|
|
|
72
|
-
// Race
|
|
73
|
-
if (
|
|
90
|
+
// Race: user may have cleared goal during async evaluation
|
|
91
|
+
if (!getActiveGoalId()) return
|
|
74
92
|
|
|
75
|
-
|
|
76
|
-
|
|
93
|
+
incrementIteration()
|
|
94
|
+
recordEvalGap(result.gap)
|
|
95
|
+
recalculateProgress()
|
|
96
|
+
|
|
97
|
+
// Check active sub-goals and blocked count for drift diagnosis
|
|
98
|
+
const ready = getReadySubGoals()
|
|
99
|
+
const blocked = getBlockedSubGoals()
|
|
100
|
+
if (blocked.length > 0 && ready.length === 0) {
|
|
101
|
+
// All sub-goals blocked — need to replan or escalate
|
|
102
|
+
void ready
|
|
103
|
+
void blocked
|
|
104
|
+
}
|
|
77
105
|
|
|
78
|
-
//
|
|
79
|
-
const evalHistory =
|
|
106
|
+
// Circuit breaker: 3 consecutive same gap
|
|
107
|
+
const evalHistory = goalState.metrics.evalHistory
|
|
80
108
|
const isRepeatedGap = evalHistory.consecutiveSameGapCount >= 3 && result.gap !== null
|
|
81
109
|
|
|
82
110
|
if (result.satisfied) {
|
|
83
|
-
|
|
111
|
+
// Goal achieved — clear from store and file system
|
|
112
|
+
const { clearAndDelete } = require('../utils/goalStore.js')
|
|
113
|
+
clearAndDelete()
|
|
84
114
|
enqueue({
|
|
85
|
-
value:
|
|
115
|
+
value: `Goal achieved (iteration ${iterCount + 1}): ${result.reason}`,
|
|
86
116
|
mode: 'task-notification',
|
|
87
117
|
priority: 'now',
|
|
88
118
|
})
|
|
89
119
|
} else if (isRepeatedGap) {
|
|
90
|
-
// Circuit breaker
|
|
91
|
-
|
|
120
|
+
// Circuit breaker — same gap repeated 3 times, stop looping
|
|
121
|
+
const { clear } = require('../utils/goalStore.js')
|
|
122
|
+
clear()
|
|
92
123
|
enqueue({
|
|
93
|
-
value:
|
|
124
|
+
value: `Goal evaluator stopped — same gap "${result.gap}" ${evalHistory.consecutiveSameGapCount}x. Adjust approach or output EVAL blocks.`,
|
|
94
125
|
mode: 'task-notification',
|
|
95
126
|
priority: 'now',
|
|
96
127
|
})
|
|
97
128
|
} else {
|
|
129
|
+
// Not yet satisfied — continue
|
|
98
130
|
const continueMsg = result.gap
|
|
99
131
|
? `Goal not yet met (${iterCount + 1}/${maxIter}). Gap: ${result.gap}. Continue toward: "${condition}"`
|
|
100
132
|
: `Goal not yet met (${iterCount + 1}/${maxIter}, reason: ${result.reason}). Continue toward: "${condition}"`
|
|
@@ -104,10 +136,12 @@ export function useGoalEvaluator({
|
|
|
104
136
|
priority: 'now',
|
|
105
137
|
})
|
|
106
138
|
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
// Evaluation error is non-fatal — log and continue
|
|
141
|
+
console.error('[GoalEvaluator] Evaluation error:', err)
|
|
107
142
|
} finally {
|
|
108
143
|
evaluating.current = false
|
|
109
144
|
}
|
|
110
145
|
})()
|
|
111
|
-
|
|
112
|
-
}, [lastQueryCompletionTime, isQueryActive])
|
|
146
|
+
}, [lastQueryCompletionTime, isQueryActive]) // messagesRef is stable ref — not included as dep
|
|
113
147
|
}
|
package/src/query.ts
CHANGED
|
@@ -110,6 +110,8 @@ import {
|
|
|
110
110
|
} from './bootstrap/state.js'
|
|
111
111
|
import { createBudgetTracker, checkTokenBudget } from './query/tokenBudget.js'
|
|
112
112
|
import { count } from './utils/array.js'
|
|
113
|
+
import { convertSDKToolResults } from './utils/goalHelpers.js'
|
|
114
|
+
import { useGoalAutoUpdater } from './hooks/useGoalAutoUpdater.js'
|
|
113
115
|
|
|
114
116
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
115
117
|
const snipModule = feature('HISTORY_SNIP')
|
|
@@ -1408,6 +1410,25 @@ async function* queryLoop(
|
|
|
1408
1410
|
}
|
|
1409
1411
|
queryCheckpoint('query_tool_execution_end')
|
|
1410
1412
|
|
|
1413
|
+
// === Goal system auto-update: PostToolUse hook ===
|
|
1414
|
+
// Update goal state based on actual tool execution results.
|
|
1415
|
+
// This replaces the old "output EVAL blocks" approach — now we track
|
|
1416
|
+
// progress automatically from what the agent actually does.
|
|
1417
|
+
if (toolUseBlocks.length > 0) {
|
|
1418
|
+
try {
|
|
1419
|
+
const goalResults = convertSDKToolResults(
|
|
1420
|
+
toolUseBlocks as ReadonlyArray<{ id: string; name: string; input: Record<string, unknown> }>,
|
|
1421
|
+
toolResults as ReadonlyArray<{ type: string; message: { content: { type: string; text?: string; tool_use_id?: string; content?: string }[] } }>,
|
|
1422
|
+
)
|
|
1423
|
+
for (const result of goalResults) {
|
|
1424
|
+
useGoalAutoUpdater(result)
|
|
1425
|
+
}
|
|
1426
|
+
} catch (err) {
|
|
1427
|
+
// Goal auto-update is non-critical — don't block the main loop
|
|
1428
|
+
logError('goal_auto_update_error', err instanceof Error ? err.message : String(err))
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1411
1432
|
// Generate tool use summary after tool batch completes — passed to next recursive call
|
|
1412
1433
|
let nextPendingToolUseSummary:
|
|
1413
1434
|
| Promise<ToolUseSummaryMessage | null>
|
|
@@ -1,27 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Goal command — CLI entry point for Goal Management system.
|
|
3
|
+
*
|
|
4
|
+
* 4-layer architecture: User Goal → Operational Goal → Sub Goals → Immediate Goal.
|
|
5
|
+
* Orchestrates creation, status queries, and management commands.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getGoalState, setUserGoal, setOperationalGoal, clearState } from '../../utils/goalStore.js'
|
|
6
9
|
import { registerBundledSkill } from '../bundledSkills.js'
|
|
7
10
|
|
|
8
|
-
const USAGE = `Usage: /goal <
|
|
11
|
+
const USAGE = `Usage: /goal <create | clear | cancel>
|
|
9
12
|
|
|
10
|
-
Set a session goal.
|
|
13
|
+
Set a session goal. Agent will work autonomously until goal met.
|
|
11
14
|
|
|
12
15
|
Examples:
|
|
13
|
-
/goal all tests pass
|
|
14
|
-
/goal login flow handles empty email without crash
|
|
15
|
-
/goal PR
|
|
16
|
+
/goal create all tests pass
|
|
17
|
+
/goal create login flow handles empty email without crash
|
|
18
|
+
/goal create PR ready for review with passing CI
|
|
16
19
|
|
|
17
|
-
To cancel: /goal clear`
|
|
20
|
+
To cancel: /goal clear or /goal cancel`
|
|
18
21
|
|
|
19
22
|
export function registerGoalSkill(): void {
|
|
20
23
|
registerBundledSkill({
|
|
21
24
|
name: 'goal',
|
|
22
25
|
description:
|
|
23
|
-
'
|
|
24
|
-
argumentHint: '<
|
|
26
|
+
'Manage task goals with hierarchical decomposition and progress tracking.',
|
|
27
|
+
argumentHint: '<create | clear | cancel>',
|
|
25
28
|
userInvocable: true,
|
|
26
29
|
async getPromptForCommand(args) {
|
|
27
30
|
const trimmed = args.trim()
|
|
@@ -30,48 +33,54 @@ export function registerGoalSkill(): void {
|
|
|
30
33
|
return [{ type: 'text', text: USAGE }]
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
// --- Cancel / Clear ---
|
|
37
|
+
if (['clear', 'stop', 'cancel'].includes(trimmed.toLowerCase())) {
|
|
38
|
+
const state = getGoalState()
|
|
39
|
+
if (state.activeGoalId) {
|
|
40
|
+
const userText = state.userGoal?.text ?? '(unknown)'
|
|
41
|
+
clearState()
|
|
37
42
|
return [
|
|
38
43
|
{
|
|
39
44
|
type: 'text',
|
|
40
|
-
text: `Goal cancelled: "${
|
|
45
|
+
text: `Goal cancelled: "${userText}". Goal state cleared from memory and disk.`,
|
|
41
46
|
},
|
|
42
47
|
]
|
|
43
48
|
}
|
|
44
49
|
return [
|
|
45
50
|
{
|
|
46
51
|
type: 'text',
|
|
47
|
-
text: 'No active goal
|
|
52
|
+
text: 'No active goal. Use `/goal create <text>` to start.',
|
|
48
53
|
},
|
|
49
54
|
]
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
// --- Create ---
|
|
58
|
+
setUserGoal(trimmed)
|
|
59
|
+
setOperationalGoal(trimmed)
|
|
60
|
+
const state = getGoalState()
|
|
61
|
+
const maxIter = state.metrics.maxIterations
|
|
54
62
|
|
|
55
63
|
return [
|
|
56
64
|
{
|
|
57
65
|
type: 'text',
|
|
58
66
|
text: `# /goal activated
|
|
59
67
|
|
|
60
|
-
Goal
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
**Goal**: "${trimmed}"
|
|
69
|
+
**Architecture**: User Goal → Operational Goal → DAG Sub Goals → Immediate Action
|
|
70
|
+
**Max iterations**: ${maxIter}
|
|
63
71
|
|
|
64
|
-
|
|
72
|
+
The evaluator now uses **environment-aware assessment** (3 tiers):
|
|
73
|
+
Level 1: Rule engine — checks file existence, test output, compile status
|
|
74
|
+
Level 2: Haiku 4.5 — semantic interpretation of evidence
|
|
75
|
+
Level 3: Main model — final verification (ambiguous cases only)
|
|
65
76
|
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
EVAL blocks still welcome for agent self-reporting, but no longer required.
|
|
78
|
+
Format: \`EVAL: metric: value / target → ✓ or ✗\`
|
|
68
79
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
Note: The EVAL block can appear anywhere in your text response (not just in quote blocks).
|
|
80
|
+
Tell user: Goal set. Agent works autonomously until "${trimmed}" achieved (max ${maxIter} turns).
|
|
81
|
+
Send \`/goal clear\` to cancel.
|
|
72
82
|
|
|
73
|
-
|
|
74
|
-
Now begin: assess current state and take the first concrete action toward the goal.`,
|
|
83
|
+
Now: assess current state, take first concrete action toward goal.`,
|
|
75
84
|
},
|
|
76
85
|
]
|
|
77
86
|
},
|