Skip to content

Commit 8bdee4a

Browse files
NikitasT2003claude
andcommitted
feat: agent-in-terminal — drop hardcoded workflows, add bash + MCP
Per user direction, remove all hardcoded agent workflow commands. The agent now drives tool + skill selection from the chat REPL, the way Claude Code does. Four deterministic utilities remain (`setup`, `init`, `status`, `ingest`); everything else happens in `chat`. Removed - `thesis curate` (was: injects long step-by-step curate prompt) - `thesis style` (was: step-by-step style-learner prompt) - `thesis write SECT` (was: step-by-step drafter prompt) - `thesis lint` (was: wiki-linter / citation-linter prompt) - `_run()` helper in cli.py - All matching tests in test_commands.py + test_extra_coverage.py - `test_cli_prompts_have_explicit_stop_markers` in test_loop_guard.py (no more hardcoded prompts to assert STOP against) Added - `src/thesis_agent/tools.py`: * `bash` tool — runs commands in the workspace root, captures stdout+stderr, 60 s timeout (tunable via THESIS_SHELL_TIMEOUT_SEC), 64 KB output cap. Disabled globally with THESIS_NO_SHELL=1. * `_load_mcp_config` + `load_mcp_tools_sync` — picks up `.thesis/mcp.json` or `mcp.json` (or $THESIS_MCP_CONFIG), supports both flat and `{servers: {...}}` shapes, uses `MultiServerMCPClient` from `langchain-mcp-adapters`. Tolerant to bad JSON, missing adapters package, failed server connections. * `build_runtime_tools()` composes bash + MCP tools into the list that `build_agent` hands to `create_deep_agent`. - `langchain-mcp-adapters` dependency in pyproject.toml. - `thesis` (no subcommand) now opens the chat REPL directly; `thesis --help` still lists commands. - 31 new tests in test_tools.py covering the bash tool, the MCP config loader (all three search paths + both config shapes), and `build_runtime_tools` composition. Updated - `agent.build_agent`: passes `tools=build_runtime_tools()` instead of `tools=[]`. Agent now has shell + MCP at runtime. - `MAIN_SYSTEM_PROMPT` in agent.py: rewritten to describe the agent-in-terminal model — here are your tools + skills + memory, here are the hard rules (citation, never-write-to-raw/data), now figure out what the user wants. Removed the "delegate to these three subagents for these three tasks" scaffolding. - `AGENTS.md` "Write scopes" section: now describes soft conventions (not hard sandbox rules) + a "Tools at runtime" section listing filesystem / bash / MCP / memory. - `README.md`: command table trimmed to the four utilities + chat; explains the available tools + how to attach MCP servers. - `tests/test_cli_smoke` + `tests/test_commands`: `--help` expectations now assert the small command list and explicitly check that curate/style/write/lint are GONE (regression guard). Tests: 348 passed, 1 skipped (Windows symlink rights). Ruff clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 22dcca3 commit 8bdee4a

11 files changed

Lines changed: 644 additions & 393 deletions

File tree

AGENTS.md

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -268,15 +268,38 @@ Report file + line numbers. Do not auto-edit.
268268

269269
---
270270

271-
## Write scopes (enforced by the sandbox middleware)
271+
## Write scopes
272272

273-
- `researcher` subagent: **read-only**.
274-
- `wiki-curator` subagent: may write under `research/wiki/**` and update
273+
The filesystem tools are scoped to the workspace root. Within the
274+
workspace the two hard rules are "never `research/raw/`" (sources are
275+
immutable — ingest normalises them via `thesis ingest` instead) and
276+
"never `data/`" (agent's own memory databases).
277+
278+
By convention:
279+
280+
- `researcher` subagent — read-only.
281+
- `wiki-curator` subagent — writes under `research/wiki/**` and edits
275282
`research/raw/_index.json`.
276-
- `drafter` subagent: may write under `thesis/**`.
277-
- Main agent: may write to `research/wiki/**`, `style/STYLE.md`, `thesis/**`,
278-
and `research/raw/_index.json`. **Never** to `research/raw/` (sources are
279-
immutable) or `data/` (reserved for memory databases).
283+
- `drafter` subagent — writes under `thesis/**`.
284+
- Main agent — writes anywhere in the workspace except the two denied
285+
paths above. Typically: `research/wiki/**`, `style/STYLE.md`,
286+
`thesis/**`, `research/raw/_index.json`.
287+
288+
## Tools at runtime
289+
290+
- **Filesystem** (`read_file`, `write_file`, `edit_file`, `ls`, `glob`,
291+
`grep`) scoped to the workspace root via
292+
`FilesystemBackend(virtual_mode=True)`.
293+
- **Bash** — shell in the workspace, captures stdout/stderr, timed out
294+
at 60 s by default (`THESIS_SHELL_TIMEOUT_SEC` to change). Use for
295+
`thesis ingest`, git, pandoc, pytest, anything deterministic you'd
296+
type at a shell. Disabled globally with `THESIS_NO_SHELL=1`.
297+
- **MCP tools** — servers listed in `.thesis/mcp.json` (or
298+
`$THESIS_MCP_CONFIG`) are connected at boot via
299+
`langchain-mcp-adapters`. Supports stdio / SSE / streamable_http.
300+
- **Memory** — writes to `/memories/**` persist across chat sessions
301+
(backed by SQLite). Use for user preferences + long-running context;
302+
wiki and thesis content stay under their own trees.
280303

281304
---
282305

README.md

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,51 @@ That's it. Non-technical users can stop reading here.
5252

5353
## Commands
5454

55+
The interface is deliberately small. Four deterministic utilities and
56+
one agent REPL. There are no hardcoded agent workflows — the agent picks
57+
the right skill itself based on what you ask.
58+
5559
| Command | What it does |
5660
|---|---|
5761
| `thesis setup` | Interactive first-run wizard (API key, workspace, examples). |
5862
| `thesis init` | Non-interactive workspace scaffold. |
5963
| `thesis status` | What's in the workspace (counts, DB sizes, thread id). |
6064
| `thesis ingest [DIR]` | Normalise PDFs/DOCX/EPUB/MD/URLs → markdown. No LLM. |
61-
| `thesis style` | Compile `style/STYLE.md` from `style/samples/`. |
62-
| `thesis curate` | Build wiki pages from `pending` sources. |
63-
| `thesis write SECTION` | Draft one thesis section. |
64-
| `thesis lint [FILE]` | Citation + grounding linter over chapters. |
65-
| `thesis chat [--new]` | Interactive REPL. `/new`, `/quit` control the session. |
66-
67-
Global flags: `--thread <id>`, `--version`.
65+
| `thesis chat` | Agent REPL with streaming + slash commands. **This is where you do everything else** — "curate the pending sources", "draft section 2.1", "lint the wiki", "show me what sources disagree about X". |
66+
| `thesis` (no subcommand) | Alias for `thesis chat`. |
67+
68+
Slash commands inside chat: `/help`, `/new`, `/thread [id]`, `/status`,
69+
`/model`, `/clear`, `/history [N]`, `/quit`. Input accepts `\` line
70+
continuation and ` ``` ` fenced blocks.
71+
72+
## What the agent has access to
73+
74+
- **Filesystem** scoped to the workspace (read_file / write_file /
75+
edit_file / ls / glob / grep).
76+
- **Bash** — shell commands run in the workspace root, 60 s timeout by
77+
default. Disable with `THESIS_NO_SHELL=1`, tune timeout with
78+
`THESIS_SHELL_TIMEOUT_SEC`.
79+
- **Skills** from `skills/` (ingest-sources, wiki-curator, wiki-linter,
80+
thesis-writer, style-learner, citation-linter). The agent triggers
81+
the right one based on what you ask.
82+
- **MCP servers** — any servers listed in `.thesis/mcp.json` (or
83+
pointed at via `$THESIS_MCP_CONFIG`) are connected at start. Example:
84+
85+
```json
86+
{
87+
"servers": {
88+
"firecrawl": {
89+
"command": "npx",
90+
"args": ["-y", "firecrawl-mcp"],
91+
"transport": "stdio",
92+
"env": {"FIRECRAWL_API_KEY": "fc-..."}
93+
}
94+
}
95+
}
96+
```
97+
- **Memory** — a SQLite-backed long-term store at `data/store.db`
98+
routed through the `/memories/` virtual path. Writes persist across
99+
chat sessions.
68100

69101
## How it works
70102

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies = [
1414
"langchain-openai>=0.3.0",
1515
"langgraph>=0.3.0",
1616
"langgraph-checkpoint-sqlite>=2.0.0",
17+
"langchain-mcp-adapters>=0.1.0",
1718
"typer>=0.12.0",
1819
"rich>=13.7.0",
1920
"questionary>=2.0.1",

src/thesis_agent/agent.py

Lines changed: 54 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,47 +16,55 @@
1616
from thesis_agent.memory import memory_context
1717
from thesis_agent.subagents import get_subagents
1818

19-
MAIN_SYSTEM_PROMPT = """You are the thesis-agent main orchestrator.
20-
21-
The schema in AGENTS.md is your operating manual; follow it exactly. The
22-
project uses the Karpathy LLM Wiki pattern — a persistent, compounding
23-
knowledge base under `research/wiki/`. Key reminders:
24-
25-
* No facts from pretraining. Every factual claim carries `[src:<file>]`
26-
tracing to `research/raw/` + the wiki.
27-
* The wiki is the index. Start with `research/wiki/index.md`, drill into
28-
concept → entity → source pages. `log.md` is the chronological record.
29-
Navigate with read_file / glob / grep. No vector search. No web. No shell.
30-
* Wiki structure:
31-
- `research/wiki/sources/` — one page per raw file
32-
- `research/wiki/entities/` — one page per entity (person, method,
33-
dataset, concept); accretes across sources
34-
- `research/wiki/concepts/` — higher-level themes tying entities
35-
- `research/wiki/queries/` — filed-back substantive Q&A
36-
- `research/wiki/index.md` — content catalog
37-
- `research/wiki/log.md` — chronological, append-only, grep-friendly
38-
* Delegate to subagents: wiki-curator to ingest / build wiki pages,
39-
drafter to write thesis chapters, researcher for read-only questions.
40-
* When a chat question required non-trivial synthesis, FILE THE ANSWER
41-
BACK as a query page so reasoning compounds instead of vanishing into
42-
chat history. Update `index.md` + `log.md`.
43-
* Write scopes are enforced by the sandbox. Do not try to write outside
44-
them.
45-
* When a claim has no grounding in the wiki/raw, say so. Don't fill from
46-
pretraining.
47-
48-
COST + EFFICIENCY RULES (non-negotiable):
49-
* Plan briefly, then act. One `ls` or one `glob` is usually enough.
50-
* Read each file AT MOST ONCE per task. AGENTS.md is already in your
51-
system prompt — never `read_file("AGENTS.md")`.
52-
* Do not re-read files you already read this turn.
19+
MAIN_SYSTEM_PROMPT = """You are thesis-agent, a terminal-native assistant for
20+
writing a thesis from indexed source material.
21+
22+
The user drives. They'll ask for things like "ingest my new papers",
23+
"curate the pending sources", "draft section 2.1", "lint the wiki", or
24+
open-ended questions. Figure out what to do. The skills in `skills/`
25+
describe conventions for each kind of task; read a skill's body only
26+
when you need it (descriptions load by default). `AGENTS.md` in this
27+
message is the schema — the wiki structure, citation format, and hard
28+
rules — follow it exactly.
29+
30+
What you have:
31+
32+
* **Filesystem tools** (read_file, write_file, edit_file, ls, glob,
33+
grep) scoped to the workspace. Virtual paths like
34+
`/research/wiki/index.md` map onto disk under the workspace root.
35+
* **Bash tool** — runs shell commands in the workspace. Use for
36+
`thesis ingest`, git, pandoc, pytest, anything you'd run at a
37+
prompt. The CLI already has a timeout; don't worry about hangs.
38+
May be disabled via `THESIS_NO_SHELL=1` — if it is, tell the user.
39+
* **Any MCP tools** the user attached via `.thesis/mcp.json`
40+
(web search, Obsidian, GitHub, etc.). Check your tool list at the
41+
start of a non-trivial task so you know what's available.
42+
* **Persistent memory** via the deepagents /memories/ path — writes
43+
there survive across chat sessions. Use it for user preferences,
44+
long-running goals, and durable context.
45+
* **Subagents** (task tool): `wiki-curator` for ingesting sources,
46+
`drafter` for writing thesis chapters, `researcher` for read-only
47+
research with citations.
48+
49+
Hard rules (from AGENTS.md):
50+
51+
* Every factual claim in `research/wiki/**` and `thesis/**` carries
52+
`[src:<raw_filename>]`. No pretraining facts.
53+
* Never write to `research/raw/` — sources are immutable. Never write
54+
to `data/` — that's the agent's own memory.
55+
* `write_file` refuses to overwrite; use `edit_file` for in-place
56+
updates. Never retry a refused write — switch strategies.
57+
* When nothing in the wiki/raw grounds a claim, say so and stop.
58+
59+
Efficiency:
60+
61+
* Plan briefly, then act. One `ls` or one `glob` is usually enough
62+
to orient yourself.
63+
* Read each file at most once per task. AGENTS.md is already here
64+
— never `read_file("AGENTS.md")`.
5365
* Prefer one decisive tool call over three speculative ones.
54-
* When delegating, give the subagent ALL context up front — don't fan
55-
out into multiple task() calls for one job.
56-
* `write_file` refuses to overwrite existing files — use `edit_file` for
57-
in-place updates (manifests, index.md, log.md, page extensions).
58-
* If a tool errors, read the error and adjust. Do not retry blindly.
59-
* Stop as soon as the user's request is satisfied. No unprompted polish.
66+
* Stop as soon as the user's request is satisfied. No unprompted
67+
polish, no re-verification loops.
6068
"""
6169

6270
# LangGraph safety ceiling — stops runaway loops before they bill a fortune.
@@ -156,6 +164,8 @@ def build_agent(
156164
except Exception:
157165
FilesystemBackend = None
158166

167+
from thesis_agent.tools import build_runtime_tools
168+
159169
with ExitStack() as stack:
160170
checkpointer, store = stack.enter_context(memory_context(p))
161171

@@ -164,7 +174,10 @@ def build_agent(
164174
# but with the curator's tighter output cap — the orchestrator
165175
# mostly plans and delegates; it rarely needs a long reply.
166176
"model": make_model(mconf.drafter, role="curator"),
167-
"tools": [], # sandbox: no shell, no network, no code exec
177+
# Runtime tools: bash (unless `THESIS_NO_SHELL=1`) + any MCP
178+
# tools the user attached via `.thesis/mcp.json`. Filesystem
179+
# tools come from the FilesystemBackend separately.
180+
"tools": build_runtime_tools(),
168181
"system_prompt": system_prompt,
169182
"subagents": get_subagents(mconf),
170183
"checkpointer": checkpointer,

src/thesis_agent/cli.py

Lines changed: 14 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -873,137 +873,18 @@ def ingest(
873873

874874

875875
# ---------------------------------------------------------------------------
876-
# agent-backed commands
876+
# chat — the ONLY agent-facing command. No hardcoded workflows.
877+
#
878+
# Previously we shipped `thesis curate` / `style` / `write` / `lint` which
879+
# injected long step-by-step prompts. That was deterministic scaffolding
880+
# and it fought with what the LLM + skills already know how to do. Now
881+
# the user opens chat, asks for what they want ("curate the pending
882+
# sources", "draft section 2.1", "lint the wiki"), and the agent picks
883+
# the right skill + tools on its own. Skills live in `skills/` and the
884+
# schema in `AGENTS.md` still define the conventions — the agent just
885+
# isn't hand-held through them.
877886
# ---------------------------------------------------------------------------
878887

879-
def _run(prompt: str, thread_id: str) -> None:
880-
from thesis_agent.agent import invoke
881-
882-
try:
883-
reply = invoke(prompt, thread_id=thread_id)
884-
except Exception as e:
885-
console.print(f"[red]agent error:[/] {e}")
886-
raise typer.Exit(1) from e
887-
console.print(reply)
888-
889-
890-
@app.command()
891-
def curate(
892-
thread: str = typer.Option(None, "--thread", help="Thread ID (defaults to persisted)."),
893-
) -> None:
894-
"""Delegate to the wiki-curator to build wiki pages from pending sources."""
895-
tid = thread or read_thread_id()
896-
write_thread_id(tid)
897-
_run(
898-
"Curate pending sources per AGENTS.md. Steps, in order, each "
899-
"done at most once:\n"
900-
"1. Read `research/raw/_index.json` to find entries with "
901-
"status 'pending'. If none, reply 'nothing to curate' and STOP.\n"
902-
"2. For each pending source, read the raw markdown ONCE.\n"
903-
"3. For each source, write exactly one file per path:\n"
904-
" - `research/wiki/sources/<filename>.md` (source summary)\n"
905-
" - `research/wiki/entities/<slug>.md` for each distinct entity\n"
906-
" - `research/wiki/concepts/<slug>.md` for each distinct concept\n"
907-
" Use `write_file` for new paths, `edit_file` for paths that "
908-
"already exist. Never retry a refused `write_file`; switch to "
909-
"`edit_file` with a specific `old_string`.\n"
910-
"4. Use `edit_file` once on `research/wiki/index.md` to add new "
911-
"entries.\n"
912-
"5. Use `edit_file` once on `research/wiki/log.md` to append "
913-
"`## [YYYY-MM-DD] ingest | <title>`.\n"
914-
"6. Use `edit_file` once on `research/raw/_index.json` to change "
915-
"'pending' to 'curated' for each processed source.\n"
916-
"7. Reply with a one-line summary and STOP. No further tool calls.",
917-
tid,
918-
)
919-
920-
921-
@app.command()
922-
def style(
923-
thread: str = typer.Option(None, "--thread"),
924-
) -> None:
925-
"""Compile style/STYLE.md from samples in style/samples/."""
926-
tid = thread or read_thread_id()
927-
write_thread_id(tid)
928-
_run(
929-
"Compile a style guide. Steps, each done at most once:\n"
930-
"1. `ls` `/style/samples/` to list files.\n"
931-
"2. `read_file` each sample (each file at most once).\n"
932-
"3. Write `/style/STYLE.md` with `write_file` if it does not "
933-
"exist, or `edit_file` if it does. Produce a prescriptive style "
934-
"guide per the `style-learner` skill (voice, sentence rhythm, "
935-
"lexicon, POV, citation placement, structure).\n"
936-
"4. Reply with a one-line summary of what you wrote and STOP. "
937-
"Do not re-read files. Do not make further tool calls after the "
938-
"write.",
939-
tid,
940-
)
941-
942-
943-
@app.command()
944-
def write(
945-
section: str = typer.Argument(..., help="Section identifier from thesis/outline.md, e.g. '2.1'."),
946-
thread: str = typer.Option(None, "--thread"),
947-
) -> None:
948-
"""Draft one thesis section, grounded in the wiki, in your style."""
949-
tid = thread or read_thread_id()
950-
write_thread_id(tid)
951-
_run(
952-
f"Invoke the thesis-writer skill to draft section '{section}' from "
953-
f"`thesis/outline.md`. Follow `style/STYLE.md` and cite every factual "
954-
f"claim with `[src:<raw_filename>]`. Write to `thesis/chapters/`.",
955-
tid,
956-
)
957-
958-
959-
@app.command()
960-
def lint(
961-
file: Path | None = typer.Argument(
962-
None,
963-
help="Chapter file to lint. If omitted, runs the wiki-linter over research/wiki/.",
964-
),
965-
citations: bool = typer.Option(
966-
False, "--citations",
967-
help="Force citation-only linting (ungrounded claims, dead [src:...] markers) even with no file argument.",
968-
),
969-
thread: str = typer.Option(None, "--thread"),
970-
) -> None:
971-
"""Lint the wiki (default) or a thesis chapter.
972-
973-
Without arguments → wiki health check (orphans, stale claims, missing
974-
cross-refs, data gaps, follow-up questions) via the `wiki-linter` skill.
975-
976-
With a chapter file or `--citations` → citation-only scan via the
977-
`citation-linter` skill (dead markers, ungrounded paragraphs).
978-
"""
979-
tid = thread or read_thread_id()
980-
write_thread_id(tid)
981-
982-
if file is not None:
983-
_run(
984-
f"Invoke the citation-linter skill on {file}. Report dead "
985-
f"citations and ungrounded paragraphs. Do not auto-edit.",
986-
tid,
987-
)
988-
return
989-
990-
if citations:
991-
_run(
992-
"Invoke the citation-linter skill on `thesis/chapters/*.md`. "
993-
"Report dead citations and ungrounded paragraphs. Do not auto-edit.",
994-
tid,
995-
)
996-
return
997-
998-
_run(
999-
"Invoke the wiki-linter skill. Scan `research/wiki/**` for "
1000-
"contradictions, stale claims, orphan pages, missing cross-refs, "
1001-
"missing entity/concept pages, and data gaps. Suggest 2-5 "
1002-
"follow-up questions. Append a lint entry to `research/wiki/log.md`. "
1003-
"Do not auto-fix.",
1004-
tid,
1005-
)
1006-
1007888

1008889
_CHAT_BANNER = """[bold]thesis-agent chat[/bold]
1009890
@@ -1294,7 +1175,10 @@ def _root(
12941175
console.print(f"thesis-agent {__version__}")
12951176
raise typer.Exit(0)
12961177
if ctx.invoked_subcommand is None:
1297-
console.print(ctx.get_help())
1178+
# Bare `thesis` drops straight into the agent chat — this is the
1179+
# primary interface. Use `thesis --help` for the command list,
1180+
# `thesis ingest` / `setup` / `init` / `status` for utilities.
1181+
ctx.invoke(chat)
12981182
raise typer.Exit(0)
12991183

13001184

0 commit comments

Comments
 (0)