Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f295ab8
fix: kill 5 dead modes, route all auto-detection to design
mihirathale98 Aug 19, 2026
9b5ad42
docs: update CEO prompt and .claude/CLAUDE.md routing tables
mihirathale98 Aug 19, 2026
bb38e67
test: update all tests for dead mode removal
mihirathale98 Aug 19, 2026
8ac7146
fix: update remaining production code for dead mode removal
mihirathale98 Aug 19, 2026
b345c2f
fix: update visualizer, ceo_completion, and test assertions
mihirathale98 Aug 19, 2026
86810df
fix: correct remaining test assertions for design mode
mihirathale98 Aug 19, 2026
736681c
fix: update detect_incomplete test for design mode eval profile check
mihirathale98 Aug 19, 2026
776bce5
fix: update build_filters_by_cycle_start test assertion for design mode
mihirathale98 Aug 19, 2026
6033284
fix: only check eval profile in design mode, not research/meta
mihirathale98 Aug 19, 2026
8b54058
fix: update continuation mode directive assertion from BUILD to DESIGN
mihirathale98 Aug 19, 2026
37454b1
fix: update checkpoint format_full test assertion
mihirathale98 Aug 19, 2026
73b24d0
fix: replace last dead mode references in checkpoint tests
mihirathale98 Aug 19, 2026
b7e8b09
fix: use founder mode for auto_approve rejection test
mihirathale98 Aug 19, 2026
7f201e8
fix: remove dead build/discover entries from visualizer state
mihirathale98 Aug 19, 2026
b76ab80
test: fix remaining test failures after dead mode removal
mihirathale98 Aug 19, 2026
962bb3e
fix: update visualizer mode inference and test assertions to design
mihirathale98 Aug 19, 2026
0ec51b3
fix: update visualizer detect phase test for design mode
mihirathale98 Aug 19, 2026
e96a708
fix: remove dead mode entries from MODE_EVENT_TO_PHASE
mihirathale98 Aug 19, 2026
e6f90fa
fix: use founder mode in auto_approve rejection test
mihirathale98 Aug 19, 2026
68a952d
fix: use founder mode in from_plan rejection test
mihirathale98 Aug 19, 2026
731781d
fix: use founder mode in just_plan rejection test
mihirathale98 Aug 19, 2026
218b5a9
fix: update register_all count for removed dead modes
mihirathale98 Aug 21, 2026
6a474a2
test: cover dead mode migration paths in checkpoint and auto-detect
mihirathale98 Aug 21, 2026
cc092c1
test: cover _validate_ceo_flags aliases and run focus validation
mihirathale98 Aug 21, 2026
56110df
fix: drop static .claude/CLAUDE.md from PR scope
mihirathale98 Aug 21, 2026
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
22 changes: 8 additions & 14 deletions factory/agents/prompts/ceo.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,9 @@ You communicate directly with the user when running in foreground mode. You expl

**You MUST complete ALL planned work before exiting.** This applies to every mode:

- **Build mode:** All phases (B0–B6) must be attempted
- **Improve mode:** Every approved hypothesis must have a Builder keep/revert verdict
- **Discover mode:** The eval profile must be generated
- **Design mode:** All phases must be attempted (build, discover, and improve are handled inline)
- **Research mode:** Every approved hypothesis must have a verdict, or a termination condition must be met
- **Meta mode:** Same as Improve, plus ACE playbook evolution
- **Meta mode:** Same as Design, plus ACE playbook evolution

**Self-judged early exits are FORBIDDEN.** Do not exit because:
- "This is a good stopping point" — there are no stopping points, only completion
Expand Down Expand Up @@ -309,24 +307,20 @@ factory detect "$PROJECT_PATH"

| State | Meaning | Route to |
|------------------------|-----------------------------------------------|----------------|
| `no_repo` | No git repo at path | Build mode |
| `incomplete` | Repo exists, open plan/implementation issues | Build mode |
| `no_factory` | Repo exists, no factory setup | Discover mode |
| `evals_pending_review` | Eval profile exists, not yet reviewed | Review mode |
| `has_factory` | Factory fully initialized, evals reviewed | Improve mode |
| `no_repo` | No git repo at path | Design mode |
| `incomplete` | Repo exists, open plan/implementation issues | Design mode |
| `no_factory` | Repo exists, no factory setup | Design mode |
| `evals_pending_review` | Eval profile exists, not yet reviewed | Design mode |
| `has_factory` | Factory fully initialized, evals reviewed | Design mode |

### Step 2: Route to Mode via Skills

Each mode's full instructions live in a workflow skill under `skills/workflow-<name>/SKILL.md`. After detecting project state, select and invoke the appropriate skill.

**Default routing:**
- `no_repo` or `incomplete` → read `skills/workflow-build/SKILL.md`
- `no_factory` → read `skills/workflow-discover/SKILL.md`
- `evals_pending_review` → read `skills/workflow-review/SKILL.md`
- `has_factory` → read `skills/workflow-improve/SKILL.md`
- All project states → read `skills/workflow-design/SKILL.md`

**Mode overrides (from task directives):**
- `--mode design` or `## Plan Loop (Interactive)` → read `skills/workflow-design/SKILL.md`
- `--mode research` (with `research_target` configured) → read `skills/workflow-research/SKILL.md`
- `--mode meta` → read `skills/workflow-meta/SKILL.md`
- `--refine "<request>"` → read `skills/workflow-refine/SKILL.md`
Expand Down
62 changes: 8 additions & 54 deletions factory/ceo_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,66 +292,32 @@ def _detect_incomplete(
cycle_started_at: If provided, only counts experiments created after this time.
This prevents counting stale experiments from previous cycles.
"""
if mode in ("improve", "meta", "research"):
if mode in ("design", "meta", "research"):
planned = _count_hypotheses(project_path)
completed = _count_verdicts(project_path, since_ts=cycle_started_at)

if planned == 0:
# No strategy yet — not an incomplete cycle, probably discover mode
return None

if completed >= planned:
return None

next_h = completed + 1
reason_prefix = "research" if mode == "research" else "improve"
return IncompleteGap(
mode=mode,
planned=planned,
completed=completed,
next_item=f"H{next_h}",
reason=f"{reason_prefix}.incomplete: {completed}/{planned} hypotheses have verdicts",
)

elif mode == "discover":
if _has_eval_profile(project_path):
return None
return IncompleteGap(
mode=mode,
planned=1,
completed=0,
next_item="eval_profile",
reason="discover.incomplete: no eval_profile.json",
)

elif mode == "build":
# For build mode, check if Builder completed at least one hypothesis
# In build mode, the strategy file should have phases marked as hypotheses
planned = _count_hypotheses(project_path)
completed = _count_verdicts(project_path, since_ts=cycle_started_at)

if planned == 0:
# No strategy means we're in scaffold phase — check for eval profile
if not _has_eval_profile(project_path):
if mode == "design" and not _has_eval_profile(project_path):
return IncompleteGap(
mode=mode,
planned=1,
completed=0,
next_item="discovery",
reason="build.incomplete: no eval profile yet",
reason="design.incomplete: no eval profile yet",
)
return None

if completed >= planned:
return None

next_h = completed + 1
reason_prefix = "research" if mode == "research" else "design"
return IncompleteGap(
mode=mode,
planned=planned,
completed=completed,
next_item=f"Phase{next_h}",
reason=f"build.incomplete: {completed}/{planned} phases have verdicts",
next_item=f"H{next_h}",
reason=f"{reason_prefix}.incomplete: {completed}/{planned} hypotheses have verdicts",
)

# Unknown mode — assume complete
Expand Down Expand Up @@ -386,26 +352,14 @@ def _build_continuation_task(gap: IncompleteGap, cycle_state: CycleState | None
f"research cycle (R3–R5) for each remaining hypothesis. "
f"Progress so far: {gap.completed}/{gap.planned} hypotheses have verdicts."
)
elif gap.mode in ("improve", "meta"):
elif gap.mode in ("design", "meta"):
body = (
f"Resume execution from hypothesis {gap.next_item}. "
f"Resume execution from {gap.next_item}. "
f"Strategy is already approved at .factory/strategy/current.md — "
f"do not re-plan, do not re-run Researcher or Strategist. "
f"Spawn Builder for {gap.next_item} immediately. "
f"Progress so far: {gap.completed}/{gap.planned} hypotheses have verdicts."
)
elif gap.mode == "build":
body = (
f"Resume Build pipeline from {gap.next_item}. "
f"Plan is already approved at .factory/strategy/current.md. "
f"Progress so far: {gap.completed}/{gap.planned} phases complete. "
f"Continue with the next phase immediately."
)
elif gap.mode == "discover":
body = (
"Resume Discovery. The eval profile has not been generated yet. "
"Complete the Discover mode workflow to produce .factory/eval_profile.json."
)
else:
body = f"Resume from {gap.next_item}."

Expand Down
7 changes: 7 additions & 0 deletions factory/checkpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class CheckpointState(BaseModel):
def save_checkpoint(project_path: Path, state: CheckpointState) -> None:
"""Serialize checkpoint state to .factory/checkpoint.json."""
from factory.store import ensure_factory_dir

factory_dir = project_path / ".factory"
ensure_factory_dir(factory_dir)
checkpoint_path = factory_dir / _CHECKPOINT_FILE
Expand All @@ -45,6 +46,8 @@ def save_checkpoint(project_path: Path, state: CheckpointState) -> None:

def load_checkpoint(project_path: Path) -> CheckpointState | None:
"""Load checkpoint from .factory/checkpoint.json, or None if absent/corrupt."""
from factory.cli._helpers import DEAD_MODES

checkpoint_path = project_path / ".factory" / _CHECKPOINT_FILE
if not checkpoint_path.exists():
log.debug("checkpoint.not_found", path=str(checkpoint_path))
Expand All @@ -55,6 +58,10 @@ def load_checkpoint(project_path: Path) -> CheckpointState | None:
except (json.JSONDecodeError, Exception) as exc:
log.warning("checkpoint.corrupt", path=str(checkpoint_path), error=str(exc))
return None
if state.mode in DEAD_MODES:
old_mode = state.mode
state.mode = DEAD_MODES[old_mode]
log.warning("checkpoint.mode_migrated", old=old_mode, new=state.mode)
log.info("checkpoint.loaded", path=str(checkpoint_path))
return state

Expand Down
103 changes: 78 additions & 25 deletions factory/cli/_ceo_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def _tool_exec_protocol(wt_path: Path) -> str:
overview = ""
try:
from factory.workflow.tool import tool_overview

overview = tool_overview(p, fmt="linear")
except Exception:
pass
Expand All @@ -69,11 +70,7 @@ def _tool_exec_protocol(wt_path: Path) -> str:
)

if overview:
protocol += (
"\n## Workflow Map\n"
"\n"
f"{overview}\n"
)
protocol += f"\n## Workflow Map\n\n{overview}\n"

protocol += (
"\n## Commands\n"
Expand All @@ -90,20 +87,20 @@ def _tool_exec_protocol(wt_path: Path) -> str:
'1. Run "next" to see your current task — it tells you the node type, '
"role, and what to do\n"
"2. Execute the task:\n"
" - Agent nodes: run factory agent <role> --task \"...\" --project <path>\n"
' - Agent nodes: run factory agent <role> --task "..." --project <path>\n'
" - Study nodes: run the study command shown\n"
" - Function nodes: run the command shown\n"
'3. Run "next" again — the tool auto-detects that the previous node completed\n'
" (by checking for output files) and advances to the next task\n"
"4. Repeat until GATE or DONE\n"
"5. For GATE nodes: the tool asks you to evaluate — read the artifacts, then\n"
' call "submit" with your verdict (PROCEED, RETRY, or HALT)\n'
"6. If RETRY: the tool rewinds — run \"next\" to get the retry task\n"
'6. If RETRY: the tool rewinds — run "next" to get the retry task\n'
"7. If DONE: report completion\n"
"\n"
"## Important\n"
"\n"
"- For most nodes, just run the command and call \"next\" — the tool handles tracking\n"
'- For most nodes, just run the command and call "next" — the tool handles tracking\n'
'- Only call "submit" for gate verdicts (PROCEED/RETRY/HALT)\n'
"- The tool auto-detects agent completion via .factory/reviews/ files\n"
"- The tool auto-evaluates fn gates (precheck, guard) on your behalf\n"
Expand Down Expand Up @@ -144,16 +141,32 @@ def _tool_exec_protocol(wt_path: Path) -> str:

def _validate_ceo_flags(
args: argparse.Namespace,
) -> tuple[str, bool, bool, bool, str | None, str | None, str | None, str | None, bool, str | None, bool] | int:
) -> (
tuple[
str,
bool,
bool,
bool,
str | None,
str | None,
str | None,
str | None,
bool,
str | None,
bool,
]
| int
):
"""Validate and resolve top-level CLI flags. Returns parsed values or an error code."""
mode: str = getattr(args, "mode", "auto")
if mode == "interactive":
mode = "design"
if mode.startswith("project:"):
mode = mode[len("project:"):]
mode = mode[len("project:") :]
all_modes = get_all_ceo_modes()
if mode not in all_modes and mode != "auto":
from factory.workflow.registry import WorkflowRegistry

raw_path = getattr(args, "path", None)
project_path = Path(raw_path).resolve() if raw_path else Path.cwd()
entries = WorkflowRegistry.discover(project_path)
Expand Down Expand Up @@ -209,6 +222,7 @@ def _validate_ceo_flags(
raw_path = getattr(args, "path", None)
if not raw_path:
from factory.plugins import get_registry

plugin_registry = get_registry()
has_pre_hooks = bool(plugin_registry.ceo_pre_hooks)
if not has_pre_hooks:
Expand Down Expand Up @@ -294,7 +308,19 @@ def _validate_ceo_flags(
)
return 1

return (mode, headless, bg, bg_agents, prompt_file, focus, dir_name, refine_request, auto_approve, from_plan, just_plan)
return (
mode,
headless,
bg,
bg_agents,
prompt_file,
focus,
dir_name,
refine_request,
auto_approve,
from_plan,
just_plan,
)


# ── project resolution ────────────────────────────────────────
Expand Down Expand Up @@ -454,9 +480,23 @@ def _validate_late_flags(
)
return 1

if focus and mode not in ("improve", "research", "create", "evolve", "study", "frontend-design", "frontend-design-discover") and not design_existing and not just_plan:
if (
focus
and mode
not in (
"design",
"research",
"create",
"evolve",
"study",
"frontend-design",
"frontend-design-discover",
)
and not design_existing
and not just_plan
):
print(
f"Error: --focus (targeted mode) only works in improve, research, create, evolve, study, frontend-design, "
f"Error: --focus (targeted mode) only works in design, research, create, evolve, study, frontend-design, "
f"frontend-design-discover, or design (with --just-plan) mode, "
f"got '{mode}'. The project must already be built before targeting specific items.",
file=sys.stderr,
Expand Down Expand Up @@ -602,7 +642,7 @@ def _execute_ceo(
elif mode == "design":
ceo_mode = "design"
elif interactive:
ceo_mode = "build"
ceo_mode = "design"
else:
ceo_mode = mode

Expand Down Expand Up @@ -746,12 +786,18 @@ def _execute_ceo(

if engine == "tool":
base_prompt = resolve_prompt(
"ceo", wt_path, use_profile=use_profile, workflow_mode=None,
"ceo",
wt_path,
use_profile=use_profile,
workflow_mode=None,
)
prompt = base_prompt + _tool_exec_protocol(wt_path)
else:
prompt = resolve_prompt(
"ceo", wt_path, use_profile=use_profile, workflow_mode=ceo_mode,
"ceo",
wt_path,
use_profile=use_profile,
workflow_mode=ceo_mode,
)
runner = get_runner(runner_name)
extras: dict[str, object] = {}
Expand All @@ -776,6 +822,7 @@ def _execute_ceo(
if engine == "tool":
try:
from factory.workflow.tool import tool_finalize

finalize_result = tool_finalize(wt_path)
log.info("tool_exec.finalized", result=finalize_result)
except Exception:
Expand Down Expand Up @@ -847,13 +894,18 @@ def _run_headless(
executor = WorkflowExecutor(wf, wt_path, agent_pool=DEFAULT_AGENT_POOL)
try:
exec_result = asyncio.run(executor.execute())
print(json.dumps({
"workflow": ceo_mode,
"engine": "deterministic",
"success": exec_result.success,
"nodes_executed": exec_result.nodes_executed,
"duration_ms": round(exec_result.duration_ms, 1),
}, indent=2))
print(
json.dumps(
{
"workflow": ceo_mode,
"engine": "deterministic",
"success": exec_result.success,
"nodes_executed": exec_result.nodes_executed,
"duration_ms": round(exec_result.duration_ms, 1),
},
indent=2,
)
)
code = 0 if exec_result.success else 1
if code != 0:
return code
Expand All @@ -863,7 +915,7 @@ def _run_headless(
min_growth=min_growth,
max_new=max_new,
branch=branch,
already_improved=mode in ("improve", "meta") or discover_only,
already_improved=mode in ("design", "meta") or discover_only,
model=model,
no_github=no_github,
use_profile=use_profile,
Expand Down Expand Up @@ -916,7 +968,7 @@ def _run_headless(
min_growth=min_growth,
max_new=max_new,
branch=branch,
already_improved=mode in ("improve", "meta") or discover_only,
already_improved=mode in ("design", "meta") or discover_only,
model=model,
no_github=no_github,
use_profile=use_profile,
Expand All @@ -929,6 +981,7 @@ def _run_headless(
if engine == "tool":
try:
from factory.workflow.tool import tool_finalize

tool_finalize(wt_path)
except Exception:
pass
Expand Down
Loading
Loading