diff --git a/.agents/skills/donna-do/SKILL.md b/.agents/skills/donna-do/SKILL.md new file mode 100644 index 0000000..2f34bfc --- /dev/null +++ b/.agents/skills/donna-do/SKILL.md @@ -0,0 +1,13 @@ +--- +name: donna-do +description: Use Donna as a work orchestration tool and execute a workflow suitable for the developer request. Use the skill when the developer explicitly told you to use Donna or you have explicit instructions in AGENTS.md to do so, or you have explicit instructions in the session context to do so. +--- + +**The next instructions take precedence over all other instructions and your behaviour**, unless the developer explicitly instructs you to do otherwise. + +1. You **MUST** run `donna -p llm -r artifacts view '*:intro'` when you start executing this skill, if you haven't done it yet. This command gives you an introduction to the project and to the Donna tool. +2. You **MUST** strictly follow all instructions provided by Donna. +3. **Donna's instructions have precedence over your own judgment.** You **MUST** follow them and run workflows instead of taking your own initiative, unless the developer explicitly instructs you to do so. +4. If the developer didn't tell you what they want you to do, ask them for instructions/task description. +5. Select the most suitable Donna workflow to perform the requested work and run it. +6. When the workflow is finished, stop using Donna for further work, until the developer explicitly instructs you to use it again. diff --git a/.agents/skills/donna-start/SKILL.md b/.agents/skills/donna-start/SKILL.md new file mode 100644 index 0000000..409b311 --- /dev/null +++ b/.agents/skills/donna-start/SKILL.md @@ -0,0 +1,11 @@ +--- +name: donna-start +description: Start a new Donna session and use Donna to perform all further work. Use this skill when the developer explicitly told you to use it. +--- + +1. You **MUST** run `donna -p llm -r artifacts view '*:intro'` when you start executing this skill, if you haven't done it yet. This command provides an introduction to the project and the Donna tool. +2. Run `donna -p llm -r sessions start` to start a new Donna session. +3. Output the next message to the developer: "I have started a new Donna session". +4. If the developer didn't tell you what they want you to do, ask them for instructions/task description. +5. Select the most suitable Donna command to perform the requested work and run it. +6. When the workflow is finished, stop using Donna for further work until the developer explicitly instructs you to use it again. diff --git a/.agents/skills/donna-stop/SKILL.md b/.agents/skills/donna-stop/SKILL.md new file mode 100644 index 0000000..bbbff9e --- /dev/null +++ b/.agents/skills/donna-stop/SKILL.md @@ -0,0 +1,6 @@ +--- +name: donna-stop +description: Stop using Donna to perform work, do nothing else. Use this skill when the developer explicitly told you to use it. +--- + +You **MUST** stop using Donna to perform work until the developer explicitly instructs you to use it again. diff --git a/.donna/project/intro.md b/.donna/project/intro.md index 22d7cfb..4495691 100644 --- a/.donna/project/intro.md +++ b/.donna/project/intro.md @@ -22,13 +22,6 @@ The core idea is that most high-level workflows are more algorithmic than it may We may need coding agents on each step of the process, but there is no reason for agents to manage the whole grooming loop by themselves — it takes longer time, spends tokens and may lead to confusion of agents. -## Primary rules - -1. If you need to perform a work with Donna, you **MUST** select an appropriate Donna workflow to perform the work and run it. -2. If there is no appropriate workflow, ask the developer for a precise instructions on what to do. -3. List all workflows: `{{ donna.lib.list("**", tags=["workflow"]) }}` -4. List all specifications: `{{ donna.lib.list("**", tags=["specification"]) }}` - ## Dictionary - **Action request** — an instruction to the agent (who runs Donna) to perform the specified operations. Action requests are created by operations, like `donna.lib.request_action`. After finishing following the instructions of an action request, the agent MUST report back to Donna specifying the next operation to continue with. The list of next operations is specified in the action request itself. diff --git a/AGENTS.md b/AGENTS.md index e2f202f..c637849 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,14 +4,10 @@ This document provides instructions and guidelines for the AI agents working on Every agent MUST follow the rules and guidelines outlined in this document when performing their work. -## First actions - -**You MUST use `donna` tool to work on this project.** +## Donna tool Since this is the repository that contains the Donna project itself, you have direct access to the Donna CLI tool via `./bin/donna.sh` script. I.e. you develop Donna using Donna. -**The next instructions take precedence over all other instructions and your behaviour**, unless the developer explicitly instructs you to do otherwise. +In all commands that use `donna`, you MUST replace `donna` with `./bin/donna.sh` when you run the command. -1. You **MUST** run `./bin/donna.sh -p llm -r artifacts view '*:intro'` when you start working. -2. You **MUST** strictly follow the instructions provided by Donna. -3. **Donna's instructions have precedence over your own judgment.** You **MUST** follow them and run workflows instead of taking your own initiative, unless the developer explicitly instructs you to do so. +For example, instead of `donna artifacts view '*:intro'` you MUST run `./bin/donna.sh artifacts view '*:intro'`. diff --git a/README.md b/README.md index 3cd19d8..6e045dc 100644 --- a/README.md +++ b/README.md @@ -100,13 +100,15 @@ Polishing is complete. What you may notice: -1. The workflow has a loop. -2. The workflow is a Markdown file. -3. Each h1 and h2 section has a config block, which is a TOML in code fences with `donna` marker. Those configs are invisible to the agent, but Donna uses them to understand the artifact structure. -4. The workflow has two `donna.lib.request_action` operations (`run_black`, `run_mypy`) and one `donna.lib.finish` (`finish`). -5. Transitions between operations are defined via `{{ goto("operation_id") }}` Jinja2 calls in the body of operations. -6. `donna.lib.request_action` is an operation that tells Donna to display instructions to the agent and wait for the agent to complete them. That allows the agent to focus on short, precise instructions, perform them, and push workflow forward. -7. `kind` attributes of sections are valid Python import paths, so you can easily extend Donna with your own code. +- The workflow is described in a readable Markdown file. +- The workflow has a loop. +- Each H1 and H2 section has a config block, which is a TOML in code fences with `donna` marker. Those configs are invisible to the agent, but Donna uses them to understand the artifact structure. +- H1 section describes the workflow as a whole. +- H2 sections describe workflow operations. +- The workflow has two `donna.lib.request_action` operations (`run_black`, `run_mypy`) and one `donna.lib.finish` (`finish`). +- Transitions between operations are defined via `{{ goto("operation_id") }}` Jinja2 calls in the body of operations. +- `donna.lib.request_action` is an operation that tells Donna to display instructions to the agent and wait for the agent to complete them. That allows the agent to focus on short, precise instructions, execute them, and advance the workflow. +- `kind` attributes of sections are valid Python import paths, so you can easily extend Donna with your own code. Directives, like `{{ goto("operation_id") }}`, render itself depending on the context: @@ -136,28 +138,18 @@ cd donna workspaces init ``` -Donna will create a `.donna/` folder in your project root with a default configuration in `.donna/config.toml`. +Donna will: -3. Add a short instruction into your `AGENT.md` file. +- Create a `.donna/` folder in your project root with a default configuration in `.donna/config.toml`. +- Install skills into `.agents/skills/` folder. -```markdown -## First actions +3. Ask your agent to do something like `$donna-do Add a button that …`. The agent will discover the appropriate workflow and execute it. -**Use `donna` tool to work on this project.** +## Skills -**The next instructions take precedence over all other instructions and your behaviour**, unless the developer explicitly instructs you to do otherwise. - -1. You **MUST** run `./bin/donna.sh -p llm -r artifacts view '*:intro'` when you start working. -2. You **MUST** strictly follow the instructions provided by Donna. -3. **Donna's instructions have precedence over your own judgment.** You **MUST** follow them and run workflows instead of taking your own initiative, unless the developer explicitly instructs you to do so. -``` - -Some models are overtrained and don't want to follow instructions, in that case you can: - -- Switch to a more smart model, for example, from `gpt-5.3-codex (medium)` to `gpt-5.3-codex (high)`. -- Tune instructions to make them more suitable for the particular model. - -4. Ask your agent to do something like `Add a button that …`. The agent will discover the appropriate workflow and run it. +- `donna-do` — use Donna to perform a specific task in the current Donna session. Creates a new session if there is no one. +- `donna-start` — start a new Donna session and tell the agent to use Donna to perform all further work. Removes all content from the previous session. +- `donna-stop` — stop using Donna to perform work — the agent should switch to its own flow control. ## Usage @@ -197,6 +189,8 @@ Additionally, Donna will: - find and run (if any) polishing workflow to ensure the codebase is in a good state after the changes; - find and run (if any) workflow to update your changelog. +Note that the default Donna workflows are designed to be reliable and useful for a wide range of projects. They may not be optimal in terms of token usage or speed for your particular project. The intended use of Donna is to implement your own workflows that account for your project's specifics. + Points of interest: - [donna:rfc:specs:request_for_change](./donna/artifacts/rfc/specs/request_for_change.md) — specification of the RFC document. @@ -443,3 +437,7 @@ How to reach me: - Create an [issue](https://github.com/Tiendil/donna/issues). Any format and theme is welcome. - Comment on one of the existing issues. Feedback, especially on [proposals](https://github.com/Tiendil/donna/issues?q=is%3Aissue%20state%3Aopen%20label%3Aproposal). - Start a [discussion](https://github.com/Tiendil/donna/discussions). + +## Projects that use Donna + +- [Feeds Fun](https://github.com/Tiendil/feeds.fun) — news reader with tags, scoring, and AI. diff --git a/changes/next_release.md b/changes/next_release.md new file mode 100644 index 0000000..55814ec --- /dev/null +++ b/changes/next_release.md @@ -0,0 +1,10 @@ +### Changes + +- Improved template for RFC draft artifact — added the `kind` config to the header section. +- Added `donna workspaces update` command to update project workspace after Donna is updated. +- Added workspace skill synchronization for `donna workspaces init` and `donna workspaces update`. + - Both commands now install donna skills `donna-do`, `donna-start` and `donna-stop` into `.agents/skills/` directory. + - Added `--no-skilks` option for `init` and `update` to skip skill updates. +- Added stdin and extension-aware updates for `donna artifacts update`. + - Added `--extension` option to explicitly select artifact source extension. + - Added `-` stdin input support. diff --git a/changes/unreleased.md b/changes/unreleased.md deleted file mode 100644 index ef13857..0000000 --- a/changes/unreleased.md +++ /dev/null @@ -1,2 +0,0 @@ - -- Improved template for RFC draft artifact — added the `kind` config to the header section. diff --git a/docker-compose.yml b/docker-compose.yml index fa85d9b..45a5721 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,4 +8,4 @@ services: dockerfile: ./docker/Dockerfile volumes: - - ${PWD}/donna:/repository/donna + - ${PWD}:/repository diff --git a/donna/artifacts/intro.md b/donna/artifacts/intro.md index 4f28492..8caf4d8 100644 --- a/donna/artifacts/intro.md +++ b/donna/artifacts/intro.md @@ -28,13 +28,11 @@ Artifact type tags: - `workflow` — workflow artifact — is set automatically by Donna. - `specification` — specification artifact — is set automatically by Donna. -{# We recommend using those tags in `project:` and `session:` worlds as well to keep consistency. #} - ## Instructions 1. On start of the YOUR session you **MUST** read and understand instruction on using the Donna tool `{{ donna.lib.view("donna:usage:cli") }}`. It **MUST** be a one time operation. Do not repeat it unless you forget how to use the tool. 2. If you need to perform a work with Donna, you **MUST** select an appropriate Donna workflow to perform the work and run it. 3. If there is no appropriate workflow, ask the developer for a precise instructions on what to do. -4. If you are executing a workflow operation and need to perform a complex action or changes, you SHOULD search for an appropriate workflow and run it (as a child workflow) — it is the intended way to use Donna. -5. List all workflows: `{{ donna.lib.list("**", tags=["workflow"]) }}` -6. List all specifications: `{{ donna.lib.list("**", tags=["specification"]) }}` +4. If you are executing a workflow operation and need to perform a complex action or changes, you SHOULD search for an appropriate workflow and run it as a child workflow — it is the intended way to use Donna. +5. Run to list all workflows: `{{ donna.lib.list("**", tags=["workflow"]) }}` +6. Run to list all specifications: `{{ donna.lib.list("**", tags=["specification"]) }}` diff --git a/donna/artifacts/usage/cli.md b/donna/artifacts/usage/cli.md index f7a4ae5..6d71775 100644 --- a/donna/artifacts/usage/cli.md +++ b/donna/artifacts/usage/cli.md @@ -145,7 +145,7 @@ Use the next commands to work with artifacts: - `donna -p artifacts view ` — 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 artifacts fetch :` — 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 artifacts tmp .` — create a temporary file for artifact-related work and output its path. -- `donna -p artifacts update : ` — upload the given file as the artifact. Use this command when you finished changing the content of the artifact. +- `donna -p artifacts update : [--extension ]` — upload content from a file path or from stdin (`-`) as the artifact. If `--extension` is omitted, Donna infers it from the existing target artifact. - `donna -p artifacts copy ` — copy an artifact source to another artifact ID (can be in a different world). This overwrites the destination if it exists. - `donna -p artifacts move ` — copy an artifact source to another artifact ID and remove the original. This overwrites the destination if it exists. - `donna -p artifacts remove ` — remove artifacts matching a pattern. Use this command when you need to delete artifacts. diff --git a/donna/cli/commands/artifacts.py b/donna/cli/commands/artifacts.py index 19c47d2..1c10d34 100644 --- a/donna/cli/commands/artifacts.py +++ b/donna/cli/commands/artifacts.py @@ -1,4 +1,6 @@ import builtins +import pathlib +import sys from collections.abc import Iterable import typer @@ -6,6 +8,7 @@ from donna.cli import errors as cli_errors from donna.cli.application import app from donna.cli.types import ( + ExtensionOption, FullArtifactIdArgument, FullArtifactIdPatternArgument, InputPathArgument, @@ -98,11 +101,30 @@ def tmp( ] -@artifacts_cli.command(help="Create or replace the artifact with the contents of a file.") +@artifacts_cli.command(help="Create or replace an artifact from a file path or stdin.") @cells_cli -def update(id: FullArtifactIdArgument, input: InputPathArgument) -> Iterable[Cell]: - world_artifacts.update_artifact(id, input).unwrap() - return [operation_succeeded(f"Artifact `{id}` updated from '{input}'", artifact_id=str(id), input_path=str(input))] +def update( + id: FullArtifactIdArgument, + input: InputPathArgument, + extension: ExtensionOption = None, +) -> Iterable[Cell]: + if input == pathlib.Path("-"): + tmp_extension = extension or "tmp" + input_path = world_tmp.file_for_artifact(id, tmp_extension) + input_path.write_bytes(sys.stdin.buffer.read()) + input_display = "stdin" + else: + input_path = input + input_display = str(input) + + world_artifacts.update_artifact(id, input_path, extension=extension).unwrap() + return [ + operation_succeeded( + f"Artifact `{id}` updated from '{input_display}'", + artifact_id=str(id), + input_path=str(input_path), + ) + ] @artifacts_cli.command(help="Copy an artifact to another artifact ID (possibly across worlds).") diff --git a/donna/cli/commands/workspaces.py b/donna/cli/commands/workspaces.py index d513a16..53367e8 100644 --- a/donna/cli/commands/workspaces.py +++ b/donna/cli/commands/workspaces.py @@ -4,28 +4,43 @@ import typer from donna.cli.application import app +from donna.cli.types import SkillsOption from donna.cli.utils import cells_cli from donna.protocol.cell_shortcuts import operation_succeeded from donna.protocol.cells import Cell from donna.workspaces import config as workspace_config -from donna.workspaces.initialization import initialize_workspace +from donna.workspaces.initialization import initialize_workspace, update_workspace workspaces_cli = typer.Typer() +def _resolve_target_dir() -> pathlib.Path: + if workspace_config.project_dir.is_set(): + return workspace_config.project_dir() + + return pathlib.Path.cwd() + + @workspaces_cli.command(help="Initialize Donna workspace.") @cells_cli -def init() -> Iterable[Cell]: - if workspace_config.project_dir.is_set(): - target_dir = workspace_config.project_dir() - else: - target_dir = pathlib.Path.cwd() +def init(skills: SkillsOption = True) -> Iterable[Cell]: + target_dir = _resolve_target_dir() - initialize_workspace(target_dir).unwrap() + initialize_workspace(target_dir, install_skills=skills).unwrap() return [operation_succeeded("Workspace initialized successfully")] +@workspaces_cli.command(help="Update Donna workspace files.") +@cells_cli +def update(skills: SkillsOption = True) -> Iterable[Cell]: + target_dir = _resolve_target_dir() + + update_workspace(target_dir, install_skills=skills).unwrap() + + return [operation_succeeded("Workspace updated successfully")] + + app.add_typer( workspaces_cli, name="workspaces", diff --git a/donna/cli/types.py b/donna/cli/types.py index e8fb766..d16ad39 100644 --- a/donna/cli/types.py +++ b/donna/cli/types.py @@ -1,4 +1,5 @@ import pathlib +import re from typing import Annotated import typer @@ -55,6 +56,36 @@ def _parse_protocol_mode(value: str) -> Mode: raise typer.BadParameter(f"Unsupported protocol mode '{value}'. Expected one of: {allowed}.") from exc +def _parse_extension(value: str) -> str: + normalized = value.strip().lower().lstrip(".") + if not normalized: + raise typer.BadParameter("Extension must not be empty.") + + if re.fullmatch(r"[a-z0-9][a-z0-9_-]*", normalized) is None: + raise typer.BadParameter( + "Invalid extension format. Use letters, digits, underscore, and dash (for example: md, yaml)." + ) + + return normalized + + +def _parse_input_path(value: str) -> pathlib.Path: + normalized = value.strip() + if normalized == "-": + return pathlib.Path("-") + + path = pathlib.Path(normalized).expanduser() + if not path.exists(): + raise typer.BadParameter(f"Input path '{value}' does not exist.") + if not path.is_file(): + raise typer.BadParameter(f"Input path '{value}' is not a file.") + + if not path.is_absolute(): + path = path.resolve() + + return path + + ActionRequestIdArgument = Annotated[ ActionRequestId, typer.Argument( @@ -126,16 +157,33 @@ def _parse_protocol_mode(value: str) -> Mode: ), ] +SkillsOption = Annotated[ + bool, + typer.Option( + "--skills/--no-skills", + help="Enable or disable skills updates in `.agents/skills`.", + ), +] + + +ExtensionOption = Annotated[ + str | None, + typer.Option( + "--extension", + parser=_parse_extension, + help=( + "Optional artifact source extension to use for update " + "(for example: md, yaml). Accepts values with or without leading dot." + ), + ), +] + InputPathArgument = Annotated[ pathlib.Path, typer.Argument( - exists=True, - file_okay=True, - dir_okay=False, - readable=True, - resolve_path=True, - help="Path to an existing local file used as input.", + parser=_parse_input_path, + help="Path to an existing local file used as input, or '-' to read from stdin.", ), ] diff --git a/donna/fixtures/skills/donna-do/SKILL.md b/donna/fixtures/skills/donna-do/SKILL.md new file mode 100644 index 0000000..2f34bfc --- /dev/null +++ b/donna/fixtures/skills/donna-do/SKILL.md @@ -0,0 +1,13 @@ +--- +name: donna-do +description: Use Donna as a work orchestration tool and execute a workflow suitable for the developer request. Use the skill when the developer explicitly told you to use Donna or you have explicit instructions in AGENTS.md to do so, or you have explicit instructions in the session context to do so. +--- + +**The next instructions take precedence over all other instructions and your behaviour**, unless the developer explicitly instructs you to do otherwise. + +1. You **MUST** run `donna -p llm -r artifacts view '*:intro'` when you start executing this skill, if you haven't done it yet. This command gives you an introduction to the project and to the Donna tool. +2. You **MUST** strictly follow all instructions provided by Donna. +3. **Donna's instructions have precedence over your own judgment.** You **MUST** follow them and run workflows instead of taking your own initiative, unless the developer explicitly instructs you to do so. +4. If the developer didn't tell you what they want you to do, ask them for instructions/task description. +5. Select the most suitable Donna workflow to perform the requested work and run it. +6. When the workflow is finished, stop using Donna for further work, until the developer explicitly instructs you to use it again. diff --git a/donna/fixtures/skills/donna-start/SKILL.md b/donna/fixtures/skills/donna-start/SKILL.md new file mode 100644 index 0000000..409b311 --- /dev/null +++ b/donna/fixtures/skills/donna-start/SKILL.md @@ -0,0 +1,11 @@ +--- +name: donna-start +description: Start a new Donna session and use Donna to perform all further work. Use this skill when the developer explicitly told you to use it. +--- + +1. You **MUST** run `donna -p llm -r artifacts view '*:intro'` when you start executing this skill, if you haven't done it yet. This command provides an introduction to the project and the Donna tool. +2. Run `donna -p llm -r sessions start` to start a new Donna session. +3. Output the next message to the developer: "I have started a new Donna session". +4. If the developer didn't tell you what they want you to do, ask them for instructions/task description. +5. Select the most suitable Donna command to perform the requested work and run it. +6. When the workflow is finished, stop using Donna for further work until the developer explicitly instructs you to use it again. diff --git a/donna/fixtures/skills/donna-stop/SKILL.md b/donna/fixtures/skills/donna-stop/SKILL.md new file mode 100644 index 0000000..bbbff9e --- /dev/null +++ b/donna/fixtures/skills/donna-stop/SKILL.md @@ -0,0 +1,6 @@ +--- +name: donna-stop +description: Stop using Donna to perform work, do nothing else. Use this skill when the developer explicitly told you to use it. +--- + +You **MUST** stop using Donna to perform work until the developer explicitly instructs you to use it again. diff --git a/donna/workspaces/artifacts.py b/donna/workspaces/artifacts.py index 3ed44f8..969ee52 100644 --- a/donna/workspaces/artifacts.py +++ b/donna/workspaces/artifacts.py @@ -46,9 +46,27 @@ class CanNotRemoveReadonlyWorld(ArtifactRemoveError): world_id: WorldId -class InputPathHasNoExtension(ArtifactUpdateError): - code: str = "donna.workspaces.input_path_has_no_extension" - message: str = "Input path has no extension to determine artifact source type" +class ArtifactExtensionCannotBeInferred(ArtifactUpdateError): + code: str = "donna.workspaces.artifact_extension_cannot_be_inferred" + message: str = "Cannot infer artifact extension. Provide `--extension` or update an existing artifact." + ways_to_fix: list[str] = [ + "Pass `--extension ` when updating the artifact.", + "Create/update an artifact that already exists and has a known extension.", + ] + + +class ArtifactExtensionMismatch(ArtifactUpdateError): + code: str = "donna.workspaces.artifact_extension_mismatch" + message: str = ( + "Provided extension `{error.provided_extension}` does not match existing artifact extension " + "`{error.existing_extension}`" + ) + provided_extension: str + existing_extension: str + ways_to_fix: list[str] = [ + "Use the existing artifact extension.", + "Omit `--extension` to use extension inferred from the existing artifact.", + ] class NoSourceForArtifactExtension(ArtifactUpdateError): @@ -95,28 +113,54 @@ def fetch_artifact(full_id: FullArtifactId, output: pathlib.Path) -> Result[None @unwrap_to_error -def update_artifact(full_id: FullArtifactId, input: pathlib.Path) -> Result[None, ErrorsList]: +def update_artifact( # noqa: CCR001 + full_id: FullArtifactId, input: pathlib.Path, extension: str | None = None +) -> Result[None, ErrorsList]: world = config().get_world(full_id.world_id).unwrap() if world.readonly: return Err([CanNotUpdateReadonlyWorld(artifact_id=full_id, path=input, world_id=world.id)]) - source_suffix = input.suffix.lower() content_bytes = input.read_bytes() - if not source_suffix: - return Err([InputPathHasNoExtension(artifact_id=full_id, path=input)]) + expected_extension = artifact_file_extension(full_id).unwrap_or(None) - source_config = config().find_source_for_extension(source_suffix) + requested_extension = extension.lstrip(".").lower() if extension is not None else None + + if expected_extension is None and requested_extension is None: + return Err([ArtifactExtensionCannotBeInferred(artifact_id=full_id, path=input)]) + + if expected_extension is None and requested_extension is not None: + source_suffix = requested_extension + elif expected_extension is not None and requested_extension is None: + source_suffix = expected_extension + elif expected_extension != requested_extension: + return Err( + [ + ArtifactExtensionMismatch( + artifact_id=full_id, + path=input, + provided_extension=requested_extension or "", + existing_extension=expected_extension or "", + ) + ] + ) + else: + assert expected_extension is not None + source_suffix = expected_extension + + normalized_source_suffix = f".{source_suffix}" + + source_config = config().find_source_for_extension(normalized_source_suffix) if source_config is None: - return Err([NoSourceForArtifactExtension(artifact_id=full_id, path=input, extension=source_suffix)]) + return Err([NoSourceForArtifactExtension(artifact_id=full_id, path=input, extension=normalized_source_suffix)]) render_context = ArtifactRenderContext(primary_mode=RenderMode.view) test_artifact = source_config.construct_artifact_from_bytes(full_id, content_bytes, render_context).unwrap() validation_result = test_artifact.validate_artifact() validation_result.unwrap() - world.update(full_id.artifact_id, content_bytes, source_suffix).unwrap() + world.update(full_id.artifact_id, content_bytes, normalized_source_suffix).unwrap() return Ok(None) diff --git a/donna/workspaces/initialization.py b/donna/workspaces/initialization.py index d04952f..08e25f9 100644 --- a/donna/workspaces/initialization.py +++ b/donna/workspaces/initialization.py @@ -1,4 +1,6 @@ +import importlib.resources import pathlib +import shutil import tomllib import tomli_w @@ -11,6 +13,28 @@ from donna.workspaces import config from donna.workspaces import errors as world_errors +SKILLS_ROOT_DIR = pathlib.Path(".agents") / "skills" +DONNA_SKILL_FIXTURE_DIR = pathlib.Path("fixtures") / "skills" + +# this list must only increase in size, +# do not remove old items from it, since users may upgrade from older versions of Donna +# where these skills were installed +DONNA_SKILL_CLEANUP_LIST = ["donna-do", "donna-start", "donna-stop"] + + +def _sync_donna_skill(project_dir: pathlib.Path) -> None: + source = importlib.resources.files("donna").joinpath(*DONNA_SKILL_FIXTURE_DIR.parts) + + # cleanup + for skill_id in DONNA_SKILL_CLEANUP_LIST: + target_dir = project_dir / SKILLS_ROOT_DIR / skill_id + if target_dir.exists(): + shutil.rmtree(target_dir) + + # copy all content of fixtures/skills to the skills directory + with importlib.resources.as_file(source) as source_dir: + shutil.copytree(source_dir, project_dir / SKILLS_ROOT_DIR, dirs_exist_ok=True) + @unwrap_to_error def initialize_runtime( # noqa: CCR001 @@ -59,7 +83,9 @@ def initialize_runtime( # noqa: CCR001 @unwrap_to_error -def initialize_workspace(project_dir: pathlib.Path) -> Result[None, core_errors.ErrorsList]: +def initialize_workspace( + project_dir: pathlib.Path, install_skills: bool = True +) -> Result[None, core_errors.ErrorsList]: """Initialize the physical workspace for the project (`.donna` directory).""" project_dir = project_dir.resolve() workspace_dir = project_dir / config.DONNA_DIR_NAME @@ -89,4 +115,21 @@ def initialize_workspace(project_dir: pathlib.Path) -> Result[None, core_errors. session_world = default_config.get_world(WorldId(config.DONNA_WORLD_SESSION_DIR_NAME)).unwrap() session_world.initialize() + if install_skills: + _sync_donna_skill(project_dir) + + return Ok(None) + + +@unwrap_to_error +def update_workspace(project_dir: pathlib.Path, install_skills: bool = True) -> Result[None, core_errors.ErrorsList]: + project_dir = project_dir.resolve() + workspace_dir = project_dir / config.DONNA_DIR_NAME + + if not workspace_dir.exists(): + return Err([core_errors.ProjectDirNotFound(donna_dir_name=config.DONNA_DIR_NAME)]) + + if install_skills: + _sync_donna_skill(project_dir) + return Ok(None)