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
2 changes: 1 addition & 1 deletion src/panopticon/container/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def main(
return
cli.render_skills(client, task_id, config_dir.parent)
cli.render_operations(client, task_id, config_dir.parent) # advance/drop/… as slash-commands
cli.write_settings(config_dir.parent) # turn-flip hooks → <home>/<config_dirname>/settings
cli.write_settings(config_dir.parent) # wire turn-flip hooks where the CLI supports them
cli.write_mcp_config(config_dir, service_url) # point the CLI at the task service's MCP server
cli.write_workflow_overview(
config_dir, client.workflow_overview(task_id)
Expand Down
16 changes: 3 additions & 13 deletions src/panopticon/container/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
"""The **agent-CLI adapter package** (ADR 0014): the :class:`AgentCLI` seam + registry, and one
module per CLI.

Panopticon drives one agent CLI today (`claude`); Milestone 3 adds others (Codex first). The
launcher (:mod:`panopticon.container.agent`) is CLI-agnostic — it drives a deterministic *bootstrap*
(render skills + turn-flip hooks, wire MCP, seed trust) then a *launch* (exec the real CLI) against
an :class:`AgentCLI` adapter, holding no ``claude`` literal. :mod:`~panopticon.container.cli.base`
holds the ABC + the name-keyed registry; each concrete adapter is its own module
(:mod:`~panopticon.container.cli.claude` and :mod:`~panopticon.container.cli.codex`). A second CLI drops in by
implementing the ABC and registering under its name (the drop-in shape of workflow discovery, ADR 0004).

The package lives **inside** ``container/`` — the sole LLM-bearing package — so the determinism
invariant holds (ADR 0014 §6): the control plane runs no CLI-specific logic.
"""The **agent-CLI adapter package** (ADR 0014). See :mod:`~panopticon.container.cli.base` for the
:class:`AgentCLI` seam + registry; concrete adapters are :mod:`~panopticon.container.cli.claude` and
:mod:`~panopticon.container.cli.codex`.
"""

from panopticon.container.cli.base import (
Expand Down
4 changes: 2 additions & 2 deletions src/panopticon/container/cli/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@ def read_hook_payload(self, stdin: TextIO) -> dict[str, Any]:

@abstractmethod
def has_live_background_task(self, payload: dict[str, Any]) -> bool:
"""Whether the Stop payload reports still-running background work (gates the turn flip)."""
"""Whether the hook payload reports still-running background work (gates the turn flip)."""

@abstractmethod
def launch(self, config_dir: Path) -> None:
"""Exec the real CLI in the foreground (resuming if a session exists); return when it exits."""
"""Exec the real CLI in the foreground; return when it exits."""


#: The adapter registry, keyed by CLI name. Adding a CLI is: implement :class:`AgentCLI`, register
Expand Down
2 changes: 1 addition & 1 deletion src/panopticon/container/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def settings() -> dict[str, Any]:

def run(actor: str, event: str | None = None, *, matcher: str | None = None) -> dict[str, Any]:
# `actor` is the turn to set; the optional `event` selects the callback's side-effect
# (briefing on the prompt hook, token report on stop) — the bare question hooks pass none.
# (briefing on the prompt hook; background-task gating of the turn flip on stop) — the bare question hooks pass none.
command = f"{HOOK_COMMAND} {actor}" + (f" {event}" if event else "")
entry: dict[str, Any] = {"hooks": [{"type": "command", "command": command}]}
if (
Expand Down
2 changes: 1 addition & 1 deletion src/panopticon/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ class Task:
#: lives in the task's plan artifact). Distinct from the ``slug`` (a short identifier the
#: agent sets later); ``None`` when the creator gave none.
memo: str | None = None
#: Optional text prefilled (unsent) into Claude's input box on the task's first spawn,
#: Optional text prefilled (unsent) into the agent CLI's input box on the task's first spawn,
#: taking precedence over ``memo`` for that purpose. ``None`` until set at creation.
initial_prompt: str | None = None
slug: str | None = None
Expand Down
2 changes: 1 addition & 1 deletion src/panopticon/core/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ def start_task(

The seed history entry carries the initial state's responsibilities (all ``PENDING``).
``memo`` is the optional brief one-line reminder of what the task is, collected at creation.
``initial_prompt`` is optional text prefilled into Claude's input box on first spawn.
``initial_prompt`` is optional text prefilled into the agent CLI's input box on first spawn.
"""
state = self.initial_label
return Task(
Expand Down
4 changes: 2 additions & 2 deletions src/panopticon/sessionservice/local_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,8 @@ def _report(phase: LifecyclePhase) -> None:
**self._extra_env,
}
if initial_prompt:
# The agent launcher reads this and passes it as a positional arg to `claude` on the
# first run (no prior session), so the agent's first action is to process the prompt.
# The agent launcher reads this and passes it as a positional arg to the resolved agent
# CLI on the first run (no prior session), so the agent's first action is to process the prompt.
env["PANOPTICON_INITIAL_PROMPT"] = initial_prompt
if turn:
env["PANOPTICON_TASK_TURN"] = turn
Expand Down
4 changes: 2 additions & 2 deletions src/panopticon/sessionservice/spawner.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,11 @@ def _spawn_container(self, task: JsonObj, repo: JsonObj) -> str:
docker_in_docker=bool((repo.get("capabilities") or {}).get("docker_in_docker")),
initial_prompt=task.get(
"initial_prompt"
), # passed as a CLI arg to claude on the first run
), # passed as a CLI arg to the agent CLI on the first run
turn=task.get("turn"), # agent's turn → INTERRUPT_PROMPT on respawn
starting_model=task.get(
"starting_model"
), # model selection passed to claude --model on first launch
), # model selection passed to the agent CLI on first launch
agent_cli=agent_cli, # picks the launcher's adapter + the config-mount path
progress=lambda phase: self._report(task_id, phase), # STARTING then AWAITING
)
Expand Down
4 changes: 3 additions & 1 deletion src/panopticon/taskservice/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ class TaskOut(BaseModel):
memo: (
str | None
) # a brief one-line reminder of what the task is, collected at creation (shown in the summary)
initial_prompt: str | None # optional text prefilled into Claude's input box on first spawn
initial_prompt: (
str | None
) # optional text prefilled into the agent CLI's input box on first spawn
slug: str | None
url: str | None # an optional external URL (PR, issue, …); the dashboard's `p` hotkey opens it
snoozed_until: str | None = (
Expand Down
6 changes: 3 additions & 3 deletions src/panopticon/taskservice/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ async def set_dependencies(task_id: str, dep_ids: list[str]) -> dict[str, Any]:
"workflows). The task is created in your own repo. Pass your own task id as "
"orchestrator_task_id. The `memo` is a brief reminder of what the task "
"is (shown in the dashboard) — not a full description; the full description goes "
"in the task's plan.md. `initial_prompt` (optional) is passed as Claude's first "
"message on first spawn — the agent starts autonomously without waiting for user "
"in the task's plan.md. `initial_prompt` (optional) is prefilled as the agent's first "
"prompt on first spawn — the agent starts autonomously without waiting for user "
'input, e.g. "review your plan". `artifacts` '
"(optional) is a name→content map of text artifacts to write immediately (e.g. "
'{"plan.md": "..."}) — written before the call returns so the spawner always '
Expand Down Expand Up @@ -167,7 +167,7 @@ async def list_workflows(orchestrator_task_id: str) -> list[str]:
description=(
"Write (create or overwrite) a task artifact, e.g. the plan. Returns its URI. "
"Pass text in `content`; for a binary artifact (e.g. a screenshot) pass base64 in "
"`content_base64` instead — supply exactly one. For a large binary the token-cheap "
"`content_base64` instead — supply exactly one. For a large binary the more efficient "
"path is the REST endpoint (PUT /tasks/{id}/artifacts/{name} with the raw bytes), which "
"keeps the base64 out of your context."
)
Expand Down
3 changes: 1 addition & 2 deletions src/panopticon/workflows/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
workflows.spike.Spike`) whose agent decomposes a high-level request into a batch of child
tasks and **seeds each one ready for the user to approve**: it creates the task, writes its
`plan.md` artifact, sets its slug, marks the child's `plan-written` responsibility met, and
hands the child's turn to the user. The user
then only has to review the plan and advance
hands the child's turn to the user. The user then only has to review the plan and advance
(`PLANNING → ITERATING`). The motivating use is fanning out `github-self-reviewed` /
`github-peer-reviewed` tasks that arrive pre-planned.

Expand Down
Loading