diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..71f41ee --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,18 @@ +{ + "permissions": { + "allow": [ + "Bash(stack build)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(stack test)", + "Bash(stack test:*)", + "Bash(find:*)", + "Bash(stack build:*)", + "Bash(mkdir:*)", + "Bash(git rev-parse:*)", + "Bash(git remote get-url:*)" + ], + "deny": [], + "ask": [] + } +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e4cea9a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,83 @@ +# Claude Code Workflow Instructions for taskrunner + +## Project Overview +This is a Haskell project that implements a task runner with caching, parallel execution, and remote storage capabilities. It uses Stack for build management and tasty-golden for snapshot testing. + +## Project Structure +- `src/` - Haskell source code (main library) +- `app/` - Executable main entry point +- `test/` - Test suite + - `test/t/` - Golden test files (`.txt` input, `.out` expected output) + - `test/Spec.hs` - Main test runner + - `test/FakeGithubApi.hs` - Mock GitHub API for testing +- `package.yaml` - Haskell package configuration (Stack format) +- `stack.yaml` - Stack resolver and build configuration +- `taskrunner.cabal` - Generated cabal file (don't edit directly) + +## Build and Development Workflow + +### Building the Project +```bash +stack build +``` + +### Running Tests +```bash +# Run tests (auto-detects S3 credentials and skips S3 tests if missing) +stack test + +# Run tests, skipping slow ones +export SKIP_SLOW_TESTS=1 +stack test + +# Run specific test by pattern +stack test --test-arguments "--pattern hello" + +# List all available tests +stack test --test-arguments "--list-tests" +``` + +### Accepting Golden Test Changes +When golden tests fail due to expected output changes: +```bash +stack test --test-arguments --accept +``` + +### Test Structure +- Test files are in `test/t/` directory +- Each test has: + - `.txt` file - shell script to execute + - `.out` file - expected output (golden file) +- Tests run through the taskrunner executable +- Special comments in `.txt` files control test behavior: + - `# check output` - check stdout/stderr + - `# check github` - check GitHub API calls + - `# no toplevel` - don't wrap in taskrunner + - `# s3` - enable S3 testing + - `# github keys` - provide GitHub credentials + +## Key Commands for Development + +### Building +- `stack build` - Build the project +- `stack build --fast` - Fast build (less optimization) +- `stack clean` - Clean build artifacts + +### Testing +- `stack test` - Run all tests +- `stack test --test-arguments --accept` - Accept golden test changes +- `SKIP_SLOW_TESTS=1 stack test` - Skip slow tests + +### Running the executable +- `stack exec taskrunner -- [args]` - Run the built executable +- `stack run -- [args]` - Build and run in one command + +## Notes +- This project uses tasty-golden for snapshot/golden file testing +- The test suite includes integration tests that verify taskrunner behavior +- **S3 Test Auto-Detection**: 15 tests require S3 credentials (marked with `# s3` directive in test files) + - `stack test` automatically skips S3 tests if credentials are missing + - To run S3 tests, set: `TASKRUNNER_TEST_S3_ENDPOINT`, `TASKRUNNER_TEST_S3_ACCESS_KEY`, `TASKRUNNER_TEST_S3_SECRET_KEY` +- GitHub tests use a fake API server and don't require real GitHub credentials +- The project uses Universum as an alternative Prelude +- Build output and temporary files are in `.stack-work/` \ No newline at end of file diff --git a/README.md b/README.md index fbec0cf..22cb060 100644 --- a/README.md +++ b/README.md @@ -144,13 +144,61 @@ The `snapshot` command supports the following flags: - `--long-running`: Indicates that the task is expected to run for a long time (e.g. a server). Currently doens't have any effect though, TODO: can we remove it? -## Tests: Update Golden Files +## Testing This project uses [tasty-golden](https://github.com/UnkindPartition/tasty-golden) for snapshot-based testing. -To update the golden files, run the test suite with the `--accept` flag passed to the test executable. -If you're using stack, the full command is: +### Running Tests -```sh +```bash +# Run all tests (auto-detects S3 credentials) +stack test + +# Run tests, skipping slow ones for faster development +export SKIP_SLOW_TESTS=1 +stack test + +# Run specific test by pattern +stack test --test-arguments "--pattern hello" + +# List all available tests +stack test --test-arguments "--list-tests" +``` + +### Test Structure + +Tests are located in `test/t/` directory with two files per test: +- `.txt` file - Shell script to execute +- `.out` file - Expected output (golden file) + +#### Test Directives + +Special comments in `.txt` files control test behavior: +- `# check output` - Check stdout/stderr (default) +- `# check github` - Check GitHub API calls +- `# no toplevel` - Don't wrap in taskrunner +- `# s3` - Requires S3 credentials (auto-skipped if missing) +- `# github keys` - Provide GitHub credentials +- `# quiet` - Run in quiet mode + +### S3 Test Auto-Detection + +15 tests require S3 credentials and are automatically skipped if credentials are missing. + +To run S3 tests, set these environment variables: +```bash +export TASKRUNNER_TEST_S3_ENDPOINT=your-s3-endpoint +export TASKRUNNER_TEST_S3_ACCESS_KEY=your-access-key +export TASKRUNNER_TEST_S3_SECRET_KEY=your-secret-key +stack test +``` + +### Accepting Golden Test Changes + +When golden tests fail due to expected output changes: + +```bash stack test --test-arguments --accept ``` + +This updates the `.out` files with new expected output. Review changes carefully before committing. diff --git a/src/App.hs b/src/App.hs index c8d840b..8884c43 100644 --- a/src/App.hs +++ b/src/App.hs @@ -62,6 +62,7 @@ getSettings = do fuzzyCacheFallbackBranches <- maybe [] (Text.words . toText) <$> lookupEnv "TASKRUNNER_FALLBACK_BRANCHES" primeCacheMode <- (==Just "1") <$> lookupEnv "TASKRUNNER_PRIME_CACHE_MODE" mainBranch <- map toText <$> lookupEnv "TASKRUNNER_MAIN_BRANCH" + quietMode <- (==Just "1") <$> lookupEnv "TASKRUNNER_QUIET" pure Settings { stateDirectory , rootDirectory @@ -76,6 +77,7 @@ getSettings = do , primeCacheMode , mainBranch , force = False + , quietMode } main :: IO () @@ -129,7 +131,7 @@ main = do -- Recursive: AppState is used before process is started (mostly for logging) rec - appState <- AppState settings jobName buildId isToplevel <$> newIORef Nothing <*> newIORef Nothing <*> newIORef False <*> pure toplevelStderr <*> pure subprocessStderr <*> pure logFile + appState <- AppState settings jobName buildId isToplevel <$> newIORef Nothing <*> newIORef Nothing <*> newIORef False <*> pure toplevelStderr <*> pure subprocessStderr <*> pure logFile <*> newIORef [] <*> newIORef Nothing when (isToplevel && appState.settings.enableCommitStatus) do @@ -171,6 +173,12 @@ main = do skipped <- readIORef appState.skipped + -- Handle quiet mode buffer based on exit code + when appState.settings.quietMode do + if exitCode == ExitSuccess + then discardQuietBuffer appState -- Success: discard buffered output + else flushQuietBuffer appState toplevelStderr -- Failure: show buffered output + logDebug appState $ "Command " <> show (args.cmd : args.args) <> " exited with code " <> show exitCode logDebugParent m_parentRequestPipe $ "Subtask " <> toText jobName <> " finished with " <> show exitCode diff --git a/src/Types.hs b/src/Types.hs index 24c13c0..8ee2230 100644 --- a/src/Types.hs +++ b/src/Types.hs @@ -19,6 +19,7 @@ data Settings = Settings , primeCacheMode :: Bool , mainBranch :: Maybe Text , force :: Bool + , quietMode :: Bool } deriving (Show) type JobName = String @@ -49,7 +50,8 @@ data AppState = AppState , toplevelStderr :: Handle , subprocessStderr :: Handle , logOutput :: Handle - + , quietBuffer :: IORef [ByteString] + -- | Lazily initialized Github client , githubClient :: IORef (Maybe GithubClient) } diff --git a/src/Utils.hs b/src/Utils.hs index 508644d..565879d 100644 --- a/src/Utils.hs +++ b/src/Utils.hs @@ -35,7 +35,14 @@ outputLine appState toplevelOutput streamName line = do | otherwise = True when shouldOutputToToplevel do - B8.hPutStrLn toplevelOutput $ timestampStr <> "[" <> jobName <> "] " <> streamName <> " | " <> line + let formattedLine = timestampStr <> "[" <> jobName <> "] " <> streamName <> " | " <> line + if appState.settings.quietMode + then do + -- In quiet mode, add to buffer instead of outputting immediately + modifyIORef appState.quietBuffer (formattedLine :) + else + -- Normal mode: output immediately + B8.hPutStrLn toplevelOutput formattedLine logLevel :: MonadIO m => ByteString -> AppState -> Text -> m () logLevel level appState msg = @@ -121,3 +128,16 @@ getCurrentCommit _appState = logFileName :: Settings -> BuildId -> JobName -> FilePath logFileName settings buildId jobName = settings.stateDirectory "builds" toString buildId "logs" (jobName <> ".log") + +-- | Flush buffered output to terminal (used when task fails in quiet mode) +flushQuietBuffer :: AppState -> Handle -> IO () +flushQuietBuffer appState toplevelOutput = do + buffer <- readIORef appState.quietBuffer + -- Output in correct order (buffer was built in reverse) + mapM_ (B8.hPutStrLn toplevelOutput) (reverse buffer) + -- Clear the buffer after flushing + writeIORef appState.quietBuffer [] + +-- | Discard buffered output (used when task succeeds in quiet mode) +discardQuietBuffer :: AppState -> IO () +discardQuietBuffer appState = writeIORef appState.quietBuffer [] diff --git a/test/Spec.hs b/test/Spec.hs index cc9d562..357c402 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -37,10 +37,34 @@ fakeGithubPort = 12345 goldenTests :: IO TestTree goldenTests = do skipSlow <- (==Just "1") <$> lookupEnv "SKIP_SLOW_TESTS" + skipS3Explicit <- (==Just "1") <$> lookupEnv "SKIP_S3_TESTS" + hasS3Creds <- hasS3Credentials + let skipS3 = skipS3Explicit || not hasS3Creds + inputFiles0 <- sort <$> findByExtension [".txt"] "test/t" + inputFiles1 <- if skipS3 + then filterM (fmap not . hasS3Directive) inputFiles0 + else pure inputFiles0 let inputFiles - | skipSlow = filter (\filename -> not ("/slow/" `isInfixOf` filename)) inputFiles0 - | otherwise = inputFiles0 + | skipSlow = filter (\filename -> not ("/slow/" `isInfixOf` filename)) inputFiles1 + | otherwise = inputFiles1 + + -- Print informative message about what tests are running + let totalTests = length inputFiles0 + s3Tests = length inputFiles0 - length inputFiles1 + slowTests = length inputFiles1 - length inputFiles + runningTests = length inputFiles + + when (skipS3 && s3Tests > 0) $ do + if skipS3Explicit + then System.IO.putStrLn $ "SKIP_S3_TESTS=1 - skipping " <> show s3Tests <> " S3-dependent tests" + else System.IO.putStrLn $ "S3 credentials not found - skipping " <> show s3Tests <> " S3-dependent tests" + System.IO.putStrLn $ "To run S3 tests, set: TASKRUNNER_TEST_S3_ENDPOINT, TASKRUNNER_TEST_S3_ACCESS_KEY, TASKRUNNER_TEST_S3_SECRET_KEY" + + when (skipSlow && slowTests > 0) $ + System.IO.putStrLn $ "SKIP_SLOW_TESTS=1 - skipping " <> show slowTests <> " slow tests" + + System.IO.putStrLn $ "Running " <> show runningTests <> "/" <> show totalTests <> " tests" pure $ Tasty.withResource (FakeGithubApi.start fakeGithubPort) FakeGithubApi.stop \fakeGithubServer -> testGroup "tests" [ goldenVsStringDiff @@ -105,6 +129,9 @@ runTest fakeGithubServer source = do , ("GITHUB_REPOSITORY_OWNER", "fakeowner") , ("GITHUB_REPOSITORY", "fakerepo") ] <> + mwhen options.quiet + [ ("TASKRUNNER_QUIET", "1") + ] <> s3ExtraEnv) , cwd = Just dir } \_ _ _ processHandle -> do @@ -142,6 +169,7 @@ data Options = Options -- | Whether to provide GitHub app credentials in environment. -- If github status is disabled, taskrunner should work without them. , githubKeys :: Bool + , quiet :: Bool } instance Default Options where @@ -150,6 +178,7 @@ instance Default Options where , toplevel = True , s3 = False , githubKeys = False + , quiet = False } getOptions :: Text -> Options @@ -169,6 +198,9 @@ getOptions source = flip execState def $ go (lines source) ["#", "github", "keys"] -> do modify (\s -> s { githubKeys = True }) go rest + ["#", "quiet"] -> do + modify (\s -> (s :: Options) { quiet = True }) + go rest -- TODO: validate? _ -> -- stop iteration @@ -213,3 +245,16 @@ maybeWithBucket Options{s3=True} block = do mwhen :: Monoid a => Bool -> a -> a mwhen True x = x mwhen False _ = mempty + +hasS3Directive :: FilePath -> IO Bool +hasS3Directive file = do + content <- System.IO.readFile file + let options = getOptions (toText content) + pure options.s3 + +hasS3Credentials :: IO Bool +hasS3Credentials = do + endpoint <- lookupEnv "TASKRUNNER_TEST_S3_ENDPOINT" + accessKey <- lookupEnv "TASKRUNNER_TEST_S3_ACCESS_KEY" + secretKey <- lookupEnv "TASKRUNNER_TEST_S3_SECRET_KEY" + pure $ isJust endpoint && isJust accessKey && isJust secretKey diff --git a/test/t/quiet-mode-failure.out b/test/t/quiet-mode-failure.out new file mode 100644 index 0000000..8db1cb3 --- /dev/null +++ b/test/t/quiet-mode-failure.out @@ -0,0 +1,4 @@ +-- output: +[toplevel] stdout | This output should be shown because the command fails +[toplevel] stdout | Second line of output +-- exit code: 1 diff --git a/test/t/quiet-mode-failure.txt b/test/t/quiet-mode-failure.txt new file mode 100644 index 0000000..d00eee2 --- /dev/null +++ b/test/t/quiet-mode-failure.txt @@ -0,0 +1,4 @@ +# quiet +echo "This output should be shown because the command fails" +echo "Second line of output" +exit 1 \ No newline at end of file diff --git a/test/t/quiet-mode-nested-child-fail.out b/test/t/quiet-mode-nested-child-fail.out new file mode 100644 index 0000000..b640c06 --- /dev/null +++ b/test/t/quiet-mode-nested-child-fail.out @@ -0,0 +1,2 @@ +-- output: +[nested] stdout | Nested output before failure diff --git a/test/t/quiet-mode-nested-child-fail.txt b/test/t/quiet-mode-nested-child-fail.txt new file mode 100644 index 0000000..07ad489 --- /dev/null +++ b/test/t/quiet-mode-nested-child-fail.txt @@ -0,0 +1,4 @@ +# quiet +echo "Toplevel output before nested call" +taskrunner -n nested sh -c 'echo "Nested output before failure"; exit 1' || echo "Handling nested failure" +echo "Toplevel continues after nested failure" \ No newline at end of file diff --git a/test/t/quiet-mode-nested-parent-fail.out b/test/t/quiet-mode-nested-parent-fail.out new file mode 100644 index 0000000..9658a95 --- /dev/null +++ b/test/t/quiet-mode-nested-parent-fail.out @@ -0,0 +1,5 @@ +-- output: +[toplevel] stdout | Toplevel output before nested call +[toplevel] stdout | Toplevel output after nested call +[toplevel] stdout | This is the last line before failure +-- exit code: 1 diff --git a/test/t/quiet-mode-nested-parent-fail.txt b/test/t/quiet-mode-nested-parent-fail.txt new file mode 100644 index 0000000..44f8a35 --- /dev/null +++ b/test/t/quiet-mode-nested-parent-fail.txt @@ -0,0 +1,6 @@ +# quiet +echo "Toplevel output before nested call" +taskrunner -n nested echo "Nested task succeeds" +echo "Toplevel output after nested call" +echo "This is the last line before failure" +exit 1 \ No newline at end of file diff --git a/test/t/quiet-mode-nested-success.out b/test/t/quiet-mode-nested-success.out new file mode 100644 index 0000000..3c9fa1f --- /dev/null +++ b/test/t/quiet-mode-nested-success.out @@ -0,0 +1 @@ +-- output: diff --git a/test/t/quiet-mode-nested-success.txt b/test/t/quiet-mode-nested-success.txt new file mode 100644 index 0000000..9778c70 --- /dev/null +++ b/test/t/quiet-mode-nested-success.txt @@ -0,0 +1,5 @@ +# quiet +echo "Toplevel output (should be hidden)" +taskrunner -n nested echo "Nested output (should also be hidden)" +echo "More toplevel output (should be hidden)" +taskrunner -n deeper sh -c 'echo "Deep nested (should be hidden)"' \ No newline at end of file diff --git a/test/t/quiet-mode-success.out b/test/t/quiet-mode-success.out new file mode 100644 index 0000000..3c9fa1f --- /dev/null +++ b/test/t/quiet-mode-success.out @@ -0,0 +1 @@ +-- output: diff --git a/test/t/quiet-mode-success.txt b/test/t/quiet-mode-success.txt new file mode 100644 index 0000000..ebec6b8 --- /dev/null +++ b/test/t/quiet-mode-success.txt @@ -0,0 +1,3 @@ +# quiet +echo "This output should be hidden in quiet mode because the command succeeds" +echo "Second line of output" \ No newline at end of file diff --git a/thoughts/shared/research/task-output-handling.md b/thoughts/shared/research/task-output-handling.md new file mode 100644 index 0000000..fc8a29b --- /dev/null +++ b/thoughts/shared/research/task-output-handling.md @@ -0,0 +1,252 @@ +--- +date: 2025-09-30T04:59:12.021Z +researcher: Claude +git_commit: 0492ba8e3b81af1fea412709e010a4e3f0f355ae +branch: quietmode +repository: taskrunner +topic: "Task Output Handling in taskrunner" +tags: [research, codebase, output-handling, logging, quiet-mode, agentic-workflows] +status: complete +last_updated: 2025-09-30 +last_updated_by: Claude +--- + +# Research: Task Output Handling in taskrunner + +**Date**: 2025-09-30T04:59:12.021Z +**Researcher**: Claude +**Git Commit**: 0492ba8e3b81af1fea412709e010a4e3f0f355ae +**Branch**: quietmode +**Repository**: taskrunner + +## Overview + +This document details how the taskrunner application handles output from tasks, including logging, streaming, output annotation mechanisms, and the quiet mode feature for agentic workflows. + +## Architecture + +The taskrunner uses a sophisticated multi-stream output handling system that: +1. Captures task stdout/stderr in real-time +2. Annotates output with task names and timestamps +3. Writes to both terminal and persistent log files +4. Supports nested task execution with proper output routing +5. Provides quiet mode for agentic workflows with conditional output display + +## Core Components + +### AppState Structure (`src/Types.hs`) + +The `AppState` contains four key output handles: +- `toplevelStderr :: Handle` - Terminal output for the main process +- `subprocessStderr :: Handle` - Error output for subprocess operations +- `logOutput :: Handle` - File handle for persistent logging +- `quietBuffer :: IORef [ByteString]` - Buffer for quiet mode output collection + +### Output Processing Flow + +#### 1. Process Creation (`src/App.hs:147-155`) + +When executing a task, taskrunner creates a subprocess with: +```haskell +(Nothing, Just stdoutPipe, Just stderrPipe, processHandle) <- Process.createProcess + (proc args.cmd args.args) { + std_in = UseHandle devnull, + std_out = CreatePipe, + std_err = CreatePipe + } +``` + +#### 2. Async Stream Handlers (`src/App.hs:164-168`) + +Three async handlers process different output streams: +```haskell +stdoutHandler <- async $ outputStreamHandler appState toplevelStdout "stdout" stdoutPipe +stderrHandler <- async $ outputStreamHandler appState toplevelStderr "stderr" stderrPipe +subprocessStderrHandler <- async $ outputStreamHandler appState toplevelStderr "stderr" subprocessStderrRead +``` + +#### 3. Stream Processing (`src/App.hs:353-357`) + +The `outputStreamHandler` reads lines and delegates to `outputLine`: +```haskell +outputStreamHandler :: AppState -> Handle -> ByteString -> Handle -> IO () +outputStreamHandler appState toplevelOutput streamName stream = do + handle ignoreEOF $ forever do + line <- B8.hGetLine stream + outputLine appState toplevelOutput streamName line +``` + +### Output Annotation (`src/Utils.hs:18-38`) + +The `outputLine` function is the core of output handling: + +#### Features: +1. **Timestamp Addition**: Optional timestamps with format `%T` (HH:MM:SS) +2. **Dual Logging**: Writes to both log file and terminal +3. **Task Name Annotation**: Prefixes output with `[jobName]` +4. **Stream Identification**: Labels output as `stdout`, `stderr`, `debug`, `info`, etc. +5. **Conditional Output**: Respects debug/info logging settings +6. **Quiet Mode Buffering**: Conditionally buffers output based on task success/failure + +#### Format: +- **Log file**: `{timestamp} {streamName} | {line}` +- **Terminal**: `{timestamp} [{jobName}] {streamName} | {line}` + +#### Example Output: +``` +14:23:42 [build-frontend] stdout | Building React components... +14:23:43 [build-frontend] stderr | Warning: Deprecated API usage +``` + +## Log File Management + +### Location (`src/Utils.hs:122-123`) +```haskell +logFileName :: Settings -> BuildId -> JobName -> FilePath +logFileName settings buildId jobName = + settings.stateDirectory "builds" toString buildId "logs" (jobName <> ".log") +``` + +### Structure +``` +$TASKRUNNER_STATE_DIRECTORY/ + builds/ + {buildId}/ + logs/ + {jobName}.log + results/ + {jobName} # Exit code +``` + +### File Properties +- **Line buffering** enabled for real-time writes +- **Binary mode** for proper encoding handling +- **Automatic closure** when task completes + +## Quiet Mode Feature + +### Overview +Quiet mode suppresses task output from the terminal unless the task fails, designed for agentic workflows to reduce noise and save tokens. + +### Behavior +- **Success**: Task output buffered and discarded, no terminal output +- **Failure**: Task output buffered and flushed to terminal for debugging +- **Always**: All output still written to log files regardless of mode + +### Implementation (`src/Utils.hs:39-45`) +```haskell +if appState.settings.quietMode + then do + -- In quiet mode, add to buffer instead of outputting immediately + modifyIORef appState.quietBuffer (formattedLine :) + else + -- Normal mode: output immediately + B8.hPutStrLn toplevelOutput formattedLine +``` + +### Buffer Management (`src/Utils.hs:133-143`) +- `flushQuietBuffer`: Outputs buffered content to terminal (on failure) +- `discardQuietBuffer`: Clears buffer without output (on success) +- Buffer stored in reverse order, flushed in correct chronological order + +### Exit Code Integration (`src/App.hs:176-180`) +```haskell +when appState.settings.quietMode do + if exitCode == ExitSuccess + then discardQuietBuffer appState -- Success: discard buffered output + else flushQuietBuffer appState toplevelStderr -- Failure: show buffered output +``` + +### Nested Task Behavior +- Each taskrunner process maintains its own quiet buffer +- Only the failing process flushes its buffer to terminal +- Successful nested tasks remain quiet even when parent fails +- Provides targeted debugging - shows output from exactly the failing component + +## Output Control Settings + +### Environment Variables +- `TASKRUNNER_DEBUG=1` - Include debug messages in terminal output +- `TASKRUNNER_LOG_INFO=1` - Include info messages in terminal output +- `TASKRUNNER_DISABLE_TIMESTAMPS=1` - Disable timestamp prefixes +- `TASKRUNNER_OUTPUT_STREAM_TIMEOUT=N` - Timeout for reading output streams +- `TASKRUNNER_QUIET=1` - Enable quiet mode (suppress output unless task fails) + +### Filtering Logic (`src/Utils.hs:32-38`) +```haskell +let shouldOutputToToplevel + | streamName == "debug" = appState.settings.logDebug + | streamName == "info" = appState.settings.logInfo + | otherwise = True +``` + +## Parallel Task Support + +### Task Name Flattening +- Nested tasks don't add repeated annotations +- All output maintains original task context +- Parent task logs reference nested task log files + +### Async Processing +- Each stream gets dedicated async handler +- Prevents blocking on individual stream delays +- Proper exception handling with `ignoreEOF` + +## Remote Cache Integration + +### Log Upload +When `uploadLogs` is enabled: +- Task logs uploaded to object store after completion +- GitHub check details link to uploaded logs +- Content-type preserved for proper rendering + +### Structure +- Logs uploaded with task hash as key +- Retrievable for debugging failed cache hits +- Integrated with commit status reporting + +## Error Handling + +### Stream Failures +- `ignoreEOF` handles normal stream closure +- Timeouts prevent hanging on unresponsive processes +- Graceful degradation when log files unavailable + +### Process Management +- Output handlers cancelled when main process exits +- Proper cleanup of file handles and pipes +- Exception propagation for critical failures + +## Key Design Principles + +1. **Real-time Output**: No buffering delays for user feedback (except in quiet mode) +2. **Comprehensive Logging**: Everything logged for debugging +3. **Nested Task Support**: Proper routing for complex workflows +4. **Configurable Verbosity**: Users control output detail level +5. **Parallel Safety**: Concurrent tasks don't interfere +6. **Remote Integration**: Logs available for CI/CD analysis +7. **Agentic Workflow Support**: Quiet mode for clean, token-efficient automation + +## Implementation Notes + +### Code Locations +- Main logic: `src/App.hs` (lines 147-168, 353-357) +- Output formatting: `src/Utils.hs` (lines 18-45) +- Quiet mode buffer management: `src/Utils.hs` (lines 133-143) +- Exit code integration: `src/App.hs` (lines 176-180) +- Type definitions: `src/Types.hs` (lines 41-57) +- Log utilities: `src/Utils.hs` (lines 122-123) + +### Dependencies +- `async` for concurrent stream processing +- `System.Process` for subprocess management +- `System.IO` for handle operations +- `Data.ByteString.Char8` for efficient line processing + +### Performance Considerations +- Line-by-line processing minimizes memory usage +- Binary mode avoids encoding overhead +- Async handlers prevent stream blocking +- Buffering disabled for real-time output (except quiet mode) +- Quiet mode buffer size naturally limited by task output volume +- Buffer memory freed immediately after task completion \ No newline at end of file