Skip to content

feat(mastracode): add /goal slash command for persistent cross-turn goals (Ralph loop) - #16065

Merged
TylerBarnes merged 30 commits into
mainfrom
devin/1777655074-goal-slash-command
May 7, 2026
Merged

TylerBarnes merged 30 commits into
mainfrom
devin/1777655074-goal-slash-command

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented May 1, 2026 •

Copy link
Copy Markdown
Contributor

Description

Implements the /goal slash command for mastracode: a persistent cross-turn goal loop where a separate judge model evaluates the assistant after each completed turn and either marks the goal done, asks the assistant to continue, or waits at an explicit user checkpoint.

Goal state is persisted in thread metadata so it survives thread switches/restarts. The loop is designed to be preemptible: queued user messages, /goal pause, and /goal clear take priority over automatic continuations, and composer input is locked while the judge is evaluating to avoid race conditions.

Features

  • /goal <text> — Set a standing goal and start the loop.
  • /goal or /goal status — Show current goal state, usage, and judge model.
  • /goal pause — Pause the continuation loop.
  • /goal resume — Resume a paused goal and send a continuation.
  • /goal clear — Drop the goal entirely.
  • /judge — Configure global goal judge defaults: judge model and max cycles.
  • Ctrl+C/Esc on empty input pauses an active goal.
  • Status line shows judge progress, e.g. judge 3/50.
  • Default max cycles is 50.
  • Judge decisions support done, continue, and waiting.
  • Judge failures fail closed by pausing the goal instead of looping blindly.
  • Goal judge threads are marked as forked subagent threads so they do not appear as normal resumable threads.

Judge behavior

The configured judge model evaluates the latest user/assistant context after each assistant turn:

  • done marks the goal complete.
  • continue injects a system-reminder continuation prompt so the assistant keeps working.
  • waiting keeps the goal active but does not auto-continue, for goals that explicitly require a human/user checkpoint.

The judge can also answer ask_user tool prompts during goal mode so the main assistant can keep working autonomously unless the goal explicitly requires human verification.

Safety and preemption

  • User follow-up messages and queued slash commands are drained before goal continuations.
  • Goal continuation re-checks the active goal id/status after async judge evaluation before firing.
  • GoalManager ignores stale judge results if the goal was paused, cleared, or replaced while judging.
  • Composer submissions and most slash commands are blocked while the judge is evaluating; /goal pause, /goal clear, and /exit remain available as escape hatches.
  • Starting /goal after /new creates and tags a fresh thread instead of reusing the previous current thread.

Persistence and rendering

  • Goal state is saved to thread metadata.
  • Initial goal and terminal judge results are persisted as user-role system reminders so the assistant treats them as external steering, not its own prior output.
  • Goal/judge reminders render as blue system reminder boxes with judge metadata separated from model-visible text.

Related Issue(s)

Requested via Slack: https://kepler-bej6556.slack.com/archives/C0ACHFXNK7T/p1777654125743969

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Code refactoring
  • Performance improvement
  • Test update

Checklist

  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works
  • I have addressed all Coderabbit comments on this PR

Link to Devin session: https://app.devin.ai/sessions/cd5f8a2e7d0440e78ffc59d6993cfb61
Requested by: @TylerBarnes

…oals (Ralph loop)

Implements the /goal command inspired by Hermes Agent and OpenAI Codex
ralph-loop patterns. After each agent turn, a lightweight judge LLM
evaluates whether the stated objective has been achieved. If not, a
continuation prompt is automatically sent to keep the agent working.

Features:
- /goal <text>: Set a standing goal and kick off first turn
- /goal status: Show current goal state and turn usage
- /goal pause: Pause the continuation loop
- /goal resume: Resume goal (resets turn counter)
- /goal clear: Drop the goal entirely
- Ctrl+C during goal loop pauses the goal
- User messages in the queue preempt the goal loop
- Status line shows active goal turn counter
- Turn budget (default 20) prevents runaway loops
- Judge fails OPEN (defaults to continue on error)

Co-Authored-By: tyler <tylerdbarnes@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@vercel

vercel Bot commented May 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mastra-docs-1.x Ready Ready Preview, Comment May 7, 2026 5:54pm
mastra-playground-ui Ready Ready Preview, Comment May 7, 2026 5:54pm

Request Review

@changeset-bot

changeset-bot Bot commented May 1, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d6ddc95

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 23 packages
Name Type
@mastra/core Patch
mastracode Minor
@mastra/mcp-docs-server Patch
@internal/playground Patch
@mastra/client-js Patch
@mastra/opencode Patch
@mastra/longmemeval Patch
mastra Patch
@mastra/deployer-cloud Patch
@mastra/deployer-vercel Patch
@mastra/playground-ui Patch
@mastra/react Patch
@mastra/server Patch
@mastra/deployer Patch
create-mastra Patch
@mastra/express Patch
@mastra/fastify Patch
@mastra/hono Patch
@mastra/koa Patch
@mastra/nestjs Patch
@mastra/deployer-cloudflare Patch
@mastra/deployer-netlify Patch
@mastra/temporal Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented May 1, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a persistent cross-turn goal system: new GoalManager with judge-model evaluation, a /goal slash command and handler, state and thread persistence, lifecycle hooks to evaluate/continue goals after agent turns, and UI/autocomplete/status-line updates.

Changes

Cohort / File(s) Summary
Goal Manager Core
mastracode/src/tui/goal-manager.ts
Introduces GoalManager, goal-related types, persistence to thread metadata, lifecycle methods (setGoal, pause, resume, clear, markDone) and evaluateAfterTurn that calls a judge model to decide continuation or completion.
Command Handler & Routing
mastracode/src/tui/commands/goal.ts, mastracode/src/tui/commands/index.ts, mastracode/src/tui/command-dispatch.ts
Adds handleGoalCommand export and routes /goal in dispatcher; implements /goal subcommands (status, pause, resume, clear) plus goal-setting flow (judge model selection, API key prompt, cycles dialog, persist and start loop).
State & Lifecycle Integration
mastracode/src/tui/state.ts, mastracode/src/tui/handlers/agent-lifecycle.ts, mastracode/src/tui/event-dispatch.ts
Adds goalManager to TUIState; initializes and syncs goal state on thread events; handleAgentEnd triggers post-turn evaluation/continuation; handleAgentAborted pauses and persists active goals on abort.
UI Components & Dialogs
mastracode/src/tui/components/goal-cycles-dialog.ts, mastracode/src/tui/components/judge-display.ts
Adds GoalCyclesDialogComponent for collecting max attempts and JudgeDisplayComponent to render judge decision and reason in the chat UI.
Help, Status & Setup
mastracode/src/tui/components/help-overlay.ts, mastracode/src/tui/status-line.ts, mastracode/src/tui/setup.ts
Registers /goal in help/autocomplete; status line shows active goal attempts; Ctrl+C/Escape handler may pause an active goal when interrupting input.
Minor wiring
mastracode/src/tui/command-dispatch.ts, mastracode/src/tui/commands/index.ts
Imports and re-exports the new command handler; dispatcher recognizes goal command string and routes to handler.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main feature: adding a /goal slash command for persistent cross-turn goals with the 'Ralph loop' context.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1777655074-goal-slash-command

Comment @coderabbitai help to get the list of available commands and usage tips.

@dane-ai-mastra

dane-ai-mastra Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Thank you for your contribution!

Please ensure that your PR fixes an existing issue and that you have linked it in the description (e.g. with Fixes #1234).

We use CodeRabbit for automated code reviews. Please address all feedback from CodeRabbit by either making changes to your PR or leaving a comment explaining why you disagree with the feedback. Since CodeRabbit is an AI, it may occasionally provide incorrect feedback.

Addressing CodeRabbit's feedback will greatly increase the chances of your PR being merged. We appreciate your understanding and cooperation in helping us maintain high code quality standards.

Comment @coderabbitai review in case you want to trigger a review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mastracode/src/tui/commands/goal.ts`:
- Around line 57-62: Wrap the kickoff sequence in try/catch so failures from
state.harness.createThread() and state.harness.sendMessage(...) are not
swallowed: call createThread inside a try and if it fails set
state.pendingNewThread = true and surface an error to the user (log and
return/fail the command), then call sendMessage inside its own try and on
failure surface a user-visible error and optionally pause the goal instead of
returning success; update the same pattern for the duplicate block that handles
lines around state.pendingNewThread (the second path at ~78-83) and reference
the goal.objective in the error message so users know which goal failed to
continue.

In `@mastracode/src/tui/goal-manager.ts`:
- Around line 121-127: The budget check currently increments this.goal.turnsUsed
then immediately enforces maxTurns causing the latest assistant response to
never be judged; update the logic in goal-manager (the code around
this.goal.turnsUsed++, the `"continue"` decision and the block that sets
this.goal.status = 'paused') so that you increment turnsUsed when a new turn
starts but only evaluate this.goal.turnsUsed >= this.goal.maxTurns after
handling the assistant's response/judgement and after a `"continue"` decision;
i.e., allow the current turn to be judged and potentially mark the goal 'done'
before setting status to 'paused' when the post-judgement count meets or exceeds
maxTurns.

In `@mastracode/src/tui/handlers/agent-lifecycle.ts`:
- Around line 113-117: The current handler only pauses goals when
state.userInitiatedAbort is set after agent_aborted; update the idle
Ctrl+C/SIGINT handlers to also check and pause the goal manager so Ctrl+C pauses
goals during the inter-turn idle window. Concretely, in the places that handle
the idle/clear-input SIGINT (the code path that currently clears input on
Ctrl+C), add logic to check state.goalManager.isActive() and call
state.goalManager.pause(), then call showInfo(state, 'Goal paused (interrupted).
Use /goal resume to continue.') and avoid the normal clear-input fallback when a
goal was paused; keep the existing userInitiatedAbort branch unchanged.
- Around line 177-199: After evaluateAfterTurn(state) resolves, re-check the
queue and current goal state before auto-continuing: inside the
.then(continuation => { ... }) block (the code that currently calls
ctx.fireMessage(continuation) and showInfo), first verify that
state.pendingQueuedActions is empty and that state.goalManager.getGoal() still
exists and is not paused/cleared/done; only call showInfo(...Continuing...) and
ctx.fireMessage(continuation) if those conditions hold. If pendingQueuedActions
is non-empty or the goal status changed (paused/cleared/done), skip the
auto-continuation and respect the user queue or new goal state. Keep the
existing .catch behavior for errors from evaluateAfterTurn().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2c2f7567-0b94-44d6-9a4e-e4d181e7c09d

📥 Commits

Reviewing files that changed from the base of the PR and between e4942bc and 3aa23e7.

📒 Files selected for processing (9)
  • mastracode/src/tui/command-dispatch.ts
  • mastracode/src/tui/commands/goal.ts
  • mastracode/src/tui/commands/index.ts
  • mastracode/src/tui/components/help-overlay.ts
  • mastracode/src/tui/goal-manager.ts
  • mastracode/src/tui/handlers/agent-lifecycle.ts
  • mastracode/src/tui/setup.ts
  • mastracode/src/tui/state.ts
  • mastracode/src/tui/status-line.ts

Comment thread mastracode/src/tui/commands/goal.ts Outdated
Comment thread mastracode/src/tui/goal-manager.ts Outdated
Comment thread mastracode/src/tui/handlers/agent-lifecycle.ts
…hread metadata

- Replace hardcoded model cascade with interactive model picker
- Preselect to last judge choice (saved in settings) or main model
- Store goal state in thread metadata so it survives thread switches
- Load goal from thread metadata on thread_changed event
- Fix prettier formatting issues from CI

Co-Authored-By: tyler <tylerdbarnes@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mastracode/src/tui/event-dispatch.ts`:
- Around line 145-146: In the thread_created branch of the event handler, call
state.goalManager.loadFromThreadMetadata with event.thread.metadata (same as
used in thread_changed) so the newly created thread hydrates its goal state; if
event.thread.metadata is undefined or null, explicitly clear the in-memory goal
(e.g., via a clear/ reset method or by calling loadFromThreadMetadata with
undefined) to avoid carrying over the previous thread’s goal—update the
thread_created branch to mirror the thread_changed behavior by invoking
state.goalManager.loadFromThreadMetadata(event.thread.metadata as Record<string,
unknown> | undefined) or clearing the goal when metadata is absent.

In `@mastracode/src/tui/goal-manager.ts`:
- Around line 254-257: The buildContinuationPrompt currently injects free-form
judgeReason into the system prompt, which can let the LLM steer future turns and
lose the original objective; change buildContinuationPrompt to produce a fixed
continuation prompt that explicitly restates the goal objective (use
this.goal.objective) and the turn counts (this.goal.turnsUsed /
this.goal.maxTurns) and do NOT embed the raw judgeReason string; if you need to
surface the judge outcome, map judgeReason to a short, known enum or omit it and
instead append a sanitized fixed note like "Judge: proceed" or "Judge: revise"
rather than the free-form text.

In `@mastracode/src/tui/handlers/agent-lifecycle.ts`:
- Around line 113-117: When handling Ctrl+C you pause the in-memory goal loop
(state.goalManager.pause()) but do not persist that paused status, so after a
thread switch/restart the goal will be restored as active; update the abort
branch to also set and persist a "paused" flag on the current thread's metadata
(using your thread metadata API on state, e.g. add something like
currentThread.metadata.paused = true and call the existing persistence method -
match your project API) immediately after calling state.goalManager.pause(), and
keep the showInfo(state, ...) call; also ensure the restore logic reads this
metadata flag to keep the goal paused on resume.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7b67f6c1-5aa1-48b4-92be-c817eed75281

📥 Commits

Reviewing files that changed from the base of the PR and between 3aa23e7 and 5c35258.

📒 Files selected for processing (4)
  • mastracode/src/tui/commands/goal.ts
  • mastracode/src/tui/event-dispatch.ts
  • mastracode/src/tui/goal-manager.ts
  • mastracode/src/tui/handlers/agent-lifecycle.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • mastracode/src/tui/commands/goal.ts

Comment thread mastracode/src/tui/event-dispatch.ts Outdated
Comment thread mastracode/src/tui/goal-manager.ts Outdated
Comment thread mastracode/src/tui/handlers/agent-lifecycle.ts
- Judge current turn before enforcing budget (allows last turn to be marked done)
- Add try/catch for sendMessage failures (pause goal with error message)
- Clear/load goal state on thread_created (not just thread_changed)
- Idle Ctrl+C (empty input) pauses active goal

Co-Authored-By: tyler <tylerdbarnes@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mastracode/src/tui/commands/goal.ts`:
- Around line 97-98: Guard against missing settings.models by using optional
chaining and nullish coalescing when reading and writing the judge model: change
the access that computes lastJudgeModelId (currently using (settings.models as
Record<string, unknown>).goalJudgeModel) to safely read
settings.models?.goalJudgeModel as string | undefined and compute preselectedId
with the nullish fallback as now; likewise update the save path that writes into
settings.models to ensure settings.models is initialized first (e.g., ensure
settings.models = settings.models ?? {} before assigning goalJudgeModel) so
reads/writes of goalJudgeModel cannot throw; adjust references in this file to
use lastJudgeModelId and preselectedId as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 38f57479-64ed-4a49-9e02-bc78db54ea84

📥 Commits

Reviewing files that changed from the base of the PR and between 5c35258 and 746277a.

📒 Files selected for processing (4)
  • mastracode/src/tui/commands/goal.ts
  • mastracode/src/tui/event-dispatch.ts
  • mastracode/src/tui/goal-manager.ts
  • mastracode/src/tui/setup.ts
✅ Files skipped from review due to trivial changes (1)
  • mastracode/src/tui/goal-manager.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • mastracode/src/tui/setup.ts

Comment thread mastracode/src/tui/commands/goal.ts Outdated
Comment on lines +97 to +98
const lastJudgeModelId = (settings.models as Record<string, unknown>).goalJudgeModel as string | undefined;
const preselectedId = lastJudgeModelId ?? state.harness.getCurrentModelId() ?? undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against missing settings.models property.

If loadSettings() returns a settings object without a models property (e.g., fresh install, corrupted settings), accessing (settings.models as Record<string, unknown>).goalJudgeModel will throw a TypeError. The same issue exists on line 112 when saving.

Proposed fix using optional chaining and nullish coalescing
   const settings = loadSettings();
-  const lastJudgeModelId = (settings.models as Record<string, unknown>).goalJudgeModel as string | undefined;
+  const lastJudgeModelId = (settings.models as Record<string, unknown> | undefined)?.goalJudgeModel as string | undefined;
   const preselectedId = lastJudgeModelId ?? state.harness.getCurrentModelId() ?? undefined;

And for line 111-113:

         // Save judge preference for next time
         const s = loadSettings();
+        if (!s.models) s.models = {};
         (s.models as Record<string, unknown>).goalJudgeModel = model.id;
         saveSettings(s);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const lastJudgeModelId = (settings.models as Record<string, unknown>).goalJudgeModel as string | undefined;
const preselectedId = lastJudgeModelId ?? state.harness.getCurrentModelId() ?? undefined;
const lastJudgeModelId = (settings.models as Record<string, unknown> | undefined)?.goalJudgeModel as string | undefined;
const preselectedId = lastJudgeModelId ?? state.harness.getCurrentModelId() ?? undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mastracode/src/tui/commands/goal.ts` around lines 97 - 98, Guard against
missing settings.models by using optional chaining and nullish coalescing when
reading and writing the judge model: change the access that computes
lastJudgeModelId (currently using (settings.models as Record<string,
unknown>).goalJudgeModel) to safely read settings.models?.goalJudgeModel as
string | undefined and compute preselectedId with the nullish fallback as now;
likewise update the save path that writes into settings.models to ensure
settings.models is initialized first (e.g., ensure settings.models =
settings.models ?? {} before assigning goalJudgeModel) so reads/writes of
goalJudgeModel cannot throw; adjust references in this file to use
lastJudgeModelId and preselectedId as before.

When sendMessage creates a new thread during goal flow, the
thread_created event was loading from empty metadata and clearing
the in-memory goal. Now if a goal is already in memory when a new
thread is created, we save it to the new thread instead of loading
empty metadata.

Co-Authored-By: tyler <tylerdbarnes@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Interactive Test Results — /goal Command

Tested end-to-end in mastracode TUI with OpenRouter/minimax-m1 as agent and judge model.

Core Flow Tests (12/12 passed)
  • PASSED — Model picker appears with title "Select Judge Model for Goal"
  • PASSED — Judge model preselected to current model (first time) / last choice (subsequent)
  • PASSED — Goal info message shows correct budget and judge model
  • PASSED — Goal persists after thread creation (bug fix verified)
  • PASSED — Full ralph loop fires continuations (ran 20 turns)
  • PASSED — Status line shows goal X/20 counter
  • PASSED — Turn budget enforced — pauses at 20/20 with message
  • PASSED — /goal status shows objective, turns, judge model
  • PASSED — /goal clear removes goal completely
  • PASSED — /help lists /goal as "Set/manage persistent goal (Ralph loop)"
  • PASSED — Ctrl+C during active processing pauses goal
  • PASSED — /goal resume restarts with counter reset to 0/20
Bug Found & Fixed

Issue: Goal state was lost when sendMessage() created a new thread. The thread_created event called loadFromThreadMetadata() with empty metadata, clearing the in-memory goal.

Fix: Modified event-dispatch.ts to save the existing goal to the new thread instead of loading from empty metadata. Commit: 012864e0f5.

Observations
  1. Judge model quality: minimax-m1 never said "done" — the loop ran all 20 turns for a trivial task. Better models (Claude, GPT-4o) would judge more accurately.
  2. Ctrl+C behavior: During question prompts, Ctrl+C cancels the question but doesn't pause the goal. During "Thinking...", it correctly aborts and pauses. This is correct design.
  3. Minor cosmetic: Status line shows stale goal 20/20 briefly after /goal clear until next render.
Key Evidence Screenshots
Model Picker Goal Set + Status Line
Model picker Goal set
Loop Running (14/20) Ctrl+C Pauses Goal
Loop Pause

Devin session

@TylerBarnes

Copy link
Copy Markdown
Member

Review from MastraCode: Thanks for adding this — the overall shape looks good. Keeping the Ralph/goal loop in the TUI layer is a nice low-blast-radius approach, and I like that queued user input preempts automatic continuation.

A few things I’d want addressed or at least confirmed before merge:

  1. Please add automated coverage for the new lifecycle paths.
    This touches command dispatch, goal persistence, agent-end continuation, Ctrl+C/abort behavior, thread switching/creation, and status-line rendering, but the PR currently relies mostly on interactive testing. Good focused test targets:

    • /goal dispatch in mastracode/src/tui/__tests__/command-dispatch.test.ts
    • goal metadata load/save in event-dispatch-thread-state.test.ts
    • Ctrl+C pause behavior in setup-keyboard-shortcuts.test.ts
    • goal X/Y status rendering in status-line.test.ts
    • direct GoalManager tests for judge done, continue, failure-open, and budget exhaustion
  2. Full Test Suite is failing.
    Even if unrelated, this feature introduces automatic repeated message sending, so I’d like the failure understood before merging.

  3. User-initiated abort pause may not persist.
    handleAgentAborted pauses the active goal in memory, but unlike the idle Ctrl+C path, it does not call saveToThread. If the user aborts an active goal and then switches threads/restarts, metadata may still say the goal is active.

    Relevant area:
    mastracode/src/tui/handlers/agent-lifecycle.ts:113-117

  4. Thread creation may copy goals too broadly.
    On thread_created, any in-memory goal is saved to the new thread. That solves the /goal first-message/new-thread case, but it may also copy a prior thread’s goal into a manually created new thread unless the surrounding thread creation flow prevents that.

    Relevant area:
    mastracode/src/tui/event-dispatch.ts:151-160

None of these block the direction of the feature for me, but I think at least the abort persistence issue and some targeted tests should be fixed before merging.

…uracy, judge display, status wording

- Add max cycles input popup (default 20, user editable) after model picker
- Improve judge prompt to be better at detecting completion (lean toward 'done')
- Show judge decision in chat with blue bordered 'Judge' component
- Change status line from 'goal X/20' to 'X/20 attempts' (remaining/total)
- Fix: persist goal pause to thread on user-initiated abort
- Fix: only copy goal to new thread if it was just set (turnsUsed === 0)

Co-Authored-By: tyler <tylerdbarnes@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (3)
mastracode/src/tui/commands/goal.ts (1)

99-100: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard settings.models before reading or writing goalJudgeModel.

If loadSettings() returns an object without a models bag, both accesses here throw and /goal fails before the picker opens or before the selection is saved.

Suggested fix
   const settings = loadSettings();
-  const lastJudgeModelId = (settings.models as Record<string, unknown>).goalJudgeModel as string | undefined;
+  const lastJudgeModelId = (settings.models as Record<string, unknown> | undefined)?.goalJudgeModel as
+    | string
+    | undefined;
   const preselectedId = lastJudgeModelId ?? state.harness.getCurrentModelId() ?? undefined;
...
         // Save judge preference for next time
         const s = loadSettings();
+        s.models ??= {};
         (s.models as Record<string, unknown>).goalJudgeModel = model.id;
         saveSettings(s);

Also applies to: 114-115

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mastracode/src/tui/commands/goal.ts` around lines 99 - 100, The code reads
and writes settings.models.goalJudgeModel without guarding that settings.models
exists; update the logic around loadSettings(), lastJudgeModelId, and any save
path to check for settings.models (use optional chaining when reading:
settings?.models?.goalJudgeModel or assign a typed empty object) and ensure you
initialize settings.models = {} before writing to it so accessing or setting
goalJudgeModel in the goal command (the variables settings, lastJudgeModelId and
any save path near lines 114-115) cannot throw when models is undefined.
mastracode/src/tui/handlers/agent-lifecycle.ts (1)

179-193: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Re-check queue and goal state after the async judge finishes.

This promise resolves later. If the user queues input or pauses/clears the goal while the judge call is running, the current code can still auto-send the continuation first, and getGoal()! can be null by the time the result comes back.

Suggested guard
   state.goalManager
     .evaluateAfterTurn(state)
     .then(({ continuation, judgeResult }) => {
+      const goal = state.goalManager.getGoal();
+
       // Display the judge result in chat if available
-      if (judgeResult) {
-        const goal = state.goalManager.getGoal()!;
+      if (judgeResult && goal) {
         const judgeComponent = new JudgeDisplayComponent(judgeResult, goal.turnsUsed, goal.maxTurns);
         state.chatContainer.addChild(judgeComponent);
         state.ui.requestRender();
       }
 
       if (continuation) {
-        const goal = state.goalManager.getGoal()!;
+        const freshGoal = state.goalManager.getGoal();
+        if (!freshGoal || freshGoal.status !== 'active' || state.pendingQueuedActions.length > 0) {
+          ctx.updateStatusLine();
+          return;
+        }
-        showInfo(state, `Continuing toward goal (attempt ${goal.turnsUsed}/${goal.maxTurns})...`);
+        showInfo(state, `Continuing toward goal (attempt ${freshGoal.turnsUsed}/${freshGoal.maxTurns})...`);
         ctx.fireMessage(continuation);
       } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mastracode/src/tui/handlers/agent-lifecycle.ts` around lines 179 - 193, The
async evaluateAfterTurn handler must re-check that a goal still exists and that
queued/paused state hasn't changed before using getGoal() or auto-sending a
continuation: after state.goalManager.evaluateAfterTurn(...) resolves, guard
every use of state.goalManager.getGoal() (used for JudgeDisplayComponent
construction, showInfo, and before calling ctx.fireMessage) by verifying the
goal is non-null (and optionally that its turnsUsed/maxTurns or an expected
identifier matches the one captured before awaiting) and skip displaying the
judge or firing the continuation if the goal was cleared/changed or user input
was queued/paused; update the branches around JudgeDisplayComponent,
state.chatContainer.addChild, state.ui.requestRender, showInfo, and
ctx.fireMessage to perform these guards.
mastracode/src/tui/goal-manager.ts (1)

266-269: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restate the goal here instead of replaying raw judge output.

This synthetic user turn is built from free-form judgeReason, so the judge can steer the next turn and the original objective can drift once the initial /goal message falls out of context.

Suggested fix
-  private buildContinuationPrompt(judgeReason: string): string {
+  private buildContinuationPrompt(_judgeReason: string): string {
     const turn = this.goal!.turnsUsed;
     const max = this.goal!.maxTurns;
-    return `Continue working toward the goal. (Turn ${turn}/${max}: ${judgeReason})`;
+    const objective = this.goal!.objective;
+    return `Continue working toward this goal: "${objective}" (attempt ${turn}/${max}). Review your last response, identify the remaining work, and continue.`;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mastracode/src/tui/goal-manager.ts` around lines 266 - 269, The
buildContinuationPrompt currently injects free-form judgeReason into the
synthetic user turn, allowing the judge to steer objectives; change it to
restate the original goal text from this.goal (e.g., use this.goal!.description
or this.goal!.objective) and include judge feedback only as a short, quoted
summary or label (e.g., "Judge feedback: <short summary>"). Update
buildContinuationPrompt to produce a prompt like "Continue working toward the
goal: <goal text> (Turn X/Y). Judge feedback: <brief summary>" so the original
objective stays primary while still surfacing reviewer notes; use
this.goal!.turnsUsed and this.goal!.maxTurns for the turn counter and
sanitize/limit judgeReason when included.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mastracode/src/tui/commands/goal.ts`:
- Around line 60-70: The resume command currently only checks if goal.status ===
'active' and proceeds for other states, which allows a 'done' goal to trigger
goalManager.resume(), saveToThread, and state.harness.sendMessage; update the
logic in the /goal resume handler to also block when goal.status === 'done' (or
the completed status your model uses) by returning early (showing an informative
ctx.showInfo) so that goalManager.resume(), goalManager.saveToThread(state), and
the subsequent state.harness.sendMessage call are not executed for completed
goals.

In `@mastracode/src/tui/components/judge-display.ts`:
- Around line 49-54: The padLine method currently returns the full text when
visibleLength >= width, causing long result.reason strings to overflow the UI;
update padLine to clamp/truncate ANSI-containing strings to the target width
instead of returning them unchanged: when visibleLength >= width, compute a
truncated visible portion of text that fits width (e.g., take the first width-1
visible characters and append an ellipsis '…'), but preserve original ANSI
escape sequences around the truncated content (you can continue to use
stripAnsi(text) for measuring and then rebuild the truncated string by walking
the original text and copying ANSI sequences plus visible characters until the
limit is reached), so padLine always returns a string whose visible length <=
width (and still works for texts with ANSI codes like result.reason).

In `@mastracode/src/tui/event-dispatch.ts`:
- Around line 155-160: The current check using currentGoal.turnsUsed === 0 is
unsafe; instead add an explicit one-shot bootstrap flag on the goal manager
(e.g., GoalManager.markBootstrapNextThread() / goalManager.bootstrapPending or
similar) that the /goal command sets when it creates a thread, then modify the
dispatch logic (replace the turnsUsed check in event-dispatch.ts where
state.goalManager.getGoal() and saveToThread(state) are called) to only
saveToThread when that bootstrap flag is set, and ensure the goal manager clears
the bootstrap flag immediately after successful saveToThread (and on failure) so
it won’t apply to subsequent threads.

In `@mastracode/src/tui/status-line.ts`:
- Around line 133-137: The status-line shows remaining attempts instead of
attempts used, causing it to read backwards; in the block that reads the goal
via state.goalManager.getGoal() and builds goalLabel, change the expression so
it displays turnsUsed/ maxTurns (i.e.
`${goalState.turnsUsed}/${goalState.maxTurns} attempts`) when goalState?.status
=== 'active' instead of `${goalState.maxTurns -
goalState.turnsUsed}/${goalState.maxTurns}` so it matches the rest of the UI
(refer to goalState, goalLabel, goalState.turnsUsed, and goalState.maxTurns).

---

Duplicate comments:
In `@mastracode/src/tui/commands/goal.ts`:
- Around line 99-100: The code reads and writes settings.models.goalJudgeModel
without guarding that settings.models exists; update the logic around
loadSettings(), lastJudgeModelId, and any save path to check for settings.models
(use optional chaining when reading: settings?.models?.goalJudgeModel or assign
a typed empty object) and ensure you initialize settings.models = {} before
writing to it so accessing or setting goalJudgeModel in the goal command (the
variables settings, lastJudgeModelId and any save path near lines 114-115)
cannot throw when models is undefined.

In `@mastracode/src/tui/goal-manager.ts`:
- Around line 266-269: The buildContinuationPrompt currently injects free-form
judgeReason into the synthetic user turn, allowing the judge to steer
objectives; change it to restate the original goal text from this.goal (e.g.,
use this.goal!.description or this.goal!.objective) and include judge feedback
only as a short, quoted summary or label (e.g., "Judge feedback: <short
summary>"). Update buildContinuationPrompt to produce a prompt like "Continue
working toward the goal: <goal text> (Turn X/Y). Judge feedback: <brief
summary>" so the original objective stays primary while still surfacing reviewer
notes; use this.goal!.turnsUsed and this.goal!.maxTurns for the turn counter and
sanitize/limit judgeReason when included.

In `@mastracode/src/tui/handlers/agent-lifecycle.ts`:
- Around line 179-193: The async evaluateAfterTurn handler must re-check that a
goal still exists and that queued/paused state hasn't changed before using
getGoal() or auto-sending a continuation: after
state.goalManager.evaluateAfterTurn(...) resolves, guard every use of
state.goalManager.getGoal() (used for JudgeDisplayComponent construction,
showInfo, and before calling ctx.fireMessage) by verifying the goal is non-null
(and optionally that its turnsUsed/maxTurns or an expected identifier matches
the one captured before awaiting) and skip displaying the judge or firing the
continuation if the goal was cleared/changed or user input was queued/paused;
update the branches around JudgeDisplayComponent, state.chatContainer.addChild,
state.ui.requestRender, showInfo, and ctx.fireMessage to perform these guards.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 334d2622-a53e-457b-b998-7d583d6639bf

📥 Commits

Reviewing files that changed from the base of the PR and between 012864e and 659343f.

📒 Files selected for processing (7)
  • mastracode/src/tui/commands/goal.ts
  • mastracode/src/tui/components/goal-cycles-dialog.ts
  • mastracode/src/tui/components/judge-display.ts
  • mastracode/src/tui/event-dispatch.ts
  • mastracode/src/tui/goal-manager.ts
  • mastracode/src/tui/handlers/agent-lifecycle.ts
  • mastracode/src/tui/status-line.ts

Comment thread mastracode/src/tui/commands/goal.ts
Comment on lines +49 to +54
private padLine(text: string, width: number): string {
const visibleLength = stripAnsi(text).length;
if (visibleLength >= width) {
return text;
}
return text + ' '.repeat(width - visibleLength);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clamp or wrap long judge reasons before returning the row.

When visibleLength >= width, this returns the full string unchanged. A long result.reason will overflow the box and break the right border alignment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mastracode/src/tui/components/judge-display.ts` around lines 49 - 54, The
padLine method currently returns the full text when visibleLength >= width,
causing long result.reason strings to overflow the UI; update padLine to
clamp/truncate ANSI-containing strings to the target width instead of returning
them unchanged: when visibleLength >= width, compute a truncated visible portion
of text that fits width (e.g., take the first width-1 visible characters and
append an ellipsis '…'), but preserve original ANSI escape sequences around the
truncated content (you can continue to use stripAnsi(text) for measuring and
then rebuild the truncated string by walking the original text and copying ANSI
sequences plus visible characters until the limit is reached), so padLine always
returns a string whose visible length <= width (and still works for texts with
ANSI codes like result.reason).

Comment thread mastracode/src/tui/event-dispatch.ts Outdated
Comment thread mastracode/src/tui/status-line.ts Outdated
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Re: MastraCode review feedback — addressed in 659343f:

3. Abort pause persistence — Added saveToThread call in handleAgentAborted so the paused state persists across thread switches/restarts.

4. Thread creation goal copy — Now only saves the goal to a new thread if turnsUsed === 0 and status is active (meaning it was just set and sendMessage triggered thread creation). If the user creates a new thread manually via /new, the old goal won't bleed over.

1. Automated tests — Agreed this needs coverage. Will add targeted tests as a follow-up.

2. Full Test Suite — This failure is pre-existing and unrelated to mastracode changes (confirmed in prior CI runs on main). Lint, Build, E2E, Memory, and Combined Store checks all pass.

…yword-match

- Judge now uses step-by-step reasoning: what does the goal require? what was produced? are all requirements met?
- Continuation message includes specific judge feedback so the agent knows what's still missing
- Parser scans for decision keyword (handles models that add preamble)
- Increased maxOutputTokens to 300 for proper reasoning

Co-Authored-By: tyler <tylerdbarnes@gmail.com>
@vercel
vercel Bot temporarily deployed to Preview – mastra-docs-1.x May 1, 2026 18:51 Inactive
TylerBarnes and others added 2 commits May 7, 2026 10:12
Add plan approval support for starting goals directly from submitted plans, plus goal-enabled slash commands and skills under /goal/<name>.

Document goal usage, preserve multiline goal objectives, improve goal status/resume behavior, and add the pr-triage goal command for prioritized PR review.

Co-Authored-By: Mastra Code (openai/gpt-5.5) <noreply@mastra.ai>
…and' into devin/1777655074-goal-slash-command

# Conflicts:
#	mastracode/README.md
#	mastracode/src/agents/__tests__/workspace-skill-paths.test.ts
#	mastracode/src/tui/command-dispatch.ts
#	mastracode/src/tui/commands/goal.ts
#	mastracode/src/tui/commands/index.ts
#	mastracode/src/tui/components/goal-cycles-dialog.ts
#	mastracode/src/tui/components/help-overlay.ts
#	mastracode/src/tui/components/judge-display.ts
#	mastracode/src/tui/event-dispatch.ts
#	mastracode/src/tui/goal-manager.ts
#	mastracode/src/tui/handlers/agent-lifecycle.ts
#	mastracode/src/tui/setup.ts
#	mastracode/src/tui/state.ts
#	mastracode/src/tui/status-line.ts
Collapse the goal-related Mastra Code changesets into a single minor entry while keeping the Harness metadata change as a core patch.

Co-Authored-By: Mastra Code (openai/gpt-5.5) <noreply@mastra.ai>
Configure the goal judge agent with the same provider compatibility and stream error processors used by Mastra Code agent calls so judge evaluations behave consistently across providers.

Co-Authored-By: Mastra Code (openai/gpt-5.5) <noreply@mastra.ai>
Explain the new /goal command from the reader's perspective so release notes do not assume prior context about the feature.

Co-Authored-By: Mastra Code (openai/gpt-5.5) <noreply@mastra.ai>
@TylerBarnes
TylerBarnes merged commit 33f5061 into main May 7, 2026
49 checks passed
@TylerBarnes
TylerBarnes deleted the devin/1777655074-goal-slash-command branch May 7, 2026 18:08
DevZonayed pushed a commit to DevZonayed/nexalance-skill-mastra-ai-mastra that referenced this pull request Aug 17, 2026
…oals (Ralph loop) (mastra-ai#16065)

## Description

Implements the `/goal` slash command for mastracode: a persistent
cross-turn goal loop where a separate judge model evaluates the
assistant after each completed turn and either marks the goal done, asks
the assistant to continue, or waits at an explicit user checkpoint.

Goal state is persisted in thread metadata so it survives thread
switches/restarts. The loop is designed to be preemptible: queued user
messages, `/goal pause`, and `/goal clear` take priority over automatic
continuations, and composer input is locked while the judge is
evaluating to avoid race conditions.

## Features

- `/goal <text>` — Set a standing goal and start the loop.
- `/goal` or `/goal status` — Show current goal state, usage, and judge
model.
- `/goal pause` — Pause the continuation loop.
- `/goal resume` — Resume a paused goal and send a continuation.
- `/goal clear` — Drop the goal entirely.
- `/judge` — Configure global goal judge defaults: judge model and max
cycles.
- Ctrl+C/Esc on empty input pauses an active goal.
- Status line shows judge progress, e.g. `judge 3/50`.
- Default max cycles is 50.
- Judge decisions support `done`, `continue`, and `waiting`.
- Judge failures fail closed by pausing the goal instead of looping
blindly.
- Goal judge threads are marked as forked subagent threads so they do
not appear as normal resumable threads.

## Judge behavior

The configured judge model evaluates the latest user/assistant context
after each assistant turn:

- `done` marks the goal complete.
- `continue` injects a system-reminder continuation prompt so the
assistant keeps working.
- `waiting` keeps the goal active but does not auto-continue, for goals
that explicitly require a human/user checkpoint.

The judge can also answer `ask_user` tool prompts during goal mode so
the main assistant can keep working autonomously unless the goal
explicitly requires human verification.

## Safety and preemption

- User follow-up messages and queued slash commands are drained before
goal continuations.
- Goal continuation re-checks the active goal id/status after async
judge evaluation before firing.
- GoalManager ignores stale judge results if the goal was paused,
cleared, or replaced while judging.
- Composer submissions and most slash commands are blocked while the
judge is evaluating; `/goal pause`, `/goal clear`, and `/exit` remain
available as escape hatches.
- Starting `/goal` after `/new` creates and tags a fresh thread instead
of reusing the previous current thread.

## Persistence and rendering

- Goal state is saved to thread metadata.
- Initial goal and terminal judge results are persisted as user-role
system reminders so the assistant treats them as external steering, not
its own prior output.
- Goal/judge reminders render as blue system reminder boxes with judge
metadata separated from model-visible text.

## Related Issue(s)

Requested via Slack:
https://kepler-bej6556.slack.com/archives/C0ACHFXNK7T/p1777654125743969

## Type of Change

- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Code refactoring
- [ ] Performance improvement
- [x] Test update

## Checklist

- [ ] I have made corresponding changes to the documentation (if
applicable)
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have addressed all Coderabbit comments on this PR

Link to Devin session:
https://app.devin.ai/sessions/cd5f8a2e7d0440e78ffc59d6993cfb61
Requested by: @TylerBarnes

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: tyler <tylerdbarnes@gmail.com>
Co-authored-by: Mastra Code (openai/gpt-5.5) <noreply@mastra.ai>

This branch was successfully deployed

2 active deployments
Preview – mastra-docs-1.x — d6ddc953 Deployed May 7, 2026 by vercel[bot]
Preview – mastra-playground-ui — d6ddc953 Deployed May 7, 2026 by vercel[bot]
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.

2 participants