fix(context,tools): context hygiene + tool ergonomics from freestyle QA

Found while live-QAing freestyle_planning on a 12B local model:

- list_dir tool: recursive, .gitignore-aware listing so weak models stop
  flooding context with `ls -R` over node_modules/build/dist. Wired into
  the fileRead toggle + advertised to the planner (architect_freestyle).
- ContextClassifier: assistantToolCall turns are STRUCTURED, so the token
  pruner never shreds the model's own tool-call history — that was causing
  amnesia loops (re-issuing calls it had already made).
- Retire instruction-doc LLMLingua pruning (DOC_SOURCE_TYPES emptied): it
  fused load-bearing procedural text into unparseable soup. The static
  block stays small by dropping CLAUDE.md at the loader instead.
- AgentInstructionsLoader: load only AGENTS.md, not CLAUDE.md — the latter
  targets the outer assistant and polluted the agent's stage context.
- DefaultSessionReducer: WorkflowFailed flips session status to FAILED (was
  stuck ACTIVE forever, so clients/approval loops never saw a terminal).
- ShellTool: run shell command lines (cd/&&/pipes) via `sh -c`; unrunnable
  program is recoverable instead of an uncaught IOException killing the
  stage; malformed argv (non-string/collapsed-array) rejected with guidance.
- llmlingua sidecar: cap force_tokens to max_force_token (big docs blew the
  assert and 500'd, so doc pruning silently failed open).

Tests added/updated across all of the above.
This commit is contained in:
2026-07-02 00:44:04 +04:00
parent cb4e41a59f
commit 968cbfa973
15 changed files with 482 additions and 55 deletions
@@ -5,12 +5,17 @@ import java.nio.file.Path
import java.nio.file.Paths
/**
* Discovers standing agent instructions (`CLAUDE.md`, `AGENTS.md`) at the bound workspace
* root only — no parent walk, no nested lookup. Mirrors [ProjectProfileLoader]: the loaded
* snapshot is bound as an event so replay reads the recorded fact, never the live file.
* Discovers standing agent instructions (`AGENTS.md`) at the bound workspace root only — no
* parent walk, no nested lookup. Mirrors [ProjectProfileLoader]: the loaded snapshot is bound
* as an event so replay reads the recorded fact, never the live file.
*
* CLAUDE.md is deliberately excluded: it targets the outer assistant (agent-dispatch / model-
* selection guidance) and is noise to an in-workflow execution agent — injecting it bloated and
* polluted the stage context. AGENTS.md holds the correx-agent operating contract (the numbered
* how-to-finish-a-stage steps), which is what the agent actually needs.
*/
object AgentInstructionsLoader {
private val FILE_NAMES = listOf("CLAUDE.md", "AGENTS.md")
private val FILE_NAMES = listOf("AGENTS.md")
fun load(workspaceRoot: String): AgentInstructions {
val sections = mutableListOf<String>()
@@ -11,17 +11,18 @@ import java.nio.file.Path
class AgentInstructionsLoaderTest {
@Test
fun `both files present yields both sections and sources`(@TempDir root: Path) {
fun `AGENTS is loaded and CLAUDE is ignored`(@TempDir root: Path) {
// CLAUDE.md is deliberately not an agent-instruction source: it targets the outer assistant
// and polluted the stage context. Only AGENTS.md (the agent operating contract) is loaded.
Files.writeString(root.resolve("CLAUDE.md"), "Claude rules")
Files.writeString(root.resolve("AGENTS.md"), "Agents rules")
val loaded = AgentInstructionsLoader.load(root.toString())
assertEquals(listOf("CLAUDE.md", "AGENTS.md"), loaded.sources)
assertTrue(loaded.content.contains("# CLAUDE.md"), "content: ${loaded.content}")
assertTrue(loaded.content.contains("Claude rules"), "content: ${loaded.content}")
assertEquals(listOf("AGENTS.md"), loaded.sources)
assertTrue(loaded.content.contains("# AGENTS.md"), "content: ${loaded.content}")
assertTrue(loaded.content.contains("Agents rules"), "content: ${loaded.content}")
assertFalse(loaded.content.contains("Claude rules"), "content: ${loaded.content}")
assertFalse(loaded.isEmpty())
}