Skip to content

feat(intake): add multi-turn project intake with deterministic gate - #545

Open
andresnunes1404 wants to merge 14 commits into
isair:developfrom
andresnunes1404:feat/project-intake
Open

feat(intake): add multi-turn project intake with deterministic gate#545
andresnunes1404 wants to merge 14 commits into
isair:developfrom
andresnunes1404:feat/project-intake

Conversation

@andresnunes1404

@andresnunes1404 andresnunes1404 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a deterministic, multi-turn "project intake" flow: the user describes a project idea over a short back-and-forth, Jarvis matches it to a template, asks one question per turn, then writes a brief to Obsidian and (on a separate later trigger) hands the plan off to Antigravity for development.

  • project_intake_sessions table (src/jarvis/memory/db.py): new CREATE TABLE IF NOT EXISTS + CRUD backing the intake state machine (awaiting_type -> in_progress -> completed), following the same pattern as the existing meals table.
  • Config keys: project_intake_enabled and project_templates_path (src/jarvis/config.py), surfaced in the auto-generated settings UI and added to the shared MockConfig test doubles used by tests/ and evals/.
  • projectIntake and startProjectDevelopment tools (src/jarvis/tools/builtin/project_intake.py, registered in the builtin catalogue): projectIntake runs the interview itself (template matching against project_templates.json, one question per turn, an abandon phrase, and the Obsidian brief write on completion); startProjectDevelopment is the separate, later single-shot trigger that resolves a saved plan note, reads it back, and delegates it to Antigravity via MCP.
  • Deterministic gate in the reply engine (src/jarvis/reply/engine.py): runs right after redaction, before recent-dialogue lookup, MCP refresh, the tool router, and the planner. An active intake session forces the projectIntake tool call and skips everything else for the turn, fail-open on any DB error. The agentic loop also now ends immediately after any projectIntake/startProjectDevelopment call (both the model-emitted and planner direct-exec call sites), so the chat model can't re-invoke the tool with fabricated input and silently advance multi-turn state more than once per turn.
  • Docs: registers project_intake.spec.md in CLAUDE.md's Spec File Registry, adds the "3c. Project Intake Gate" entry and flow-diagram step to docs/llm_contexts.md as the first non-LLM pre-planner gate that also calls an MCP server directly from tool code, and mentions the capability in README's Built-in Tools list.
  • Eval (evals/test_project_intake_flow.py): covers the one part of the flow that genuinely depends on the LLM router/planner rather than the deterministic gate — the initial trigger phrase must route to projectIntake with a reply that is exactly the type question — and asserts the follow-up turn is gated deterministically instead of relying on the model to pick the tool again on its own.

Note on MCP tool names

The Obsidian and Antigravity MCP server/tool names in project_intake.py (OBSIDIAN_MCP_SERVER = "Jarvis Brain", OBSIDIAN_WRITE_TOOL = "create_note", OBSIDIAN_SEARCH_TOOL = "simple_search", OBSIDIAN_READ_TOOL = "get_file_contents", OBSIDIAN_PATCH_TOOL = "patch_content", ANTIGRAVITY_MCP_SERVER = "Antigravity", ANTIGRAVITY_DISPATCH_TOOL = "run_task") are best-effort defaults based on common MCP server conventions and are placeholders pending confirmation against the user's actual configured servers.

Test plan

  • tests/tools/builtin/test_project_intake.py and tests/tools/builtin/test_start_project_development.py pass
  • tests/test_engine_project_intake_gate.py passes
  • evals/test_project_intake_flow.py passes
  • Confirm real Obsidian/Antigravity MCP tool names against the user's setup before relying on the flow end-to-end

🤖 Generated with Claude Code

andresnunes1404 and others added 9 commits July 21, 2026 14:25
Body background, grid-bg opacity, and vignette darkening were tuned
too dark to be legible; lighten them so the grid and navy background
show through. Also fix REGISTO DO SISTEMA log ordering: column-reverse
combined with insertBefore(firstChild) was pushing the newest line to
the bottom instead of the top.
…cise full inference pipeline

The warmup was sending an empty /api/generate request, which loads the
model into memory but does not trigger the full chat-inference pipeline
(JIT compilation, KV-cache allocation). The first real /api/chat call
with a large prompt (e.g. intent judge system prompt + transcript)
could therefore exceed even generous timeouts on laptop GPUs.

Switch to a minimal /api/chat with a system + user message pair and
num_predict=1, matching the approach already used by
OpenAICompatibleBackend.warm_up().

Closes isair#537
… to exercise full inference pipeline"

This reverts commit ce9e198.
Backs the multi-turn project intake tool's state machine
(awaiting_type -> in_progress -> completed) with a dedicated table,
following the same CREATE TABLE IF NOT EXISTS pattern as meals.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
…ettings

Also surfaces both in the auto-generated settings UI and extends the
shared MockConfig test doubles used by tests/ and evals/.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
projectIntake runs the deterministic, multi-turn interview (template
matching, one question per turn, abandon phrase, Obsidian brief write
on completion). startProjectDevelopment is the separate, later
single-shot trigger that resolves a saved plan note, reads it, and
hands it off to Antigravity via MCP. Both are registered in the
builtin tool catalogue.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Runs right after redaction, before recent-dialogue lookup, MCP
refresh, the tool router, and the planner: an active intake session
forces the projectIntake tool call and skips everything else for the
turn. Fail-open on any DB error. Also ends the agentic loop
immediately after any projectIntake/startProjectDevelopment call
(both call sites: model-emitted and planner direct-exec) so the chat
model can never re-invoke the tool with fabricated input and silently
advance multi-turn intake state more than once per turn.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Registers project_intake.spec.md in CLAUDE.md's Spec File Registry,
adds the "3c. Project Intake Gate" entry (and Flow-diagram step) to
docs/llm_contexts.md as the first non-LLM pre-planner gate to also
call an MCP server directly from tool code, and mentions the new
capability in README's Built-in Tools list.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Covers the one point in the flow where tool selection genuinely
depends on the LLM router/planner rather than the deterministic gate:
the initial trigger phrase must be routed to projectIntake and the
reply must be exactly the type question. Also asserts the follow-up
turn is gated deterministically rather than relying on the model to
pick the tool again on its own.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
@andresnunes1404 andresnunes1404 changed the title Feat/project intake feat(intake): add multi-turn project intake with deterministic gate Jul 22, 2026
Saying the start-a-new-project phrase again while a projectIntake
session is already awaiting_type/in_progress was being silently
swallowed as free-text input to the current question. Detect it with
the same substring/normalisation approach used for the abandon phrase
and ask the user to explicitly finish or abandon first, without
touching current_index/answers_json.
A session with no explicit abandon phrase and no activity stayed
in_progress/awaiting_type forever, hijacking every future turn via the
gate — even across app restarts, since it's a DB row. get_gated_session
now checks updated_at against the new project_intake_stale_minutes
config key (default 30) and marks a stale session
completed/abandoned, letting the turn fall through to normal routing.
Two paths could crash a turn permanently: the awaiting_type ->
in_progress transition's context.db.update_intake_session() call had
no try/except (unlike the sibling in_progress path), and
json.loads() on questions_json/answers_json had none either — a
malformed value crashed the turn, and since the gate forces this tool
every subsequent turn too, the conversation stayed permanently broken.
Both now fail gracefully: a DB write failure returns a friendly error
without corrupting session state, and malformed JSON abandons the
session with a friendly message instead of crashing.
Verified live via list_tools against cfg.mcps: the Obsidian server is
keyed "obsidian" (not "Jarvis Brain"), and delegation goes through the
"jarvis-router" server's run_antigravity tool rather than a standalone
"Antigravity" server. Tool names and call shapes are corrected to
match: vault_write, search_simple, vault_read, vault_patch,
run_antigravity. search_simple/vault_read return JSON payloads (not
plain text/markdown), so _extract_note_paths and the new
_extract_read_content helper unwrap them, with a fallback for plain
text. vault_patch requires targetType/target/operation rather than a
raw content overwrite, so the status update now targets the "Status"
heading directly instead of regex-rewriting the whole file. run_antigravity's
schema only accepts a "task" string, so the delegation framing is
folded into that field instead of a separate "instructions" argument.
@isair
isair force-pushed the develop branch 2 times, most recently from 075622a to 2d40388 Compare July 26, 2026 20:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants