From ae2d59bda78d52712c2512da04fef0f4c053b8b0 Mon Sep 17 00:00:00 2001 From: Maciej Bielecki Date: Mon, 29 Sep 2025 12:05:49 +0000 Subject: [PATCH 1/9] Add workflow documentation in CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents project structure, build commands, testing workflow, and development setup for future reference. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..dbc248c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,80 @@ +# 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 all tests (may be slow) +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 +- Some tests require S3 credentials and GitHub API tokens (set via environment variables) +- The project uses Universum as an alternative Prelude +- Build output and temporary files are in `.stack-work/` \ No newline at end of file From 7146e53549c27eef52aae1b216c1b5a825f8ca85 Mon Sep 17 00:00:00 2001 From: Maciej Bielecki Date: Mon, 29 Sep 2025 12:15:04 +0000 Subject: [PATCH 2/9] Add SKIP_S3_TESTS option to test runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tests marked with `# s3` directive now skippable via SKIP_S3_TESTS=1 - Allows running 35/50 tests without S3 credentials - Updated CLAUDE.md with usage instructions and S3 requirements 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 15 +++++++++++++-- test/Spec.hs | 14 ++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index dbc248c..a2c6123 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,13 +23,21 @@ stack build ### Running Tests ```bash -# Run all tests (may be slow) +# Run all tests (may be slow, requires S3 setup) stack test # Run tests, skipping slow ones export SKIP_SLOW_TESTS=1 stack test +# Run tests, skipping S3 tests (recommended if no local S3/minio) +export SKIP_S3_TESTS=1 +stack test + +# Skip both slow and S3 tests for fastest development +export SKIP_SLOW_TESTS=1 SKIP_S3_TESTS=1 +stack test + # Run specific test by pattern stack test --test-arguments "--pattern hello" @@ -75,6 +83,9 @@ stack test --test-arguments --accept ## Notes - This project uses tasty-golden for snapshot/golden file testing - The test suite includes integration tests that verify taskrunner behavior -- Some tests require S3 credentials and GitHub API tokens (set via environment variables) +- 15 tests require S3 credentials (marked with `# s3` directive in test files): + - Set `SKIP_S3_TESTS=1` to skip these tests if you don't have local S3/minio configured + - S3 tests require: `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/test/Spec.hs b/test/Spec.hs index cc9d562..54ef298 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -37,10 +37,14 @@ fakeGithubPort = 12345 goldenTests :: IO TestTree goldenTests = do skipSlow <- (==Just "1") <$> lookupEnv "SKIP_SLOW_TESTS" + skipS3 <- (==Just "1") <$> lookupEnv "SKIP_S3_TESTS" 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 pure $ Tasty.withResource (FakeGithubApi.start fakeGithubPort) FakeGithubApi.stop \fakeGithubServer -> testGroup "tests" [ goldenVsStringDiff @@ -213,3 +217,9 @@ 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 From 0e58767ff7e8c68789528faa00dcdb5d9c49d9fe Mon Sep 17 00:00:00 2001 From: Maciej Bielecki Date: Mon, 29 Sep 2025 12:16:17 +0000 Subject: [PATCH 3/9] Add local settings for Claude permissions This commit introduces a new configuration file, settings.local.json, which defines the permissions for various Bash commands used in the Claude environment. The allowed commands include stack build, git add, git commit, stack test, find, and stack build with specific arguments. No commands are currently denied or require confirmation. --- .claude/settings.local.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..45be412 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,14 @@ +{ + "permissions": { + "allow": [ + "Bash(stack build)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(stack test)", + "Bash(find:*)", + "Bash(stack build:*)" + ], + "deny": [], + "ask": [] + } +} \ No newline at end of file From 69bfb25d7065170dab2e131ecbc00ebd7af63378 Mon Sep 17 00:00:00 2001 From: Maciej Bielecki Date: Mon, 29 Sep 2025 13:17:35 +0000 Subject: [PATCH 4/9] Auto-detect S3 credentials and skip tests with clear reporting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tests automatically skip when S3 environment variables are missing - Clear informative messages explain test execution (e.g. "Running 35/50 tests") - Provides guidance on enabling S3 tests when credentials are absent - Maintains backward compatibility with SKIP_S3_TESTS=1 override - Enables agentic workflows to use `stack test` without environment exports 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 11 ++++++----- test/Spec.hs | 29 ++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a2c6123..37fece7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,14 +23,14 @@ stack build ### Running Tests ```bash -# Run all tests (may be slow, requires S3 setup) +# 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 tests, skipping S3 tests (recommended if no local S3/minio) +# Explicitly skip S3 tests even if credentials are present export SKIP_S3_TESTS=1 stack test @@ -83,9 +83,10 @@ stack test --test-arguments --accept ## Notes - This project uses tasty-golden for snapshot/golden file testing - The test suite includes integration tests that verify taskrunner behavior -- 15 tests require S3 credentials (marked with `# s3` directive in test files): - - Set `SKIP_S3_TESTS=1` to skip these tests if you don't have local S3/minio configured - - S3 tests require: `TASKRUNNER_TEST_S3_ENDPOINT`, `TASKRUNNER_TEST_S3_ACCESS_KEY`, `TASKRUNNER_TEST_S3_SECRET_KEY` +- **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` + - Use `SKIP_S3_TESTS=1` to explicitly skip S3 tests even when credentials are present - 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/test/Spec.hs b/test/Spec.hs index 54ef298..4e75c67 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -37,7 +37,10 @@ fakeGithubPort = 12345 goldenTests :: IO TestTree goldenTests = do skipSlow <- (==Just "1") <$> lookupEnv "SKIP_SLOW_TESTS" - skipS3 <- (==Just "1") <$> lookupEnv "SKIP_S3_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 @@ -45,6 +48,23 @@ goldenTests = do let inputFiles | 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 @@ -223,3 +243,10 @@ 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 From 1befeb16b9b60ce0e07fd7250ad4585483814f01 Mon Sep 17 00:00:00 2001 From: Maciej Bielecki Date: Mon, 29 Sep 2025 16:11:54 +0000 Subject: [PATCH 5/9] Add quiet mode feature for agentic workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Task output suppressed from terminal unless task fails - Successful tasks: output goes only to log files (clean terminal) - Failed tasks: full buffered output displayed for debugging - Enabled via TASKRUNNER_QUIET=1 environment variable - Added # quiet test directive and comprehensive test coverage - All existing functionality preserved, no regressions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/App.hs | 10 +++++++++- src/Types.hs | 4 +++- src/Utils.hs | 22 +++++++++++++++++++++- test/Spec.hs | 8 ++++++++ test/t/quiet-mode-failure.out | 4 ++++ test/t/quiet-mode-failure.txt | 4 ++++ test/t/quiet-mode-success.out | 1 + test/t/quiet-mode-success.txt | 3 +++ 8 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 test/t/quiet-mode-failure.out create mode 100644 test/t/quiet-mode-failure.txt create mode 100644 test/t/quiet-mode-success.out create mode 100644 test/t/quiet-mode-success.txt 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 4e75c67..357c402 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -129,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 @@ -166,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 @@ -174,6 +178,7 @@ instance Default Options where , toplevel = True , s3 = False , githubKeys = False + , quiet = False } getOptions :: Text -> Options @@ -193,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 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-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 From e16cd3b77a08f6e97abc7fa8eada8335f48114c8 Mon Sep 17 00:00:00 2001 From: Maciej Bielecki Date: Mon, 29 Sep 2025 16:27:04 +0000 Subject: [PATCH 6/9] Add comprehensive nested task tests for quiet mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - quiet-mode-nested-success: No output when all nested tasks succeed - quiet-mode-nested-parent-fail: Shows parent output when parent fails - quiet-mode-nested-child-fail: Shows child output when child fails - Validates that quiet mode works correctly in complex nested scenarios - Each task process maintains its own buffer, only failing process shows output 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- test/t/quiet-mode-nested-child-fail.out | 2 ++ test/t/quiet-mode-nested-child-fail.txt | 4 ++++ test/t/quiet-mode-nested-parent-fail.out | 5 +++++ test/t/quiet-mode-nested-parent-fail.txt | 6 ++++++ test/t/quiet-mode-nested-success.out | 1 + test/t/quiet-mode-nested-success.txt | 5 +++++ 6 files changed, 23 insertions(+) create mode 100644 test/t/quiet-mode-nested-child-fail.out create mode 100644 test/t/quiet-mode-nested-child-fail.txt create mode 100644 test/t/quiet-mode-nested-parent-fail.out create mode 100644 test/t/quiet-mode-nested-parent-fail.txt create mode 100644 test/t/quiet-mode-nested-success.out create mode 100644 test/t/quiet-mode-nested-success.txt 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 From 082318c956cc6616c00412b7001b0753bcccf563 Mon Sep 17 00:00:00 2001 From: Maciej Bielecki Date: Mon, 29 Sep 2025 16:29:13 +0000 Subject: [PATCH 7/9] Update Claude settings and add research documentation --- .claude/settings.local.json | 7 +- .../shared/research/task-output-handling.md | 184 ++++++++++++++++++ 2 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 thoughts/shared/research/task-output-handling.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 45be412..68040f9 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -5,10 +5,13 @@ "Bash(git add:*)", "Bash(git commit:*)", "Bash(stack test)", + "Bash(stack test:*)", "Bash(find:*)", - "Bash(stack build:*)" + "Bash(stack build:*)", + "Bash(mkdir:*)" ], "deny": [], "ask": [] } -} \ 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..0ef493e --- /dev/null +++ b/thoughts/shared/research/task-output-handling.md @@ -0,0 +1,184 @@ +# Task Output Handling in taskrunner + +## Overview + +This document details how the taskrunner application handles output from tasks, including logging, streaming, and output annotation mechanisms. + +## 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 + +## Core Components + +### AppState Structure (`src/Types.hs`) + +The `AppState` contains three 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 + +### 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 + +#### 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 + +## 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 + +### 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 +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 + +## Implementation Notes + +### Code Locations +- Main logic: `src/App.hs` (lines 147-168, 353-357) +- Output formatting: `src/Utils.hs` (lines 18-38) +- Type definitions: `src/Types.hs` (lines 41-55) +- 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 \ No newline at end of file From 0492ba8e3b81af1fea412709e010a4e3f0f355ae Mon Sep 17 00:00:00 2001 From: Maciej Bielecki Date: Tue, 30 Sep 2025 04:57:57 +0000 Subject: [PATCH 8/9] Refactor testing documentation and clarify quiet mode feature - Updated CLAUDE.md to remove explicit S3 test skipping section. - Revised testing commands in README.md for consistency and clarity. - Expanded test structure and directives information. - Introduced a comprehensive section on the new quiet mode feature in task output handling. - Enhanced explanations for output behavior in quiet mode and its integration with nested tasks and exit codes in task-output-handling.md. --- CLAUDE.md | 9 --- README.md | 56 +++++++++++++++-- .../shared/research/task-output-handling.md | 61 +++++++++++++++++-- 3 files changed, 107 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 37fece7..e4cea9a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,14 +30,6 @@ stack test export SKIP_SLOW_TESTS=1 stack test -# Explicitly skip S3 tests even if credentials are present -export SKIP_S3_TESTS=1 -stack test - -# Skip both slow and S3 tests for fastest development -export SKIP_SLOW_TESTS=1 SKIP_S3_TESTS=1 -stack test - # Run specific test by pattern stack test --test-arguments "--pattern hello" @@ -86,7 +78,6 @@ stack test --test-arguments --accept - **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` - - Use `SKIP_S3_TESTS=1` to explicitly skip S3 tests even when credentials are present - 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/thoughts/shared/research/task-output-handling.md b/thoughts/shared/research/task-output-handling.md index 0ef493e..195e8be 100644 --- a/thoughts/shared/research/task-output-handling.md +++ b/thoughts/shared/research/task-output-handling.md @@ -2,7 +2,7 @@ ## Overview -This document details how the taskrunner application handles output from tasks, including logging, streaming, and output annotation mechanisms. +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 @@ -11,15 +11,17 @@ The taskrunner uses a sophisticated multi-stream output handling system that: 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 three key output handles: +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 @@ -65,6 +67,7 @@ The `outputLine` function is the core of output handling: 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}` @@ -101,6 +104,46 @@ $TASKRUNNER_STATE_DIRECTORY/ - **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 @@ -108,6 +151,7 @@ $TASKRUNNER_STATE_DIRECTORY/ - `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 @@ -156,19 +200,22 @@ When `uploadLogs` is enabled: ## Key Design Principles -1. **Real-time Output**: No buffering delays for user feedback +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-38) -- Type definitions: `src/Types.hs` (lines 41-55) +- 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 @@ -181,4 +228,6 @@ When `uploadLogs` is enabled: - Line-by-line processing minimizes memory usage - Binary mode avoids encoding overhead - Async handlers prevent stream blocking -- Buffering disabled for real-time output \ No newline at end of file +- 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 From f430ec7dc2f0475a279a20a98c548f80335e5539 Mon Sep 17 00:00:00 2001 From: Maciej Bielecki Date: Tue, 30 Sep 2025 05:00:14 +0000 Subject: [PATCH 9/9] Update research documentation with proper metadata header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added YAML front matter with date, researcher, git commit, branch info - Included comprehensive tags for searchability - Enhanced task-output-handling.md with complete quiet mode documentation - Updated Claude settings to allow additional git commands 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .claude/settings.local.json | 7 ++++--- .../shared/research/task-output-handling.md | 21 ++++++++++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 68040f9..71f41ee 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -8,10 +8,11 @@ "Bash(stack test:*)", "Bash(find:*)", "Bash(stack build:*)", - "Bash(mkdir:*)" + "Bash(mkdir:*)", + "Bash(git rev-parse:*)", + "Bash(git remote get-url:*)" ], "deny": [], "ask": [] } -} - +} \ No newline at end of file diff --git a/thoughts/shared/research/task-output-handling.md b/thoughts/shared/research/task-output-handling.md index 195e8be..fc8a29b 100644 --- a/thoughts/shared/research/task-output-handling.md +++ b/thoughts/shared/research/task-output-handling.md @@ -1,4 +1,23 @@ -# Task Output Handling in taskrunner +--- +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