fix(agent-team): board gate vs inert semantics + verify against live data
isGate() now keys off topology node kind 'gate' (clarify) or the 'gate' stage, not stage.gated — which actually means INERT (disabled P3 nodes build/verify/ dispatch). Inert columns get a dimmed + 'inert' treatment instead of a false 'human gate' lock. Verified against the live R720 dashboard (10 stages) via a dev-proxy smoke test; typecheck + 36 tests + build all green.
This commit is contained in:
parent
9b4bef1f4c
commit
bf5d259dfd
1 changed files with 17 additions and 3 deletions
|
|
@ -74,9 +74,13 @@ function deriveStages(tasks: TaskView[], topology: Topology | null): Stage[] {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check if a stage is a human gate (stage.gated OR topology node kind === 'gate'). */
|
/**
|
||||||
|
* A human-decision gate — distinct from `stage.gated`, which means the stage is
|
||||||
|
* INERT (a disabled/not-yet-wired P3 node like build/verify/dispatch). A gate is
|
||||||
|
* the "gate" stage or a topology node of kind "gate" (e.g. clarify).
|
||||||
|
*/
|
||||||
function isGate(stage: Stage, topology: Topology | null): boolean {
|
function isGate(stage: Stage, topology: Topology | null): boolean {
|
||||||
if (stage.gated) return true;
|
if (stage.key === "gate") return true;
|
||||||
const topoNode = topology?.nodes.find((n) => n.id === stage.key);
|
const topoNode = topology?.nodes.find((n) => n.id === stage.key);
|
||||||
return topoNode?.kind === "gate";
|
return topoNode?.kind === "gate";
|
||||||
}
|
}
|
||||||
|
|
@ -219,17 +223,27 @@ interface ColumnProps {
|
||||||
|
|
||||||
function KanbanColumn({ stage, topology, selectedId, onSelect }: ColumnProps) {
|
function KanbanColumn({ stage, topology, selectedId, onSelect }: ColumnProps) {
|
||||||
const gate = isGate(stage, topology);
|
const gate = isGate(stage, topology);
|
||||||
|
// `gated` here means INERT (disabled P3 node), not a human gate — dim it.
|
||||||
|
const inert = stage.gated;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col gap-1.5 w-52 shrink-0 rounded-lg bg-muted/40 border border-border p-2",
|
"flex flex-col gap-1.5 w-52 shrink-0 rounded-lg bg-muted/40 border border-border p-2",
|
||||||
|
inert && "opacity-60",
|
||||||
)}
|
)}
|
||||||
aria-label={`Stage: ${stage.label}`}
|
aria-label={`Stage: ${stage.label}`}
|
||||||
>
|
>
|
||||||
{/* Column header */}
|
{/* Column header */}
|
||||||
<div className="flex items-center justify-between gap-1 mb-0.5">
|
<div className="flex items-center justify-between gap-1 mb-0.5">
|
||||||
<span className="text-xs font-semibold text-foreground truncate">{stage.label}</span>
|
<span className="flex items-center gap-1 truncate text-xs font-semibold text-foreground">
|
||||||
|
{stage.label}
|
||||||
|
{inert && (
|
||||||
|
<span className="text-[9px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
|
inert
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
<Badge variant="outline" className="px-1.5 py-0 text-xs">
|
<Badge variant="outline" className="px-1.5 py-0 text-xs">
|
||||||
{stage.count}
|
{stage.count}
|
||||||
|
|
|
||||||
Reference in a new issue