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
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Druks runs durable agent applications on DBOS and Postgres. It owns
workflow execution, persisted state and events, gates, webhooks, sandbox access,
and the shared dashboard. Apps are **extensions**: standalone Python packages
that self-register through the `druks.extensions` entry point. `build` is the
that self-register through the `druks.extensions` entry point. `ship` is the
bundled reference extension for coordinating coding agents through GitHub PRs.

## Read map
Expand All @@ -26,7 +26,7 @@ For extension-surface changes, inspect the proof extension at
## Architectural boundaries

- Keep platform and extension ownership explicit. GitHub issue, branch, PR, and
coding-agent policy belongs to `build`, not to Druks core.
coding-agent policy belongs to `ship`, not to Druks core.
- Describe durability precisely: completed durable checkpoints are reused when
orchestration replays, but an interrupted operation may run again. Do not imply
arbitrary-line resume or exactly-once external side effects.
Expand Down Expand Up @@ -95,7 +95,7 @@ truth for CI, including the proof-extension install phase.
routing, architectural boundaries, and contributor rules.
- Link to one canonical explanation instead of copying it into multiple pages.
- Verify behavioral claims against current source and focused tests. Distinguish
framework capabilities from `build` behavior and guarantees from policy.
framework capabilities from `ship` behavior and guarantees from policy.
- Update this file only when contributor routing, repository structure, commands,
or a load-bearing architectural invariant changes.

Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
> before 1.0; `main` and `latest` are edge builds, not stable releases.

Druks is the self-hosted **home for durable agent apps**, running on the
Claude and Codex subscriptions you already pay for. Build ships out of the
box: autonomous software delivery from ticket to reviewed pull request.
Claude and Codex subscriptions you already pay for. Ship comes bundled:
autonomous software delivery from ticket to reviewed pull request.

An ordinary agent script loses its place when the process dies. A Druks
workflow records the result of each completed durable operation in Postgres.
Expand Down Expand Up @@ -55,7 +55,7 @@ DRUKS_PROVIDER=docker bash <(curl -fsSL https://raw.githubusercontent.com/czpyth

Then follow [full local setup](docs/full-local.md) to start Drukbox and connect
the agent harnesses. A complete installation needs GitHub Apps because the
bundled `build` extension is installed; a standalone extension may have
bundled `ship` extension is installed; a standalone extension may have
different integration requirements.

```text
Expand Down Expand Up @@ -84,9 +84,9 @@ Python distribution registered through the `druks.extensions` entry-point
group. Installing the distribution registers it; Druks does not need an
extension-specific plugin list.

The bundled `build` extension is a concrete example. It coordinates coding
The bundled `ship` extension is a concrete example. It coordinates coding
agents through tickets and GitHub pull requests, but GitHub PR orchestration is
`build` behavior—not the definition of Druks.
`ship` behavior—not the definition of Druks.

## Documentation

Expand Down
2 changes: 1 addition & 1 deletion backend/druks/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def get_timeout(self) -> int:
return min(resolved, MAX_AGENT_TIMEOUT_SECONDS)

async def __call__(self, **context: object) -> Any:
"""Run the agent — ``await Build.implement(...)`` — as a durable step in the
"""Run the agent — ``await Ship.implement(...)`` — as a durable step in the
current workflow and return its parsed output. An agent run is always memoized —
this picks which step does it: its own, or the @step it's already inside.
workflow_id comes from the workflow context, not the caller; everything
Expand Down
2 changes: 1 addition & 1 deletion backend/druks/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ async def _unhandled_exception_handler(


# Platform-core routers, mounted by hand at their own prefixes. Extension routers
# (core, build, usage, …) are discovered and mounted under /api/<extension> by load().
# (core, ship, usage, …) are discovered and mounted under /api/<extension> by load().
# /api sits behind the identity gate except the identity/connection surface and
# the health probe; /_external routes carry their own authentication. The
# boundary test pins the split. The auth and harness-connection routers mount
Expand Down
File renamed without changes.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
from pydantic import BaseModel, Field, model_validator

from druks.agents import AgentOutput
from druks.build.enums import (
from druks.contrib.ship.enums import (
EvaluationVerdict,
HumanFeedbackAction,
ReviewDecision,
)
from druks.workflows import Gate, Workflow

if TYPE_CHECKING:
from druks.build.workflows import BuildWorkflow
from druks.contrib.ship.workflows import Build


class ReviewWork(Gate):
Expand All @@ -25,7 +25,7 @@ class ReviewWork(Gate):

@classmethod
async def on_wait(cls, workflow: Workflow) -> None:
build = cast("BuildWorkflow", workflow)
build = cast("Build", workflow)
await build.set_pr_draft(draft=False)
await build.request_assignee_review()

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from pydantic import BaseModel, Field

from druks.agents import Agent
from druks.build.contracts import (
from druks.contrib.ship.contracts import (
CodeReviewOutput,
ContractRevisionOutput,
EvaluationOutput,
Expand All @@ -11,8 +11,8 @@
ReviewOutput,
TriageOutput,
)
from druks.build.models import WorkItem
from druks.build.schemas import WorkItemSummary
from druks.contrib.ship.models import WorkItem
from druks.contrib.ship.schemas import WorkItemSummary
from druks.db import db_session
from druks.events import Event, FeedItem
from druks.extensions import Extension
Expand All @@ -24,10 +24,10 @@
}


class Build(Extension):
name = "build"
class Ship(Extension):
name = "ship"
subject = WorkItem
# build's tables (projects, work_items, ...) are already unprefixed in core's
# These tables (projects, work_items, ...) are already unprefixed in core's
# migration history, so they must stay that way.
prefix_tables = False
icon = "hammer"
Expand Down Expand Up @@ -78,50 +78,50 @@ def resting_status(cls, source: str) -> str:
# them. The attribute name is each agent's id (its durable settings/timeline key).
generate_plan = Agent(
description="ticket → implementation plan",
prompt="build/build_workflow/generate_plan.md",
prompt="ship/build/generate_plan.md",
contract=PlanOutput,
model="codex",
)
review_plan = Agent(
description="critiques the plan before any work starts",
prompt="build/build_workflow/review_plan.md",
prompt="ship/build/review_plan.md",
contract=ReviewOutput,
model="claude",
)
revise_contract = Agent(
description="revises the plan contract on feedback",
prompt="build/build_workflow/revise_contract.md",
prompt="ship/build/revise_contract.md",
contract=ContractRevisionOutput,
model="codex",
)
implement = Agent(
description="plan → diff, in a drukbox",
prompt="build/build_workflow/implement.md",
prompt="ship/build/implement.md",
contract=ImplementationOutput,
model="claude",
)
evaluate_implementation = Agent(
description="adversarial review of the diff",
prompt="build/build_workflow/evaluate_implementation.md",
prompt="ship/build/evaluate_implementation.md",
contract=EvaluationOutput,
model="codex",
effort="medium",
)
review_code = Agent(
description="line-level code review on the PR",
prompt="build/build_workflow/review_code.md",
prompt="ship/build/review_code.md",
contract=CodeReviewOutput,
model="claude",
)
triage_human_feedback = Agent(
description="routes a human's PR feedback back into the workflow",
prompt="build/build_workflow/triage_human_feedback.md",
prompt="ship/build/triage_human_feedback.md",
contract=TriageOutput,
model="codex",
)
repo_profiler = Agent(
description="reads a repo once and reports its stack, verification commands, and skills",
prompt="build/profile/repo_profiler.md",
prompt="ship/profile/repo_profiler.md",
contract=RepoProfilerOutput,
model="codex",
)
Expand Down Expand Up @@ -157,7 +157,7 @@ def format_event(cls, event: Event) -> FeedItem:
id=f"event:{event.id}",
at=event.created_at,
kind=kind,
source=run_kind or "build",
source=run_kind or "ship",
summary=summary,
link_path=f"/work-items/{wid}" if wid else None,
meta={"ticketRef": ticket_ref} if ticket_ref else {},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from contextlib import suppress

from druks.build.contracts import (
from druks.contrib.ship.contracts import (
EvaluationOutput,
ImplementationOutput,
PlanData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship

from druks.build.enums import HandoffStatus
from druks.build.policy import RepoPolicy
from druks.contrib.ship.enums import HandoffStatus
from druks.contrib.ship.policy import RepoPolicy
from druks.core.apis.github import get_github_client
from druks.db import Base, Subject, db_session
from druks.settings import load_settings
Expand Down Expand Up @@ -284,22 +284,22 @@ def get_for_branch(cls, *, repo: str, branch: str) -> "WorkItem | None":
async def ship(self) -> None:
# A build parked on the operator's review is stranded by their merge; a running
# one converges on its own, its merge step finding the PR already closed.
from druks.build.workflows import BuildWorkflow
from druks.contrib.ship.workflows import Build

self.set_status(HandoffStatus.SHIPPED)
build = get_subject_status(self.subject_type, str(self.id), kind=BuildWorkflow.kind)
build = get_subject_status(self.subject_type, str(self.id), kind=Build.kind)
if build.is_parked:
await BuildWorkflow.cancel(self, failure="pr merged while parked")
await Build.cancel(self, failure="pr merged while parked")
await self.set_remote_status(TicketStatus.DONE)

async def close_external(self) -> None:
# The attempt was abandoned, not the ticket, so the ticket returns to the
# provider's resting pool. Branch cleanup is best-effort: a fetch failure
# must not strand it there.
from druks.build.workflows import BuildWorkflow
from druks.contrib.ship.workflows import Build

self.set_status(HandoffStatus.CANCELLED, event_payload={"external": True})
await BuildWorkflow.cancel(self, failure="pr closed without merge")
await Build.cancel(self, failure="pr closed without merge")
db_session().flush()
try:
if (await RepoPolicy.resolve(self.repo)).delete_branch:
Expand Down Expand Up @@ -343,9 +343,9 @@ def set_status(
call-site convention. None clears the lane on (re)dispatch: no event."""
if status:
# cycle: the extension imports this module at file scope.
import druks.build.extension as build_extension
import druks.contrib.ship.extension as ship_extension

build_extension.Build.record_event(type=status, subject=self, payload=event_payload)
ship_extension.Ship.record_event(type=status, subject=self, payload=event_payload)
self.status = status
self.updated_at = Base.utc_now()
db_session().flush()
Expand All @@ -354,13 +354,13 @@ async def set_remote_status(self, status: TicketStatus) -> None:
# No-op for sources without a configured tracker (github, absent creds).
if not is_tracker_source(self.source):
return
# Lazy: the Build extension imports this module, so it can't be imported at top.
import druks.build.extension as build_extension
# Lazy: the Ship extension imports this module, so it can't be imported at top.
import druks.contrib.ship.extension as ship_extension

try:
tracker = get_tracker(
self.source,
ready_for_agent_status=build_extension.Build.resting_status(self.source),
ready_for_agent_status=ship_extension.Ship.resting_status(self.source),
)
except TrackerNotConfigured:
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class VerificationProfile(BaseModel):


class RepoPolicy(BaseModel):
"""The operator's ``.druks/build/config.yml``, validated whole so a typo'd
"""The operator's ``.druks/ship/config.yml``, validated whole so a typo'd
key fails loud at resolution."""

model_config = {"frozen": True, "extra": "forbid"}
Expand All @@ -43,7 +43,7 @@ class RepoPolicy(BaseModel):

@classmethod
async def resolve(cls, repo: str | None) -> "RepoPolicy":
return await resolve_extension_config("build", repo=repo, model=cls)
return await resolve_extension_config("ship", repo=repo, model=cls)

def plan_approval_gate(self, auto_dispatch: bool) -> GateValue:
return self.gates.plan_approval or ("none" if auto_dispatch else "human")
Expand All @@ -63,7 +63,7 @@ async def verification_block(self, *, profile: dict[str, Any], repo: str | None)
{"label": "Tests", "commands": verification.get("test_commands", [])},
]
body = await render_prompt(
"build/verification_block.md",
"ship/verification_block.md",
repo=repo,
sections=sections,
has_commands=any(section["commands"] for section in sections),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from dataclasses import dataclass

from druks.build.journal import BuildJournal
from druks.build.models import ProjectRepo
from druks.contrib.ship.journal import BuildJournal
from druks.contrib.ship.models import ProjectRepo


@dataclass(frozen=True)
class BuildPromptContext:
"""What build's prompt templates render against — the run's identity facts
plus its journal, assembled per agent call by ``BuildWorkflow.get_prompt_context``.
plus its journal, assembled per agent call by ``Build.get_prompt_context``.
The templates read ``build.<field>`` and nothing else off the run, so this is
the whole contract between the workflow and its prompts; the workflow itself
stays free of template accessors.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from fastapi import APIRouter, Body, HTTPException, Query, Response, status
from sqlalchemy import func, select, update

from druks.build.models import Project, ProjectRepo, WorkItem
from druks.build.schemas import (
from druks.contrib.ship.models import Project, ProjectRepo, WorkItem
from druks.contrib.ship.schemas import (
AddProjectRepoRequest,
CreateProjectRequest,
DashboardItem,
Expand All @@ -15,15 +15,15 @@
ProjectSummary,
WorkItemsHistoryResponse,
)
from druks.build.workflows import Profile
from druks.contrib.ship.workflows import Profile
from druks.core.apis.github import get_github_client
from druks.db import db_session
from druks.settings import load_settings

logger = logging.getLogger(__name__)


# /api/build/projects Project / ProjectRepo
# /api/ship/projects Project / ProjectRepo

projects_router = APIRouter(prefix="/projects", tags=["projects"])

Expand Down Expand Up @@ -228,7 +228,7 @@ async def delete_project_repo(project_id: int, repo_id: int) -> None:
session.flush()


# /api/build/work-items WorkItem CRUD
# /api/ship/work-items WorkItem CRUD

work_items_router = APIRouter(prefix="/work-items", tags=["work-items"])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from pydantic import BaseModel, Field

from druks.build.models import Project, ProjectRepo, WorkItem
from druks.contrib.ship.models import Project, ProjectRepo, WorkItem
from druks.schemas import BaseResponse
from druks.workflows import RunState, SubjectSummary

Expand Down Expand Up @@ -98,7 +98,7 @@ def from_work_item(cls, item: WorkItem) -> "Links":


class WorkItemSummary(SubjectSummary):
# The work item's domain header — what only build knows. Status (where it is
# The work item's domain header — what only Ship knows. Status (where it is
# in its lifecycle) and the timeline come from the platform's subject read-side,
# which composes this with them; ``id`` is the platform subject key (str).
source: Literal["linear", "github", "jira"]
Expand Down
Loading