Skip to content

Commit 193b30d

Browse files
Shahinyanmclaude
andauthored
fix(classifier): feed prompt on stdin so --disallowed-tools can't eat it (#52)
The claude -p classifier passed the prompt as a positional arg right after --disallowed-tools; the current claude CLI parses that flag greedily and swallowed the prompt words as bogus deny-rules ("Permission deny rule \"You\" matches no known tool"), failing every classification and piling chunks into the pending queue. Switch the classifier to ClaudeBinaryStdinRunner (already used by the complete/enrich and dream backends) so the prompt goes on stdin with no positional collision; also dodges E2BIG. Extracted default_runner() + a feeds_prompt_on_stdin trait flag and a regression test that locks the choice without spawning claude. Bump 0.26.4. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7bc532a commit 193b30d

4 files changed

Lines changed: 55 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.26.4] - 2026-06-17
11+
12+
### Fixed
13+
- **Auto-classifier no longer fails every chunk.** The `claude -p` classifier
14+
passed the prompt as a positional arg right after `--disallowed-tools`, which
15+
the current `claude` CLI parses greedily — it swallowed the prompt words as
16+
bogus deny-rules (`Permission deny rule "You" matches no known tool`) and
17+
failed every classification, silently piling chunks into the pending queue.
18+
The classifier now feeds the prompt on **stdin** (like the `complete`/enrich
19+
and dream backends already did), so chunks classify instead of dead-lettering.
20+
Run `task-journal pending retry` once to drain a backlog accumulated by the
21+
old behavior.
22+
1023
## [0.26.3] - 2026-06-16
1124

1225
### Added

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ members = [
77
]
88

99
[workspace.package]
10-
version = "0.26.3"
10+
version = "0.26.4"
1111
edition = "2021"
1212
rust-version = "1.88"
1313
license = "MIT"

crates/tj-core/src/classifier/agent_sdk.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ pub trait CommandRunner: Send + Sync {
3838
/// Run the classification for `prompt` against `model`, returning the raw
3939
/// stdout (the `--output-format json` wrapper) on success.
4040
fn run(&self, model: &str, prompt: &str) -> anyhow::Result<String>;
41+
42+
/// True when the runner feeds the prompt on **stdin** rather than as a
43+
/// positional argv arg. The classifier MUST use a stdin runner: the base
44+
/// command ends with `--disallowed-tools <tools>`, which the current
45+
/// `claude` CLI parses greedily — a positional prompt right after it is
46+
/// swallowed as bogus deny-rules (`Permission deny rule "You" matches no
47+
/// known tool`), failing every classification. Defaults to false.
48+
fn feeds_prompt_on_stdin(&self) -> bool {
49+
false
50+
}
4151
}
4252

4353
/// Build the base `claude` invocation shared by both runners: print mode, the
@@ -188,6 +198,10 @@ impl CommandRunner for ClaudeBinaryRunner {
188198
pub struct ClaudeBinaryStdinRunner;
189199

190200
impl CommandRunner for ClaudeBinaryStdinRunner {
201+
fn feeds_prompt_on_stdin(&self) -> bool {
202+
true
203+
}
204+
191205
fn run(&self, model: &str, prompt: &str) -> anyhow::Result<String> {
192206
use std::io::Write;
193207
use std::process::Stdio;
@@ -222,6 +236,14 @@ pub struct ClaudeCliClassifier {
222236
runner: Box<dyn CommandRunner>,
223237
}
224238

239+
/// The runner the production classifier uses. MUST feed the prompt on stdin —
240+
/// see [`CommandRunner::feeds_prompt_on_stdin`] for why a positional prompt is
241+
/// silently eaten by `--disallowed-tools`. Extracted so a unit test can lock
242+
/// this choice without spawning `claude`.
243+
fn default_runner() -> Box<dyn CommandRunner> {
244+
Box::new(ClaudeBinaryStdinRunner)
245+
}
246+
225247
impl ClaudeCliClassifier {
226248
/// Build from environment. Returns `None` unless a `claude` binary is on
227249
/// PATH (probed with `claude --version`) — the caller then falls through to
@@ -233,7 +255,7 @@ impl ClaudeCliClassifier {
233255
let model = std::env::var("TJ_AGENT_SDK_MODEL").unwrap_or_else(|_| DEFAULT_MODEL.into());
234256
Some(Self {
235257
model,
236-
runner: Box::new(ClaudeBinaryRunner),
258+
runner: default_runner(),
237259
})
238260
}
239261

@@ -488,4 +510,19 @@ mod tests {
488510
let err = c.classify(&input()).unwrap_err();
489511
assert!(format!("{err}").contains("error"), "got: {err}");
490512
}
513+
514+
/// Regression: the production classifier MUST feed the prompt on stdin. With
515+
/// the argv runner the prompt lands right after `--disallowed-tools` and the
516+
/// current `claude` CLI swallows it as bogus deny-rules, failing every
517+
/// classification (the "139 pending" backlog). Lock the choice here.
518+
#[test]
519+
fn production_runner_feeds_prompt_on_stdin() {
520+
assert!(
521+
default_runner().feeds_prompt_on_stdin(),
522+
"classifier prompt must go on stdin, not as an argv positional"
523+
);
524+
// the argv runner is the one that collides — keep the contrast explicit
525+
assert!(!ClaudeBinaryRunner.feeds_prompt_on_stdin());
526+
assert!(ClaudeBinaryStdinRunner.feeds_prompt_on_stdin());
527+
}
491528
}

0 commit comments

Comments
 (0)