Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions cowork/api/v1/endpoints/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ def list_projects(session: SessionDep):

@router.post("/", status_code=status.HTTP_201_CREATED)
def create_project(body: ProjectCreateRequest, session: SessionDep):
return ProjectService(session).create_project(body.name)
try:
return ProjectService(session).create_project(body.name, body.path, body.instructions)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))


@router.patch("/{project_id}")
def update_project(project_id: UUID, body: ProjectUpdateRequest, session: SessionDep):
try:
return ProjectService(session).update_project(
project_id, name=body.name, is_active=body.is_active
project_id, name=body.name, is_active=body.is_active, instructions=body.instructions
)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""add project instructions

Revision ID: 55a019954465
Revises: c4e7a1b9d2f0
Create Date: 2026-06-19 15:28:23.246712

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '55a019954465'
down_revision: Union[str, Sequence[str], None] = 'c4e7a1b9d2f0'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
op.add_column("projects", sa.Column("instructions", sa.Text(), nullable=True))


def downgrade() -> None:
"""Downgrade schema."""
op.drop_column("projects", "instructions")
2 changes: 2 additions & 0 deletions cowork/harnesses/anton_harness/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@ def _settings_path(value: object, fallback: Path) -> Path:
f"Your scratchpad's working directory is {str(base)} — bare relative paths like `open('data.csv')` resolve from the project root."
+ attachment_context
)
if conversation.project.instructions:
project_context += f"\n\nProject instructions: {conversation.project.instructions}"
output_context = (
# Artifacts now live in their own visible folder at the
# project root (`<base>/artifacts/<slug>/...`), one folder
Expand Down
11 changes: 7 additions & 4 deletions cowork/harnesses/hermes_harness/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,7 @@ def run_sync() -> dict:
str(conversation.id),
prompt,
history,
project_name=conversation.project.name,
project_path=project_path,
project=conversation.project,
conversation_topic=conversation_topic,
stream_callback=stream_callback,
tool_start_callback=tool_start_callback,
Expand Down Expand Up @@ -227,8 +226,7 @@ def _run(
prompt: str,
history: list[dict],
*,
project_name: str,
project_path: str,
project: Project,
conversation_topic: str | None = None,
stream_callback=None,
tool_start_callback=None,
Expand Down Expand Up @@ -257,6 +255,9 @@ def _run(
register_connector_tools()
register_artifact_tools()

project_path = project.path
project_name = project.name

# Same folder-per-artifact convention as the Anton harness, so
# Hermes outputs surface in the (harness-agnostic) Artifacts UI.
artifacts_root = Path(project_path) / ".anton" / "artifacts"
Expand Down Expand Up @@ -308,6 +309,8 @@ def _run(
"The only other files that you are allowed to access are any items that are attached to the conversation."
"Access to any files not attached to the conversation or located outside the project is strictly forbidden."
)
if project.instructions:
project_context += f"\n\nProject instructions: {project.instructions}"
datasource_context = _build_datasource_context(vault, disabled_keys)
memory_context = HermesMemoryAdapter().build_prompt_context(Path(project_path))
system_context = "\n\n".join(
Expand Down
2 changes: 1 addition & 1 deletion cowork/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ class Project(BaseSQLModel, table=True):
max_length=1024,
)
is_active: bool = Field(default=True, description="Whether the project is active")

instructions: str | None = Field(default=None, description="Instructions for the project")
11 changes: 10 additions & 1 deletion cowork/schemas/projects.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
from pydantic import BaseModel
from pathlib import Path
from pydantic import field_validator

from cowork.schemas.base import CamelRequest


class ProjectCreateRequest(CamelRequest):
name: str
path: Path | None = None
instructions: str | None = None

@field_validator("path")
@classmethod
def validate_path(cls, path: str | None) -> Path | None:
return Path(path) if path else None


class ProjectUpdateRequest(CamelRequest):
name: str | None = None
is_active: bool | None = None
instructions: str | None = None
30 changes: 11 additions & 19 deletions cowork/services/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,6 @@ def _root_dir(self) -> Path:
def _project_path(self, name: str) -> Path:
return self._root_dir() / name

# TODO: Move this. This should only be done when using Anton.
def _scaffold(self, target: Path) -> None:
anton_dir = target / ".anton"
anton_dir.mkdir(parents=True, exist_ok=True)
(anton_dir / "anton.md").touch()

def _unique_name(self, base: str, *, exclude: str | None = None) -> str:
existing = {
p.name for p in self.session.exec(select(Project)).all()
Expand Down Expand Up @@ -86,13 +80,13 @@ def get_project_by_name(self, name: str) -> Project:
def get_project_by_name_or_none(self, name: str) -> Project | None:
return self.session.exec(select(Project).where(Project.name == name)).first()

def create_project(self, name: str) -> Project:
def create_project(self, name: str, path: Path | None = None, instructions: str | None = None) -> Project:
sanitized = self._sanitize_name(name)
final_name = self._unique_name(sanitized)
path = self._project_path(final_name)
path.mkdir(parents=True)
# self._scaffold(path)
project = Project(name=final_name, path=str(path), is_active=False)
path = path or self._project_path(final_name)
path.mkdir(parents=True, exist_ok=True)

project = Project(name=final_name, path=str(path), is_active=False, instructions=instructions)
self.session.add(project)
self.session.commit()
self.session.refresh(project)
Expand All @@ -103,6 +97,7 @@ def update_project(
project_id: UUID,
name: str | None = None,
is_active: bool | None = None,
instructions: str | None = None,
) -> Project:
project = self.session.get(Project, project_id)
if project is None:
Expand All @@ -112,14 +107,8 @@ def update_project(
if project.name == GENERAL_PROJECT:
raise ValueError("Cannot rename the General project")
sanitized = self._sanitize_name(name)
final_name = self._unique_name(sanitized, exclude=project.name)
if final_name != project.name:
old_path = Path(project.path)
new_path = self._project_path(final_name)
if old_path.exists():
old_path.rename(new_path)
project.name = final_name
project.path = str(new_path)
final_name = self._unique_name(sanitized)
project.name = final_name

if is_active is not None:
if is_active:
Expand All @@ -129,6 +118,9 @@ def update_project(
self.session.add(other)
project.is_active = is_active

if instructions is not None:
project.instructions = instructions or None

self.session.add(project)
self.session.commit()
self.session.refresh(project)
Expand Down