feat(gateway): substantive-plan gate — approach >=150 + real sub_task descriptions (#171)

Plans were vague because the gate accepted the bare minimum: PM
approach >=20 chars and title-only sub_tasks. minimax wrote exactly
the minimum.

- IWillPlanRequest.approach min_length 20 -> 150 (kept in sync with
  _PM_APPROACH_MIN_LEN; the gate enforces it at the choreographer
  layer too so direct/MCP callers can't bypass the HTTP boundary).
- New _thin_subtask_hint: every PM sub_task must have a title and a
  description >= _PM_SUBTASK_DESC_MIN_LEN (60) saying what the step
  does — each sub_task is both a delegate target AND a
  progress-checklist item, so a title alone is not a plan.
- cell_pm/main_pm role prompts: explicit "the gate REJECTS thin plans"
  framing + concrete sub_task example + the new minimums.
- Updated all affected test fixtures across the suite to use
  substantive approaches/descriptions; added thin-sub_task rejection
  coverage.

Commit 1 of 3 for the plan/progress quality work (#171/#172/#173).
This commit is contained in:
Renn F
2026-05-16 10:14:45 +02:00
parent e94159dce8
commit ed828a719b
14 changed files with 415 additions and 82 deletions
@@ -118,11 +118,26 @@ async def test_i_will_plan_claims_starts_and_sets_plan() -> None:
rich_plan={
"approach": (
"Three-cell decomposition: backend, frontend, and ux each "
"own a vertical slice of the work."
"own a vertical slice of the work. Backend lands first, QA "
"reviews each PR after it opens, documentation follows, then "
"complete and submit up. Strict sequencing with no cross-cell "
"dependencies beyond the stated ordering."
),
"sub_tasks": [
{"title": "Backend slice", "description": "API + DB"},
{"title": "Frontend slice", "description": "UI integration"},
{
"title": "Backend slice",
"description": (
"be-dev-1 implements the API + DB migration with "
"tests and opens the leaf PR for QA review."
),
},
{
"title": "Frontend slice",
"description": (
"fe-dev-1 wires the UI integration with loading and "
"error states and opens the leaf PR for QA."
),
},
],
},
)
@@ -189,11 +204,26 @@ async def test_i_will_plan_blocks_when_journal_decision_at_claim_missing() -> No
rich_plan={
"approach": (
"Three-cell decomposition: backend, frontend, and ux each "
"own a vertical slice of the work."
"own a vertical slice of the work. Backend lands first, QA "
"reviews each PR after it opens, documentation follows, then "
"complete and submit up. Strict sequencing with no cross-cell "
"dependencies beyond the stated ordering."
),
"sub_tasks": [
{"title": "Backend slice", "description": "API + DB"},
{"title": "Frontend slice", "description": "UI integration"},
{
"title": "Backend slice",
"description": (
"be-dev-1 implements the API + DB migration with "
"tests and opens the leaf PR for QA review."
),
},
{
"title": "Frontend slice",
"description": (
"fe-dev-1 wires the UI integration with loading and "
"error states and opens the leaf PR for QA."
),
},
],
},
)
@@ -239,8 +269,22 @@ async def test_i_will_plan_rejects_non_pending_state() -> None:
task_id,
plan="x",
rich_plan={
"approach": "Single-cell decomposition: backend handles all scope.",
"sub_tasks": [{"title": "Slice A", "description": "backend API work"}],
"approach": (
"Single-cell decomposition: backend handles all scope. "
"be-dev-1 owns the change end to end — branch, implement, "
"test, open PR; QA reviews after the PR opens, documentation "
"follows, then be-pm completes and submits up. No cross-cell "
"dependencies for this planning task."
),
"sub_tasks": [
{
"title": "Slice A",
"description": (
"be-dev-1 implements the backend API change with "
"tests and opens the leaf PR for QA review."
),
}
],
},
)
body = env.as_dict()
@@ -300,12 +344,27 @@ async def test_i_will_plan_calls_claim_when_pre_assigned_and_pending() -> None:
plan="distribute to be-pm and fe-pm",
rich_plan={
"approach": (
"Two-cell dispatch: be-pm owns backend vertical, "
"fe-pm owns frontend vertical."
"Two-cell dispatch: be-pm owns the backend vertical, fe-pm "
"owns the frontend vertical. Backend lands first, QA reviews "
"each PR after it opens, documentation follows, then complete "
"and submit up. Strict sequencing with no cross-cell "
"dependencies beyond the stated ordering."
),
"sub_tasks": [
{"title": "Backend cell", "description": "Assign to be-pm"},
{"title": "Frontend cell", "description": "Assign to fe-pm"},
{
"title": "Backend cell",
"description": (
"be-pm decomposes and delegates the backend vertical "
"to be-dev-1, who implements and opens the leaf PR."
),
},
{
"title": "Frontend cell",
"description": (
"fe-pm decomposes and delegates the frontend vertical "
"to fe-dev-1, who implements and opens the leaf PR."
),
},
],
},
)
@@ -363,8 +422,22 @@ async def test_i_will_plan_surfaces_start_failure_instead_of_faking_ok() -> None
task_id,
plan="x",
rich_plan={
"approach": "Single-cell decomposition: backend handles all scope.",
"sub_tasks": [{"title": "Slice A", "description": "backend API work"}],
"approach": (
"Single-cell decomposition: backend handles all scope. "
"be-dev-1 owns the change end to end — branch, implement, "
"test, open PR; QA reviews after the PR opens, documentation "
"follows, then be-pm completes and submits up. No cross-cell "
"dependencies for this planning task."
),
"sub_tasks": [
{
"title": "Slice A",
"description": (
"be-dev-1 implements the backend API change with "
"tests and opens the leaf PR for QA review."
),
}
],
},
)
body = env.as_dict()
@@ -508,8 +581,22 @@ async def test_i_will_plan_still_rejects_in_progress_for_other_agent() -> None:
task_id,
plan="x",
rich_plan={
"approach": "Single-cell decomposition: backend handles all scope.",
"sub_tasks": [{"title": "Slice A", "description": "backend API work"}],
"approach": (
"Single-cell decomposition: backend handles all scope. "
"be-dev-1 owns the change end to end — branch, implement, "
"test, open PR; QA reviews after the PR opens, documentation "
"follows, then be-pm completes and submits up. No cross-cell "
"dependencies for this planning task."
),
"sub_tasks": [
{
"title": "Slice A",
"description": (
"be-dev-1 implements the backend API change with "
"tests and opens the leaf PR for QA review."
),
}
],
},
)
body = env.as_dict()
@@ -534,8 +621,22 @@ async def test_i_will_plan_returns_tracing_gap_without_plan() -> None:
task_id,
plan="",
rich_plan={
"approach": "Single-cell decomposition: backend handles all scope.",
"sub_tasks": [{"title": "Slice A", "description": "backend API work"}],
"approach": (
"Single-cell decomposition: backend handles all scope. "
"be-dev-1 owns the change end to end — branch, implement, "
"test, open PR; QA reviews after the PR opens, documentation "
"follows, then be-pm completes and submits up. No cross-cell "
"dependencies for this planning task."
),
"sub_tasks": [
{
"title": "Slice A",
"description": (
"be-dev-1 implements the backend API change with "
"tests and opens the leaf PR for QA review."
),
}
],
},
)
body = env.as_dict()