Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 48 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ per-tick; v1.1 routes it to the real concurrency cap)),
`JARVIS_WORK_MAX_RETRIES` (default `1`),
`JARVIS_WORK_RUN_TIMEOUT_MS` (default `300000` — 5 min wall-clock
budget per agent loop pickup),
`JARVIS_REVIEWER_AUTO_ACCEPT` (any non-empty / non-`0` / non-`false`
value opts in to reviewer-subagent dispatch on Review → Done under
`AcceptancePolicy::Subagent`; default off — see "Reviewer
auto-accept" below),
`JARVIS_WORKTREE_MODE` (`off` / `per_run` / `per_unit`; auto mode
upgrades from `off` to `per_run` automatically so the scheduler
never mutates the main checkout),
Expand Down Expand Up @@ -685,11 +689,50 @@ REST surface around triage:
- All `depends_on` entries reach `RequirementStatus::Done` (topo sort)
- No in-flight Pending/Running run for the same requirement
- `failed_count < max_retries`

The four guards are silent skips — the row stays in Backlog until
all conditions clear. Operators see the missing pickups in the
activity timeline (no `RunStarted` row) rather than via an explicit
"blocked" signal.
- v1.0 SubAgent — rows at Review under
`acceptance_policy == AcceptancePolicy::Human` are skipped (the
policy gate keeps them at Review until a person clicks
"complete"; re-running would burn LLM budget without ever
advancing status).

The guards are silent skips — the row stays in Backlog (or Review,
under the human-policy gate) until all conditions clear. Operators
see the missing pickups in the activity timeline (no `RunStarted`
row) rather than via an explicit "blocked" signal.

**Acceptance policy** — `Requirement.acceptance_policy`
(v1.0 SubAgent):
- `Subagent` (default): preserves the pre-v1.0 auto-flip
Review → Done semantics **unless** `JARVIS_REVIEWER_AUTO_ACCEPT`
is set — see below.
- `Human`: keeps the row at Review after a Completed run. The
picker also skips already-at-Review rows under this policy so
the auto loop doesn't burn cycles re-running them. Use it when
the verification plan can't model what "done" means
(UX/visual design, security-sensitive changes, anything subtly
judgment-dependent).

**Reviewer auto-accept** — opt-in via
`JARVIS_REVIEWER_AUTO_ACCEPT=1` (any non-empty / non-`0` /
non-`false` value enables it). When enabled and a Completed run
arrives against a `Subagent`-policy requirement, the auto loop
holds the row at Review and dispatches the
[`subagent.review`](crates/harness-subagents/src/reviewer.rs)
subagent (looked up in `state.tools`). The reviewer's terminal
call to
[`requirement.review_verdict`](crates/harness-tools/src/requirement.rs)
flips the row to Done (`pass`) or InProgress (`fail`) with the
commentary attached so the next pickup can adapt. Two Activity
rows fire around the dispatch:
`{kind:"reviewer_dispatched", run_id}` before, plus
`{kind:"reviewer_dispatch_failed", run_id, error}` if the
subagent tool isn't registered or the invocation errors.

Default off so existing deployments keep the synchronous
auto-flip. Tests live in
[`auto_mode.rs::tests`](crates/harness-server/src/auto_mode.rs)
(`advance_dispatches_reviewer_when_flag_enabled_and_policy_subagent`,
`advance_skips_dispatch_when_flag_disabled`, etc.).

**Roadmap → Work bootstrap** — `POST /v1/roadmap/import`:

Expand Down
90 changes: 90 additions & 0 deletions apps/jarvis-web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ import { SettingsPage } from "./components/Settings/SettingsPage";
import { ProjectsPage } from "./components/Projects/ProjectsPage";
import { DocsPage } from "./components/Docs/DocsPage";
import { WorkOverviewPage } from "./components/Projects/WorkOverview/WorkOverviewPage";
import { AutoModeDashboardPage } from "./components/AutoMode/AutoModeDashboardPage";
import {
ConversationDeepLinkRedirect,
ConversationsArchivePage,
} from "./components/Conversations/ConversationsArchivePage";
import { WorktreesPage } from "./components/Worktrees/WorktreesPage";
import { SubAgentDemoPage } from "./components/SubAgent/SubAgentDemoPage";
import { DesktopStartupOverlay } from "./components/Desktop/DesktopStartupOverlay";
import { useAppStore, appStore } from "./store/appStore";
Expand Down Expand Up @@ -72,6 +78,8 @@ export function App() {
<Routes>
<Route path="/" element={<ChatLayout />} />
<Route path="/projects/overview" element={<WorkOverviewLayout />} />
<Route path="/projects/auto-mode" element={<AutoModeDashboardLayout />} />
<Route path="/projects/worktrees" element={<WorktreesLayout />} />
<Route path="/projects/list" element={<ProjectsLayout />} />
{/* `/projects/:projectId` deep-links into a specific project's
kanban so browser back, bookmarks, and sidebar links all
Expand All @@ -94,6 +102,14 @@ export function App() {
path="/diagnostics"
element={<Navigate to="/projects/overview" replace />}
/>
<Route path="/conversations" element={<ConversationsArchiveLayout />} />
{/* `/conversations/:id` resumes the persisted conversation
and redirects to chat. Useful for bookmarks / shared URLs
that should land back in the right thread. */}
<Route
path="/conversations/:id"
element={<ConversationDeepLinkRedirect />}
/>
<Route path="/settings" element={<SettingsPage />} />
{/* SubAgent UI preview — static prototype with mocked frame
data. Reachable directly only; not linked from nav. Will
Expand Down Expand Up @@ -164,6 +180,80 @@ function DocsLayout() {
);
}

// Conversation history archive — full-page browse over every
// persisted conversation. Same shell as ProjectsLayout so the
// sidebar stays put.
function ConversationsArchiveLayout() {
return (
<>
<a className="skip-link" href="#conversations-archive-page">
Skip to main content
</a>
<div id="app" className="page-app projects-app">
<AppSidebar />
<ConversationsArchivePage />

<div
id="resize-sidebar"
className="resize-handle resize-sidebar"
role="separator"
aria-orientation="vertical"
aria-label="Resize sidebar"
tabIndex={-1}
/>

<QuickSwitcher />
</div>
</>
);
}

// Worktree management — sibling to the Auto-mode dashboard under
// Work mode. Lists orphan git worktrees + cleanup.
function WorktreesLayout() {
return (
<>
<a className="skip-link" href="#worktrees-page">
Skip to main content
</a>
<div id="app" className="page-app projects-app">
<AppSidebar />
<WorktreesPage />

<div
id="resize-sidebar"
className="resize-handle resize-sidebar"
role="separator"
aria-orientation="vertical"
aria-label="Resize sidebar"
tabIndex={-1}
/>

<QuickSwitcher />
</div>
</>
);
}

// Auto-mode scheduler dashboard. Sibling to Overview / List under
// the Work-mode sidebar; same shell so the sidebar nav stays put as
// the user moves between Work sub-pages.
function AutoModeDashboardLayout() {
return (
<>
<a className="skip-link" href="#auto-mode-page">Skip to main content</a>
<div id="app" className="page-app projects-app">
<AppSidebar />
<AutoModeDashboardPage />

<div id="resize-sidebar" className="resize-handle resize-sidebar" role="separator" aria-orientation="vertical" aria-label="Resize sidebar" tabIndex={-1} />

<QuickSwitcher />
</div>
</>
);
}

// v1.0 — full-page WorkOverview, reachable from the Work-mode
// sidebar's "工作总览" link. Same shell as `ProjectsLayout` (same
// `page-app projects-app` class so the sidebar layout matches);
Expand Down
2 changes: 2 additions & 0 deletions apps/jarvis-web/src/components/AppChatPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { ApprovalCard } from "./Approvals/ApprovalCard";
import { BypassBanner } from "./Approvals/BypassBanner";
import { ModeBadge } from "./Approvals/ModeBadge";
import { PlanModeBanner } from "./Approvals/PlanModeBanner";
import { PlanProposedCard } from "./Approvals/PlanProposedCard";
import { ModelMenu } from "./ModelMenu/ModelMenu";
import { UsageBadge } from "./UsageBadge";
Expand Down Expand Up @@ -52,6 +53,7 @@

<Banner />
<BypassBanner />
<PlanModeBanner />

<MessageList />

Expand Down Expand Up @@ -149,7 +151,7 @@
label="Voice input"
onClick={() => {
const SpeechRecognition =
(window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;

Check warning on line 154 in apps/jarvis-web/src/components/AppChatPane.tsx

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type

Check warning on line 154 in apps/jarvis-web/src/components/AppChatPane.tsx

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
if (!SpeechRecognition) {
showBanner("Voice input is not supported by this browser.");
return;
Expand All @@ -158,7 +160,7 @@
recognition.lang = navigator.language || "en-US";
recognition.interimResults = false;
recognition.maxAlternatives = 1;
recognition.onresult = (event: any) => {

Check warning on line 163 in apps/jarvis-web/src/components/AppChatPane.tsx

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
const transcript = event.results?.[0]?.[0]?.transcript;
if (!transcript) return;
const spacer = value.trim().length ? " " : "";
Expand Down
46 changes: 46 additions & 0 deletions apps/jarvis-web/src/components/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,29 @@ function ChatSidebarBody() {
<>
<nav className="nav-list" aria-label={t("sidebarModeChat")}>
<NewConvoButton />
<NavLink
to="/conversations"
className={({ isActive }) =>
"nav-item" + (isActive ? " active" : "")
}
>
<svg
width="17"
height="17"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.9"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<rect x="3" y="3" width="18" height="18" rx="2" />
<path d="M3 9h18" />
<path d="M9 21V9" />
</svg>
<span>{t("sidebarNavConversationsArchive")}</span>
</NavLink>
</nav>
<ConvoList />
</>
Expand Down Expand Up @@ -203,6 +226,29 @@ function WorkSidebarBody() {
</svg>
<span>{t("sidebarNavWorkOverview")}</span>
</NavLink>
<NavLink
to="/projects/auto-mode"
className={({ isActive }) => "nav-item" + (isActive ? " active" : "")}
>
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="9" />
<polyline points="12 7 12 12 15 14" />
</svg>
<span>{t("sidebarNavAutoMode")}</span>
</NavLink>
<NavLink
to="/projects/worktrees"
className={({ isActive }) => "nav-item" + (isActive ? " active" : "")}
>
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M6 3v6a3 3 0 0 0 3 3h6a3 3 0 0 0 3-3V3" />
<circle cx="6" cy="18" r="3" />
<circle cx="18" cy="18" r="3" />
<path d="M12 12v3" />
<circle cx="12" cy="18" r="3" />
</svg>
<span>{t("sidebarNavWorktrees")}</span>
</NavLink>
<NavLink
to="/projects/list"
className={({ isActive }) => "nav-item" + (isActive ? " active" : "")}
Expand Down
52 changes: 52 additions & 0 deletions apps/jarvis-web/src/components/Approvals/PlanModeBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Persistent banner shown above the message list when the per-socket
// permission mode is `plan` AND no plan has been proposed yet. Tells
// the user "you're in plan mode — the agent will draft a plan before
// touching anything; nothing will execute until you accept it." Once
// the agent calls `exit_plan` and a `plan_proposed` frame arrives,
// `PlanProposedCard` takes over the review dock and this banner
// becomes redundant — so we hide it whenever `proposedPlan != null`.
//
// The "Exit plan mode" button switches back to `ask` (the safest
// mode). Same pattern as BypassBanner.

import { useAppStore } from "../../store/appStore";
import { setSocketMode } from "../../services/permissions";
import { t } from "../../utils/i18n";

export function PlanModeBanner() {
const mode = useAppStore((s) => s.permissionMode);
const proposedPlan = useAppStore((s) => s.proposedPlan);
if (mode !== "plan") return null;
// Once a plan arrives, PlanProposedCard provides much richer feedback
// and the banner becomes noise.
if (proposedPlan) return null;
return (
<div className="plan-mode-banner" role="status">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M9 11l3 3L22 4" />
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
</svg>
<span className="plan-mode-banner-text">
{t("permModePlanActiveBanner")}
</span>
<button
type="button"
className="plan-mode-banner-btn"
onClick={() => setSocketMode("ask")}
title={t("permModePlanExitHint")}
>
{t("permModePlanExitBtn")}
</button>
</div>
);
}
Loading
Loading