bingocode 1.1.100 → 1.1.101

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bingocode",
3
- "version": "1.1.100",
3
+ "version": "1.1.101",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude": "bin/claude-win.cjs",
@@ -135,6 +135,10 @@ 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
+ // Session-only goal condition for /goal command. Null when no active goal.
139
+ goalCondition: string | null
140
+ goalIterationCount: number
141
+ goalMaxIterations: number
138
142
  // Session-only cron tasks created via CronCreate with durable: false.
139
143
  // Fire on schedule like file-backed tasks but are never written to
140
144
  // .claude/scheduled_tasks.json — they die with the process. Typed via
@@ -357,6 +361,10 @@ function getInitialState(): State {
357
361
  sessionBypassPermissionsMode: false,
358
362
  // Scheduled tasks disabled until flag or dialog enables them
359
363
  scheduledTasksEnabled: false,
364
+ // Goal condition for /goal command (null = no active goal)
365
+ goalCondition: null,
366
+ goalIterationCount: 0,
367
+ goalMaxIterations: 20,
360
368
  sessionCronTasks: [],
361
369
  sessionCreatedTeams: new Set(),
362
370
  // Session-only trust flag (not persisted to disk)
@@ -1772,3 +1780,28 @@ export function setPromptId(id: string | null): void {
1772
1780
  STATE.promptId = id
1773
1781
  }
1774
1782
 
1783
+ // ============================================================================
1784
+ // /goal session state accessors
1785
+ // ============================================================================
1786
+
1787
+ export function getGoalCondition(): string | null {
1788
+ return STATE.goalCondition
1789
+ }
1790
+
1791
+ export function setGoalCondition(condition: string | null): void {
1792
+ STATE.goalCondition = condition
1793
+ STATE.goalIterationCount = 0
1794
+ }
1795
+
1796
+ export function getGoalIterationCount(): number {
1797
+ return STATE.goalIterationCount
1798
+ }
1799
+
1800
+ export function incrementGoalIterationCount(): void {
1801
+ STATE.goalIterationCount++
1802
+ }
1803
+
1804
+ export function getGoalMaxIterations(): number {
1805
+ return STATE.goalMaxIterations
1806
+ }
1807
+
@@ -0,0 +1,98 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import type { MutableRefObject } from 'react'
3
+ import type { MessageType } from '../components/messages.js'
4
+ import {
5
+ getGoalCondition,
6
+ getGoalIterationCount,
7
+ getGoalMaxIterations,
8
+ incrementGoalIterationCount,
9
+ setGoalCondition,
10
+ } from '../bootstrap/state.js'
11
+ import { enqueue } from '../utils/messageQueueManager.js'
12
+ import { evaluateGoal } from '../utils/goalEvaluator.js'
13
+
14
+ type UseGoalEvaluatorParams = {
15
+ lastQueryCompletionTime: number
16
+ messagesRef: MutableRefObject<MessageType[]>
17
+ isQueryActive: boolean
18
+ }
19
+
20
+ /**
21
+ * React hook that fires an independent goal evaluator after each turn.
22
+ *
23
+ * Triggered by lastQueryCompletionTime changing (set in REPL.tsx after
24
+ * queryGuard.end() succeeds). Uses messagesRef (not React state) to avoid
25
+ * batching issues — the ref is synchronously updated via Zustand pattern
26
+ * before React re-renders.
27
+ *
28
+ * On goal not satisfied: enqueues a continuation message with priority 'now'
29
+ * so useQueueProcessor picks it up immediately for the next turn.
30
+ * On goal satisfied or max iterations reached: clears the goal condition.
31
+ */
32
+ export function useGoalEvaluator({
33
+ lastQueryCompletionTime,
34
+ messagesRef,
35
+ isQueryActive,
36
+ }: UseGoalEvaluatorParams): void {
37
+ const lastEvaluatedAt = useRef(0)
38
+ const evaluating = useRef(false)
39
+
40
+ useEffect(() => {
41
+ const condition = getGoalCondition()
42
+ if (!condition) return
43
+ if (isQueryActive) return
44
+ if (lastQueryCompletionTime === 0) return
45
+ if (lastQueryCompletionTime === lastEvaluatedAt.current) return
46
+ if (evaluating.current) return
47
+
48
+ lastEvaluatedAt.current = lastQueryCompletionTime
49
+ evaluating.current = true
50
+
51
+ void (async () => {
52
+ try {
53
+ const iterCount = getGoalIterationCount()
54
+ const maxIter = getGoalMaxIterations()
55
+
56
+ if (iterCount >= maxIter) {
57
+ setGoalCondition(null)
58
+ enqueue({
59
+ value: `⚠️ /goal stopped after ${maxIter} iterations. Goal not achieved: "${condition}"`,
60
+ mode: 'task-notification',
61
+ priority: 'now',
62
+ })
63
+ return
64
+ }
65
+
66
+ // Read snapshot BEFORE await to avoid stale data
67
+ const messages = messagesRef.current
68
+ const result = await evaluateGoal(condition, messages)
69
+
70
+ // Race protection: user may have called /goal clear during the await
71
+ if (getGoalCondition() !== condition) return
72
+
73
+ incrementGoalIterationCount()
74
+
75
+ if (result.satisfied) {
76
+ setGoalCondition(null)
77
+ enqueue({
78
+ value: `✅ Goal achieved (iteration ${iterCount + 1}): ${result.reason}`,
79
+ mode: 'task-notification',
80
+ priority: 'now',
81
+ })
82
+ } else {
83
+ const continueMsg = result.gap
84
+ ? `Goal not yet met (${iterCount + 1}/${maxIter}). Gap: ${result.gap}. Continue toward: "${condition}"`
85
+ : `Goal not yet met (${iterCount + 1}/${maxIter}, reason: ${result.reason}). Continue toward: "${condition}"`
86
+ enqueue({
87
+ value: continueMsg,
88
+ mode: 'task-notification',
89
+ priority: 'now',
90
+ })
91
+ }
92
+ } finally {
93
+ evaluating.current = false
94
+ }
95
+ })()
96
+ // messages intentionally excluded from deps — read via ref to avoid batching issues
97
+ }, [lastQueryCompletionTime, isQueryActive])
98
+ }
@@ -197,6 +197,7 @@ const PROACTIVE_FALSE = () => false;
197
197
  const SUGGEST_BG_PR_NOOP = (_p: string, _n: string): boolean => false;
198
198
  const useProactive = feature('PROACTIVE') || feature('KAIROS') ? require('../proactive/useProactive.js').useProactive : null;
199
199
  const useScheduledTasks = feature('AGENT_TRIGGERS') ? require('../hooks/useScheduledTasks.js').useScheduledTasks : null;
200
+ const useGoalEvaluator = require('../hooks/useGoalEvaluator.js').useGoalEvaluator
200
201
  /* eslint-enable @typescript-eslint/no-require-imports */
201
202
  import { isAgentSwarmsEnabled } from '../utils/agentSwarmsEnabled.js';
202
203
  import { useTaskListWatcher } from '../hooks/useTaskListWatcher.js';
@@ -3888,6 +3889,11 @@ export function REPL({
3888
3889
  hasActiveLocalJsxUI: isShowingLocalJSXCommand,
3889
3890
  queryGuard
3890
3891
  });
3892
+ useGoalEvaluator({
3893
+ lastQueryCompletionTime,
3894
+ messagesRef,
3895
+ isQueryActive,
3896
+ });
3891
3897
 
3892
3898
  // We'll use the global lastInteractionTime from state.ts
3893
3899
 
@@ -0,0 +1,71 @@
1
+ import {
2
+ getGoalCondition,
3
+ getGoalMaxIterations,
4
+ setGoalCondition,
5
+ } from '../../bootstrap/state.js'
6
+ import { registerBundledSkill } from '../bundledSkills.js'
7
+
8
+ const USAGE = `Usage: /goal <condition>
9
+
10
+ Set a session goal. The agent will keep working until the condition is met.
11
+
12
+ Examples:
13
+ /goal all tests pass
14
+ /goal login flow handles empty email without crash
15
+ /goal PR is ready for review with passing CI
16
+
17
+ To cancel: /goal clear`
18
+
19
+ export function registerGoalSkill(): void {
20
+ registerBundledSkill({
21
+ name: 'goal',
22
+ description:
23
+ 'Set a session-level goal condition and loop until met. Use when user says "/goal <condition>" or wants autonomous execution until a specific outcome is reached.',
24
+ argumentHint: '<condition | clear>',
25
+ userInvocable: true,
26
+ async getPromptForCommand(args) {
27
+ const trimmed = args.trim()
28
+
29
+ if (!trimmed) {
30
+ return [{ type: 'text', text: USAGE }]
31
+ }
32
+
33
+ if (['clear', 'stop', 'cancel'].includes(trimmed)) {
34
+ const current = getGoalCondition()
35
+ if (current) {
36
+ setGoalCondition(null)
37
+ return [
38
+ {
39
+ type: 'text',
40
+ text: `Goal cancelled: "${current}". Tell the user their goal has been cancelled.`,
41
+ },
42
+ ]
43
+ }
44
+ return [
45
+ {
46
+ type: 'text',
47
+ text: 'No active goal to cancel. Tell the user there is no active goal.',
48
+ },
49
+ ]
50
+ }
51
+
52
+ setGoalCondition(trimmed)
53
+ const maxIter = getGoalMaxIterations()
54
+
55
+ return [
56
+ {
57
+ type: 'text',
58
+ text: `# /goal activated
59
+
60
+ Goal condition: "${trimmed}"
61
+
62
+ This goal is now registered for this session. An independent evaluator model will check after each turn whether the goal is satisfied. Maximum ${maxIter} iterations.
63
+
64
+ Tell the user: Goal set — you will work autonomously until "${trimmed}" is achieved (max ${maxIter} turns). Send \`/goal clear\` to cancel.
65
+
66
+ Now begin: assess current state and take the first concrete action toward the goal.`,
67
+ },
68
+ ]
69
+ },
70
+ })
71
+ }
@@ -22,6 +22,7 @@ export function initBundledSkills(): void {
22
22
  require('./simplify.js').registerSimplifySkill()
23
23
  require('./batch.js').registerBatchSkill()
24
24
  require('./stuck.js').registerStuckSkill()
25
+ require('./goal.js').registerGoalSkill()
25
26
  if (feature('KAIROS') || feature('KAIROS_DREAM')) {
26
27
  const { registerDreamSkill } = require('./dream.js')
27
28
  registerDreamSkill()
@@ -0,0 +1,64 @@
1
+ import Anthropic from '@anthropic-ai/sdk'
2
+ import type { MessageType } from '../components/messages.js'
3
+
4
+ export const GOAL_EVALUATOR_MODEL = 'claude-haiku-4-5'
5
+
6
+ export type GoalEvalResult = {
7
+ satisfied: boolean
8
+ reason: string
9
+ gap: string | null
10
+ }
11
+
12
+ /**
13
+ * Evaluate whether the goal condition has been met based on recent messages.
14
+ *
15
+ * Runs as an independent Anthropic client call — completely decoupled from the
16
+ * main query chain. Never pollutes conversation state or tool history.
17
+ */
18
+ export async function evaluateGoal(
19
+ goalCondition: string,
20
+ messages: MessageType[],
21
+ ): Promise<GoalEvalResult> {
22
+ const client = new Anthropic()
23
+
24
+ const recentAssistantTexts = messages
25
+ .filter(m => m.type === 'assistant' || m.role === 'assistant')
26
+ .slice(-5)
27
+ .map(m => {
28
+ if (typeof m.message?.content === 'string') return m.message.content
29
+ if (Array.isArray(m.message?.content)) {
30
+ return m.message.content
31
+ .filter((b: { type: string }) => b.type === 'text')
32
+ .map((b: { text: string }) => b.text)
33
+ .join('\n')
34
+ }
35
+ return ''
36
+ })
37
+ .filter(Boolean)
38
+ .join('\n---\n')
39
+
40
+ const prompt = `You are a goal completion evaluator. Determine if the goal has been fully achieved.
41
+
42
+ Goal: "${goalCondition}"
43
+
44
+ Recent assistant output:
45
+ ${recentAssistantTexts || '(none yet)'}
46
+
47
+ Respond in JSON only:
48
+ {"satisfied": true|false, "reason": "<one sentence>", "gap": "<missing item or null>"}`
49
+
50
+ const response = await client.messages.create({
51
+ model: GOAL_EVALUATOR_MODEL,
52
+ max_tokens: 256,
53
+ messages: [{ role: 'user', content: prompt }],
54
+ })
55
+
56
+ const text =
57
+ response.content[0]?.type === 'text' ? response.content[0].text : ''
58
+ try {
59
+ const cleaned = text.replace(/^```(?:json)?\n?|\n?```$/g, '').trim()
60
+ return JSON.parse(cleaned) as GoalEvalResult
61
+ } catch {
62
+ return { satisfied: false, reason: 'Evaluator parse error', gap: text }
63
+ }
64
+ }