Story: Stop ores.shell menus from silently shadowing each other

Table of Contents

This page documents a story in Sprint 26. It captures the goal, current status, acceptance criteria, and the tasks that compose it.

1. Goal

The shell has one owner per menu and per verb. A duplicate registration fails loudly at startup instead of being resolved silently by the order of the calls in repl.cpp, and the dead hand-written verbs are migrated or deleted.

2. Status

Field Value
State DONE
Parent sprint Sprint 26
Now Nothing.
Waiting on Review of PR #2272.
Next Nothing.
Last touched 2026-09-27

3. Acceptance

  • No two shell command units register the same menu name, and a duplicate would fail at startup rather than be shadowed.
  • The seven dead verbs are either migrated to the generated menus or deleted with the reason recorded.
  • The shell's command surface is unchanged for every verb that currently answers.
  • A menu's help lists every verb that answers at that address, including the ones another unit contributes.

4. Tasks

Task State Start End Description
Scaffold story: Stop ores.shell menus from silently shadowing each other DONE 2026-09-27 2026-09-27 Story scaffolding rides this task: documents, sprint wiring, and the scaffold PR. Close it before merging that PR.
Implement Stop ores.shell menus from silently shadowing each other DONE 2026-09-27 2026-09-27 Initial task for: Stop ores.shell menus from silently shadowing each other

5. Decisions

5.1. One owner per menu, enforced at startup, not by registration order

cli::Menu holds its children in a vector and answers a name by first match, so the order of the calls in repl.cpp decided which unit answered. The root is now a shell_root_menu that claims a name once and refuses a second claim, and every unit inserts through a seam the codegen template declares, so a component that gains a unit joins the rule with no edit.

5.2. A second unit extends a menu instead of registering its own

The generated entity unit owns the menu named for its entity. The hand-written unit beside it contributes the verbs the model cannot express through extend_menu, and the root applies those once every unit has registered, so the order the units register in cannot decide whether an extension lands. Merging rather than renaming keeps every verb at the address callers and recipes already use.

5.3. The name is read through Menu::Prompt()

cli::Command keeps a command's name protected, so the root cannot read it back directly. Prompt() is the same string because every shell menu is constructed from its name alone, and check_shell_menu_collisions.py rejects a construction that passes a separate prompt, which is what holds the two together.

6. Out of scope

  • The twenty generated units in refdata and synthetic that carry their own components' pending model drift. They do not claim their names yet, and the gate covers them until their next regeneration brings the seam with it.

7. Result

Merged as PR #2272, all five checks green. One owner per menu name is enforced at startup by shell_root_menu and held earlier by check_shell_menu_collisions.py, and a second unit contributes verbs to a menu it does not own rather than registering a second one. The reported symptom is gone: the help inside accounts listed five verbs and hid the seventeen the hand-written unit answered, and it now lists all twenty-two.

The census the story carried was wrong in two places and was corrected before any edit: four menus were registered twice rather than five, because the dq and reporting collision went with the generator fix, and seven verbs were dead rather than eight, because permissions get was counted and never existed.

Two further defects were forced by the guard and fixed: page-size was registered twice in one file and answered by falling through on arity, and the provisioning smoke recipe called the generated tenants add with the hand-written five-argument form and an invalid tenant type.

Emacs 29.3 (Org mode 9.6.15)