diff --git a/src/renderer/src/pages/HomePage.tsx b/src/renderer/src/pages/HomePage.tsx
index f77faf34..a99b9692 100644
--- a/src/renderer/src/pages/HomePage.tsx
+++ b/src/renderer/src/pages/HomePage.tsx
@@ -1,3 +1,4 @@
+import { isBusinessTask } from "./work/task-helpers";
import { useMemo, useState, type CSSProperties } from "react";
import { Avatar, Button, Icon, Pill } from "../shared/ui";
import { TopbarActions } from "../shared/TopbarActions";
@@ -26,6 +27,8 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) {
const organization = requireActiveOrganization(snapshot);
const employee = requireActiveEmployee(snapshot);
const tasks = useMemo(() => snapshot.tasks.filter((task) => task.orgId === orgId), [snapshot.tasks, orgId]);
+ // Progress and workload count real work only; run titles still resolve against every task.
+ const businessTasks = useMemo(() => tasks.filter(isBusinessTask), [tasks]);
const agents = useMemo(() => snapshot.agents.filter((agent) => agent.orgId === orgId), [snapshot.agents, orgId]);
const approvals = useMemo(() => snapshot.approvals.filter((item) => item.orgId === orgId), [snapshot.approvals, orgId]);
const goals = useMemo(() => snapshot.goals.filter((item) => item.orgId === orgId), [snapshot.goals, orgId]);
@@ -43,7 +46,7 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) {
const pendingApprovals = approvals.filter((approval) => approval.state === "pending");
const totalBudgetCents = agents.reduce((sum, agent) => sum + agent.budgetCents, 0);
const primaryGoal = goals.find((goal) => goal.status === "active") ?? goals[0] ?? null;
- const goalProgress = computeGoalProgress(primaryGoal, tasks);
+ const goalProgress = computeGoalProgress(primaryGoal, businessTasks);
const firstName = employee.displayName.split(/\s+/)[0] ?? employee.displayName;
const greeting = greetingFor(new Date().getHours());
@@ -68,7 +71,7 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) {
-
+
diff --git a/src/renderer/src/pages/OfficePage.tsx b/src/renderer/src/pages/OfficePage.tsx
index 34cb49cc..19efdecc 100644
--- a/src/renderer/src/pages/OfficePage.tsx
+++ b/src/renderer/src/pages/OfficePage.tsx
@@ -1,3 +1,4 @@
+import { isBusinessTask } from "./work/task-helpers";
import { useEffect, useMemo, useRef, useState, type MouseEvent } from "react";
import type { PageProps } from "./types";
import { activeOrgId, requireActiveEmployee } from "./types";
@@ -39,7 +40,7 @@ export function OfficePage({ snapshot }: PageProps) {
const orgAgents = snapshot.agents
.filter((agent) => agent.orgId === orgId && agent.lifecycle === "active")
.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
- const orgTasks = snapshot.tasks.filter((task) => task.orgId === orgId);
+ const orgTasks = snapshot.tasks.filter((task) => task.orgId === orgId && isBusinessTask(task));
return orgAgents.map((agent) => ({
id: agent.id,
name: agent.name,
diff --git a/src/renderer/src/pages/WorkPage.tsx b/src/renderer/src/pages/WorkPage.tsx
index 459eebb9..a696deb7 100644
--- a/src/renderer/src/pages/WorkPage.tsx
+++ b/src/renderer/src/pages/WorkPage.tsx
@@ -20,7 +20,7 @@ import { TaskDetailDialog } from "./work/TaskDetailDialog";
import { ProjectOverview } from "./work/ProjectOverview";
import { ListView } from "./work/ListView";
import { TimelineView } from "./work/TimelineView";
-import { latestTaskRun, nextStatus, prettyRunState, providerFor, runBlocker, runTone } from "./work/task-helpers";
+import { isBusinessTask, latestTaskRun, nextStatus, prettyRunState, providerFor, runBlocker, runTone } from "./work/task-helpers";
import type { PageProps } from "./types";
import { activeOrgId, type Focus } from "./types";
import { t } from "../i18n/i18n";
@@ -52,7 +52,9 @@ export function WorkPage({ snapshot, actions, navigate, focus }: PageProps) {
const [scope, setScope] = useState
(focus && (focus.kind === "goal" || focus.kind === "project") ? focus : null);
const [actionError, setActionError] = useState(null);
- const allTasks = useMemo(() => snapshot.tasks.filter((task) => task.orgId === orgId), [snapshot.tasks, orgId]);
+ // The board, its counts and its filters show real work; chat replies and goal checkpoints show on Messages, Home and Runs.
+ const orgTasks = useMemo(() => snapshot.tasks.filter((task) => task.orgId === orgId), [snapshot.tasks, orgId]);
+ const allTasks = useMemo(() => orgTasks.filter(isBusinessTask), [orgTasks]);
const tasks = useMemo(
() =>
allTasks.filter((task) => {
@@ -88,7 +90,8 @@ export function WorkPage({ snapshot, actions, navigate, focus }: PageProps) {
setActionError(null);
operation().catch((error: unknown) => setActionError(error instanceof Error ? error.message : String(error)));
};
- const detailTask = detailTaskId ? allTasks.find((task) => task.id === detailTaskId) ?? null : null;
+ // A link (from a run, say) can open an internal task too, though the board does not list it.
+ const detailTask = detailTaskId ? orgTasks.find((task) => task.id === detailTaskId) ?? null : null;
return (
diff --git a/src/renderer/src/pages/work/task-helpers.ts b/src/renderer/src/pages/work/task-helpers.ts
index 6c2a5ca2..1441d774 100644
--- a/src/renderer/src/pages/work/task-helpers.ts
+++ b/src/renderer/src/pages/work/task-helpers.ts
@@ -18,6 +18,11 @@ export const WRITING_OPTIONS: ReadonlyArray<{ value: WritingMode; label: string
];
+/** Real work: not the vehicle of a chat reply or a goal checkpoint, which the system creates for itself. */
+export function isBusinessTask(task: TaskRecord): boolean {
+ return task.origin !== "chat_wake" && task.origin !== "goal_review";
+}
+
export function nextStatus(status: TaskStatus): TaskStatus | null {
if (status === TASK_STATUS.BACKLOG) return TASK_STATUS.IN_PROGRESS;
if (status === TASK_STATUS.IN_PROGRESS) return TASK_STATUS.REVIEW;
diff --git a/src/renderer/src/shared/Shell.tsx b/src/renderer/src/shared/Shell.tsx
index 9b373ffa..4a325099 100644
--- a/src/renderer/src/shared/Shell.tsx
+++ b/src/renderer/src/shared/Shell.tsx
@@ -1,3 +1,4 @@
+import { isBusinessTask } from "../pages/work/task-helpers";
import { unreadBySpace } from "../pages/chat/chat-helpers";
import { useEffect, useState, type FormEvent, type ReactNode } from "react";
import type { AppSnapshot, EmployeeRecord } from "../../../shared/domain";
@@ -219,7 +220,7 @@ function computeBadges(snapshot: AppSnapshot, orgId: string): Partial item.orgId === orgId);
const approvals = snapshot.approvals.filter((item) => item.orgId === orgId);
return {
- work: tasks.filter((task) => task.status !== "done").length,
+ work: tasks.filter((task) => task.status !== "done" && isBusinessTask(task)).length,
chat: employee ? countUnreadMessages(snapshot, orgId, employee.id) : 0,
approvals: approvals.filter((approval) => approval.state === "pending").length,
};
diff --git a/src/shared/domain.ts b/src/shared/domain.ts
index 11bf8210..eb62bc12 100644
--- a/src/shared/domain.ts
+++ b/src/shared/domain.ts
@@ -126,6 +126,8 @@ export interface TaskRecord {
enqueueError: string | null;
/** Why the operator blocked the task; no session starts while it is set. */
blockedReason: string | null;
+ /** Provenance: a chat reply vehicle, a goal checkpoint, a manager's review of work, or null for plain work. */
+ origin: "chat_wake" | "goal_review" | "task_review" | null;
/** Why the open task is not moving now, or null when nothing holds it. */
hold: TaskHold | null;
/** Runs at or below this task_runs rowid no longer count toward the run budget. */
diff --git a/tests/e2e/ceo-chat.spec.ts b/tests/e2e/ceo-chat.spec.ts
index ea93cd31..9665c203 100644
--- a/tests/e2e/ceo-chat.spec.ts
+++ b/tests/e2e/ceo-chat.spec.ts
@@ -74,6 +74,8 @@ test("the pinned CEO conversation is the Messages front door", async () => {
// From a task, its goal, run and back are one click each.
await page.getByRole("button", { name: "Work" }).click();
+ // The CEO's reply to the founder is a chat vehicle, not work: it stays off the board.
+ await expect(page.getByRole("button", { name: /Reply to Yang/ })).toHaveCount(0);
await page.getByRole("button", { name: /Operationalize goal: Ship the FlowPilot MVP\./ }).first().click();
const taskDialog = page.getByRole("dialog", { name: "Operationalize goal: Ship the FlowPilot MVP." });
await taskDialog.getByRole("button", { name: "Open goal Ship the FlowPilot MVP." }).click();
diff --git a/tests/unit/business-tasks.test.ts b/tests/unit/business-tasks.test.ts
new file mode 100644
index 00000000..a1bda70e
--- /dev/null
+++ b/tests/unit/business-tasks.test.ts
@@ -0,0 +1,10 @@
+import { describe, expect, test } from "vitest";
+import { isBusinessTask } from "../../src/renderer/src/pages/work/task-helpers";
+import type { TaskRecord } from "../../src/shared/domain";
+
+describe("what counts as work", () => {
+ test("chat reply vehicles and goal checkpoints are not work; reviews of work and plain tasks are", () => {
+ const task = (origin: TaskRecord["origin"]) => ({ origin }) as TaskRecord;
+ expect([null, "task_review", "chat_wake", "goal_review"].map((origin) => isBusinessTask(task(origin as TaskRecord["origin"])))).toEqual([true, true, false, false]);
+ });
+});