the status dot was on the wrong axis

a commander watching its folder perfectly showed AMBER, titled "Armed, not
currently running", because green was keyed on `running` — which is an
agent TURN in progress, not the loop. so the healthy steady state, which is
what a commander is in for weeks at a time, was painted as the failure
colour and the owner reasonably concluded nothing was watching.

the failure amber was designed for could not happen either: watcherArmed is
computed from the live pid, so a dead loop turns it off rather than leaving
the pair mismatched.

the server now reports `watcherRunning` separately — the loop pid is alive,
right now — which is what makes the distinction available at all. green is
watching, amber is a row that says armed while its loop is gone, grey is a
commander that never watched.

and the flow: after arming, do not wait for the backlog. the pipeline was
proved on one item, so the rest working is expected rather than news — say
it is armed and move to the next question. a failure in the backlog is
worth saying; a success is not. then ask what comes next, because a watcher
that quietly does one job is a complete answer and the conversation should
offer it rather than assume it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-25 06:23:44 +00:00
co-authored by Claude Opus 5
parent ee1cd006af
commit f4e983693e
5 changed files with 41 additions and 12 deletions
+14 -2
View File
@@ -351,8 +351,12 @@ export function commanderClaudeMd(params: CommanderClaudeMdParams): string {
'broken on its first day.',
'',
'Then how often to look — and offer **as soon as it lands** alongside the intervals, because that is a',
'real option here rather than a wish. Categories and dispatching come later still, one at a time, and',
'most commanders never need either.',
'real option here rather than a wish.',
'',
'Once it is armed, the conversation is not over: ask what should happen next. Filing what it produces',
'into categories, doing something with a result beyond keeping it, or nothing at all — leaving it as a',
'watcher that quietly does one job is a complete answer and often the right one. Ask, one at a time,',
'rather than deciding for them that there is nothing more to discuss.',
'',
'**Never arm a watcher before the owner has agreed what it does.** Arming ahead of that is arming a',
'guess, and a wrong condition either never fires or fires constantly — either way it is found out days',
@@ -367,6 +371,14 @@ export function commanderClaudeMd(params: CommanderClaudeMdParams): string {
'knows how to handle items it has not seen — that is the whole of what a watcher is — so handing it the',
'backlog costs nothing and hands the owner a running commander in the time one item takes.',
'',
'**Then keep talking. Do not wait for the backlog to finish.** You proved the pipeline on one item, so',
'the rest working is expected rather than news — say it is armed and move straight to the next',
'question. No sleeping, no polling a log, no "I will confirm once it is done". The owner does not need a',
'progress report on work they already know succeeds.',
'',
'If something DOES go wrong in the backlog, that is worth saying — in the conversation if it is still',
'open, or as a notification if it is not. A failure is news. A success is not.',
'',
'Once the job is settled, write it into this file, replacing this section: what you watch for, what you',
'do about it, and which numbers were guesses. The next session should read the answer, not the',
'questions.',
+4
View File
@@ -274,6 +274,9 @@ commandHqRouter.get('/commanders', async (ctx) => {
// on `dirName`, not the transcript id: the pid file lives in this commander's OWN `state/`, not
// beside the transcript.
watcherArmed: row.watcherArmed || liveLoopPid(who.email, row.dirName) !== null,
// Whether the LOOP is alive right now, separate from `running` (an agent turn in progress) and from
// `watcherArmed` (which is also true for a row that says armed but whose loop has gone).
watcherRunning: liveLoopPid(who.email, row.dirName) !== null,
watcherArmedAt: row.watcherArmedAt,
watcherStoppedAt: row.watcherStoppedAt,
watcherLastFiredAt: lastFiredByRow.get(row.id) ?? null,
@@ -457,6 +460,7 @@ commandHqRouter.get('/commanders/:sessionId', async (ctx) => {
tagIds: tagsByRow.get(row.id) ?? [],
running,
watcherArmed: row.watcherArmed || liveLoopPid(who.email, row.dirName) !== null,
watcherRunning: liveLoopPid(who.email, row.dirName) !== null,
watcherArmedAt: row.watcherArmedAt,
watcherStoppedAt: row.watcherStoppedAt,
watcherLastFiredAt: lastFiredByRow.get(row.id) ?? null,
@@ -608,8 +608,10 @@ function MetaBar({ sessionId, title, isConnected, isGenerating, onDisconnect }:
</span>
{commander.watcherArmed ? (
<span className="flex items-center gap-1">
<span className={`h-1.5 w-1.5 rounded-full ${commander.running ? 'bg-success' : 'bg-warning'}`} />
watcher armed
<span
className={`h-1.5 w-1.5 rounded-full ${commander.watcherRunning ? 'bg-success' : 'bg-warning'}`}
/>
{commander.watcherRunning ? 'watching' : 'armed, loop not running'}
<button
type="button"
onClick={() => disarmWatcher.mutate()}
@@ -15,14 +15,21 @@ import { useTranslation } from '../../locales';
// `docs/navigation-audit.md`.
/**
* Three states, not two — the pair `COMMS/command-hq/mobile-4.md` proposed and the design record adopted:
* green running, amber armed-but-not (the failure `watcherArmed && !running` exists to make visible), grey
* neither. `running` alone cannot tell "never meant to watch" from "was watching and stopped"; `watcherArmed`
* alone cannot tell "briefly between fire and re-arm" (normal) from "silently dead". Only the pair says it.
* Three states, on the right axis.
*
* Green is the LOOP being alive — a commander watching its folder, which is the whole point of one and its
* normal condition for weeks at a time. Amber is a row that says armed while the loop is gone: the genuine
* failure, and the only thing worth a warning colour. Grey is a commander that was never watching.
*
* This used to key green on `running`, which is an agent TURN in progress. So a perfectly healthy watcher
* with nobody chatting to it showed amber, titled "Armed, not currently running", and the owner reasonably
* concluded nothing was running. The failure amber was designed for could not happen either: `watcherArmed`
* is computed from the live pid, so a dead loop turns it off rather than leaving the pair mismatched. The
* server reports `watcherRunning` separately now, which is what makes the distinction available at all.
*/
function StatusDot({ running, watcherArmed }: { running: boolean; watcherArmed: boolean }) {
const color = running ? 'bg-success' : watcherArmed ? 'bg-warning' : 'bg-muted-foreground/40';
const title = running ? 'Running' : watcherArmed ? 'Armed, not currently running' : 'Not running';
function StatusDot({ watcherRunning, watcherArmed }: { watcherRunning: boolean; watcherArmed: boolean }) {
const color = watcherRunning ? 'bg-success' : watcherArmed ? 'bg-warning' : 'bg-muted-foreground/40';
const title = watcherRunning ? 'Watching' : watcherArmed ? 'Armed, but its loop is not running' : 'Not watching';
return <span className={`inline-block h-2 w-2 shrink-0 rounded-full ${color}`} title={title} />;
}
@@ -63,7 +70,7 @@ const Card = ({ commander, to, selected, onDeleted }: CardProps) => {
<Link to={to} className="absolute inset-0" aria-label={commander.title} />
<div className="flex items-center gap-2 pr-6">
<StatusDot running={commander.running} watcherArmed={commander.watcherArmed} />
<StatusDot watcherRunning={commander.watcherRunning === true} watcherArmed={commander.watcherArmed} />
<span className="truncate text-sm font-medium">{commander.title}</span>
</div>
<div className="flex items-center justify-between text-xs text-muted-foreground">
@@ -41,6 +41,8 @@ export type CommandHqCommander = {
tagIds: number[];
running: boolean;
watcherArmed: boolean;
/** The loop process is alive right now. Distinct from `running`, which is an agent TURN. */
watcherRunning?: boolean;
watcherArmedAt: string | null;
watcherStoppedAt: string | null;
watcherLastFiredAt: string | null;
@@ -58,6 +60,8 @@ export type CommandHqCommanderDetail = {
tagIds: number[];
running: boolean;
watcherArmed: boolean;
/** The loop process is alive right now. Distinct from `running`, which is an agent TURN. */
watcherRunning?: boolean;
watcherArmedAt: string | null;
watcherStoppedAt: string | null;
watcherLastFiredAt: string | null;