Skip to content

feat(loop): add streaming output mode with --stream flag#1605

Merged
Crunchyman-ralph merged 3 commits intonextfrom
ralph/feat/improve.loop.output
Jan 25, 2026
Merged

feat(loop): add streaming output mode with --stream flag#1605
Crunchyman-ralph merged 3 commits intonextfrom
ralph/feat/improve.loop.output

Conversation

@Crunchyman-ralph
Copy link
Collaborator

@Crunchyman-ralph Crunchyman-ralph commented Jan 23, 2026

Summary

  • Add --stream flag for real-time output display using Claude's stream-json format
  • Add --no-output flag to exclude full output from iteration results (saves memory)
  • Improved error handling and robustness with multiple race condition fixes

Changes

New Features

  • Streaming mode: task-master loop --stream displays Claude's output in real-time as it generates
  • Output control: --no-output prevents storing large output in iteration results

Bug Fixes & Improvements

  • Fix race condition between error and close events (prevent multiple promise resolutions)
  • Fix null safety for child.stdout before attaching listeners
  • Fix race condition in buffer processing with proper stdout end event handling
  • Add validation rejecting incompatible --stream + --sandbox combination
  • Add JSON parse error logging for debugging malformed events
  • Add event structure validation before accessing properties
  • Add stream cleanup on error (remove listeners, kill process)
  • Enhanced JSDoc to document config/result field relationships

Test plan

  • All 46 loop.service.spec.ts tests pass
  • All 35 loop.command.spec.ts tests pass
  • TypeScript type check passes for all packages
  • Manual test: task-master loop --stream -n 1
  • Manual test: task-master loop --no-output -n 1
  • Manual test: task-master loop --stream --sandbox (should error)

Note

Introduces real-time loop streaming and configurable output retention, plus robustness and UX improvements across CLI and core.

  • CLI (loop): adds -v, --verbose to stream Claude output (thinking, tool calls) and --no-output to skip storing full output; defaults includeOutput to true; surfaces errors via callbacks; shows iteration progress and final error message when present; auto-includes auth brief in progress file
  • Core (tm-core loop): extends LoopConfig/LoopResult with includeOutput, verbose, brief, callbacks, and optional errorMessage; LoopService implements verbose streaming via spawn and --output-format stream-json, fixes race conditions, validates incompatible verbose + sandbox, centralizes error reporting, updates progress header, and removes tasks.json from context header
  • Presets/tests: standardize wording to "Taskmaster"; update snapshots; test setup cleans signal handlers
  • Misc: relax apps/extension dependency to task-master-ai: "*"

Written by Cursor Bugbot for commit b428fdb. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Real-time verbose streaming during loop execution via -v / --verbose
    • --no-output flag to omit full model output and reduce memory use
  • Bug Fixes

    • Validation to block incompatible option combinations
    • Improved error reporting and more robust streaming/JSON handling
  • Chores / Docs

    • Minor preset text updates ("Taskmaster")
    • Test teardown added to clean up signal handlers

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link

changeset-bot bot commented Jan 23, 2026

🦋 Changeset detected

Latest commit: b428fdb

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

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
Copy link
Contributor

coderabbitai bot commented Jan 23, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds verbose/streaming output and an option to omit full model output for the loop command; wires lifecycle and streaming callbacks into loop execution, validates incompatible options (verbose + sandbox), and updates progress header and tests.

Changes

Cohort / File(s) Summary
Changeset
/.changeset/cuddly-wings-drop.md
Documents new CLI flags (--verbose, --no-output) and notes validation and streaming/callback additions.
CLI command
apps/cli/src/commands/loop.command.ts, apps/cli/src/commands/loop.command.spec.ts
Adds --no-output and `-v
Core types & exports
packages/tm-core/src/modules/loop/types.ts, packages/tm-core/src/modules/loop/index.ts, packages/tm-core/src/index.ts
Introduces LoopOutputCallbacks, extends LoopConfig with includeOutput, verbose, brief, and callbacks, adds output? to LoopIteration, and exports the new callback type.
Loop domain
packages/tm-core/src/modules/loop/loop-domain.ts
buildConfig now forwards includeOutput, verbose, brief, and callbacks into the constructed LoopConfig.
Loop service
packages/tm-core/src/modules/loop/services/loop.service.ts, packages/tm-core/src/modules/loop/services/loop.service.spec.ts
Adds validation for incompatible options (verbose + sandbox); implements streaming/verbose iteration path with line-buffered JSON event parsing, helpers (event validation/handling, buffered processing, error iteration creation, command arg builder, error formatting), per-iteration callbacks (onIterationStart, onIterationEnd, onText, onToolUse, onStderr, onOutput, onError); refactors non-stream path to reuse builders; updates progress header and test expectations.
Presets text updates
packages/tm-core/src/modules/loop/presets/*
Cosmetic edits: replace "Task Master" with "Taskmaster" across presets and comments.
Misc tests / setup
tests/setup.js
Adds afterAll teardown to remove signal handlers to avoid open-handle warnings.
Other
apps/extension/package.json
Relaxed devDependency version spec for task-master-ai.

Sequence Diagram

sequenceDiagram
    participant CLI as CLI Command
    participant LS as Loop Service
    participant EXEC as Executor (spawn/sandbox)
    participant CB as Callbacks

    CLI->>LS: startLoop(config + callbacks)
    LS->>LS: validate options (verbose + sandbox)
    LS->>CB: onIterationStart(i, total)
    alt verbose/stream enabled
        LS->>EXEC: spawn with streaming args
        EXEC-->>LS: stream of JSON lines/events
        LS->>LS: buffer & parse lines
        LS->>CB: onText(text) / onToolUse(tool) / onStderr(iter, text)
        LS->>CB: onOutput(finalOutput) [if includeOutput]
    else non-stream
        LS->>EXEC: spawn normally
        EXEC-->>LS: full result
        LS->>CB: onOutput(result) [if includeOutput]
    end
    LS->>CB: onIterationEnd(iteration)
    LS->>CB: onError(message) [if error]
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Ralph/feat/implement.ralph #1571: Related changes to the loop subsystem — types, domain, and service updates overlapping streaming/callback additions.
  • Release 0.42.0 #1577: Modifies loop config and service behavior (sandbox/auth fields) that intersect with this PR’s config/validation changes.
  • ralph/chore/fix.tests #1578: Alters loop execution and test mocks; touches same test fixtures and sandbox-related behavior.

Suggested reviewers

  • eyaltoledano
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title claims to add a '--stream flag' but the changeset actually introduces '--verbose' and '--no-output' flags as the primary CLI additions; no '--stream' flag is present in the code changes. Correct the title to accurately reflect the actual changes: e.g., 'feat(loop): add verbose streaming and output control flags' or rename to match the '--verbose' and '--no-output' flags that are actually implemented.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

cursor[bot]

This comment was marked as outdated.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@apps/extension/package.json`:
- Line 278: The dependency "task-master-ai" in package.json is pinned to
"0.42.0-rc.0" causing workspace linking to resolve externally; update the
version spec for "task-master-ai" to "*" (matching other internal workspace
members like "@tm/core") so npm resolves the local workspace member instead of
the registry.

In `@CHANGELOG.md`:
- Around line 3-99: Add a Minor Changes entry for PR `#1605` to both the 0.42.0
and 0.42.0-rc.0 sections in CHANGELOG.md following the existing pattern ("-
[`#PR`](...) [`commit`] Thanks [`@author`]! - Description") that highlights the new
loop flags: add a bullet like "Add --stream and --no-output options to loop
command to stream live output or suppress output for unattended runs" (mirror
the exact text into both sections), include the PR link and commit hash
placeholder, and ensure it's placed alongside other "Minor Changes" entries
under the 0.42.0 and 0.42.0-rc.0 headings.

In `@packages/tm-core/src/modules/loop/services/loop.service.ts`:
- Around line 594-608: In handleStreamEvent, stop injecting extra newlines into
streamed chunks: remove the concatenated '\n' when calling onText (use
onText(block.text)) and stop logging with added line breaks (log the raw
block.text or remove the console.log) so chunks are emitted exactly as received;
ensure only existing newlines in block.text are preserved so parseCompletion can
match tags like <loop-complete> correctly.

@Crunchyman-ralph Crunchyman-ralph force-pushed the ralph/feat/improve.loop.output branch from 8a888e6 to da17063 Compare January 23, 2026 16:11
cursor[bot]

This comment was marked as outdated.

- Add --stream flag for real-time output display using stream-json format
- Add --no-output flag to exclude full output from iteration results
- Add validation rejecting incompatible stream + sandbox combination
- Fix race condition between error/close events with resolveOnce wrapper
- Fix null safety check for child.stdout before attaching listeners
- Fix race condition in buffer processing with proper stdout 'end' handling
- Add JSON parse error logging for debugging malformed events
- Add event structure validation before accessing properties
- Add stream cleanup on error (remove listeners, kill process)
- Enhance JSDoc to document config/result field relationships
@Crunchyman-ralph Crunchyman-ralph force-pushed the ralph/feat/improve.loop.output branch from da17063 to 5657152 Compare January 23, 2026 16:23
@Crunchyman-ralph Crunchyman-ralph changed the base branch from main to next January 23, 2026 16:24
cursor[bot]

This comment was marked as outdated.

- Add LoopOutputCallbacks interface for presentation layer separation
- Move all console.log/error from loop.service.ts to CLI callbacks
- CLI provides chalk-formatted callback implementations
- Move stream+sandbox validation to run() start (fail once, not per iteration)
- Simplify streaming: use result event for output, onText for display only
- Export LoopOutputCallbacks from tm-core public API

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/cli/src/commands/loop.command.ts`:
- Around line 183-187: The onIterationStart callback in loop.command.ts has
formatting issues flagged by the Biome formatter; run the formatter (biome
format .) or reformat the onIterationStart block (the anonymous function
handling iteration logging in the loop command) to match project
style/whitespace conventions so CI passes—ensure the console.log and chalk.cyan
lines are formatted according to Biome rules and then commit the formatted file.
♻️ Duplicate comments (3)
packages/tm-core/src/modules/loop/services/loop.service.ts (3)

417-426: Child process not killed when stdout is null.

When child.stdout is null, the code resolves with an error but the spawned process at line 380 continues running. Add child.kill('SIGTERM') before returning to prevent leaking the process.

🐛 Proposed fix
 			// Handle null stdout (shouldn't happen with pipe, but be defensive)
 			if (!child.stdout) {
+				child.kill('SIGTERM');
 				resolveOnce(
 					this.createErrorIteration(
 						iterationNum,
 						startTime,
 						'Failed to capture stdout from child process'
 					)
 				);
 				return;
 			}

479-483: Clear buffer after processing in close handler to prevent duplicates.

If close fires before end, the buffer is processed but not cleared. If end subsequently fires, the same content could be processed again. Clear the buffer after processing.

🐛 Proposed fix
 			child.on('close', (exitCode: number | null) => {
 				// Process remaining buffer only if stdout hasn't already ended
 				if (!stdoutEnded && buffer) {
 					processLine(buffer);
+					buffer = '';
 				}

454-457: Streaming mode omits stderr from captured output.

The LoopIteration.output documentation states it "Contains concatenated stdout and stderr." However, in streaming mode, stderr is only sent to the callback (line 456) but not accumulated into finalResult. When includeOutput=true, the returned output will be incomplete.

Consider accumulating stderr alongside the result events:

🐛 Proposed fix
+			let stderrOutput = '';
+
 			child.stderr?.on('data', (data: Buffer) => {
 				const stderrText = data.toString('utf-8');
 				callbacks?.onStderr?.(stderrText, iterationNum);
+				stderrOutput += stderrText;
 			});
 
 			// ... in close handler:
 			resolveOnce({
 				iteration: iterationNum,
 				status,
 				duration: Date.now() - startTime,
 				message,
-				...(includeOutput && { output: finalResult })
+				...(includeOutput && { output: finalResult + stderrOutput })
 			});

Also applies to: 496-503

// Capture final result for includeOutput feature
if (event.type === 'result') {
finalResult = typeof event.result === 'string' ? event.result : '';
}
Copy link

Choose a reason for hiding this comment

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

Streaming mode fails to detect loop completion markers

High Severity

In streaming mode, finalResult is only populated from event.type === 'result' events, but the <loop-complete> and <loop-blocked> markers that Claude outputs appear in assistant event text content. The handleStreamEvent function displays this text via callbacks but doesn't accumulate it. When parseCompletion(finalResult, exitCode) runs on close, the markers won't be found, causing the loop to continue running when it should have stopped at 'all_complete' or 'blocked' status.

Additional Locations (1)

Fix in Cursor Fix in Web

@Crunchyman-ralph Crunchyman-ralph merged commit efedc85 into next Jan 25, 2026
11 of 12 checks passed
@Crunchyman-ralph Crunchyman-ralph deleted the ralph/feat/improve.loop.output branch January 25, 2026 10:50
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

)
);
return;
}
Copy link

Choose a reason for hiding this comment

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

Spawned process not killed when stdout check fails

Medium Severity

In executeVerboseIteration, when child.stdout is unexpectedly null, the code returns early with an error but does not kill the spawned child process. The process was already started at line 403 via spawn(). Without calling child.kill() before returning, the Claude or Docker process continues running as an orphan, causing a resource leak.

Fix in Cursor Fix in Web

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