diff --git a/.config/jp/tools/src/git/diff_commit.rs b/.config/jp/tools/src/git/diff_commit.rs index cfbd1373..a32aa6a8 100644 --- a/.config/jp/tools/src/git/diff_commit.rs +++ b/.config/jp/tools/src/git/diff_commit.rs @@ -65,10 +65,10 @@ fn git_diff_commit_impl( }; let mut result = String::new(); + write!(result, "```diff\n{}\n```", content.trim_end())?; if let Some(note) = note { - writeln!(result, "{note}\n")?; + writeln!(result, "\n\n{note}\n")?; } - write!(result, "```diff\n{}\n```", content.trim_end())?; Ok(result.into()) } diff --git a/.config/jp/tools/src/git/diff_commit_tests.rs b/.config/jp/tools/src/git/diff_commit_tests.rs index f06e8539..b0ac0309 100644 --- a/.config/jp/tools/src/git/diff_commit_tests.rs +++ b/.config/jp/tools/src/git/diff_commit_tests.rs @@ -205,7 +205,11 @@ fn diff_commit_with_pattern() { assert!(content.contains("```diff\n")); assert!(content.contains("world")); - assert!(content.ends_with("\n```")); + // The closing diff fence is followed by the `[Showing ...]` note, + // so we check that both the fence and the note are present rather + // than that the output ends with the fence. + assert!(content.contains("\n```\n"), "got: {content}"); + assert!(content.contains("[Showing"), "got: {content}"); } #[test] diff --git a/.ignore b/.ignore index 744aee4d..3990a1fe 100644 --- a/.ignore +++ b/.ignore @@ -15,6 +15,7 @@ # Then the exclusions (applied within the un-ignored trees) .jp/conversations/ +.jp/local-conversations/ docs/.yarn/ docs/.vitepress/cache/ docs/.vitepress/dist/ diff --git a/.jp/config/personas/pr-reviewer.toml b/.jp/config/personas/pr-reviewer.toml index 7aaf0a82..3a928166 100644 --- a/.jp/config/personas/pr-reviewer.toml +++ b/.jp/config/personas/pr-reviewer.toml @@ -47,8 +47,8 @@ intentional: your job is to surface findings the user can curate before publishi """ [assistant.model] -id = "opus" -parameters.reasoning.effort = "xhigh" +id = "gpt" +parameters.reasoning.effort = "max" [[assistant.instructions]] title = "Review Workflow" diff --git a/.jp/config/skill/rust-development.toml b/.jp/config/skill/rust-development.toml index 03806ba1..28f09e50 100644 --- a/.jp/config/skill/rust-development.toml +++ b/.jp/config/skill/rust-development.toml @@ -83,6 +83,13 @@ items = [ underscore (`crate_`, `type_`) or a raw identifier (`r#type`). Never misspell the word (e.g. \ `krate` is wrong).\ """, + """\ + **No redundant type annotations.** Do not annotate `let` bindings when the right-hand side \ + already determines the type. Write `let foo = vec![1, 2, 3];` not `let foo: Vec = \ + vec![1, 2, 3];`. Annotations are fine when the compiler genuinely can't infer (e.g. \ + `.collect()` into a specific collection), when you need a non-default numeric type, or when \ + an annotation is clearer than a turbofish.\ + """, ] [[assistant.instructions.examples]] @@ -167,3 +174,8 @@ match val { }\ """ reason = "Never start a match arm pattern with |." + +[[assistant.instructions.examples]] +good = "let items = vec![1, 2, 3];" +bad = "let items: Vec = vec![1, 2, 3];" +reason = "The macro already determines the type. Redundant annotation is noise." diff --git a/.jp/mcp/tools/git/add_intent.toml b/.jp/mcp/tools/git/add_intent.toml index 135b08f9..4e7066d4 100644 --- a/.jp/mcp/tools/git/add_intent.toml +++ b/.jp/mcp/tools/git/add_intent.toml @@ -1,7 +1,8 @@ [conversation.tools.git_add_intent] enable = "explicit" run = "unattended" -style.inline_results = "full" +style.inline_results = "off" +style.parameters = "function_call" source = "local" command = "just serve-tools {{context}} {{tool}}" diff --git a/Cargo.toml b/Cargo.toml index 2d793d3f..d9111e78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,10 +12,10 @@ resolver = "3" jp_attachment = { path = "crates/jp_attachment" } jp_attachment_bear_note = { path = "crates/jp_attachment_bear_note" } jp_attachment_cmd_output = { path = "crates/jp_attachment_cmd_output" } -jp_attachment_internal = { path = "crates/jp_attachment_internal" } jp_attachment_file_content = { path = "crates/jp_attachment_file_content" } jp_attachment_github = { path = "crates/jp_attachment_github" } jp_attachment_http_content = { path = "crates/jp_attachment_http_content" } +jp_attachment_internal = { path = "crates/jp_attachment_internal" } jp_attachment_mcp_resources = { path = "crates/jp_attachment_mcp_resources" } jp_config = { path = "crates/jp_config" } jp_conversation = { path = "crates/jp_conversation" } diff --git a/crates/jp_cli/Cargo.toml b/crates/jp_cli/Cargo.toml index 04ea7803..09f9f571 100644 --- a/crates/jp_cli/Cargo.toml +++ b/crates/jp_cli/Cargo.toml @@ -17,10 +17,10 @@ version.workspace = true jp_attachment = { workspace = true } jp_attachment_bear_note = { workspace = true } jp_attachment_cmd_output = { workspace = true } -jp_attachment_internal = { workspace = true } jp_attachment_file_content = { workspace = true } jp_attachment_github = { workspace = true } jp_attachment_http_content = { workspace = true } +jp_attachment_internal = { workspace = true } jp_attachment_mcp_resources = { workspace = true } jp_config = { workspace = true } jp_conversation = { workspace = true } @@ -69,8 +69,8 @@ indoc = { workspace = true } inquire = { workspace = true, features = ["crossterm"] } minijinja = { workspace = true } quick-xml = { workspace = true, features = ["serialize"] } -relative-path = { workspace = true } rayon = { workspace = true } +relative-path = { workspace = true } reqwest = { workspace = true } schemars = { workspace = true } schematic = { workspace = true, features = ["schema_serde", "renderer_template", "toml"] } @@ -103,7 +103,11 @@ which = { workspace = true, features = ["real-sys"] } libc = { workspace = true } [target.'cfg(windows)'.dependencies] -windows-sys = { workspace = true, features = ["Win32_System_Console", "Win32_System_Threading", "Win32_Foundation"] } +windows-sys = { workspace = true, features = [ + "Win32_System_Console", + "Win32_System_Threading", + "Win32_Foundation", +] } [build-dependencies] chrono = { workspace = true } diff --git a/crates/jp_cli/src/lib.rs b/crates/jp_cli/src/lib.rs index 3d20b49c..217496ca 100644 --- a/crates/jp_cli/src/lib.rs +++ b/crates/jp_cli/src/lib.rs @@ -112,16 +112,6 @@ struct Globals { )] config: Vec, - #[arg( - short = 'I', - long = "no-inherit", - global = true, - value_parser = BoolValueParser::new().map(|v| !v), - default_value_t = true, - help = "Disable loading of non-CLI provided config.", - )] - load_non_cli_config: bool, - /// Increase verbosity of logging. /// /// Can be specified multiple times to increase verbosity. diff --git a/crates/jp_cli/src/lib_tests.rs b/crates/jp_cli/src/lib_tests.rs index 7c82ac3e..ccbe361e 100644 --- a/crates/jp_cli/src/lib_tests.rs +++ b/crates/jp_cli/src/lib_tests.rs @@ -106,7 +106,7 @@ fn test_load_cli_cfg_args_user_global_root() { let result = build_cfg(partial, &overrides, None).unwrap(); assert_eq!(result.assistant.name.as_deref(), Some("from-global")); - unsafe { std::env::remove_var("JP_GLOBAL_CONFIG_FILE") }; + unsafe { std::env::remove_var("JP_GLOBAL_CONFIG_DIR") }; } #[test] diff --git a/docs/architecture/index.md b/docs/architecture/index.md index 5a08d642..cd54e513 100644 --- a/docs/architecture/index.md +++ b/docs/architecture/index.md @@ -27,3 +27,8 @@ This section describes the technical architecture of JP. WebAssembly components. Covers the `wasmtime` runtime, WIT contract, builtin tools (embedded), local Wasm tools (disk-loaded), and the `jp_tool_learn` guest crate. + +- [Ubiquitous Language](ubiquitous-language.md) - The canonical glossary of + JP's domain vocabulary. Defines the core terms (Workspace, Conversation, + Turn, Thread, Attachment, Inquiry, Provider, Backend, etc.) and the + distinctions between them. diff --git a/docs/architecture/ubiquitous-language.md b/docs/architecture/ubiquitous-language.md new file mode 100644 index 00000000..3abe53f3 --- /dev/null +++ b/docs/architecture/ubiquitous-language.md @@ -0,0 +1,114 @@ +# Ubiquitous Language + +This is JP's domain vocabulary: the shared, rigorous terms used across code, +documentation, commits, RFDs, CLI help, and error messages. Every contributor +(human or AI) should use these terms *as written* — don't paraphrase or +substitute near-synonyms. + +When you encounter a new concept that doesn't fit existing terms, add it here. +When an existing term is contradicted by usage or misleading, update the +definition — don't paper over the drift with aliases or inline comments +explaining the mismatch. + +In disagreements between code and docs, the code is authoritative. + +## Table of Contents + + +- [Ubiquitous Language](#ubiquitous-language) + - [Table of Contents](#table-of-contents) + - [Terms](#terms) + - [Attachment](#attachment) + - [Conversation](#conversation) + - [Conversation Event](#conversation-event) + - [Inquiry](#inquiry) + - [Persona](#persona) + - [Provider](#provider) + - [RFD](#rfd) + - [Thread](#thread) + - [Tool Call](#tool-call) + - [Turn](#turn) + - [Workspace](#workspace) + + +## Terms + +### Attachment + +External content attached to a conversation to provide context: a file, URL +contents, command output, Bear note, MCP resource, etc. Implemented as +`Attachment` in `jp_attachment`. Each attachment kind is a separate crate +(`jp_attachment_file_content`, `jp_attachment_cmd_output`, and so on). + +### Conversation + +A persistent sequence of events identified by a `ConversationId`, living within +a Workspace. Implemented as `ConversationStream` in `jp_conversation`. The +user-facing notion of "a chat history with the assistant." + +**Not to be confused with Thread.** A Conversation is the stored entity; a +Thread is what we build from it to send to an LLM. + +### Conversation Event + +The atomic unit of a conversation. Implemented as `ConversationEvent` (with +`EventKind`) in `jp_conversation`. The variants are `TurnStart`, `ChatRequest`, +`ChatResponse`, `ToolCallRequest`, `ToolCallResponse`, `InquiryRequest`, +`InquiryResponse`. + +Not every event is sent to LLM providers. `EventKind::is_provider_visible()` +filters the stream down to the chat and tool-call events; turn markers and +inquiries are internal. + +### Inquiry + +A structured question-and-answer pair between the assistant, a tool, and/or the +user — distinct from a regular chat message. Carried as `InquiryRequest` and +`InquiryResponse` events within a conversation. Used for mid-turn clarification +that should not appear in the main chat stream or be sent to the LLM provider as +context. + +### Provider + +An LLM vendor integration — one of `anthropic`, `google`, `openai`, +`openrouter`, `llamacpp`, `ollama`, `cerebras`, `deepseek`. Each implements the +`Provider` trait in `jp_llm`. + +### RFD + +"Request for Discussion" — JP's design document format, stored in `docs/rfd/`. +Each RFD captures design rationale for a significant change. Numeric-prefixed +RFDs (`001-`, `002-`, …) are the accepted series; `D`-prefixed RFDs (`D01-`, +`D02-`, …) are drafts or abandoned proposals. The process itself is defined in +[RFD-001](../rfd/001-jp-rfd-process.md). + +### Thread + +The decomposed, provider-facing projection of a Conversation: a rendered system +prompt, rendered instruction sections, raw attachments, and a filtered event +stream, ready to be sent to an LLM provider. Implemented as `Thread` in +`jp_conversation::thread`. + +A Conversation becomes a Thread at query time, via the config and conversation +pipeline. A Thread is transient; a Conversation is persisted. + +### Tool Call + +An LLM-requested function invocation (`ToolCallRequest`) and its eventual +response (`ToolCallResponse`). Tool calls are events within a Turn. The tool +itself can be a built-in, a local command, an MCP-provided tool, or a plugin. + +### Turn + +A group of conversation events delimited by a `TurnStart` marker: one user chat +request through the assistant's final response for that request, including any +intermediate tool calls and inquiries. Implemented as `Turn<'a>` in +`jp_conversation::stream::turn_iter`. + +A single Conversation contains many Turns, separated by `TurnStart` events. + +### Workspace + +The top-level project unit, housing conversations, configuration, plugins, and +state for JP. Identified by a `.jp/` directory at the project root. Implemented +as `Workspace` in `jp_workspace`. diff --git a/docs/rfd/index.md b/docs/rfd/index.md index 396b6761..9cd00b21 100644 --- a/docs/rfd/index.md +++ b/docs/rfd/index.md @@ -89,7 +89,7 @@ const filtered = computed(() => { if (textQuery) { rows = rows.filter(r => - [r.title, r.category, r.status, r.summary] + [r.num, r.title, r.category, r.status, r.summary] .some(v => v?.toLowerCase().includes(textQuery)) ) } diff --git a/justfile b/justfile index 49f41264..2131c527 100644 --- a/justfile +++ b/justfile @@ -354,6 +354,76 @@ rfd-review NNN *ARGS: _install-jp printf "Reviewing $file\n\n" >&2 jp query --attach "$file" --new --cfg=personas/rfd-reviewer $args +# Triage feedback on an RFD from a reviewer conversation. +# +# NNN is the RFD (permanent number like 41/041, or draft ID like D01). +# MODE is either `new` (start a fresh triage conversation) or `continue` +# (append to the current session, e.g. to follow up on the implementation +# conversation that produced the RFD). +# CONVO is the conversation ID of the `rfd-review` run to pull feedback from. +# Only the final assistant response of that conversation is attached. +[group('rfd')] +[positional-arguments] +rfd-triage NNN MODE CONVO *ARGS: _install-jp + #!/usr/bin/env sh + set -eu + + shift 3 # remove NNN, MODE, CONVO from positional params + args="$@" + msg="I received feedback on the RFD. Read the attached reviewer response \ + carefully, then triage it item by item. Ground each point against the code \ + and related RFDs. Do not assume the feedback is correct. For each item \ + give a verdict (accept / amend / dismiss / defer) with reasoning, and for \ + accepted or amended items describe the concrete change you would make to \ + the RFD. Do NOT edit the RFD yet; give your opinion first." + + # Resolve the target RFD file. + arg="{{NNN}}" + if echo "$arg" | grep -qiE '^D[0-9]+$'; then + draft_id=$(echo "$arg" | tr '[:lower:]' '[:upper:]') + file=$(ls docs/rfd/drafts/${draft_id}-*.md 2>/dev/null | head -1) + if [ -z "$file" ]; then + echo "No draft RFD found with ID ${draft_id}." >&2; exit 1 + fi + elif echo "$arg" | grep -qE '^[0-9]+$'; then + n=$(echo "$arg" | sed 's/^0*//') + num=$(printf "%03d" "${n:-0}") + file=$(ls docs/rfd/${num}-*.md 2>/dev/null | head -1) + if [ -z "$file" ]; then + echo "No RFD found with number ${num}." >&2; exit 1 + fi + else + echo "Invalid argument '${arg}'. Use a number (41) or draft ID (D01)." >&2; exit 1 + fi + + # Resolve MODE. Explicit to avoid silently picking a default. + case "{{MODE}}" in + new) new_flag="--new" ;; + continue) new_flag="" ;; + *) + echo "Invalid MODE '{{MODE}}'. Use 'new' or 'continue'." >&2 + exit 1 ;; + esac + + starts_with() { case ${2-} in "$1"*) true;; *) false;; esac; } + contains() { case ${2-} in *"$1"*) true;; *) false;; esac; } + if starts_with "-- " "$@"; then + elif starts_with "-" "$@" && ! contains "-- " "$@"; then + args="$* -- $msg" + elif [ -n "$args" ]; then + args="$msg\n\n Here is additional context: $args" + elif [ -z "$args" ]; then + args="$msg" + fi + + printf "Triaging feedback on $file (mode: {{MODE}})\n\n" >&2 + jp query \ + --attach "file://$file" \ + --attach "jp://{{CONVO}}?select=a" \ + $new_flag \ + --cfg=personas/rfd-triager \ + $args + # Create a new RFD draft. CATEGORY is 'design', 'decision', 'guide', or 'process'. # Drafts are created as docs/rfd/drafts/DNN-slug.md; a permanent number is assigned # and the file is moved up to docs/rfd/ at Discussion.