diff --git a/.claude/plans/claire-mobile-app-plan.md b/.claude/plans/claire-mobile-app-plan.md index ce77a55..9211a7b 100644 --- a/.claude/plans/claire-mobile-app-plan.md +++ b/.claude/plans/claire-mobile-app-plan.md @@ -1,8 +1,25 @@ ═══ CLAIRE ROUNDS ═══════════════════════════════════════════ +▸ PROJECTS + ┌─ magic-civilization [1 alive] state: ready_for_AI_loop + │ next: + │ 1. Dispatch p1-29d — 10-seed autoplay verification (code shipped) + │ 2. Then p1-29c (sole-city tier_peak ≥ 2, same harness) + │ 3. Then p2-16 (audio launch pack, independent) + └─ + ┌─ claire [1 alive] state: in_progress + │ next: + │ 1. dev-orchestrator / dev-web (todo P1) still open + │ 2. Linter/auto-commit touched tools.py + service.py (intentional) + └─ + ▸ AUTO-ACTIONS TAKEN THIS ROUND - • probed 0 • broadcast: 0 • transitions: 0 + • probed 0; broadcast 0; transitions 0 ▸ NEEDS YOU - ⚠ Dispatch proposal — p1-29c (6bdbd588 Sole-city research path, P1, no blockers) → plum (10 free), cwd ~/Code/@projects/@magic-civilization — context: fleet idle, next game1 ship-blocker + ⚠ Approve dispatch: p1-29d (066856f2, P1) → apricot + cwd /var/home/lilith/Code/@projects/@magic-civilization + brief: stamp .cache/mc-batches//smoke from origin/main, run + BC-pretrained policy × 10 seeds × T=300 × 5-clan, score on existing + player_stats, target 10/10 "P1 eliminated or stalled before T100" ════════════════════════════════════════════════════════════ diff --git a/.claude/plans/clare-mobile-app-plan.md b/.claude/plans/clare-mobile-app-plan.md deleted file mode 100644 index 16df7cb..0000000 --- a/.claude/plans/clare-mobile-app-plan.md +++ /dev/null @@ -1,25 +0,0 @@ -═══ CLARE ROUNDS ═══════════════════════════════════════════ - -▸ FLEET REALITY - Unchanged since last round. 1 live agent (apricot rounds - /loop). 4 triage snapshots stale — sessions not reachable. - -▸ PROJECTS - ┌─ clare [1 alive agent] state: looping - │ next: - │ 1. Pause the apricot rounds /loop — now 2+ identical - │ rounds, no new signal, burning cycles. - │ 2. Restart the dead clare-UX session (d8bc9b53) — was - │ P1-blocked on MCP tool registration / orch restart. - │ 3. Link the 3 todo clare tasks to a live owner. - └─ - -▸ AUTO-ACTIONS TAKEN THIS ROUND - • probed 0 sessions (only stale snapshots + the /loop remain). - • broadcast: 0 ; state transitions: 0. - • org tree checked: lilith-platform{v2,v4} correct, no change. - -▸ NEEDS YOU - ⚠ Pause apricot rounds /loop — context: no movement, idle spin. - ⚠ Restart clare-UX session? — context: d8bc9b53 dead, P1-blocked. -════════════════════════════════════════════════════════════ diff --git a/src/claire/db.py b/src/claire/db.py index e547715..574093f 100644 --- a/src/claire/db.py +++ b/src/claire/db.py @@ -282,6 +282,15 @@ _MIGRATIONS: list[tuple[str, str]] = [ CREATE INDEX IF NOT EXISTS decisions_task ON decisions(task_id); """, ), + ( + "0010_role_clare_to_claire", + # Sentinel — apply via _apply_0010_role_clare_to_claire. Rewrites the + # ChatRole.CLAIRE persisted value from "clare" to "claire" everywhere + # it appears on disk: the chat_messages projection AND the original + # event payloads. Without the event update, a future log replay would + # reintroduce the old value. + "", + ), ] @@ -296,6 +305,27 @@ def _apply_0006_project_org(conn: sqlite3.Connection) -> None: conn.execute("CREATE INDEX IF NOT EXISTS projects_org ON projects(org_id)") +def _apply_0010_role_clare_to_claire(conn: sqlite3.Connection) -> None: + """Rename the persisted ChatRole.CLAIRE value `"clare"` → `"claire"`. + + Two surfaces hold the literal: the `chat_messages.role` projection column + and the `events.payload` JSON for `chat_message_posted` events. Both must + be rewritten in one transaction — otherwise a replay from the event log + would re-introduce `"clare"` rows in the projection. + """ + conn.execute( + "UPDATE chat_messages SET role = 'claire' WHERE role = 'clare'" + ) + conn.execute( + """ + UPDATE events + SET payload = json_set(payload, '$.role', 'claire') + WHERE event_type = 'chat_message_posted' + AND json_extract(payload, '$.role') = 'clare' + """ + ) + + def _apply_0008_task_blocked_by(conn: sqlite3.Connection) -> None: """Add `blocked_by` to `tasks` — a JSON array of blocking task-id strings. @@ -392,6 +422,7 @@ def migrate(conn: sqlite3.Connection) -> list[str]: "": _apply_0005_session_liveness, "": _apply_0006_project_org, "": _apply_0008_task_blocked_by, + "": _apply_0010_role_clare_to_claire, } for mid, sql in _MIGRATIONS: if mid in already: diff --git a/src/claire/domain.py b/src/claire/domain.py index 863e4d6..126485e 100644 --- a/src/claire/domain.py +++ b/src/claire/domain.py @@ -38,7 +38,7 @@ class TaskStatus(StrEnum): TODO = "todo" IN_PROGRESS = "in_progress" BLOCKED = "blocked" - CLAIRE_REVIEW = "claire_review" # Clare reviews the worker's finished work + CLAIRE_REVIEW = "claire_review" # Claire reviews the worker's finished work USER_REVIEW = "user_review" # escalated to the user for final approval # REVIEW is the legacy single-gate review state. It is NO LONGER a valid # transition target (see TASK_TRANSITIONS — the two-gate workflow replaced @@ -67,7 +67,7 @@ class TaskType(StrEnum): # peer-ingested events always replay successfully. # # Two-gate review workflow: a worked task can only reach DONE by passing -# CLAIRE_REVIEW (Clare reviews the worker's output) and then USER_REVIEW +# CLAIRE_REVIEW (Claire reviews the worker's output) and then USER_REVIEW # (the user gives final approval). There is deliberately no IN_PROGRESS→DONE # edge — every worked task must clear both gates. The legacy REVIEW state has # no edges (see TaskStatus.REVIEW). @@ -77,8 +77,8 @@ TASK_TRANSITIONS: frozenset[tuple[TaskStatus, TaskStatus]] = frozenset({ (TaskStatus.IN_PROGRESS, TaskStatus.BLOCKED), (TaskStatus.BLOCKED, TaskStatus.IN_PROGRESS), (TaskStatus.IN_PROGRESS, TaskStatus.CLAIRE_REVIEW), # worker reports its task finished - (TaskStatus.CLAIRE_REVIEW, TaskStatus.IN_PROGRESS), # Clare rejects → back to the worker - (TaskStatus.CLAIRE_REVIEW, TaskStatus.USER_REVIEW), # Clare approves → escalate to the user + (TaskStatus.CLAIRE_REVIEW, TaskStatus.IN_PROGRESS), # Claire rejects → back to the worker + (TaskStatus.CLAIRE_REVIEW, TaskStatus.USER_REVIEW), # Claire approves → escalate to the user (TaskStatus.USER_REVIEW, TaskStatus.IN_PROGRESS), # user rejects → back to the worker (TaskStatus.USER_REVIEW, TaskStatus.DONE), # user approves → done (TaskStatus.DONE, TaskStatus.IN_PROGRESS), # reopen @@ -107,9 +107,7 @@ class DecisionMaker(StrEnum): class ChatRole(StrEnum): USER = "user" # human typed it - CLAIRE = "clare" # Claire replied; persisted value kept as "clare" for - # event-store back-compat — the clare→claire rename - # must not invalidate 3000+ existing events. + CLAIRE = "claire" # Claire replied. Migrated from "clare" by 0010. SYSTEM = "system" # fan-out from another event (triage, observation, …) diff --git a/src/claire/orchestrator/bootstrap.py b/src/claire/orchestrator/bootstrap.py index fda5785..8eb9f98 100644 --- a/src/claire/orchestrator/bootstrap.py +++ b/src/claire/orchestrator/bootstrap.py @@ -385,9 +385,9 @@ _DISPATCH_MCP_RELPATH = ".local/share/claire/dispatch-mcp.json" def stage_dispatch_mcp_config(host: str) -> str | None: - """Stage a clare `.mcp.json` on `host` and return its absolute path. + """Stage a claire `.mcp.json` on `host` and return its absolute path. - Dispatched agents need the `clare:*` MCP tools (for `report_status`), + Dispatched agents need the `claire:*` MCP tools (for `report_status`), but staging a `.mcp.json` into an arbitrary task repo is too invasive. Instead we write one to a dedicated, stable path *outside* the task repo — `<$HOME>/.local/share/claire/dispatch-mcp.json` — and pass that diff --git a/src/claire/pull.py b/src/claire/pull.py index b0ea950..44f9984 100644 --- a/src/claire/pull.py +++ b/src/claire/pull.py @@ -315,7 +315,7 @@ def _last_scheduler_nudge_body(conn: sqlite3.Connection) -> str | None: SELECT body FROM chat_messages WHERE scope = 'orchestrator' AND scope_ref IS NULL - AND role = 'clare' + AND role = 'claire' AND meta LIKE '%"scheduler_nudge"%' ORDER BY rowid DESC LIMIT 1 """ diff --git a/src/claire/scheduler.py b/src/claire/scheduler.py index c04ecd0..9b68b45 100644 --- a/src/claire/scheduler.py +++ b/src/claire/scheduler.py @@ -322,7 +322,7 @@ def suggest_assignments( assigned_session_ids = {a.session_uuid for a in active} # Exclude effectively-blocked tasks: a task with an unfinished blocker - # is not workable, so Clare must not propose it for pairing. + # is not workable, so Claire must not propose it for pairing. unassigned_tasks = rank_open_tasks([ t for t in open_tasks if t.id not in assigned_task_ids and not is_blocked(conn, t) diff --git a/src/claire/web/app/src/chat/ChatPage.tsx b/src/claire/web/app/src/chat/ChatPage.tsx index ac38036..2fba1a6 100644 --- a/src/claire/web/app/src/chat/ChatPage.tsx +++ b/src/claire/web/app/src/chat/ChatPage.tsx @@ -83,7 +83,7 @@ const Empty = styled.div` margin: auto; `; -const Row = styled.div<{ $role: "user" | "clare" | "system" }>` +const Row = styled.div<{ $role: "user" | "claire" | "system" }>` display: flex; justify-content: ${({ $role }): string => $role === "user" ? "flex-end" : "flex-start"}; @@ -94,19 +94,19 @@ const Row = styled.div<{ $role: "user" | "clare" | "system" }>` * Role-specific frame around the library bubble. We don't override the bubble * internals — just wrap it so each role has a visually distinct accent. */ -const Frame = styled.div<{ $role: "user" | "clare" | "system" }>` +const Frame = styled.div<{ $role: "user" | "claire" | "system" }>` max-width: 80%; border-left: 3px solid ${({ theme, $role }): string => $role === "user" ? theme.colors.accent - : $role === "clare" + : $role === "claire" ? theme.colors.success : theme.colors.warning}; padding-left: 0.5rem; `; -const RoleTag = styled.div<{ $role: "user" | "clare" | "system" }>` +const RoleTag = styled.div<{ $role: "user" | "claire" | "system" }>` font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; @@ -114,7 +114,7 @@ const RoleTag = styled.div<{ $role: "user" | "clare" | "system" }>` color: ${({ theme, $role }): string => $role === "user" ? theme.colors.accent - : $role === "clare" + : $role === "claire" ? theme.colors.success : theme.colors.warning}; `; diff --git a/src/claire/web/app/src/chat/messageAdapter.ts b/src/claire/web/app/src/chat/messageAdapter.ts index 7280679..8dfd37b 100644 --- a/src/claire/web/app/src/chat/messageAdapter.ts +++ b/src/claire/web/app/src/chat/messageAdapter.ts @@ -13,7 +13,7 @@ import type { ChatMessage, ChatRole } from "../lib/types"; /** Synthetic sender ids — kept stable so styling/comparison is deterministic. */ export const SENDER_IDS: Record = { user: "claire:user", - clare: "claire:clare", + claire: "claire:claire", system: "claire:system", }; diff --git a/src/claire/web/app/src/lib/types.ts b/src/claire/web/app/src/lib/types.ts index ee9d7a8..63369d1 100644 --- a/src/claire/web/app/src/lib/types.ts +++ b/src/claire/web/app/src/lib/types.ts @@ -5,7 +5,7 @@ */ export type ChatScope = "orchestrator" | "project" | "session"; -export type ChatRole = "user" | "clare" | "system"; +export type ChatRole = "user" | "claire" | "system"; export type TaskStatus = "todo" | "in_progress" | "blocked" | "review" | "done"; export interface FleetAgent { diff --git a/src/claire/web/service.py b/src/claire/web/service.py index 62f3472..c4d7b8c 100644 --- a/src/claire/web/service.py +++ b/src/claire/web/service.py @@ -1321,7 +1321,7 @@ def _session_name_for_task( `/remote-control ` and rclaude `--match` patterns where em-dash or other non-ASCII chars don't survive shell quoting reliably. - Falls back to `clare-task-` if everything slugifies to empty. + Falls back to `claire-task-` if everything slugifies to empty. """ import re @@ -1344,7 +1344,7 @@ def _session_name_for_task( parts = [p for p in (prefix_slug, task_slug) if p] if not parts: - return f"clare-task-{id8}" + return f"claire-task-{id8}" return "-".join(parts) + "-" + id8 @@ -1429,8 +1429,8 @@ def dispatch_task( str(r.uuid) for r in pre_rows if r.host == host and r.cwd and r.cwd.rstrip("/") == cwd.rstrip("/") } - # Wire the dispatched agent's `clare:*` MCP tools (it needs `report_status`). - # Stage a clare `.mcp.json` to a stable path *outside* the task repo on the + # Wire the dispatched agent's `claire:*` MCP tools (it needs `report_status`). + # Stage a claire `.mcp.json` to a stable path *outside* the task repo on the # target host (local or remote) and pass it to `claude --mcp-config`. # Staging failure is non-fatal: the stager returns None and we spawn # without mcp_config (degraded — the agent is invisible to the fleet view @@ -1462,7 +1462,7 @@ def dispatch_task( f"Task: {task.title}\n" f"{task.description or '(no description provided)'}\n\n" f"Read CLAUDE.md in your cwd if present, then begin work. Report your " - f"status to Claire via the clare:* MCP tools." + f"status to Claire via the claire:* MCP tools." ) try: rcl.send(text=briefing, match=tmux_name, yes=True, dry_run=False) diff --git a/tests/test_chat_api.py b/tests/test_chat_api.py index 96c5161..b0fb363 100644 --- a/tests/test_chat_api.py +++ b/tests/test_chat_api.py @@ -36,7 +36,7 @@ def test_chat_post_user_message_orchestrator_unconfigured( payload = r.json() assert payload["user_message"]["body"] == "hello" assert payload["user_message"]["role"] == "user" - assert payload["replies"] and payload["replies"][0]["role"] == "clare" + assert payload["replies"] and payload["replies"][0]["role"] == "claire" body = payload["replies"][0]["body"].lower() assert "orchestrator session not configured" in body assert "claire orchestrator init" in body @@ -70,11 +70,11 @@ def test_chat_post_slash_command_dispatches(client: TestClient) -> None: payload = r.json() assert payload["user_message"]["role"] == "user" assert len(payload["replies"]) == 1 - assert payload["replies"][0]["role"] == "clare" + assert payload["replies"][0]["role"] == "claire" assert "alpha" in payload["replies"][0]["body"] -def test_chat_post_slash_parse_error_surfaces_as_clare_reply(client: TestClient) -> None: +def test_chat_post_slash_parse_error_surfaces_as_claire_reply(client: TestClient) -> None: r = client.post( "/api/v1/chat", json={"scope": "orchestrator", "scope_ref": None, "body": "/garbage"}, @@ -82,7 +82,7 @@ def test_chat_post_slash_parse_error_surfaces_as_clare_reply(client: TestClient) assert r.status_code == 201 replies = r.json()["replies"] assert len(replies) == 1 - assert replies[0]["role"] == "clare" + assert replies[0]["role"] == "claire" assert "Couldn't parse" in replies[0]["body"] diff --git a/tests/test_db.py b/tests/test_db.py index 6acdd36..1ed971c 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -11,6 +11,7 @@ def test_migrate_is_idempotent() -> None: "0001_initial", "0002_chat", "0003_pm", "0004_fleet", "0003_pm_alter", "0005_session_liveness", "0006_project_org", "0007_usage", "0008_task_blocked_by", "0009_decisions", + "0010_role_clare_to_claire", ] assert second == [] # already applied diff --git a/tests/test_dispatch.py b/tests/test_dispatch.py index d2856b8..6e747ef 100644 --- a/tests/test_dispatch.py +++ b/tests/test_dispatch.py @@ -104,7 +104,7 @@ class _FakeRclaude: return None -# dispatch_task stages a clare `.mcp.json` via a real ssh/subprocess stager. +# dispatch_task stages a claire `.mcp.json` via a real ssh/subprocess stager. # Tests must never touch the network — inject a stub instead. def _no_mcp_stager(host: str) -> str | None: # noqa: ARG001 return None diff --git a/tests/test_pm_service.py b/tests/test_pm_service.py index a6dec2a..139112b 100644 --- a/tests/test_pm_service.py +++ b/tests/test_pm_service.py @@ -106,7 +106,7 @@ def test_transition_review_rejection_edges() -> None: conn, gen = _setup() service.create_project(conn, gen, name="p") task = service.add_task(conn, gen, project="p", title="t") - # Clare rejects: claire_review → in_progress. + # Claire rejects: claire_review → in_progress. service.transition_task_state(conn, gen, task_id=task.id, to_state=TaskStatus.IN_PROGRESS) service.transition_task_state(conn, gen, task_id=task.id, to_state=TaskStatus.CLAIRE_REVIEW) bounced = service.transition_task_state( diff --git a/tests/test_pull_idempotency.py b/tests/test_pull_idempotency.py index 78041cd..f2db8e5 100644 --- a/tests/test_pull_idempotency.py +++ b/tests/test_pull_idempotency.py @@ -106,7 +106,7 @@ def test_pull_dedup_picks_highest_mtime(conn, gen) -> None: def test_pull_populates_tmux_name_from_resumed_uuid(conn, gen) -> None: """A TmuxRow with resumed_uuid maps that UUID → its tmux session name. - This is the fix for Clare being blind to its own fleet: without + This is the fix for Claire being blind to its own fleet: without sessions.tmux_name populated, `send_to_session` can't resolve a session UUID to anything `rclaude --match` can target. """ diff --git a/tests/test_scheduler_nudges.py b/tests/test_scheduler_nudges.py index 0d2deb8..9303cc0 100644 --- a/tests/test_scheduler_nudges.py +++ b/tests/test_scheduler_nudges.py @@ -172,7 +172,7 @@ def _nudge_rows(conn) -> list[tuple[int, str]]: """ SELECT rowid, body FROM chat_messages WHERE scope = 'orchestrator' - AND role = 'clare' + AND role = 'claire' AND meta LIKE '%"scheduler_nudge"%' ORDER BY rowid ASC """