Files
zero/internal/config
KRATOS fec3276e38 feat(config): the plan-size ceiling is a configurable tier (gap report §5.9)
A plan's task count was bounded by ONE hard-coded number with no way to move
it. Twenty was too many for someone on a metered provider and too few for a real
sweep, and the only way to discover it was to read plan_tool.go.

profiles.planSize now names the ceiling: small 5, medium 20, large 50,
unrestricted. NAMED rather than numeric so the config file says what it means
and so "no ceiling at all" is expressible without a sentinel integer.

TWO DELIBERATE DIVERGENCES FROM THE REPORT, both worth stating.

It stays a HARD CAP, not the advisory warning the reference implementation has.
Under this posture every plan task is a child inheriting a 320-turn ceiling, so
fifty tasks authorise 16,000 child turns from a single tool call. A warning the
model emits to itself is not a bound on that. What the report actually wanted —
that the number be discoverable and movable — is delivered by the rejection
naming the tier and the setting instead of printing a bare integer.

And medium is 20, not the reference's 15, because 20 is the number
defaultPlanMaxTasks already held. Making a bound configurable and moving it in
the same change hides a behaviour change inside a mechanism change; the tier
mechanism is the deliverable and the number stays where it was.

PROJECT CONFIG MAY ONLY TIGHTEN IT. A cloned repo can ask for a smaller ceiling
and can never raise one, because raising it raises a spend ceiling for whoever
opens the repo. Same privilege boundary as Sandbox.Network's allow/deny rule and
DisableZeromaxing's disable-only rule, and a looser tier is dropped silently the
way an ignored network "allow" is: the project scope simply does not hold that
privilege, which is not a malformed config.

Fail closed throughout. An unrecognised tier resolves to the default and never
to "no ceiling" — a typo must not be the thing that removes a bound — and the
same is true when the early config resolve in exec fails.

The mutation sweep found that rank() returned -1 for an unknown tier with a
comment claiming that made it safe. It does the opposite: the tighten-only
comparison adopts the SMALLER rank, so -1 would have beaten every real tier.
Nothing was reachable through it, because ParsePlanSize rejects the name first —
but a defence-in-depth value that only holds because of the guard in front of it
is not defence in depth.

Verified against the real binary, not just the tests: the same six-task plan is
admitted with no setting and refused at 5 with planSize small, and a project
config asking for unrestricted against a user config of small stays refused.
2026-07-29 23:03:54 +05:30
..