Skip to content

feat: add /goal slash command for autonomous goal-driven continuation - #240

Merged
bkrabach merged 2 commits into
mainfrom
feat/goal-command
Jul 30, 2026
Merged

feat: add /goal slash command for autonomous goal-driven continuation#240
bkrabach merged 2 commits into
mainfrom
feat/goal-command

Conversation

@bkrabach

@bkrabach bkrabach commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

"## Summary\n\nAdds the /goal slash command — parity with Claude Code (v2.1.139+) and OpenAI Codex CLI. Developers already familiar with this command from other tools can now use it in Amplifier with the same semantics.\n\n## What it does\n\n- /goal <condition> — sets a completion condition and immediately starts a turn. After each turn, a separate evaluator model (no tools) judges "done? yes/no + reason". If not done, another turn starts automatically with the reason as guidance. If done, the goal clears.\n- /goal --max-turns N <condition> — same, with a hard, Python-enforced turn cap. Invalid values (non-integer, 0, negatives) fail loud: error printed, no goal set, no turn runs.\n- /goal (bare) — shows condition, turns used (against cap if set), and evaluator's most recent reason.\n- /goal clear — clears it. Aliases: stop, off, reset, none, cancel. /clear also clears an active goal.\n\n## Headless support\n\nWorks in interactive mode and headless:\n\nbash\namplifier run --mode single \"/goal all tests in tests/ pass with pytest exiting 0\"\n\n\nThe entire loop runs in one invocation. SIGINT handling has been added to the headless path: first Ctrl-C requests graceful cancellation, second requests immediate.\n\n## Implementation & orchestrator dependency\n\nThis PR sets session.coordinator.session_state[\"goal\"] only. The auto-continue loop itself lives in the orchestrator — this PR depends on microsoft/amplifier-module-loop-streaming#30. Without that orchestrator change, /goal sets state but no auto-continuation occurs.\n\nThis split is deliberate: putting the loop in the orchestrator makes it work headless, in the REPL, and in every other app that mounts it, rather than needing one implementation per app.\n\n## CLI-side goal-progress renderer (NEW)\n\nProblem: The orchestrator PR (microsoft/amplifier-module-loop-streaming#30) removes all print() calls from the module, because bare print() from an orchestrator corrupts protocol channels for other hosts — amplifier-agent uses stdout as a JSON-RPC channel, amplifierd/amplifier-chat/amplifier-voice are daemons/web apps where prints land in journald invisible to the client, and amplifier-resolver-sdk defensively redirects stdout→stderr to protect the JSON protocol.\n\nSolution: The orchestrator now emits orchestrator:goal_progress events instead. This PR adds a new CLI-side hook (goal_progress_hook.py) that subscribes to these events and renders to the CLI's rich console, preserving the exact user-visible progress strings (⟳ goal: turn N/CAP — reason, ✓ goal achieved: ..., ⚠ goal: hit turn cap (N) — stopping., ✗ goal: cancelled — ..., ✗ goal: evaluator failed — ...).\n\nThe hook is registered unconditionally in both interactive_chat and execute_single, following the same pattern as incremental_save.py, so goal output works consistently across all CLI modes.\n\n## Mechanical --max-turns cap\n\nThe --max-turns flag is the opt-in bound — mechanically enforced by Python, not model-judged. Default is no turn cap, matching Claude Code.\n\n- Bad values (abc, 0, -5) fail loud: ValueError is raised, no goal is set, no turn runs.\n- Good values (1, 5, 100) are accepted and enforced by the orchestrator loop.\n- The shared parser _parse_goal_max_turns() is used by both interactive (CommandProcessor) and headless (execute_single) entry points, ensuring flag syntax and failure behavior cannot drift between them.\n\n### What the cap does NOT bound\n\n--max-turns bounds goal continuations — the number of times the goal loop starts a new turn. It does not bound work happening inside a single turn.\n\nAn agent can make an unlimited number of tool calls within one turn; the turn ends only when the model stops requesting tools. If an agent gets stuck polling something that never completes, it never ends its turn, the goal evaluator never runs, and the cap never fires.\n\nThis was verified: an agent given a job-status script that always exited 0 and never reached 100% polled it 59+ times inside a single turn with --max-turns 8 set. The cap was never reached because no turn ever ended.\n\n/goal is a completion gate, not a runaway guard. It catches an agent that stops too early. It cannot catch one that never stops. For unattended runs, bound the process externally (a wall-clock timeout, a CI job limit) in addition to --max-turns.\n\n## New documentation\n\ndocs/GOAL_COMMAND.md covers usage, but importantly how to write a condition that converges. A vague or ambiguous condition is the dominant cost driver:\n\n- One run finished the actual work on turn 1 (28 tests passing) but the condition accidentally omitted the literal pytest command, so the evaluator correctly refused to accept it and pushed back twice more.\n- The doc prescribes: a measurable end state, the literal check command, and explicit MUST-NOT constraints.\n\n## Validation (independently verified)\n\nThe feature was proven to do its actual job — catching premature completion:\n\n- Staged-review trap: Agent implemented 1 of 6 required functions and returned. Evaluator caught it — "only the slugify function has been implemented so far; the other 5 functions remain as stubs" — pushed back; agent completed all 6. Reviewer independently verified all 6 present and working.\n- Hidden-remainder trap: Spec required 12 functions plus a per-function changelog policy. Agent did all obvious work and returned. Evaluator caught a genuinely subtle deviation — "the CHANGELOG.md contains all 12 function entries... but they are all under a single dated entry rather than having a distinct dated entry for every one of the 12 functions as required" — pushed back; agent fixed it. Reviewer independently verified 12 functions, 12 distinct dates, all 12 named.\n\nBoth pushback reasons were specific and actionable, and both drove to a verified-correct final state. Zero evaluator failures across every validation run.\n\nPrior validation:\n- Headless and interactive both proven end-to-end (3 auto-continuations → ✓ goal achieved).\n- Regression: with no goal set, behavior is unchanged.\n- --max-turns trips exactly at the cap; bad values fail loud with no goal set.\n- SIGINT mid-run → goal cleared, loop stopped, clean exit.\n- In a Digital Twin Universe, an agent built a stdlib-only URL shortener REST API; the reviewer independently ran pytest (28 passed, exit 0) and curled every endpoint (302/404/400/409 correct).\n- A separate "never use any import statement" run held that constraint too (grep -c import → 0, 39 tests passing).\n\n## Pre-existing issue (flagged for separate PR)\n\nDuring validation, a pre-existing bug was discovered: with amplifier-core 1.3.0 as locked, every turn fails with TypeError: Object of type Decimal is not JSON serializable, reproducible on a plain prompt with no goal involved. This is unrelated to this feature and deliberately not fixed here. It should be addressed in a separate issue/PR.\n\n---\n\nGenerated with Amplifier\n"

Adds the `/goal` slash command, parity with Claude Code (v2.1.139+) and OpenAI Codex CLI. Developers already familiar with this command from other tools can now use it in Amplifier with the same semantics.

**Behavior:**
- `/goal <condition>` — sets a completion condition and immediately starts a turn. After each turn, a separate evaluator model (no tools) judges "done? yes/no + reason". If not done, another turn starts automatically with the reason as guidance. If done, the goal clears.
- `/goal --max-turns N <condition>` — same, with a hard, Python-enforced turn cap. Invalid values (non-integer, 0, negatives) fail loud: error printed, no goal set, no turn runs.
- `/goal` (bare) — shows condition, turns used (against cap if set), and evaluator's most recent reason.
- `/goal clear` — clears it. Aliases: stop, off, reset, none, cancel. `/clear` also clears an active goal.
- Works headless: `amplifier run --mode single "/goal ..."` parses the prefix in `execute_single()`, so the whole loop runs in one invocation.
- SIGINT handling added to headless path: first Ctrl-C requests graceful cancellation, second requests immediate.

**Implementation details:**
- This PR *sets* `session.coordinator.session_state["goal"]` only. The auto-continue loop lives in the orchestrator (microsoft/amplifier-module-loop-streaming#30). This split is deliberate: putting the loop in the orchestrator makes it work headless, in the REPL, and in every other app that mounts it.
- `_parse_goal_max_turns()` is a shared parser used by both interactive (CommandProcessor) and headless (execute_single) entry points, ensuring flag syntax and failure behavior cannot drift.
- Default is no turn cap, matching Claude Code. The `--max-turns` flag is the opt-in bound — mechanically enforced.
- New doc `docs/GOAL_COMMAND.md` covers usage and importantly how to write a condition that converges.

**Validation:**
- Headless and interactive end-to-end tested (3 auto-continuations → ✓ goal achieved).
- Regression: with no goal set, behavior unchanged.
- `--max-turns` trips exactly at cap; bad values fail loud with no goal set.
- SIGINT mid-run → goal cleared, loop stopped, clean exit.
- Independent validation in DTU: stdlib-only URL shortener, 28 tests passing, every endpoint verified.
- Another run with "never use import" constraint held (grep count: 0, 39 tests passing).

**Known pre-existing issue (flagged separately):**
With amplifier-core 1.3.0 as locked, every turn fails with `TypeError: Object of type Decimal is not JSON serializable`, reproducible on a plain prompt with no goal involved. This is unrelated to this feature and deliberately not fixed here.

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
The auto-continue loop (microsoft/amplifier-module-loop-streaming#30)
now emits orchestrator:goal_progress events instead of printing directly.
Bare print() from an orchestrator corrupts protocol channels for other
hosts (amplifier-agent's stdout JSON-RPC, amplifierd/amplifier-chat's
journald-only stdout).

This hook subscribes to the event and renders to the CLI's rich console,
preserving the exact user-visible progress strings.

Also adds documentation clarifying what --max-turns does NOT bound:
- It bounds goal continuations (number of turns), not work within a turn
- An agent that never stops its turn will bypass the cap
- /goal is a completion gate, not a runaway guard
- Unattended runs need external wall-clock limits too

Registered in both interactive_chat and execute_single for consistent
goal output across CLI modes.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
@bkrabach
bkrabach merged commit 75279e8 into main Jul 30, 2026
1 check passed
@bkrabach
bkrabach deleted the feat/goal-command branch July 30, 2026 10:50
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