Skip to content

feat(agents): add Grok CLI agent support - #183

Open
bsy wants to merge 7 commits into
kunchenguid:mainfrom
bsy:feat/grok-agent
Open

feat(agents): add Grok CLI agent support#183
bsy wants to merge 7 commits into
kunchenguid:mainfrom
bsy:feat/grok-agent

Conversation

@bsy

@bsy bsy commented Jul 30, 2026

Copy link
Copy Markdown

Intent

Add native Grok CLI agent support so gnhf can run overnight loops with xAI's grok harness

What Changed

  • Added a native grok agent (src/core/agents/grok.ts) wired through factory.ts, the AGENT_NAMES list, reserved-arg validation, and the bootstrapped config template/README agent table. It spawns the xAI grok CLI per iteration with -p --output-format streaming-json --json-schema, defaults to --always-approve, and reads structuredOutput plus token usage from the final end event (so --max-tokens stops after an iteration rather than mid-iteration).
  • Lifted two shared helpers out of the existing agents instead of copying them for grok: spawnAgentProcess in stream-utils.ts is now the single spawn path for claude/codex/copilot/pi/grok and owns Windows .cmd/.bat shim detection plus cmd.exe argv escaping, and parseAgentOutputJson in json-extract.ts replaces the duplicated schema-preferring parse-and-validate routine.
  • Added grok unit tests, factory/config coverage, json-extract and stream-utils tests, and an e2e suite driving a mock grok CLI fixture over the real wire format; refreshed AGENTS.md, CONTRIBUTING.md, skills/gnhf/SKILL.md, and the bug-report template accordingly.

Risk Assessment

⚠️ Medium: All four round-1 findings are genuinely fixed and the new shared spawn/parse boundaries are well-tested and behavior-preserving for the four refactored agents, but the Windows .cmd-shim path that the change set out to harden still silently truncates the multi-line prompt (and hands its remainder to cmd.exe) while the README continues to advertise shim support, which is worth resolving as a follow-up rather than blocking this merge.

Testing

Built the CLI, ran the targeted unit suites for the grok agent and the shared stream/JSON helpers the refactor touched (238 agent tests) plus the three grok e2e cases, then proved the intent end-to-end at the product level: the real xAI grok 0.2.114 CLI is installed and authenticated on this machine, so I ran gnhf's actual loop against it in a scratch repo and it produced a genuine code change committed on a gnhf/ branch (113K in / 829 out tokens reported, 1 good iteration). I also cross-checked every flag gnhf emits against the real grok --help so the wire format is validated against the actual harness rather than only the fixture, and manually exercised config-file agent selection with path/arg overrides, the --max-tokens guarantee that a finished iteration is committed instead of rolled back, and grok's presence in --help, the unknown-agent error, and the bootstrapped config. Evidence includes rendered screenshots of the live TUI and exit summary plus raw CLI transcripts, git logs, and debug-log events. Everything passed; no product or setup issues surfaced. The screenshots are VT replays of the captured terminal streams rendered through headless Chrome rather than photos of a live terminal, so the ASCII banner's box-drawing glyphs sit slightly apart in the browser font -- a replay artifact, not a product defect.

  • Evidence: Live TUI during a real grok turn (screenshot) (local file: /var/folders/92/rcfm4q4s51bcz30v1tw_vjl00000gn/T/no-mistakes-evidence/01KYSTS1VAGJKQVWGK8Z5HSVE6/gnhf-grok-REAL-live-tui.png)
  • Evidence: Exit summary after the real grok run (screenshot) (local file: /var/folders/92/rcfm4q4s51bcz30v1tw_vjl00000gn/T/no-mistakes-evidence/01KYSTS1VAGJKQVWGK8Z5HSVE6/gnhf-grok-REAL-exit-summary.png)
Evidence: Real grok CLI run: commit and diff gnhf produced
$ node dist/cli.mjs "Add a JSDoc comment to the addTodo function in todo.js. Keep it to one small change." --agent grok --max-iterations 1

x gnhf stopped
grok ran for 31s before: max iterations reached (1)

iterations 1 total 1 good 0 failed
tokens 113K in 829 out
branch diff 1 commit +6 -0
files 0 added 1 updated 0 deleted

$ git log --oneline --decorate
37f3986 (HEAD -> gnhf/add-a-jsdoc-comment-97493a) gnhf 1: Added a JSDoc comment to the addTodo function in todo.js documenting its purpose, parameters, and return value.
f6ce36e (main) init

$ git diff HEAD~1 HEAD
--- a/todo.js
+++ b/todo.js
@@ -1,3 +1,9 @@
+/**
+ * Adds a new incomplete todo item to the list.
+ * @param {Array<{text: string, done: boolean}>} list - Existing todos.
+ * @param {string} text - Text for the new todo.
+ * @returns {Array<{text: string, done: boolean}>} A new list with the todo appended.
+ */
export function addTodo(list, text) {
return [...list, { text, done: false }];
}
Evidence: Raw terminal transcript of the real grok run
�[?1049h�[?25l�[H                                                                            �[1m°�[0m  
                              �[2mg n h f�[0m  �[2m·�[0m  �[2mg r o k�[0m                          �[2m⋆�[0m   
                                                                         �[1m·�[0m     
                                                                               
            �[1m┏━╸┏━┓┏━┓╺┳┓   ┏┓╻╻┏━╸╻ ╻╺┳╸   ╻ ╻┏━┓╻ ╻┏━╸   ┏━╸╻ ╻┏┓╻�[0m      �[1m⋆�[0m     
 �[1m⋆�[0m          �[1m┃╺┓┃ ┃┃ ┃ ┃┃   ┃┗┫┃┃╺┓┣━┫ ┃    ┣━┫┣━┫┃┏┛┣╸    ┣╸ ┃ ┃┃┗┫�[0m            
            �[1m┗━┛┗━┛┗━┛╺┻┛   ╹ ╹╹┗━┛╹ ╹ ╹    ╹ ╹╹ ╹┗┛ ┗━╸   ╹  ┗━┛╹ ╹�[0m            
                                                                               
  �[2m⋆�[0m     �[2mAdd a JSDoc comment to the addTodo function in todo.js. Keep it�[0m        
                             �[2mto one small change.�[0m                             �[1m·�[0m
    �[1m·�[0m                                                                          
      �[2m✧�[0m                                                                        
                                                                               
     �[2m·�[0m             �[1m00:00:00�[0m  �[2m·�[0m  0 in  �[2m·�[0m  0 out  �[2m·�[0m  0 commits                �[1m·�[0m  
                                                                               
                                                                               
                                  �[2mworking...�[0m                                   
                                                                               
                                                                               
                                                                               
                                                                           �[1m·�[0m   
                                      🌓                                       
                     �[2m[ctrl+c to stop, gnhf again to resume]�[0m                     
                                                                                �[22;39H�[0m🌔�[9;3H�[0m�[1m⋆�[22;39H�[0m🌕�[22;39H�[0m🌖�[22;39H�[0m🌗�[14;27H�[0m�[1m1�[22;39H�[0m🌘�[22;39H�[0m🌑�[5;75H�[0m�[2m╱�[6;74H╱�[7;73H╱�[8;72H╱�[21;76H·�[22;39H�[0m🌒�[5;75H�[0m �[6;74H �[21;76H �[22;39H🌓�[7;73H�[0m �[21;76H�[0m�[2m·�[22;39H�[0m🌔�[8;72H�[0m �[14;27H�[0m�[1m2�[21;76H·�[22;39H�[0m🌕�[22;39H�[0m🌖�[22;39H�[0m🌗�[22;39H�[0m🌘�[22;39H�[0m🌑�[14;27H�[0m�[1m3�[22;39H�[0m🌒�[22;39H�[0m🌓�[22;39H�[0m🌔�[22;39H�[0m🌕�[22;39H�[0m🌖�[10;79H�[0m�[2m·�[14;27H�[0m�[1m4�[22;39H�[0m🌗�[10;79H�[0m �[22;39H🌘�[22;39H�[0m🌑�[10;79H�[0m�[2m·�[22;39H�[0m🌒�[10;79H�[0m�[1m·�[22;39H�[0m🌓�[14;27H�[0m�[1m5�[22;39H�[0m🌔�[22;39H�[0m🌕�[5;4H�[0m�[2m°�[22;39H�[0m🌖�[5;4H�[0m�[1m°�[22;39H�[0m🌗�[22;39H�[0m🌘�[5;4H�[0m�[2m°�[14;27H�[0m�[1m6�[22;39H�[0m🌑�[5;4H�[0m �[22;39H🌒�[22;39H�[0m🌓�[22;39H�[0m🌔�[22;39H�[0m🌕�[14;27H�[0m�[1m7�[22;39H�[0m🌖�[22;39H�[0m🌗�[14;6H�[0m�[1m·�[22;39H�[0m🌘�[14;6H�[0m�[2m·�[22;39H�[0m🌑�[22;39H�[0m🌒�[3;74H�[0m�[2m·�[12;7H�[0m�[1m✧�[14;27H8�[22;39H�[0m🌓�[3;74H�[0m �[22;39H🌔�[3;74H�[0m�[2m·�[12;7H✧�[22;39H�[0m🌕�[3;74H�[0m�[1m·�[22;39H�[0m🌖�[5;74H�[0m�[2m⋆�[11;5H·�[22;39H�[0m🌗�[5;74H�[0m �[11;5H �[14;27H�[0m�[1m9�[22;39H�[0m🌘�[5;74H�[0m�[2m⋆�[11;5H·�[22;39H�[0m🌑�[5;74H�[0m�[1m⋆�[11;5H·�[14;77H�[0m�[2m·�[22;39H�[0m🌒�[14;77H�[0m �[22;39H🌓�[14;77H�[0m�[2m·�[22;39H�[0m🌔�[14;26H�[0m�[1m10�[14;77H·�[22;39H�[0m🌕�[22;39H�[0m🌖�[22;39H�[0m🌗�[22;39H�[0m🌘�[22;39H�[0m🌑�[14;27H�[0m�[1m1�[22;39H�[0m🌒�[22;39H�[0m🌓�[22;39H�[0m🌔�[22;39H�[0m🌕�[22;39H�[0m🌖�[14;27H�[0m�[1m2�[22;39H�[0m🌗�[22;39H�[0m🌘�[22;39H�[0m🌑�[21;76H�[0m�[2m·�[22;39H�[0m🌒�[21;76H�[0m �[22;39H🌓�[14;27H�[0m�[1m3�[21;76H�[0m�[2m·�[22;39H�[0m🌔�[21;76H�[0m�[1m·�[22;39H�[0m🌕�[1;77H�[0m�[2m°�[22;39H�[0m🌖�[1;77H�[0m �[22;39H🌗�[1;77H�[0m�[2m°�[22;39H�[0m🌘�[14;27H�[0m�[1m4�[22;39H�[0m🌑�[1;77H�[0m�[1m°�[22;39H�[0m🌒�[9;3H�[0m�[2m⋆�[22;39H�[0m🌓�[22;39H�[0m🌔�[9;3H�[0m �[22;39H🌕�[9;3H�[0m�[2m⋆�[14;27H�[0m�[1m5�[22;39H�[0m🌖�[9;3H�[0m�[1m⋆�[22;39H�[0m🌗�[22;39H�[0m🌘�[22;39H�[0m🌑�[22;39H�[0m🌒�[14;27H�[0m�[1m6�[22;39H�[0m🌓�[22;39H�[0m🌔�[22;39H�[0m🌕�[22;39H�[0m🌖�[22;39H�[0m🌗�[2;76H�[0m�[1m⋆�[14;27H7�[22;39H�[0m🌘�[22;39H�[0m🌑�[2;76H�[0m�[2m⋆�[22;39H�[0m🌒�[22;39H�[0m🌓�[22;39H�[0m🌔�[10;4H�[0m�[2m╱�[11;3H╱�[12;2H╱�[13;1H�[0m�[1m╱�[14;27H8�[22;39H�[0m🌕�[10;4H�[0m �[11;3H �[13;1H�[0m�[2m╱�[22;39H�[0m🌖�[12;2H�[0m �[22;39H🌗�[13;1H�[0m �[14;6H�[0m�[1m·�[22;39H�[0m🌘�[14;6H�[0m�[2m·�[22;39H�[0m🌑�[14;27H�[0m�[1m9�[22;39H�[0m🌒�[22;39H�[0m🌓�[3;74H�[0m �[22;39H🌔�[3;74H�[0m�[2m·�[22;39H�[0m🌕�[3;74H�[0m�[1m·�[6;2H�[0m�[2m⋆�[22;39H�[0m🌖�[14;26H�[0m�[1m20�[22;39H�[0m🌗�[6;2H�[0m �[22;39H🌘�[22;39H�[0m🌑�[5;74H�[0m�[2m⋆�[6;2H⋆�[22;39H�[0m🌒�[22;39H�[0m🌓�[6;2H�[0m�[1m⋆�[14;27H1�[17;35H�[0m     �[0m�[2m{�[0m    �[22;39H🌔�[5;4H�[0m�[2m°�[5;74H�[0m�[1m⋆�[11;5H�[0m�[2m·�[17;11H{ "success": true, "summary": "Added a JSDoc comment to the�[18;11HaddTodo function in todo.js.", "key_changes_made": ["Added�[22;39H�[0m🌕�[11;5H�[0m �[14;77H�[0m�[2m·�[18;10Had�[18;13HTodo function in todo.js.", "key_changes_made": ["Ad�[18;66Hed a�[19;12HJSDoc block documenting addTodo's parameters (list, text)�[22;39H�[0m🌖�[5;4H�[0m�[1m°�[10;79H�[0m�[2m·�[11;5H·�[17;11Hsummary": �[17;22HAdded a�[17;30HJSDoc comment to the addTod�[17;58H fu�[17;62Hcti�[17;66Hn in�[18;10H�[0m    �[0m�[2mtodo.js.", "key_change�[18;37H_made": ["Added a JSDoc�[18;61Hblock�[0m    �[19;9H�[0m�[2mdocumenting addT�[19;26Hdo's parameters (list, tex�[19;53H) and re�[19;62Hurn value…�[22;39H�[0m🌗�[5;4H�[0m�[2m°�[10;79H�[0m �[11;5H�[0m�[1m·�[17;11H�[0m  �[0m�[2min todo.js.", "key_changes_made":�[17;47H["Adde�[17;54H a JSDoc bl�[17;66Hck�[0m  �[18;9H�[0m�[2mdocumenting addTodo's p�[18;33Hrameters (list, t�[18;51Hxt)�[18;55Hand return value�[19;9H�[0m  �[0m�[2m(new �[19;17Hodo�[19;22Hrray with the i�[19;39Hm appended as incompl�[19;62He)."],…�[0m   �[22;39H🌘�[5;4H�[0m �[14;27H�[0m�[1m2�[17;11H�[0m�[2mDoc bl�[17;18Hck documenting addTodo's parameters (list, text) and�[18;9H�[0m    �[0m�[2mr�[18;15Htur�[18;19H v�[18;22Hlue (new�[18;31Htodo array�[18;42Hwith the i�[18;53Hem appended as�[0m    �[19;9H�[0m�[2mincomplete)."], "key_learnings": ["to�[19;47Ho.js is a �[19;58Hinimal module…�[22;39H�[0m🌑�[10;79H�[0m�[2m·�[14;77H�[0m�[1m·�[17;10H�[0m�[2mt) and return value (new todo array with the item appended as�[18;9Hincomplete)."], "key_learnings":�[18;42H["�[18;45Hodo.js is a minimal�[18;65Hmodule�[19;9Hwith a single exported �[19;33HddTodo func�[19;45Hion and no ex�[19;59Hsting�[19;65HJSDoc;�[22;39H�[0m🌒�[10;79H�[0m�[1m·�[17;10H�[0m�[2my with�[17;17Hthe item append�[17;33Hd�[17;35Has incomplete)."], "key_learnings":�[0m �[18;9H �[0m�[2m["todo.js is a�[18;25Hminimal module with a single exported addTodo�[0m �[19;9H �[0m�[2mfunction and�[19;23Hno existing JSD�[19;39Hc; the functio�[19;54H appends {�[19;65Htext,…�[0m �[22;39H🌓�[17;9H�[0m�[2mtem append�[17;20Hd as incompl�[17;33Hte)."], "key_l�[17;48Harnings": ["todo.js is a�[18;10Hminimal module�[18;25Hw�[18;27Hth �[18;31H single exported addTodo function and no�[19;9Hexisting JSDoc; the function appends�[19;46H{ text, done: false } via…�[22;39H�[0m🌔�[22;39H�[0m🌕�[14;27H�[0m�[1m3�[22;39H�[0m🌖�[22;39H�[0m🌗�[22;39H�[0m🌘�[21;76H�[0m�[2m·�[22;39H�[0m🌑�[21;76H�[0m �[22;39H🌒�[14;27H�[0m�[1m4�[21;76H�[0m�[2m·�[22;39H�[0m🌓�[21;76H�[0m�[1m·�[22;39H�[0m🌔�[22;39H�[0m🌕�[22;39H�[0m🌖�[22;39H�[0m🌗�[14;27H�[0m�[1m5�[22;39H�[0m🌘�[12;7H�[0m�[1m✧�[22;39H�[0m🌑�[8;76H�[0m�[2m╱�[9;75H╱�[10;74H╱�[11;73H╱�[12;72H�[0m�[1m╱�[22;39H�[0m🌒�[8;76H�[0m �[12;72H�[0m�[2m╱�[22;39H�[0m🌓�[9;75H�[0m �[10;74H �[12;7H�[0m�[2m✧�[22;39H�[0m🌔�[11;73H�[0m �[12;72H �[14;27H�[0m�[1m6�[22;39H�[0m🌕�[22;39H�[0m🌖�[22;39H�[0m🌗�[22;39H�[0m🌘�[22;39H�[0m🌑�[14;27H�[0m�[1m7�[22;39H�[0m🌒�[22;39H�[0m🌓�[17;9H�[0m�[2mplete)."], "key_learnings": ["todo.js is a minimal m�[17;63Hule w�[17;69Hth�[0m �[18;10H�[0m�[2ma s�[18;14Hng�[18;17He exported addTodo functi�[18;43Hn and no existi�[18;59Hg JSDoc; the�[19;9H�[0m   �[0m�[2mfunction appends�[19;29H{ text, done: false } via spread."] }{…�[0m    �[22;39H🌔�[17;9H�[0m�[2mdo function and no ex�[17;31Hsting�[17;37HJSD�[17;41Hc; the funct�[17;54Hon �[17;58Hppen�[17;63Hs {�[17;67Htext,�[18;10H�[0m               �[0m�[2mdone: false } via spread."] }{�[0m                �[19;12H                    �[0m�[2m"success"�[19;43Htrue,…�[0m                   �[22;39H🌕�[17;9H�[0m               �[0m�[2mt, done: false } via spread."] }{�[0m               �[18;25H       �[0m�[2m"success":�[18;43Htru�[18;47H,�[0m       �[19;10H�[0m�[2m"summary": "Added a JSDoc �[19;37Homment to the addTodo function in…�[22;39H�[0m🌖�[14;27H�[0m�[1m8�[17;14H�[0m�[2med a JSDoc comment�[17;33Hto the addTodo function in todo.js�[18;12Hdocumenting its purpo�[18;34He, parame�[18;44Hers�[18;48H and return value.",�[19;10H�[0m                   �[0m�[2m"key_�[19;35Hhanges_made": […�[0m                    �[22;39H🌗�[17;12H�[0m�[2mdocumenting its purpose, par�[17;41Hmeters, a�[17;51Hd retur�[17;60Hvalue.",�[18;12H�[0m                  �[0m�[2m"key_changes_made": [�[0m                 �[19;10H�[0m�[2m"Added a JSDoc block above addTodo describing the function,…�[22;39H�[0m🌘�[17;12H�[0m                            �[0m�[2m[�[0m                           �[18;11H�[0m�[2m"Added a JSDoc block above addTodo describing the function,�[19;10H�[0m   �[0m�[2m@par�[19;18Hm list�[19;25Hand text, and @returns a new list with �[19;65Hhe…�[0m  �[22;39H🌑�[17;12H�[0m�[2m, @param list and text, and @returns a new list with the�[18;11H�[0m                �[0m�[2min�[18;30Homplet�[18;38Hto�[18;41Ho appended."�[0m                 �[19;13H                          �[0m�[2m],…�[0m                          �[22;39H🌒�[17;12H�[0m             �[0m�[2mthe incomplete�[17;40Htodo appended."�[0m             �[18;27H            �[0m�[2m],�[0m            �[19;31H�[0m�[2m"key_learnings": […�[22;39H�[0m🌓�[14;27H�[0m�[1m9�[17;25H�[0m        �[0m�[2my_�[17;37Harnings": [�[0m       �[18;9H�[0m�[2m"todo.js is a single-export module with no package.json, tests,�[19;12Hor linters; validation is lim�[19;42Hted to reviewing the file…�[22;39H�[0m🌔�[11;6H�[0m�[2m╱�[12;5H╱�[13;4H╱�[14;3H╱�[15;2H�[0m�[1m╱�[17;14H�[0m�[2mxport module with no package.json, tests, or linters;�[18;9H�[0m    �[0m�[2mvalidation �[18;25Hs �[18;28Himited �[18;36Ho revi�[18;43Hwing the�[18;52Hfile content.",�[0m     �[19;10H�[0m�[2m"addTodo is a�[19;24Hpure function�[19;38Hthat spreads l�[19;53Hst and appends {…�[22;39H�[0m🌕�[1;77H�[0m�[2m°�[9;3H⋆�[11;6H�[0m �[15;2H�[0m�[2m╱�[16;1H�[0m�[1m╱�[22;39H�[0m🌖�[1;77H�[0m �[9;3H �[12;5H �[13;4H �[16;1H�[0m�[2m╱�[22;39H�[0m🌗�[9;3H�[0m�[2m⋆�[14;3H�[0m �[14;6H�[0m�[1m·�[15;2H�[0m �[22;39H🌘�[1;77H�[0m�[2m°�[9;3H�[0m�[1m⋆�[14;6H�[0m�[2m·�[14;26H�[0m�[1m30�[16;1H�[0m �[22;39H🌑�[1;77H�[0m�[1m°�[22;39H�[0m🌒�[22;39H�[0m🌓�[3;74H�[0m�[2m·�[22;39H�[0m🌕�[3;74H�[0m �[22;39H🌖�[3;74H�[0m�[2m·�[14;27H�[0m�[1m1�[22;39H�[0m🌗�[?25h�[?1049l
╭────────────────────────────────────────────────────────────╮
│ × gnhf stopped                                             │
│   grok ran for 31s before: max iterations reached (1)      │
╰────────────────────────────────────────────────────────────╯

  iterations      1 total       1 good       0 failed
  tokens          113K in       829 out      
  branch diff     1 commit      +6           -0
  files           0 added       1 updated    0 deleted

  notes           /private/tmp/gnhf-grok-real-BaIL/todo-app/.gnhf/runs/add-a-jsdoc-comment-97493a/notes.md
  debug log       /private/tmp/gnhf-grok-real-BaIL/todo-app/.gnhf/runs/add-a-jsdoc-comment-97493a/gnhf.log

  next steps      git log --oneline f6ce36ea97b5..HEAD
                  git diff --stat f6ce36ea97b5..HEAD
                  gh pr create

  too much        git push no-mistakes:
  to review?      https://github.com/kunchenguid/no-mistakes
Evidence: gnhf argv vs the real `grok --help` (grok 0.2.114)
### argv gnhf builds for the grok CLI (captured from a real gnhf iteration)

argv[0] -p
argv[1] You are working autonomously towards an objective given below... [2133 chars]
argv[2] --output-format
argv[3] streaming-json
argv[4] --json-schema
argv[5] {"type":"object","additionalProperties":false,...} [307 chars]
argv[6] --always-approve

### the same flags in `grok --help` (grok 0.2.114, installed locally)

-p, --single <PROMPT> Single-turn prompt. Prints the response to stdout and exits
--output-format <FORMAT> [default: plain] [possible values: plain, json, streaming-json]
--json-schema <SCHEMA> JSON Schema for structured output
--always-approve Auto-approve all tool executions
--permission-mode <MODE> Permission mode
--allow <RULE> Permission allow rule
--deny <RULE> Permission deny rule
--disallowed-tools <TOOLS> Built-in tools to remove
--tools <TOOLS> Built-in tools to allow
- Evidence: Live TUI during a fixture-driven grok run (screenshot) (local file: /var/folders/92/rcfm4q4s51bcz30v1tw_vjl00000gn/T/no-mistakes-evidence/01KYSTS1VAGJKQVWGK8Z5HSVE6/gnhf-grok-live-tui.png) - Evidence: Exit summary after a 2-iteration fixture run (screenshot) (local file: /var/folders/92/rcfm4q4s51bcz30v1tw_vjl00000gn/T/no-mistakes-evidence/01KYSTS1VAGJKQVWGK8Z5HSVE6/gnhf-grok-exit-summary.png)
Evidence: Fixture run: git log, README, notes.md, gnhf.log agent events
### git log (branch created by gnhf)
e7d5c3e (HEAD -> gnhf/add-one-bullet-to-th-df55f2) gnhf 2: appended a mock grok change to README.md
0834360 gnhf 1: appended a mock grok change to README.md
337f3a8 (main) init

### README.md after the loop
# demo project

A tiny repo used to demo gnhf running an overnight loop.
- grok change 7-9042
- grok change 7-9071

### notes.md (iteration history gnhf feeds back into the next prompt)
# gnhf run: add-one-bullet-to-th-df55f2

Objective: see .gnhf/runs/add-one-bullet-to-th-df55f2/prompt.md

## Iteration Log

### Iteration 1

**Summary:** appended a mock grok change to README.md

**Changes:**
- README.md: appended one line

**Learnings:**
- the mock grok CLI streams events as JSONL

### Iteration 2

**Summary:** appended a mock grok change to README.md

**Changes:**
- README.md: appended one line

**Learnings:**
- the mock grok CLI streams events as JSONL

### gnhf.log lifecycle events for the grok agent
{"event": "run:start", "agent": "grok"}
{"event": "iteration:start", "iteration": 1}
{"event": "agent:run:start", "iteration": 1, "agent": "grok"}
{"event": "agent:run:end", "iteration": 1, "success": true}
{"event": "iteration:end", "iteration": 1, "success": true}
{"event": "iteration:start", "iteration": 2}
{"event": "agent:run:start", "iteration": 2, "agent": "grok"}
{"event": "agent:run:end", "iteration": 2, "success": true}
{"event": "iteration:end", "iteration": 2, "success": true}
{"event": "run:complete"}
Evidence: --max-tokens stops after the iteration and keeps the commit
$ gnhf "keep improving the readme" --agent grok --max-iterations 5 --max-tokens 100

x gnhf stopped
grok ran for 0s before: max tokens reached (2100/100)

iterations 1 total 1 good 0 failed
tokens 2K in 600 out
branch diff 1 commit +1 -0

### commits kept on the branch
c2c32f9 gnhf 1: appended a mock grok change to README.md
c6c96ca init

### grok was invoked exactly once (no wasted extra turn)
1

### no abort was issued mid-iteration
0
Evidence: Selecting grok via ~/.gnhf/config.yml with path and arg overrides
### ~/.gnhf/config.yml (no --agent flag passed)
agent: grok
agentPathOverride:
grok: <repo>/e2e/fixtures/grok
agentArgsOverride:
grok:
- -m
- grok-4.5-build

### resulting grok argv (agentArgsOverride lands before gnhf's own flags)
-m grok-4.5-build -p You are working autonomously towards ... --output-format streaming-json --json-schema {"type":"object",...} --always-approve

### exit summary
x gnhf stopped
grok ran for 0s before: max iterations reached (1)

### commit produced
fb07e49 gnhf 1: appended a mock grok change to README.md
bf87097 init
Evidence: grok discoverability in --help, error text, and bootstrapped config
### gnhf --help
--agent <agent> Agent to use (claude, codex, rovodev, opencode,
copilot, pi, grok, or acp:<target-or-command>)

### unknown agent name -> error lists grok
Unknown agent: grokk. Use "claude", "codex", "rovodev", "opencode", "copilot", "pi", or "grok", or "acp:<target-or-command>" (e.g. acp:gemini).

### bootstrapped ~/.gnhf/config.yml offers grok in its override examples
# grok: /path/to/custom-grok
# grok:
# - -m
# - grok-4.5-build

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 3 issues (1 warning, 2 infos)
  • 🚨 e2e/e2e-grok.test.ts:155 - The three new grok e2e tests run on every CI OS, but the grok spawn path cannot work on Windows. createTestEnv puts e2e/fixtures (containing both an extensionless grok sh script and grok.cmd) on PATH, so shouldUseWindowsShell(&#34;grok&#34;, &#34;win32&#34;) shells out to where grok. If it resolves the extensionless file first, shell is false and spawn(&#34;grok&#34;) fails ENOENT (libuv only appends .com/.exe for extensionless names). If it resolves grok.cmd, shell: true is used and Node's normalizeSpawnArguments joins [file, ...args] with plain spaces and no quoting - I confirmed this locally - so the multi-line -p iteration prompt and the --json-schema JSON blob are split into separate tokens, breaking expect(invocation.prompt).toContain(...) and JSON.parse(invocation.schema). The existing opencode e2e passes on Windows only because all its argv tokens are single words and the prompt travels over HTTP. Sibling suites already gate this class of test with it.skipIf(process.platform === &#34;win32&#34;) (e2e/e2e.test.ts:456, e2e/e2e-acp.test.ts:206). AGENTS.md/CONTRIBUTING require all three CI OS jobs green. Fix: gate the three grok e2e tests on win32 like the siblings.
  • ⚠️ src/core/agents/grok.ts:241 - Product-level counterpart of the e2e issue: on Windows, an npm-installed grok.cmd shim makes shouldUseWindowsShell return true, and Node then builds the command line as [bin, ...args].join(&#34; &#34;) with no per-arg quoting. The concrete path is: Windows user installs the grok CLI as a .cmd shim -> buildGrokArgs emits -p &lt;multi-line iteration prompt&gt; --output-format streaming-json --json-schema {&#34;type&#34;:&#34;object&#34;,...} -> cmd.exe receives that unquoted, so grok gets -p with only the prompt's first word and a shredded schema argument. The invariant that argv tokens survive the spawn boundary is violated. This is pre-existing and equally reachable for claude/codex/copilot/pi, and this change only moved the helper into stream-utils.ts rather than introducing it, so it is not a regression - but grok is a newly reachable instance of it. The earliest shared boundary for a durable fix is the shouldUseWindowsShell + spawn call sites in stream-utils.ts: resolve the shim path and spawn cmd.exe /d /s /c with explicit quoting plus windowsVerbatimArguments, rather than relying on Node's unquoted shell join. Flagging for the author's decision since fixing it changes behavior for all five spawn agents.
  • ℹ️ src/core/agents/grok.ts:186 - parseGrokOutput gates on end.structuredOutput !== undefined, so an end event that explicitly carries &#34;structuredOutput&#34;: null (a common JSON wire convention for "no structured answer") is treated as present and sent straight to validateGrokOutput, which throws Invalid grok output: .... That skips the streamed-text recovery path the function was written for, so a grok build that emits an explicit null instead of omitting the field would fail every iteration despite printing a valid JSON answer as prose. Widening the guard to end.structuredOutput != null costs nothing and keeps the documented fallback reachable.
  • ℹ️ src/core/agents/grok.ts:181 - parseGrokOutput plus its matchesSchema helper is the third near-verbatim copy of the schema-preferring parse-and-validate routine, after parseCopilotOutput (src/core/agents/copilot.ts:148) and parseOpenCodeOutput (src/core/agents/opencode.ts:216); rovodev has a fourth variant at src/core/agents/rovodev.ts:749. All four do: parseAgentJson(text, schema-accepts) -> validate, else parseAgentJson(text) -> validate, else throw &#34;&lt;agent&gt; output did not contain a parseable JSON object&#34;. AGENTS.md already names json-extract.ts as the home for this ("use it before adding ad-hoc parsing"). Non-functional dedup: lift a parseAndValidateAgentOutput(text, schema, agentName) helper there and have the four agents call it.

🔧 Fix: harden windows agent argv escaping and share output parsing
3 issues (1 warning, 2 infos) still open:

  • ⚠️ src/core/agents/stream-utils.ts:97 - The Windows escaping is correct for spaces, quotes and operators, but it does not make the original failure unreachable: \r/\n cannot be caret-escaped for cmd.exe (a caret before a newline is line-continuation, which deletes it), and cmd terminates the /c command at an embedded LF. Concrete path: Windows user has claude/codex/copilot/grok installed as an npm .cmd shim -> shouldUseWindowsShell returns true -> spawnAgentProcess escapes and hands cmd /d /s /c a command line containing the always-multi-line iteration prompt from src/templates/iteration-prompt.ts -> the agent receives only the prompt's first line as -p, and the remaining lines are parsed by cmd.exe as further commands (prompt and notes text is attacker-influenceable in a loop that runs unattended overnight). Rust's std made exactly this call for batch files, rejecting args containing a newline as unescapable. pi is unaffected because it sends the prompt over stdin. The e2e suite documents the limitation (e2e/e2e-grok.test.ts:155 comment: "cannot carry the multi-line iteration prompt as a single argv token no matter how it is quoted"), but the product code neither rejects nor works around it, and README.md:315 still states "On Windows, .cmd and .bat wrappers are supported". Earliest shared boundary is spawnAgentProcess itself: refuse the cmd.exe route when any arg contains \r/\n - either resolve the shim's real target and spawn it directly, or carry the prompt via stdin/a prompt file the way pi already does - or at minimum fail fast with a clear error instead of silently truncating. Flagging for the author since the options differ in user-visible behavior and this is a pre-existing limitation the change improved rather than introduced.
  • ℹ️ src/core/agents/stream-utils.ts:73 - escapeWindowsShellArgument reproduces cross-spawn's escapeArgument (same metachar regex /([()\][%!^&#34;<>&|;, ?])/g, same backslash/quote doubling) but omits its needsDoubleEscapeMetaCharsstep, which cross-spawn applies precisely when the target matchesnode_modules/.bin/.cmd- the caseshouldUseWindowsShell&#39;s own doc comment names (&#34;npm installs some agent CLIs as .cmd/.batshims&#34;). Mechanism: the shim re-expands%*intonode "<cli>.js" <args>, so cmd parses the argument text a second time; cmd&#39;s quote tracking toggles on every "and does not understand the&feat(agents): add Jules cloud agent with REST API client #34;escapes we emit, so roughly half of a quote-dense argument&#39;s segments are outside quotes on that second pass, and any&, |, <, >, (, )landing there is interpreted rather than passed through. The built--json-schemapayload happens to be safe (buildAgentOutputSchema emits no prose descriptions, and,/[/]/{/}are not cmd operators), so this is latent rather than always-hit today, but it becomes the live failure as soon as the newline issue above is addressed. Adding the secondreplace(WINDOWS_SHELL_META_CHARS, "^$1")` pass for shim targets restores parity with the reference algorithm.
  • ℹ️ src/core/agents/rovodev.ts:313 - stream-utils.ts:15 now states shouldUseWindowsShell is "kept module-private so every caller goes through spawnAgentProcess, which is the only place that knows how to keep argv intact once a shell is in play", but rovodev.ts:73 still holds a verbatim private copy of that function and rovodev.ts:313 spawns with shell: shouldUseWindowsShell(this.bin, this.platform) and unescaped argv - including ...(this.extraArgs ?? []) from agentArgsOverride.rovodev. So a rovodev override value containing spaces or metachars against a .cmd-shimmed acli is still split apart, and the two copies of the resolution heuristic can now drift. Route rovodev's server spawn through spawnAgentProcess and delete the local copy (rovodev.ts:103 terminateRovoDevProcess is likewise a copy of terminateChildProcess). Note opencode.ts:471 deliberately uses a different policy (shell: isWindows unconditionally) that its Windows e2e depends on, so it should not be folded in without separate thought.
✅ **Test** - passed

✅ No issues found.

  • npx vitest run src/core/agents/ (238 tests: grok.test.ts, factory.test.ts, json-extract.test.ts, stream-utils.test.ts, plus claude/codex/copilot/pi/rovodev/opencode tests affected by the shared-helper refactor)
  • npx vitest run e2e/e2e-grok.test.ts (wire format + commit, --max-tokens keeps the finished commit, text-only JSON recovery)
  • npx vitest run src/core/config.test.ts src/core/agents/factory.test.ts (grok config/factory plumbing)
  • pnpm run build (required before e2e, which drives dist/cli.mjs)
  • Manual end-to-end with the REAL grok CLI: node dist/cli.mjs &#34;Add a JSDoc comment to the addTodo function in todo.js...&#34; --agent grok --max-iterations 1 in a scratch git repo -> real grok turn, real file edit, real gnhf commit
  • Manual wire-format cross-check: captured argv gnhf builds vs grok --help (grok 0.2.114) for -p, --output-format streaming-json, --json-schema, --always-approve, and the --permission-mode/--allow/--deny/--disallowed-tools/--tools allowlist
  • Manual multi-iteration run against the e2e grok fixture: node dist/cli.mjs &#34;add one bullet to the README&#34; --agent grok --max-iterations 2 -> 2 commits, notes.md history, gnhf.log agent lifecycle events
  • Manual config-file path: ~/.gnhf/config.yml with agent: grok + agentPathOverride.grok + agentArgsOverride.grok and no --agent flag -> grok selected, overrides land before gnhf's own flags
  • Manual budget path: --agent grok --max-iterations 5 --max-tokens 100 -> stops after the iteration with the commit kept, grok invoked exactly once, no agent:run:aborted event
  • Manual discoverability: gnhf --help, gnhf x --agent grokk error text, and the bootstrapped ~/.gnhf/config.yml override examples
  • Terminal-stream replay + headless Chrome screenshots of the live TUI and exit summary for both the real-grok run and the fixture run
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant