Hi Kristian,
The short version: channel chat is deliberately literal when you type it directly. Both the channel alias (<alias> message) and a hand-typed @cemit chan=message send the text exactly as written — no u() calls, functions, or %-substitutions are evaluated. That's by design, so that ordinary chat containing brackets, percent signs, or function-looking text appears as-is instead of being surprisingly evaluated (or used to spoof).
The escape hatch is that @cemit does evaluate its message when it's run from softcode rather than typed live at the keyboard. So for your use case — handler objects posting system/debug info to a private channel — it works with no special effort, because those objects are running @cemit from their own code:
&do_report obj=@cemit staff=Load: [u(obj/gather_stats)] at [time()]
$+report:@trigger me/do_report
When do_report fires, the [u(...)] and [time()] are evaluated and the finished line goes out to the channel.
If you want to confirm it for yourself from the command line, this one-liner forces the @cemit through the queue (which is what makes it evaluate):
@force me=@cemit staff=2+2 is [add(2,2)]
You should see 2+2 is 4 on the channel — versus the literal text if you'd typed the same @cemit directly.
So nothing's broken; you just need the @cemit to originate from softcode, which your handler objects already do. Hope that unblocks you — shout if the evaluation isn't behaving the way you expect once you wire it up.
Bests,
Brazil