0055 PR 1 — sticky ticket mode with phases #137
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "glen/0055-chat-modes"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements PR 1 of docs/0055-chat-modes.md (approved plan v5). PR 2 (mode chip, usage control in the composer bar, header cleanup) follows separately.
What changes
conversations.skill = "ticket"is the mode (write-once → sticky); newconversations.mode_phase(migration 0058) holdsplan → implement → review → fixup → ship.lib/modes.ts(pure core inlib/agent/mode-ids.ts, db-free for workflow files): transitions — forward one, review→ship on a clean review, fixup⇄review loop, backward any step with a note. One gate:implementneeds the latest plan approved (planState, durable sqlite fact).enter_mode(approval-gated via the existing card; project chats only; refuses when already moded) writes skill + phase and returns the intro +plansection.advance_phase(no approval) validates and returns the next phase's##section — just-in-time runbook.use_skillis list-only: the agent can no longer silently bind.mode: "ticket"on a send /?mode=ticket(?skill=ticketaliases it) appends arunEvent: "mode"row carrying the runbook — never the system prompt.[thread state] mode: ticket · phase: implement (2/5) · next: reviewnote is stored as user-row metadata and rebuilt in place bylistTurnMessages(like[viewing]). Test proves turn N's model input is an exact prefix of turn N+1's history. Also fixes a pre-existing metadata clobber (runEvent overwrote viewing).ticketrewritten orchestrator-shaped (intro + one section per phase, verification in implement);shipretired (seededdisabledversion, restorable). Unedited git rows ofticketand the base prompt are upgraded; owner/agent-edited rows untouched (frozen exact-match constants, verified against main).advance_phasewith a note adopts them into a phase (plan gate still applies).Review
Fresh-eyes review (thread c_tmvhls7ro103cf68f38c3) verdict was "fix first"; all fixed:
whenwas earlier than 0057's, so drizzle skipped it on existing DBs → fixed; verified by migrating a main-schema db then this branch (column present).advance_phasein the runbook against the rule.5–7. Nits fixed: migration newline,
[object Object]on answered gated cards, orphan conversation on a refused fresh mode send.Checks: typecheck, lint (2 pre-existing warnings), 1016 tests pass.
The chat send route accepts mode: "ticket" on a send (fresh or an existing chat, project-scoped — unlike `skill`, not creation-only; the legacy `?skill=ticket` means the same thing now that ticket is a mode). lib/chat/mode-entry.ts writes skill + phase directly with no self-approval (this is the owner's own action, not the agent's) and appends a runEvent "mode" row carrying the rendered plan-phase runbook BEFORE the owner's own send — a transcript chip ("Entered ticket mode"), never the system prompt. `?mode=ticket` on the fresh composer URL reuses the existing skill-id plumbing (the route already treats skill: "ticket" as mode entry), so no new client wiring was needed beyond page.tsx reading the query param.The per-turn [thread state] note was injected fresh every turn from live state but never stored, unlike the [viewing] note — so it vanished from the rebuilt history on the next turn, shifting every earlier turn's messages and invalidating the prompt cache for the whole prefix each time. Same fix as viewing: the turn's phase now rides as metadata.threadState on its user row (recordUserMessage, loaded via loadSkillStep BEFORE the row is written so it has the phase to stamp), and listTurnMessages reinserts threadStateNote(phase) before that row on every later turn — byte-identical to what the turn actually sent the model, so history never shifts underneath an earlier turn. Also fixes a real data-loss bug in the same function: viewing and runEvent were written as two separate `{ metadata: {...} }` spreads, so the second silently clobbered the first whenever a send carried both. All three (viewing, runEvent, threadState) now merge into one metadata object. Added a test proving turn N's model input is a byte-identical prefix of turn N+1's rebuilt history, and that all three metadata fields survive together on one row. Updated turn.ts's comments and the AGENTS.md ticket-mode bullet to describe the note as rebuilt from row metadata (like viewing), not "never persisted".review→ship was wrongly refused as a "skip" — a clean review should be able to go straight to ship without a pointless detour through fixup. validateTransition now allows it explicitly, alongside the existing fixup⇄review loop and forward-one-step rule. Also fixes a dead end for legacy ticket-skill threads (skill="ticket", mode_phase null, bound before this column existed): advance_phase used to refuse them "not-moded" while the upgraded runbook tells them to call it, and enter_mode correctly refuses re-entering a skill that's already bound, leaving no way in. checkLegacyAdopt (lib/modes.ts) lets advance_phase adopt such a thread directly: a note is required (there's no prior phase to validate a transition from), and anything past "plan" still needs the conversation's latest plan approved — the same gate that protects a fresh ticket's implement phase. Added: review→ship allowed (still refusing other same-distance skips like plan→review, implement→ship); a test that walks every advance_phase({to}) call inside skills/ticket/SKILL.md's phase sections and checks validateTransition agrees, so the runbook and the rule can never silently disagree again; checkLegacyAdopt coverage (adopt with a note, refuse without one, refuse implement-or-later without an approved plan, accept it once approved).GatedApprovalCard's answered state called String(part.output) directly, which stringifies a plain object to "[object Object]" — enter_mode's denial ({ ok: false, message }) and similar object-shaped results all rendered that way. Prefer output.message when present, else a clipped JSON dump, same 300-char clip as before.`mode: true` in SKILL.md front matter (implies thread) flows through parseFrontMatter, lib/skills.ts's Skill type, every db/prompt-settings.ts write/version/restore/reset/repair path, and propose_prompt_change's patch. db/chat.ts stores the per-turn [thread state] note as the already-rendered string (not {mode, phase}) so listTurnMessages stays db-free and byte-stable on replay. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>A `## Phase: <Name>` whose name slugs to "" (punctuation only, e.g. "!@#") collided with the phaseless-mode sentinel ("" meaning "entered, no phases" on mode_phase); parseModeSkill now treats it as ordinary brief content instead of a declared phase, matching any other non-phase `##` heading. threadStateNote no longer claims a bogus "(0/n) · next: done" when the stored phase isn't declared by the CURRENT body (a heading renamed/removed mid-run) — it says so plainly and lists the current phases, so the agent knows to advance_phase rather than reading it as finished. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>