diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3664068..763daab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,7 +13,7 @@ date, grouped by `Added` / `Changed` / `Fixed` / `Removed`.
### Added
-- **A redesigned dashboard that gets you back to work faster.** The dashboard now opens with a **You** zone — *Focus today* (your assigned work, priority-first) and *Pick up where you left off* (your most recently touched issues) — followed by a **Workspace & agents** zone (agent activity, attention, handoffs, stalled, pipeline). The "Suggestions" strip (unassigned + stalled) moved out of your personal area down into the workspace zone, where it belongs.
+- **A redesigned dashboard cockpit — less scrolling, no gaps.** The dashboard is now a two-column cockpit: a wide work column on the left (*Focus today*, *Pick up where you left off*, and a compact **Pipeline** card) beside an always-visible rail on the right (agent activity, attention, standup, what's new) — instead of one long stacked column. The "Suggestions" strip (unassigned + stalled) stays in the work column, below Pipeline.
- **Richer issue cards on the dashboard.** Focus and Pick-up cards now show who's on it (assignee + agent avatars with a presence dot), sub-issue progress, a live agent-run status (running / waiting / stalled), project / label / due / SLA context, and a one-line description when the title is short — each shown only when it applies. Cards size to their own content, so the empty gaps between sparse cards are gone.
- **Set a per-issue SLA target — and let the Coach react to breaches.** Each issue's detail rail now has an **SLA target** field: pick a preset (1 hour, 4 hours, 1 day, 1 week…) or a custom number of minutes, or clear it. When **Enforce per-issue SLA** is on (Settings → Workspace → Agent SLA), an issue that ages past its target raises an SLA breach — which posts a Coach diagnostic comment and shows in your activity feed. Issues with no target are never breached, so turning enforcement on is safe.
- **See your Coach agent's health at a glance.** Settings → Workspace → AI now shows whether the Coach is *armed*, whether it can reach a model, whether the Coach agent exists, which events trigger it (issue stalled / missed ack / SLA breach — and which are turned off), and when it last fired. No more guessing why it is or isn't commenting.
@@ -24,6 +24,7 @@ date, grouped by `Added` / `Changed` / `Fixed` / `Removed`.
- **Cap self-service workspace creation.** Operators can set `MAX_WORKSPACES_PER_USER` to limit how many workspaces a single (non-admin) user can create; the default is unlimited, so nothing changes unless you set it.
- **Approve or reject a paused agent run right where you see it.** When an agent pauses for permission (a Codex/Hermes run flagged a command or file change), the run row in Mission Control's Live tab now shows **Approve** / **Reject** inline — no detour to the Command Center.
- **See a plan's wall-clock burn.** The plan cockpit's budget meter now shows elapsed time vs. the wall-time cap alongside cost, so a plan approaching its time budget is visible at a glance (matching the goal cockpit).
+- **Set the engagement mode right when you assign an agent in Quick Create.** Type `/assign @handle` in the new-issue overlay and a small Execute / Research / Review / Discuss picker appears next to the assignee chip — pick one to override the workspace's default for that dispatch, or leave it alone to keep using the default.
### Changed
@@ -43,6 +44,7 @@ date, grouped by `Added` / `Changed` / `Fixed` / `Removed`.
- **Approving or stopping an agent run tells you if it didn't go through.** A failed Approve or Stop now surfaces an error instead of showing success while the run stays blocked at the runtime.
- **Goals and Plans tell a load error apart from an empty list.** A network hiccup now shows "couldn't load … Retry" instead of "No goals yet" / "Goal not found — may have been abandoned", which looked like your work had been deleted.
- **A freshly registered runtime is no longer a dead-end.** You can open a just-registered local daemon's settings — run its self-test, add credentials, bind a repo — straight from the global **Runtimes** page, without waiting for an agent to use it first.
+- **Research and Review dispatches can actually read the repo.** A Hermes runtime with no per-mode tool profile configured used to hand a Research or Review run zero local tools — even though the mode's own contract promises "read, search, run read-only tools." It now gets the read-oriented subset (filesystem + git, never terminal) of whatever the runtime declared, matching what was already documented.
### Changed
diff --git a/DEVLOG.md b/DEVLOG.md
index 381243a..f51f038 100644
--- a/DEVLOG.md
+++ b/DEVLOG.md
@@ -2,6 +2,72 @@
> Append-only session log. Read at session start. Update at session end.
+## 2026-07-07 — Dashboard Cockpit layout, RESEARCH/REVIEW tool-access default, Quick Create mode picker
+
+Three independent pieces from one session, all typecheck + lint clean.
+
+- **Dashboard: Cockpit layout (variant A of a 3-mockup comparison).** Replaced
+ the single stacked `max-w-6xl` column with a two-column cockpit at
+ `max-w-[100rem]`: primary work column (`lg:col-span-8` — Focus today, Pick up
+ where you left off, a new **Pipeline** card, Suggestions) beside an always-
+ visible rail (`lg:col-span-4` — the existing customizable widget stack,
+ forced to a single column). `DashboardStack` gained a `columns?: 1 | 2` prop
+ (default 2, back-compat — it's only used on this one page) that drops the
+ `lg:grid-cols-2` track and hides the half/full resize affordances when `1`,
+ since they'd have no visible effect in a forced single column. The old
+ "By status" link-list became `PipelineCard` — the same `statusRows`/
+ `statusMax` data as horizontal proportional bars in a card instead of a
+ 2-column list, matching the mockup and removing the ragged-bottom gap next
+ to Focus/Pick-up. Default widget order in the rail now leads with
+ agent-activity/agent-attention/standup/whats-new/quick-notes (existing
+ per-user saved order still overrides via `orderWidgets()` — no regression
+ for anyone who already customized). Verified end-to-end against the local
+ `dev:local` stack with real seeded + assigned issues (3-col Focus grid,
+ populated agent-activity rail, real Standup counts) via a scripted
+ Playwright login — not just typecheck.
+- **RESEARCH/REVIEW tool-access default (`src/lib/runtime-tools.ts`).** Traced
+ a Hermes RESEARCH-mode run's "no filesystem/git/terminal, web search not
+ configured" complaint to two separate root causes: web search isn't a Forge
+ concept at all (not in `RUNTIME_TOOL_CAPABILITIES`, docs explicitly say it
+ stays available regardless of mode — that's a Hermes-gateway-config
+ question, not a Forge bug); but `runtimeModeToolCapabilities()` returned
+ `[]` for **any** non-EXECUTE mode with no explicit `modeToolProfiles`
+ override, contradicting `docs/agents/engagement-modes.md`'s own promise
+ ("Research: Read, search, run read-only tools"). Fixed: RESEARCH/REVIEW now
+ default to the read-oriented subset (`filesystem`, `git` — never
+ `terminal`) of whatever the runtime actually declared; EXECUTE and an
+ explicit `modeToolProfiles` override are unchanged; DISCUSS still gets
+ nothing. The Runtime settings page's per-mode checkbox matrix
+ (`settings/runtimes/page.tsx`) reads this function directly, so operators
+ now see the corrected default there too. New `tests/unit/runtime-tools.test.ts`
+ (7 cases). `modeToolPolicyEnforced` still defaults off — this only fixes
+ what's *advertised*/prompted; hard host enforcement stays opt-in per Runtime.
+- **Quick Create: engagement mode picker on `/assign`.** The new-issue global
+ overlay already assigned agents via `/assign @handle`, but had no way to set
+ the run's engagement mode — the server hardcoded `explicit: null`, so it
+ always fell back to the workspace's assignment default. `SlashCommand`'s
+ `assign` variant gained an optional `mode?: EngagementMode` (UI-only — never
+ parsed from typed text, matching the existing "mode is set via UI, not
+ slash-syntax" split with the @-mention grammar). `quick-create.tsx` renders a
+ compact 4-button mode picker (reusing `EngagementModeGlyph`/`MODE_ORDER` from
+ the issue-detail `AgentPickerModal`) right next to the committed assign
+ badge, unset by default (no button pre-highlighted — honest about "no
+ override yet" rather than guessing the resolved default). `issue.ts`'s
+ `applySlashCommandsToIssue` "assign" case now passes `cmd.mode ?? null` as
+ `explicit` to `resolveEngagementMode()` instead of the hardcoded `null`. No
+ Prisma migration — `Issue` never had an `engagementMode` column by design
+ (mode lives on the `AgentRun`); this just stops discarding an already-storable
+ value. Verified via a scripted Playwright run: typed `/assign victor`,
+ confirmed the picker appears unselected, clicked "Research," confirmed
+ `aria-checked` flips correctly and only on that button.
+
+Housekeeping: this worktree's `node_modules` was missing `server-only`,
+failing 21 unrelated `tests/unit/*.test.ts` files (none touched by this
+session) with `Cannot find module 'server-only'`; `pnpm install` didn't
+resolve it. Pre-existing environmental gap, not a regression — all tests in
+files actually touched here pass (`runtime-tools`, `slash-commands`,
+`chat-slash-command-gating` — 36/36).
+
## 2026-07-06 — Agent-runtime audit: Phase 3 UI consistency (P3.2)
Orchestration-surface polish on `worktree-audit-fixes`. Six sub-items, all
diff --git a/src/app/(app)/w/[slug]/dashboard/page.tsx b/src/app/(app)/w/[slug]/dashboard/page.tsx
index d2889b9..3d79b8b 100644
--- a/src/app/(app)/w/[slug]/dashboard/page.tsx
+++ b/src/app/(app)/w/[slug]/dashboard/page.tsx
@@ -149,10 +149,11 @@ export default function DashboardPage() {
[setPrefsMut],
);
- // Zone-2 (Workspace & agents) widget stack. Agents lead the default
- // order; the rest stays user-reorderable/hideable via Customize. The
- // "Pick up where you left off" tile is gone — it's now a first-class
- // Zone-1 section rendered as rich cards.
+ // Rail widget stack (the ambient "workspace & agents" column beside your
+ // work). Agents/standup/news lead the default order since they're the
+ // highest-signal glance items; the rest stays user-reorderable/hideable
+ // via Customize. The "Pick up where you left off" tile is gone — it's
+ // now a first-class primary-column section rendered as rich cards.
const widgets: DashboardWidget[] = useMemo(
() => [
{
@@ -167,6 +168,21 @@ export default function DashboardPage() {
defaultWidth: "half",
node: ,
},
+ { id: "standup", title: "Standup", defaultWidth: "half", node: },
+ {
+ id: "whats-new",
+ title: "What's new",
+ defaultWidth: "half",
+ node: (
+
+ ),
+ },
+ {
+ id: "quick-notes",
+ title: "Notes & journal",
+ defaultWidth: "full",
+ node: ,
+ },
{
id: "today",
title: "Today",
@@ -174,28 +190,13 @@ export default function DashboardPage() {
node: ,
},
{ id: "pulse", title: "Pulse", defaultWidth: "half", node: },
+ { id: "ideas", title: "Ideas", defaultWidth: "half", node: },
{
id: "workspace-activity",
title: "Workspace activity",
defaultWidth: "full",
node: ,
},
- { id: "standup", title: "Standup", defaultWidth: "half", node: },
- { id: "ideas", title: "Ideas", defaultWidth: "half", node: },
- {
- id: "quick-notes",
- title: "Notes & journal",
- defaultWidth: "full",
- node: ,
- },
- {
- id: "whats-new",
- title: "What's new",
- defaultWidth: "half",
- node: (
-
- ),
- },
],
[slug, workspaceKey, account?.changelogSeenAt],
);
@@ -258,7 +259,7 @@ export default function DashboardPage() {
{/* Ambient background now lives once in the app shell
(.forge-page-bg, driven by the per-user data-bg pref). */}
-
+
- {/* ── Zone 1 · YOU ─────────────────────────────────────────
- Focus (assigned, priority-first) + Pick-up (recent). Both
- render rich auto-height cards. If there's no personal work at
- all, Suggestions takes the slot as the primary handoff. */}
- {focusCards.length === 0 && resumeCards.length === 0 && !myWorkLoading ? (
-
- ) : (
-
- {(focusCards.length > 0 || myWorkLoading) && (
-
-
-
-
- )}
- {resumeCards.length > 0 && (
-
-
-
-
+ {/* ── Cockpit body: a wide work column + a compact ambient rail.
+ Primary (8/12) is "what should I do" — Focus, Pick-up,
+ Pipeline. The rail (4/12) is "what's going on around me" —
+ agents, standup, news — always visible beside your work
+ instead of scrolled past below it. */}
+
+
+ {/* Focus (assigned, priority-first) + Pick-up (recent). Both
+ render rich auto-height cards. If there's no personal work
+ at all, Suggestions takes the slot as the primary handoff. */}
+ {focusCards.length === 0 && resumeCards.length === 0 && !myWorkLoading ? (
+
+ ) : (
+ <>
+ {(focusCards.length > 0 || myWorkLoading) && (
+
+
+
+
+ )}
+ {resumeCards.length > 0 && (
+
+
+
+
+ )}
+ >
)}
-
- )}
-
- {/* ── Zone 2 · WORKSPACE & AGENTS ──────────────────────────
- Agents, attention, and the rest of the customizable stack;
- handoffs & stalled (the old under-Focus Suggestions, demoted
- here); and a by-status pipeline. */}
-
-
-
+
-
+
+
-
-
-
- {statusRows.map(({ status, count }) => (
-
-
-
- {status.name}
-
-
-
-
-
-
- ))}
-
-
+
+
+
+
@@ -491,6 +465,53 @@ function WorkCardGrid({
);
}
+/** Active-work pipeline as proportional bars in a single card — replaces
+ * the old two-column status link-list with something that reads at a
+ * glance and doesn't leave a ragged column bottom next to Focus/Pick-up. */
+function PipelineCard({
+ statusRows,
+ statusMax,
+ href,
+}: {
+ statusRows: { status: { id: string; name: string; color: string }; count: number }[];
+ statusMax: number;
+ href: string;
+}) {
+ if (statusRows.length === 0) return null;
+ return (
+
+
+
+ {statusRows.map(({ status, count }) => (
+
+
+
+ {status.name}
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+}
+
/** Labeled hairline divider that opens a dashboard zone. */
function ZoneDivider({ label }: { label: string }) {
return (
diff --git a/src/components/dashboard/dashboard-stack.tsx b/src/components/dashboard/dashboard-stack.tsx
index b7607ed..0b031c6 100644
--- a/src/components/dashboard/dashboard-stack.tsx
+++ b/src/components/dashboard/dashboard-stack.tsx
@@ -86,11 +86,16 @@ export function DashboardStack({
layout,
editing,
onChange,
+ columns = 2,
}: {
widgets: DashboardWidget[];
layout: DashboardLayout;
editing: boolean;
onChange: (next: DashboardLayout) => void;
+ /** 1 forces a single-column stack (e.g. a narrow rail) — half/full width
+ * then both render at the rail's full width, so the resize control is
+ * hidden rather than offered with no visible effect. */
+ columns?: 1 | 2;
}) {
const ordered = orderWidgets(widgets, layout.order);
const orderedIds = ordered.map((w) => w.id);
@@ -189,13 +194,20 @@ export function DashboardStack({
return (