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
46 changes: 27 additions & 19 deletions src/command_system/workflows_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,32 +63,40 @@ def _workflow_to_command(path: Path, loaded_from: str) -> Optional[PromptCommand


_ULTRACODE_DIRECTIVE = (
"The user invoked /ultracode — they are explicitly opting into multi-agent "
'workflow orchestration and want you to AUTHOR and run a workflow (a "pipeline") '
"for the task below, instead of doing it turn by turn.\n\n"
"Design a workflow script for the task: decompose it into phases, fan out "
"subagents for the independent parts (parallel/pipeline), and adversarially verify "
"findings before synthesizing. Then launch it by calling the Workflow tool with "
"your script passed inline as `script` (see the Workflow tool's own description for "
"the script shape — it begins with `export const meta = {...}`). If the task below "
"is empty, ask the user what they want the workflow to do instead of guessing.\n\n"
"The Workflow tool launches the run in the BACKGROUND and returns a `run_id` "
"immediately. As soon as it returns, STOP: reply with one short sentence confirming "
"the workflow started (mention the run_id) and END YOUR TURN. Do NOT wait for it, "
"poll it, call another tool, or do the work yourself — the finished result is "
"delivered automatically when the run completes.\n\n"
"The user invoked /ultracode — AUTHOR a reusable multi-agent workflow (a "
'"pipeline") for the task below and SAVE it as a slash command. Do NOT run it '
"now, and do NOT call the Workflow tool — the user will run it themselves with "
"`/<name>` (the same way `/deep-research` runs).\n\n"
"Steps:\n"
"1. Read `src/workflow/bundled/deep_research.py` first and mirror its exact "
"format: a top-level `meta = {\"name\": ..., \"description\": ..., \"phases\": "
"[{\"title\": ...}, ...]}` dict, then an async body that uses ONLY the injected "
"primitives — `await agent(prompt, schema=...)`, `parallel`, `pipeline`, "
"`phase`, `log`, `budget` — and ends with `return <result>`. It is sandboxed "
"Python: no `import`, no `open`, no `Date.now()`/`random` (the runtime withholds "
"them).\n"
"2. Design the workflow for the task: decompose into phases, fan out subagents "
"for the independent parts, verify, then synthesize. Give `meta.description` a "
"clear one-line summary — it becomes the slash command's description.\n"
"3. Choose a short kebab-case name (e.g. `hn-scraper`) and Write the script to "
"`.claude/workflows/<name>.py` with the Write tool (create the directory if "
"needed). The filename stem IS the command name.\n"
"4. Reply in two or three lines: confirm the workflow is saved and tell the user "
"to run it with `/<name> <args>` (runs in the background like /deep-research). "
"If the task below is empty, ask what the workflow should do instead of guessing.\n\n"
"Author and SAVE only — do not launch it.\n\n"
"Task:\n$ARGUMENTS"
)


def _ultracode_command() -> PromptCommand:
"""The ``/ultracode`` command: a discoverable, autocompleting slash form of the
``ultracode`` authoring keyword. ``/ultracode <task>`` directs the model to
author a fresh workflow script for the task and launch it via the Workflow tool
(vs. ``/deep-research`` / saved ``/<name>``, which run an *existing* script)."""
"""The ``/ultracode`` command: author a fresh workflow and SAVE it as a reusable
``/<name>`` slash command (it does **not** run — the user launches it later with
``/<name>``, exactly like ``/deep-research``). ``/deep-research`` and saved
``/<name>`` run an *existing* script; ``/ultracode`` is the *generator*."""
return PromptCommand(
name="ultracode",
description="Author and run a multi-agent workflow (pipeline) for a task",
description="Author a multi-agent workflow (pipeline) and save it as a /<name> command",
kind="workflow",
loaded_from="bundled",
source="bundled",
Expand Down
39 changes: 39 additions & 0 deletions src/repl/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,10 @@ def _get_mcp_servers_for_prompt() -> list[str]:
# permission dialog or an in-progress chat turn.
self._at_prompt = False
self._notif_stop: threading.Event | None = None
# mtime signature of the .claude/workflows dirs, so a workflow just
# authored via /ultracode is picked up as a /<name> command mid-session
# (see _refresh_workflow_commands) without re-globbing every turn.
self._wf_dirs_sig: tuple | None = None
# Permission dialogs can be requested from different worker paths
# (e.g. subagents/tools). Serialize interactive prompts so we never
# mount competing prompt_toolkit applications at once.
Expand Down Expand Up @@ -1982,6 +1986,38 @@ def _notification_watcher(self, stop: threading.Event) -> None:
pass
stop.wait(0.4)

def _refresh_workflow_commands(self) -> None:
"""Pick up newly-saved ``.claude/workflows/*.py`` as ``/<name>`` commands
without a restart — so a workflow just authored via ``/ultracode`` is
runnable this session, exactly like ``/deep-research``.

mtime-gated on the project + personal workflow dirs so it only re-discovers
when one actually changes (idempotent: the shadowing guard in
``load_and_register_workflows`` skips already-registered names)."""
try:
from pathlib import Path

dirs = [Path.cwd() / ".claude" / "workflows", Path.home() / ".claude" / "workflows"]
sig = tuple((str(d), d.stat().st_mtime_ns) for d in dirs if d.is_dir())
except Exception:
return
if sig == self._wf_dirs_sig:
return
self._wf_dirs_sig = sig
try:
from src.command_system.workflows_integration import load_and_register_workflows

# Global registry → dispatch (`/<name> <args>`) + autocomplete.
load_and_register_workflows(registry=None)
# Local registry → bare `/<name>` routes to execution (parity with
# /deep-research), not the slash palette.
registry = getattr(self, "command_registry", None)
if registry is not None:
load_and_register_workflows(registry=registry)
self._update_built_in_commands_with_command_system()
except Exception:
pass

def _start_notification_watcher(self) -> None:
if self._notif_stop is not None:
return
Expand Down Expand Up @@ -2332,6 +2368,9 @@ def run(self):

while True:
try:
# Pick up a workflow just authored via /ultracode so its /<name>
# command is runnable this turn (before the completer rebuilds).
self._refresh_workflow_commands()
self._refresh_completer()
# Surface any background task (workflow/agent) that finished
# since the last turn: print its banner and let the agent
Expand Down
56 changes: 50 additions & 6 deletions tests/test_ultracode.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,18 +162,62 @@ def test_ultracode_is_a_registered_command():
assert getattr(cmd, "kind", None) == "workflow"


def test_ultracode_command_directive_authors_and_launches():
def test_ultracode_command_directive_authors_and_saves():
from src.command_system.workflows_integration import _ultracode_command

directive = _ultracode_command().markdown_content or ""
assert "AUTHOR" in directive # author a fresh workflow…
assert "Workflow tool" in directive # …and launch it via the tool
assert "$ARGUMENTS" in directive # the task is substituted in
assert "END YOUR TURN" in directive # background run → don't block
cmd = _ultracode_command()
directive = cmd.markdown_content or ""
assert "AUTHOR" in directive # author a fresh workflow…
assert ".claude/workflows" in directive # …saved as a /<name> command
assert "Write" in directive # via the Write tool
assert "do NOT call the Workflow tool" in directive # NOT run immediately
assert "deep_research.py" in directive # format template referenced
assert "$ARGUMENTS" in directive # the task is substituted in
# description reflects "save as a /<name> command", not "run"
desc = (cmd.description or "").lower()
assert "save" in desc and "/<name>" in desc


def test_ultracode_command_absent_when_workflows_disabled(monkeypatch):
from src.command_system.builtins import get_builtin_commands

monkeypatch.setenv("CLAUDE_CODE_DISABLE_WORKFLOWS", "1")
assert "ultracode" not in _names(get_builtin_commands())


# ── in-session pickup of a freshly-authored /<name> workflow ──────────────────


def test_refresh_workflow_commands_picks_up_new_file(tmp_path, monkeypatch):
"""A workflow saved into .claude/workflows/ mid-session becomes a /<name>
command without a restart (the /ultracode → /<name> handoff)."""
from src.command_system.registry import CommandRegistry, get_command_registry
from src.repl.core import ClawcodexREPL

monkeypatch.chdir(tmp_path)
monkeypatch.setattr(Path, "home", lambda: tmp_path / "home") # empty personal dir

wfdir = tmp_path / ".claude" / "workflows"
wfdir.mkdir(parents=True)
(wfdir / "ultratest-scraper.py").write_text(
'meta = {"name": "ultratest-scraper", "description": "scrape something"}\n'
'return await agent("x")\n',
encoding="utf-8",
)

repl = ClawcodexREPL.__new__(ClawcodexREPL)
repl._wf_dirs_sig = None
repl.command_registry = CommandRegistry()
repl._original_built_ins = []
repl._built_in_commands = []

repl._refresh_workflow_commands()

# dispatchable via the global registry, and bare /<name> routes to execution
assert get_command_registry().get("ultratest-scraper") is not None
assert "/ultratest-scraper" in repl._built_in_commands

# mtime-gated: a second call with no dir change is a no-op (signature unchanged)
sig_after = repl._wf_dirs_sig
repl._refresh_workflow_commands()
assert repl._wf_dirs_sig == sig_after