@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.
Files changed (312) hide show
  1. package/README.md +160 -164
  2. package/dist/agent/autonomous/agentic-coding-contract.d.ts +6 -6
  3. package/dist/agent/codebuddy-agent.d.ts +1 -1
  4. package/dist/agent/codebuddy-agent.js +101 -13
  5. package/dist/agent/execution/agent-executor.js +122 -12
  6. package/dist/agent/execution/tool-dependency-graph.js +7 -0
  7. package/dist/agent/execution/tool-hooks.d.ts +6 -0
  8. package/dist/agent/execution/tool-hooks.js +13 -5
  9. package/dist/agent/execution/tool-selection-strategy.d.ts +11 -2
  10. package/dist/agent/execution/tool-selection-strategy.js +67 -10
  11. package/dist/agent/extended-thinking.d.ts +8 -0
  12. package/dist/agent/extended-thinking.js +27 -0
  13. package/dist/agent/hermes-browser-backends.js +151 -11
  14. package/dist/agent/hermes-claw-migrate.d.ts +59 -0
  15. package/dist/agent/hermes-claw-migrate.js +427 -26
  16. package/dist/agent/hermes-parity-manifest.js +78 -71
  17. package/dist/agent/hermes-runtime-lifecycle.d.ts +67 -0
  18. package/dist/agent/hermes-runtime-lifecycle.js +468 -0
  19. package/dist/agent/lesson-auto-proposer.js +10 -0
  20. package/dist/agent/middleware/index.d.ts +1 -0
  21. package/dist/agent/middleware/index.js +1 -0
  22. package/dist/agent/middleware/session-duration.d.ts +36 -0
  23. package/dist/agent/middleware/session-duration.js +78 -0
  24. package/dist/agent/middleware/visual-validation-middleware.d.ts +17 -0
  25. package/dist/agent/middleware/visual-validation-middleware.js +67 -0
  26. package/dist/agent/model-benchmark.d.ts +77 -0
  27. package/dist/agent/model-benchmark.js +309 -0
  28. package/dist/agent/model-tier.d.ts +14 -0
  29. package/dist/agent/model-tier.js +71 -0
  30. package/dist/agent/multi-agent/agents/orchestrator-agent.js +17 -6
  31. package/dist/agent/multi-agent/multi-agent-system.js +3 -1
  32. package/dist/agent/multi-agent/session-fleet-bridge.d.ts +2 -0
  33. package/dist/agent/multi-agent/session-fleet-bridge.js +9 -1
  34. package/dist/agent/multi-agent/session-tools.js +22 -5
  35. package/dist/agent/reasoning/mcts.js +20 -6
  36. package/dist/agent/session-end-flush.d.ts +71 -0
  37. package/dist/agent/session-end-flush.js +233 -0
  38. package/dist/agent/streaming/streaming-handler.d.ts +7 -0
  39. package/dist/agent/streaming/streaming-handler.js +8 -1
  40. package/dist/agent/tool-executor.js +1 -0
  41. package/dist/agent/tool-handler.d.ts +5 -0
  42. package/dist/agent/tool-handler.js +130 -4
  43. package/dist/browser-automation/browser-operator-executor.js +15 -0
  44. package/dist/browser-automation/browser-use-runner.d.ts +96 -0
  45. package/dist/browser-automation/browser-use-runner.js +492 -0
  46. package/dist/browser-automation/camofox-runner.d.ts +107 -0
  47. package/dist/browser-automation/camofox-runner.js +287 -0
  48. package/dist/channels/dingtalk/index.js +3 -0
  49. package/dist/channels/discord/client.d.ts +9 -0
  50. package/dist/channels/discord/client.js +12 -0
  51. package/dist/channels/feishu/index.d.ts +167 -1
  52. package/dist/channels/feishu/index.js +432 -7
  53. package/dist/channels/gateway-lifecycle.d.ts +132 -0
  54. package/dist/channels/gateway-lifecycle.js +219 -0
  55. package/dist/channels/google-chat/index.d.ts +5 -1
  56. package/dist/channels/google-chat/index.js +5 -1
  57. package/dist/channels/imessage/index.d.ts +14 -0
  58. package/dist/channels/imessage/index.js +64 -17
  59. package/dist/channels/index.d.ts +4 -0
  60. package/dist/channels/index.js +4 -0
  61. package/dist/channels/irc/index.d.ts +73 -3
  62. package/dist/channels/irc/index.js +446 -11
  63. package/dist/channels/line/index.js +3 -0
  64. package/dist/channels/mattermost/index.d.ts +51 -4
  65. package/dist/channels/mattermost/index.js +303 -20
  66. package/dist/channels/nextcloud-talk/index.d.ts +89 -8
  67. package/dist/channels/nextcloud-talk/index.js +367 -16
  68. package/dist/channels/nostr/index.d.ts +121 -1
  69. package/dist/channels/nostr/index.js +396 -8
  70. package/dist/channels/ntfy/index.js +3 -0
  71. package/dist/channels/qq/index.js +3 -0
  72. package/dist/channels/slash-parity.d.ts +117 -0
  73. package/dist/channels/slash-parity.js +185 -0
  74. package/dist/channels/synology-chat/index.js +3 -0
  75. package/dist/channels/teams/index.d.ts +3 -0
  76. package/dist/channels/teams/index.js +3 -0
  77. package/dist/channels/telegram/client.d.ts +9 -0
  78. package/dist/channels/telegram/client.js +12 -0
  79. package/dist/channels/twilio-voice/index.js +3 -0
  80. package/dist/channels/webchat/index.d.ts +7 -1
  81. package/dist/channels/webchat/index.js +7 -1
  82. package/dist/channels/wecom/index.js +3 -0
  83. package/dist/channels/weixin/index.js +3 -0
  84. package/dist/channels/zalo/index.js +3 -0
  85. package/dist/cli/config-loader.js +1 -1
  86. package/dist/codebuddy/client.d.ts +30 -1
  87. package/dist/codebuddy/client.js +167 -20
  88. package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +5 -0
  89. package/dist/codebuddy/providers/provider-chatgpt-responses.js +1 -0
  90. package/dist/codebuddy/providers/provider-gemini-native.js +1 -1
  91. package/dist/codebuddy/providers/provider-openai-compat.d.ts +5 -0
  92. package/dist/codebuddy/providers/provider-openai-compat.js +82 -2
  93. package/dist/codebuddy/tool-definitions/computer-control-tools.js +32 -2
  94. package/dist/codebuddy/tool-definitions/core-tools.js +3 -3
  95. package/dist/codebuddy/tool-definitions/index.d.ts +2 -1
  96. package/dist/codebuddy/tool-definitions/index.js +2 -1
  97. package/dist/codebuddy/tool-definitions/multimodal-tools.d.ts +1 -0
  98. package/dist/codebuddy/tool-definitions/multimodal-tools.js +44 -1
  99. package/dist/codebuddy/tool-definitions/windows-tools.d.ts +2 -0
  100. package/dist/codebuddy/tool-definitions/windows-tools.js +34 -0
  101. package/dist/codebuddy/tools.d.ts +1 -1
  102. package/dist/codebuddy/tools.js +27 -24
  103. package/dist/commands/cli/hermes-commands.js +108 -0
  104. package/dist/commands/cli/native-engine-commands.js +107 -3
  105. package/dist/commands/cli/utility-commands.js +45 -0
  106. package/dist/commands/enhanced-command-handler.js +5 -0
  107. package/dist/commands/goal-cli.d.ts +71 -0
  108. package/dist/commands/goal-cli.js +280 -0
  109. package/dist/commands/handlers/goal-handler.d.ts +34 -0
  110. package/dist/commands/handlers/goal-handler.js +155 -0
  111. package/dist/commands/handlers/index.d.ts +1 -0
  112. package/dist/commands/handlers/index.js +2 -0
  113. package/dist/commands/handlers/infra-handlers.js +36 -0
  114. package/dist/commands/handlers/memory-handlers.js +96 -1
  115. package/dist/commands/headless-slash.d.ts +2 -0
  116. package/dist/commands/headless-slash.js +12 -0
  117. package/dist/commands/llm-provider-resolution.d.ts +5 -3
  118. package/dist/commands/llm-provider-resolution.js +87 -33
  119. package/dist/commands/ollama.d.ts +25 -0
  120. package/dist/commands/ollama.js +100 -0
  121. package/dist/commands/provider.d.ts +5 -0
  122. package/dist/commands/provider.js +106 -55
  123. package/dist/commands/slash/builtin-commands.js +20 -0
  124. package/dist/commands/spec-next.js +2 -1
  125. package/dist/commands/spec-plan.js +5 -16
  126. package/dist/commands/tunnel.d.ts +2 -0
  127. package/dist/commands/tunnel.js +48 -0
  128. package/dist/config/config-resolver.d.ts +2 -1
  129. package/dist/config/config-resolver.js +54 -41
  130. package/dist/config/constants.d.ts +28 -0
  131. package/dist/config/constants.js +7 -0
  132. package/dist/config/env-schema.js +770 -0
  133. package/dist/config/feature-flags.js +7 -0
  134. package/dist/config/model-tools.js +4 -4
  135. package/dist/config/toml-config.d.ts +16 -0
  136. package/dist/config/toml-config.js +3 -0
  137. package/dist/context/context-manager-v2.d.ts +39 -0
  138. package/dist/context/context-manager-v2.js +91 -0
  139. package/dist/daemon/agent-task-executor.js +12 -1
  140. package/dist/daemon/autonomous-daemon.d.ts +1 -1
  141. package/dist/daemon/autonomous-daemon.js +5 -3
  142. package/dist/daemon/autonomous-loop.d.ts +21 -1
  143. package/dist/daemon/autonomous-loop.js +69 -0
  144. package/dist/daemon/colab-goal.d.ts +38 -0
  145. package/dist/daemon/colab-goal.js +81 -0
  146. package/dist/daemon/cron-agent-bridge.d.ts +12 -3
  147. package/dist/daemon/cron-agent-bridge.js +25 -9
  148. package/dist/desktop/codebuddy-engine-adapter.d.ts +32 -0
  149. package/dist/desktop/codebuddy-engine-adapter.js +257 -80
  150. package/dist/desktop/engine-adapter.d.ts +14 -0
  151. package/dist/desktop-automation/automation-manager.js +16 -0
  152. package/dist/desktop-automation/omniparser-runner.d.ts +63 -0
  153. package/dist/desktop-automation/omniparser-runner.js +115 -0
  154. package/dist/desktop-automation/smart-snapshot.d.ts +11 -0
  155. package/dist/desktop-automation/smart-snapshot.js +72 -5
  156. package/dist/doctor/index.js +40 -23
  157. package/dist/events/types.d.ts +11 -0
  158. package/dist/fleet/colab-store.d.ts +21 -0
  159. package/dist/fleet/colab-store.js +28 -0
  160. package/dist/fleet/peer-session-bridge.d.ts +1 -1
  161. package/dist/fleet/peer-session-bridge.js +243 -2
  162. package/dist/fleet/peer-session-store.d.ts +3 -0
  163. package/dist/fleet/peer-tool-bridge.js +14 -0
  164. package/dist/fleet/privacy-lint.d.ts +8 -0
  165. package/dist/fleet/privacy-lint.js +22 -0
  166. package/dist/goals/goal-decomposer.d.ts +27 -0
  167. package/dist/goals/goal-decomposer.js +293 -0
  168. package/dist/goals/goal-judge-client.d.ts +15 -0
  169. package/dist/goals/goal-judge-client.js +45 -0
  170. package/dist/goals/goal-judge.d.ts +38 -0
  171. package/dist/goals/goal-judge.js +158 -0
  172. package/dist/goals/goal-loop.d.ts +37 -0
  173. package/dist/goals/goal-loop.js +101 -0
  174. package/dist/goals/goal-manager.d.ts +77 -0
  175. package/dist/goals/goal-manager.js +289 -0
  176. package/dist/goals/goal-state.d.ts +99 -0
  177. package/dist/goals/goal-state.js +315 -0
  178. package/dist/goals/goal-store.d.ts +28 -0
  179. package/dist/goals/goal-store.js +87 -0
  180. package/dist/goals/index.d.ts +5 -0
  181. package/dist/goals/index.js +6 -0
  182. package/dist/hooks/use-input-handler.js +36 -1
  183. package/dist/hooks/user-hooks.js +17 -3
  184. package/dist/index.js +506 -25
  185. package/dist/input/text-to-speech.d.ts +2 -6
  186. package/dist/input/text-to-speech.js +2 -27
  187. package/dist/integrations/screenpipe/screenpipe-client.d.ts +10 -0
  188. package/dist/integrations/screenpipe/screenpipe-client.js +12 -2
  189. package/dist/integrations/tailscale.d.ts +13 -0
  190. package/dist/integrations/tailscale.js +87 -34
  191. package/dist/knowledge/workspace-indexer.js +53 -9
  192. package/dist/mcp/client.js +1 -0
  193. package/dist/mcp/mcp-desktop-tools.d.ts +23 -0
  194. package/dist/mcp/mcp-desktop-tools.js +158 -0
  195. package/dist/mcp/mcp-server.js +2 -0
  196. package/dist/memory/enhanced-memory.d.ts +10 -0
  197. package/dist/memory/enhanced-memory.js +33 -2
  198. package/dist/memory/index.d.ts +3 -1
  199. package/dist/memory/index.js +4 -1
  200. package/dist/memory/memory-auto-proposer.d.ts +23 -0
  201. package/dist/memory/memory-auto-proposer.js +308 -0
  202. package/dist/memory/memory-candidate-queue.d.ts +92 -0
  203. package/dist/memory/memory-candidate-queue.js +261 -0
  204. package/dist/memory/persistent-memory.d.ts +45 -2
  205. package/dist/memory/persistent-memory.js +236 -40
  206. package/dist/ml/bayesian-qualifier.d.ts +1 -1
  207. package/dist/ml/bayesian-qualifier.js +35 -2
  208. package/dist/observability/run-store.d.ts +1 -1
  209. package/dist/openclaw/gateway-bridge.js +5 -0
  210. package/dist/plugins/marketplace.d.ts +1 -0
  211. package/dist/plugins/marketplace.js +7 -0
  212. package/dist/prompts/execution-discipline.d.ts +14 -0
  213. package/dist/prompts/execution-discipline.js +29 -0
  214. package/dist/prompts/variation-injector.js +9 -2
  215. package/dist/protocols/acp/acp-agentic-runner.d.ts +7 -6
  216. package/dist/protocols/acp/acp-agentic-runner.js +115 -18
  217. package/dist/protocols/acp/acp-session-store.d.ts +23 -0
  218. package/dist/protocols/acp/acp-session-store.js +77 -0
  219. package/dist/protocols/acp/acp-stdio-server.d.ts +7 -0
  220. package/dist/protocols/acp/acp-stdio-server.js +71 -16
  221. package/dist/providers/active-llm-registry.d.ts +37 -0
  222. package/dist/providers/active-llm-registry.js +186 -0
  223. package/dist/providers/auxiliary-provider.d.ts +25 -0
  224. package/dist/providers/auxiliary-provider.js +192 -0
  225. package/dist/providers/codex-oauth.d.ts +1 -1
  226. package/dist/providers/codex-oauth.js +27 -4
  227. package/dist/providers/index.d.ts +3 -0
  228. package/dist/providers/index.js +4 -0
  229. package/dist/providers/provider-catalog.d.ts +70 -0
  230. package/dist/providers/provider-catalog.js +738 -0
  231. package/dist/providers/provider-fallback.d.ts +35 -0
  232. package/dist/providers/provider-fallback.js +171 -0
  233. package/dist/providers/xai-oauth.d.ts +128 -0
  234. package/dist/providers/xai-oauth.js +735 -0
  235. package/dist/scheduler/cron-scheduler.d.ts +15 -3
  236. package/dist/scheduler/cron-scheduler.js +53 -7
  237. package/dist/scheduler/script-runner.d.ts +2 -0
  238. package/dist/scheduler/script-runner.js +19 -4
  239. package/dist/scheduler/watchdog-handlers.js +7 -20
  240. package/dist/search/usearch-index.js +7 -2
  241. package/dist/security/tool-policy/tool-groups.js +2 -0
  242. package/dist/server/index.js +17 -2
  243. package/dist/server/routes/mobile.d.ts +12 -4
  244. package/dist/server/routes/mobile.js +116 -1
  245. package/dist/server/tls-config.d.ts +35 -0
  246. package/dist/server/tls-config.js +142 -0
  247. package/dist/server/tunnel-manager.d.ts +20 -0
  248. package/dist/server/tunnel-manager.js +58 -0
  249. package/dist/server/websocket/fleet-bridge.d.ts +13 -1
  250. package/dist/server/websocket/fleet-bridge.js +16 -0
  251. package/dist/services/prompt-builder.d.ts +1 -0
  252. package/dist/services/prompt-builder.js +49 -8
  253. package/dist/shared/engine-types.d.ts +15 -1
  254. package/dist/sidecar/sidecar-bridge.d.ts +1 -0
  255. package/dist/sidecar/sidecar-bridge.js +21 -10
  256. package/dist/spec/spec-store.js +8 -1
  257. package/dist/talk-mode/providers/audioreader-tts.d.ts +1 -1
  258. package/dist/talk-mode/providers/audioreader-tts.js +8 -8
  259. package/dist/templates/project-scaffolding.js +3 -2
  260. package/dist/themes/theme-schema.d.ts +10 -10
  261. package/dist/tools/application-profiles.js +38 -0
  262. package/dist/tools/bash/bash-tool.d.ts +1 -0
  263. package/dist/tools/bash/bash-tool.js +16 -2
  264. package/dist/tools/bash/command-validator.js +3 -0
  265. package/dist/tools/bash/rtk-rewrite.d.ts +16 -0
  266. package/dist/tools/bash/rtk-rewrite.js +100 -0
  267. package/dist/tools/bash/streaming-executor.js +14 -1
  268. package/dist/tools/computer-control-tool.d.ts +16 -1
  269. package/dist/tools/computer-control-tool.js +317 -5
  270. package/dist/tools/document-generator.d.ts +14 -0
  271. package/dist/tools/document-generator.js +79 -0
  272. package/dist/tools/execute-code-rpc-invoker.d.ts +26 -2
  273. package/dist/tools/execute-code-rpc-invoker.js +46 -5
  274. package/dist/tools/metadata.js +28 -0
  275. package/dist/tools/office-macro-tool.d.ts +10 -0
  276. package/dist/tools/office-macro-tool.js +93 -0
  277. package/dist/tools/registry/index.d.ts +2 -1
  278. package/dist/tools/registry/index.js +2 -1
  279. package/dist/tools/registry/memory-tools.d.ts +20 -0
  280. package/dist/tools/registry/memory-tools.js +225 -3
  281. package/dist/tools/registry/multimodal-tools.js +2 -2
  282. package/dist/tools/registry/vision-tools.d.ts +33 -0
  283. package/dist/tools/registry/vision-tools.js +308 -6
  284. package/dist/tools/registry/windows-tools.d.ts +2 -0
  285. package/dist/tools/registry/windows-tools.js +47 -0
  286. package/dist/tools/route-peer-tool.js +14 -0
  287. package/dist/tools/screenshot-tool.js +14 -2
  288. package/dist/tools/text-to-speech-tool.d.ts +1 -1
  289. package/dist/tools/text-to-speech-tool.js +2 -12
  290. package/dist/tools/tool-selector.js +3 -59
  291. package/dist/utils/config-validation/schema.d.ts +40 -7
  292. package/dist/utils/config-validation/schema.js +130 -1
  293. package/dist/utils/cost-tracker.js +21 -1
  294. package/dist/utils/disk-guard.d.ts +180 -0
  295. package/dist/utils/disk-guard.js +404 -0
  296. package/dist/utils/interactive-setup.js +35 -2
  297. package/dist/utils/model-utils.js +11 -2
  298. package/dist/utils/output-sanitizer.js +11 -0
  299. package/dist/utils/provider-detector.d.ts +8 -7
  300. package/dist/utils/provider-detector.js +19 -77
  301. package/dist/utils/settings-manager.d.ts +7 -0
  302. package/dist/utils/token-display.d.ts +3 -3
  303. package/dist/utils/token-display.js +22 -4
  304. package/dist/wizard/onboarding.d.ts +39 -0
  305. package/dist/wizard/onboarding.js +433 -21
  306. package/dist/wizard/provider-onboarding.d.ts +8 -23
  307. package/dist/wizard/provider-onboarding.js +39 -158
  308. package/package.json +4 -2
  309. package/dist/plugins/provider-onboarding.d.ts +0 -23
  310. package/dist/plugins/provider-onboarding.js +0 -116
  311. package/dist/utils/kokoro-tts.d.ts +0 -8
  312. 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
@@ -0,0 +1,5 @@
1
+ export * from './goal-state.js';
2
+ export * from './goal-store.js';
3
+ export * from './goal-judge.js';
4
+ export * from './goal-manager.js';
5
+ export * from './goal-loop.js';
@@ -0,0 +1,6 @@
1
+ export * from './goal-state.js';
2
+ export * from './goal-store.js';
3
+ export * from './goal-judge.js';
4
+ export * from './goal-manager.js';
5
+ export * from './goal-loop.js';
6
+ //# sourceMappingURL=index.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,
@@ -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.kill('SIGTERM');
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.write(JSON.stringify({ event, ...context }));
210
- child.stdin.end();
224
+ child.stdin.end(JSON.stringify({ event, ...context }));
211
225
  }
212
226
  catch {
213
227
  // stdin may already be closed