diff --git a/BUILD-LOG.md b/BUILD-LOG.md
index 76fb7772..1c70815e 100644
--- a/BUILD-LOG.md
+++ b/BUILD-LOG.md
@@ -2040,3 +2040,26 @@ packages/mosaic 69/69. Live check at 14:27Z on the real board: orch-01
(assistant + toolCall) working, rev-code-01 (tool result last) working,
velma (text-only, stop) waiting. Sonnet review: APPROVED, no findings.
No seat changed state because of this change; it closes a gap, not a bug.
+
+## 2026-09-12 — Control board step 3, Gate A fix: task, project, workspace per row (#1503)
+
+Before: a row said who and what state, but not what the seat was asked to
+do or where it was working. Jason asked (through the professor session)
+for task, project and workspace per row, with "unknown" instead of a guess.
+
+After: readSession records the first user message (task); tmuxInspect
+reads `#{pane_current_command}\t#{pane_current_path}` and returns
+{ alive, workspace }; findRepoRoot walks up from the workspace to the
+nearest `.git` directory or worktree file (activeProject). The grouping
+column and seen.json keys are unchanged. Page: Task and Active project
+columns in all three tables, Task, Active project and Workspace in the
+detail row, every value through esc(), nulls shown as "unknown". Tests:
+six fixture tests (task from blocks, task from a string, no task, workspace
+tmux/cwd/none, activeProject dir/file/none, record on disk) plus parsePanes
+and two tmuxInspect tests and one page test. Suites: control-board 79/79,
+packages/mosaic 69/69. Live check at 14:51Z on a scratch server (port
+7332): 42 rows, all three fields derived on every row, 16 workspaces from
+a live pane, the rest from the log; every fleet seat reads `.mosaic` as
+its active project because `~/.mosaic` is a git checkout and the roster
+names no project. Missing launcher signals are listed in the plan page
+Step 3 log. Sonnet review: APPROVED, no findings.
diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md
index 907aebea..d0efbf10 100644
--- a/docs/SESSIONS.md
+++ b/docs/SESSIONS.md
@@ -219,3 +219,4 @@ are never rewritten or removed; corrections are new entries.
- 2026-09-12 — jarvis (Claude Code, coordinator) — Control board step 3, third refinement (#1503): Jason suggested a per-project "Hide seen" checkbox beside "Hide offline"; added, on by default, with a combined hidden-count note. Page-only change plus one static test (65/65); sonnet review APPROVED (one test-rigor FYI fixed); live check in Chrome.
- 2026-09-12 — jarvis (Claude Code, coordinator) — Control board step 3, fourth refinement (#1503): Jason asked for "N of N" in the project header; now "shown of total" while a checkbox hides rows, plain total otherwise. Page-only change plus one static test (66/66); sonnet review APPROVED (no findings); live check in Chrome at all four edges.
- 2026-09-12 — jarvis (Claude Code, coordinator) — Control board step 3, mid-tool-call rule (#1503), asked by the professor session on Jason's behalf: scanner already honoured the rule via stopReason; added a content-level toolCall check, 3 fixture tests + 3 table cases (69/69); live check on orch-01/rev-code-01 (working) and velma (waiting); sonnet review APPROVED (no findings; thinking-block nuance already recorded).
+- 2026-09-12 — jarvis (Claude Code, coordinator) — Control board step 3, Gate A fix (#1503), asked by the professor session on Jason's behalf: per-row Task (first user message), Active project (nearest git checkout above the workspace) and Workspace (live tmux pane path, else session cwd), "unknown" when not derivable; missing launcher signals logged on the plan page. Fixture test per field (79/79); live check on a scratch server; sonnet review APPROVED (no findings).
diff --git a/docs/plans/2026-09-12_control-board-mvp.md b/docs/plans/2026-09-12_control-board-mvp.md
index 1b15a244..b9ba82e1 100644
--- a/docs/plans/2026-09-12_control-board-mvp.md
+++ b/docs/plans/2026-09-12_control-board-mvp.md
@@ -50,6 +50,25 @@ Acceptance:
### Step 3 log
+**2026-09-12 — Gate A fix: task, active project and workspace per row.**
+Asked by the professor session on Jason's behalf. Each row now shows
+Task (the session's first user message), Active project (basename of the
+nearest git checkout above the workspace) and, in the detail row,
+Workspace (the live `#{pane_current_path}` of the pane running `pi`, else
+the session log's `cwd`). Anything that cannot be derived shows the word
+"unknown". Missing signals, for launchers: (1) pi session logs carry no
+task envelope entry, so Task is the free-text first user message, which
+for fleet seats is a fleet-comms envelope and for hand-started seats is
+whatever was typed ("resume", "who are you?"); a launcher that wants a
+clean task label should emit one as the first line of the first message
+or write a per-seat status file the scanner can read. (2) Neither
+`roster.yaml` nor a seat's `profile.json` names a project, and every fleet
+seat runs in `~/.mosaic`, which is itself a git checkout, so Active
+project reads `.mosaic` for all of them; a `project` field in the roster
+would fix that. (3) Workspace is only live while a `pi` pane exists; for
+offline seats it is history from the log. Fixture test per field, a
+tmux-inspect parse test, and a page test (79/79). Sonnet review: APPROVED, no findings.
+
**2026-09-12 — liveness follows the tmux pane, not just the session.**
Jason found agents marked "waiting" that were actually dead: their tmux
session still existed but no longer ran `pi` (it had exited to a shell or
diff --git a/packages/control-board/README.md b/packages/control-board/README.md
index 1399f04c..cb72c305 100644
--- a/packages/control-board/README.md
+++ b/packages/control-board/README.md
@@ -95,6 +95,23 @@ only when Jason clicks "Seen" or "Unsee" on the page (via `POST
the agent's newest message still has that exact `lastActivity` timestamp —
as soon as the agent writes anything new, `lastActivity` changes, the mark
no longer matches, and the row falls back into "Waiting on you" on its own.
+Every row also shows three "what and where" fields, each derived from the
+session log and tmux or shown as the word "unknown", never guessed:
+
+- **Task** — the session's first user message (collapsed to one line, 240
+ characters). pi logs carry no task envelope, so this is the only
+ assignment signal available. For fleet seats it is usually the
+ fleet-comms envelope the seat was started with.
+- **Active project** — the basename of the nearest git checkout (a `.git`
+ directory or worktree file) above the workspace. Fleet seats that run in
+ `~/.mosaic` therefore show `.mosaic`. The grouping column "Project" is
+ unchanged: it still comes from where the seat's logs live, and `seen.json`
+ keys depend on it.
+- **Workspace** (detail row, and the hover title of Active project) — the
+ current path of the tmux pane running `pi`; when the seat is offline or
+ tmux could not be asked, the `cwd` from the session log. The record says
+ which one it used (`workspaceSource`: `tmux-pane` or `session-cwd`).
+
Marked rows are listed under a collapsed "Seen (N)" section on the page,
each with an "Unsee" button, so nothing marked is ever out of reach. Each
project table has "Hide offline" and "Hide seen" checkboxes (both on by
@@ -118,6 +135,11 @@ values), the scan refuses rather than silently dropping every mark.
"sessionFile": "/mnt/storage/src/mosaic-stack/.pi/state/darkwing/sessions/2026-09-12.jsonl",
"sessionId": "01a06e48-0718-71f2-a889-c263c4800fb9",
"cwd": "/mnt/storage/src/mosaic-stack",
+ "task": "Read agents/darkwing/work/RESTART.md",
+ "taskSource": "first-user-message",
+ "workspace": "/mnt/storage/src/mosaic-stack",
+ "workspaceSource": "tmux-pane",
+ "activeProject": "mosaic-stack",
"lastActivity": "2026-09-12T15:04:33.000Z",
"ageSeconds": 42,
"lastAssistantText": "Ready for the next step whenever you are.",
diff --git a/packages/control-board/src/cli.mjs b/packages/control-board/src/cli.mjs
index f871a9ee..23c3a8b8 100755
--- a/packages/control-board/src/cli.mjs
+++ b/packages/control-board/src/cli.mjs
@@ -5,7 +5,7 @@
// Exit 0 on success, 2 on a config refusal or bad usage.
import { join } from "node:path";
import { homedir } from "node:os";
-import { loadConfig, defaultConfigPath, discoverRepoAgents, discoverFleetAgents, scan, tmuxIsAlive, ConfigError } from "./scan.mjs";
+import { loadConfig, defaultConfigPath, discoverRepoAgents, discoverFleetAgents, scan, tmuxInspect, ConfigError } from "./scan.mjs";
import { startServer } from "./serve.mjs";
const USAGE = "usage: mosaic-board scan|serve [--config PATH] [--repo PATH] [--fleet PATH|none] [--liveness tmux|assume-alive] [--print] [--port N] [--host 127.0.0.1]";
@@ -38,7 +38,7 @@ async function main() {
const opts = parseArgs(process.argv.slice(2));
const { dataRoot } = loadConfig(opts.config);
const specs = [...discoverRepoAgents(opts.repo), ...(opts.fleet === "none" ? [] : discoverFleetAgents(opts.fleet))];
- const isAlive = opts.liveness === "tmux" ? tmuxIsAlive : () => true;
+ const isAlive = opts.liveness === "tmux" ? tmuxInspect : () => true;
const boardDir = join(dataRoot, "board");
if (opts.command === "serve") {
const server = await startServer({ host: opts.host, port: opts.port, specs, boardDir, isAlive });
diff --git a/packages/control-board/src/page.html b/packages/control-board/src/page.html
index e2d54404..620b689d 100644
--- a/packages/control-board/src/page.html
+++ b/packages/control-board/src/page.html
@@ -63,6 +63,8 @@
.seen-tag{font-size:.72rem;color:var(--muted);margin-left:6px;vertical-align:middle}
.msg-error{color:var(--danger)}
.msg-text,.msg-error{display:block;max-width:36ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+ .task-text{display:block;max-width:28ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+ .unknown{color:var(--muted);font-style:italic}
.detail-row td{background:var(--raised)}
.detail-list{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;margin:0;font-size:.85rem;font-family:var(--mono)}
.detail-list dt{color:var(--muted);font-family:var(--font)}
@@ -191,15 +193,24 @@
? '' + esc(rec.lastError) + ""
: '' + esc(rec.lastAssistantText || "—") + "";
var projectCell = showProject ? "
" + esc(rec.project) + "
" : "";
+ // Gate A fields: derived from the log and tmux, or "unknown". Never guessed.
+ var task = rec.task
+ ? '' + esc(rec.task) + ""
+ : 'unknown';
+ var activeProject = rec.activeProject
+ ? '' + esc(rec.activeProject) + ""
+ : 'unknown';
var main =
'