Skip to content
Merged
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
10 changes: 1 addition & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,17 +228,9 @@ By default, Donna uses the next worlds:
- `project` — project-level artifacts in `<project-root>/.donna/project/` folder;
- `session` — session-level artifacts in `<project-root>/.donna/session/` folder.

Besides that, there is `<project-root>/.donna/tmp` folder used to store temporary files.

A world can be read-only. By default, writable worlds are `session` (current work scope) and `project` (project scope).

Agents are not allowed to edit artifacts directly because artifact consistency is important. Instead, they follow the next algorithm:

- Fetch an artifact into the temporary file with the command `donna -p llm artifacts fetch ...`.
- Edit the temporary file.
- Upload an artifact with the command `donna -p llm artifacts upload ...`.

On upload, Donna validates the artifact and accepts it only when there are no errors. For example, Donna will not accept a workflow that can not be finished.
Agents are not allowed to edit artifacts through Donna CLI directly because artifact consistency is important. Instead, they update the underlying files in writable worlds and rely on Donna validation when needed.

### Rendering

Expand Down
1 change: 1 addition & 0 deletions changes/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Removed `donna artifacts` mutation commands and the supporting artifact-mutation code paths.
- Removed `readonly` world-artifact mutability modeling from workspace config and world abstractions.
- Updated artifact and world usage specs to state that developers and external tools mutate world artifacts while Donna validates them.
- Removed `donna artifacts fetch` and `donna artifacts tmp` commands and all related code.
Comment on lines 10 to +12
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Config.tmp_dir was removed (see donna/workspaces/config.py), and BaseEntity forbids unknown fields; existing config.toml files that set tmp_dir will now fail validation at startup. Please add an explicit migration/breaking-change note here (or under "Breaking Changes") telling users to remove tmp_dir from their config and explaining that Donna no longer manages a workspace temp directory via config.

Copilot uses AI. Check for mistakes.

### Breaking Changes

Expand Down
6 changes: 2 additions & 4 deletions donna/artifacts/usage/artifacts.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ The text artifact has a source and one or more rendered representations, produce

To change the artifact, developers and agents edit its source.

When you need a scratch file for artifact-related work, use `donna -p <protocol> artifacts tmp '<slug>.<extension>'` to create a temporary file in the workspace temp directory.

To get information from the artifact, developers, agents and Donna view one of its representations (typically via the view rendering mode).

**If you need an information from the artifact, you MUST view its representation**. Artifact sources are only for editing.
Expand All @@ -46,7 +44,7 @@ Here are some examples:

Donna allows all of Jinja2 expressions in artifact sources, except inheritance-related once: `{{ "{% extends %}" }}` , `{{ "{% block %}" }}`, etc.

Donna intentionally hides some parts of the source in the rendered output, but they remain visible in source views (for example, in `artifacts fetch` output or on GitHub):
Donna intentionally hides some parts of the source in the rendered output, but they remain visible in the source files themselves (on filesystem):

- fenced code blocks with the `donna` marker (they contain technical information for the Donna, not information for the agent).
- Jinja2 comments like `{{ "{# ... #}" }}`.
Expand Down Expand Up @@ -100,7 +98,7 @@ The configuration block properties format is `property1 property2=value2 propert

The content of the block is parsed according to the primary format and interpreted according its properties.

Configuration blocks are parsed by Donna and removed from rendered Markdown representations (see "Jinja2 rendering"); they remain in the source for editing and inspection (e.g., via `artifacts fetch` or the repository file).
Configuration blocks are parsed by Donna and removed from rendered Markdown representations (see "Jinja2 rendering"); they remain in the source for editing and inspection on the file system.

Fences without `donna` keyword are considered regular code blocks and have no special meaning for Donna.

Expand Down
4 changes: 1 addition & 3 deletions donna/artifacts/usage/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ There are four sets of commands:

- `donna -p <protocol> workspaces …` — manages workspaces. Most-likely it will be used once per your project to initialize it.
- `donna -p <protocol> sessions …` — manages sessions. You will use these commands to start, push forward, and manage your work.
- `donna -p <protocol> artifacts …` — manages artifact discovery, reading, fetching, temporary files, and validation.
- `donna -p <protocol> artifacts …` — manages artifact discovery, reading, and validation.
- `donna -p <protocol> journal …` — manages session actions journal. You will use these commands to log and inspect the history of actions performed during the session.

Use:
Expand Down Expand Up @@ -144,8 +144,6 @@ Use the next commands to work with artifacts:

- `donna -p <protocol> artifacts list [<artifact-pattern>]` — list all artifacts corresponding to the given pattern. If `<artifact-pattern>` is omitted, list all artifacts in all worlds. Use this command when you need to find an artifact or see what artifacts are available.
- `donna -p <protocol> artifacts view <artifact-pattern>` — get the meaningful (rendered) content of all matching artifacts. This command shows the rendered information about each artifact. Use this command when you need to read artifact content.
- `donna -p <protocol> artifacts fetch <world>:<artifact>` — download the original source of the artifact content, outputs the file path to the artifact's copy, you can change. Use this command when you need to change the content of the artifact.
- `donna -p <protocol> artifacts tmp <slug>.<extension>` — create a temporary file for artifact-related work and output its path.
- `donna -p <protocol> artifacts validate [<artifact-pattern>]` — validate all artifacts corresponding to the given pattern. If `<artifact-pattern>` is omitted, validate all artifacts in all worlds.

Donna does not mutate artifacts stored in worlds. Developers and external tools are responsible for creating, updating, moving, copying, or deleting world artifacts before Donna reads or validates them.
Expand Down
59 changes: 1 addition & 58 deletions donna/cli/commands/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,24 @@

import typer

from donna.cli import errors as cli_errors
from donna.cli.application import app
from donna.cli.types import (
FullArtifactIdArgument,
FullArtifactIdPatternArgument,
OutputPathOption,
PredicateOption,
)
from donna.cli.utils import cells_cli
from donna.context.context import context
from donna.core.errors import ErrorsList
from donna.core.result import Err, Ok, Result
from donna.domain.ids import FullArtifactIdPattern
from donna.machine import journal as machine_journal
from donna.protocol.cell_shortcuts import operation_succeeded
from donna.protocol.cells import Cell
from donna.workspaces import tmp as world_tmp
from donna.workspaces.artifacts import RENDER_CONTEXT_VIEW

artifacts_cli = typer.Typer()

DEFAULT_ARTIFACT_PATTERN = FullArtifactIdPattern.parse("**").unwrap()


def _parse_slug_with_extension(value: str) -> Result[tuple[str, str], ErrorsList]:
normalized = value.strip()
if "." not in normalized:
return Err([cli_errors.InvalidSlugWithExtension(value=normalized)])

slug, extension = normalized.rsplit(".", 1)
if not slug or not extension:
return Err([cli_errors.InvalidSlugWithExtension(value=normalized)])

return Ok((slug, extension))


def _log_artifact_operation(message: str) -> None:
machine_journal.add(message=message)

Expand Down Expand Up @@ -80,45 +62,6 @@ def view(
return [artifact.node().info() for artifact in artifacts]


@artifacts_cli.command(
help=(
"Fetch an artifact source into a local file. When --output is omitted, "
"a temporary file will be created in the project's temp directory."
)
)
@cells_cli
def fetch(id: FullArtifactIdArgument, output: OutputPathOption = None) -> Iterable[Cell]:
if output is None:
extension = context().artifacts.file_extension(id).unwrap()
output = world_tmp.file_for_artifact(id, extension)

_log_artifact_operation(f"Fetch artifact `{id}` to '{output}'")

context().artifacts.fetch(id, output).unwrap()

return [
operation_succeeded(f"Artifact `{id}` fetched to '{output}'", artifact_id=str(id), output_path=str(output))
]


@artifacts_cli.command(help="Create a temporary file for artifact-related work and print its path.")
@cells_cli
def tmp(
slug_with_extension: str = typer.Argument(..., help="Temporary file slug with extension (example: 'draft.md').")
) -> Iterable[Cell]:
slug, extension = _parse_slug_with_extension(slug_with_extension).unwrap()
output = world_tmp.create_file_for_slug(slug, extension)

_log_artifact_operation(f"Created temporary file {output}")

return [
operation_succeeded(
f"Temporary file created at '{output}'",
output_path=str(output),
)
]


@artifacts_cli.command(help="Validate artifacts matching a pattern (defaults to all artifacts) and return any errors.")
@cells_cli
def validate(
Expand All @@ -145,5 +88,5 @@ def validate(
app.add_typer(
artifacts_cli,
name="artifacts",
help="Inspect, fetch, and validate stored artifacts across all Donna worlds.",
help="Inspect and validate stored artifacts across all Donna worlds.",
)
9 changes: 0 additions & 9 deletions donna/cli/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,3 @@ class InternalError(core_errors.InternalError):

class CliError(core_errors.EnvironmentError):
cell_kind: str = "cli_error"


class InvalidSlugWithExtension(CliError):
code: str = "donna.cli.invalid_slug_with_extension"
message: str = "Invalid slug with extension: '{error.value}'."
ways_to_fix: list[str] = [
"Use the format '<slug>.<extension>' (for example: 'draft.md').",
]
value: str
10 changes: 0 additions & 10 deletions donna/cli/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,16 +178,6 @@ def _parse_input_path(value: str) -> pathlib.Path:
]


OutputPathOption = Annotated[
pathlib.Path | None,
typer.Option(
resolve_path=True,
dir_okay=False,
file_okay=True,
help="Optional output file path (file only). Defaults to a temporary file if omitted.",
),
]

ProjectDirArgument = Annotated[
pathlib.Path | None,
typer.Argument(
Expand Down
14 changes: 0 additions & 14 deletions donna/context/artifacts.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from pathlib import Path
from typing import TYPE_CHECKING

from donna.context.entity_cache import TimedCache, TimedCacheValue
Expand Down Expand Up @@ -115,19 +114,6 @@ def resolve_section(
artifact = self.load(target_id.full_artifact_id, render_context).unwrap()
return Ok(artifact.get_section(target_id.local_id).unwrap())

@unwrap_to_error
def file_extension(self, full_id: FullArtifactId) -> Result[str, ErrorsList]:
from donna.workspaces import artifacts as workspace_artifacts

return Ok(workspace_artifacts.artifact_file_extension(full_id).unwrap())

@unwrap_to_error
def fetch(self, full_id: FullArtifactId, output: Path) -> Result[None, ErrorsList]:
from donna.workspaces import artifacts as workspace_artifacts

workspace_artifacts.fetch_artifact(full_id, output).unwrap()
return Ok(None)

@unwrap_to_error
def _list_artifact_if_matches(
self,
Expand Down
3 changes: 0 additions & 3 deletions donna/machine/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from donna.machine.state import ConsistentState, MutableState
from donna.protocol.cell_shortcuts import operation_succeeded
from donna.protocol.cells import Cell
from donna.workspaces import tmp as world_tmp
from donna.workspaces import utils as workspace_utils
from donna.workspaces.artifacts import RENDER_CONTEXT_VIEW

Expand Down Expand Up @@ -61,7 +60,6 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> CellsResult:

@unwrap_to_error
def start() -> Result[list[Cell], ErrorsList]:
world_tmp.clear()
workspace_utils.session_world().unwrap().initialize(reset=True).unwrap()

machine_journal.reset().unwrap()
Expand All @@ -80,7 +78,6 @@ def reset() -> Result[list[Cell], ErrorsList]:

@unwrap_to_error
def clear() -> Result[list[Cell], ErrorsList]:
world_tmp.clear()
workspace_utils.session_world().unwrap().initialize(reset=True).unwrap()
return Ok([operation_succeeded("Cleared session.")])

Expand Down
23 changes: 0 additions & 23 deletions donna/workspaces/artifacts.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import pathlib

from donna.core.entities import BaseEntity
from donna.core.errors import ErrorsList
from donna.core.result import Ok, Result, unwrap_to_error
from donna.domain.ids import FullArtifactId
from donna.machine.tasks import Task, WorkUnit
from donna.workspaces.config import config
from donna.workspaces.templates import RenderMode


Expand All @@ -16,20 +10,3 @@ class ArtifactRenderContext(BaseEntity):


RENDER_CONTEXT_VIEW = ArtifactRenderContext(primary_mode=RenderMode.view)


@unwrap_to_error
def artifact_file_extension(full_id: FullArtifactId) -> Result[str, ErrorsList]:
world = config().get_world(full_id.world_id).unwrap()
return Ok(world.file_extension_for(full_id.artifact_id).unwrap().lstrip("."))


@unwrap_to_error
def fetch_artifact(full_id: FullArtifactId, output: pathlib.Path) -> Result[None, ErrorsList]:
world = config().get_world(full_id.world_id).unwrap()
raw_artifact = world.fetch(full_id.artifact_id).unwrap()

with output.open("wb") as f:
f.write(raw_artifact.get_bytes())

return Ok(None)
1 change: 0 additions & 1 deletion donna/workspaces/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ class Config(BaseEntity):
_worlds_instances: list[BaseWorld] = pydantic.PrivateAttr(default_factory=list)
_sources_instances: list[SourceConfigValue] = pydantic.PrivateAttr(default_factory=list)

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing tmp_dir from Config will cause existing config.toml files that still include tmp_dir to fail validation because BaseEntity forbids extra fields. If this is intended, it should be treated as a breaking change (with explicit migration notes). If not intended, consider keeping tmp_dir as a deprecated/ignored field to preserve backward compatibility.

Suggested change
# Deprecated: kept for backward compatibility with existing config files.
tmp_dir: pathlib.Path | None = None

Copilot uses AI. Check for mistakes.
tmp_dir: pathlib.Path = pathlib.Path("./tmp")
cache_lifetime: float = 1.0

def model_post_init(self, __context: Any) -> None: # noqa: CCR001
Expand Down
51 changes: 0 additions & 51 deletions donna/workspaces/tmp.py

This file was deleted.

3 changes: 0 additions & 3 deletions donna/workspaces/worlds/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@ def fetch(self, artifact_id: ArtifactId) -> Result[RawArtifact, ErrorsList]: ...
def has_artifact_changed(self, artifact_id: ArtifactId, since: Milliseconds) -> Result[bool, ErrorsList]:
pass

@abstractmethod
def file_extension_for(self, artifact_id: ArtifactId) -> Result[str, ErrorsList]: ... # noqa: E704

@abstractmethod
def list_artifacts(self, pattern: FullArtifactIdPattern) -> list[ArtifactId]: ... # noqa: E704

Expand Down
8 changes: 0 additions & 8 deletions donna/workspaces/worlds/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,6 @@ def has_artifact_changed(self, artifact_id: ArtifactId, since: Milliseconds) ->

return Ok((path.stat().st_mtime_ns // 1_000_000) > since)

@unwrap_to_error
def file_extension_for(self, artifact_id: ArtifactId) -> Result[str, ErrorsList]:
path = self._resolve_artifact_file(artifact_id).unwrap()
if path is None:
return Err([world_errors.ArtifactNotFound(artifact_id=artifact_id, world_id=self.id)])

return Ok(path.suffix)

def read_state(self, name: str) -> Result[bytes | None, ErrorsList]:
if not self.session:
return Err([world_errors.WorldStateStorageUnsupported(world_id=self.id)])
Expand Down
8 changes: 0 additions & 8 deletions donna/workspaces/worlds/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,6 @@ def fetch(self, artifact_id: ArtifactId) -> Result[RawArtifact, ErrorsList]: #
def has_artifact_changed(self, artifact_id: ArtifactId, since: Milliseconds) -> Result[bool, ErrorsList]:
return Ok(False)

@unwrap_to_error
def file_extension_for(self, artifact_id: ArtifactId) -> Result[str, ErrorsList]:
resource_path = self._resolve_artifact_file(artifact_id).unwrap()
if resource_path is None:
return Err([world_errors.ArtifactNotFound(artifact_id=artifact_id, world_id=self.id)])

return Ok(pathlib.Path(resource_path.name).suffix)

def list_artifacts(self, pattern: FullArtifactIdPattern) -> list[ArtifactId]: # noqa: CCR001
return list_artifacts_by_pattern(
world_id=self.id,
Expand Down
Loading