Skip to content

Commit c13b465

Browse files
zylaclaude
andauthored
Add quiet mode feature for agentic workflows (#10)
* Add workflow documentation in CLAUDE.md 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 <[email protected]> * Add SKIP_S3_TESTS option to test runner - 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 <[email protected]> * 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. * Auto-detect S3 credentials and skip tests with clear reporting - 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 <[email protected]> * Add quiet mode feature for agentic workflows - 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 <[email protected]> * Add comprehensive nested task tests for quiet mode - 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 <[email protected]> * Update Claude settings and add research documentation * 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. * Update research documentation with proper metadata header - 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 <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent 8e5c351 commit c13b465

18 files changed

+520
-9
lines changed

β€Ž.claude/settings.local.jsonβ€Ž

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(stack build)",
5+
"Bash(git add:*)",
6+
"Bash(git commit:*)",
7+
"Bash(stack test)",
8+
"Bash(stack test:*)",
9+
"Bash(find:*)",
10+
"Bash(stack build:*)",
11+
"Bash(mkdir:*)",
12+
"Bash(git rev-parse:*)",
13+
"Bash(git remote get-url:*)"
14+
],
15+
"deny": [],
16+
"ask": []
17+
}
18+
}

β€ŽCLAUDE.mdβ€Ž

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Claude Code Workflow Instructions for taskrunner
2+
3+
## Project Overview
4+
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.
5+
6+
## Project Structure
7+
- `src/` - Haskell source code (main library)
8+
- `app/` - Executable main entry point
9+
- `test/` - Test suite
10+
- `test/t/` - Golden test files (`.txt` input, `.out` expected output)
11+
- `test/Spec.hs` - Main test runner
12+
- `test/FakeGithubApi.hs` - Mock GitHub API for testing
13+
- `package.yaml` - Haskell package configuration (Stack format)
14+
- `stack.yaml` - Stack resolver and build configuration
15+
- `taskrunner.cabal` - Generated cabal file (don't edit directly)
16+
17+
## Build and Development Workflow
18+
19+
### Building the Project
20+
```bash
21+
stack build
22+
```
23+
24+
### Running Tests
25+
```bash
26+
# Run tests (auto-detects S3 credentials and skips S3 tests if missing)
27+
stack test
28+
29+
# Run tests, skipping slow ones
30+
export SKIP_SLOW_TESTS=1
31+
stack test
32+
33+
# Run specific test by pattern
34+
stack test --test-arguments "--pattern hello"
35+
36+
# List all available tests
37+
stack test --test-arguments "--list-tests"
38+
```
39+
40+
### Accepting Golden Test Changes
41+
When golden tests fail due to expected output changes:
42+
```bash
43+
stack test --test-arguments --accept
44+
```
45+
46+
### Test Structure
47+
- Test files are in `test/t/` directory
48+
- Each test has:
49+
- `.txt` file - shell script to execute
50+
- `.out` file - expected output (golden file)
51+
- Tests run through the taskrunner executable
52+
- Special comments in `.txt` files control test behavior:
53+
- `# check output` - check stdout/stderr
54+
- `# check github` - check GitHub API calls
55+
- `# no toplevel` - don't wrap in taskrunner
56+
- `# s3` - enable S3 testing
57+
- `# github keys` - provide GitHub credentials
58+
59+
## Key Commands for Development
60+
61+
### Building
62+
- `stack build` - Build the project
63+
- `stack build --fast` - Fast build (less optimization)
64+
- `stack clean` - Clean build artifacts
65+
66+
### Testing
67+
- `stack test` - Run all tests
68+
- `stack test --test-arguments --accept` - Accept golden test changes
69+
- `SKIP_SLOW_TESTS=1 stack test` - Skip slow tests
70+
71+
### Running the executable
72+
- `stack exec taskrunner -- [args]` - Run the built executable
73+
- `stack run -- [args]` - Build and run in one command
74+
75+
## Notes
76+
- This project uses tasty-golden for snapshot/golden file testing
77+
- The test suite includes integration tests that verify taskrunner behavior
78+
- **S3 Test Auto-Detection**: 15 tests require S3 credentials (marked with `# s3` directive in test files)
79+
- `stack test` automatically skips S3 tests if credentials are missing
80+
- To run S3 tests, set: `TASKRUNNER_TEST_S3_ENDPOINT`, `TASKRUNNER_TEST_S3_ACCESS_KEY`, `TASKRUNNER_TEST_S3_SECRET_KEY`
81+
- GitHub tests use a fake API server and don't require real GitHub credentials
82+
- The project uses Universum as an alternative Prelude
83+
- Build output and temporary files are in `.stack-work/`

β€ŽREADME.mdβ€Ž

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,61 @@ The `snapshot` command supports the following flags:
144144
- `--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?
145145

146146

147-
## Tests: Update Golden Files
147+
## Testing
148148

149149
This project uses [tasty-golden](https://github.com/UnkindPartition/tasty-golden) for snapshot-based testing.
150150

151-
To update the golden files, run the test suite with the `--accept` flag passed to the test executable.
152-
If you're using stack, the full command is:
151+
### Running Tests
153152

154-
```sh
153+
```bash
154+
# Run all tests (auto-detects S3 credentials)
155+
stack test
156+
157+
# Run tests, skipping slow ones for faster development
158+
export SKIP_SLOW_TESTS=1
159+
stack test
160+
161+
# Run specific test by pattern
162+
stack test --test-arguments "--pattern hello"
163+
164+
# List all available tests
165+
stack test --test-arguments "--list-tests"
166+
```
167+
168+
### Test Structure
169+
170+
Tests are located in `test/t/` directory with two files per test:
171+
- `.txt` file - Shell script to execute
172+
- `.out` file - Expected output (golden file)
173+
174+
#### Test Directives
175+
176+
Special comments in `.txt` files control test behavior:
177+
- `# check output` - Check stdout/stderr (default)
178+
- `# check github` - Check GitHub API calls
179+
- `# no toplevel` - Don't wrap in taskrunner
180+
- `# s3` - Requires S3 credentials (auto-skipped if missing)
181+
- `# github keys` - Provide GitHub credentials
182+
- `# quiet` - Run in quiet mode
183+
184+
### S3 Test Auto-Detection
185+
186+
15 tests require S3 credentials and are automatically skipped if credentials are missing.
187+
188+
To run S3 tests, set these environment variables:
189+
```bash
190+
export TASKRUNNER_TEST_S3_ENDPOINT=your-s3-endpoint
191+
export TASKRUNNER_TEST_S3_ACCESS_KEY=your-access-key
192+
export TASKRUNNER_TEST_S3_SECRET_KEY=your-secret-key
193+
stack test
194+
```
195+
196+
### Accepting Golden Test Changes
197+
198+
When golden tests fail due to expected output changes:
199+
200+
```bash
155201
stack test --test-arguments --accept
156202
```
203+
204+
This updates the `.out` files with new expected output. Review changes carefully before committing.

β€Žsrc/App.hsβ€Ž

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ getSettings = do
6262
fuzzyCacheFallbackBranches <- maybe [] (Text.words . toText) <$> lookupEnv "TASKRUNNER_FALLBACK_BRANCHES"
6363
primeCacheMode <- (==Just "1") <$> lookupEnv "TASKRUNNER_PRIME_CACHE_MODE"
6464
mainBranch <- map toText <$> lookupEnv "TASKRUNNER_MAIN_BRANCH"
65+
quietMode <- (==Just "1") <$> lookupEnv "TASKRUNNER_QUIET"
6566
pure Settings
6667
{ stateDirectory
6768
, rootDirectory
@@ -76,6 +77,7 @@ getSettings = do
7677
, primeCacheMode
7778
, mainBranch
7879
, force = False
80+
, quietMode
7981
}
8082

8183
main :: IO ()
@@ -129,7 +131,7 @@ main = do
129131
-- Recursive: AppState is used before process is started (mostly for logging)
130132
rec
131133

132-
appState <- AppState settings jobName buildId isToplevel <$> newIORef Nothing <*> newIORef Nothing <*> newIORef False <*> pure toplevelStderr <*> pure subprocessStderr <*> pure logFile
134+
appState <- AppState settings jobName buildId isToplevel <$> newIORef Nothing <*> newIORef Nothing <*> newIORef False <*> pure toplevelStderr <*> pure subprocessStderr <*> pure logFile <*> newIORef []
133135
<*> newIORef Nothing
134136

135137
when (isToplevel && appState.settings.enableCommitStatus) do
@@ -171,6 +173,12 @@ main = do
171173

172174
skipped <- readIORef appState.skipped
173175

176+
-- Handle quiet mode buffer based on exit code
177+
when appState.settings.quietMode do
178+
if exitCode == ExitSuccess
179+
then discardQuietBuffer appState -- Success: discard buffered output
180+
else flushQuietBuffer appState toplevelStderr -- Failure: show buffered output
181+
174182
logDebug appState $ "Command " <> show (args.cmd : args.args) <> " exited with code " <> show exitCode
175183
logDebugParent m_parentRequestPipe $ "Subtask " <> toText jobName <> " finished with " <> show exitCode
176184

β€Žsrc/Types.hsβ€Ž

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ data Settings = Settings
1919
, primeCacheMode :: Bool
2020
, mainBranch :: Maybe Text
2121
, force :: Bool
22+
, quietMode :: Bool
2223
} deriving (Show)
2324

2425
type JobName = String
@@ -49,7 +50,8 @@ data AppState = AppState
4950
, toplevelStderr :: Handle
5051
, subprocessStderr :: Handle
5152
, logOutput :: Handle
52-
53+
, quietBuffer :: IORef [ByteString]
54+
5355
-- | Lazily initialized Github client
5456
, githubClient :: IORef (Maybe GithubClient)
5557
}

β€Žsrc/Utils.hsβ€Ž

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,14 @@ outputLine appState toplevelOutput streamName line = do
3535
| otherwise = True
3636

3737
when shouldOutputToToplevel do
38-
B8.hPutStrLn toplevelOutput $ timestampStr <> "[" <> jobName <> "] " <> streamName <> " | " <> line
38+
let formattedLine = timestampStr <> "[" <> jobName <> "] " <> streamName <> " | " <> line
39+
if appState.settings.quietMode
40+
then do
41+
-- In quiet mode, add to buffer instead of outputting immediately
42+
modifyIORef appState.quietBuffer (formattedLine :)
43+
else
44+
-- Normal mode: output immediately
45+
B8.hPutStrLn toplevelOutput formattedLine
3946

4047
logLevel :: MonadIO m => ByteString -> AppState -> Text -> m ()
4148
logLevel level appState msg =
@@ -121,3 +128,16 @@ getCurrentCommit _appState =
121128

122129
logFileName :: Settings -> BuildId -> JobName -> FilePath
123130
logFileName settings buildId jobName = settings.stateDirectory </> "builds" </> toString buildId </> "logs" </> (jobName <> ".log")
131+
132+
-- | Flush buffered output to terminal (used when task fails in quiet mode)
133+
flushQuietBuffer :: AppState -> Handle -> IO ()
134+
flushQuietBuffer appState toplevelOutput = do
135+
buffer <- readIORef appState.quietBuffer
136+
-- Output in correct order (buffer was built in reverse)
137+
mapM_ (B8.hPutStrLn toplevelOutput) (reverse buffer)
138+
-- Clear the buffer after flushing
139+
writeIORef appState.quietBuffer []
140+
141+
-- | Discard buffered output (used when task succeeds in quiet mode)
142+
discardQuietBuffer :: AppState -> IO ()
143+
discardQuietBuffer appState = writeIORef appState.quietBuffer []

β€Žtest/Spec.hsβ€Ž

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,34 @@ fakeGithubPort = 12345
3737
goldenTests :: IO TestTree
3838
goldenTests = do
3939
skipSlow <- (==Just "1") <$> lookupEnv "SKIP_SLOW_TESTS"
40+
skipS3Explicit <- (==Just "1") <$> lookupEnv "SKIP_S3_TESTS"
41+
hasS3Creds <- hasS3Credentials
42+
let skipS3 = skipS3Explicit || not hasS3Creds
43+
4044
inputFiles0 <- sort <$> findByExtension [".txt"] "test/t"
45+
inputFiles1 <- if skipS3
46+
then filterM (fmap not . hasS3Directive) inputFiles0
47+
else pure inputFiles0
4148
let inputFiles
42-
| skipSlow = filter (\filename -> not ("/slow/" `isInfixOf` filename)) inputFiles0
43-
| otherwise = inputFiles0
49+
| skipSlow = filter (\filename -> not ("/slow/" `isInfixOf` filename)) inputFiles1
50+
| otherwise = inputFiles1
51+
52+
-- Print informative message about what tests are running
53+
let totalTests = length inputFiles0
54+
s3Tests = length inputFiles0 - length inputFiles1
55+
slowTests = length inputFiles1 - length inputFiles
56+
runningTests = length inputFiles
57+
58+
when (skipS3 && s3Tests > 0) $ do
59+
if skipS3Explicit
60+
then System.IO.putStrLn $ "SKIP_S3_TESTS=1 - skipping " <> show s3Tests <> " S3-dependent tests"
61+
else System.IO.putStrLn $ "S3 credentials not found - skipping " <> show s3Tests <> " S3-dependent tests"
62+
System.IO.putStrLn $ "To run S3 tests, set: TASKRUNNER_TEST_S3_ENDPOINT, TASKRUNNER_TEST_S3_ACCESS_KEY, TASKRUNNER_TEST_S3_SECRET_KEY"
63+
64+
when (skipSlow && slowTests > 0) $
65+
System.IO.putStrLn $ "SKIP_SLOW_TESTS=1 - skipping " <> show slowTests <> " slow tests"
66+
67+
System.IO.putStrLn $ "Running " <> show runningTests <> "/" <> show totalTests <> " tests"
4468
pure $ Tasty.withResource (FakeGithubApi.start fakeGithubPort) FakeGithubApi.stop \fakeGithubServer ->
4569
testGroup "tests"
4670
[ goldenVsStringDiff
@@ -105,6 +129,9 @@ runTest fakeGithubServer source = do
105129
, ("GITHUB_REPOSITORY_OWNER", "fakeowner")
106130
, ("GITHUB_REPOSITORY", "fakerepo")
107131
] <>
132+
mwhen options.quiet
133+
[ ("TASKRUNNER_QUIET", "1")
134+
] <>
108135
s3ExtraEnv)
109136
, cwd = Just dir
110137
} \_ _ _ processHandle -> do
@@ -142,6 +169,7 @@ data Options = Options
142169
-- | Whether to provide GitHub app credentials in environment.
143170
-- If github status is disabled, taskrunner should work without them.
144171
, githubKeys :: Bool
172+
, quiet :: Bool
145173
}
146174

147175
instance Default Options where
@@ -150,6 +178,7 @@ instance Default Options where
150178
, toplevel = True
151179
, s3 = False
152180
, githubKeys = False
181+
, quiet = False
153182
}
154183

155184
getOptions :: Text -> Options
@@ -169,6 +198,9 @@ getOptions source = flip execState def $ go (lines source)
169198
["#", "github", "keys"] -> do
170199
modify (\s -> s { githubKeys = True })
171200
go rest
201+
["#", "quiet"] -> do
202+
modify (\s -> (s :: Options) { quiet = True })
203+
go rest
172204
-- TODO: validate?
173205
_ ->
174206
-- stop iteration
@@ -213,3 +245,16 @@ maybeWithBucket Options{s3=True} block = do
213245
mwhen :: Monoid a => Bool -> a -> a
214246
mwhen True x = x
215247
mwhen False _ = mempty
248+
249+
hasS3Directive :: FilePath -> IO Bool
250+
hasS3Directive file = do
251+
content <- System.IO.readFile file
252+
let options = getOptions (toText content)
253+
pure options.s3
254+
255+
hasS3Credentials :: IO Bool
256+
hasS3Credentials = do
257+
endpoint <- lookupEnv "TASKRUNNER_TEST_S3_ENDPOINT"
258+
accessKey <- lookupEnv "TASKRUNNER_TEST_S3_ACCESS_KEY"
259+
secretKey <- lookupEnv "TASKRUNNER_TEST_S3_SECRET_KEY"
260+
pure $ isJust endpoint && isJust accessKey && isJust secretKey
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- output:
2+
[toplevel] stdout | This output should be shown because the command fails
3+
[toplevel] stdout | Second line of output
4+
-- exit code: 1
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# quiet
2+
echo "This output should be shown because the command fails"
3+
echo "Second line of output"
4+
exit 1
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- output:
2+
[nested] stdout | Nested output before failure

0 commit comments

Comments
Β (0)