refactor(domain): rename ChatRole persisted value clare → claire (migration 0010)
ChatRole.CLAIRE now persists as "claire" everywhere. Migration 0010 rewrites both the chat_messages.role column and the chat_message_posted event payloads in one transaction so a future replay reconstructs the same projection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f1ff4c8b72
commit
53921b4e94
17 changed files with 80 additions and 58 deletions
|
|
@ -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/<ts>/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"
|
||||
════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
════════════════════════════════════════════════════════════
|
||||
|
|
@ -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.
|
||||
"<sentinel:apply_0010_role_clare_to_claire>",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -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]:
|
|||
"<sentinel:apply_0005_session_liveness>": _apply_0005_session_liveness,
|
||||
"<sentinel:apply_0006_project_org>": _apply_0006_project_org,
|
||||
"<sentinel:apply_0008_task_blocked_by>": _apply_0008_task_blocked_by,
|
||||
"<sentinel:apply_0010_role_clare_to_claire>": _apply_0010_role_clare_to_claire,
|
||||
}
|
||||
for mid, sql in _MIGRATIONS:
|
||||
if mid in already:
|
||||
|
|
|
|||
|
|
@ -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, …)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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<ChatRole, string> = {
|
||||
user: "claire:user",
|
||||
clare: "claire:clare",
|
||||
claire: "claire:claire",
|
||||
system: "claire:system",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -1321,7 +1321,7 @@ def _session_name_for_task(
|
|||
`/remote-control <name>` and rclaude `--match` patterns where em-dash
|
||||
or other non-ASCII chars don't survive shell quoting reliably.
|
||||
|
||||
Falls back to `clare-task-<id8>` if everything slugifies to empty.
|
||||
Falls back to `claire-task-<id8>` 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)
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue