Skip to content

Commit 19a9eca

Browse files
tildesrcclaude
andcommitted
feat(core): lifecycle engine, workflow + state classes, Spike seed + CI
Slice 1, PR 1 of 4 β€” the core contracts the rest of the slice builds on. - panopticon.core.models: domain types (Task, Repo[+git_url], HistoryEntry, Turn, Mode, Status, Responsibility) β€” pure data, no clock, no I/O. - panopticon.core.state: states as classes β€” State (non-terminal, inherits a DROPPED transition) / TerminalState, with built-in Complete/Dropped terminals. - panopticon.core.workflow: the Workflow interface owns the whole state machine β€” nested State classes discovered and their string/class transitions resolved + validated at instantiation; queries (transitions, can_transition, is_terminal, …); and task lifecycle (start_task, apply_transition with turn tracking and per- responsibility gating). Dropping is the universal escape hatch and bypasses gating. - panopticon.workflows.Spike: the minimal seed workflow. - Golden tests for resolution/gating; a determinism test forbidding LLM imports. - GitHub Actions CI: uv sync, mypy, pytest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7b401d7 commit 19a9eca

15 files changed

Lines changed: 1217 additions & 0 deletions

File tree

β€Ž.gitignoreβ€Ž

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,13 @@
11
# Editor / Obsidian vault metadata
22
.obsidian/
3+
4+
# Python
5+
__pycache__/
6+
*.py[cod]
7+
.venv/
8+
.mypy_cache/
9+
.pytest_cache/
10+
.ruff_cache/
11+
*.egg-info/
12+
dist/
13+
build/

β€ŽREADME.mdβ€Ž

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# panopticon
2+
3+
Orchestrate multiple coding agents across isolated tasks and **configurable workflows**.
4+
5+
A ground-up rewrite of the [cloude-cade](https://github.com/tildesrc/cloude-cade)
6+
prototype. The design lives on the [`design-docs`](../../tree/design-docs) branch
7+
(goals, parity analysis, architecture, roadmap, and ADRs).
8+
9+
## Architecture in one paragraph
10+
11+
A deterministic control plane (the **task service**) owns task state and drives
12+
per-workflow state machines; a per-machine **runner** spawns task containers and host
13+
tmux sessions; a **terminal controller** runs the dashboard. **All LLM calls happen
14+
inside task containers** β€” the control plane, runner, and dashboard never call a model.
15+
See the `design-docs` branch for the full picture.
16+
17+
## Status
18+
19+
Early development. Building Milestone 1 in vertical slices (see the roadmap). This slice
20+
lands the core contracts:
21+
22+
- `panopticon.core` β€” domain models, the workflow port (`Workflow` ABC), and the
23+
deterministic lifecycle engine (state machine, turn tracking, responsibility gating).
24+
- `panopticon.workflows.FreeFormWorkflow` β€” the minimal seed workflow.
25+
26+
## Development
27+
28+
```sh
29+
uv sync # create the venv and install dev deps
30+
uv run pytest # run tests
31+
uv run mypy -p panopticon # type-check
32+
```

β€Žpyproject.tomlβ€Ž

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[project]
2+
name = "panopticon"
3+
version = "0.0.1"
4+
description = "Orchestrate multiple coding agents across isolated tasks and configurable workflows."
5+
requires-python = ">=3.11"
6+
dependencies = []
7+
8+
[dependency-groups]
9+
dev = [
10+
"pytest>=8",
11+
"mypy>=1.11",
12+
]
13+
14+
[build-system]
15+
requires = ["hatchling"]
16+
build-backend = "hatchling.build"
17+
18+
[tool.hatch.build.targets.wheel]
19+
packages = ["src/panopticon"]
20+
21+
[tool.pytest.ini_options]
22+
testpaths = ["tests"]
23+
addopts = "-ra"
24+
25+
[tool.mypy]
26+
python_version = "3.11"
27+
strict = true
28+
mypy_path = "src"
29+
namespace_packages = true
30+
explicit_package_bases = true

β€Žsrc/panopticon/__init__.pyβ€Ž

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""panopticon β€” keep an eye on your agents."""
2+
3+
__version__ = "0.0.1"
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Core domain: models, state classes, and the workflow interface (the state machine).
2+
3+
Nothing in this package performs I/O or calls an LLM (enforced by a determinism test).
4+
"""
5+
6+
from panopticon.core.models import (
7+
HistoryEntry,
8+
Mode,
9+
Repo,
10+
Responsibility,
11+
Status,
12+
Task,
13+
Turn,
14+
)
15+
from panopticon.core.state import (
16+
BaseState,
17+
Complete,
18+
Dropped,
19+
State,
20+
TerminalState,
21+
)
22+
from panopticon.core.workflow import (
23+
IllegalTransition,
24+
InvalidWorkflow,
25+
ResolvedState,
26+
ResponsibilitiesNotMet,
27+
Workflow,
28+
)
29+
30+
__all__ = [
31+
"BaseState",
32+
"Complete",
33+
"Dropped",
34+
"HistoryEntry",
35+
"IllegalTransition",
36+
"InvalidWorkflow",
37+
"Mode",
38+
"Repo",
39+
"ResolvedState",
40+
"ResponsibilitiesNotMet",
41+
"Responsibility",
42+
"State",
43+
"Status",
44+
"Task",
45+
"TerminalState",
46+
"Turn",
47+
"Workflow",
48+
]
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""Core domain models β€” pure data, no I/O, no LLM.
2+
3+
These types are the vocabulary the whole system shares. They deliberately carry no
4+
behavior beyond holding state; the rules live in :class:`panopticon.core.workflow.Workflow`, and
5+
the state classes live in :mod:`panopticon.core.state`.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from dataclasses import dataclass, field
11+
from enum import Enum
12+
13+
14+
class Turn(str, Enum):
15+
"""Who currently holds the turn β€” who must act next before anything else happens."""
16+
17+
USER = "user"
18+
AGENT = "agent"
19+
20+
21+
class Mode(str, Enum):
22+
"""Per-state classification a workflow supplies; the workflow derives the turn from it."""
23+
24+
FOREGROUND = "foreground" # user-driven β€” the turn goes to the user on entry
25+
BACKGROUND = "background" # agent-driven β€” the turn goes to the agent on entry
26+
27+
28+
class Status(str, Enum):
29+
"""Resolution status of a single responsibility."""
30+
31+
PENDING = "pending" # not yet resolved β€” blocks handing the turn back
32+
MET = "met"
33+
FAILED = "failed" # could not be satisfied; requires a comment
34+
35+
36+
@dataclass(frozen=True)
37+
class Responsibility:
38+
"""An agent obligation for a state.
39+
40+
The workflow supplies these as *definitions* (``status`` ``PENDING``, no comment). The
41+
agent resolves each to ``MET`` or ``FAILED`` before handing the turn back; a ``FAILED``
42+
responsibility must carry a ``comment`` explaining why. They are agent-only β€” user
43+
actions drive transitions directly rather than being modelled as responsibilities.
44+
"""
45+
46+
key: str
47+
description: str
48+
status: Status = Status.PENDING
49+
comment: str | None = None
50+
51+
def resolve(self, status: Status, comment: str | None = None) -> Responsibility:
52+
"""Return a resolved copy carrying the definition's ``key``/``description``."""
53+
return Responsibility(
54+
key=self.key, description=self.description, status=status, comment=comment
55+
)
56+
57+
58+
@dataclass
59+
class Repo:
60+
"""A repository tasks operate on. Owns secret references (added in a later slice)."""
61+
62+
id: str
63+
name: str
64+
git_url: str
65+
default_base: str = "main"
66+
67+
68+
@dataclass(frozen=True)
69+
class HistoryEntry:
70+
"""One append-only entry in a task's transition log.
71+
72+
Timestamps are passed in by the caller (the task service stamps them); the core
73+
never reads the clock, which keeps the state machine deterministic and testable.
74+
``responsibilities`` holds the set the agent resolved on this transition (empty when the
75+
state being left defines none).
76+
"""
77+
78+
at: str # ISO-8601 timestamp, supplied by the caller
79+
from_state: str | None
80+
to_state: str
81+
via: str | None = None
82+
note: str | None = None
83+
responsibilities: tuple[Responsibility, ...] = ()
84+
85+
86+
@dataclass
87+
class Task:
88+
"""A unit of work. Identity is the internal ``id``; ``slug`` is a human label set later."""
89+
90+
id: str
91+
repo_id: str
92+
workflow: str
93+
state: str
94+
turn: Turn
95+
slug: str | None = None
96+
history: list[HistoryEntry] = field(default_factory=list)

β€Žsrc/panopticon/core/state.pyβ€Ž

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Workflow states, expressed as classes (declarative, Γ  la an ORM model).
2+
3+
A state is a **class** used as a type-level identity (never instantiated). Subclass:
4+
5+
* :class:`State` β€” a non-terminal state. It carries an inherited transition to
6+
:class:`Dropped`, so every task is always droppable; concrete states add their own
7+
``transitions`` (which accumulate with the inherited ``Dropped`` across the hierarchy).
8+
* :class:`TerminalState` β€” a terminal state (no outgoing transitions).
9+
10+
``transitions`` entries are other state **classes** or their ``label`` **strings**; strings
11+
are resolved to classes when the owning :class:`~panopticon.core.workflow.Workflow` is built
12+
(forward references β€” e.g. cycles β€” must use strings, as with an ORM relationship).
13+
"""
14+
15+
from __future__ import annotations
16+
17+
from abc import ABC
18+
from typing import ClassVar
19+
20+
from panopticon.core.models import Mode, Responsibility
21+
22+
TransitionRef = "type[BaseState] | str"
23+
24+
25+
class BaseState(ABC):
26+
"""Common base for workflow states β€” use :class:`State` or :class:`TerminalState`."""
27+
28+
#: Stable identifier β€” persisted in ``Task.state`` and shown on the dashboard.
29+
label: ClassVar[str]
30+
#: Foreground (user-driven) or background (agent-driven).
31+
mode: ClassVar[Mode]
32+
#: The agent's obligations while in this state (empty = ungated).
33+
responsibilities: ClassVar[tuple[Responsibility, ...]] = ()
34+
35+
36+
class TerminalState(BaseState):
37+
"""A terminal state: the task is finished here; no outgoing transitions.
38+
39+
Defaults ``mode`` to foreground β€” the turn returns to the user once a task is terminal.
40+
"""
41+
42+
mode: ClassVar[Mode] = Mode.FOREGROUND
43+
44+
45+
class Complete(TerminalState):
46+
"""Built-in terminal state for successfully finished tasks."""
47+
48+
label: ClassVar[str] = "COMPLETE"
49+
50+
51+
class Dropped(TerminalState):
52+
"""Built-in terminal state for abandoned tasks. Reachable from every non-terminal state."""
53+
54+
label: ClassVar[str] = "DROPPED"
55+
56+
57+
class State(BaseState):
58+
"""A non-terminal state.
59+
60+
Concrete states set ``label``, ``mode``, optionally ``responsibilities``, and
61+
``transitions``. The inherited transition to :class:`Dropped` makes every task droppable
62+
without each state having to declare it.
63+
"""
64+
65+
transitions: ClassVar[tuple[type[BaseState] | str, ...]] = (Dropped,)

0 commit comments

Comments
Β (0)