feat(ui): fold the Workflows page into Dashboard → Analytics → Workflow
The workflow analytics now render under the Analytics tab's Workflow sub-tab; ?tab= and ?sub= deep-link the Dashboard, /workflows redirects there, and the sidebar drops its Workflows item.
This commit is contained in:
+9
-9
@@ -75,11 +75,10 @@ graph TB
|
||||
Layout[Layout Component]
|
||||
|
||||
subgraph "Pages"
|
||||
Home[Dashboard<br/>Monitor / Analytics / Health tabs]
|
||||
Home[Dashboard<br/>Monitor / Analytics (incl. Workflow) / Health tabs]
|
||||
Sessions[Sessions]
|
||||
Detail[SessionDetail]
|
||||
Feed[ActivityFeed]
|
||||
Workflows[Workflows]
|
||||
Settings[Settings]
|
||||
end
|
||||
|
||||
@@ -106,7 +105,7 @@ graph TB
|
||||
end
|
||||
|
||||
Router --> Layout
|
||||
Layout --> Home & Sessions & Detail & Feed & Workflows & Settings
|
||||
Layout --> Home & Sessions & Detail & Feed & Settings
|
||||
Home & Detail --> AgentCard & StatCard & StatusBadge
|
||||
Feed --> EventDetail
|
||||
API --> REST
|
||||
@@ -198,14 +197,14 @@ client/
|
||||
│ │ │ ├── HomeDirSetting.tsx # CLAUDE_HOME / CODEX_HOME editor (env-owned warning)
|
||||
│ │ │ ├── CodexHooksSettings.tsx # Codex → Overview → Codex hooks card
|
||||
│ │ │ └── AntigravityHooksSettings.tsx # Antigravity → Antigravity hooks card
|
||||
│ │ └── workflows/ # Workflows page panels: own-pipeline stats + D3 analytics (11 files)
|
||||
│ │ └── workflows/ # Dashboard → Analytics → Workflow panels: own-pipeline stats + D3 analytics (11 files)
|
||||
│ │
|
||||
│ ├── pages/ # Route pages
|
||||
│ │ ├── Dashboard.tsx # Monitor / Analytics / Health tabs
|
||||
│ │ ├── Sessions.tsx # Filterable table; shows each session's real name (synced live from the transcript), falls back to the short ID
|
||||
│ │ ├── SessionDetail.tsx # Agent tree + event timeline + Conversation tab (slash-command pills & output, inline rename markers)
|
||||
│ │ ├── ActivityFeed.tsx # Real-time event log; row click expands payload; Session btn navigates
|
||||
│ │ ├── Workflows.tsx
|
||||
│ │ ├── Workflows.tsx # Workflow analytics, rendered in Dashboard → Analytics → Workflow
|
||||
│ │ ├── Settings.tsx
|
||||
│ │ ├── AiToolConfig.tsx # Settings → <tool>: the generic tab of every installed, enabled catalog tool (path, Update link, sessions/cost/lanes, default model, vendor pricing, catalog-declared config files; Antigravity adds its hooks card)
|
||||
│ │ └── NotFound.tsx
|
||||
@@ -252,11 +251,12 @@ graph TB
|
||||
Sessions[Sessions<br/>filterable table]
|
||||
Detail[SessionDetail<br/>agent hierarchy + timeline]
|
||||
Feed[ActivityFeed<br/>streaming event log]
|
||||
Workflows[Workflows<br/>Lane pipelines + D3 analytics]
|
||||
Workflows[Workflows<br/>Analytics → Workflow sub-tab:<br/>lane pipelines + D3 analytics]
|
||||
Settings[SettingsHub /settings/:section<br/>General · Appearance · Notifications<br/>Agents: AI tools, Claude Code, Codex, each catalog tool<br/>Routing: AI Router, Pricing · Git: credentials, worktrees<br/>Projects: one page per repo]
|
||||
|
||||
App --> Layout
|
||||
Layout --> Dashboard & Sessions & Detail & Feed & Workflows & Settings
|
||||
Layout --> Dashboard & Sessions & Detail & Feed & Settings
|
||||
Dashboard --> Workflows
|
||||
|
||||
Dashboard --> StatCard[StatCard × 6]
|
||||
Dashboard --> AgentCard[AgentCard × N]
|
||||
@@ -469,7 +469,7 @@ graph TB
|
||||
Sessions["/sessions (tabs: sessions | events)"]
|
||||
Detail["/sessions/:id"]
|
||||
Feed["/activity (redirect)"]
|
||||
Workflows["/workflows"]
|
||||
Workflows["/workflows (redirect → /?tab=analytics&sub=workflow)"]
|
||||
CcConfig["/cc-config (redirect)"]
|
||||
Lanes["/lanes (redirect)"]
|
||||
Run["/run (redirect)"]
|
||||
@@ -500,7 +500,7 @@ function App() {
|
||||
<Route path="sessions/:id" element={<SessionDetail />} />
|
||||
{/* legacy path - the activity feed is now a tab on /sessions */}
|
||||
<Route path="activity" element={<Navigate to="/sessions?view=events" replace />} />
|
||||
<Route path="workflows" element={<Workflows />} />
|
||||
<Route path="workflows" element={<Navigate to="/?tab=analytics&sub=workflow" replace />} />
|
||||
{/* legacy path - Claude config now lives in a Settings tab */}
|
||||
<Route path="cc-config" element={<Navigate to="/settings/agents/claude-code" replace />} />
|
||||
{/* legacy paths - lanes and runs are one page now */}
|
||||
|
||||
+4
-3
@@ -41,7 +41,6 @@
|
||||
* - `./pages/Dashboard`
|
||||
* - `./pages/History`
|
||||
* - `./pages/SessionDetail`
|
||||
* - `./pages/Workflows`
|
||||
* - `./pages/SettingsHub`
|
||||
* - `./pages/Workspace`
|
||||
*
|
||||
@@ -64,7 +63,6 @@ import { GlobalTooltip } from "./components/Tooltip";
|
||||
import { Dashboard } from "./pages/Dashboard";
|
||||
import { History } from "./pages/History";
|
||||
import { SessionDetail } from "./pages/SessionDetail";
|
||||
import { Workflows } from "./pages/Workflows";
|
||||
import { SettingsHub } from "./pages/SettingsHub";
|
||||
import { Workspace } from "./pages/Workspace";
|
||||
import { NotFound } from "./pages/NotFound";
|
||||
@@ -99,7 +97,10 @@ export default function App() {
|
||||
<Route path="sessions" element={<History />} />
|
||||
<Route path="sessions/:id" element={<SessionDetail />} />
|
||||
<Route path="activity" element={<Navigate to="/sessions?view=events" replace />} />
|
||||
<Route path="workflows" element={<Workflows />} />
|
||||
<Route
|
||||
path="workflows"
|
||||
element={<Navigate to="/?tab=analytics&sub=workflow" replace />}
|
||||
/>
|
||||
<Route path="lanes" element={<Navigate to="/workspace" replace />} />
|
||||
<Route
|
||||
path="cc-config"
|
||||
|
||||
@@ -24,8 +24,6 @@ export function DocumentTitle() {
|
||||
title = `${t("sessions")} · ${sessionId.slice(0, 8)}`;
|
||||
} else if (path.startsWith("/sessions")) {
|
||||
title = t("sessions");
|
||||
} else if (path.startsWith("/workflows")) {
|
||||
title = t("workflows");
|
||||
} else if (path.startsWith("/workspace")) {
|
||||
title = t("workspace");
|
||||
} else if (path.startsWith("/settings")) {
|
||||
|
||||
@@ -55,7 +55,6 @@ import {
|
||||
LayoutDashboard,
|
||||
FolderOpen,
|
||||
BarChart3,
|
||||
Workflow,
|
||||
Play,
|
||||
Settings,
|
||||
Wifi,
|
||||
@@ -89,7 +88,6 @@ function isUpdatePayload(x: unknown): x is UpdateStatusPayload {
|
||||
const NAV_KEYS = [
|
||||
{ to: "/", icon: LayoutDashboard, key: "nav:dashboard" },
|
||||
{ to: "/sessions", icon: FolderOpen, key: "nav:sessions" },
|
||||
{ to: "/workflows", icon: Workflow, key: "nav:workflows" },
|
||||
{ to: "/workspace", icon: Play, key: "nav:workspace" },
|
||||
{ to: "/settings", icon: Settings, key: "nav:settings" },
|
||||
] as const;
|
||||
|
||||
@@ -33,7 +33,6 @@ describe("DocumentTitle", () => {
|
||||
it("titles the other top-level routes", () => {
|
||||
expect(titleFor("/")).toMatch(/^Dashboard · /);
|
||||
expect(titleFor("/sessions")).toMatch(/^Sessions · /);
|
||||
expect(titleFor("/workflows")).toMatch(/^Workflows · /);
|
||||
expect(titleFor("/settings")).toMatch(/^Settings · /);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
"upToDate": "Up to date",
|
||||
"updateAvailable": "Update available",
|
||||
"website": "Website",
|
||||
"workflows": "Workflows",
|
||||
"workspace": "Workspace",
|
||||
"wsEndpoint": "WebSocket",
|
||||
"releaseAvailable": "{{version}} available",
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
"upToDate": "Đã là mới nhất",
|
||||
"updateAvailable": "Có bản cập nhật",
|
||||
"website": "Website",
|
||||
"workflows": "Quy trình",
|
||||
"workspace": "Không gian làm việc",
|
||||
"wsEndpoint": "WebSocket",
|
||||
"releaseAvailable": "Có {{version}}",
|
||||
|
||||
@@ -63,7 +63,8 @@ import {
|
||||
useRef,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { Workflows } from "./Workflows";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
@@ -732,9 +733,14 @@ function AnalyticsTab() {
|
||||
const [costData, setCostData] = useState<CostResult | null>(null);
|
||||
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
|
||||
const [loading, setLoading] = useState(true);
|
||||
// `?sub=` deep-links a sub-tab (the old /workflows route lands on `workflow`).
|
||||
const [params] = useSearchParams();
|
||||
const [analyticsSubTab, setAnalyticsSubTab] = useState<
|
||||
"tokens" | "cost" | "workflow" | "productivity"
|
||||
>("cost");
|
||||
>(() => {
|
||||
const sub = params.get("sub");
|
||||
return sub === "tokens" || sub === "workflow" || sub === "productivity" ? sub : "cost";
|
||||
});
|
||||
// Global data scope; a change re-runs `load` (api injects the `sources` param).
|
||||
const [scope] = useDataScope();
|
||||
|
||||
@@ -1340,81 +1346,89 @@ function AnalyticsTab() {
|
||||
)}
|
||||
|
||||
{analyticsSubTab === "workflow" && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Agent type distribution */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">
|
||||
{t("subagentTypes")}
|
||||
</h3>
|
||||
{(data?.agent_types ?? []).length === 0 ? (
|
||||
<p className="text-sm text-fg-muted">{t("noSubagentData")}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{(data?.agent_types ?? []).slice(0, 10).map(({ subagent_type, count }) => (
|
||||
<BarRow
|
||||
key={subagent_type}
|
||||
label={subagent_type}
|
||||
count={count}
|
||||
max={maxAgentTypeCount}
|
||||
color="bg-violet-400"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Agent status donut */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("agentStatus")}</h3>
|
||||
<DonutChart segments={agentStatusSegments} />
|
||||
<div className="mt-4 pt-4 border-t border-border space-y-1.5">
|
||||
<div className="flex justify-between text-xs text-fg-muted">
|
||||
<span>{t("totalAgentsLabel")}</span>
|
||||
<Tip raw={(data?.overview.total_agents ?? 0).toLocaleString()}>
|
||||
<span className="text-fg-secondary font-mono">
|
||||
{fmt(data?.overview.total_agents ?? 0)}
|
||||
</span>
|
||||
</Tip>
|
||||
</div>
|
||||
{agentStatusSegments.map((s) => (
|
||||
<div
|
||||
key={s.label}
|
||||
className="flex items-center justify-between text-xs text-fg-muted"
|
||||
>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="w-2 h-2 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: s.color }}
|
||||
<div className="space-y-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Agent type distribution */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">
|
||||
{t("subagentTypes")}
|
||||
</h3>
|
||||
{(data?.agent_types ?? []).length === 0 ? (
|
||||
<p className="text-sm text-fg-muted">{t("noSubagentData")}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{(data?.agent_types ?? []).slice(0, 10).map(({ subagent_type, count }) => (
|
||||
<BarRow
|
||||
key={subagent_type}
|
||||
label={subagent_type}
|
||||
count={count}
|
||||
max={maxAgentTypeCount}
|
||||
color="bg-violet-400"
|
||||
/>
|
||||
{s.label}
|
||||
</span>
|
||||
<Tip raw={s.value.toLocaleString()}>
|
||||
<span className="text-fg-secondary font-mono">{fmt(s.value)}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Agent status donut */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">
|
||||
{t("agentStatus")}
|
||||
</h3>
|
||||
<DonutChart segments={agentStatusSegments} />
|
||||
<div className="mt-4 pt-4 border-t border-border space-y-1.5">
|
||||
<div className="flex justify-between text-xs text-fg-muted">
|
||||
<span>{t("totalAgentsLabel")}</span>
|
||||
<Tip raw={(data?.overview.total_agents ?? 0).toLocaleString()}>
|
||||
<span className="text-fg-secondary font-mono">
|
||||
{fmt(data?.overview.total_agents ?? 0)}
|
||||
</span>
|
||||
</Tip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Event type breakdown */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("eventTypes")}</h3>
|
||||
{(data?.event_types ?? []).length === 0 ? (
|
||||
<p className="text-sm text-fg-muted">{t("noEventData")}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{(data?.event_types ?? []).map(({ event_type, count }) => (
|
||||
<BarRow
|
||||
key={event_type}
|
||||
label={event_type}
|
||||
count={count}
|
||||
max={maxEventTypeCount}
|
||||
color={EVENT_TYPE_COLORS[event_type] ?? "bg-surface-4"}
|
||||
/>
|
||||
{agentStatusSegments.map((s) => (
|
||||
<div
|
||||
key={s.label}
|
||||
className="flex items-center justify-between text-xs text-fg-muted"
|
||||
>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="w-2 h-2 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: s.color }}
|
||||
/>
|
||||
{s.label}
|
||||
</span>
|
||||
<Tip raw={s.value.toLocaleString()}>
|
||||
<span className="text-fg-secondary font-mono">{fmt(s.value)}</span>
|
||||
</Tip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Event type breakdown */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">
|
||||
{t("eventTypes")}
|
||||
</h3>
|
||||
{(data?.event_types ?? []).length === 0 ? (
|
||||
<p className="text-sm text-fg-muted">{t("noEventData")}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{(data?.event_types ?? []).map(({ event_type, count }) => (
|
||||
<BarRow
|
||||
key={event_type}
|
||||
label={event_type}
|
||||
count={count}
|
||||
max={maxEventTypeCount}
|
||||
color={EVENT_TYPE_COLORS[event_type] ?? "bg-surface-4"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* The former Workflows page, folded into this sub-tab. */}
|
||||
<Workflows />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2346,7 +2360,10 @@ export function Dashboard() {
|
||||
const { t } = useTranslation("dashboard");
|
||||
|
||||
// Persistent Tab State
|
||||
const [tabParams] = useSearchParams();
|
||||
const [activeTab, setActiveTab] = useState<"monitor" | "analytics" | "health">(() => {
|
||||
const tab = tabParams.get("tab");
|
||||
if (tab === "monitor" || tab === "analytics" || tab === "health") return tab;
|
||||
return (
|
||||
(localStorage.getItem("dashboard_tab") as "monitor" | "analytics" | "health") || "analytics"
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* @file Workflows.tsx
|
||||
* @description Workflows page. Opens on this dashboard's OWN pipelines — per-node
|
||||
* @description The workflow analytics, shown inside Dashboard → Analytics → Workflow (the
|
||||
* old `/workflows` route redirects there). Opens on this dashboard's OWN pipelines — per-node
|
||||
* pass rates across every lane, and which skills/plugins actually get used — then
|
||||
* the Claude Code orchestration analytics: headline stats, Workflow-tool runs, the
|
||||
* spawning DAG, error propagation, session complexity, compaction impact and the
|
||||
@@ -494,7 +495,7 @@ function PageHeader({
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg font-semibold text-fg-primary">{t("title")}</h1>
|
||||
<h2 className="text-lg font-semibold text-fg-primary">{t("title")}</h2>
|
||||
{wsConnected ? (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot" />
|
||||
|
||||
@@ -9390,11 +9390,11 @@ exports[`screen snapshots > Workflows 1`] = `
|
||||
<div
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<h1
|
||||
<h2
|
||||
class="text-lg font-semibold text-fg-primary"
|
||||
>
|
||||
Workflows
|
||||
</h1>
|
||||
</h2>
|
||||
<span
|
||||
class="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full"
|
||||
>
|
||||
|
||||
@@ -559,6 +559,17 @@ describe("screen snapshots", () => {
|
||||
it("Dashboard", async () => {
|
||||
await snapshot(<Dashboard />, "/");
|
||||
});
|
||||
it("Dashboard deep link opens the Workflow sub-tab with the workflow analytics", async () => {
|
||||
localStorage.removeItem("dashboard_tab");
|
||||
const { container } = render(
|
||||
<MemoryRouter initialEntries={["/?tab=analytics&sub=workflow"]}>
|
||||
<Dashboard />
|
||||
</MemoryRouter>
|
||||
);
|
||||
await settle();
|
||||
const headings = [...container.querySelectorAll("h2")].map((h) => h.textContent);
|
||||
expect(headings).toContain(i18n.t("workflows:title"));
|
||||
});
|
||||
it("Sessions", async () => {
|
||||
await snapshot(<Sessions />, "/sessions");
|
||||
});
|
||||
|
||||
+1
-1
@@ -2120,7 +2120,7 @@ DELETE /api/run/:id Kill (SIGTERM → SIGKILL after 5 s)
|
||||
|
||||
**PTY streaming:** Frames from the tmux pane are streamed to the client over `/ws-pty/:runId` as binary WebSocket frames (not JSON). The Workspace page's TerminalView component feeds these frames to xterm.js for live rendering. Simultaneously, `agenthub lanes shell` can attach the same session via a real local terminal, staying in sync with the browser view.
|
||||
|
||||
Every history row carries `lane_id` (the workspace whose run it belongs to) and `tool` (the CLI that was invoked: `"claude"` or `"codex"`). `GET /api/run/history?laneId=<n>` returns only that workspace's runs. Spawned Claude processes fire the dashboard's hooks like any other CLI session, so they show up in `/api/sessions`, the Dashboard's Analytics tab, and the Workflows page automatically. Codex runs do not emit these hooks; they are recorded only in run history and `dashboard_runs`.
|
||||
Every history row carries `lane_id` (the workspace whose run it belongs to) and `tool` (the CLI that was invoked: `"claude"` or `"codex"`). `GET /api/run/history?laneId=<n>` returns only that workspace's runs. Spawned Claude processes fire the dashboard's hooks like any other CLI session, so they show up in `/api/sessions`, and the Dashboard's Analytics tab (Workflow sub-tab included) automatically. Codex runs do not emit these hooks; they are recorded only in run history and `dashboard_runs`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+2
-2
@@ -815,7 +815,7 @@ under `agenthub feature activate`: the backwards declaration prunes the records
|
||||
ahead of it, so the previous pass leaves nothing behind to count, while a
|
||||
feature switch archives them first. Neither ever counted *passes* — one workspace row
|
||||
contributes at most one record per node, however many times it ran — so this
|
||||
narrows the history, it does not skew a rate. The Workflows page renders this as one bar per node (see
|
||||
narrows the history, it does not skew a rate. Dashboard → Analytics → Workflow renders this as one bar per node (see
|
||||
`client/src/components/workflows/LanePipelinePanel.tsx`).
|
||||
|
||||
**A new run is opened by a DECLARATION, never by a detection alone.** `runs` is
|
||||
@@ -1042,7 +1042,7 @@ Create a JSON file in your custom pipeline directory (e.g., `~/.agenthub/pipelin
|
||||
- **label** (string, optional): display name in the UI; defaults to `id` if omitted
|
||||
- **icon** (string, optional): unicode emoji or single character shown before the stage name
|
||||
- **gate** (boolean, optional): `true` if this stage blocks unless explicitly passed; defaults to `false`
|
||||
- **command** (string, optional): the one command that drives THIS stage — a slash command (`"/my-training-skill"`) or a shell one (`"agenthub lanes hook ci-gate"`). Where `skill` above names the driver for the whole pipeline, this narrows it to the step, so a reader of the pipeline map sees what moves each node instead of inferring it from the node's `detect` regexes. Omit it (or leave it empty) for a node with no single driver; use `"agenthub stage <id>"` for a node whose only "work" is the declaration itself (a terminal `done`, a gate another agent declares). Surfaced in the node's tooltip on a workspace card and as copyable text per node on the Workflows page, and run by `agenthub lanes node <id>` (below); **never executed by the dashboard**, same contract as `skill`.
|
||||
- **command** (string, optional): the one command that drives THIS stage — a slash command (`"/my-training-skill"`) or a shell one (`"agenthub lanes hook ci-gate"`). Where `skill` above names the driver for the whole pipeline, this narrows it to the step, so a reader of the pipeline map sees what moves each node instead of inferring it from the node's `detect` regexes. Omit it (or leave it empty) for a node with no single driver; use `"agenthub stage <id>"` for a node whose only "work" is the declaration itself (a terminal `done`, a gate another agent declares). Surfaced in the node's tooltip on a workspace card and as copyable text per node in Dashboard → Analytics → Workflow, and run by `agenthub lanes node <id>` (below); **never executed by the dashboard**, same contract as `skill`.
|
||||
- **aliases** (array, optional): alternative names for the stage (e.g., `["training", "fit"]` lets you call `agenthub stage training`). An alias may also name a **sub-state** — *why* the workspace sits on a node — without adding a node to the map. Two constraints: no alias may collide with another node's id or alias (`phaseIdx` takes the FIRST match, so a duplicate silently resolves a declaration onto the wrong node), and all aliases of one node share one record slot (`stageRecords` resolves each declared key onto its node, and the canonical id's record wins), so a sub-state that needs its own `--evidence` has to be a real node.
|
||||
- **detect** (array, optional): stage-detection rules for this node — see "Stage detection → Where the rules live".
|
||||
|
||||
|
||||
Reference in New Issue
Block a user