From f95ea368ee50020d797527a09b88f2c385491727 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Thu, 7 May 2026 21:38:35 +0200 Subject: [PATCH 1/4] removed `donna journal` cli --- .agents/donna/intro.donna.md | 19 +--- .agents/donna/usage/cli.donna.md | 56 +++++------ .agents/donna/usage/worlds.donna.md | 2 +- README.md | 27 +++-- changes/unreleased.md | 4 + donna/cli/__main__.py | 1 - donna/cli/commands/artifacts.py | 6 +- donna/cli/commands/journal.py | 49 --------- donna/fixtures/specs/intro.donna.md | 19 +--- donna/fixtures/specs/usage/cli.donna.md | 56 +++++------ donna/fixtures/specs/usage/worlds.donna.md | 2 +- donna/machine/artifacts.py | 6 +- donna/machine/errors.py | 2 +- donna/machine/journal.py | 23 +---- donna/machine/sessions.py | 1 - donna/workspaces/config.py | 44 +++++++++ donna/workspaces/errors.py | 25 +++++ donna/workspaces/initialization.py | 2 +- donna/workspaces/journal.py | 109 +++++++++++++++++++++ donna/workspaces/sessions.py | 95 ------------------ 20 files changed, 270 insertions(+), 278 deletions(-) delete mode 100644 donna/cli/commands/journal.py create mode 100644 donna/workspaces/journal.py diff --git a/.agents/donna/intro.donna.md b/.agents/donna/intro.donna.md index d4072f3b..0eb043c4 100644 --- a/.agents/donna/intro.donna.md +++ b/.agents/donna/intro.donna.md @@ -39,21 +39,10 @@ Artifact type tags: ## Journaling -You MUST use `donna journal write` to track your actions and thoughts, according the description in `{{ donna.lib.view("./usage/cli.donna.md") }}`. +Donna creates internal journal records for important workflow events, according to the description in `{{ donna.lib.view("./usage/cli.donna.md") }}`. -Journaling is a required part of workflow execution. An action request MUST be considered incomplete until required journal records are written. +Journal records can be forwarded to a third-party tool by configuring `[journal].cmd` in `/.donna/config.toml`. -Journaling lifecycle for each non-trivial action request: +The configured command is a list of command arguments. Arguments whose first and last characters are `{` and `}` are replaced with attributes of `JournalRecord`. -1. Start intent (`Goal:`) before substantial work begins. -2. Progress updates (`Step:`) at significant phase boundaries. -3. Concrete edits (`Change:`) after meaningful source/artifact update batches. -4. Completion handoff (`Step:`) before calling `sessions action-request-completed`. - -Journal records MUST be change/decision-oriented and SHOULD be sufficient for another agent to continue work without re-discovery. - -If you perform a long operation (e.g., exploring the codebase, designing a solution) that takes more than 10 seconds, you MUST journal your progress. - -You MUST use `donna journal view --lines 100` to read the last records after you compress your context. - -If your work is interrupted and you resume later, you MUST first journal `Resume context and next action`. +If `[journal].cmd` is omitted, Donna treats it as `None` and performs no journal writing. diff --git a/.agents/donna/usage/cli.donna.md b/.agents/donna/usage/cli.donna.md index a50cc5b9..2b7ffc8f 100644 --- a/.agents/donna/usage/cli.donna.md +++ b/.agents/donna/usage/cli.donna.md @@ -70,12 +70,11 @@ Donna renders cells differently, depending on the protocol used. ### Commands -There are four sets of commands: +There are three sets of commands: - `donna -p workspaces …` — manages workspaces. Most-likely it will be used once per your project to initialize it. - `donna -p sessions …` — manages sessions. You will use these commands to start, push forward, and manage your work. - `donna -p artifacts …` — manages artifact discovery, reading, and validation. -- `donna -p journal …` — manages session actions journal. You will use these commands to log and inspect the history of actions performed during the session. Use: @@ -162,42 +161,39 @@ The format of `` is as follows: CLI arguments MUST NOT use relative artifact paths like `./...` or `../../...`; use absolute `@/...` paths or rooted wildcard forms. -### Working with journal +### Journal integrations -Use the next commands to work with session journal: +Donna creates internal `JournalRecord` values for important workflow events. +Donna does not expose a journal CLI command. -- `donna -p journal write ` — record a single new entry to the journal with the given **single-line** `message` (newlines are not allowed). Donna automatically adds a timestamp and other relevant information to the journal entry. -- `donna -p journal view [--lines N] [--follow]` — display journal records. +To forward journal records to a third-party tool, configure the workspace +`/.donna/config.toml` file: -Agents MUST use `donna -p journal write ` to log: - -- Goals of the long-running agent-side operations: `Goal: `. -- Significant steps of the long-running agent-side operations: `Step: `. -- Significant thoughts during the long-running operations: `Thought: `. -- Significant assumptions during the long-running operations: `Assumption: `. -- Changes in the project source code or in the project structure: `Change: `. - -For each non-trivial action request, agents MUST follow this journaling contract: - -1. Write exactly one `Goal:` record at action-request start. -2. Write `Step:` records at significant phase boundaries. If an action request describes a multi-step process, there MUST be at least one `Step:` record per specified step and one `Step:` record for the completion handoff. -3. Write `Change:` records after each meaningful source update batch. -4. Write one final `Step:` record immediately before `sessions action-request-completed`. +```toml +[journal] +cmd = ["cli-tool", "--message", "{message}"] +``` -Agents MUST consider these cases as significant phase boundaries: +`cmd` is a list of command arguments. If an argument starts with `{` and ends +with `}`, Donna treats the whole argument as a `JournalRecord` attribute name +and replaces it with that value. Donna validates placeholders when loading +config. -- A work phase expected to take more than 10 seconds. -- Transition from analysis/research to implementation/editing. -- Transition to a new step in a multi-step process described in the action request. -- Start or completion of a multi-file or multi-artifact change batch. -- A decision that changes implementation direction. +Supported attributes: -Before `sessions action-request-completed`, agents MUST check journal completeness for the current action request. +- `timestamp` — record creation time, formatted as ISO-8601. +- `actor_id` — actor that created the record; empty string when unknown. +- `message` — single-line journal message. +- `current_task_id` — current task id; empty string when no task is active. +- `current_work_unit_id` — current work unit id; empty string when no work unit is active. +- `current_operation_id` — current operation artifact section id; empty string when no operation is active. -Agents MUST NOT log: +If `journal.cmd` is omitted, Donna treats it as `None` and performs no journal +writing. -- CLI commands they execute. -- Elementary/trivial steps. +Donna still prints newly created internal journal records immediately using the +selected protocol formatter, so agents receive live feedback even when no +external journal command is configured. ## IMPORTANT ON DONNA TOOL USAGE diff --git a/.agents/donna/usage/worlds.donna.md b/.agents/donna/usage/worlds.donna.md index a37627a1..09ec4d56 100644 --- a/.agents/donna/usage/worlds.donna.md +++ b/.agents/donna/usage/worlds.donna.md @@ -33,7 +33,7 @@ Donna has read access to artifacts stored in the project world. It discovers, fe Developers and external tools are responsible for mutating project artifacts before Donna reads or validates them. -Donna still writes its own session state and journal data under `/.donna/session`, but that internal state storage is separate from world-artifact mutation. +Donna still writes its own session state under `/.donna/session`, but that internal state storage is separate from world-artifact mutation. ## Intro Artifacts diff --git a/README.md b/README.md index 020d184e..36780687 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,6 @@ Your agent will generate [state machines](https://en.wikipedia.org/wiki/Finite-s Donna allows your agent to execute hundreds of consecutive steps without swaying away from the defined workflow. Branching, loops, nested calls, and recursion — all possible. -![Journal log demonstration](./docs/images/journal-demo.gif) - ## What is Donna? Donna is a CLI tool that helps coding agents like Codex focus on the task at hand by keeping high-level control flow in explicit Donna workflows. Donna dictates what should be done at each step of the work, so the agent can focus on the actual piece. @@ -162,18 +160,33 @@ If you upgrade Donna later, run `donna workspaces update` to refresh `.agents/do **Donna is a CLI tool for agents.** You rarely need to use it directly. -However, it is convenient to run `donna journal view --follow` in a separate terminal to see what is going on in the current session. - Commands you may need: - `donna workspaces init` — Initialize Donna in your project. - `donna sessions start` — start a new working session, remove everything from the previous session. - `donna artifacts list ` — list artifacts with short descriptions. -- `donna journal view [--lines N] [--follow]` — view the log of work performed in the current session. -Here is an example of the real Donna session work log: +Donna can send internal journal records to a third-party tool. Configure it in `.donna/config.toml`: + +```toml +[journal] +cmd = ["cli-tool", "--message", "{message}"] +``` + +`cmd` is a list of command arguments. If an argument starts with `{` and ends with `}`, Donna treats the whole argument as a `JournalRecord` attribute name and replaces it with that value. Donna validates placeholders when loading config. + +Supported attributes: + +- `timestamp` — record creation time, formatted as ISO-8601. +- `actor_id` — actor that created the record; empty string when unknown. +- `message` — single-line journal message. +- `current_task_id` — current task id; empty string when no task is active. +- `current_work_unit_id` — current work unit id; empty string when no work unit is active. +- `current_operation_id` — current operation artifact section id; empty string when no operation is active. + +By default `journal.cmd` is omitted and Donna treats it as `None`, so no journal writing is performed. -![Journal log demonstration](./docs/images/journal-demo.gif) +Donna still prints newly created internal journal records immediately using the selected protocol formatter, so agents receive live feedback even when no external journal command is configured. Use `donna --help` for a quick reference. diff --git a/changes/unreleased.md b/changes/unreleased.md index 1772c1e4..73cf4db8 100644 --- a/changes/unreleased.md +++ b/changes/unreleased.md @@ -4,6 +4,7 @@ - Run `donna workspaces update` in existing projects so bundled Donna specs are installed into `.agents/donna` for the new filesystem-backed `donna` world. - Update your scripts and specs to use external tools or direct file edits to create, update, move, copy, or delete world artifacts instead using removed Donna commands. - Update artifact references from legacy ids like `specs:intro` to filepath ids like `@/specs/intro.md`, and include file extensions on all artifact references. +- Set `journal.cmd` in the `.donna/config.toml` as a list of command arguments if you relied on `donna journal` output or session journal files. ### Changes @@ -21,6 +22,7 @@ - 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. +- Removed native journal CLI functionality in favor of external journal integrations. ### Breaking Changes @@ -28,7 +30,9 @@ - Donna no longer exposes bundled specs through the Python-backed `donna` world; `donna workspaces init|update` now sync them into `.agents/donna`. - `donna artifacts` no longer supports `update`, `copy`, `move`, or `remove`. - Donna no longer mutates world artifacts through workspace APIs or world configuration. +- Donna no longer exposes the `donna journal` CLI command or session journal viewing/following. ### Removals - Removed the Python world implementation and the `donna.artifacts` package-backed source of bundled Donna specs. +- Removed the `donna journal` CLI command and session journal JSONL read/follow support. diff --git a/donna/cli/__main__.py b/donna/cli/__main__.py index 414fdb6b..197d48cc 100644 --- a/donna/cli/__main__.py +++ b/donna/cli/__main__.py @@ -1,6 +1,5 @@ from donna.cli.application import app # noqa: F401 from donna.cli.commands import artifacts # noqa: F401 -from donna.cli.commands import journal # noqa: F401 from donna.cli.commands import sessions # noqa: F401 from donna.cli.commands import version # noqa: F401 from donna.cli.commands import workspaces # noqa: F401 diff --git a/donna/cli/commands/artifacts.py b/donna/cli/commands/artifacts.py index f8536337..6fa1beea 100644 --- a/donna/cli/commands/artifacts.py +++ b/donna/cli/commands/artifacts.py @@ -3,11 +3,7 @@ import typer from donna.cli.application import app -from donna.cli.types import ( - ArtifactIdPatternArgument, - PredicateOption, - validate_supported_artifact_pattern, -) +from donna.cli.types import ArtifactIdPatternArgument, PredicateOption, validate_supported_artifact_pattern from donna.cli.utils import cells_cli from donna.context.context import context from donna.domain.artifact_ids import ArtifactIdPattern diff --git a/donna/cli/commands/journal.py b/donna/cli/commands/journal.py deleted file mode 100644 index 4cca9bb9..00000000 --- a/donna/cli/commands/journal.py +++ /dev/null @@ -1,49 +0,0 @@ -import sys -from collections.abc import Iterable - -import typer - -from donna.cli.application import app -from donna.cli.utils import cells_cli, output_cells -from donna.machine import journal as machine_journal -from donna.protocol.cell_shortcuts import operation_succeeded -from donna.protocol.cells import Cell -from donna.protocol.modes import get_cell_formatter - -journal_cli = typer.Typer() - - -@journal_cli.command(help="Append a new journal record.") -@cells_cli -def write( - message: str = typer.Argument(..., help="Single-line message to append to journal (newlines are not allowed)."), -) -> Iterable[Cell]: - machine_journal.add(message=message).unwrap() - return [operation_succeeded("Journal record appended.")] - - -@journal_cli.command(help="View journal records.") -def view( # noqa: CCR001 - lines: int | None = typer.Option(None, min=1, help="Show only the last N records."), - follow: bool = typer.Option(False, help="Keep printing records as they are appended."), -) -> None: - iterator = machine_journal.read(lines=lines, follow=follow) - - formatter = get_cell_formatter() - - for record_result in iterator: - if record_result.is_err(): - output_cells([error.node().info() for error in record_result.unwrap_err()]) - return - - record = record_result.unwrap() - rendered = formatter.format_journal(record) - sys.stdout.buffer.write(rendered + b"\n") - sys.stdout.buffer.flush() - - -app.add_typer( - journal_cli, - name="journal", - help="Append and inspect session actions journal records.", -) diff --git a/donna/fixtures/specs/intro.donna.md b/donna/fixtures/specs/intro.donna.md index d4072f3b..0eb043c4 100644 --- a/donna/fixtures/specs/intro.donna.md +++ b/donna/fixtures/specs/intro.donna.md @@ -39,21 +39,10 @@ Artifact type tags: ## Journaling -You MUST use `donna journal write` to track your actions and thoughts, according the description in `{{ donna.lib.view("./usage/cli.donna.md") }}`. +Donna creates internal journal records for important workflow events, according to the description in `{{ donna.lib.view("./usage/cli.donna.md") }}`. -Journaling is a required part of workflow execution. An action request MUST be considered incomplete until required journal records are written. +Journal records can be forwarded to a third-party tool by configuring `[journal].cmd` in `/.donna/config.toml`. -Journaling lifecycle for each non-trivial action request: +The configured command is a list of command arguments. Arguments whose first and last characters are `{` and `}` are replaced with attributes of `JournalRecord`. -1. Start intent (`Goal:`) before substantial work begins. -2. Progress updates (`Step:`) at significant phase boundaries. -3. Concrete edits (`Change:`) after meaningful source/artifact update batches. -4. Completion handoff (`Step:`) before calling `sessions action-request-completed`. - -Journal records MUST be change/decision-oriented and SHOULD be sufficient for another agent to continue work without re-discovery. - -If you perform a long operation (e.g., exploring the codebase, designing a solution) that takes more than 10 seconds, you MUST journal your progress. - -You MUST use `donna journal view --lines 100` to read the last records after you compress your context. - -If your work is interrupted and you resume later, you MUST first journal `Resume context and next action`. +If `[journal].cmd` is omitted, Donna treats it as `None` and performs no journal writing. diff --git a/donna/fixtures/specs/usage/cli.donna.md b/donna/fixtures/specs/usage/cli.donna.md index 7cb3a138..6504fe9d 100644 --- a/donna/fixtures/specs/usage/cli.donna.md +++ b/donna/fixtures/specs/usage/cli.donna.md @@ -70,12 +70,11 @@ Donna renders cells differently, depending on the protocol used. ### Commands -There are four sets of commands: +There are three sets of commands: - `donna -p workspaces …` — manages workspaces. Most-likely it will be used once per your project to initialize it. - `donna -p sessions …` — manages sessions. You will use these commands to start, push forward, and manage your work. - `donna -p artifacts …` — manages artifact discovery, reading, and validation. -- `donna -p journal …` — manages session actions journal. You will use these commands to log and inspect the history of actions performed during the session. Use: @@ -170,42 +169,39 @@ The format of `` is as follows: CLI arguments MUST NOT use relative artifact paths like `./...` or `../../...`; use absolute `@/...` paths or rooted wildcard forms. -### Working with journal +### Journal integrations -Use the next commands to work with session journal: +Donna creates internal `JournalRecord` values for important workflow events. +Donna does not expose a journal CLI command. -- `donna -p journal write ` — record a single new entry to the journal with the given **single-line** `message` (newlines are not allowed). Donna automatically adds a timestamp and other relevant information to the journal entry. -- `donna -p journal view [--lines N] [--follow]` — display journal records. +To forward journal records to a third-party tool, configure the workspace +`/.donna/config.toml` file: -Agents MUST use `donna -p journal write ` to log: - -- Goals of the long-running agent-side operations: `Goal: `. -- Significant steps of the long-running agent-side operations: `Step: `. -- Significant thoughts during the long-running operations: `Thought: `. -- Significant assumptions during the long-running operations: `Assumption: `. -- Changes in the project source code or in the project structure: `Change: `. - -For each non-trivial action request, agents MUST follow this journaling contract: - -1. Write exactly one `Goal:` record at action-request start. -2. Write `Step:` records at significant phase boundaries. If an action request describes a multi-step process, there MUST be at least one `Step:` record per specified step and one `Step:` record for the completion handoff. -3. Write `Change:` records after each meaningful source update batch. -4. Write one final `Step:` record immediately before `sessions action-request-completed`. +```toml +[journal] +cmd = ["cli-tool", "--message", "{message}"] +``` -Agents MUST consider these cases as significant phase boundaries: +`cmd` is a list of command arguments. If an argument starts with `{` and ends +with `}`, Donna treats the whole argument as a `JournalRecord` attribute name +and replaces it with that value. Donna validates placeholders when loading +config. -- A work phase expected to take more than 10 seconds. -- Transition from analysis/research to implementation/editing. -- Transition to a new step in a multi-step process described in the action request. -- Start or completion of a multi-file or multi-artifact change batch. -- A decision that changes implementation direction. +Supported attributes: -Before `sessions action-request-completed`, agents MUST check journal completeness for the current action request. +- `timestamp` — record creation time, formatted as ISO-8601. +- `actor_id` — actor that created the record; empty string when unknown. +- `message` — single-line journal message. +- `current_task_id` — current task id; empty string when no task is active. +- `current_work_unit_id` — current work unit id; empty string when no work unit is active. +- `current_operation_id` — current operation artifact section id; empty string when no operation is active. -Agents MUST NOT log: +If `journal.cmd` is omitted, Donna treats it as `None` and performs no journal +writing. -- CLI commands they execute. -- Elementary/trivial steps. +Donna still prints newly created internal journal records immediately using the +selected protocol formatter, so agents receive live feedback even when no +external journal command is configured. ## IMPORTANT ON DONNA TOOL USAGE diff --git a/donna/fixtures/specs/usage/worlds.donna.md b/donna/fixtures/specs/usage/worlds.donna.md index 70e08728..126a823d 100644 --- a/donna/fixtures/specs/usage/worlds.donna.md +++ b/donna/fixtures/specs/usage/worlds.donna.md @@ -33,7 +33,7 @@ Donna has read access to artifacts stored in the project filesystem. It discover Developers and external tools are responsible for mutating project artifacts before Donna reads or validates them. -Donna still writes its own session state and journal data under `/.donna/session`, but that internal state storage is separate from project-artifact mutation. +Donna still writes its own session state under `/.donna/session`, but that internal state storage is separate from project-artifact mutation. ## Intro Artifacts diff --git a/donna/machine/artifacts.py b/donna/machine/artifacts.py index 413f3db4..068d16c8 100644 --- a/donna/machine/artifacts.py +++ b/donna/machine/artifacts.py @@ -9,11 +9,7 @@ from donna.domain.artifact_ids import ArtifactId from donna.domain.ids import SectionId from donna.domain.python_path import PythonPath -from donna.machine.errors import ( - ArtifactPrimarySectionMissing, - ArtifactSectionNotFound, - MultiplePrimarySectionsError, -) +from donna.machine.errors import ArtifactPrimarySectionMissing, ArtifactSectionNotFound, MultiplePrimarySectionsError from donna.protocol.cells import Cell from donna.protocol.nodes import Node diff --git a/donna/machine/errors.py b/donna/machine/errors.py index 7411a98e..c9689b39 100644 --- a/donna/machine/errors.py +++ b/donna/machine/errors.py @@ -38,7 +38,7 @@ class JournalMessageContainsNewlines(EnvironmentError): code: str = "donna.machine.journal_message_contains_newlines" message: str = "Journal message must be a single line and must not contain newline characters." ways_to_fix: list[str] = [ - "Provide `journal write` message as a single line.", + "Provide journal messages as single-line strings.", "Replace newline characters with spaces or split the text into multiple journal records.", ] diff --git a/donna/machine/journal.py b/donna/machine/journal.py index 4066ccaa..d2a2ccbd 100644 --- a/donna/machine/journal.py +++ b/donna/machine/journal.py @@ -1,6 +1,5 @@ import datetime import json -from collections.abc import Iterable import pydantic @@ -11,7 +10,7 @@ from donna.domain.artifact_ids import ArtifactSectionId from donna.domain.internal_ids import TaskId, WorkUnitId from donna.machine import errors as machine_errors -from donna.workspaces import sessions as workspace_sessions +from donna.workspaces import journal as workspace_journal from donna.workspaces.config import protocol as protocol_mode @@ -45,17 +44,6 @@ def serialize_record(record: JournalRecord) -> bytes: ).encode("utf-8") -def deserialize_record(content: bytes) -> JournalRecord: - payload = json.loads(content.decode("utf-8").strip()) - return JournalRecord.model_validate(payload) - - -@unwrap_to_error -def reset() -> Result[None, ErrorsList]: - workspace_sessions.reset_journal() - return Ok(None) - - def smart_agent_id() -> str: from donna.protocol.modes import Mode as ProtocolMode @@ -99,14 +87,7 @@ def add( # noqa: CCR001 current_operation_id=parsed_operation_id, ) - serialized = serialize_record(record) - workspace_sessions.append_journal_record(serialized) - + workspace_journal.write_record(record).unwrap() instant_output_journal(record) return Ok(record) - - -def read(lines: int | None = None, follow: bool = False) -> Iterable[Result[JournalRecord, ErrorsList]]: - for raw_record in workspace_sessions.read_journal(lines=lines, follow=follow): - yield Ok(deserialize_record(raw_record)) diff --git a/donna/machine/sessions.py b/donna/machine/sessions.py index 2359a5c2..ee5c5c8d 100644 --- a/donna/machine/sessions.py +++ b/donna/machine/sessions.py @@ -63,7 +63,6 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> CellsResult: def start() -> Result[list[Cell], ErrorsList]: workspace_sessions.reset_dir() - machine_journal.reset().unwrap() _save_state(MutableState.build().freeze()).unwrap() machine_journal.add(message="Started new session.").unwrap() diff --git a/donna/workspaces/config.py b/donna/workspaces/config.py index 5a4490d3..e3e14547 100644 --- a/donna/workspaces/config.py +++ b/donna/workspaces/config.py @@ -42,6 +42,49 @@ class FileFilter(BaseEntity): pattern: ArtifactIdPattern +class JournalRecordAttribute(str, enum.Enum): + timestamp = "timestamp" + actor_id = "actor_id" + message = "message" + current_task_id = "current_task_id" + current_work_unit_id = "current_work_unit_id" + current_operation_id = "current_operation_id" + + @classmethod + def has_attribute(cls, name: str) -> bool: + return name in cls.__members__ + + +def _is_journal_variable_argument(argument: str) -> bool: + return len(argument) >= 2 and argument[0] == "{" and argument[-1] == "}" + + +class JournalConfig(BaseEntity): + cmd: list[str] | None = None + + @pydantic.field_validator("cmd", mode="after") + @classmethod + def validate_cmd(cls, value: list[str] | None) -> list[str] | None: + if value is None: + return value + + if not value: + raise ValueError("Journal command config is invalid: Configured command is empty.") + + for argument in value: + if not _is_journal_variable_argument(argument): + continue + + name = argument[1:-1] + + if not JournalRecordAttribute.has_attribute(name): + raise ValueError( + f"Journal command config is invalid: `{name}` is not a supported `JournalRecord` argument." + ) + + return value + + def _default_sources() -> list[SourceConfig]: return [ SourceConfig.model_validate( @@ -68,6 +111,7 @@ def _default_file_filters() -> list[FileFilter]: class Config(BaseEntity): sources: list[SourceConfig] = pydantic.Field(default_factory=_default_sources) file_filters: list[FileFilter] = pydantic.Field(default_factory=_default_file_filters) + journal: JournalConfig = pydantic.Field(default_factory=JournalConfig) _sources_instances: list[SourceConfigValue] = pydantic.PrivateAttr(default_factory=list) cache_lifetime: float = 1.0 diff --git a/donna/workspaces/errors.py b/donna/workspaces/errors.py index 03eddea1..7fa78c48 100644 --- a/donna/workspaces/errors.py +++ b/donna/workspaces/errors.py @@ -43,6 +43,31 @@ class WorkspaceAlreadyInitialized(WorkspaceError): project_dir: pathlib.Path +class JournalCommandConfigInvalid(WorkspaceError): + code: str = "donna.workspaces.journal_command_config_invalid" + message: str = "Journal command config is invalid: {error.details}" + ways_to_fix: list[str] = [ + "Configure `[journal].cmd` as a list of command arguments.", + "Use whole-argument placeholders like `{message}` for `JournalRecord` attributes.", + "Omit `journal.cmd` to disable external journal writing.", + ] + argument: str + details: str + + +class JournalCommandFailed(WorkspaceError): + code: str = "donna.workspaces.journal_command_failed" + message: str = "Journal command failed: {error.details}" + ways_to_fix: list[str] = [ + "Check that the configured journal command exists and is executable.", + "Check command arguments generated from `[journal].cmd`.", + "Omit `journal.cmd` to disable external journal writing.", + ] + command: list[str] + returncode: int | None + details: str + + class SourceError(WorkspaceError): cell_kind: str = "source_error" source_id: str diff --git a/donna/workspaces/initialization.py b/donna/workspaces/initialization.py index 86c63759..ba58432a 100644 --- a/donna/workspaces/initialization.py +++ b/donna/workspaces/initialization.py @@ -120,7 +120,7 @@ def initialize_workspace( config_path = workspace_dir / config.DONNA_CONFIG_NAME config_path.write_text( - tomli_w.dumps(default_config.model_dump(mode="json")), + tomli_w.dumps(default_config.model_dump(mode="json", exclude_none=True)), encoding="utf-8", ) diff --git a/donna/workspaces/journal.py b/donna/workspaces/journal.py new file mode 100644 index 00000000..9532988f --- /dev/null +++ b/donna/workspaces/journal.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +import subprocess # noqa: S404 +from typing import TYPE_CHECKING + +from donna.core.errors import ErrorsList +from donna.core.result import Err, Ok, Result, unwrap_to_error +from donna.workspaces import errors as workspace_errors +from donna.workspaces.config import JournalRecordAttribute + +if TYPE_CHECKING: + from donna.machine.journal import JournalRecord + + +def _is_variable_argument(argument: str) -> bool: + return len(argument) >= 2 and argument[0] == "{" and argument[-1] == "}" + + +def _parse_record_attribute(name: str, argument: str) -> Result[JournalRecordAttribute, ErrorsList]: + if not JournalRecordAttribute.has_attribute(name): + return Err( + [ + workspace_errors.JournalCommandConfigInvalid( + argument=argument, + details=f"`{name}` is not a supported `JournalRecord` argument.", + ) + ] + ) + + return Ok(JournalRecordAttribute[name]) + + +def _format_record_attribute(attribute: JournalRecordAttribute, record: JournalRecord) -> str: + match attribute: + case JournalRecordAttribute.timestamp: + return record.timestamp.isoformat() + case JournalRecordAttribute.actor_id: + return record.actor_id or "" + case JournalRecordAttribute.message: + return record.message + case JournalRecordAttribute.current_task_id: + return str(record.current_task_id) if record.current_task_id is not None else "" + case JournalRecordAttribute.current_work_unit_id: + return str(record.current_work_unit_id) if record.current_work_unit_id is not None else "" + case JournalRecordAttribute.current_operation_id: + return str(record.current_operation_id) if record.current_operation_id is not None else "" + + raise AssertionError(f"Unsupported journal record attribute: {attribute}") + + +def _resolve_command_argument(argument: str, record: JournalRecord) -> Result[str, ErrorsList]: + if not _is_variable_argument(argument): + return Ok(argument) + + name = argument[1:-1] + attribute = _parse_record_attribute(name, argument).unwrap() + return Ok(_format_record_attribute(attribute, record)) + + +@unwrap_to_error +def _build_command_args(command: list[str], record: JournalRecord) -> Result[list[str], ErrorsList]: + args = [] + + for argument in command: + args.append(_resolve_command_argument(argument, record).unwrap()) + + return Ok(args) + + +@unwrap_to_error +def write_record(record: JournalRecord) -> Result[None, ErrorsList]: + from donna.workspaces import config as workspace_config + + command = workspace_config.config().journal.cmd + if command is None: + return Ok(None) + + args = _build_command_args(command, record).unwrap() + + try: + result = subprocess.run(args, check=False, capture_output=True, text=True) # noqa: S603 + except OSError as e: + return Err( + [ + workspace_errors.JournalCommandFailed( + command=args, + returncode=None, + details=str(e), + ) + ] + ) + + if result.returncode != 0: + details = f"exit code {result.returncode}" + stderr = result.stderr.strip() + if stderr: + details = f"{details}; stderr: {stderr}" + + return Err( + [ + workspace_errors.JournalCommandFailed( + command=args, + returncode=result.returncode, + details=details, + ) + ] + ) + + return Ok(None) diff --git a/donna/workspaces/sessions.py b/donna/workspaces/sessions.py index 86ec942e..c36b855b 100644 --- a/donna/workspaces/sessions.py +++ b/donna/workspaces/sessions.py @@ -1,9 +1,5 @@ -import os import pathlib import shutil -import stat -import time -from collections.abc import Iterable from donna.workspaces.config import DONNA_DIR_NAME, DONNA_WORLD_SESSION_DIR_NAME, project_dir @@ -36,94 +32,3 @@ def write_state(content: bytes) -> None: path = dir() / "state.json" ensure_dir() path.write_bytes(content) - - -def reset_journal() -> None: - path = dir() / "journal.jsonl" - ensure_dir() - path.write_bytes(b"") - - -def append_journal_record(content: bytes) -> None: - path = dir() / "journal.jsonl" - ensure_dir() - - with path.open("ab") as stream: - stream.write(content.rstrip(b"\n")) - stream.write(b"\n") - - -def read_journal(lines: int | None = None, follow: bool = False) -> Iterable[bytes]: - path = dir() / "journal.jsonl" - - yield from _journal_read_some(path, lines=lines) - - if not follow: - return - - yield from _journal_follow(path) - - -def _journal_read_all(path: pathlib.Path) -> list[bytes]: - if not path.exists(): - return [] - - with path.open("rb") as stream: - return [line.rstrip(b"\n") for line in stream if line.strip()] - - -def _journal_file_identity(path: pathlib.Path) -> tuple[int, int] | None: - try: - path_stat = path.stat() - except FileNotFoundError: - return None - - if not stat.S_ISREG(path_stat.st_mode): - return None - - return (path_stat.st_dev, path_stat.st_ino) - - -def _journal_read_some(path: pathlib.Path, lines: int | None = None) -> Iterable[bytes]: - records = _journal_read_all(path) - - if lines is not None: - records = records[-lines:] if lines > 0 else [] - - yield from records - - -def _journal_follow(path: pathlib.Path, poll_interval: float = 0.25) -> Iterable[bytes]: # noqa: CCR001 - stream = None - stream_identity: tuple[int, int] | None = None - start_from_head = False - - while True: - file_identity = _journal_file_identity(path) - - if stream is not None and stream_identity != file_identity: - stream.close() - stream = None - stream_identity = None - - if file_identity is None or file_identity == stream_identity: - start_from_head = True - - if stream is None and file_identity is not None: - stream = path.open("rb") - - if not start_from_head: - stream.seek(0, os.SEEK_END) - - stream_identity = file_identity - - if stream is None: - time.sleep(poll_interval) - continue - - while line := stream.readline(): - line = line.rstrip(b"\n") - if line.strip(): - yield line - - time.sleep(poll_interval) From 5b87d8143d6ec5f245928381dd3b2489827064e3 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sat, 9 May 2026 20:35:10 +0200 Subject: [PATCH 2/4] session --- .agents/skills/session/SKILL.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .agents/skills/session/SKILL.md diff --git a/.agents/skills/session/SKILL.md b/.agents/skills/session/SKILL.md new file mode 100644 index 00000000..75b29309 --- /dev/null +++ b/.agents/skills/session/SKILL.md @@ -0,0 +1,19 @@ +--- +name: session +description: Manage task-local temporary working files in the project `.session/` directory. Use whenever an agent needs to create scratch files, notes, temporary plans, intermediate reasoning artifacts, generated helper files, or other task-scoped temporary data; also use when the user asks to start a new session. +--- + +# Session + +## Temporary Files + +- Store every temporary file created during work on a task under `/.session/`. +- Create `/.session/` before creating temporary files if the directory does not already exist. +- Use files in `/.session/` for task-scoped temporary information such as intermediate notes, plans, scratch artifacts, and working data. +- Do not place temporary task files outside `/.session/`. + +## New Sessions + +When the user tells you to start a new session, remove all files and directories inside `/.session/`. + +Keep the `.session` directory itself available for later temporary files when practical. From 0da123d0b771606ccafe281de39d1bd0a9773e9b Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sat, 9 May 2026 21:53:26 +0200 Subject: [PATCH 3/4] taskwarior integration --- .donna/config.toml | 2 ++ .taskrc | 12 ++++++++ AGENTS.md | 50 +++++++++++++++++++++++++++++++ bin/taskwarior.sh | 75 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 .donna/config.toml create mode 100644 .taskrc create mode 100755 bin/taskwarior.sh diff --git a/.donna/config.toml b/.donna/config.toml new file mode 100644 index 00000000..19086356 --- /dev/null +++ b/.donna/config.toml @@ -0,0 +1,2 @@ +[journal] +cmd = ["./bin/taskwarior.sh", "log", "+journal", "+donna", "kind:event", "{message}"] diff --git a/.taskrc b/.taskrc new file mode 100644 index 00000000..087517ba --- /dev/null +++ b/.taskrc @@ -0,0 +1,12 @@ +data.location=.session/taskwarrior +confirmation=no + +uda.kind.type=string +uda.kind.label=Kind +uda.kind.values=goal,step,thought,assumption,change,event, + +uda.logged_at.type=string +uda.logged_at.label=Logged + +uda.logged_time.type=string +uda.logged_time.label=Time diff --git a/AGENTS.md b/AGENTS.md index 2a91455a..f0ba107c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,3 +31,53 @@ You MUST use it to: You MUST NOT use it for: - Implementing huge features or behaviors that require adding massive blocks of code (like adding a new class, module, writing a huge function, etc.). + +### `task` + +`task` — Taskwarrior — is the project journal for significant agent-side work. + +You MUST use it to write journal records with these exact command templates from the project root: + +```bash +./bin/taskwarior.sh log +journal +agent kind:goal "" +./bin/taskwarior.sh log +journal +agent kind:step "" +./bin/taskwarior.sh log +journal +agent kind:thought "" +./bin/taskwarior.sh log +journal +agent kind:assumption "" +./bin/taskwarior.sh log +journal +agent kind:change "" +``` + +Journal messages MUST be single-line strings. + +You MUST log: + +- Goals of long-running agent-side operations with `kind:goal`. +- Significant steps of long-running operations with `kind:step`. +- Significant thoughts during long-running operations with `kind:thought`. +- Significant assumptions during long-running operations with `kind:assumption`. +- Changes in project source code or project structure with `kind:change`. + +You MAY add extra tags after `+agent` and before the message: + +```bash +./bin/taskwarior.sh log +journal +agent kind: +... "" +``` + +For each non-trivial Donna action request or long-running agent task: + +1. Write exactly one `goal` record at action-request or task start. +2. Write `step` records at significant phase boundaries. +3. Write `change` records after each meaningful source update batch. +4. Write one final `step` record immediately before reporting completion or handing work back to the developer. + +You MUST consider these cases significant phase boundaries: + +- A work phase expected to take more than 10 seconds. +- Transition from analysis or research to implementation. +- Transition to a new step in a multi-step process. +- Start or completion of a multi-file or multi-artifact change batch. +- A decision that changes implementation direction. + +You MUST NOT log: + +- CLI commands you execute. +- Elementary or trivial steps. diff --git a/bin/taskwarior.sh b/bin/taskwarior.sh new file mode 100755 index 00000000..223c2572 --- /dev/null +++ b/bin/taskwarior.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.."; pwd)" + +if [[ $# -eq 0 ]]; then + echo "Usage: $0 " >&2 + exit 2 +fi + +cd "$ROOT_DIR" + +logged_at() { + local alphabet + local encoded + local epoch_seconds + local nanoseconds + local remainder + local seconds + local timestamp + local value + + alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + epoch_seconds=1767225600 + timestamp="$(date -u +%s:%N)" + seconds="${timestamp%:*}" + nanoseconds="${timestamp#*:}" + value=$(((seconds - epoch_seconds) * 1000000000 + 10#$nanoseconds)) + + if ((value < 0)); then + echo "logged_at timestamp is before the project epoch" >&2 + return 1 + fi + + encoded="" + + for _ in {1..10}; do + remainder=$((value % 62)) + encoded="${alphabet:remainder:1}${encoded}" + value=$((value / 62)) + done + + if ((value > 0)); then + echo "logged_at timestamp exceeds the fixed-width range" >&2 + return 1 + fi + + printf "%s\n" "$encoded" +} + +logged_time() { + date +%H:%M:%S +} + +ARGS=("$@") +OVERRIDES=() + +while [[ ${#ARGS[@]} -gt 0 && "${ARGS[0]}" == rc.* ]]; do + OVERRIDES+=("${ARGS[0]}") + ARGS=("${ARGS[@]:1}") +done + +if [[ ${#ARGS[@]} -eq 0 ]]; then + echo "Usage: $0 " >&2 + exit 2 +fi + +case "${ARGS[0]}" in + add | log) + exec task rc:.taskrc rc.confirmation:no "${OVERRIDES[@]}" "${ARGS[@]}" project:donna "logged_at:$(logged_at)" "logged_time:$(logged_time)" + ;; +esac + +exec task rc:.taskrc rc.confirmation:no "${OVERRIDES[@]}" project:donna "${ARGS[@]}" From e7bb6b996051c1f0ccf5feafd4a1a91cff5a9f5f Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Sat, 9 May 2026 22:05:59 +0200 Subject: [PATCH 4/4] log following script --- bin/journal-follow.py | 136 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100755 bin/journal-follow.py diff --git a/bin/journal-follow.py b/bin/journal-follow.py new file mode 100755 index 00000000..9b1e5c59 --- /dev/null +++ b/bin/journal-follow.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 + +import argparse +import json +import subprocess +import sys +import time +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + + +ROOT_DIR = Path(__file__).resolve().parent.parent +TASKWARIOR = ROOT_DIR / "bin" / "taskwarior.sh" +COLUMN_PADDING = 2 + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Follow project journal records.") + parser.add_argument("-n", "--lines", type=int, default=20, help="number of existing records to show") + parser.add_argument("-i", "--interval", type=float, default=1.0, help="poll interval in seconds") + return parser.parse_args() + + +def load_records() -> list[dict[str, Any]]: + result = subprocess.run( + [str(TASKWARIOR), "rc.verbose:nothing", "+journal", "export"], + cwd=ROOT_DIR, + check=True, + stdout=subprocess.PIPE, + text=True, + ) + + return json.loads(result.stdout) + + +def record_key(record: dict[str, Any]) -> tuple[str, str, str]: + return ( + str(record.get("logged_at") or ""), + str(record.get("entry") or ""), + str(record.get("uuid") or ""), + ) + + +def record_id(record: dict[str, Any]) -> str: + uuid = record.get("uuid") + + if uuid: + return str(uuid) + + return "|".join(record_key(record) + (str(record.get("description") or ""),)) + + +def record_time(record: dict[str, Any]) -> str: + logged_time = record.get("logged_time") + + if logged_time: + return str(logged_time) + + entry = str(record.get("entry") or "") + + if not entry: + return "" + + try: + return datetime.strptime(entry, "%Y%m%dT%H%M%SZ").replace(tzinfo=UTC).astimezone().strftime("%H:%M:%S") + except ValueError: + return "" + + +def record_actor(record: dict[str, Any]) -> str: + return " ".join(str(tag) for tag in record.get("tags", []) if tag != "journal") + + +def record_kind(record: dict[str, Any]) -> str: + return str(record.get("kind") or "") + + +class Formatter: + def __init__(self) -> None: + self.actor_width = 0 + self.kind_width = 0 + + def observe(self, records: list[dict[str, Any]]) -> None: + for record in records: + self.actor_width = max(self.actor_width, len(record_actor(record))) + self.kind_width = max(self.kind_width, len(record_kind(record))) + + def format_record(self, record: dict[str, Any]) -> str: + actor_width = self.actor_width + COLUMN_PADDING + kind_width = self.kind_width + COLUMN_PADDING + + return "{time} {actor:<{actor_width}} {kind:<{kind_width}} {description}".format( + time=record_time(record), + actor=record_actor(record), + actor_width=actor_width, + kind=record_kind(record), + kind_width=kind_width, + description=str(record.get("description") or ""), + ).rstrip() + + +def print_records(records: list[dict[str, Any]], formatter: Formatter) -> None: + for record in records: + print(formatter.format_record(record), flush=True) + + +def main() -> int: + args = parse_args() + records = sorted(load_records(), key=record_key) + formatter = Formatter() + formatter.observe(records) + seen = {record_id(record) for record in records} + + if args.lines > 0: + print_records(records[-args.lines :], formatter) + + while True: + time.sleep(args.interval) + + records = sorted(load_records(), key=record_key) + formatter.observe(records) + new_records = [record for record in records if record_id(record) not in seen] + + for record in new_records: + seen.add(record_id(record)) + + print_records(new_records, formatter) + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except KeyboardInterrupt: + print(file=sys.stderr) + raise SystemExit(130)