@phuetz/code-buddy 1.0.0 → 1.2.0
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/README.md +160 -164
- package/dist/agent/autonomous/agentic-coding-contract.d.ts +6 -6
- package/dist/agent/codebuddy-agent.d.ts +1 -1
- package/dist/agent/codebuddy-agent.js +101 -13
- package/dist/agent/execution/agent-executor.js +122 -12
- package/dist/agent/execution/tool-dependency-graph.js +7 -0
- package/dist/agent/execution/tool-hooks.d.ts +6 -0
- package/dist/agent/execution/tool-hooks.js +13 -5
- package/dist/agent/execution/tool-selection-strategy.d.ts +11 -2
- package/dist/agent/execution/tool-selection-strategy.js +67 -10
- package/dist/agent/extended-thinking.d.ts +8 -0
- package/dist/agent/extended-thinking.js +27 -0
- package/dist/agent/hermes-browser-backends.js +151 -11
- package/dist/agent/hermes-claw-migrate.d.ts +59 -0
- package/dist/agent/hermes-claw-migrate.js +427 -26
- package/dist/agent/hermes-parity-manifest.js +78 -71
- package/dist/agent/hermes-runtime-lifecycle.d.ts +67 -0
- package/dist/agent/hermes-runtime-lifecycle.js +468 -0
- package/dist/agent/lesson-auto-proposer.js +10 -0
- package/dist/agent/middleware/index.d.ts +1 -0
- package/dist/agent/middleware/index.js +1 -0
- package/dist/agent/middleware/session-duration.d.ts +36 -0
- package/dist/agent/middleware/session-duration.js +78 -0
- package/dist/agent/middleware/visual-validation-middleware.d.ts +17 -0
- package/dist/agent/middleware/visual-validation-middleware.js +67 -0
- package/dist/agent/model-benchmark.d.ts +77 -0
- package/dist/agent/model-benchmark.js +309 -0
- package/dist/agent/model-tier.d.ts +14 -0
- package/dist/agent/model-tier.js +71 -0
- package/dist/agent/multi-agent/agents/orchestrator-agent.js +17 -6
- package/dist/agent/multi-agent/multi-agent-system.js +3 -1
- package/dist/agent/multi-agent/session-fleet-bridge.d.ts +2 -0
- package/dist/agent/multi-agent/session-fleet-bridge.js +9 -1
- package/dist/agent/multi-agent/session-tools.js +22 -5
- package/dist/agent/reasoning/mcts.js +20 -6
- package/dist/agent/session-end-flush.d.ts +71 -0
- package/dist/agent/session-end-flush.js +233 -0
- package/dist/agent/streaming/streaming-handler.d.ts +7 -0
- package/dist/agent/streaming/streaming-handler.js +8 -1
- package/dist/agent/tool-executor.js +1 -0
- package/dist/agent/tool-handler.d.ts +5 -0
- package/dist/agent/tool-handler.js +130 -4
- package/dist/browser-automation/browser-operator-executor.js +15 -0
- package/dist/browser-automation/browser-use-runner.d.ts +96 -0
- package/dist/browser-automation/browser-use-runner.js +492 -0
- package/dist/browser-automation/camofox-runner.d.ts +107 -0
- package/dist/browser-automation/camofox-runner.js +287 -0
- package/dist/channels/dingtalk/index.js +3 -0
- package/dist/channels/discord/client.d.ts +9 -0
- package/dist/channels/discord/client.js +12 -0
- package/dist/channels/feishu/index.d.ts +167 -1
- package/dist/channels/feishu/index.js +432 -7
- package/dist/channels/gateway-lifecycle.d.ts +132 -0
- package/dist/channels/gateway-lifecycle.js +219 -0
- package/dist/channels/google-chat/index.d.ts +5 -1
- package/dist/channels/google-chat/index.js +5 -1
- package/dist/channels/imessage/index.d.ts +14 -0
- package/dist/channels/imessage/index.js +64 -17
- package/dist/channels/index.d.ts +4 -0
- package/dist/channels/index.js +4 -0
- package/dist/channels/irc/index.d.ts +73 -3
- package/dist/channels/irc/index.js +446 -11
- package/dist/channels/line/index.js +3 -0
- package/dist/channels/mattermost/index.d.ts +51 -4
- package/dist/channels/mattermost/index.js +303 -20
- package/dist/channels/nextcloud-talk/index.d.ts +89 -8
- package/dist/channels/nextcloud-talk/index.js +367 -16
- package/dist/channels/nostr/index.d.ts +121 -1
- package/dist/channels/nostr/index.js +396 -8
- package/dist/channels/ntfy/index.js +3 -0
- package/dist/channels/qq/index.js +3 -0
- package/dist/channels/slash-parity.d.ts +117 -0
- package/dist/channels/slash-parity.js +185 -0
- package/dist/channels/synology-chat/index.js +3 -0
- package/dist/channels/teams/index.d.ts +3 -0
- package/dist/channels/teams/index.js +3 -0
- package/dist/channels/telegram/client.d.ts +9 -0
- package/dist/channels/telegram/client.js +12 -0
- package/dist/channels/twilio-voice/index.js +3 -0
- package/dist/channels/webchat/index.d.ts +7 -1
- package/dist/channels/webchat/index.js +7 -1
- package/dist/channels/wecom/index.js +3 -0
- package/dist/channels/weixin/index.js +3 -0
- package/dist/channels/zalo/index.js +3 -0
- package/dist/cli/config-loader.js +1 -1
- package/dist/codebuddy/client.d.ts +30 -1
- package/dist/codebuddy/client.js +167 -20
- package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +5 -0
- package/dist/codebuddy/providers/provider-chatgpt-responses.js +1 -0
- package/dist/codebuddy/providers/provider-gemini-native.js +1 -1
- package/dist/codebuddy/providers/provider-openai-compat.d.ts +5 -0
- package/dist/codebuddy/providers/provider-openai-compat.js +82 -2
- package/dist/codebuddy/tool-definitions/computer-control-tools.js +32 -2
- package/dist/codebuddy/tool-definitions/core-tools.js +3 -3
- package/dist/codebuddy/tool-definitions/index.d.ts +2 -1
- package/dist/codebuddy/tool-definitions/index.js +2 -1
- package/dist/codebuddy/tool-definitions/multimodal-tools.d.ts +1 -0
- package/dist/codebuddy/tool-definitions/multimodal-tools.js +44 -1
- package/dist/codebuddy/tool-definitions/windows-tools.d.ts +2 -0
- package/dist/codebuddy/tool-definitions/windows-tools.js +34 -0
- package/dist/codebuddy/tools.d.ts +1 -1
- package/dist/codebuddy/tools.js +27 -24
- package/dist/commands/cli/hermes-commands.js +108 -0
- package/dist/commands/cli/native-engine-commands.js +107 -3
- package/dist/commands/cli/utility-commands.js +45 -0
- package/dist/commands/enhanced-command-handler.js +5 -0
- package/dist/commands/goal-cli.d.ts +71 -0
- package/dist/commands/goal-cli.js +280 -0
- package/dist/commands/handlers/goal-handler.d.ts +34 -0
- package/dist/commands/handlers/goal-handler.js +155 -0
- package/dist/commands/handlers/index.d.ts +1 -0
- package/dist/commands/handlers/index.js +2 -0
- package/dist/commands/handlers/infra-handlers.js +36 -0
- package/dist/commands/handlers/memory-handlers.js +96 -1
- package/dist/commands/headless-slash.d.ts +2 -0
- package/dist/commands/headless-slash.js +12 -0
- package/dist/commands/llm-provider-resolution.d.ts +5 -3
- package/dist/commands/llm-provider-resolution.js +87 -33
- package/dist/commands/ollama.d.ts +25 -0
- package/dist/commands/ollama.js +100 -0
- package/dist/commands/provider.d.ts +5 -0
- package/dist/commands/provider.js +106 -55
- package/dist/commands/slash/builtin-commands.js +20 -0
- package/dist/commands/spec-next.js +2 -1
- package/dist/commands/spec-plan.js +5 -16
- package/dist/commands/tunnel.d.ts +2 -0
- package/dist/commands/tunnel.js +48 -0
- package/dist/config/config-resolver.d.ts +2 -1
- package/dist/config/config-resolver.js +54 -41
- package/dist/config/constants.d.ts +28 -0
- package/dist/config/constants.js +7 -0
- package/dist/config/env-schema.js +770 -0
- package/dist/config/feature-flags.js +7 -0
- package/dist/config/model-tools.js +4 -4
- package/dist/config/toml-config.d.ts +16 -0
- package/dist/config/toml-config.js +3 -0
- package/dist/context/context-manager-v2.d.ts +39 -0
- package/dist/context/context-manager-v2.js +91 -0
- package/dist/daemon/agent-task-executor.js +12 -1
- package/dist/daemon/autonomous-daemon.d.ts +1 -1
- package/dist/daemon/autonomous-daemon.js +5 -3
- package/dist/daemon/autonomous-loop.d.ts +21 -1
- package/dist/daemon/autonomous-loop.js +69 -0
- package/dist/daemon/colab-goal.d.ts +38 -0
- package/dist/daemon/colab-goal.js +81 -0
- package/dist/daemon/cron-agent-bridge.d.ts +12 -3
- package/dist/daemon/cron-agent-bridge.js +25 -9
- package/dist/desktop/codebuddy-engine-adapter.d.ts +32 -0
- package/dist/desktop/codebuddy-engine-adapter.js +257 -80
- package/dist/desktop/engine-adapter.d.ts +14 -0
- package/dist/desktop-automation/automation-manager.js +16 -0
- package/dist/desktop-automation/omniparser-runner.d.ts +63 -0
- package/dist/desktop-automation/omniparser-runner.js +115 -0
- package/dist/desktop-automation/smart-snapshot.d.ts +11 -0
- package/dist/desktop-automation/smart-snapshot.js +72 -5
- package/dist/doctor/index.js +40 -23
- package/dist/events/types.d.ts +11 -0
- package/dist/fleet/colab-store.d.ts +21 -0
- package/dist/fleet/colab-store.js +28 -0
- package/dist/fleet/peer-session-bridge.d.ts +1 -1
- package/dist/fleet/peer-session-bridge.js +243 -2
- package/dist/fleet/peer-session-store.d.ts +3 -0
- package/dist/fleet/peer-tool-bridge.js +14 -0
- package/dist/fleet/privacy-lint.d.ts +8 -0
- package/dist/fleet/privacy-lint.js +22 -0
- package/dist/goals/goal-decomposer.d.ts +27 -0
- package/dist/goals/goal-decomposer.js +293 -0
- package/dist/goals/goal-judge-client.d.ts +15 -0
- package/dist/goals/goal-judge-client.js +45 -0
- package/dist/goals/goal-judge.d.ts +38 -0
- package/dist/goals/goal-judge.js +158 -0
- package/dist/goals/goal-loop.d.ts +37 -0
- package/dist/goals/goal-loop.js +101 -0
- package/dist/goals/goal-manager.d.ts +77 -0
- package/dist/goals/goal-manager.js +289 -0
- package/dist/goals/goal-state.d.ts +99 -0
- package/dist/goals/goal-state.js +315 -0
- package/dist/goals/goal-store.d.ts +28 -0
- package/dist/goals/goal-store.js +87 -0
- package/dist/goals/index.d.ts +5 -0
- package/dist/goals/index.js +6 -0
- package/dist/hooks/use-input-handler.js +36 -1
- package/dist/hooks/user-hooks.js +17 -3
- package/dist/index.js +506 -25
- package/dist/input/text-to-speech.d.ts +2 -6
- package/dist/input/text-to-speech.js +2 -27
- package/dist/integrations/screenpipe/screenpipe-client.d.ts +10 -0
- package/dist/integrations/screenpipe/screenpipe-client.js +12 -2
- package/dist/integrations/tailscale.d.ts +13 -0
- package/dist/integrations/tailscale.js +87 -34
- package/dist/knowledge/workspace-indexer.js +53 -9
- package/dist/mcp/client.js +1 -0
- package/dist/mcp/mcp-desktop-tools.d.ts +23 -0
- package/dist/mcp/mcp-desktop-tools.js +158 -0
- package/dist/mcp/mcp-server.js +2 -0
- package/dist/memory/enhanced-memory.d.ts +10 -0
- package/dist/memory/enhanced-memory.js +33 -2
- package/dist/memory/index.d.ts +3 -1
- package/dist/memory/index.js +4 -1
- package/dist/memory/memory-auto-proposer.d.ts +23 -0
- package/dist/memory/memory-auto-proposer.js +308 -0
- package/dist/memory/memory-candidate-queue.d.ts +92 -0
- package/dist/memory/memory-candidate-queue.js +261 -0
- package/dist/memory/persistent-memory.d.ts +45 -2
- package/dist/memory/persistent-memory.js +236 -40
- package/dist/ml/bayesian-qualifier.d.ts +1 -1
- package/dist/ml/bayesian-qualifier.js +35 -2
- package/dist/observability/run-store.d.ts +1 -1
- package/dist/openclaw/gateway-bridge.js +5 -0
- package/dist/plugins/marketplace.d.ts +1 -0
- package/dist/plugins/marketplace.js +7 -0
- package/dist/prompts/execution-discipline.d.ts +14 -0
- package/dist/prompts/execution-discipline.js +29 -0
- package/dist/prompts/variation-injector.js +9 -2
- package/dist/protocols/acp/acp-agentic-runner.d.ts +7 -6
- package/dist/protocols/acp/acp-agentic-runner.js +115 -18
- package/dist/protocols/acp/acp-session-store.d.ts +23 -0
- package/dist/protocols/acp/acp-session-store.js +77 -0
- package/dist/protocols/acp/acp-stdio-server.d.ts +7 -0
- package/dist/protocols/acp/acp-stdio-server.js +71 -16
- package/dist/providers/active-llm-registry.d.ts +37 -0
- package/dist/providers/active-llm-registry.js +186 -0
- package/dist/providers/auxiliary-provider.d.ts +25 -0
- package/dist/providers/auxiliary-provider.js +192 -0
- package/dist/providers/codex-oauth.d.ts +1 -1
- package/dist/providers/codex-oauth.js +27 -4
- package/dist/providers/index.d.ts +3 -0
- package/dist/providers/index.js +4 -0
- package/dist/providers/provider-catalog.d.ts +70 -0
- package/dist/providers/provider-catalog.js +738 -0
- package/dist/providers/provider-fallback.d.ts +35 -0
- package/dist/providers/provider-fallback.js +171 -0
- package/dist/providers/xai-oauth.d.ts +128 -0
- package/dist/providers/xai-oauth.js +735 -0
- package/dist/scheduler/cron-scheduler.d.ts +15 -3
- package/dist/scheduler/cron-scheduler.js +53 -7
- package/dist/scheduler/script-runner.d.ts +2 -0
- package/dist/scheduler/script-runner.js +19 -4
- package/dist/scheduler/watchdog-handlers.js +7 -20
- package/dist/search/usearch-index.js +7 -2
- package/dist/security/tool-policy/tool-groups.js +2 -0
- package/dist/server/index.js +17 -2
- package/dist/server/routes/mobile.d.ts +12 -4
- package/dist/server/routes/mobile.js +116 -1
- package/dist/server/tls-config.d.ts +35 -0
- package/dist/server/tls-config.js +142 -0
- package/dist/server/tunnel-manager.d.ts +20 -0
- package/dist/server/tunnel-manager.js +58 -0
- package/dist/server/websocket/fleet-bridge.d.ts +13 -1
- package/dist/server/websocket/fleet-bridge.js +16 -0
- package/dist/services/prompt-builder.d.ts +1 -0
- package/dist/services/prompt-builder.js +49 -8
- package/dist/shared/engine-types.d.ts +15 -1
- package/dist/sidecar/sidecar-bridge.d.ts +1 -0
- package/dist/sidecar/sidecar-bridge.js +21 -10
- package/dist/spec/spec-store.js +8 -1
- package/dist/talk-mode/providers/audioreader-tts.d.ts +1 -1
- package/dist/talk-mode/providers/audioreader-tts.js +8 -8
- package/dist/templates/project-scaffolding.js +3 -2
- package/dist/themes/theme-schema.d.ts +10 -10
- package/dist/tools/application-profiles.js +38 -0
- package/dist/tools/bash/bash-tool.d.ts +1 -0
- package/dist/tools/bash/bash-tool.js +16 -2
- package/dist/tools/bash/command-validator.js +3 -0
- package/dist/tools/bash/rtk-rewrite.d.ts +16 -0
- package/dist/tools/bash/rtk-rewrite.js +100 -0
- package/dist/tools/bash/streaming-executor.js +14 -1
- package/dist/tools/computer-control-tool.d.ts +16 -1
- package/dist/tools/computer-control-tool.js +317 -5
- package/dist/tools/document-generator.d.ts +14 -0
- package/dist/tools/document-generator.js +79 -0
- package/dist/tools/execute-code-rpc-invoker.d.ts +26 -2
- package/dist/tools/execute-code-rpc-invoker.js +46 -5
- package/dist/tools/metadata.js +28 -0
- package/dist/tools/office-macro-tool.d.ts +10 -0
- package/dist/tools/office-macro-tool.js +93 -0
- package/dist/tools/registry/index.d.ts +2 -1
- package/dist/tools/registry/index.js +2 -1
- package/dist/tools/registry/memory-tools.d.ts +20 -0
- package/dist/tools/registry/memory-tools.js +225 -3
- package/dist/tools/registry/multimodal-tools.js +2 -2
- package/dist/tools/registry/vision-tools.d.ts +33 -0
- package/dist/tools/registry/vision-tools.js +308 -6
- package/dist/tools/registry/windows-tools.d.ts +2 -0
- package/dist/tools/registry/windows-tools.js +47 -0
- package/dist/tools/route-peer-tool.js +14 -0
- package/dist/tools/screenshot-tool.js +14 -2
- package/dist/tools/text-to-speech-tool.d.ts +1 -1
- package/dist/tools/text-to-speech-tool.js +2 -12
- package/dist/tools/tool-selector.js +3 -59
- package/dist/utils/config-validation/schema.d.ts +40 -7
- package/dist/utils/config-validation/schema.js +130 -1
- package/dist/utils/cost-tracker.js +21 -1
- package/dist/utils/disk-guard.d.ts +180 -0
- package/dist/utils/disk-guard.js +404 -0
- package/dist/utils/interactive-setup.js +35 -2
- package/dist/utils/model-utils.js +11 -2
- package/dist/utils/output-sanitizer.js +11 -0
- package/dist/utils/provider-detector.d.ts +8 -7
- package/dist/utils/provider-detector.js +19 -77
- package/dist/utils/settings-manager.d.ts +7 -0
- package/dist/utils/token-display.d.ts +3 -3
- package/dist/utils/token-display.js +22 -4
- package/dist/wizard/onboarding.d.ts +39 -0
- package/dist/wizard/onboarding.js +433 -21
- package/dist/wizard/provider-onboarding.d.ts +8 -23
- package/dist/wizard/provider-onboarding.js +39 -158
- package/package.json +4 -2
- package/dist/plugins/provider-onboarding.d.ts +0 -23
- package/dist/plugins/provider-onboarding.js +0 -116
- package/dist/utils/kokoro-tts.d.ts +0 -8
- package/dist/utils/kokoro-tts.js +0 -67
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent session goals — the Ralph loop for Code Buddy.
|
|
3
|
+
*
|
|
4
|
+
* A goal is a free-form user objective that stays active across turns. After
|
|
5
|
+
* each turn completes, a small judge call asks an auxiliary model "is this
|
|
6
|
+
* goal satisfied by the assistant's last response?". If not, Code Buddy feeds
|
|
7
|
+
* a continuation prompt back into the same session and keeps working until
|
|
8
|
+
* the goal is done, the turn budget is exhausted, the user pauses/clears it,
|
|
9
|
+
* or a real user message preempts the loop.
|
|
10
|
+
*
|
|
11
|
+
* Ported from Hermes Agent's goal system (hermes_cli/goals.py). Invariants:
|
|
12
|
+
* - The continuation prompt is a plain user message — no system-prompt
|
|
13
|
+
* mutation, no toolset swap, prompt caching stays intact.
|
|
14
|
+
* - Judge failures are fail-OPEN ("continue"): a broken judge must not wedge
|
|
15
|
+
* progress; the turn budget is the backstop.
|
|
16
|
+
* - A real user message mid-loop preempts the continuation prompt; the judge
|
|
17
|
+
* re-runs after that turn.
|
|
18
|
+
*/
|
|
19
|
+
import { formatGoalPlan, goalPlanToCriteria, normalizeGoalPlan, } from './goal-decomposer.js';
|
|
20
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
21
|
+
// Constants & defaults
|
|
22
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
23
|
+
export const DEFAULT_MAX_TURNS = 20;
|
|
24
|
+
export const DEFAULT_JUDGE_TIMEOUT_MS = 30_000;
|
|
25
|
+
// Judge output budget. The judge returns a one-line JSON verdict, but
|
|
26
|
+
// reasoning models burn tokens on hidden reasoning before emitting the
|
|
27
|
+
// visible JSON. Tight caps truncate the JSON and trip the auto-pause.
|
|
28
|
+
export const DEFAULT_JUDGE_MAX_TOKENS = 4096;
|
|
29
|
+
// Caps how much of the inputs we send to the judge.
|
|
30
|
+
export const JUDGE_GOAL_SNIPPET_CHARS = 2000;
|
|
31
|
+
export const JUDGE_SUBGOALS_SNIPPET_CHARS = 2000;
|
|
32
|
+
export const JUDGE_RESPONSE_SNIPPET_CHARS = 4000;
|
|
33
|
+
// After this many consecutive judge *parse* failures (empty output /
|
|
34
|
+
// non-JSON), the loop auto-pauses and points the user at the judge config.
|
|
35
|
+
// Guards against small models that can't follow the strict JSON contract.
|
|
36
|
+
export const MAX_CONSECUTIVE_PARSE_FAILURES = 3;
|
|
37
|
+
export const CONTINUATION_PROMPT_TEMPLATE = '[Continuing toward your standing goal]\n' +
|
|
38
|
+
'Goal: {goal}\n\n' +
|
|
39
|
+
'Continue working toward this goal. Take the next concrete step. ' +
|
|
40
|
+
'If you believe the goal is complete, state so explicitly and stop. ' +
|
|
41
|
+
'If you are blocked and need input from the user, say so clearly and stop.';
|
|
42
|
+
export const CONTINUATION_PROMPT_WITH_SUBGOALS_TEMPLATE = '[Continuing toward your standing goal]\n' +
|
|
43
|
+
'Goal: {goal}\n\n' +
|
|
44
|
+
'Additional criteria the user added mid-loop:\n' +
|
|
45
|
+
'{subgoals_block}\n\n' +
|
|
46
|
+
'Continue working toward the goal AND all additional criteria. Take ' +
|
|
47
|
+
'the next concrete step. If you believe the goal and every ' +
|
|
48
|
+
'additional criterion are complete, state so explicitly and stop. ' +
|
|
49
|
+
'If you are blocked and need input from the user, say so clearly ' +
|
|
50
|
+
'and stop.';
|
|
51
|
+
export const CONTINUATION_PROMPT_WITH_PLAN_TEMPLATE = '[Continuing toward your standing goal]\n' +
|
|
52
|
+
'Goal: {goal}\n\n' +
|
|
53
|
+
'Decomposition plan:\n' +
|
|
54
|
+
'{goal_plan_block}\n\n' +
|
|
55
|
+
'{manual_subgoals_block}' +
|
|
56
|
+
'Continue working through the plan. Prefer ready tasks whose dependencies ' +
|
|
57
|
+
'are already satisfied, keep independent lanes separate, and produce ' +
|
|
58
|
+
'concrete evidence for every acceptance criterion. If you believe the goal ' +
|
|
59
|
+
'and every criterion are complete, state so explicitly and stop. If you are ' +
|
|
60
|
+
'blocked and need input from the user, say so clearly and stop.';
|
|
61
|
+
export const JUDGE_SYSTEM_PROMPT = "You are a strict judge evaluating whether an autonomous agent has " +
|
|
62
|
+
"achieved a user's stated goal. You receive the goal text and the " +
|
|
63
|
+
"agent's most recent response. Your only job is to decide whether " +
|
|
64
|
+
'the goal is fully satisfied based on that response.\n\n' +
|
|
65
|
+
'The response may begin with internal evidence metadata such as ' +
|
|
66
|
+
'`[tool evidence: none]` or `[tool:<name> success]`. Treat those ' +
|
|
67
|
+
'bracketed metadata lines as evidence annotations only. They are NOT ' +
|
|
68
|
+
"part of the assistant's answer text for exact-answer goals; ignore " +
|
|
69
|
+
'the metadata itself when comparing exact requested output. For ' +
|
|
70
|
+
'side-effect goals, use the metadata and any following tool output to ' +
|
|
71
|
+
'decide whether concrete evidence exists.\n\n' +
|
|
72
|
+
'A goal is DONE only when:\n' +
|
|
73
|
+
'- For answer-only or explanation goals, the response directly answers the requested question, OR\n' +
|
|
74
|
+
'- The response clearly shows the final deliverable was produced, OR\n' +
|
|
75
|
+
'- The response explains the goal is unachievable / blocked / needs ' +
|
|
76
|
+
'user input (treat this as DONE with reason describing the block).\n\n' +
|
|
77
|
+
'For goals that require external side effects such as creating, editing, ' +
|
|
78
|
+
'moving, deleting, reading, or verifying files; running commands; calling ' +
|
|
79
|
+
'tools; changing repository state; or producing artifacts, unsupported ' +
|
|
80
|
+
'assistant claims are NOT evidence. Do not accept statements like "I ' +
|
|
81
|
+
'created the file" or "I verified it" unless the response includes concrete ' +
|
|
82
|
+
'evidence such as a tool result, command output, file contents excerpt, diff, ' +
|
|
83
|
+
'test output, or artifact path backed by tool output. If a side-effect goal ' +
|
|
84
|
+
'has no concrete evidence, return CONTINUE.\n\n' +
|
|
85
|
+
'Otherwise the goal is NOT done — CONTINUE.\n\n' +
|
|
86
|
+
'Reply ONLY with a single JSON object on one line:\n' +
|
|
87
|
+
'{"done": <true|false>, "reason": "<one-sentence rationale>"}';
|
|
88
|
+
export const JUDGE_USER_PROMPT_TEMPLATE = 'Goal:\n{goal}\n\n' +
|
|
89
|
+
'Current time: {current_time}\n\n' +
|
|
90
|
+
"Agent's most recent response is delimited by <response> tags. " +
|
|
91
|
+
'Text outside those tags is prompt metadata, not part of the response:\n' +
|
|
92
|
+
'<response>\n{response}\n</response>\n\n' +
|
|
93
|
+
'Is the goal satisfied?';
|
|
94
|
+
export const JUDGE_USER_PROMPT_WITH_SUBGOALS_TEMPLATE = 'Goal:\n{goal}\n\n' +
|
|
95
|
+
'Additional criteria the user added mid-loop (all must also be ' +
|
|
96
|
+
'satisfied for the goal to be DONE):\n{subgoals_block}\n\n' +
|
|
97
|
+
'Current time: {current_time}\n\n' +
|
|
98
|
+
"Agent's most recent response is delimited by <response> tags. " +
|
|
99
|
+
'Text outside those tags is prompt metadata, not part of the response:\n' +
|
|
100
|
+
'<response>\n{response}\n</response>\n\n' +
|
|
101
|
+
'Decision: For each numbered criterion above, find concrete ' +
|
|
102
|
+
"evidence in the agent's response that the criterion is " +
|
|
103
|
+
"satisfied. Do not accept generic phrases like 'all requirements " +
|
|
104
|
+
"met' or 'implying it was done' — require specific evidence (a " +
|
|
105
|
+
'file contents excerpt, an output line, a command result). If ' +
|
|
106
|
+
'ANY criterion lacks specific evidence in the response, the goal ' +
|
|
107
|
+
'is NOT done — return CONTINUE.\n\n' +
|
|
108
|
+
'Is the goal AND every additional criterion satisfied?';
|
|
109
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
110
|
+
// Helpers
|
|
111
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
112
|
+
export function truncateText(text, limit) {
|
|
113
|
+
if (!text)
|
|
114
|
+
return '';
|
|
115
|
+
if (text.length <= limit)
|
|
116
|
+
return text;
|
|
117
|
+
return text.slice(0, limit) + '… [truncated]';
|
|
118
|
+
}
|
|
119
|
+
/** Render subgoals as a numbered `- N. text` block. Empty string when none. */
|
|
120
|
+
export function renderSubgoalsBlock(subgoals) {
|
|
121
|
+
if (!subgoals.length)
|
|
122
|
+
return '';
|
|
123
|
+
return subgoals.map((text, i) => `- ${i + 1}. ${text}`).join('\n');
|
|
124
|
+
}
|
|
125
|
+
export function getGoalJudgeCriteria(state) {
|
|
126
|
+
return [...goalPlanToCriteria(state.goalPlan), ...state.subgoals];
|
|
127
|
+
}
|
|
128
|
+
export function createGoalState(goal, maxTurns = DEFAULT_MAX_TURNS) {
|
|
129
|
+
if (!Number.isSafeInteger(maxTurns) || maxTurns <= 0) {
|
|
130
|
+
throw new Error('maxTurns must be a positive integer');
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
goal,
|
|
134
|
+
status: 'active',
|
|
135
|
+
turnsUsed: 0,
|
|
136
|
+
maxTurns,
|
|
137
|
+
createdAt: Date.now(),
|
|
138
|
+
lastTurnAt: 0,
|
|
139
|
+
consecutiveParseFailures: 0,
|
|
140
|
+
subgoals: [],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Defensive deserialization of a persisted goal state. Returns null when the
|
|
145
|
+
* payload isn't a usable goal. Old payloads without `subgoals` load unchanged.
|
|
146
|
+
*/
|
|
147
|
+
export function normalizeGoalState(raw) {
|
|
148
|
+
if (!raw || typeof raw !== 'object')
|
|
149
|
+
return null;
|
|
150
|
+
const data = raw;
|
|
151
|
+
const goal = typeof data.goal === 'string' ? data.goal : '';
|
|
152
|
+
if (!goal.trim())
|
|
153
|
+
return null;
|
|
154
|
+
const status = ['active', 'paused', 'done', 'cleared'].includes(String(data.status))
|
|
155
|
+
? data.status
|
|
156
|
+
: 'active';
|
|
157
|
+
const subgoals = Array.isArray(data.subgoals)
|
|
158
|
+
? data.subgoals.map(s => String(s).trim()).filter(Boolean)
|
|
159
|
+
: [];
|
|
160
|
+
const goalPlan = normalizeGoalPlan(data.goalPlan);
|
|
161
|
+
const verdict = ['done', 'continue', 'skipped'].includes(String(data.lastVerdict))
|
|
162
|
+
? data.lastVerdict
|
|
163
|
+
: undefined;
|
|
164
|
+
const state = {
|
|
165
|
+
goal,
|
|
166
|
+
status,
|
|
167
|
+
turnsUsed: toNonNegativeInt(data.turnsUsed, 0),
|
|
168
|
+
maxTurns: toPositiveInt(data.maxTurns, DEFAULT_MAX_TURNS),
|
|
169
|
+
createdAt: toNumber(data.createdAt, 0),
|
|
170
|
+
lastTurnAt: toNumber(data.lastTurnAt, 0),
|
|
171
|
+
consecutiveParseFailures: toNonNegativeInt(data.consecutiveParseFailures, 0),
|
|
172
|
+
subgoals,
|
|
173
|
+
};
|
|
174
|
+
if (goalPlan)
|
|
175
|
+
state.goalPlan = goalPlan;
|
|
176
|
+
if (typeof data.goalPlanAttempted === 'boolean') {
|
|
177
|
+
state.goalPlanAttempted = data.goalPlanAttempted;
|
|
178
|
+
}
|
|
179
|
+
if (typeof data.goalPlanLastError === 'string') {
|
|
180
|
+
state.goalPlanLastError = data.goalPlanLastError;
|
|
181
|
+
}
|
|
182
|
+
if (verdict)
|
|
183
|
+
state.lastVerdict = verdict;
|
|
184
|
+
if (typeof data.lastReason === 'string')
|
|
185
|
+
state.lastReason = data.lastReason;
|
|
186
|
+
if (typeof data.pausedReason === 'string')
|
|
187
|
+
state.pausedReason = data.pausedReason;
|
|
188
|
+
return state;
|
|
189
|
+
}
|
|
190
|
+
function toPositiveInt(value, fallback) {
|
|
191
|
+
const n = parseSafeInteger(value, /^[1-9]\d*$/);
|
|
192
|
+
return Number.isSafeInteger(n) && n > 0 ? n : fallback;
|
|
193
|
+
}
|
|
194
|
+
function toNonNegativeInt(value, fallback) {
|
|
195
|
+
const n = parseSafeInteger(value, /^(?:0|[1-9]\d*)$/);
|
|
196
|
+
return Number.isSafeInteger(n) && n >= 0 ? n : fallback;
|
|
197
|
+
}
|
|
198
|
+
function parseSafeInteger(value, stringPattern) {
|
|
199
|
+
if (typeof value === 'number')
|
|
200
|
+
return value;
|
|
201
|
+
if (typeof value !== 'string')
|
|
202
|
+
return Number.NaN;
|
|
203
|
+
const trimmed = value.trim();
|
|
204
|
+
if (!stringPattern.test(trimmed))
|
|
205
|
+
return Number.NaN;
|
|
206
|
+
return Number(trimmed);
|
|
207
|
+
}
|
|
208
|
+
function toNumber(value, fallback) {
|
|
209
|
+
const n = Number(value);
|
|
210
|
+
return Number.isFinite(n) ? n : fallback;
|
|
211
|
+
}
|
|
212
|
+
/** Printable one-liner for /goal status. */
|
|
213
|
+
export function formatGoalStatusLine(state) {
|
|
214
|
+
if (!state || state.status === 'cleared') {
|
|
215
|
+
return 'No active goal. Set one with /goal <text>.';
|
|
216
|
+
}
|
|
217
|
+
const turns = `${state.turnsUsed}/${state.maxTurns} turns`;
|
|
218
|
+
const sub = state.subgoals.length
|
|
219
|
+
? `, ${state.subgoals.length} subgoal${state.subgoals.length !== 1 ? 's' : ''}`
|
|
220
|
+
: '';
|
|
221
|
+
const plan = state.goalPlan
|
|
222
|
+
? `, plan ${state.goalPlan.tasks.length} task${state.goalPlan.tasks.length !== 1 ? 's' : ''}`
|
|
223
|
+
: '';
|
|
224
|
+
if (state.status === 'active') {
|
|
225
|
+
return `⊙ Goal (active, ${turns}${sub}${plan}): ${state.goal}`;
|
|
226
|
+
}
|
|
227
|
+
if (state.status === 'paused') {
|
|
228
|
+
const extra = state.pausedReason ? ` — ${state.pausedReason}` : '';
|
|
229
|
+
return `⏸ Goal (paused, ${turns}${sub}${plan}${extra}): ${state.goal}`;
|
|
230
|
+
}
|
|
231
|
+
if (state.status === 'done') {
|
|
232
|
+
return `✓ Goal done (${turns}${sub}${plan}): ${state.goal}`;
|
|
233
|
+
}
|
|
234
|
+
return `Goal (${state.status}, ${turns}${sub}${plan}): ${state.goal}`;
|
|
235
|
+
}
|
|
236
|
+
/** The canonical user-role continuation message for an active goal. */
|
|
237
|
+
export function buildContinuationPrompt(state) {
|
|
238
|
+
if (state.goalPlan) {
|
|
239
|
+
const manualSubgoalsBlock = state.subgoals.length
|
|
240
|
+
? `Additional user criteria:\n${renderSubgoalsBlock(state.subgoals)}\n\n`
|
|
241
|
+
: '';
|
|
242
|
+
return CONTINUATION_PROMPT_WITH_PLAN_TEMPLATE.replace('{goal}', state.goal)
|
|
243
|
+
.replace('{goal_plan_block}', formatGoalPlan(state.goalPlan))
|
|
244
|
+
.replace('{manual_subgoals_block}', manualSubgoalsBlock);
|
|
245
|
+
}
|
|
246
|
+
if (state.subgoals.length) {
|
|
247
|
+
return CONTINUATION_PROMPT_WITH_SUBGOALS_TEMPLATE.replace('{goal}', state.goal).replace('{subgoals_block}', renderSubgoalsBlock(state.subgoals));
|
|
248
|
+
}
|
|
249
|
+
return CONTINUATION_PROMPT_TEMPLATE.replace('{goal}', state.goal);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Apply a judge outcome to an active goal — the exact Hermes ladder.
|
|
253
|
+
* MUTATES `state` (turn counter, verdict bookkeeping, status transitions)
|
|
254
|
+
* and returns the decision; the caller persists the state wherever it
|
|
255
|
+
* lives (GoalStore file, peer-session record, …).
|
|
256
|
+
*/
|
|
257
|
+
export function applyJudgeOutcome(state, outcome, nowMs = Date.now()) {
|
|
258
|
+
state.turnsUsed += 1;
|
|
259
|
+
state.lastTurnAt = nowMs;
|
|
260
|
+
state.lastVerdict = outcome.verdict;
|
|
261
|
+
state.lastReason = outcome.reason;
|
|
262
|
+
// Reset the parse-failure streak on any usable reply, including
|
|
263
|
+
// API/transport errors (parseFailed=false), so a flaky network doesn't
|
|
264
|
+
// trip the auto-pause meant for bad judge models.
|
|
265
|
+
state.consecutiveParseFailures = outcome.parseFailed ? state.consecutiveParseFailures + 1 : 0;
|
|
266
|
+
if (outcome.verdict === 'done') {
|
|
267
|
+
state.status = 'done';
|
|
268
|
+
return {
|
|
269
|
+
status: 'done',
|
|
270
|
+
shouldContinue: false,
|
|
271
|
+
continuationPrompt: null,
|
|
272
|
+
verdict: 'done',
|
|
273
|
+
reason: outcome.reason,
|
|
274
|
+
message: `✓ Goal achieved: ${outcome.reason}`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (state.consecutiveParseFailures >= MAX_CONSECUTIVE_PARSE_FAILURES) {
|
|
278
|
+
state.status = 'paused';
|
|
279
|
+
state.pausedReason = `judge model returned unparseable output ${state.consecutiveParseFailures} turns in a row`;
|
|
280
|
+
return {
|
|
281
|
+
status: 'paused',
|
|
282
|
+
shouldContinue: false,
|
|
283
|
+
continuationPrompt: null,
|
|
284
|
+
verdict: 'continue',
|
|
285
|
+
reason: outcome.reason,
|
|
286
|
+
message: `⏸ Goal paused — the judge model (${state.consecutiveParseFailures} turns) ` +
|
|
287
|
+
'isn\'t returning the required JSON verdict. Route the judge to a stricter ' +
|
|
288
|
+
'model in .codebuddy/settings.json:\n' +
|
|
289
|
+
' { "goals": { "judgeModel": "<a model that follows JSON instructions>" } }\n' +
|
|
290
|
+
'Then /goal resume to continue.',
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (state.turnsUsed >= state.maxTurns) {
|
|
294
|
+
state.status = 'paused';
|
|
295
|
+
state.pausedReason = `turn budget exhausted (${state.turnsUsed}/${state.maxTurns})`;
|
|
296
|
+
return {
|
|
297
|
+
status: 'paused',
|
|
298
|
+
shouldContinue: false,
|
|
299
|
+
continuationPrompt: null,
|
|
300
|
+
verdict: 'continue',
|
|
301
|
+
reason: outcome.reason,
|
|
302
|
+
message: `⏸ Goal paused — ${state.turnsUsed}/${state.maxTurns} turns used. ` +
|
|
303
|
+
'Use /goal resume to keep going, or /goal clear to stop.',
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
status: 'active',
|
|
308
|
+
shouldContinue: true,
|
|
309
|
+
continuationPrompt: buildContinuationPrompt(state),
|
|
310
|
+
verdict: 'continue',
|
|
311
|
+
reason: outcome.reason,
|
|
312
|
+
message: `↻ Continuing toward goal (${state.turnsUsed}/${state.maxTurns}): ${outcome.reason}`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
//# sourceMappingURL=goal-state.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable per-session goal persistence.
|
|
3
|
+
*
|
|
4
|
+
* One JSON file per session key under `~/.codebuddy/goals/` (honors
|
|
5
|
+
* CODEBUDDY_HOME). Files are tiny (<1 KB) so I/O is synchronous — same
|
|
6
|
+
* trade-off as TodoTracker. Writes are atomic (tmp + rename). Loads are
|
|
7
|
+
* fail-soft: a corrupt or unreadable file reads as "no goal".
|
|
8
|
+
*
|
|
9
|
+
* Cleared goals keep a tombstone (`status: 'cleared'`) for audit, mirroring
|
|
10
|
+
* Hermes' SessionDB behavior.
|
|
11
|
+
*/
|
|
12
|
+
import { GoalState } from './goal-state.js';
|
|
13
|
+
export interface GoalStoreOptions {
|
|
14
|
+
/** Override the storage directory (test isolation). */
|
|
15
|
+
storeDir?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class GoalStore {
|
|
18
|
+
private storeDir;
|
|
19
|
+
constructor(options?: GoalStoreOptions);
|
|
20
|
+
getStoreDir(): string;
|
|
21
|
+
load(key: string): GoalState | null;
|
|
22
|
+
save(key: string, state: GoalState): void;
|
|
23
|
+
delete(key: string): void;
|
|
24
|
+
private fileFor;
|
|
25
|
+
private fileCandidatesForRead;
|
|
26
|
+
private legacyFileFor;
|
|
27
|
+
private fileLabel;
|
|
28
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable per-session goal persistence.
|
|
3
|
+
*
|
|
4
|
+
* One JSON file per session key under `~/.codebuddy/goals/` (honors
|
|
5
|
+
* CODEBUDDY_HOME). Files are tiny (<1 KB) so I/O is synchronous — same
|
|
6
|
+
* trade-off as TodoTracker. Writes are atomic (tmp + rename). Loads are
|
|
7
|
+
* fail-soft: a corrupt or unreadable file reads as "no goal".
|
|
8
|
+
*
|
|
9
|
+
* Cleared goals keep a tombstone (`status: 'cleared'`) for audit, mirroring
|
|
10
|
+
* Hermes' SessionDB behavior.
|
|
11
|
+
*/
|
|
12
|
+
import { createHash } from 'crypto';
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { getCodeBuddyPath } from '../utils/codebuddy-home.js';
|
|
16
|
+
import { logger } from '../utils/logger.js';
|
|
17
|
+
import { normalizeGoalState } from './goal-state.js';
|
|
18
|
+
export class GoalStore {
|
|
19
|
+
storeDir;
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.storeDir = options.storeDir ?? getCodeBuddyPath('goals');
|
|
22
|
+
}
|
|
23
|
+
getStoreDir() {
|
|
24
|
+
return this.storeDir;
|
|
25
|
+
}
|
|
26
|
+
load(key) {
|
|
27
|
+
if (!key)
|
|
28
|
+
return null;
|
|
29
|
+
for (const file of this.fileCandidatesForRead(key)) {
|
|
30
|
+
try {
|
|
31
|
+
if (!fs.existsSync(file))
|
|
32
|
+
continue;
|
|
33
|
+
const raw = fs.readFileSync(file, 'utf-8');
|
|
34
|
+
return normalizeGoalState(JSON.parse(raw));
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
logger.debug('GoalStore: failed to load goal state', { key, file, error: String(error) });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
save(key, state) {
|
|
43
|
+
if (!key)
|
|
44
|
+
return;
|
|
45
|
+
const file = this.fileFor(key);
|
|
46
|
+
try {
|
|
47
|
+
fs.mkdirSync(this.storeDir, { recursive: true });
|
|
48
|
+
const tmp = `${file}.tmp.${process.pid}`;
|
|
49
|
+
fs.writeFileSync(tmp, JSON.stringify(state, null, 2), 'utf-8');
|
|
50
|
+
fs.renameSync(tmp, file);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
logger.debug('GoalStore: failed to save goal state', { key, error: String(error) });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
delete(key) {
|
|
57
|
+
if (!key)
|
|
58
|
+
return;
|
|
59
|
+
for (const file of this.fileCandidatesForRead(key)) {
|
|
60
|
+
try {
|
|
61
|
+
fs.rmSync(file, { force: true });
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
logger.debug('GoalStore: failed to delete goal state', { key, file, error: String(error) });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
fileFor(key) {
|
|
69
|
+
const label = this.fileLabel(key);
|
|
70
|
+
const digest = createHash('sha256').update(key).digest('hex').slice(0, 16);
|
|
71
|
+
return path.join(this.storeDir, `${label}-${digest}.json`);
|
|
72
|
+
}
|
|
73
|
+
fileCandidatesForRead(key) {
|
|
74
|
+
return Array.from(new Set([this.fileFor(key), this.legacyFileFor(key)]));
|
|
75
|
+
}
|
|
76
|
+
legacyFileFor(key) {
|
|
77
|
+
// Session ids and dir-hash keys are already filesystem-safe; sanitize
|
|
78
|
+
// anyway so a malformed key can't escape the store directory.
|
|
79
|
+
const safe = key.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
80
|
+
return path.join(this.storeDir, `${safe}.json`);
|
|
81
|
+
}
|
|
82
|
+
fileLabel(key) {
|
|
83
|
+
const safe = key.replace(/[^a-zA-Z0-9._-]/g, '_').replace(/^\.+$/, '_').slice(0, 80);
|
|
84
|
+
return safe || 'goal';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=goal-store.js.map
|
|
@@ -16,6 +16,7 @@ import { ClientCommandDispatcher } from "../commands/client-dispatcher.js";
|
|
|
16
16
|
import { extractFileReference, getFileSuggestions } from "../ui/components/FileAutocomplete.js";
|
|
17
17
|
// Import interaction logger for session tracking
|
|
18
18
|
import { getInteractionLogger } from "../logging/interaction-logger.js";
|
|
19
|
+
import { maybeContinueGoalAfterTurn } from "../goals/goal-loop.js";
|
|
19
20
|
import { logger } from '../utils/logger.js';
|
|
20
21
|
// Import history manager for persistent command history
|
|
21
22
|
import { getHistoryManager } from "../utils/history-manager.js";
|
|
@@ -35,6 +36,11 @@ export function useInputHandler({ agent, chatHistory, setChatHistory, setIsProce
|
|
|
35
36
|
// Track last escape time for double-escape detection
|
|
36
37
|
const lastEscapeTimeRef = useRef(0);
|
|
37
38
|
const DOUBLE_ESCAPE_THRESHOLD = 500; // ms
|
|
39
|
+
// Goal loop (Ralph loop): monotonically increasing turn sequence. A real
|
|
40
|
+
// user submit mid-turn bumps it, which voids the stale turn's continuation
|
|
41
|
+
// (the judge then runs after the user's turn instead — Hermes preemption).
|
|
42
|
+
const turnSeqRef = useRef(0);
|
|
43
|
+
const goalInterruptedRef = useRef(false);
|
|
38
44
|
/**
|
|
39
45
|
* Save instruction to .codebuddyrules file (Standard # capture)
|
|
40
46
|
*/
|
|
@@ -88,6 +94,7 @@ export function useInputHandler({ agent, chatHistory, setChatHistory, setIsProce
|
|
|
88
94
|
return true;
|
|
89
95
|
}
|
|
90
96
|
if (isProcessing || isStreaming) {
|
|
97
|
+
goalInterruptedRef.current = true;
|
|
91
98
|
agent.abortCurrentOperation();
|
|
92
99
|
setIsProcessing(false);
|
|
93
100
|
setIsStreaming(false);
|
|
@@ -465,6 +472,8 @@ export function useInputHandler({ agent, chatHistory, setChatHistory, setIsProce
|
|
|
465
472
|
return await ClientCommandDispatcher.dispatch(input, context);
|
|
466
473
|
};
|
|
467
474
|
const processUserMessage = async (userInput) => {
|
|
475
|
+
const mySeq = ++turnSeqRef.current;
|
|
476
|
+
goalInterruptedRef.current = false;
|
|
468
477
|
const userEntry = {
|
|
469
478
|
type: "user",
|
|
470
479
|
content: userInput,
|
|
@@ -484,10 +493,10 @@ export function useInputHandler({ agent, chatHistory, setChatHistory, setIsProce
|
|
|
484
493
|
setIsProcessing(true);
|
|
485
494
|
setCurrentActivity?.('Sending to LLM...');
|
|
486
495
|
clearInput();
|
|
496
|
+
let fullResponseContent = "";
|
|
487
497
|
try {
|
|
488
498
|
setIsStreaming(true);
|
|
489
499
|
let streamingEntry = null;
|
|
490
|
-
let fullResponseContent = "";
|
|
491
500
|
for await (const chunk of agent.processUserMessageStream(userInput)) {
|
|
492
501
|
switch (chunk.type) {
|
|
493
502
|
case "content":
|
|
@@ -677,6 +686,32 @@ export function useInputHandler({ agent, chatHistory, setChatHistory, setIsProce
|
|
|
677
686
|
setIsProcessing(false);
|
|
678
687
|
setCurrentActivity?.('');
|
|
679
688
|
processingStartTime.current = 0;
|
|
689
|
+
// Goal loop (Ralph loop): judge the finished turn and maybe auto-continue.
|
|
690
|
+
// Failures here must never break a turn — the goal system is advisory.
|
|
691
|
+
try {
|
|
692
|
+
const outcome = await maybeContinueGoalAfterTurn({
|
|
693
|
+
client: agent.getClient(),
|
|
694
|
+
lastResponse: fullResponseContent,
|
|
695
|
+
interrupted: goalInterruptedRef.current,
|
|
696
|
+
});
|
|
697
|
+
if (outcome?.message) {
|
|
698
|
+
setChatHistory((prev) => [
|
|
699
|
+
...prev,
|
|
700
|
+
{ type: 'assistant', content: outcome.message, timestamp: new Date() },
|
|
701
|
+
]);
|
|
702
|
+
}
|
|
703
|
+
// Skip the continuation if a newer turn started while we were judging —
|
|
704
|
+
// the user's message preempts the loop and gets judged after its turn.
|
|
705
|
+
if (outcome?.continuationPrompt && turnSeqRef.current === mySeq) {
|
|
706
|
+
const continuation = outcome.continuationPrompt;
|
|
707
|
+
setTimeout(() => {
|
|
708
|
+
void processUserMessage(continuation);
|
|
709
|
+
}, 50);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
catch (error) {
|
|
713
|
+
logger.debug('goal after-turn hook failed', { error: String(error) });
|
|
714
|
+
}
|
|
680
715
|
};
|
|
681
716
|
return {
|
|
682
717
|
input,
|
package/dist/hooks/user-hooks.js
CHANGED
|
@@ -188,6 +188,8 @@ export class UserHooksManager {
|
|
|
188
188
|
const shellFlag = isWindows ? '/c' : '-c';
|
|
189
189
|
const child = spawn(shell, [shellFlag, expandedCommand], {
|
|
190
190
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
191
|
+
detached: !isWindows,
|
|
192
|
+
windowsHide: true,
|
|
191
193
|
env: {
|
|
192
194
|
...process.env,
|
|
193
195
|
TOOL_NAME: context.toolName ?? '',
|
|
@@ -202,12 +204,24 @@ export class UserHooksManager {
|
|
|
202
204
|
let timedOut = false;
|
|
203
205
|
const timer = setTimeout(() => {
|
|
204
206
|
timedOut = true;
|
|
205
|
-
child.
|
|
207
|
+
if (!isWindows && child.pid) {
|
|
208
|
+
try {
|
|
209
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
child.kill('SIGTERM');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
child.kill('SIGTERM');
|
|
217
|
+
}
|
|
206
218
|
}, timeout);
|
|
207
219
|
// Write context JSON to stdin
|
|
220
|
+
child.stdin.on('error', (err) => {
|
|
221
|
+
logger.debug(`[user-hooks] command stdin closed: ${err.message}`);
|
|
222
|
+
});
|
|
208
223
|
try {
|
|
209
|
-
child.stdin.
|
|
210
|
-
child.stdin.end();
|
|
224
|
+
child.stdin.end(JSON.stringify({ event, ...context }));
|
|
211
225
|
}
|
|
212
226
|
catch {
|
|
213
227
|
// stdin may already be closed
|