All notable changes to this project will be documented in this file.
-
terminal/themes/mq-theme-manager.shcould only run from$HOME/macos-scripts. It read${HOME}/macos-scriptsoutright, wheretools/scripts/doctor.sh,tools/scripts/scan.shand — since #172 —mq-zsh-theme-switcher.shall read${MACOS_SCRIPTS_HOME:-$HOME/macos-scripts}. This is the sibling #172 named and deliberately left alone.Measured with
HOMEpointed away from the checkout:list exit 0 current exit 0 reset exit 0 apply exit 0 preview exit 1 line 140: .../macos-scripts/ui/terminal-ui/mq-ui.shOnly
previewreadsBASE_DIR, throughUI_LIB, which is why this stayed invisible: four of the five verbs work anywhere, and on a developer machine$HOME/macos-scriptsexists so the fifth does too.THEME_FILEis untouched at${HOME}/.mq-theme. The selected theme is user state, the same class as~/.zshrc, and belongs in$HOMEwherever the checkout lives.tests/theme-manager-path-smoke.shruns the manager from a temporary tree holding nothing but a symlinkedui/, requirespreviewto render its panel rather than merely exit 0, and drives the other four verbs through apply → current → reset. Proven able to fail: with the hard-coded path put back it stops at step 2.Every run gets its own
HOME, and the last step compares a checksum of the real~/.mq-themefrom before the test.applywrites andresetdeletes that file, and a probe against a realHOMEearlier in this work applied a theme to the machine running it — the suite does not get to do that. -
The menu family disagreed about what it returns without a terminal. Measured headless, all ten local menus ended at their own prompt on EOF and three answered differently:
git release shortcuts tools workflows dev performance exit 0 system theme apps exit 1appsis a third outlier the first sweep missed, having read it as an AI command and excluded it on cost.halwas in the sweep and answered 0, but it is not on this list: it delegates tomq_hal_run, a bridge into the mq-hal repo, so its status is the delegate's and 127 on a machine without mq-hal is the correct answer. The contract covers ten local menus.#168 had already settled which answer is right: a menu loop exits non-zero without a terminal by design (
tests/menu-eof-smoke.sh), so propagating that reports "the command failed" for "there was no terminal". All three now take the split #168 gaveworkflows— the menu path returns 0, the argument path is untouched.system bogusverbstill exits 2,theme apply bogusandtheme bogusverbnow exit 2 as well (see the entry below), and a failedapps askstill carries its status.Step 7 of
tests/delegated-exit-code-smoke.shcould not have caught any of this. It flags a branch only when the branch both invokes a$BASE_DIRscript and ends in a barereturn 0.themeinvokes no script, and the mixed shape — deliberate 0 on one path, propagation on the other — is not what the pattern describes. Applying the fix then made the step fail for the wrong reason:systemandappsdo call scripts, and now hold a deliberatereturn 0.Rather than reword the rule until it passed, the step now carries a named exception list with the reason beside each entry, and honours an entry only when the same branch propagates somewhere. An exception therefore cannot cover a branch that discards status on every path, and an entry describing a branch that no longer looks that way fails the step instead of passing quietly. The behavioural proof is separate: step 10 grew to ten propagating paths, step 11 to eight deliberate zeros, and step 12 stubs
hal-terminal-guide.shthrough a fakeBASE_DIRto observe bothappspaths.Both gates were proven able to fail. Restoring the
themedefect stops the run at step 11; removing the propagating path fromappsstops it at step 7.One more thing the fix surfaced: lifting the menu case into a guard above the
caseremoved the literalmenu)arm, and the registry validator reads that arm to confirm the declared subcommand exists. The menu path went back inside thecaseand returns 0 from within it. -
An invalid
themeargument reported 1 where the rest of the surface reports 2.mqlaunch system bogusverbhas always answered 2, and so does thesrmnamespace;mq-zsh-theme-switcher.shanswered 1 for an unknown command word, forapplywith no variant, and for a variant that does not exist. 1 is the code a caller reads as "the theme could not be applied" rather than "that is not a theme". All three are 2 now.The runtime failures keep 1 — a missing UI library, a missing theme file — because the distinction is the point of using 2 at all.
tests/theme-command-surface-smoke.shwas unaffected: its exit-code step stubstheme_cmdwith an arbitrary status and asserts propagation, not a particular value.tests/menu-exit-contract-smoke.shholds the whole contract end to end throughbin/mqlaunchrather than through stubs, on both surfaces: ten local menus exit 0 without a terminal and draw their prompt exactly once, the same holds on a real pty whose stdin is closed, four operations still report their own result, four usage errors are 2, andreposkeeps 1 as the documented exception — it asks for a terminal-dependent picker while offering headless subcommands, so "there was no terminal" is the true answer there. Proven able to fail: puttingtheme applyback to 1 stops it at step 6, and letting the theme menu propagate again stops it at step 2.Writing that test found a second thing.
mq-zsh-theme-switcher.shresolved its own root as${HOME}/macos-scriptsoutright, wheretools/scripts/doctor.shandtools/scripts/scan.shboth read${MACOS_SCRIPTS_HOME:-$HOME/macos-scripts}. A checkout anywhere else could not run the switcher at all — it exited 1 withMissing UI librarybefore reaching its first command. It uses the same resolution as its siblings now. (terminal/themes/mq-theme-manager.shstill hardcodes the path; it is untouched here and outside this change.)That also made the runtime branch testable without side effects.
applywith a valid variant rewrites$ZSHRC, and a first version of the step tried to steer it by settingTHEME_FILE— which the switcher assigns unconditionally and never reads from the environment, so the override did nothing and the theme was applied to the machine running the suite.MACOS_SCRIPTS_HOMEis the handle that works: the step now runs the switcher against an isolated tree and an isolatedHOME, requires exit 1 and theMissing theme filemessage, and requires that no.zshrcwas written on the way to that verdict. -
The two commands the P2 operator inventory measured as unclear when called with no argument. Both answered with something the operator had not asked about:
$ mqlaunch skills usage: mq-skills.py [-h] [--repo REPO] {audit,validate,new} ... mq-skills.py: error: the following arguments are required: command exit 2 $ mqlaunch repos GitHub repo picker needs a terminal. exit 1skillshanded over the delegate's file name and argparse's phrasing for a command typed asmqlaunch skills.reposnamed a subject that never appeared in the command line — barereposroutes to the hub, which is an interactive picker — and offered no way forward, though sixrepossubcommands work headless. Now:$ mqlaunch skills Usage: mqlaunch skills <command> [args] Commands: audit, validate, new exit 2 $ mqlaunch repos mqlaunch repos with no command opens the repo hub, which needs a terminal. Usage: mqlaunch repos <command> [args] Commands: list, status, roadmaps, skills, wiki-status, diff-summary exit 1Message, usage and next step only. Nothing underneath changed, and both exit statuses are deliberately what they already were: 2 for
skills, because a missing verb is a usage error either way, and 1 forrepos, because barereposis valid on a terminal — the failure is the environment, not the command line, so no caller's exit-code handling moves.Two things left alone on purpose. An invalid
skillsverb still goes to the delegate: its error names the word the operator typed, and intercepting it would mean carrying a second copy of the verb list. And on a terminal, barereposstill opens the hub —tests/operator-usage-message-smoke.shasserts that with a stubbed hub under a pty, alongsideskills audit,skills bogusandrepos liststill reaching their delegates. A fix that stopped delegating would pass a message-only check. -
The two colour surfaces that never went through the shared library, and so never inherited the P1 output contract.
tools/scripts/pulse.shdefines its own six colour variables;tools/cli/mq-ui.shdefines four more and is sourced byscan.sh,brew-check.shanddoctor.sh. Both assigned the escapes unconditionally, so redirecting stdout wrote them into the file and neitherNO_COLOR=1nor--no-colorremoved one:before after on a real terminal mqlaunch pulse 20 0 20 colour escapes, unchanged mqlaunch scan 36 0 34 colour escapes, unchanged mqlaunch doctor - 0Both files now gate on
[[ -t 1 && -z "${NO_COLOR:-}" ]], the same condition the central guard inui/terminal-ui/mq-ui.shuses.Two details the measurement turned up that reading would not have.
pulse.shopened with a bareclear, which writes^[[3J^[[H^[[2Jto a pipe whenever it can read a terminfo entry — three escapes ahead of the first line of the report, and invisible in a run withTERMunset. It is now gated on-t 1alone, matchingclear_screen(): a screen clear is a terminal action, not colour, and every other mqlaunch screen still clears underNO_COLOR. Andblink_errcarried a literal\033[5minside its ownprintfformat, so it would have kept leaking one escape per call after the colour variables were emptied; the blink moved into a guardedC_BLINK.Only the escapes are conditional. The ASCII banner, the headings, the box rules and the
✔ ⚠ ✖glyphs are untouched, andtests/pulse-cli-color-contract-smoke.shchecks that half explicitly — suppressing colour by dropping output would otherwise pass a bare "no ANSI" check. It stubs the Wi-Fi and network probes so every colour branch fires, compares the colour and plain runs line by line, and drives the real path under a pseudo-terminal.mqlaunch scanwithNO_COLOR=1emits zero colour escapes and 5708 bytes, against 5426 with colour: the output grew. -
The seven remaining branches that called a shell-function delegate and then discarded its status. Each was measured with the delegate stubbed to exit 7 before deciding anything, and the measurement split them in a way reading them would not have:
review-brain operation 0 -> 7 mq-agent review repo --brain signal-brain operation 0 -> 7 mq-agent signal --brain learn-promote operation 0 -> 7 mq-agent learn promote --approve workflows save operation 0 -> 7 with a subcommand login status operation 0 -> 7 with arguments shortcuts list operation 0 -> 7 with arguments workflows menu 0 -> 0 deliberate login menu 0 -> 0 deliberate shortcuts menu 0 -> 0 deliberate atlas interactive session 0 -> 0 deliberatelearn-promoteis the one that matters most: it runsmq-agent learn promote <slug> --approve, a Class C write, and reported success whatever happened.Three of them are mixed, which is why a per-branch answer would have been wrong.
workflows,loginandshortcutsopen a menu when called bare and run an operation when given arguments. A first attempt propagated the status in both cases and the full suite stayed green — butmqlaunch shortcutswith no terminal went from exit 0 to exit 1, because a menu loop exits non-zero on EOF by design (tests/menu-eof-smoke.sh). Propagating there reports "the command failed" for "there was no terminal". The suite did not catch it; running the real command did. Only the argument path propagates now.atlasis an interactive session in both forms and returns 0 on purpose.Steps 10 and 11 pin both halves — six operation paths propagate, four interactive entrypoints return 0 — so the deliberate half cannot later be mistaken for the bug and "fixed".
-
mqlaunch brainand the seven verbs beside it reported success no matter what the bridge did. Both branches ended in an unconditionalreturn 0after callingmq_brain_run, so a failing brain command exited 0 and a script could not tell that anything had gone wrong.tests/delegated-exit-code-smoke.shstep 7 already forbids exactly this, and it did not catch it: the structural check only inspects branches that invoke a$BASE_DIR/...script, andmq_brain_runis a shell function. The rule was right and its reach was short.Step 8 runs the branches instead of reading them —
mq_brain_runstubbed to exit 0, 1, 2 and 127, across all eight verbs (brain,note,sessions,decisions,reviews,learn,verified,systems) — and was red on the old code. Step 9 pins theelsearm that was already correct, so fixing the success path could not turn a missing bridge into a silent success.Verified on the real path:
mqlaunch brain not-a-real-verbexits 1, where it exited 0 before.Not fixed here, and deliberately: seven other branches call a function delegate and then
return 0—workflows,review-brain,signal-brain,learn-promote,atlas,login,shortcuts. Several open interactive menus, where the exit status of the menu is not obviously the thing to propagate, so each needs a judgment this PR was not scoped to make. Sweeping them together would have hidden that.
-
tools/scripts/srm.sh— 156 lines that built a system prompt and calledhttps://api.openai.com/v1/responsesdirectly withfile_searchagainst a hardcoded vector store, reached bymqlaunch srm ask|search|inspectand by any unrecognised word aftersrm.Retired rather than moved, because the capability already existed at its owner:
mq-agent memory statusreports the same vector store,vs_69ffa9a4ef5c81919d7d237c3ecdc260, through repo-signal and mq-mcp. The local path was a second route to the same memory, around the repo that owns it.askandsearchwere one query with two spellings and aremq-agent memory searchnow;inspectismq-agent memory status.The fall-through is gone with it. Sending any unrecognised word to an LLM is what ROADMAP.md calls a v2.0.0 non-goal — "do not introduce hidden AI fallbacks for unknown commands" — and an unknown verb prints usage and exits 2. The rejection sits in an explicit
*)arm inside the nested case rather than after it, so the registry can stateunknown_subcommand: rejectandvalidate-command-registry.pycan see that it does.srmtherefore delegates every verb and its owner ismq-agent, notmacos-scripts.LOCAL_ROLE_EXEMPTis empty, the quick-command gate covers the whole local surface with no exception, andtests/command-registry-smoke.shstep 23 now fails on any entry rather than on any entry other thansrm.Two tests pinned the retired behaviour and were retargeted, not deleted.
tests/command-word-normalization-smoke.shstep 4 checked that a free-form question reachedsrm.shwith its case intact; the rule it protects — only the command word is normalised — is now asked ofsrm SEARCH "What does HAL do".tests/mq-agent-routing-smoke.shgained steps 12 and 13: every verb reaches mq-agent,srm.shis absent, no OpenAI endpoint is reachable from the route in code, and an unknown verb prints usage.One defect found while verifying the real path and fixed here:
memory statusdefaults to., and_run_agentruns inside$MQ_AGENT_BIN, somqlaunch srm inspectreportedrepo: /Users/mansys/mq-agentinstead of the operator's directory. It supplies$PWDwhen no path is given and never overwrites an explicit one — the same shape as_flow_has_repo_flag.
-
local_role— the positive rule for what a command this repo owns is allowed to be.owner: macos-scriptsused to say only "not delegated", so a command could drift into orchestration, execution or memory and the registry would record nothing unusual. All 60 local commands are now classified by what their own code owns rather than by what they open: 28terminal-ux, 14host-operation, 17thin-entrypoint, and one exemption.validate-command-registry.pyfails a local command with no role, a role outside the three, alocal_roleon a command another repo owns, and an exempt command that also classifies. Four negative fixtures intests/command-registry-smoke.shprove each rule fires.The exemption is
srm, and it is the finding. Its first four verbs delegate tomq-agent memory-*; everything else falls through totools/scripts/srm.sh, 156 lines that build a system prompt and callhttps://api.openai.com/v1/responsesdirectly withfile_searchagainst a hardcoded vector store. That is semantic memory cognition in shell — memory belongs to mqobsidian, orchestration to mq-agent, and "do not implement memory promotion in shell" is a v2.0.0 non-goal. Classifying itthin-entrypointwould have recorded the breach as approved, so it is named inLOCAL_ROLE_EXEMPTwith the reason and the removal condition beside it. The smoke test fails if that list ever holds anything other than exactlysrm, so a second breach cannot be resolved by naming it.No runtime changed. The
srmfix is a separate PR.Noted while classifying and deliberately not resolved:
ask,fixandchatcall the same OpenAI endpoint from their own scripts. They are classifiedthin-entrypoint, which is accurate about their four-line dispatch arms and says nothing about what those scripts own.
-
The
stackroute named three of mq-agent's sixteen verbs and omitted the one the roadmap was waiting for.mqlaunch stack --helpanddocs/COMMANDS.mdboth listedstatus, contract-check, truth-export; the release cockpit ismq-agent stack cockpit, it already worked throughmqlaunch stack cockpitbecause the route forwards everything, and the word appeared nowhere in this repo. An operator had to read mq-agent's help to find it.Both surfaces name it now, say that unlisted verbs forward, and state that
--jsonis an option on the subcommands rather than the group —stack status --jsonandstack cockpit --jsonemit machine documents,stack --jsonexits 2 from mq-agent. The registry summary went from "Show stack status and operations" to "Show stack status and the read-only release cockpit", so help says it too.No routing change, and no
mq-agentchange. A baremqlaunch stackstill meansmq-agent stack status— the only local decision on this route — andtests/mq-agent-routing-smoke.shsteps 10 and 11 now pin that default, the verbatim forwarding of every other verb including one this repo has never heard of, and the delegate's exit code for 0, 1, 2 and 127. Both steps were proved able to fail by planting a defect and watching them report it.The roadmap task this closes had been waiting since it was written for
mq-agent ship status, a command that does not exist and is not planned.mq-agent --helplists 34 commands; the cockpit shipped understack. -
mqlaunch review file <path> --repo <repo>reviewed the wrong thing.--repowas accepted here as a scope selector — an undocumented alias forreview repo— and it also happens to be a real mq-agent option onreview file, naming the external repo the file lives in. The scope arm won, so the path was dropped and the invocation becamereview repo <repo>: an entire repository reviewed instead of the one named file, silently, with the operator's argument read as the repo. The option was unreachable from the launcher.Scope is positional now —
diff,repo,file <path>— which is the only formdocs/COMMANDS.mdever documented. The--diff,--repoand--filespellings are gone, and anything unrecognised falls through to passthrough, so mq-agent options reach mq-agent.Found by reviewing the delegation handlers for the P2 roadmap item rather than by a report. The three existing routing assertions could not have caught it: they grep
mq-agent-menu.shfor the strings it should contain, which proves the file mentions_run_agent review fileand nothing about what an operator's arguments turn into.tests/mq-agent-routing-smoke.shsteps 8 and 9 run the translation against a stubbed delegate and compare the built command line — red on the old code, and the nine documented forms are pinned with it. Verified end to end throughmq-agent --dry-run:review file README.md --repo ~/mq-agentnow reachesmq-mcp review_file README.md repo_path=/Users/mansys/mq-agent. -
The three behaviour defects the performance golden pinned. One commit each, the golden red before the fix and regenerated after it.
-
Seven of the nine Performance screens printed
print_section: command not foundwhere a heading belongs — eleven error lines, counting the twoprint_kvand oneprint_dividerin Quick Watch. All three helpers lived only in the frozen v1 tree'slib/ui.sh, and the live path never sourced that file:mq-performance-menu.shsourcesmq-ui.shand the data layer, nothing else. Rows 3 to 9 have been broken for as long as this path has existed.Restored verbatim from
94d0ebaintomqlaunch/lib/performance.shrather than intoui/terminal-ui/mq-ui.sh, because this file is their only caller andterminal/release/mq-release-check.shalready carries its ownprint_section. Guarded withcommand -v, so a later definition in the UI authority wins without this one having to be removed first. -
Load (1m)showed all three load averages glued together —1.501.251.10.perf_load_1msplituptimeon", "while macOS separates the figures with spaces, so it kept all three andtr -d ' 'closed the gaps. The same value appeared in the menu's Signals row. It splits on whitespace now, verified on both formats: macOSload averages: 1.50 1.25 1.10and Linuxload average: 0.15, 0.20, 0.18.The masking had to go first, and that is the more useful half of the change.
<LOADAVG>covered exactly the field the bug lived in, so the golden could not have gone red on a fix — and the malformed value matched the IP rule, four dot-separated groups being what1.501.251.10looks like, so the fixture readLoad (1m): <IP>. Those lines pass through unmasked now. -
Quick Watch could not stop. It refreshed until something killed it, and the loop did not trap
SIGINT, so Ctrl+C took the wholemqlaunchsession rather than returning to the Performance panel. With no terminal there was no way out at all.A trap sets a flag the loop checks and
trap - INTrestores the caller's handling; the loop also breaks after one frame when there is no tty orMQ_NO_TUIis set, matching the guardopen_git_menualready uses. Proven against the pre-fix code:SIGINTmid-watch used to kill the caller and now returns to it with status 0. With a real pty it still refreshes — six frames in nine seconds — so the guard is not firing where it should not.All nine screens exit 0 now, where the golden used to record a 124.
-
tests/performance-screens-golden-smoke.shandtests/fixtures/performance-screens.golden— the snapshotdocs/plans/step-12-v1-removal.mdasked for as 12.3 and never got.The plan gated the performance migration on a golden of its output. That fixture was never written, so 12.4 shipped verified by looking at the rendered panel, which proves the panel and not the nine
command_perf_*screens behind it. All nine are live: rows 1–9 of the Performance menu call them directly.Reconstructed after the fact from a worktree of
94d0eba, the commit before the migration, and compared screen by screen — stdout, stderr and exit status. All nine matched, and the committed golden was then verified against the pre-migration implementation as well, so it pins the behaviour the migration was supposed to preserve rather than whatever happens to be true now.tools/scripts/normalize-performance-screen.pymasks only what is volatile: paths, load averages, uptime, timestamps, IPs, sizes, percentages,psrows anddurows. Labels, section headings, box drawing, row counts, ordering and error text pass through. Step 3 of the test asserts the golden still carries eight of those labels, because a normalizer can be tightened until everything masks to the same thing and the comparison passes vacuously. Step 4 renders against a copy of the data layer with one character changed in a heading and requires the diff to show it.The system commands are stubbed (
tests/fixtures/perf-stubs) so the fixture is a rendering snapshot rather than a photograph of one laptop. The first version captured this machine's battery, load and process list and failed CI, which runs Linux and has nopmset. With the inputs fixed, almost nothing needs masking, which makes the comparison sharper rather than looser.The golden pins four defects, all pre-existing and all found by writing it:
print_section: command not found, eleven times. Seven of the nine screens callprint_sectionand one also callsprint_kv; both live only in the v1 tree'slib/ui.sh, which the live path never sourced —mq-performance-menu.shsourcesmq-ui.shand the data layer, nothing else. Rows 3 to 9 have been printing that error instead of a heading for as long as this path existed. Verified identical on94d0eba, so neither the migration nor the deletion caused it.Load (1m)renders as one run of concatenated decimals —1.541.481.58.perf_load_1msplitsuptimeon", "while macOS separates the three load averages with spaces, so it keeps all three andtr -d ' 'glues them. The same value appears in the menu's own Signals row.command_perf_quick_watchcannot exit on its own. It is bounded in the harness and its status in the golden is the timeout's 124.awk -v load=...inperf_health_score.loadis a gawk builtin, so gawk rejects it as a variable name and the whole health score fails on any system with GNU awk. BSD awk on macOS accepts it, which is why it went unseen for as long as this code has existed.
The fourth is fixed here, because it is what kept the test from being a CI gate at all — CI runs Linux. The variable is renamed and nothing else changed; the golden is unchanged on macOS, which is the proof the rename preserves behaviour there.
The other three are recorded, not fixed. They are behaviour changes and this branch is a deletion that has to stay revertible.
Verified as a chain rather than asserted: the pre-migration implementation extracted from
94d0ebarenders byte-identical to the committed golden, the current one does too, and a one-character change to a heading is reported.
-
terminal/mqlaunch-v1/— 23 files, 1125 shell LOC, plus its owntools/scripts/test-mqlaunch-v1.sh. The last legacy runtime, and the last duplicate UI implementation: it shipped its ownlib/ui.shalongsideui/terminal-ui/mq-ui.sh.Its live edges went first, in the PR before this one, which is why this one deletes rather than migrates. Four checks were run against
mainbefore anything was removed: the freeze gate reported 0 compat edges; a sweep of every tracked.sh,.zshand.pyoutside the tree found nosource,bashorexecreaching it; the 24 files still naming it were all history, tests, docs or tooling; and the suite passed after the deletion.Nothing that ran was edited. Seven tooling files named the tree — to exclude it from lint, to test it, or to police it — which is exactly the distinction the freeze gate's two lists were built to make:
test-mqlaunch-v1.sh deleted with the tree test-all.sh v1 selftest block removed test-mqlaunch.sh v1 launcher assertions removed lint.sh exclusion had nothing left to exclude shellcheck-report.sh same generate-wiki-command-ref.sh same check-runtime-authority.sh became a tombstone gateThe lint surface went from 189 files to 188 and stayed clean at warning severity. Four of the five SC2034 findings that
RUNTIME_AUTHORITY.mddocumented as deliberately exempt left with the tree — they were colour variables in itslib/core.shthat the scripts sourcing it did read.scripts/check-runtime-authority.shkeeps running as a tombstone. A path that no longer exists cannot be depended on by accident, but it can be recreated, and a second runtime is what the v2.0.0 track spent its length removing. Its compat list is empty permanently; a non-empty one means the legacy runtime is back.Three READMEs still claimed performance routed through v1.
terminal/bridges/README.mdwas 109 lines about a migration that is now finished and is rewritten;terminal/README.mdandterminal/launchers/README.mdare corrected.One gap is recorded rather than papered over:
docs/plans/step-12-v1-removal.mdcalled for a golden snapshot of performance output before the migration, and that fixture was never written. The migration was verified by driving the menu before and after — same score, same signals, same rows — which proves the panel but not each of the ninecommand_perf_*screens behind it. R1 in that plan's risk table is specifically about unnoticed output drift, and its mitigation is the step that was skipped.
-
Nothing live reaches
terminal/mqlaunch-v1/any more. The freeze gate reports 0 compat edges, where it reported four.The one that mattered was not a legacy route.
mq-performance-menu.sh:26sourcedcommands/performance.shout of the frozen tree — 504 lines of workingperf_*readings behind the Performance Hub's health score, signals line and ten rows. The tree was classified live in order to supply code, not to keep an old path open, which is why "delete the compat tree" was never the small job P3's Status line implied. The file moved verbatim tomqlaunch/lib/performance.sh; it sources nothing and reads only$PROJECT_ROOT, so it was a move rather than a port. Verified by drivingmqlaunch perfbefore and after — same score, same signals.The other three were retired rather than relocated:
terminal/bridges/tools-bridge.shforwardedtoolsto the v1 launcher as a subprocess. Deleted — neitheropen_v1_tools_menunorrun_v1_tools_commandhad a caller anywhere in the tree.terminal/bridges/performance-bridge.shfell back to the v1 launcher when the current menu was missing. A missing menu is a broken checkout, and answering it by running a frozen launcher hid that; it reports and returns 1 now.run_performance_command,open_v1_performance_menuandrun_v1_performance_commandwent with it, none of them called.tools/scripts/create-debug-bundle.shranbash v1/mqlaunch.sh helpas a health probe frommq-system-menu.shoption 6. A bundle reporting on a tree nothing routes to is noise.
The dependency runs the other way now:
terminal/mqlaunch-v1/mqlaunch.sh:22sources the data layer from its new home. Legacy depending on live is the allowed direction, and it keeps the tree runnable so that deleting it stays a decision rather than a consequence of a move.tests/compat-path-delegation-smoke.shno longer proves the legacy shim forwards correctly and preserves its exit status — there is no shim. It proves the shim is absent, that the performance bridge does not name the frozen tree, and that it fails rather than falling through when its menu is missing.tests/runtime-authority-classification-smoke.shasserted the performance menu is COMPAT; it now asserts the menu reachesmqlaunch/liband not the frozen tree, checked against the menu rather than the map's prose about it.COMPAT_EDGESin the freeze gate is an empty array, which needed the${arr[@]+"${arr[@]}"}guard: underset -u, bash 3.2 —/bin/bashon macOS — aborts on an empty"${arr[@]}". Verified under both 3.2 and 5.3.
-
gitlaunchreported a bad repo argument and then succeeded. Found by driving the command on a real machine after #156 put it onPATH.gitlaunchtakes a repo path, not a subcommand — the registry's contract forgitisunknown_subcommand: forward— sogitlaunch statusforwardedstatusas a path. It printedPath not found: status, dropped into the menu with no repo, and exited 0. Three separate places dropped the status:detect_repoingitlaunch.shdidset_repo "$REQUESTED_REPO" || REPO="", turning an explicit argument the operator got wrong into a silent fall-through to detection. An argument that does not resolve is now an error, and the message names what the argument is for:Repo path not found: status.open_git_menuended on a&&whose status became the function's, and its restart loop treated a failed start as the mid-session crash it is meant for — five identical errors before giving up. It now breaks on a non-zero exit and returns it.- The dispatcher's
git)arm ended in a barereturn 0, the same defect the theme arm had in #150.
One pre-existing status had to be corrected for propagation to be safe: the no-repo panel's Exit row was
*) exit ;;, a bareexitthat inherits the previous command's status and had been leaving 1 behind. Harmless while the caller discarded it, wrong the moment it is passed on. Nowexit 0.Verified on every path — back
0, EOF0, no-repo Exit0, bad path1— directly, throughmqlaunch git, and throughbin/gitlaunch.
-
Two of
gitlaunch.sh's menu rows were invisible to the command-surface inventory, so the loop an operator sees as ten was measured as eight — under the gate whose whole subject is how many choices a loop offers.ARM_OPEN,ARM_CLOSEandARM_BODY_LINESwere each defined twice ininventory-command-surfaces.py, and in Python the second definition is the one that runs. That copy predated theKEYpattern and accepted all-digits or all-letters but never the mixed form, so wideningKEYhad no effect on multi-line case arms:7|m|M)and8|p|P)matched nothing.The gate still passes at
--max-loop 10with both rows counted; the total option count goes 299 → 301. Step 9 oftests/command-discovery-inventory-smoke.shrequires two mixed-key rows in that menu and fails when the old regex is put back.
-
tools/scripts/mqlaunch_desktop.sh, 1104 lines that nothing started.docs/AUTHORITY_MAP.mdcalled it an "alternate live entry". No file in the repo invoked it — the only two tracked mentions were comments ininventory-command-surfaces.pyexplaining why it was excluded from the command-surface count. It was not inbin/, not linked from/usr/local/bin, not named in a shell rc, and there was no LaunchAgent, Raycast or Alfred integration on the machine this was checked on.It was a second dispatcher rather than a wrapper: 63 numbered menu arms, its own
eval "$cmd", and its own vocabulary —theme-amber,theme-green,theme-ice,netlaunch,gitlaunch— none of it in the command registry. It knew nothing ofagent,obsidian,hal,theme applyorrepos. A snapshot of an older mqlaunch, and exactly the shape the map's own "Forbidden" rule names: parallel implementations of the same menu responsibility.terminal/menus/mq-git-menu.shwas live only through it and is now DEPRECATED, but kept:mq-git-menu.sh log|status|…is a documented entry and three tests drive its functions. Nine function names are shared withgitlaunch.shand the two have already diverged —safe_pushis 26 lines against 64,pr_aware_pushtakes a different signature and onlygitlaunch.shrefuses to push from a detached HEAD. Two designs, not two copies, so the overlap is recorded in the map rather than merged in passing.
-
The login flow preferred the frozen v1 launcher over the current runtime.
detect_mqlaunch_baseinautomation/login/mqlogin.shtriedcommand -v mqlaunchfirst and themqlaunch-v1launcher second, ahead ofterminal/launchers/. On a machine withmqlaunchonPATHthe first branch always wins, which is why nobody saw it; without it, the login flow booted into the compat tree. The fallback is nowbin/mqlaunch. -
The runtime authority freeze gate scanned three directories —
terminal/,ui/andmqlaunch/— which is narrower than the "live runtime shell" it claimed to cover.automation/andtools/were outside it, so two live→v1 edges were never recorded: themqlogin.shfallback above, andtools/scripts/create-debug-bundle.sh:74, which runsbash v1/mqlaunch.sh helpfrommq-system-menu.shoption 6.The gate now reads
git ls-filesand covers every tracked shell file except the v1 tree itself andtests/, which drives v1 on purpose. Same source asinventory-command-surfaces.py, and for the same reason: an untracked copy of a menu in the working tree can neither add an edge nor hide one.Widening it needed two lists rather than a longer one.
COMPAT_EDGESis live code that reaches v1 at runtime — now four entries, with the debug bundle added.TOOLINGis the seven build, lint and documentation scripts that name v1 to exclude, test or police it; deleting v1 would edit those and break the others. A single allowlist could not say which.tests/runtime-authority-freeze-smoke.shgrew from one assertion to four. It plants an edge inautomation/and one intools/so the widened scope is proven rather than declared, asserts that every reference to v1 in the repo sits on one of the two lists, and drivesdetect_mqlaunch_basewithPATHemptied — the only condition under which the fallback order is observable. -
install.shsymlinkedterminal/launchers/mqlaunch.shontoPATH, which is one file past the official entrypoint.mqlaunch replis routed inbin/mqlaunchand nowhere else — the launcher it execs reportsreplas an unknown command and suggestsmqlaunch repos— so a fresh install produced a workingmqlaunchwith no REPL. Both link targets were verified rather than reasoned about: through the launcher,replprintsUnknown command: repl; throughbin/mqlaunchit does not.The installer now links every executable under
bin/, discovered rather than named. That is three commands:mqlaunch,mq, and the newgitlaunch.mqwas in the same position asgitlaunch— onPATHas a hand-made copy that no install step maintains.Uninstall follows the same list, and removes only symlinks. A copy sitting at one of those names is reported and left alone: uninstall takes back what install put there, and a file on
PATHthe repo never owned is not the installer's to delete.
-
bin/gitlaunch, so the git menu can be reached by typinggitlaunchwithout a second copy of it existing.gitlaunchwas onPATHas a hand-made copy ofterminal/launchers/gitlaunch.sh— 453 lines dated 25 June against the repo's 1122. Everything since had reachedmqlaunch gitand not the command the hand types: the eight-choice grouping, the 92-column convergence, the protected-push guard. Committing to the repo cannot update a copy, which is the argument for a link.It is a wrapper rather than an entrypoint:
exec bin/mqlaunch git "$@". Going straight toterminal/launchers/gitlaunch.shwould skip the repo argument handling and the dashboard-cache invalidation inopen_git_menu, and would add a second way into a command the dispatcher already routes — the classtests/command-discovery-inventory-smoke.shholds at zero.tests/install-contract-smoke.shinstalls into a temporary bin dir and asserts every entrypoint is linked, that each link resolves intobin/rather than past it, that the installedmqlaunchroutesrepl, and thatgitlaunchhandsgit <args>to the dispatcher — the last one driven through a stub, so what is checked is the argv rather than the source text.
-
docs/COMMANDS.mddocumented the wrong git menu.mqlaunch gitopensterminal/launchers/gitlaunch.sh, whose panel puts safe merge on7/mand PR merge on8/p, with repo switching under9. Repo and workspace. The section describedmq-git-menu.shinstead — ten rows, safe merge on9, repo actions under10. Repo and remote— so anyone reading the docs and pressing9for a merge got a submenu. -
tests/runtime-authority-classification-smoke.shchecked that the authority map declares an entry point, never that anything can enter through one. A seventh step now requires every path in the entry-point table to sit inbin/, whichinstall.shlinks ontoPATHwholesale, or to be named by a tracked file outsidedocs/andtests/.Comment lines are stripped before that search. The first version of the check passed, because the two mentions of the desktop script were comments saying it is not part of the live surface — prose reads exactly like a caller to a grep, which is the mistake one level up from the one being caught.
-
The Performance menu executed anything it did not recognise. Its last case arm was
*) /bin/zsh -lc "$choice", so mistyping a menu number ran the typo as a login-shell command — the pattern #137 removed from the HAL menu, left in the one menu that was not part of that change, and here without even|| true.Shell is still reachable, behind the explicit
!prefix the main prompt already advertises. Unrecognised input is reported instead.tests/menu-shell-guard-smoke.shholds the rule for every menu, not just this one: a static check that no menu hands its choice to a shell, plus a run of the real menu, because copying the choice into another variable first would pass the static check. Both were proven against planted defects. -
The Performance menu never returned when its input ran out. The fallback
read -r choicediscarded the EOF, so the panel redrew as fast as it could render, forever.tests/menu-eof-smoke.shcoversmqlaunch perfand passed: on that pathread_main_choiceis defined and its call site does act on the return. The defect was in the branch the file takes when it is executed directly, which is a supported entry point that no test drove.
-
The Release menu shows nine choices instead of twelve, and stops printing two of them out of position. Option 12 sat inside CHECKS between 4 and 5, and option 11 sat inside SHIP between 6 and 7, so anyone picking by position rather than by label got the wrong command — and on this menu the wrong command ships a release.
CHECKS is Release status, Repo signal check and Latest tags. SHIP is Dry run, Run release, Auto release and Create GitHub release, in that order. Changelog and Setup are submenus; Setup holds the three things done once per repo rather than once per release.
Nothing was dropped —
tests/release-menu-smoke.shlists every original handler by name, so a regrouping cannot quietly become a deletion. -
The Dev menu shows ten choices instead of sixteen: Prompts, Edit mqlaunch, Backup mqlaunch, Folders, Create repo, Repo signal folder check, Comment scripts, Env snapshot, Excalidraw, Menus.
Prompts, Folders and Menus are submenus. Nothing was dropped —
tests/dev-menu-smoke.shlists every original handler by name, so a regrouping cannot quietly become a deletion.Network Tools, Themes and Tools Menu were rows 9-11, three doors to other menus sitting between actions. They are grouped rather than removed: Dev is the only menu that reaches Themes and Tools at all, and removing them would have left both reachable only as typed commands.
tools/scripts/test-mqlaunch.shasserted the literal row text"9. Network Tools" "10. Themes", so it failed on a menu where both were still reachable. It pins the routes now, which is what its own name says it checks. -
The System menu shows ten choices instead of sixteen, grouped by what an operator is trying to find out: Performance, Network, Processes, Doctor, Checks, Debug bundle, Maintenance, Desktop, Repo folder, Repo in browser.
Checks, Maintenance and Desktop are submenus. Nothing was dropped —
tests/system-menu-smoke.shlists every original handler by name, so a regrouping cannot quietly become a deletion.Doctor stays on the front menu although it is a check: it is the exit gate and the command doctor itself recommends on a healthy machine. Debug bundle stays because it is reached when something is already wrong and the evidence has to go somewhere, which is the wrong moment to add a keystroke.
The two network rows were removed rather than moved.
show_network_infoand the ghost route are options 1 and 8 of the network menu, so the System menu offered two ways to reach two of its functions and no way to reach the other seven. One row opens that menu instead.
- The System menu numbered its rows 1-13, 16, 17, 14, 15 — MAINTENANCE was printed above NAVIGATION while keeping its later numbers, so anyone picking by position rather than by label got the wrong command. It is 1-10 now, and a test compares the numbers the panel prints against the numbers the case arms answer, failing on a gap, a duplicate, or an option with no arm.
-
The READY banner printed with no colour at all, while the panel directly below it was white.
print_dashboard_headercaptures the dashboard with$( ). Inside a command substitution stdout is a pipe, so the dashboard set its colours behind a guard that acceptsMQ_DASHBOARD_FORCE_COLOR, then sourcedmq-ui.sh— whose guard was-t 1alone. That reset every colour to empty after the dashboard had set them, so the banner carried no escape sequence whatsoever. Both guards accept the flag now.This is why picking a brighter white twice did not fix the banner: the value was never the problem there, the sequence was being discarded.
-
Two panels were drawn with no colour at all, and the stack disagreed about what white meant.
hal_menu_missingandmq_obsidian_missingpassed""as the colour on every row, so the one panel an operator meets when something is already wrong was also the only one that ignored the theme. #139 made the colour themeable but only caught menus that set their own escape — passing an empty string is the same defect with the opposite symptom.C_WHITEmeant1;97ingitlaunch.sh, the zsh theme and the prompt preview, but37— grey — in both dashboards and the miami background. The READY banner sits directly above a panel, so the disagreement showed up as two shades of almost-white on one screen.mq-ui.shdefinesC_WHITEnow (MQ_COLOR_WHITE), and the panel reads the same value, so the stack has one white rather than two that nearly match.That white is
38;2;255;255;255, not a palette index.0;37,1;37and1;97are all names the terminal profile resolves, and on this machine the border still read as dim after two of them; naming the colour outright takes the profile out of the decision.97is emitted first so terminals without truecolor get bright white instead of falling back to the default foreground.tests/panel-color-smoke.shgained both rules. Step 7 was first written as three piped greps, one holding an empty alternation that BSD grep rejects — the middle stage errored, the pipeline returned non-zero, and the step printed "ok" having checked nothing. It is python now.
-
The HAL submenus drew a header and then died on
bad substitution. The panel built its section heading with${title^^}— a bash 4 expansion. macOS ships/bin/bash3.2, and the menu is sourced into whatever shell mqlaunch runs under, so opening Memory produced a half-drawn box and no menu.The pipeline was green throughout: CI runs bash 5, where the expansion works, and
bash -ndoes not evaluate substitutions. The smoke test drove only the front loop, so no submenu was ever entered by anything.tests/hal-menu-smoke.shnow renders all three submenus under every shell present on the machine and requires the heading to appear — a panel that fails to render prints no heading either, so the check cannot pass on nothing.
-
The HAL menu ran unrecognised input through a shell. Its front loop ended in
*) /bin/zsh -lc "$choice" 2>/dev/null || true, so a typo at thehal>prompt was a shell command — and2>/dev/nullmeant it failed silently when it was not one.Measured rather than reasoned about. Typing a
touchline at the prompt on the previous menu created the file; on this one it does not, and! touch …still does:before touch <path> the file appeared after touch <path> "Unknown HAL choice", nothing ran after ! touch <path> the file appearedShell now needs an explicit
!, the panel says so, and2>/dev/nullis gone so a failing command is visible. Two steps intests/hal-menu-smoke.shdrive the real menu and check for the file either way, so neither half can be removed quietly.The first version of the guard step passed on CI while proving nothing.
mq_hal_menu_mainreturns 127 before reading a line when$MQ_HAL_BINis missing, and a runner has no~/mq-hal— so the menu never ran, no file was created, and the step reported "a typo stays a typo". The step after it caught that, because a run which never happened cannot execute! toucheither. Both steps drive a stub backend now, and reintroducing the fallback was checked to make the guard fail.The same fallback is still in
terminal/menus/mq-performance-menu.sh:98, without even the|| true. Out of scope here and reported rather than fixed quietly.
-
The HAL menu shows ten choices instead of seventeen, grouped by what an operator wants rather than by backend command name: Brief, Repo status, Release readiness, CI status, Doctor, Fix plan, Memory, Diagnostics, Repos, Prompt.
Memory, Diagnostics and Prompt are submenus. Every action that was on the flat menu is still reachable.
Two of the seventeen were not in the grouping brief:
auditandcontext. Neither is dropped — both are in Diagnostics, andauditalso keeps itsa/auditshortcut typed straight into the front loop, which costs no visible row. -
Back and quit were half-counted. The inventory treated
x|X) exit 0as an operator choice whileb|B|back)never matched its arm pattern at all, so whether a menu's exit row reached the total depended on how the arm happened to be spelled. ROADMAP P2 counts what a menu offers to do, not the ways out of it, and exit arms are classified as navigation now. That tookperformanceoff the over-limit list without touching the menu. -
The panel border and its host/mode row are now themeable, and white by default.
surface_panel_color()printed a literal\033[0;37m— ANSI white at normal intensity, which terminals render grey — so the box around every menu was the one surface element no theme could reach, while the title it framed tookMQ_COLOR_TITLE.It now reads
MQ_COLOR_PANEL, defaulting to1;37. Four menus (mq-main-menu.shtwice,mq-help-center-menu.sh,mq-performance-menu.sh) assigned the same grey escape themselves behind their own[[ -t 1 ]]guard; they call the library now, so the tty andNO_COLORchecks live in one place.tests/panel-color-smoke.shpins the colour as themeable and single-sourced rather than pinning a shade: the default may change, but the panel has to keep reading it from one place. Its render checks run through a pty, since the colour block is guarded by[[ -t 1 ]]and would otherwise compare empty strings and pass.
-
mqlaunch focus— the Pomodoro timer intools/scripts/focus.shnow has a command. #132 removed its Tools menu row, and since it had no dispatcher route that left the script orphaned rather than demoted: 169 lines of working tool reachable by nothing.It works, which is why it was routed rather than deleted. Checked before deciding, not after:
focus.sh log read-only, prints the session log focus.sh not-a-thing exit 1 focus.sh (no args) interactive menu, quits cleanly on qArguments are forwarded rather than enumerated.
focus.shownsstart,deepandlogand rejects anything else itself, so declaring them in the dispatcher would be a second vocabulary to keep in step with the first.local-writerather thanread-only: it appends to~/.local/share/mq-focus/sessions.log.It is a public entrypoint, so it appears in
mqlaunch helpunderUTILITY. It did not go back on the Tools menu — that menu is at exactly ten choices, and adding an eleventh to restore a row would undo #132 for a command that now has a better home.
-
The Tools menu shows ten choices instead of thirty, and the Agent menu ten instead of twenty-one. Grouped, not cut:
Tools Skills, Repos and Markdown became submenus; System check and the four folder openers went, being `mqlaunch check` and `mqlaunch repo` with extra steps; Focus Timer went. Agent Repo analysis, Review to brain, Co-change and memory, MCP and Environment became submenus. Demo flow moved to Workflows, beside the other full-stack run. Learn promotion moved into Co-change and memory, where the rest of the memory writes already were.Nothing in the submenus became unreachable; the rows are one level down.
focus.shis the exception — the Tools menu was its only entry point, and it has no dispatcher route, so it is now orphaned rather than merely demoted. -
The per-menu count was measured per file, and a file is not a menu.
mq-tools-menu.shholds five loops, so the inventory reported 23 choices for a menu showing ten — and splitting a long menu into submenus, which is the fix ROADMAP P2 asks for, could never have improved the number. Each option is attributed to the loop containing it now. -
The two remaining menu targets pull against each other. Restructuring removed eleven flat rows and added sixteen, because every submenu costs a row in the parent and a Back arm of its own. Per-loop counts fell; the total rose from 243 to 244. Reaching the 190 target means deleting capability rather than regrouping it, which is a different decision and is recorded as one.
-
Three tests pinned menu layout rather than menu behaviour and broke on the regrouping:
skills-repos-smoke.shgrepped for the labelsSkills auditandRepos diff, andmq-memory-cochange-routing-smoke.shrequired20) _agent_menu_cochangeby number. Both now assert that some option reaches the handler, which is what they were for — the same correctionmarkdownlint-routing-smoke.shneeded. -
No menu option runs a script the dispatcher also routes.
excalidrawin the Apps menu,reapin the System menu, and theself-checkrows in the System and Tools menus go throughbin/mqlaunch. The pin was a ratchet at three while the count came down; it is a hard zero now.A ratchet at zero cannot prove itself the way the others do — "one lower must fail" has nowhere to go.
tests/command-discovery-inventory-smoke.shplants a bypass in a tracked menu instead and requires it to be reported, restoring the file through a trap. The first version of that fixture appended a function and passed while proving nothing: the classifier reads menu options, so the plant has to be a case arm. -
The Tools menu drops its
doctor,doctor --jsonandself-checkrows, and renumbers. All three are on the System menu, which is where checks belong, and all three still run from the CLI. That takes cross-menu duplication to zero without adding an allow-list: nothing yet needs a documented exception, and building the mechanism first would have made the target reachable by writing prose. -
The duplication count was wrong, and it was wrong because of a change made two commits earlier. The inventory reported five commands offered by more than one menu. Four were the generated help list: its rows read
mqlaunch doctor Check the environment, which the invocation scanner matched exactly like a call, somq-help-menu.shlooked like a menu reaching half the registry. Printing a command's name is not a way in. The scanner skips generated list blocks now, and the real count was 1.This also corrects the "three duplications" figure reported while planning this work.
-
tests/markdownlint-routing-smoke.shasserted23. Markdown lintand24. Markdown fixby number, so renumbering the Tools menu broke it — a true statement about the old layout and nothing about whether the rows route correctly, which is what the test is for. It now reads the number out of the panel and requires that option to reachrun_markdownlint, so the rows can move without the gate going red or, worse, being loosened. -
mqlaunch helptakes its descriptions from the registry'ssummaryfield. They used to be typed beside it — two sources for one sentence, the same shape as thechatdrift between help and the index, only on the description instead of the name.45 summaries were rewritten as short, user-facing text. The technical delegation phrasing goes, since
delegates_toalready records it:Delegate stack status and stack operations to mq-agent -> Show stack status and operations Delegate a diff review to mq-agent -> Review a diff System namespace: performance, network, doctor, ... -> Performance, network, doctor, checks, utilitiestools/scripts/generate-help-list.pywrites the block interminal/menus/mq-help-menu.sh, andtests/registry-consumer-parity-smoke.shregenerates it and requires the file to be current. It is generated rather than read at runtime becausedoctorreportspython3as a check that can be missing, and help is the one command that has to keep working on a machine where things are missing — a help screen needing a JSON parser to render is the wrong trade.The validator caps a summary at 66 characters and rejects a multi-line one. 66 is 92 columns, the clamp in
ui/terminal-ui/terminal-width.sh, minus the 26-charactermqlaunch <name>row prefix — derived rather than chosen. The widest row help now renders is 73 columns.Three things help lost, all consequences of having one source:
mqlaunch doctor --jsonis no longer shown. An argument is not a command, and the registry does not model one.- the argument hints
ask "fråga"andfix "fel"are now plainaskandfix. - the AI rows were the only Swedish on the screen and are now English, like the rest of it.
All three forms are in
docs/COMMANDS.md, which remains the complete listing. Say the word if any of them should come back — a small declared set of example rows in the generator would do it without reintroducing a second source of descriptions.
-
A run of
mqlaunch doctoron a healthy machine ends in something to do:✔ MQ operational — 12 checks passed Next: mqlaunch stackDoctor could already say what to fix (#128); on a machine with nothing to fix it stopped at the count. That answered half of what ROADMAP P2's exit gate asks — the operator learned the machine was fine and nothing about what to run.
mqlaunch stackis the landing because it is the one command that shows the whole stack, every repo with its readiness and its own next action, rather than a menu to navigate afterwards.nextis no longer null on a clean run. The field now means "one instruction", not "one fix": the highest-priority repair while anything needs attention, the recommended landing once nothing does. It was added yesterday and has not shipped, so nothing external depended on the old meaning.The recommendation is held to the advertised surface rather than to being non-empty.
tests/doctor-status-contract-smoke.shrequires it to be a registry command withoperator_surfacetrue, so a new operator can find it again inmqlaunch help. Three rejections were each proven separately:mqlaunch markdownlint not advertised by help — could not find it again mqlaunch not-a-command not a registry command open the menu not a mqlaunch commandWith this, P2's exit gate closes: a new operator can run
mqlaunch doctor, read a verdict that matches its own checks, fix what it names, and be pointed at one command that shows the whole stack. -
operator_surfaceinmqlaunch/lib/command-registry.json: whether a command is a public operator entrypoint.mqlaunch helpandmqlaunch commandsshow the 48 that are, grouped bynamespace, and stay quiet about the other 26.Help was curated by whoever last edited the text. It advertised 40 of 74 commands, and the selection did not follow a rule anyone could state —
reviewandrisk-reviewwere listed whilestack,architectureandrepo-healthwere not, though all five are the same kind of delegation to mq-agent.The rule is now a field, and the field is enforced.
tests/registry-consumer-parity-smoke.shrequires help to advertise exactly the public set, and to print each command under a heading naming its namespace.tools/scripts/validate-command-registry.pyrejects acompat_onlycommand marked public, and a public command with no namespace — which would have nowhere to be printed and would show up as a missing row rather than an error.The 26 unadvertised commands stay dispatchable and stay in
docs/COMMANDS.md, which remains the complete listing. They are the ones reached through a namespace or menu (git-log,kill-port,workspace,release-check), variants and implementation detail (theme-macos,docwrite,markdownlint), and second spellings of something already advertised (index,self-check, andmqlaunch, which iscompat_onlyand therefore never advertisable).POPULAR FLOWSis exempt from the heading rule, since it is a selection rather than a namespace — but the exemption is bounded: every command it highlights must also appear under its own namespace, so the section can promote a command and never be the only place it is listed.stackis advertised now, underAGENTand inPOPULAR FLOWS. The entrypoint already worked and already showed all five stack repos; it was simply invisible.Fixed while making the comparison load-bearing: the help extractor matched
mqlaunch\\s+(\\w+), and\\scrosses newlines, so the baremqlaunchline underPOPULAR FLOWSborrowed the next row's first word and reportedmqlaunchas advertised. Harmless while the set was only searched for ghosts —mqlaunchis a real registry word — but a phantom member fails an equality check on every run. -
mqlaunch doctorsays what to do about every check that does not pass, and ends in the one thing to do first:⚠ gh missing — brew install gh ⚠ mqlaunch not in PATH — run ./install.sh from the repo to install the symlink ⚠ 9 of 12 checks need attention Next: run ./install.sh from the repo to install the symlinkThe document carries the same advice —
hinton each check that did not pass, and a top-levelnextthat isnullwhen the status isok— so a script and a person reading the same run are told the same thing. Both fields are additive.The nine brew formulae are listed by name rather than caught by a
*arm. A fallback would turn any check added later intobrew install <whatever>, and a confidently wrong instruction is worse than none:pbcopyhas no formula, it ships with macOS. The names were confirmed withbrew inforather than recalled, and the two non-tool checks point at what the repo actually provides —install.shfor the symlink, the shell profile forOPENAI_API_KEY.The next step follows an explicit fix order, not the order the checks print in. Those are grouped for reading, and following them would advise installing
ezawhilemqlaunchis not onPATH. The launcher comes first because nothing else here is reachable without it, then the tools the launcher shells out to, withezalast because it only changes how listings look.tests/doctor-status-contract-smoke.shgrew two steps. Hints are checked exhaustively rather than sampled — the stripped world warns on all twelve, so a check added without a hint fails the suite instead of printing a blank. The order is pinned by two worlds that differ by a single tool: with onlyezamissing the next step isbrew install eza; withezaand the launcher both missing it must not beeza. One world could have come out right by luck.Found while writing it: the hint step read its input through
< <(python3 …), where a process substitution hides the exit status. A python that bailed out left the array empty and every loop below passed over nothing, reporting "0 warnings, each carrying a hint". It writes to a file now, and asserts the hint count equals the warning count.
-
mqlaunch doctorreported success no matter what it found.tools/scripts/doctor.shended in an unconditionalok "MQ operational", and the counters it might have consulted were only ever updated in JSON mode. On a machine with nine of twelve checks warning, the two modes said opposite things:--json {"status": "warn", "summary": {"ok": 3, "warn": 9, "fail": 0}} human ✔ MQ operational EXIT=0The JSON was honest. The screen — the surface a new operator actually reads — was not, which made this ROADMAP P2's exit gate rather than a cosmetic defect.
Both modes now derive the same verdict from the same counters and exit
0only when every check passes. The summary branches on the run's status rather than on a warning count, so afailcheck added later cannot slip past awarn-shaped condition and print "operational" again.This changes an exit-code contract. No caller gates on it:
terminal/release/mq-release-check.shruns doctor without|| exit 1— unlike thevalidate.shline directly below it — and isset -u, notset -e, so it continues and still exits 0, which was verified by running it against a warning doctor rather than by reading the script.Two tests did gate on it, and both were asserting the wrong thing:
tests/headless-smoke.shran doctor bare underset -e. It is about pausing and output shape, so it now accepts 0 or 1 — and still rejects 124, which a plain|| truewould have swallowed along with the hang.tests/output-mode-parity-smoke.shtreated any non-zero exit as a parity problem. A health command reports its verdict that way, so the rule is now that the exit code and the document must agree: non-zero is accepted only when the JSON says something is wrong. A third fixture proves that allowance is not a hole, by replaying an observation that exited 1 while reporting"status": "ok".
tests/doctor-status-contract-smoke.shis new. It builds a provisioned machine and a stripped one out ofPATHrather than testing whichever machine runs the suite — a CI runner has noezaand a laptop does, and asserting either would have produced a test that passes in one place and fails in the other. It pins that the two modes agree, that the exit status follows the verdict, and that the summary line differs between the two worlds, so it cannot be a constant again. -
mqlaunch helpandmqlaunch commandswere two hand-maintained copies of the same command list, and they had already drifted:chatwas in the index and not in help. Both now render one list,command_listinterminal/menus/mq-help-menu.sh, so a command can no longer reach one surface and miss the other.mqlaunch commandswas not checked by anything.tests/registry-consumer-parity-smoke.shheld help,docs/COMMANDS.mdand the palette against the registry; the index was the one advertised surface with no gate on it, which is why the drift survived. It is now a fourth consumer with help's contract — every word it offers must dispatch, and it must not promote a deprecated alias — plus one of its own: help and the index must offer the same commands.The comparison is on command words, not on text. The index adds a banner and a footer, so the two captures are not meant to be byte-identical.
Run against the previous
terminal/menus/mq-help-menu.sh, the new step names the real defect:`mqlaunch help` and `mqlaunch commands` advertise different commands — only in help: mqlaunch; only in the index: chatThe index is read with a one-or-two-space indent while help is read with two. A stricter anchor was tried first and reported all 43 commands as missing, because the old index indented by one — a true failure for a reason that hid the one worth reading.
-
mqlaunch pulsewroteTERM environment variable not set.to stderr and the four dispatched tools that callclearunguarded now guard it:tools/scripts/pulse.sh,blackout.sh,chat.shandnetwork-ghost.sh.Same class as the
doctordefect: a bareclearfails in a stripped environment — GUI launch, cron, a CI runner — writing to stderr and exiting non-zero. The repo already had the fix in four other scripts asclear 2>/dev/null || true; it simply was not everywhere, so all four adopt the existing idiom rather than a new helper.tests/plain-output-contract-smoke.shgained a static step. Step 11 provesdoctoris quiet by running it, and that does not generalise:pulsespends twenty seconds probing the network, so executing every command to check the same property would make the suite unusable. The new step reads the dispatcher for thetools/scripts/paths it invokes and rejects a bareclearin any of them — 15 tools, expected count zero, so there is no ratchet and no allowance for a wrong row.Scoped to dispatched tools because that is the surface a caller reaches through the documented CLI. Menus and TUI internals have a terminal by construction; the eleven remaining unguarded
clearcalls live there and are out of scope. The relation is read from the dispatcher rather than matched on filenames, since a basename heuristic would be exactly the kind of approximation that has produced two wrong attributions in this area already. -
ghostwas declaredsafety: read-onlyin the command registry whiletools/scripts/network-ghost.shransudo ifconfig $INTERFACE ether $NEW_MACto spoof the machine's MAC address and flushed the DNS cache withsudo killall -HUP mDNSResponder. It is nowdestructive.safetyexists so a consumer can decide what is safe to run, andread-onlyis the value that invites running something unattended. This was a false label with a security consequence rather than a cosmetic one — and the truth was already written down one file over, indocs/COMMANDS.md:mqlaunch ghost # network cloaking (MAC/DNS spoof).tools/scripts/validate-command-registry.pynow rejectsread-onlyon any command whose dispatched script escalates, so the label cannot drift back. The check looks forsudoin command position, not anywhere in the file, and that distinction is what makes it usable:tools/scripts/scan.shcontainsecho "- Restart audio if glitching: sudo killall coreaudiod"— a suggestion printed for the operator — andscanis correctlyread-only. A substring search would have relabelled it.The gate is one-directional on purpose. It says
read-onlyis wrong when a script escalates; it does not choose betweenlocal-writeanddestructive, which is a judgement about blast radius that a regex has no standing to make.Two steps in
tests/command-registry-smoke.shcover it: one restores the oldread-onlyvalue and requires the validator to reject it, the other pins thatscanstaysread-onlyand that its printed suggestion is still there, so the false-positive guard cannot quietly stop proving anything.
-
POPULAR FLOWSmoved from the bottom ofmqlaunch helpto the top of the shared list, so it is the first thing both surfaces show and the index gets it too. ROADMAP P2 asks for the most useful workflows first; they were last. -
The network signal rows go through the dispatcher:
run_network_pulseandrun_network_ghostinmqlaunch/lib/network.sh, and the system menu's GHOST row. Unlikedoctor, thepulseandghostroutes end inreturn $?with no pause of their own, so these callers keep theirpause_enter— dropping it would return straight to the menu and repaint over the output.The pinned bypass count drops from nine to three, and again only part of that is the rerouting:
9 → 6 three rows were attributed to pulse.sh that never called it (see Fixed) 6 → 3 the two network rows and the system GHOST row now route through mqlaunchRemaining:
excalidraw.sh,overseer.sh, and thetest-all.shrow behindrun_self_check. -
Every menu path to
doctorgoes through the dispatcher instead of runningtools/scripts/doctor.shitself.docs/RUNTIME_AUTHORITY.mdnamesdispatch_cli_commandas the single entry point, and the inventory below found twelve menu options with a second one.Three rows changed: the system menu's DOCTOR row, the tools menu's DOCTOR CHECK row, and the tools menu's DOCTOR JSON row. The first dropped its own
pause_enterbecause the dispatcher already pauses, and doing both would stop twice. The JSON row keeps its pause, because the dispatcher deliberately skips it for--jsonso a piped caller is never left waiting on input. That row also loses an existence guard and a missing-script panel — the menu duplicating a decision the dispatcher owns.The pinned bypass count drops from twelve to nine, and the split is worth stating because only part of it is this change:
12 → 11 the inventory stopped misclassifying one row (see Fixed) 11 → 9 the two pinned doctor rows now route through the dispatchertest-allis deliberately untouched. Its menu row goes throughrun_self_checkinmqlaunch/lib/diagnostics.sh, a presentation wrapper with its own header, footer and pause, so rerouting it changes what the menu looks like rather than where the call goes. It belongs in its own slice.
-
The command discovery inventory read past the end of a short handler. Function bodies were excerpted as a fixed 60 lines from the definition, so a 9-line handler's excerpt ran on into whichever function followed it in the file.
That is how
ping_test,show_dns_gatewayandopen_network_settingswere all reported as runningpulse.sh: none of them touch it, but all three sit intools/scripts/mqlaunch_desktop.shnear a neighbour that does. The bypass list claimed fourpulse.shrows where there is one. Bodies are now cut at the function's closing brace, with the line count kept only as a safety cap.Worth stating plainly, because the slice that followed was scoped from the wrong number:
pulse.sh ×4was an artifact. -
The command discovery inventory misclassified any arm that opens a submenu.
2) open_system_menuwas read by following the handler into its body — which is the system menu's own case statement — so the arm was labelled by whatever the first 60 lines of the child menu happened to invoke. It was reported asdispatcher-bypassbecausenetwork-ghost.shran nearby, and it flipped tovia-dispatcherwhen an unrelated row in that menu was rerouted.Neither label was true: the arm opens a menu. Submenu openers are now matched by name before the body is read, which moved 17 arms from
menu-localtonavigationand removed one row from the bypass list.
-
tools/scripts/inventory-command-surfaces.pyandtests/command-discovery-inventory-smoke.sh. First slice of P2 command discovery, and deliberately an inventory rather than polish: the menus are the one discovery surface with no comparison against the command registry.The registry already gates every machine-readable surface —
docs/COMMANDS.md, the README,--helpand the palette are all compared againstmqlaunch/lib/command-registry.json, and the registry against the dispatcher. The 19 interactive menus, which is where an operator actually looks, had nothing. The inventory classifies all 243 numbered and letter-key options:via-dispatcher invokes `mqlaunch <command>` — the single authority outside-registry invokes a word the registry does not declare (e.g. repl) navigation opens another menu or launcher dispatcher-bypass runs a script the dispatcher also routes — two ways in menu-only-tool runs a script the dispatcher does not route at all menu-local local UI or shell logic with no command equivalentFindings, to be acted on in later slices rather than this one: twelve options are
dispatcher-bypass, givingdoctor.sh,network-ghost.sh,pulse.sh,test-all.sh,overseer.shandexcalidraw.sha second entry point beside the dispatcher that runtime authority names as the single one. Fifteen tools are menu-only. Of 74 registry commands, 45 are invoked somewhere in menu code and 29 are CLI-only.The twelve are pinned, not fixed:
--max-bypassfails the suite if the count rises, and the smoke test proves the ratchet is not vacuous by requiring one lower to fail. The classification is heuristic — it reads shell with regexes and follows an option one function deep — so the gate asserts the properties that make the report trustworthy rather than individual rows: every option classified, the counts summing to the option total, and output independent of filesystem order.Four measurements were wrong before they were right, which is the case for inventorying before polishing:
- Handler names are defined in more than one file, and resolving them globally made the inventory shift with directory order — 4 dispatcher calls on one run, 17 on the next. Resolution is now menu-local first.
- The source list walked the filesystem, so a local gitignored
backups/scripts/tree of old menu copies took part in that resolution. The inventory reported nine bypass options here and twelve on a clean CI runner. It now comes fromgit ls-files, and a step plants an untracked colliding handler to prove untracked files cannot move the numbers. - A numeric-only scan of case arms missed that the menus route most commands
through letter keys (
r|R), not numbers. - An invocation regex that did not strip comments read the menus' own prose about themselves ("mqlaunch owns …") as command calls.
-
tests/manifest.tsvandtests/test-inventory-smoke.sh. Every file undertests/must now be classified —active,broken,manual, orobsolete— and the gate enforces four things: a new test file with no row fails, a row whose file is gone fails, anything classifiedactivemust be listed intools/scripts/test-all.sh, and anything notactivemust not be.brokenandobsoleteadditionally require a written reason.Eleven files had sat under
tests/without ever being listed in the suite. Six of them were red. Nothing reported it, because a test nobody runs cannot fail — the repo looked better covered than it was. The suite now runs 47 tests where it ran 40.The five still failing are classified
brokenwith the specific reason, so they are visible as debt rather than as coverage. All five assert source text that has since been rewritten or moved: two againstROADMAP.md, one against the command dispatcher, and two againstterminal/launchers/mqlaunch.sh.The gate was checked by breaking it five ways: an unclassified new file, a row pointing at a deleted file, an
activetest removed from the suite, abrokenrow with its reason stripped, and abrokentest wired into the suite. All five were caught.
-
mqlaunch doctorprinted shell errors whenTERMwas unset — a GUI launch, cron, a nested launcher, a CI runner.tools/cli/mq-ui.shdrew its section separators with a bareprintf "%*s\n" "$(tput cols)", and with no terminal to ask,tputwrote its own complaint to stderr and returned nothing, leavingprintfwith an empty field width:tput: No value for $TERM and no -T specified tools/cli/mq-ui.sh: line 44: printf: : invalid numberTwelve such lines from the first command a new operator is told to run, with blank lines where the six separators belonged — and an exit status of 0 throughout, which is why nothing reported it.
hr()now takes its width fromsurface_terminal_widthinui/terminal-ui/terminal-width.sh, the helper the surface converged on. This was a fourth copy of that decision and the only one carrying no fallback at all. Verified against a stubbedtput: 200 columns clamps to 112, 40 clamps up to 60, 100 passes through, and a failingtputfalls back to 92. Separators on a wide terminal now stop at 112 instead of spanning it, which is the same bound the rest of the surface already uses.Writing the test surfaced a second defect in the same line.
tr ' ' '─'is byte-oriented, so under a C locale it mapped each space to0xe2— the first byte of─— and the separator arrived as 92 bytes of invalid UTF-8. A stripped environment dropsLANGfor the same reason it dropsTERM, so both defects fire together; CI caught this one because its runner has no UTF-8 locale.hr()now builds the rule with parameter expansion, which substitutes the whole sequence.tools/scripts/watch.shhas the sameprintf "%*s\n" "$(tput cols)" | trline and is deliberately left alone: it also callstput civisandtput cup, so it cannot render without a terminal at all. A width fallback there would be unreachable code. Therepeat_charhelpers inui/terminal-ui/mq-ui.shandterminal/launchers/gitlaunch.shshare thetrhazard and are out of this slice. -
tests/plain-output-contract-smoke.shcould not see the defect above. Every check in it either setsTERM=xterm-256coloror sends stderr toDEVNULL, sodoctorcould emit a screenful of shell errors and still pass the output contract. A step now runs it withTERMandCOLUMNSunset, and underLC_ALL=C, requiring empty stderr and a separator that decodes as valid UTF-8 at no less than the 60-column clamp. It asserts by decoding rather than grepping for─, because a grep for that glyph depends on the locale of whoever runs the suite — the same trap the rule itself fell into. -
The last three
brokenrows are closed, and the manifest is at zero. Each was run first and diagnosed from its actual output, because two of the first five reasons written into the manifest turned out to be wrong.All three shared a cause the manifest did not name:
ROADMAP.mdwas rewritten from| Done | ... |tables to prose sections withStatus: Done, sogrep -c '^|' ROADMAP.mdis now 0 and every verbatim table-row assertion had become unsatisfiable.mq-agent-routing-smoke.shandmq-obsidian-command-routes-smoke.shwere rewritten. Their roadmap greps are gone; the live assertions they already carried stay. In the obsidian test the dropped rows claimed the release gate detects schema drift, so the replacement asserts the gate — thatcheck_mqobsidian_manifest_contractis both defined and actually called interminal/release/mq-release-check.sh, since a check nobody calls is not a gate either.mq-memory-cochange-routing-smoke.shwas replaced rather than rewritten. Its failing assertion looked for the literalrun_agent_command memory-cochange, but the dispatcher resolves the verb through a variable —cochange)sets_mem_verb, thenrun_agent_command "$_mem_verb"runs it — so the string can never appear no matter how correct the routing is. The step now sources command mode, stubs the bridge, and callsdispatch_cli_command memory cochange, asserting the verb that arrives and that trailing arguments are forwarded untouched. Same harness astests/delegated-exit-code-smoke.sh.A rule follows from this, written into the manifest header: tests do not assert
ROADMAP.mdtext. A roadmap is a plan, and rewriting it is its job. Two active tests were still violating it —mq-stack-contract-smoke.shmatched three claims there andmq-obsidian-menu-no-promotion-smoke.shone — so both were repointed atdocs/architecture/MQ_BOUNDARY.md, which states the same prohibitions underMust not ownand exists to be binding. No documentation changed; the claims were already there.52 active tests, zero classified out.
-
tests/brain-bridge-smoke.shandtests/skills-repos-smoke.shasserted thatterminal/launchers/mqlaunch.shstill contained the case armsverified|systemsandskills|skill. It contains neither — the command surface moved intoterminal/launchers/mqlaunch-command-mode.sh, whichmqlaunch.shsources at line 166.The
|in those greps is deliberate: the tests match case-arm source text, not a regex alternation, so-Ewas never the missing piece. The file under assertion was the wrong one. Both steps now assert what they meant — the routing lives in command mode, which each test already checks one step earlier, andmqlaunch.shreaches it by sourcing that module.Wiring
skills-repos-smoke.shinto the suite then exposed a second reason it had never run: its last four assertions expectmq-mcp,mq-ums, andmq-agentto appear inmq-repos.py listoutput, which needs those repos checked out beside this one. CI clonesmacos-scriptsalone, so they are now asserted only where there is something to find. The commands themselves still run unconditionally — a crash inmq-skills.pyormq-repos.pyfails the suite everywhere.Reclassified
activeintests/manifest.tsvand wired intotools/scripts/test-all.sh: 49 active tests, threebrokenrows left.
-
Terminal width had two implementations:
surface_terminal_widthinui/terminal-ui/mq-ui.sh, used by 23 files, andgitlaunch_terminal_widthinterminal/launchers/gitlaunch.sh, used only by gitlaunch. The comment on the second said it matched the first so nested panels line up.It did not.
mq-ui.shfalls back to${BOX_INNER:-92}and also defaultsBOX_INNERto 88, so the menus fell back to 88 while gitlaunch hardcoded 92 — and which value applied depended on whethermq-ui.shhad been sourced yet. The clamp bounds agreed; the fallback did not.Both now source
ui/terminal-ui/terminal-width.sh. The fallback is a constant, so it no longer depends on sourcing order, andBOX_INNERis left to mean box width rather than terminal width. The helper is written for both shells because gitlaunch is zsh and everything else is bash. Driven for real: gitlaunch andmq-git-menunow render at the same 92 columns under identical conditions.
-
tests/mq-git-protected-push-smoke.shwas never listed intools/scripts/test-all.sh, so it had never run. It was also red: one assertion grepped gitlaunch.sh for the wordcontinue, which appears in no commit reachable fromHEAD. A test nobody runs cannot fail, so nothing reported it.It is wired in now, the dead assertion is gone, and its four width assertions — which grepped for the function name and the literals
width > 112andwidth < 60— are replaced bytests/terminal-width-smoke.shdriving the clamp. Those four pinned the implementation rather than the behaviour: they passed for code that never ran and would have failed for a correct refactor.Eleven further files under
tests/are still absent fromtest-all.sh, six of them failing when run by hand. That is left as its own piece of work rather than folded in here. -
The wiki Command Reference's banner rule had never fired once. Its grep pattern starts with
--, so grep read the pattern as an option and exited 2 before matching anything — every script whose only title is its header art was published with an em dash.mission-controllooked like a counterexample, but it takes its title from rule 1'sAPP_NAME.Making the rule reachable was one
--. Making it correct needed more: the pattern also matched inside--- SECTION ---separators and<!-- BEGIN ... -->markers, which would have published "COLORS (subtle)" formqlaunch-repland "BEGIN GENERATED SKILLS TABLE" forcheck-skills. Pinning the delimiter to exactly two dashes separates a banner from a separator. The rule's comment also said "banner comment line", but these areprintf/echocalls in the scripts' header art, not comments — the fixtures match what the tree actually contains.Measured before and after against the real tree: 11 rows gain a description, all of them genuine banners, and nothing else on the page moves. The loose pattern would have changed 16 rows, 5 of them wrong.
-
The published wiki Command Reference carried a literal
$dashboardas the description forui/terminal-ui/mq-ui.sh.extract_metamatchedprint_dashboard_header "$dashboard"with itsheader "rule and published the unexpanded variable.Rule 1 was meant to prevent exactly this for
APP_NAME/APP_TITLE, and its comment said so — but the guard was written into asedsubstitution, and on a value likeAPP_NAME="$title"that pattern simply fails to match. An unmatchedsedprints the line unchanged, so the guard did not drop the value; it kept the entireAPP_NAME="$title"line as the description. The check is now its own step applied to all four rules.tests/wiki-command-ref-smoke.shruns one fixture per rule, asserts real descriptions still survive, and then generates the whole page against the real tree and fails if any description column contains a$.Found while auditing the regenerated page after the 2.0.1 release, not by a gate — hence the gate.
-
ROADMAP.mdstill saidCurrent version: 1.0.1two releases later, and the Definition of Done for v2.0.0 stood almost entirely unchecked — after v2.0.0 had shipped and every P0 and P1 block below it was marked Done. A roadmap that disagrees with its own blocks is worse than no roadmap: it makes finished work look outstanding.The version is now 2.0.0 and the Definition of Done is closed against the tree, each box naming the test or document that proves it so the next reader can re-run the proof instead of trusting a checkmark. Everything cited runs in CI.
Seven boxes stay unchecked on purpose. Five claim that
mq-agent,mq-mcp,mqobsidian,repo-signal, andmq-halstill own their responsibilities — claims about other repositories' trees, which this one cannot verify and must not assert. Two namecontract-checkandstack-preflight, neither of which is amqlaunchcommand; both returnUnknown command. The reasons are written into the roadmap next to the boxes rather than left for someone to rediscover.
-
release.shpushedmaindirectly while.mq/repo-contract.jsondeclaredrelease_mode: pull_request.maincarries no branch protection on GitHub, so nothing outside the contract would have refused the push — and v2.0.0 was in fact released through PR #106 plus a hand-made tag, meaning the script was the one part of the flow that disagreed with how releases actually happen.The mode now comes from the contract rather than a flag; a flag would have left
./release.sh <version>working as a direct-push path in a repo whose contract forbids one. Underpull_requestthe bump lands on arelease/v<version>branch, the branch is pushed, a PR is opened viaghwhen available, the checkout returns tomain, and no tag is created — tagging is a printed post-merge step, because the tag belongs on the merge commit.release_mode: directkeeps the previous behaviour for repos whose contract asks for it.tests/release-pull-request-mode-smoke.shproves it against a real bare origin rather than a command log:main's SHA is unchanged, the release branch exists and carries the bump, no tag was pushed, and the checkout is back onmain. Dry-run is asserted to leave no branch, no commit, and a clean tree;directis asserted to still advancemainand push its tag. -
release.shpromised in its usage text that "if the script aborts before commit, VERSION, README.md and the contract are restored" — and never did so for any of its gates. Bash does not run anERRtrap for an explicitexit, and every gate exits rather than failing a command, so the CHANGELOG check, the contract re-gate, and the tag checks all skipped the rollback. Driving./release.sh --dry-run 2.0.1against this repo found it: three bumped files left on disk, no rollback line, no failure line.An
EXITtrap now covers whatERRcould not. It is guarded on a mutation flag rather than running unconditionally: before the first bump the tree still holds the operator's own work, andgit checkout --there would discard it rather than restore anything — mistyping a flag must not clean the tree. The fix ships with therelease_modechange rather than after it, because a gate tripping after the release branch is cut would otherwise strand the checkout on that branch. -
docs/AUTHORITY_MAP.mdlistedterminal/menus/mq-hal-menu.sh.bak.20260519-115142under Dead — DEPRECATED as a file awaiting deletion. It is not in the repository and never has been..gitignorehas matchedterminal/menus/*.bak.*since 2026-04-12 and the file is dated 2026-05-19, so it could not have been committed:git log --allon the path is empty, and the path returns 404 onmain. It is an untracked editor backup in a local working copy.The 2.0.0 entry that introduced the claim stays as written — a published changelog records what was believed then — and this is the correction. The error was reading a working directory and calling it the repository.
git ls-filesanswers that question and was not asked.
Runtime authority and command-surface governance. One dispatcher, one command registry with five gated consumers, an enforced output contract, and ShellCheck raised from error to warning severity.
-
A command must delegate to the repo that owns it.
validate-command-registry.pyalready requireddelegates_toto be non-empty when the owner was another repo, but not to name that owner — so an entry could declaremq-agentas owner while routing tomq-mcp, the boundary violationdocs/RUNTIME_AUTHORITY.mdforbids.delegates_toholds the whole delegated command (mq-agent review, notmq-agent), so the rule compares its first word. All 16 delegating commands already satisfied it; the point is that they now have to. Fixture [17/17] proves the gate fires.Found by checking the roadmap against the tree rather than trusting its checkboxes: "Test delegation ownership" was true by convention and ungated.
-
tests/docs-file-inventory-smoke.sh— a README that documents a file by name must not outlive it. Three READMEs describe their directory file by file as###name`` sections:ui/ascii, `terminal/menus` and `automation/workflows`. That form is a hand-written inventory, and it had already gone stale: `ui/ascii/README.md` carried a section for `mq-skull.txt` long after PR #32 deleted the file. Confirmed the gate catches it — run against the README as it stood before the fix, it reports `mq-skull.txt`.One direction only, the same contract
mqlaunch helpand README follow: documented files must exist, coverage is not required.terminal/menus/README.mdwrites up 11 of its 21 files by design, and forcing the rest in would turn it into a directory listing the shell already provides. The uncovered count is printed rather than enforced — a number moving is a prompt to look, not a build break.docs/AUTHORITY_MAP.mdclaimed stray*.sh.bakfiles were removed in PR #32. One survives —terminal/menus/mq-hal-menu.sh.bak.20260519-115142, dated after that cleanup, referenced by nothing. The claim is corrected and the file is listed as dead rather than deleted here, since deletions belong in their own diff. -
tools/scripts/shellcheck-report.sh— measures what a stricter ShellCheck gate would cost, and changes nothing. Deliberately not wired intotest-all.sh: a measurement that gates is no longer a measurement, and it would slow every suite run for a number nobody is reading that minute.It corrected the premise the roadmap was planning against. ShellCheck is not warn-only today —
tools/scripts/lint.shrunsshellcheck -S errorwith no|| true,test-all.shcalls it, and CI runstest-all.sh. Error severity has been a hard gate all along. What is warn-only is the separatequality.ymlstep, which runs the same severity over a wider surface and discards the result.Measured: 0 errors, 96 warnings across 37 files, 270 at info, 276 at style. 51 of the 96 warnings are one rule (SC2034, unused variable) and the next four rules account for 36 more, so roughly nine tenths of the warning surface is five rules. 10 zsh scripts — including
mqlaunch.sh— are outside ShellCheck entirely, since it cannot parse zsh; they are covered byzsh -n. 34 files are scanned by the warn-only CI step but not by the gate, and at error severity they are clean too.The report verifies its own relevance: it runs
lint.shand fails if the file count it scanned differs from the countlint.shgates, so a divergence in exclusions surfaces instead of quietly turning the numbers into a description of something else. Rule descriptions come from ShellCheck's own output rather than a table, so they cannot go stale against the installed version. -
deprecated_aliasesin the command registry. An alias can outlive the reason it was added: deleting it breaks whoever still types it, and leaving it inaliasestells every consumer it is a current name. The field is the third state — still dispatched, no longer part of the surface a consumer may advertise.It is metadata on the alias, not a flag on the command. Retiring an old spelling says nothing about the command it points at, and
deprecated: trueon the entry could not express the difference. Keeping it out ofaliasesalso makes the "not advertised" rule structural rather than enforced: a consumer building its surface fromaliasesexcludes it without knowing the field exists.validate-command-registry.pyenforces four rules — a deprecated alias must name areplacement; that replacement must be an active command name or alias and must not itself be deprecated; a word must not be listed as both active and deprecated; one word belongs to exactly one command. A deprecated word is still dispatched, so it stays subject to registry-versus-dispatch parity.tests/registry-consumer-parity-smoke.shcarries the consumer half: neithermqlaunch helpnor the palette may offer a deprecated word, whiledocs/COMMANDS.mdmay still document one — a reference that records the old spelling is doing its job.Five fixtures, each asserting the reason and not merely a non-zero exit: a well-formed deprecation still validates, a missing replacement fails, a replacement nothing dispatches fails, a word that is active and deprecated at once fails, and a deprecated word two commands claim fails. The consumer fixture is built from a word
mqlaunch helpactually prints, so it raises rather than quietly testing nothing if help stops advertising aliases.Nothing in the registry is deprecated today. The words that would have qualified —
tools-menu,dev-v1,gitlaunch— lived in the second dispatcher and went with it. The rules exist so the first real deprecation is stated rather than remembered. -
Subcommands in the command registry. The nine namespaces that dispatch a nested
case "$sub" in—workspace,srm,repos,system,git,release,dev,help,obsidian— now declare their 47 subcommands with aliases and summaries, plusunknown_subcommand, which records whether the namespace rejects an unrecognised word itself (exit 2) or forwards it to a delegate. That is the one thing a consumer must know before publishing the list as complete:systemrejects, so its list is the whole surface;reposforwards, so the registry can only speak for whatmqlaunchroutes.validate-command-registry.pygates all of it in both directions — a subcommand the dispatcher does not handle, a dispatched subcommand or alias the registry omits, a namespace that grows a nested case and declares nothing, and anunknown_subcommandvalue that contradicts the*branch. Five fixtures intests/command-registry-smoke.shassert the gate fires for the right reason, not merely that it exits non-zero. -
mqlaunch/lib/command-registry.json— the canonical inventory of top-levelmqlaunchcommands: 67 entries covering 145 names, each carrying its aliases, namespace, summary, owner repo, safety mode, output modes, JSON support, interactivity, compatibility status and delegation target.tools/scripts/validate-command-registry.pyis the gate: it rejects duplicate names and alias collisions, empty summaries, unknown owners or safety modes, and JSON claims not backed by a JSON output mode — then walksdispatch_cli_commandand fails if the registry and the dispatcher disagree in either direction. Registered astests/command-registry-smoke.sh, which also asserts that the gate itself fires rather than passing everything. The registry sits on the authority-owned path required bydocs/RUNTIME_AUTHORITY.md. Nothing consumes it at runtime yet; help, palette and docs generation come later in the v2.0.0 block.
-
ShellCheck is enforced at warning severity.
tools/scripts/lint.shruns-S warninginstead of-S error, which is the actual change —errorhas been a hard gate since before this work, becauselint.shhas never carried|| trueandtest-all.shcalls it. The roadmap said "ShellCheck must not remain warn-only forever" and was planning against a premise that was wrong.The warn-only step in
.github/workflows/quality.ymlnow callslint.shrather than re-deriving the file list with its ownfindand its own exclusions. It could not simply drop|| true: its wider surface includestools/legacy/andterminal/mqlaunch-v1/, which still hold 5 warnings thatlint.shdeliberately excludes, so removing the safety net would have failed CI on frozen paths that are closed to new work. Sharing one surface fixes that and removes the duplicate definition.shellcheckis installed in CI and its presence asserted before the gate runs.lint.shexits 0 when the binary is missing so a developer without it can still run the suite; unguarded, that convenience would have made the CI step unfailable.Verified by mutation: an unused variable added to a throwaway script fails the gate with SC2034 and exit 1, and removing it returns to green.
The ShellCheck version is pinned in CI rather than taken from apt. The first enforced run failed on two SC2120 findings the version used for the measurement does not report at all — the rule set moves between releases, so an unpinned gate means something different depending on when it runs. Both findings were fixed and the baseline is now zero under 0.9.0 and 0.11.0 alike:
surface_git_statekeeps its repo argument with a directive naming the contract test that passes one, and_run_demo_flowlost a parameter no caller had ever supplied. -
The nine unquoted command substitutions ShellCheck flagged for word splitting are now quoted (SC2046). Eight are
read A B C <<< $(…)intools/scripts/scan.sh; the ninth passes a default-gateway lookup as an argument intools/scripts/pulse.sh.Quoting a here-string is only safe when the command emits one line — unquoted, word splitting flattens newlines into spaces, so a multi-line result feeds every field to
read, while quoted it would read the first line and drop the rest. All eight emit exactly one line by construction (awk 'NR==2 …'and a singleEND { printf }), which is why quoting them changes nothing.Verified against a deterministic stub rather than the live process list:
scan.shreadspsandvm_stat, so its output differs between two runs of the same code and whole-output diffing cannot decide the question. With a stubbedpsemitting irregular whitespace and a process name containing spaces, both forms produce identical variables, as does the empty-output edge case.pulse.shoutput is byte-identical before and after.With this the ShellCheck warning baseline is zero, down from 96 when the measurement started.
tools/scripts/shellcheck-report.shnow reports-S warning already clean — a gate here costs nothing today. -
The five unquoted
@{u}git refs now match the two the repo already quoted.@{u}is git's upstream shorthand, not shell — and the braces have to reach git literally. They already did: neither bash nor zsh expands a single-element brace, which is why{a,b}splits and{u}does not. So SC1083 was flagging a form that worked, in the two spellings the repo used side by side.Quoting is what ShellCheck suggests, is what
ui/ascii/mqlaunch-dashboard-v7.1.sh:131andautomation/workflows/workspace.sh:100already did, and states that the braces belong to git. Verified identical against a real upstream (origin/main,1 0), against a repo with no upstream (exit 128 either way), and throughtests/git-status-contract-smoke.sh, which drives the live dashboard.Everything ShellCheck governs is now quoted.
terminal/launchers/gitlaunch.shhas five more unquoted, unflagged because it is zsh and outside ShellCheck's reach; it was checked and is not a defect — zsh leaves@{u}literal exactly as bash does.SC1083 is now zero; the warning count is 19 → 9.
-
The ten dynamic
sourcecalls ShellCheck could not follow now carry a directive. Four name the real file —mq-ui.shfrommacos-tweaks.shandmq-ui-demo.sh,mq-ai-prompts.shfrom the release check, andmq-performance-menu.shfrom the performance bridge, which had adisable=SC1091standing in for a path it could simply have named. Six cannot: the mqobsidian and recommendations menus build their paths inside a loop over a list of library names, andask/chat/fix/srmsource an operator's~/.env, which lives outside the repo and is optional by design. Those getsource=/dev/nulland a comment saying why no target exists.Paths are script-relative, which is the form that resolves wherever ShellCheck is invoked from. That matters more than it looks:
source=resolves against the working directory by default and against the script's directory under--source-path=SCRIPTDIR, andlint.shruns from neither predictably. All four were verified to resolve to a file that exists.SC1090 is now zero; the warning count is 31 → 21. The diff is comments only — 17 lines added, and the single removed line is the
disable=SC1091a directive replaced. -
tests/command-docs-smoke.shis a README contract instead of a second, hand-maintained command registry. It named eleven commands —palette,ghost,pulse,reap,guard,mc,nickname-set,theme-macos,theme-reset,bundle— and grepped for each indocs/COMMANDS.mdand again in the dispatcher. All twenty assertions were strictly weaker than gates that already run:tests/registry-consumer-parity-smoke.shrequires COMMANDS.md coverage in both directions for all 73 commands, andvalidate-command-registry.pyholds registry-versus-dispatch parity exactly. All eleven words were verified present in the registry and in COMMANDS.md before the assertions were removed, so nothing stopped being checked — it is checked for 73 commands now rather than eleven.In their place, README becomes the fifth consumer of the registry, and the first one that was never checked at all. It is the first page anyone reads, it prints commands in runnable blocks, and nothing verified those commands exist. README has the same contract as
mqlaunch help: curated, so coverage is not required, but every command it shows must dispatch and it must not promote a word the registry is retiring. Extraction reads only fenced runnable blocks, so the product boundary written as prose —mqlaunch shows the right workflow— does not contribute a command calledshows. Two fixtures, each asserting the reason rather than a non-zero exit: a command README shows that nothing dispatches, and a deprecated spelling README still teaches.One assertion was dropped rather than moved: nothing now requires README to mention
mqlaunch palettespecifically. Which commands earn a place on the front page is a curation decision, not a contract, and a gate cannot tell the difference between a command being removed from README and a command being removed. -
Added a root
release-check.shconforming to therepo_release_check.v1contract:--jsonemits the machine-readable verdict (schema,repo,status,blockers,warnings,evidence) on clean stdout and exits 0; human mode prints per-check ok/FAIL. Runs the read-only checks (contract/ CHANGELOG/README version surfaces, check-skills, runtime-authority freeze,bash -nsyntax, mqlaunch smoke suite). Lets mq-agent'sstack release --all --preflightread the release verdict.
-
ui/ascii/mq-dashboard.shaborted halfway through rendering. Three lines wrote${mq_repeat_char "-" "$width"}— a parameter expansion of a variable by that name — where$(mq_repeat_char …), a command substitution, was meant. bash raisesbad substitutionat runtime, whichbash -ncannot catch because it is not a parse error.The effect was not a missing separator. The function stopped at the first one, so ten lines never printed: user, host, time, shell, OS, repo, branch and all three rules. Verified by diffing the script's output before and after —
mainemits one line to stderr and 12 lines of dashboard, this emits nothing to stderr and 22.ShellCheck reported it as SC2154, "mq_repeat_char is referenced but not assigned", and only for the first of the three occurrences.
docs/AUTHORITY_MAP.mdlists this file under "Dead — DEPRECATED … safe-to-delete candidates", alongsidemq-dashboard-v3.shandmq-banner.sh. Deleting the three is a separate decision and a diff worth seeing on its own; this makes the code correct without presuming it. -
backup_zshrcinterminal/themes/mq-zsh-theme-switcher.shdeclared and assigned in one statement, solocalmasked the exit status of the command substitution (SC2155). Split. Both callers use$(backup_zshrc)and read stdout, and the function's status comes from itsecho, so nothing observable changes — confirmed by driving the function against a temp directory. -
Three submenus advertised
x. Exitand did not implement it.mq-apps-menu,mq-system-menuandmq-help-center-menueach carriedb|B|x|X|exit) returnfollowed by anx|X)branch that printedExiting …and calledexit 0. The first branch already matchedx, so the second was unreachable: pressingxreturned to the parent menu while the panel said it would exit.The label was wrong, not the code. Every other submenu in the repo —
mq-dev-menu,mq-git-menu,mq-ai-menu,mq-net-menu,mq-tools-menu— uses the sameb|B|x|X|exit) returnand advertises onlyb. Back. These three were the outliers. The dead branch is removed and the panel row now readsb. Backalone, so what is shown matches what happens.Behaviour is unchanged, and that is verified rather than asserted: both
bandxwere driven through all three menus againstmainand against the branch, and every exit status is identical. -
A duplicated pattern in the main menu's REPL dispatch.
hal\ *|"hal "*)spelled the same match twice — an escaped space and a quoted one — so the second alternative could never be reached. Collapsed to"hal "*). -
repo_statereportedcleanfor a checkout holding untracked files. All three reporting surfaces —mqlaunch status --json, the status dashboard andmqlaunch version— derived it fromgit diff --quiet --ignore-submodules HEAD, which sees tracked changes only.mqlaunch githas always gated ongit status --porcelain, which counts untracked files, so the same tree could readcleanfrom one command and have changes according to another. One repo should not answer "is this tree clean" two ways (#66).dirtynow means any working-tree change, untracked files included. The question a consumer is asking is whether the checkout is safe to act on, and an untracked file is a change even though it does not differ fromHEAD.The third value is
not-a-git-repoeverywhere.mqlaunch status --jsonsaidunknown, which reads as "could not determine" — a different claim from the one being made. The dashboard had no third value at all: a missing checkout read asdirty. The token covers git being unavailable too, since without it a checkout cannot be told from any other directory and the caller's options are the same either way.The derivation now lives in one place,
mq_repo_stateinui/terminal-ui/mq-ui.sh, next tomq_git_status_snapshot— which already counted untracked files and already distinguished a non-checkout. The divergence was three surfaces re-deriving an answer the shared library was producing correctly.tests/plain-output-contract-smoke.shdrives all four states against real checkouts in a temp directory, asserts that the untracked-only fixture is one the old logic would have called clean, and fails if any surface goes back to a tracked-only diff. -
mqlaunch release-check --jsonprinted the human banner and exited 0. The route forwarded flags toterminal/release/mq-release-check.sh, which reads only--brainand discards everything else, so a caller asking for JSON got a successful-looking banner to parse.--jsonnow reachesrelease-check.sh, which owns therepo_release_check.v1contract, and its output is identical to running that script directly. Unknown flags exit 2 with a usage error instead of being ignored. Human mode is unchanged and keeps the wider review the two scripts do not share — secrets scan, mqobsidian manifest contract, changelog versus commits. The registry entry declared"json": falseand now matches. -
mqlaunch git helpnever returned. It reachedopen_git_menu, which treats its argument as a repo path, sohelpbecame a failed path lookup and the interactive menu opened and looped on EOF — 12580 bytes and no exit without a terminal.help,-hand--helpnow resolve before the namespace body for every namespace mqlaunch routes, sogit,system,releaseanddevjoin the seven that already exited 0.system,releaseanddevpreviously exited 2 for a successful help request. -
An unknown subcommand now behaves the same across the namespaces mqlaunch routes itself: a diagnostic on stderr, help on stdout, exit 2, and no menu.
obsidianused to print a hand-written usage string and exit 1, whilesystem,release,devandhelpexited 2 silently.git,srm,reposandworkspaceare deliberately unchanged — they have no closed subcommand set, and the reasoning is indocs/plans/P1-command-registry-subcommands.md. -
mqlaunch repos LISTfailed whilemqlaunch system TIMEworked. Command words are lowercased intosubbefore routing, but thesrmandreposbranches shifted and re-read the raw"${1:-}", so those two namespaces were case-sensitive and nothing else was. Both now route on the normalised word, andreposforwards it tomq-repos.py, which matches its command word exactly. Only the command word is normalised: arguments keep the case the user typed, an unknown subcommand is still forwarded verbatim so the delegate's error names what was actually typed, and the free-text question thatsrmhands tosrm.shis untouched. Implements D1 ofdocs/plans/P1-command-registry-subcommands.md. -
22 dispatch branches invoked a script under
$BASE_DIRand then ended in an unconditionalreturn 0, so a failing delegate was reported as success:mqlaunch repos LISTandmqlaunch skills no-such-subcommandboth wrote a usage error to stderr and exited 0. Every delegating branch now capturescommand_statusand returns it, matching the idiom the agent and HAL routes already used.pause_enterno longer overwrites the status on the routes that call it.tests/delegated-exit-code-smoke.shgained the two behavioural cases and a structural check that fails if any delegating branch reverts to an unconditionalreturn 0. -
mqlaunchdispatchedmemoryfrom two different branches. Thesrm|memory|repo-memorybranch claims it first, so thememorylisted in the brain-bridge branch below it could never match —mqlaunch memoryhas always gone to the SRM surface, never to the brain bridge. Removed the unreachable token; behaviour is unchanged, and the registry gate now fails if a duplicate route is introduced again. Found while mapping the command surface for the registry. -
docs/RUNTIME_AUTHORITY.md— the runtime-governance contract formqlaunch. Names the single live authority (bin/mqlaunch→mqlaunch.sh→mqlaunch-command-mode.sh), the allowed and forbidden responsibilities of that path, the LIVE/COMPAT/DEPRECATED/TEST-ONLY classes, the compatibility policy and its removal gate, and the dependency direction that forbids new live edges intoterminal/mqlaunch-v1/.AUTHORITY_MAP.mdkeeps the path-by-path inventory; this document holds the boundary that must stay true while those paths migrate. Linked from the README documentation map,COMMAND_SURFACE.mdandMQ_BOUNDARY.md. Closes the P1 "Single runtime authority" roadmap block, whose delegation tests already existed intests/compat-path-delegation-smoke.sh. -
mqlaunch status --json(andabout --json) now emit machine-readable JSON only. Theabout|statusroute ignored its arguments and always rendered the dashboard, so a caller piping--jsongot ~5.9 KB of ASCII banner, no JSON, and exit 0 — a silent failure.dispatch_cli_commandnow branches onhas_json_flagintoprint_status_json, which prints one JSON document (project,version,release_stage,repo_state,latest_bundle) on clean stdout. The JSON path also drops the dashboard's smoke-test field, which shelled out to the full test suite: the call went from 20.7 s to 0.1 s and no longer recurses when run fromtest-all.sh.repo_statereportsunknowninstead ofdirtywhen the repo root is not a git checkout. Proven by two new steps intests/plain-output-contract-smoke.sh. -
The git "auto commit + push" automation no longer leaves the working checkout off-main. After creating and pushing an
mq/...PR branch,create_pr_branch_for_push(ingitlaunch.shandmq-git-menu.sh) now callstools/scripts/git-restore-to-base.sh, which restores the checkout to the base branch and rewinds the local base ref to origin — non-destructively, since the commit is already on the pushed PR branch. Runs even when the push or PR step failed; reports repo/branch/dirty/next-command if it cannot restore. Proven bytests/git-restore-to-base-smoke.sh.
-
Nine variables that were assigned and never read:
SCRIPT_NAMEinmqshortcuts.sh(its usage heredoc is quoted, so nothing expanded it),REPO_ROOTindemo-flow.sh,MQ_MCP_REVIEWin the tools menu (a path constant naming a script that menu never invokes),C_DIMinmacos-tweaks.shandBOLDinenv-snap.sh(the only colours in either palette nothing used), an unusedwarningslocal in the performance menu,top_glowandbot_glowinmq-dashboard-v3.sh(built from═while the lines the dashboard actually draws use▄and▀inline), and adelaylocal invault-scan.sh.vault-scan.shis worth flagging rather than quietly resolving: the local said0.02while the loop below it sleeps0.01as a literal. Either the variable was meant to be used or it is a leftover. Wiring it up would double the animation delay, so this removes the dead local and changes no timing. If the intent wassleep "$delay", that is a separate decision about how the effect should look.Two loop counters the bodies never read — in
excalidraw.shandwatch.sh— became_.Six more SC2034 findings stay, each with a directive naming what reads it: three positional
readfields inscan.shwhere dropping the name would makereadappend the value to the previous variable, and three dynamic-scope contracts (THEME_SCRIPTandMQ_THEME_ERROR_HEADING_BOLDread bymqlaunch/lib/themes.sh,MQ_SURFACE_WIDTHby the prompt-separator pattern documented in.claude/templates/).SC2034 is now zero; the ShellCheck warning count is 96 → 45.
-
run_arg_command, the second command dispatcher, and the freeze that held it. It answered the command palette and accepted 93 words the registry never modelled — names that worked when chosen and printedUnknown commandwhen typed. Rerouting the palette throughdispatch_cli_commandleft it with no callers, so this deletes 148 lines of unreachable launcher, themqlaunch/lib/legacy-command-vocabulary.txtbaseline that recorded its words, andtests/legacy-vocabulary-freeze-smoke.sh, which existed to hold a surface that no longer exists.legacy_alias_noticegoes with it: its only callers were inside the deleted function.dispatch_cli_commandis now the only dispatcher, so the registry governs the whole command surface rather than most of it.No command changed behaviour — every word in the deleted function had already stopped dispatching. What did change is what the gates prove.
tests/compat-path-delegation-smoke.shwas matching the wordrun_arg_commandin a comment, so it would have stayed green after the call it checks was deleted; it now anchors to the call and fails if a second dispatcher reappears anywhere underterminal/,mqlaunch/orui/. Four assertions intools/scripts/test-mqlaunch.shwere pointed at case labels inside the dead function: the performance, dev and tools routes moved tomqlaunch-command-mode.sh, which is the file that can now be wrong about them, andrestartmoved toterminal/menus/mq-main-menu.sh, where it is a menu key rather than a command word. The check for the legacy Tools aliases —tools-menu,toolsmenu,menu-tools,tools-v1,menu-tools-v1— is gone with them; nothing advertises them, and asserting the strings were still present only proved they were remembered.tools/scripts/mqlaunch_desktop.shkeeps its own copy. It is a separate live entrypoint that dispatches for itself, not a caller of this one.
mq_debug— a diagnostic logger inmq-ui.sh, silent unlessMQ_DEBUGis set, writing to stderr and always returning 0. Use it as… || mq_debug "why"to make an unexpected best-effort failure observable instead of swallowing it with2>/dev/null || true. Adopted at the git-screencdguard and the Obsidian menu's optional-lib loader; broader adoption is incremental.scripts/check-skills.sh— validates skill frontmatter, cross-references, referenced paths, and the generated SKILLS.md table; wired into the Quality workflow.--fixregenerates the table.- Evals sections (should/should-not trigger) in every skill.
- ROADMAP now tracks the
mqobsidianv0.1.0 single-source-of-truth foundation as an upstream dependency, while keepingmqlaunchread-only or delegate-only for truth, inbox, ranking, and promotion surfaces. - Documented the canonical mqobsidian view-manifest keys consumed by
mqlaunchand marked that roadmap deliverable done. - Added a smoke test that locks the MQ Obsidian menu out of memory promotion or rejection routes, preserving the review-gated ownership boundary.
- Verified the review command delegation boundary through
tests/mq-agent-routing-smoke.shand marked the roadmap item done. - Added direct
mqlaunch obsidian status,mqlaunch obsidian inbox, andmqlaunch obsidian viewsroutes for read-only mqobsidian status/navigation. - Added
mqlaunch obsidian promoteas a thin delegate tomq-agent obsidian promote, keeping scoring and memory writes out of mqlaunch. - Added a release-check gate for the local mqobsidian view-manifest consumer
contract, blocking malformed or drifted
views.jsonbefore release. - Added delegate-only
mqlaunch stack status/mqlaunch stack ...routing tomq-agent stack, keeping canonical truth parsing outside mqlaunch. - Monolith de-layering (Step 11a): five concerns moved verbatim out of
terminal/launchers/mqlaunch.shinto dedicated libraries sourced back into the launcher's scope — the network concern (status/diagnostics/connectivity, 17 functions →mqlaunch/lib/network.sh), the fzf interactive pickers (git log/branch, kill process/port, run snippet, recent files, 6 functions →mqlaunch/lib/fzf-pickers.sh), diagnostics (version reporting, self-check, debug bundle, release notes, plus the system check, 6 functions →mqlaunch/lib/diagnostics.sh), the git & release menu launchers (2 functions →mqlaunch/lib/git-menus.sh), and the GitHub repo picker (1 function →mqlaunch/lib/repo-picker.sh). No behavior change; the launcher drops ~720 lines and each concern now has a named owner. Guarded bytests/monolith-delayer-smoke.sh, a table-driven check that fails if any extracted function is redefined in the monolith or a lib source is dropped. print_headernow has a single owner:ui/terminal-ui/mq-ui.sh. The launcher no longer overrides it; instead it opts the main loop into the dashboard-v7.1 header viaMQ_USE_DASHBOARD_HEADER=1. Behavior change: the main-loop header now renders through mq-ui's cached dashboard path (bounded byMQ_DASHBOARD_CACHE_TTL, default 5s, and invalidated by the git/release flows) instead of re-forking the dashboard script uncached on every screen. This removes a hidden fork in render behavior and aligns the main loop with the cache and invalidation model the rest of the runtime already used; it also avoids a dashboard re-fork per screen render. Escape hatch: setMQ_DASHBOARD_CACHE_TTL=0for an uncached, per-screen header while investigating. The deadDASHBOARD_V71launcher variable (only the removed override used it) is gone.semantic-memory-maintainerrenamed tovector-store-maintainerto avoid routing collision with mq-mcp's skill of the same name.command-template-libraryskill moved to mq-ums, where the contracts and generator it documents live.- The mqlaunch menu/GUI construction standard now has a single owner
(
mqlaunch-command-surface);terminal-ui-polisherreferences it instead of duplicating it. Stack surfaces (agent menu 17–18,mq b2 stack) documented. release-readinessrebuilt aroundmqlaunch release-check, the stack contract gate, and smoke tests.docs-maintainerno longer routes to the non-existentrepo-product-auditor.
- The HAL command surface (
hal …in the REPL and main menu) no longer runs user input througheval. Requests are tokenized into argv withread -ra, so*,;,|, and$(…)in a request stay literal instead of being expanded or executed. Behavior change: a quoted multi-word argument (hal remember "buy milk") is now split on whitespace rather than kept together by the shell — quote it at the HAL level if grouping is needed.
- Dead
run_git_screeninterminal/launchers/mqlaunch.sh— it had no live caller in the launcher graph (the live copy lives intools/scripts/mqlaunch_desktop.sh). Surfaced while de-layering the git concern (Step 11a); removed as a standalone cleanup rather than folded into a refactor.
- B2 Stack Cockpit (
mq b2 stack) — aggregated dashboard: repo health, B2 prompts, roadmap drift, last run, validation health, Obsidian sync status. - v0.7.0 mq-agent bridge —
mq b2 review-last,--reviewon compose,route --compose --reviewfull pipeline. Thin subprocess bridge to mq-agent. - v0.8.0 review contract —
review_contract.py: capture--jsonfrom mq-agent, B2-style severity rendering, BLOCKER exit gate.--riskflag added. - v0.9.0 repo-signal integration —
mq b2 repo-status,--export,mq b2 roadmap-drift, repo health warning in route pipeline. - 89 passing tests total across b2_tui test suite.
- B2 Atlas Prompt OS TUI — full terminal interface for structured prompt
work (
mq b2). Phases 0–12 complete: CLI commands (list, show, compose, run, route, validate, config, history, export-last, open-last), textual TUI with two-panel browser + preview pane, rule-based task router, Obsidian run writer, JSONL history, 40 passing tests. Accessible via Dev menu →a. - Added
docs/B2_TUI.md— full B2 TUI reference. - Added
mq b2section todocs/COMMANDS.md.
- Polished the Dev menu with clearer groups and safer script fallbacks.
- Bumped version to 0.6.0 — B2 Atlas Prompt OS TUI MVP.
- Added
mqlaunch workflows validatefor workflow command-surface health checks. - Added workflow validation to
mqlaunch release-check. - Added
document-functions.sh --qualityto flag generic function comments. - Added a Document Functions menu entry for comment quality checks.
- Synced README, ROADMAP and release metadata for
0.5.1. - Tightened the Ollama document-review prompt to return fewer, higher-signal comment suggestions without full diffs by default.
- Added mq-agent review, risk-review, architecture, repo-health and mcp-status routing from mqlaunch.
- Added MQ ecosystem repo status, roadmap, skills and diff-summary commands.
- Added command-template-library skill.
- Improved README onboarding with requirements, usage examples, docs map, roadmap context and contribution guidance.
- Updated Git menu AI COMMIT flow so it returns to the Git menu instead of the start menu.
- Preserved mq-agent bridge loading when running mq-mcp review from the Tools menu.
- Fixed protected-branch push handling by routing main/master pushes through PR branches.
- Fixed auto comment flow so existing comments are preserved.
- Fixed mq-agent bridge loading in
run_mq_mcp_review.
.github/workflows/quality.yml— CI shell syntax check:bash -noninstall.sh,release.sh, and all.shfiles;shellcheck(warn-only).scripts/install-smoke.sh— local install smoke test coveringinstall.sh --dry-run,release.shsyntax, all.shbash -n,mqlaunch doctor --json, andmqlaunch selftest.terminal/menus/mq-agent-menu.sh— mq-agent submenu with ten commands across repo analysis, AI, and environment groups.Proofsection in README listing what is verified: dry-run, release validation, JSON health report, selftest, publish gate, HAL read-only commands, and CI syntax coverage.
- README version badge bumped to
0.4.12. - Main menu panel: added
g. Agentquick access slot. mqlaunch.sh: sourcesmq-agent-menu.shwhen present.mq-main-menu.sh: routesg/Gand text aliases (agent,score,signal,audit,doctor) to mq-agent menu.
- Reworked
docs/index.htmlinto a clearer GitHub Pages project front door. - Added a visible Install, Run, Explore flow for first-time users.
- Added a screenshots section covering HAL, main menu, performance, and release workflows.
- Added a docs map linking the case study, HAL page, command reference, and terminal guide.
- Updated the GitHub Pages sitemap for the refreshed front door and HAL page.
- Added Pages index smoke test coverage.
- Added
docs/screenshots/hal-menu.pngas a rendered HAL menu screenshot. - Added the HAL screenshot to
docs/hal.html. - Added a stronger HAL preview card on the GitHub Pages index.
- Added HAL screenshot smoke test coverage.
- Added
docs/hal.htmlas a GitHub Pages overview for the MQLaunch HAL command surface. - Linked the HAL overview from existing Pages docs.
- Added HAL Pages smoke test.
- Linked HAL overview from README and command reference.
- Added
docs/hal-gallery.mdas a visual reference for the MQLaunch HAL menu. - Added
docs/hal-menu-preview.txtwith a plain text menu preview. - Added HAL gallery smoke test.
- Linked HAL gallery docs from README and command reference.
- Added
docs/hal-command-surface.mdfor the full MQLaunch HAL command surface. - Added smoke test for HAL command surface documentation.
- Added HAL menu layout smoke test.
- Added HAL file formatting smoke test.
- Linked HAL command surface docs from README and command reference.
- Added Ollama review as option 7 in the Document Functions menu.
- Added local review-only helper for comments, docstrings, and function descriptions.
- Added documentation for Ollama Document Review.
- Aligned Ollama review default model with installed
qwen3:4b-instructtag.
- Ollama review is review-only and does not modify files automatically.
- Fixed
mq-release-check.shopening ChatGPT browser when called non-interactively (e.g. frommq-hal release-brief). AI prompt calls (mq_ai_prompt_review,mq_ai_prompt_ui) are now guarded with[[ -t 1 ]]— skipped when stdout is not a TTY.
- Fixed
mqlaunch hal release-brief(and anymqlaunch <cmd>typed from inside the menu prompt) routing to AI —dispatch_cli_commandnow handlesarea="mqlaunch"by stripping the prefix and re-dispatching, so typing the fullmqlaunch hal <sub>form inside the menu works the same as typinghal <sub>.
- Restored
mq_hal_run()alias in bridge — mqlaunch callsmq_hal_runbut the function was renamed tomq_hal_mainin a prior refactor, causing allmqlaunch hal <command>calls to route to AI instead of the bridge.
- HAL menu restored to the correct mqlaunch surface pattern:
surface_panel_header,surface_split_row,surface_bottom, andread_main_choice "hal". - Menu now renders identically to other mqlaunch submenus (panel box + pinned prompt).
_hal_pause_enterhelper added for standalone-safe pause.
- HAL menu now uses
print_headerwhen called from within mqlaunch, matching the style of all other submenus. Falls back to a plain standalone header when run directly.
- Rewrote HAL menu as self-contained (
mq-hal-menu.shno longer depends onsurface_*,print_header, orread_main_choice). - Reordered OBSERVE: Audit is now item 2 (between Brief and Release Brief).
- Expanded
tests/hal-menu-smoke.shfrom 6 to 8 checks — now verifies audit and release-brief routes and menu labels.
- Added
mqlaunch hal auditbridge command (publish quality + README score viarepo-signal). - Added Audit as item 8 in HAL menu OBSERVE section (total 16 items).
- Documented HAL Audit in
docs/COMMANDS.mdand README quick-reference.
- Added
mqlaunch hal release-briefbridge command. - Added
release-briefto HAL bridge usage text. - Updated HAL menu OBSERVE section: Release Brief is now item 2; total 15 items.
- Documented HAL Release Brief in
docs/COMMANDS.md. - Added
brief,release-brief,repo-status, andcito README quick-reference.
- Added
mqlaunch hal repo-statusbridge command. - Added
mqlaunch hal cibridge command. - Updated HAL menu OBSERVE section: added Repo Status (2) and CI Status (3); items renumbered to 14.
- Documented HAL Repo Status and CI Status in
docs/COMMANDS.md.
- Added
mqlaunch hal briefbridge command. - Rewrote HAL bridge (
hal-bridge.sh) withmq_hal_mainentry point, robust help text, and cleaner subcommand routing. - Rewrote HAL menu (
mq-hal-menu.sh) as a standalone grouped menu (Observe, Plan, Memory, Debug) with box-drawn prompts; nosurface_*dependency. - Updated smoke test (
tests/hal-menu-smoke.sh) to 5 checks including brief coverage. - Added HAL Brief section to
docs/COMMANDS.md.
- Added interactive
mqlaunch halmenu (terminal/menus/mq-hal-menu.sh). - Added HAL menu smoke test (
tests/hal-menu-smoke.sh). - Documented HAL menu in README and
docs/COMMANDS.md.
- Added
mqlaunch hal timelinebridge command. - Documented HAL Timeline UI in README and command reference.
- Added
mqlaunch hal session,mqlaunch hal last,mqlaunch hal remember, andmqlaunch hal memory-pathbridge commands. - Added HAL Session Memory section to README and command reference.
- Added
mqlaunch hal fix-doctorbridge command for HAL Fix Planner. - Documented HAL Fix Planner in README and command reference.
- Added
mqlaunch hal doctor— delegates tomq-hal doctor-summaryfor local doctor JSON summaries and safe next-step recommendations. - Added HAL Doctor Summary docs to README and
docs/COMMANDS.md.
- Added
mqlaunch halbridge — local Ollama-powered safe command router via mq-hal. - Added
hal)route indispatch_cli_commandandrun_arg_command. - Added
terminal/bridges/hal-bridge.shwithmq_hal_run(). - Added HAL bridge docs to README and
docs/COMMANDS.md.
- Fixed
mqlaunch halrouting —dispatch_cli_commandcatch-all was interceptinghalbeforerun_arg_commandcould handle it. - Removed stale
halalias fromapps|hal|guide-aicase; old terminal guide still reachable viaguide-ai.
- Fixed
mqlaunch doctor --jsonarg passthrough —dispatch_cli_commandwas callingdoctor.shwithout forwarding flags, silently dropping--jsonand returning ANSI output instead of JSON. - Fixed
doctor --jsonsummary counts — subshell$(...)calls lost counter updates; replaced with in-process string accumulation. - Fixed
read_menu_choiceprompt rendering —vared(ZSH ZLE) was clearing below-cursor content on init, erasing bottom separator and hint. Replaced with plainread. - Fixed
read-only variable: statusZSH error in release menu — renamed conflicting locals tofiles_status/exit_code. - Fixed mq-help-menu.sh function name collisions — guarded standalone
print_header/print_footer/rowetc. so they only activate outside mqlaunch context. - Fixed
xandexitnot working as back/quit in all 13 submenus.
- Added
doctor --jsonfull output:project,version,status,checks[],summary{}per spec. - Added
docs/COMMANDS.md— complete command reference for all mqlaunch commands, menus, env vars, and exit shortcuts. - Added
x/exitas back shortcut in all submenu prompts. - Added prompt hint text:
>> option, mqlaunch command, shell command, or x to exit. - Added VERIFY section to Tools menu (doctor, doctor --json, selftest, smoke test).
- Fixed Document Functions submenu prompt missing separator lines — pre-draws full separator block before input using cursor repositioning, matching all other menu prompts.
- Fixed subprocess menus (git, release, themes, shortcuts, login) converted to in-process sourced modules — eliminates exiting mqlaunch on return.
- Removed stale Git Menu option from dev menu and renumbered options 10–14.
- Added submenu prompt separator template to
.claude/templates/for future reference. - Updated
tools/scripts/README.mdwith status table entries for brew-check, port-scan, focus, env-snap, and cleanup scripts.
- Added
mq-repo-signal-check.sh— wrapper that runsrepo-signal publish-checklist . --fail-under 14as a release gate. - Added repo-signal check to
mq-release-check.sh— blocks release if publish checklist score is below threshold. - Added option 12 (Repo Signal Check) to
mqlaunchrelease menu. - Added
MQ_REPO_SIGNAL_FAIL_UNDERenv var to configure publish checklist threshold without hardcoding. - Added
ROADMAP.md,.github/ISSUE_TEMPLATE/bug_report.md, and security note to README somacos-scriptsscores 16/16.
- update shell scripts
- add: auto-update wiki Command-Reference on release
- add: wiki Command-Reference generator
- update shell scripts
- update shell scripts
- update shell scripts
- fix: separator lines match panel width and use white color
- fix: match prompt separator width to actual panel width
- refactor: extract themes menu to separate script
- fix: themes menu header matches tools menu — dashboard then panel
- fix: use clear_screen instead of print_header in themes menu
- fix: remove duplicate host row and add themes label in themes menu prompt
- fix: consistent surface style for themes menu, rename option 7
- fix: remove redundant push in auto_release (release.sh already pushes)
- feat: add Auto comment option to Document Functions menu
- update project files
- update project files
- update project files
- update shell scripts
- update project files
- update project files
- update shell scripts
- update project files
- update project files
- update shell scripts
- update project files
- update project files
- update project files
- update project files
- Improve demo mode — pause_enter between steps, add AI and release commands
- Fix all 16 markdownlint errors in README.md
- Update README — add AI/Atlas/nickname/release sections, fix lint
- gitlaunch: replace 9. EXIT with b. BACK
- Git menu: remove option 9 (git log), b/9 both map to Back
- Auto-generate changelog section from git commits before dry run
- update documentation
- Prompt to open CHANGELOG before dry run if version section is missing
- Add nickname support — shown in header, set via mqlaunch nickname-set
- Intercept atlas at entry point before dispatch reaches ai-mode.sh
- Fix atlas routing — source ai-prompts directly in mqlaunch.sh
- Add Atlas REPL — interactive AI session with mq atlas
- Add git push --tags between live release and GitHub release in auto_release
- Add Auto Release (option 11) to release menu
- Fix ask usage hint to English on main menu
- Add mqlaunch chat — conversational AI mode with memory
- Show ask usage hint on main menu start page
- Add AI section to command index and help text
- Use read_main_choice in help menu for consistent prompt style
- Add pause_enter after ask in REPL so answer stays visible
- Fix ask routing in REPL — zsh no-split bug
- Fix ask.sh truncating multiline responses
- update shell scripts
- update shell scripts
- Fix cmd variable name in unknown command fallback
- update project files
- Route unknown commands to /ask in command-mode layer
- Improve mqlaunch ask UX and fallback
- update shell scripts
- update shell scripts
- update shell scripts
- update shell scripts
- update shell scripts
- update shell scripts
- update shell scripts
- update shell scripts
- update shell scripts
- update shell scripts
- update shell scripts
- update shell scripts
- update shell scripts
- Slash commands
/doctor,/scan,/atlas,/release-checkwired into main menu dispatch pause_enterafter slash command output so menu does not redraw before user reads result- gitlaunch prompt with separator lines and direct 1-9 key input (no arrow keys)
- Missing helper functions (
print_section,pass,fail,warn) in release check script
- Slash command dispatch no longer falls through to shell runner on recognized commands
/asknow passes question text through tomq_ai_prompt_ask- Prompt separator width pinned to same value used when drawing the box above it
- Added breathing room (blank rows) around Command Surface figures
release-checkdisplayed as/release-checkin main menu for consistencycheck_changelog_matches_commitsnow runs before "Status: release-check complete" line
/ui,/doctorand other slash commands doing nothing when typed in main menu/atlasrouting tosafe_run_ai atlasinstead of missingatlas.shmq_ai_prompt_reviewandmq_ai_prompt_uifalling through to shell on missing helper
- AI skill prompt commands (
/review,/ui,/ask) in mqlaunch main menu - Release check command (
/release-check) integrated into mqlaunch - Repo question prompt command (
/ask) - Doctor checks improved and integrated into release check
- Document release check workflow
- Show new commands in main menu panel
- Add changelog template initialization for new releases
- Improve release workflow validation
- Refine release script behavior for smoother version preparation
- Prevent release attempts from proceeding without a matching changelog entry
- Fix release dry-run flow
- Update shell scripts and wrapper behavior
- Refine release process for consistent versioning
- Clarify README structure
- Improve repo presentation
- Strengthen quick start and demo flow
- Improve release script with dry-run support and rollback handling
- Enhance release workflow safety and validation
- Initial release automation script (
tools/release.sh)
- Improve version badge handling in README
- Refine release preparation workflow
- About and status dashboard in
mqlaunch - Release notes and changelog command
- Align README, help, command index, and menu labels
- Base structure for macos-scripts
- Initial mqlaunch functionality
- Core terminal workflows