Skip to content

feat(workspace): per-topic project roots as CLI working directory#178

Open
minakovd wants to merge 1 commit into
PleasePrompto:mainfrom
minakovd:feat/project-roots
Open

feat(workspace): per-topic project roots as CLI working directory#178
minakovd wants to merge 1 commit into
PleasePrompto:mainfrom
minakovd:feat/project-roots

Conversation

@minakovd

Copy link
Copy Markdown

Use case

Forum topics already give each conversation an isolated CLI context, but every topic shares the same ~/.ductor/workspace cwd. With Claude Code in particular that leaves a lot on the table: CLAUDE.md, .claude/agents, .claude/skills, and .mcp.json all resolve from the CLI's working directory. If a topic is a project ("Paper X", "MyProject"), starting the CLI in that project's directory makes the agent pick up the project's own instructions, subagents, skills, and MCP servers with zero extra configuration.

This PR adds an opt-in project_roots config map from topic to directory:

"project_roots": {
  "MyProject": "~/projects/my-project",
  "-100123456789:19": "~/projects/my-paper"
}

Keys are matched by topic name first, then by <chat_id>:<topic_id>, then by bare <topic_id>.

Design highlights

  • Central resolver in CLIService._make_cli. The working-dir override is applied at the single choke point where every CLI instance is built, so --resume stays consistent across all paths that spawn a CLI for the same session — normal messages, memory flush, heartbeat, and injection. Resolving anywhere higher up would risk resuming a session from a different cwd than it was created in.
  • Docker guard. In Docker mode the override is skipped entirely: docker_wrap maps cwd into the container via relative_to() and would fail on paths outside the workspace.
  • Named sessions excluded. ns:-labelled sessions keep the default workspace so their resume behavior is unchanged.
  • Hot-reloadable. project_roots is listed in _HOT_RELOADABLE; config edits apply without a restart. Entries pointing at non-existent directories are skipped with a warning.
  • Zero-cost when unused. An empty/absent map means the resolver returns None for every request — behavior is byte-identical to current main.
  • When the override applies, a short system-prompt note points the agent back at the shared workspace (memory_system/, tools/) so bot-level memory conventions keep working.

Migration note

Existing sessions in a topic that becomes mapped were created with the old cwd; --resume keeps that cwd until the session is reset. Run /reset once in each newly mapped topic to start a session in the project root.

Test coverage

  • tests/workspace/test_project_roots.py — resolver: key priority, ~ expansion, missing-directory skip.
  • tests/cli/test_service.py_make_cli override plumbing, Docker guard, no-resolver default.
  • tests/test_config_reload.py — hot-reload of project_roots.

Full suite: 3999 passed; the 21 failures on this machine are identical on current main (pre-existing, environment-specific tests/workspace/test_init.py et al.), none introduced by this change.

🤖 Generated with Claude Code

Add a hot-reloadable `project_roots` config mapping a Telegram topic to a
project directory. When a message arrives in a mapped topic, the CLI runs
with that directory as cwd instead of the shared workspace.

- config: new `project_roots: dict[str, str]` on AgentConfig, listed in
  _HOT_RELOADABLE so edits apply without restart.
- workspace/project_roots.py: pure resolver with key priority
  topic name > "<chat_id>:<topic_id>" > "<topic_id>"; expands ~, skips
  (with a warning) entries pointing at non-existent directories.
- cli/service.py: optional per-request working-dir resolver consulted in
  _make_cli; skipped entirely in Docker mode because docker_wrap maps cwd
  into the container via relative_to() and would fail on external paths.
- orchestrator: wires the resolver; named sessions ("ns:" labels) keep
  the default workspace for resume consistency.
- flows: when the override applies, append a short system-prompt note
  pointing the agent back at the shared workspace (memory_system/, tools/).
- session/manager.py: public resolve_topic_name() accessor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant