fix(kernel,tools,workflow): session-robustness QA sweep

Uncommitted work from the session-robustness-and-dox branch sweep
(docs/qa/QA-session-robustness-and-dox.md), verified alongside the
compression/context fixes:

- SandboxedToolExecutor: validate tool args centrally before dispatch. A
  malformed/missing-arg call becomes a recoverable ERROR: (surfaced with the
  tool's arg schema so the model can correct + retry) instead of stranding
  the stage with no artifact. + validation test.
- PlanLinter: seed artifacts (analysis) produced by the planning phase count
  as available producers, so a plan stage that `needs` them isn't flagged as
  an unproduced-need; H1 unproduced-needs + trap-state checks. + tests.
- DefaultSessionOrchestrator: live-QA robustness fixes (event-tail /
  per-stage budget + retry handling).
- workflow prompts/configs: DOX AGENTS.md alignment + freestyle/task/role
  prompt tweaks.
- SessionOrchestratorIntegrationTest: coverage for the above.
- FreestylePlanningWorkflowTest: allow list_dir in analyst tools (follows the
  list_dir wiring in 968cbfa).
- QA plan doc for the branch sweep.
This commit is contained in:
2026-07-02 00:56:45 +04:00
parent 968cbfa973
commit 18cbd34739
13 changed files with 296 additions and 24 deletions
@@ -20,7 +20,7 @@ data class PlanLintResult(
/**
* Deterministic, pure-Kotlin lint over a compiled plan graph (plan-pipeline-spec §5). Zero inference,
* zero I/O — a function of the graph alone, so it is replay-safe by construction. It complements
* zero I/O — a function of the graph and a fixed seed set, so it is replay-safe by construction. It complements
* [ExecutionPlanCompiler] (which already throws on unreachable-from-start / unknown-kind / bad-edge):
* the lint adds the checks the compiler does NOT make.
*
@@ -32,15 +32,22 @@ object PlanLinter {
private const val STAGE_CEILING = 12
private const val FAN_OUT_THRESHOLD = 4
fun lint(graph: WorkflowGraph): PlanLintResult =
/**
* Artifacts that pre-exist in the session before the execution plan runs — produced by the
* planning phase, not by any plan stage. The architect prompt explicitly allows `needs` to
* reference these (notably `analysis`), so the linter must treat them as available producers.
*/
private val seedArtifacts = setOf("analysis")
fun lint(graph: WorkflowGraph, seeds: Set<String> = seedArtifacts): PlanLintResult =
PlanLintResult(
hardFailures = unproducedNeeds(graph) + trapStates(graph),
hardFailures = unproducedNeeds(graph, seeds) + trapStates(graph),
softFindings = stageCount(graph) + fanOut(graph) + emptyBriefs(graph) + duplicateBriefs(graph),
)
/** H1: a stage `needs` an artifact that no stage `produces`. */
private fun unproducedNeeds(graph: WorkflowGraph): List<PlanLintFinding> {
val produced = graph.stages.values.flatMap { it.produces }.map { it.name.value }.toSet()
/** H1: a stage `needs` an artifact that neither a plan stage `produces` nor a seed provides. */
private fun unproducedNeeds(graph: WorkflowGraph, seeds: Set<String>): List<PlanLintFinding> {
val produced = graph.stages.values.flatMap { it.produces }.map { it.name.value }.toSet() + seeds
return graph.stages.entries.flatMap { (id, stage) ->
stage.needs.map { it.value }.filterNot { it in produced }.map { need ->
PlanLintFinding(
@@ -73,7 +73,7 @@ class FreestylePlanningWorkflowTest {
// analyst frames the work: search + open one task or decompose into a graph (the architect
// threads the named task into the plan).
assertEquals(
setOf("file_read", "ShellTool", "task_search", "task_context", "task_create", "task_decompose"),
setOf("file_read", "list_dir", "shell", "task_search", "task_context", "task_create", "task_decompose"),
graph.stages[StageId("analyst")]!!.allowedTools,
)
}
@@ -78,6 +78,20 @@ class PlanLinterTest {
assertTrue(h.detail.contains("z"))
}
@Test
fun `a need satisfied by the analysis seed is not a failure`() {
val g = graph(
stages = mapOf(
"a" to stage(produces = listOf("x"), needs = listOf("analysis"), prompt = "research"),
"b" to stage(needs = listOf("x"), prompt = "build"),
),
edges = listOf("a" to "b", "b" to "done"),
start = "a",
)
val result = PlanLinter.lint(g)
assertTrue(result.passed, "analysis is a planning-phase seed, not an unproduced need")
}
@Test
fun `a cycle with no exit is a trap-state hard failure`() {
val g = graph(