diff --git a/CLAUDE.md b/CLAUDE.md index 342da1b..39b1f58 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,17 +35,48 @@ apps/ # settings page). crates/ + harness-channel/ # Channel value types (OutboundMessage, SendOutcome, + # ChannelInstance, ChannelBinding, inbound primitives) + + # ChannelDispatcher trait + ChannelInstanceStore / + # ChannelBindingStore traits. Shared vocabulary between + # harness-tools (the channel.send tool), harness-store + # (persistence impls), and harness-server (adapter + # registry + HTTP routes). harness-core does NOT + # depend on it. + harness-project/ # Project domain — Project / Requirement / + # RequirementRun / Activity / Comment / Label / + # DocProject / DocDraft / ProjectMemory value types + + # their 8 *Store traits. The "Work" feature + # (kanban + audit timeline + comments + labels). + # Extracted from harness-core so the latter stays + # agent-loop-only. Depends on harness-core for the + # single shared LLM type (`Usage` on RequirementRun). + # Used by harness-tools, harness-store, harness-server. harness-core/ # Agent, Conversation, Message, Tool, LlmProvider, Memory, Approver traits + run loop harness-llm/ # LlmProvider impls: OpenAI, Anthropic, Google, Codex (ChatGPT OAuth) harness-mcp/ # MCP bridge (rmcp): McpClient adapts remote tools into Tool; # McpServer exposes a local ToolRegistry over stdio harness-memory/ # Memory impls: SlidingWindowMemory + SummarizingMemory - harness-server/ # Axum router + `serve(addr, AppState)` helper + harness-observability/ # Local quality / telemetry value types and + # persistence traits — Eval* (suite runs / grader + # verdicts / baselines) + Observed* (run / span + # facts / metrics / dashboards) + ObservabilityStore + # / EvalStore. Extracted from harness-core to + # decouple "is the agent doing well?" concerns + # from the agent loop. True leaf — no harness-* + # deps. + harness-server/ # Axum router + `serve(addr, AppState)` helper. + # Owns the ChannelAdapter trait + per-kind impls + # (wecom_webhook / feishu_bot / dingtalk_bot / + # wecom_app) and the inbound callback routes. harness-store/ # ConversationStore / ProjectStore / TodoStore; # JSON-file + in-memory by default, SQLite / - # Postgres / MySQL behind opt-in cargo features + # Postgres / MySQL behind opt-in cargo features. + # Also implements the channel store traits from + # harness-channel. harness-tools/ # Built-in `Tool` impls: echo, time.now, http.fetch, - # fs.{read,list,write,edit}, code.grep, shell.exec + # fs.{read,list,write,edit}, code.grep, shell.exec, + # channel.send (talks to harness-channel::ChannelDispatcher) ``` `Cargo.toml` at the root is a workspace manifest with shared `[workspace.dependencies]`; @@ -172,6 +203,20 @@ budget per agent loop pickup), value opts in to reviewer-subagent dispatch on Review → Done under `AcceptancePolicy::Subagent`; default off — see "Reviewer auto-accept" below), +`JARVIS_SUBAGENT_CLAUDE_CODE_BIN` (default `claude` — path / name +of the Claude Code CLI used by `subagent.claude_code`), +`JARVIS_SUBAGENT_CLAUDE_CODE_MODEL` (default unset — forwarded as +`--model `; when absent the CLI picks whatever +`claude /model` configured), +`JARVIS_SUBAGENT_CLAUDE_CODE_ARGS` (default unset — whitespace-split +extra args forwarded verbatim to the `claude` binary, inserted +after `--model` and before the task positional. Use for +`--bare` in keychain-less environments, etc.), +`JARVIS_SUBAGENT_CODEX_MODEL` / `JARVIS_SUBAGENT_READER_MODEL` / +`JARVIS_SUBAGENT_REVIEWER_MODEL` (each defaults to unset — +overrides the model for the named subagent's internal agent loop), +`JARVIS_SUBAGENT_MAX_CONCURRENCY` (default `3` — parallel-dispatch +cap for `subagent.batch`), `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), @@ -723,24 +768,40 @@ row) rather than via an explicit "blocked" signal. **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 +arrives against a `Subagent`-policy requirement, the auto picker +re-picks the row at Review and runs the main agent again via +[`drive_one`](crates/harness-server/src/auto_mode.rs); the system +prompt directs the agent to delegate verification to the [`subagent.review`](crates/harness-subagents/src/reviewer.rs) -subagent (looked up in `state.tools`). The reviewer's terminal -call to +subagent. 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 +commentary attached so the next pickup can adapt. + +Default off so completed work pauses at Review for a human +acceptance click — that's the v1.0 safe default. 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.). +under the `reviewer_flag`-prefixed names (e.g. +`tick_does_not_re_pick_review_subagent_after_completed_run_under_reviewer_flag`, +`tick_still_picks_review_row_with_no_completed_history_under_reviewer_flag`, +`tick_skips_review_with_completed_history_when_reviewer_flag_off`). + +**Manual reviewer trigger** — `POST /v1/requirements/:id/review`: + +Operator-initiated dispatch for the same flow, useful when +`JARVIS_REVIEWER_AUTO_ACCEPT` is off (the default) or when the +auto picker has skipped the row (e.g. +`review_completed_awaiting_acceptance` guard fired) and a human +wants to ask Jarvis to verify on demand. Validates +`status == Review`, `acceptance_policy == Subagent`, and that +`subagent.review` is registered in `state.tools`. Spawns the run +via [`auto_mode::spawn_background_run`](crates/harness-server/src/auto_mode.rs) +and returns `202 Accepted` with `{dispatched: true, requirement_id}`; +the new run id is discoverable through +`GET /v1/requirements/:id/runs`. Writes a +`{kind:"reviewer_dispatched_manually"}` Comment Activity for the +audit trail. 409s when the row is at the wrong status or under +Human policy; 503 when `subagent.review` isn't registered. **Roadmap → Work bootstrap** — `POST /v1/roadmap/import`: diff --git a/Cargo.lock b/Cargo.lock index 0c5daaf..5b4891b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -500,6 +511,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -648,6 +668,15 @@ dependencies = [ "toml 0.9.12+spec-1.1.0", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.60" @@ -717,6 +746,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.6.1" @@ -1962,6 +2001,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "harness-channel" +version = "0.1.0" +dependencies = [ + "async-trait", + "chrono", + "serde", + "serde_json", + "uuid", +] + [[package]] name = "harness-cloud" version = "0.1.0" @@ -1991,6 +2041,7 @@ dependencies = [ "futures", "serde", "serde_json", + "sha2", "thiserror 1.0.69", "tokio", "tracing", @@ -2047,11 +2098,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "harness-observability" +version = "0.1.0" +dependencies = [ + "async-trait", + "serde", + "serde_json", +] + [[package]] name = "harness-plugin" version = "0.1.0" dependencies = [ "chrono", + "harness-channel", "harness-core", "harness-mcp", "harness-skill", @@ -2063,6 +2124,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "harness-project" +version = "0.1.0" +dependencies = [ + "async-trait", + "chrono", + "harness-core", + "serde", + "serde_json", + "tokio", + "uuid", +] + [[package]] name = "harness-requirement" version = "0.1.0" @@ -2070,6 +2144,7 @@ dependencies = [ "async-trait", "chrono", "harness-core", + "harness-project", "harness-store", "serde", "serde_json", @@ -2084,26 +2159,36 @@ dependencies = [ name = "harness-server" version = "0.1.0" dependencies = [ + "aes", "async-stream", "async-trait", "axum", + "base64 0.22.1", + "cbc", "chrono", "futures", + "harness-channel", "harness-cloud", "harness-core", "harness-llm", "harness-mcp", + "harness-observability", "harness-plugin", + "harness-project", "harness-requirement", "harness-skill", "harness-store", "harness-tools", + "hmac", "include_dir", "portable-pty", + "rand 0.8.6", "reqwest 0.12.28", "serde", "serde_json", "serde_yaml", + "sha1", + "sha2", "tempfile", "thiserror 1.0.69", "tokio", @@ -2135,7 +2220,10 @@ version = "0.1.0" dependencies = [ "async-trait", "chrono", + "harness-channel", "harness-core", + "harness-observability", + "harness-project", "serde", "serde_json", "sqlx", @@ -2166,7 +2254,10 @@ dependencies = [ "async-trait", "chrono", "diffy", + "harness-channel", "harness-core", + "harness-observability", + "harness-project", "harness-requirement", "harness-store", "ignore", @@ -2608,6 +2699,16 @@ dependencies = [ "cfb", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -2674,11 +2775,14 @@ dependencies = [ "chrono", "clap", "dialoguer", + "harness-channel", "harness-core", "harness-llm", "harness-mcp", "harness-memory", + "harness-observability", "harness-plugin", + "harness-project", "harness-server", "harness-skill", "harness-store", diff --git a/Cargo.toml b/Cargo.toml index 3cad2b0..aea9e1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,15 @@ [workspace] resolver = "2" members = [ + "crates/harness-channel", "crates/harness-cloud", "crates/harness-core", "crates/harness-llm", "crates/harness-mcp", "crates/harness-memory", + "crates/harness-observability", "crates/harness-plugin", + "crates/harness-project", "crates/harness-requirement", "crates/harness-server", "crates/harness-skill", @@ -25,11 +28,14 @@ license = "MIT" rust-version = "1.80" [workspace.dependencies] +harness-channel = { path = "crates/harness-channel" } harness-core = { path = "crates/harness-core" } harness-llm = { path = "crates/harness-llm" } harness-mcp = { path = "crates/harness-mcp" } harness-memory = { path = "crates/harness-memory" } +harness-observability = { path = "crates/harness-observability" } harness-plugin = { path = "crates/harness-plugin" } +harness-project = { path = "crates/harness-project" } harness-requirement = { path = "crates/harness-requirement" } harness-server = { path = "crates/harness-server" } harness-skill = { path = "crates/harness-skill" } @@ -74,6 +80,10 @@ clap = { version = "4", features = ["derive"] } toml = "0.8" dialoguer = "0.11" sha2 = "0.10" +sha1 = "0.10" +hmac = "0.12" +aes = "0.8" +cbc = "0.1" rand = "0.8" base64 = "0.22" open = "5" diff --git a/apps/jarvis-web/package-lock.json b/apps/jarvis-web/package-lock.json index 7bed70a..4824c33 100644 --- a/apps/jarvis-web/package-lock.json +++ b/apps/jarvis-web/package-lock.json @@ -30,6 +30,7 @@ "diff": "^9.0.0", "lottie-web": "^5.13.0", "mdast-util-to-string": "^4.0.0", + "qrcode.react": "^4.2.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^7.14.2", @@ -6109,6 +6110,15 @@ "node": ">=6" } }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react": { "version": "19.2.5", "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", diff --git a/apps/jarvis-web/package.json b/apps/jarvis-web/package.json index d30b715..1724edd 100644 --- a/apps/jarvis-web/package.json +++ b/apps/jarvis-web/package.json @@ -34,6 +34,7 @@ "diff": "^9.0.0", "lottie-web": "^5.13.0", "mdast-util-to-string": "^4.0.0", + "qrcode.react": "^4.2.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^7.14.2", diff --git a/apps/jarvis-web/src/App.tsx b/apps/jarvis-web/src/App.tsx index e7349e9..c371551 100644 --- a/apps/jarvis-web/src/App.tsx +++ b/apps/jarvis-web/src/App.tsx @@ -31,12 +31,9 @@ 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 { OAuthResultPage } from "./components/OAuth/OAuthResultPage"; import { DesktopStartupOverlay } from "./components/Desktop/DesktopStartupOverlay"; import { useAppStore, appStore } from "./store/appStore"; import { boot, applyI18n } from "./services/boot"; @@ -46,6 +43,7 @@ import { showHelpOverlay } from "./services/slash_commands"; import { newConversation, resumeConversation } from "./services/conversations"; import { loadProviders } from "./services/providers"; import { apiUrl } from "./services/api"; +import { ConfirmDialogHost } from "./components/ui"; import "./styles.css"; export function App() { @@ -112,15 +110,6 @@ export function App() { path="/diagnostics" element={} /> - } /> - {/* `/conversations/:id` resumes the persisted conversation - and redirects to the stable session URL. Useful for old - bookmarks / shared URLs that should land back in the - right thread. */} - } - /> } /> } /> {/* SubAgent UI preview — static prototype with mocked frame @@ -128,11 +117,17 @@ export function App() { be replaced by the real components consuming WS events once the subagent backend lands. */} } /> + {/* WeCom OAuth2 landing page. Reached by `/v1/channels/:id/oauth/callback` + after it 302s here with `?userid=&ctx=`. Standalone — no + sidebar / chrome, since the audience is a freshly opened + tab from a WeCom client deep link. */} + } /> {/* Catch-all: send unknown SPA paths home rather than rendering a blank page. Server-side `spa_fallback` already serves index.html for these, so this is the client-side mirror. */} } /> + ); } @@ -211,36 +206,8 @@ function DocsLayout() { ); } -// Conversation history archive — full-page browse over every -// persisted conversation. Same shell as ProjectsLayout so the -// sidebar stays put. -function ConversationsArchiveLayout() { - return ( - <> - - Skip to main content - -
- - - - - - ); -} - // Customize — unified entry point for Skills / MCP / Plugins. -// Reachable from the chat sidebar under "全部会话". Shares the same +// Reachable from the chat sidebar under "能力市场". Shares the same // shell as ProjectsLayout so the global sidebar still anchors the // page. function CustomizeLayout() { diff --git a/apps/jarvis-web/src/components/AppSidebar.tsx b/apps/jarvis-web/src/components/AppSidebar.tsx index 4b42594..d89fa9f 100644 --- a/apps/jarvis-web/src/components/AppSidebar.tsx +++ b/apps/jarvis-web/src/components/AppSidebar.tsx @@ -148,29 +148,6 @@ function ChatSidebarBody() { <>
@@ -457,6 +480,10 @@ export function ProjectBoard({ onOpenDetail={(id) => setSelectedId(id)} />
+ ) : activeTab === "files" ? ( +
+ +
) : (
- {t("harnessObsEvalCapability")} + {pct(state.evalSummary.capability_pass_rate)}
- {t("harnessObsEvalRegression")} + {pct(state.evalSummary.regression_pass_rate)}
- {t("harnessObsEvalPassAtK")} + {pct(state.evalSummary.trial_reliability.pass_at_k)}
- {t("harnessObsEvalPassAll")} + {pct(state.evalSummary.trial_reliability.pass_all)}
- {t("harnessObsEvalGraders")} + {countKeys(state.evalSummary.by_grader_kind)}
- {t("harnessObsEvalTranscripts")} + {state.evalSummary.transcript_cases}/{state.evalSummary.total_cases} diff --git a/apps/jarvis-web/src/components/Projects/WorkOverview/KpiStrip.tsx b/apps/jarvis-web/src/components/Projects/WorkOverview/KpiStrip.tsx index 2eec167..4bab697 100644 --- a/apps/jarvis-web/src/components/Projects/WorkOverview/KpiStrip.tsx +++ b/apps/jarvis-web/src/components/Projects/WorkOverview/KpiStrip.tsx @@ -11,15 +11,47 @@ interface KpiCardProps { label: string; value: string; hint?: string; + formulaLines?: string[]; tone?: "neutral" | "danger" | "ok"; icon: ReactNode; loading?: boolean; } +function FormulaHint({ lines }: { lines: string[] }) { + return ( + + + + {lines.map((line) => ( + {line} + ))} + + + ); +} + +function LabelWithHint({ label, lines }: { label: string; lines?: string[] }) { + if (!lines || lines.length === 0) return {label}; + return ( + + {label} + + + ); +} + function KpiCard({ label, value, hint, + formulaLines, tone = "neutral", icon, loading = false, @@ -38,7 +70,9 @@ function KpiCard({ - {label} + + +
{loading ? (