feat(guides): Add Goose CLI coding agent guide - #14
jamesmurdza wants to merge 3 commits into
Conversation
Adds typescript/goose/goose-cli, a headless coding agent powered by Block's Goose CLI running in a Daytona sandbox. Follows the gemini-cli pattern: a single PTY for the whole session, minimal inline JSON-event parsing (no separate parser module), and boolean-flag session resume (Goose's --resume just continues the most recent session, no ID to track). Also handles Goose's quirk of wrapping provider/model errors as assistant text instead of a distinct error event. Signed-off-by: James Murdza <jamesmurdza@users.noreply.github.com>
The install script's trailing \`goose configure\` step passes a \`[ -r /dev/tty ]\` check in the sandbox's exec environment even though there's no real controlling terminal attached, so the subsequent read fails with "No such device or address" and the install command exits non-zero. Pass CONFIGURE=false to skip that step, as documented by the installer itself. Signed-off-by: James Murdza <jamesmurdza@users.noreply.github.com>
Mirrors amp-sdk's approach for background servers, adapted to Goose's native --system flag instead of Amp's fake-first-message trick: - Send a Daytona-aware system prompt via `--system` on the first turn only (Goose carries it forward across --resume), telling Goose the sandbox's preview URL pattern and to write server-start commands to /home/daytona/start.sh instead of running them directly. - This matters because `goose run` blocks until the command exits, so a foreground dev server started inside a turn would hang that turn (and the whole prompt loop) forever. - After each turn, check for start.sh and, if present, run it in a separate async Daytona session so the server runs in the background while the conversation continues. Server sessions are cleaned up alongside the sandbox on exit. Signed-off-by: James Murdza <jamesmurdza@users.noreply.github.com>
|
All contributors have signed the CLA. ✅ Thank you! |
There was a problem hiding this comment.
9 issues found and verified against the latest diff
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="typescript/goose/goose-cli/src/utils.ts">
<violation number="1" location="typescript/goose/goose-cli/src/utils.ts:23">
P3: Inline code spans are processed last, so markdown markers inside them get styled instead of printed literally. The code regex runs after the bold/italic replacements (e.g. `a * b * c` inside backticks gets italic injected mid-span, `**kwargs**` gets bolded). Escape or protect code spans before applying the bold/italic rules, or match code first and skip styled spans.</violation>
</file>
<file name="typescript/goose/goose-cli/src/types.ts">
<violation number="1" location="typescript/goose/goose-cli/src/types.ts:18">
P2: Goose emits `toolRequest`/`toolResponse` blocks, not `tool_use`/`tool_result`. Update the event model and `handleEvent` to parse Goose's actual `toolCall`/`toolResult` shape; otherwise the guide does not render the tool activity and errors promised in its README.</violation>
</file>
<file name="typescript/goose/goose-cli/src/session.ts">
<violation number="1" location="typescript/goose/goose-cli/src/session.ts:78">
P2: When Goose emits a failed tool result in a user-role message, this guard returns before the `[tool error]` branch, so tool failures disappear from the displayed session. Allow user messages containing tool-result blocks through the event handling.</violation>
<violation number="2" location="typescript/goose/goose-cli/src/session.ts:82">
P2: Assistant output is not streamed for responses without tool events: `pendingText` waits until `complete` before being rendered. Render safe text incrementally, or flush bounded chunks while retaining only the markdown fragment needed for the next chunk.</violation>
<violation number="3" location="typescript/goose/goose-cli/src/session.ts:169">
P1: When Goose completes before `sendInput` returns, the completion event runs before `onResponseComplete` is assigned, so `processPrompt` hangs indefinitely. Create and assign the completion promise before sending the command.</violation>
</file>
<file name="typescript/goose/goose-cli/src/index.ts">
<violation number="1" location="typescript/goose/goose-cli/src/index.ts:82">
P2: When the installer download fails, `bash` exits successfully with no script, so this check passes and the first `goose` command can hang the prompt loop waiting for completion. Download to a file with `&&` or enable `pipefail` before accepting the install result.</violation>
<violation number="2" location="typescript/goose/goose-cli/src/index.ts:108">
P1: After Goose creates `start.sh` once, this existence check stays true, so every later prompt starts another copy of the same server. Track or consume the processed script before checking the next turn.</violation>
<violation number="3" location="typescript/goose/goose-cli/src/index.ts:108">
P2: After the first server turn, every later prompt reruns the stale `/home/daytona/start.sh` because the script is never consumed. Move or remove the script atomically before launching it, then execute the moved one so each startup request is handled once.</violation>
</file>
<file name="typescript/goose/goose-cli/README.md">
<violation number="1" location="typescript/goose/goose-cli/README.md:9">
P3: The README claims assistant output "streams as it arrives" and is "real-time message ... activity", but `GooseSession.handleEvent` accumulates assistant text in `pendingText` and only prints it at a tool-call boundary or on `complete` (src/session.ts flushes via `flushPendingText`). The buffering is deliberate (it avoids splitting a reply across `message` events so markdown/links render intact), so the doc should say assistant text is rendered and printed at message boundaries rather than streaming live, leaving tool events as the only truly real-time output.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| await this.ptyHandle!.sendInput(`cd ${WORK_DIR} && ${command}\n`) | ||
| await new Promise<void>((resolve) => { | ||
| this.onResponseComplete = resolve | ||
| }) |
There was a problem hiding this comment.
P1: When Goose completes before sendInput returns, the completion event runs before onResponseComplete is assigned, so processPrompt hangs indefinitely. Create and assign the completion promise before sending the command.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At typescript/goose/goose-cli/src/session.ts, line 169:
<comment>When Goose completes before `sendInput` returns, the completion event runs before `onResponseComplete` is assigned, so `processPrompt` hangs indefinitely. Create and assign the completion promise before sending the command.</comment>
<file context>
@@ -0,0 +1,182 @@
+ const command = ['goose', ...flags].join(' ')
+ debug('running:', command)
+
+ await this.ptyHandle!.sendInput(`cd ${WORK_DIR} && ${command}\n`)
+ await new Promise<void>((resolve) => {
+ this.onResponseComplete = resolve
</file context>
| await this.ptyHandle!.sendInput(`cd ${WORK_DIR} && ${command}\n`) | |
| await new Promise<void>((resolve) => { | |
| this.onResponseComplete = resolve | |
| }) | |
| const completion = new Promise<void>((resolve) => { | |
| this.onResponseComplete = resolve | |
| }) | |
| await this.ptyHandle!.sendInput(`cd ${WORK_DIR} && ${command}\n`) | |
| await completion |
|
|
||
| const startServerFromScript = async () => { | ||
| // Only run when Goose has produced a start script for this turn. | ||
| const startScriptCheck = await activeSandbox.process.executeCommand('test -f /home/daytona/start.sh') |
There was a problem hiding this comment.
P1: After Goose creates start.sh once, this existence check stays true, so every later prompt starts another copy of the same server. Track or consume the processed script before checking the next turn.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At typescript/goose/goose-cli/src/index.ts, line 108:
<comment>After Goose creates `start.sh` once, this existence check stays true, so every later prompt starts another copy of the same server. Track or consume the processed script before checking the next turn.</comment>
<file context>
@@ -0,0 +1,146 @@
+
+ const startServerFromScript = async () => {
+ // Only run when Goose has produced a start script for this turn.
+ const startScriptCheck = await activeSandbox.process.executeCommand('test -f /home/daytona/start.sh')
+ if (startScriptCheck.exitCode !== 0) {
+ return
</file context>
| } | ||
|
|
||
| export interface GooseToolUseBlock { | ||
| type: 'tool_use' |
There was a problem hiding this comment.
P2: Goose emits toolRequest/toolResponse blocks, not tool_use/tool_result. Update the event model and handleEvent to parse Goose's actual toolCall/toolResult shape; otherwise the guide does not render the tool activity and errors promised in its README.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At typescript/goose/goose-cli/src/types.ts, line 18:
<comment>Goose emits `toolRequest`/`toolResponse` blocks, not `tool_use`/`tool_result`. Update the event model and `handleEvent` to parse Goose's actual `toolCall`/`toolResult` shape; otherwise the guide does not render the tool activity and errors promised in its README.</comment>
<file context>
@@ -0,0 +1,52 @@
+}
+
+export interface GooseToolUseBlock {
+ type: 'tool_use'
+ id: string
+ name: string
</file context>
| switch (event.type) { | ||
| case 'message': { | ||
| const msg = (event as GooseMessageEvent).message | ||
| if (msg.role !== 'assistant') return |
There was a problem hiding this comment.
P2: When Goose emits a failed tool result in a user-role message, this guard returns before the [tool error] branch, so tool failures disappear from the displayed session. Allow user messages containing tool-result blocks through the event handling.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At typescript/goose/goose-cli/src/session.ts, line 78:
<comment>When Goose emits a failed tool result in a user-role message, this guard returns before the `[tool error]` branch, so tool failures disappear from the displayed session. Allow user messages containing tool-result blocks through the event handling.</comment>
<file context>
@@ -0,0 +1,182 @@
+ switch (event.type) {
+ case 'message': {
+ const msg = (event as GooseMessageEvent).message
+ if (msg.role !== 'assistant') return
+
+ for (const block of msg.content) {
</file context>
| if (msg.role !== 'assistant') return | |
| if (msg.role !== 'assistant' && !msg.content.some((block) => block.type === 'tool_result')) return |
| // in the sandbox's exec environment even though there is no real controlling | ||
| // terminal attached, so the read fails instead of falling back cleanly. | ||
| const install = await activeSandbox.process.executeCommand( | ||
| 'curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash', |
There was a problem hiding this comment.
P2: When the installer download fails, bash exits successfully with no script, so this check passes and the first goose command can hang the prompt loop waiting for completion. Download to a file with && or enable pipefail before accepting the install result.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At typescript/goose/goose-cli/src/index.ts, line 82:
<comment>When the installer download fails, `bash` exits successfully with no script, so this check passes and the first `goose` command can hang the prompt loop waiting for completion. Download to a file with `&&` or enable `pipefail` before accepting the install result.</comment>
<file context>
@@ -0,0 +1,146 @@
+ // in the sandbox's exec environment even though there is no real controlling
+ // terminal attached, so the read fails instead of falling back cleanly.
+ const install = await activeSandbox.process.executeCommand(
+ 'curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash',
+ )
+ if (install.exitCode !== 0) {
</file context>
| 'curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash', | |
| 'curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh -o /tmp/goose-download_cli.sh && CONFIGURE=false bash /tmp/goose-download_cli.sh', |
|
|
||
| for (const block of msg.content) { | ||
| if (block.type === 'text') { | ||
| this.pendingText += block.text |
There was a problem hiding this comment.
P2: Assistant output is not streamed for responses without tool events: pendingText waits until complete before being rendered. Render safe text incrementally, or flush bounded chunks while retaining only the markdown fragment needed for the next chunk.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At typescript/goose/goose-cli/src/session.ts, line 82:
<comment>Assistant output is not streamed for responses without tool events: `pendingText` waits until `complete` before being rendered. Render safe text incrementally, or flush bounded chunks while retaining only the markdown fragment needed for the next chunk.</comment>
<file context>
@@ -0,0 +1,182 @@
+
+ for (const block of msg.content) {
+ if (block.type === 'text') {
+ this.pendingText += block.text
+ } else if (block.type === 'tool_use') {
+ this.flushPendingText()
</file context>
|
|
||
| const startServerFromScript = async () => { | ||
| // Only run when Goose has produced a start script for this turn. | ||
| const startScriptCheck = await activeSandbox.process.executeCommand('test -f /home/daytona/start.sh') |
There was a problem hiding this comment.
P2: After the first server turn, every later prompt reruns the stale /home/daytona/start.sh because the script is never consumed. Move or remove the script atomically before launching it, then execute the moved one so each startup request is handled once.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At typescript/goose/goose-cli/src/index.ts, line 108:
<comment>After the first server turn, every later prompt reruns the stale `/home/daytona/start.sh` because the script is never consumed. Move or remove the script atomically before launching it, then execute the moved one so each startup request is handled once.</comment>
<file context>
@@ -0,0 +1,146 @@
+
+ const startServerFromScript = async () => {
+ // Only run when Goose has produced a start script for this turn.
+ const startScriptCheck = await activeSandbox.process.executeCommand('test -f /home/daytona/start.sh')
+ if (startScriptCheck.exitCode !== 0) {
+ return
</file context>
| ) | ||
| .replace(/\*\*(.+?)\*\*/g, `${BOLD}$1${RESET}`) | ||
| .replace(/(?<!\*)\*([^*\n]+?)\*(?!\*)/g, `${ITALIC}$1${RESET}`) | ||
| .replace(/`([^`]+?)`/g, `${DIM}$1${RESET}`) |
There was a problem hiding this comment.
P3: Inline code spans are processed last, so markdown markers inside them get styled instead of printed literally. The code regex runs after the bold/italic replacements (e.g. a * b * c inside backticks gets italic injected mid-span, **kwargs** gets bolded). Escape or protect code spans before applying the bold/italic rules, or match code first and skip styled spans.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At typescript/goose/goose-cli/src/utils.ts, line 23:
<comment>Inline code spans are processed last, so markdown markers inside them get styled instead of printed literally. The code regex runs after the bold/italic replacements (e.g. `a * b * c` inside backticks gets italic injected mid-span, `**kwargs**` gets bolded). Escape or protect code spans before applying the bold/italic rules, or match code first and skip styled spans.</comment>
<file context>
@@ -0,0 +1,24 @@
+ )
+ .replace(/\*\*(.+?)\*\*/g, `${BOLD}$1${RESET}`)
+ .replace(/(?<!\*)\*([^*\n]+?)\*(?!\*)/g, `${ITALIC}$1${RESET}`)
+ .replace(/`([^`]+?)`/g, `${DIM}$1${RESET}`)
+}
</file context>
|
|
||
| - **Secure sandbox execution:** The Goose CLI and any code it runs stay inside an isolated Daytona sandbox. | ||
| - **Fully headless:** Runs non-interactively with a fixed provider/model and auto-approved tool calls - no setup wizard, no permission prompts. | ||
| - **Streaming output:** Parses the CLI's `stream-json` events for real-time message and tool activity, rendering basic markdown (bold, italic, inline code, links) as ANSI in the terminal. |
There was a problem hiding this comment.
P3: The README claims assistant output "streams as it arrives" and is "real-time message ... activity", but GooseSession.handleEvent accumulates assistant text in pendingText and only prints it at a tool-call boundary or on complete (src/session.ts flushes via flushPendingText). The buffering is deliberate (it avoids splitting a reply across message events so markdown/links render intact), so the doc should say assistant text is rendered and printed at message boundaries rather than streaming live, leaving tool events as the only truly real-time output.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At typescript/goose/goose-cli/README.md, line 9:
<comment>The README claims assistant output "streams as it arrives" and is "real-time message ... activity", but `GooseSession.handleEvent` accumulates assistant text in `pendingText` and only prints it at a tool-call boundary or on `complete` (src/session.ts flushes via `flushPendingText`). The buffering is deliberate (it avoids splitting a reply across `message` events so markdown/links render intact), so the doc should say assistant text is rendered and printed at message boundaries rather than streaming live, leaving tool events as the only truly real-time output.</comment>
<file context>
@@ -0,0 +1,102 @@
+
+- **Secure sandbox execution:** The Goose CLI and any code it runs stay inside an isolated Daytona sandbox.
+- **Fully headless:** Runs non-interactively with a fixed provider/model and auto-approved tool calls - no setup wizard, no permission prompts.
+- **Streaming output:** Parses the CLI's `stream-json` events for real-time message and tool activity, rendering basic markdown (bold, italic, inline code, links) as ANSI in the terminal.
+- **Session continuity:** Reuses Goose's most recent session across prompts (`--resume`) for multi-turn context.
+- **Preview URLs for servers:** A Daytona-aware system prompt tells Goose to write server-start commands to a script instead of running them, so they can be started outside the turn and exposed via a Daytona preview URL.
</file context>
dcc1c12 to
720bd3b
Compare
|
I have read the CLA Document and I hereby sign the CLA |
Description
Add Goose CLI coding agent guide
Adds a new guide, typescript/goose/goose-cli: a working, runnable example of Block's open source Goose CLI as a headless coding agent in a Daytona sandbox.
It's modeled on the existing gemini-cli guide (PTY streaming, JSON events parsed), with a few differences specific to Goose:
Also adds a row for the guide to the root README.
Testing:
Scope
(e.g.
fix(codex-sdk): ...,docs(readme): ...).Checks
Legal
DCO (
git commit -s); the sign-off matches thecommit author.
Contributor License Agreement.
On my first PR, the CLA assistant will comment and I will reply to sign (once).
See CONTRIBUTING.md for details.