From a5489718f9659b447f3724dd4f494c830970efbe Mon Sep 17 00:00:00 2001 From: justsisyphus Date: Tue, 27 Jan 2026 17:40:41 +0900 Subject: [PATCH] feat(commands): add /handoff builtin command with programmatic context synthesis Port handoff concept from ampcode as a builtin command that extracts detailed context summary from current session for seamless continuation in a new session. Enhanced with programmatic context gathering: - Add HANDOFF_TEMPLATE with phased extraction (gather programmatic context via session_read/todoread/git, extract context, format, instruct) - Gather concrete data: session history, todo state, git diff/status - Include compaction-style sections: USER REQUESTS (AS-IS) verbatim, EXPLICIT CONSTRAINTS verbatim, plus all original handoff sections - Register handoff in BuiltinCommandName type and command definitions - Include session context variables (SESSION_ID, TIMESTAMP, ARGUMENTS) - Add 14 tests covering registration, template content, programmatic gathering, compaction-style sections, and emoji-free constraint --- .../builtin-commands/commands.test.ts | 138 ++++++++++++++ src/features/builtin-commands/commands.ts | 17 ++ .../builtin-commands/templates/handoff.ts | 177 ++++++++++++++++++ src/features/builtin-commands/types.ts | 2 +- 4 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 src/features/builtin-commands/commands.test.ts create mode 100644 src/features/builtin-commands/templates/handoff.ts diff --git a/src/features/builtin-commands/commands.test.ts b/src/features/builtin-commands/commands.test.ts new file mode 100644 index 000000000..c6927bc70 --- /dev/null +++ b/src/features/builtin-commands/commands.test.ts @@ -0,0 +1,138 @@ +import { describe, test, expect } from "bun:test" +import { loadBuiltinCommands } from "./commands" +import { HANDOFF_TEMPLATE } from "./templates/handoff" +import type { BuiltinCommandName } from "./types" + +describe("loadBuiltinCommands", () => { + test("should include handoff command in loaded commands", () => { + //#given + const disabledCommands: BuiltinCommandName[] = [] + + //#when + const commands = loadBuiltinCommands(disabledCommands) + + //#then + expect(commands.handoff).toBeDefined() + expect(commands.handoff.name).toBe("handoff") + }) + + test("should exclude handoff when disabled", () => { + //#given + const disabledCommands: BuiltinCommandName[] = ["handoff"] + + //#when + const commands = loadBuiltinCommands(disabledCommands) + + //#then + expect(commands.handoff).toBeUndefined() + }) + + test("should include handoff template content in command template", () => { + //#given - no disabled commands + + //#when + const commands = loadBuiltinCommands() + + //#then + expect(commands.handoff.template).toContain(HANDOFF_TEMPLATE) + }) + + test("should include session context variables in handoff template", () => { + //#given - no disabled commands + + //#when + const commands = loadBuiltinCommands() + + //#then + expect(commands.handoff.template).toContain("$SESSION_ID") + expect(commands.handoff.template).toContain("$TIMESTAMP") + expect(commands.handoff.template).toContain("$ARGUMENTS") + }) + + test("should have correct description for handoff", () => { + //#given - no disabled commands + + //#when + const commands = loadBuiltinCommands() + + //#then + expect(commands.handoff.description).toContain("context summary") + }) +}) + +describe("HANDOFF_TEMPLATE", () => { + test("should include session reading instruction", () => { + //#given - the template string + + //#when / #then + expect(HANDOFF_TEMPLATE).toContain("session_read") + }) + + test("should include compaction-style sections in output format", () => { + //#given - the template string + + //#when / #then + expect(HANDOFF_TEMPLATE).toContain("USER REQUESTS (AS-IS)") + expect(HANDOFF_TEMPLATE).toContain("EXPLICIT CONSTRAINTS") + }) + + test("should include programmatic context gathering instructions", () => { + //#given - the template string + + //#when / #then + expect(HANDOFF_TEMPLATE).toContain("todoread") + expect(HANDOFF_TEMPLATE).toContain("git diff") + expect(HANDOFF_TEMPLATE).toContain("git status") + }) + + test("should include context extraction format", () => { + //#given - the template string + + //#when / #then + expect(HANDOFF_TEMPLATE).toContain("WORK COMPLETED") + expect(HANDOFF_TEMPLATE).toContain("CURRENT STATE") + expect(HANDOFF_TEMPLATE).toContain("PENDING TASKS") + expect(HANDOFF_TEMPLATE).toContain("KEY FILES") + expect(HANDOFF_TEMPLATE).toContain("IMPORTANT DECISIONS") + expect(HANDOFF_TEMPLATE).toContain("CONTEXT FOR CONTINUATION") + expect(HANDOFF_TEMPLATE).toContain("GOAL") + }) + + test("should enforce first person perspective", () => { + //#given - the template string + + //#when / #then + expect(HANDOFF_TEMPLATE).toContain("first person perspective") + }) + + test("should limit key files to 10", () => { + //#given - the template string + + //#when / #then + expect(HANDOFF_TEMPLATE).toContain("Maximum 10 files") + }) + + test("should instruct plain text format without markdown", () => { + //#given - the template string + + //#when / #then + expect(HANDOFF_TEMPLATE).toContain("Plain text with bullets") + expect(HANDOFF_TEMPLATE).toContain("No markdown headers") + }) + + test("should include user instructions for new session", () => { + //#given - the template string + + //#when / #then + expect(HANDOFF_TEMPLATE).toContain("new session") + expect(HANDOFF_TEMPLATE).toContain("opencode") + }) + + test("should not contain emojis", () => { + //#given - the template string + + //#when / #then + const emojiRegex = /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2702}-\u{27B0}\u{24C2}-\u{1F251}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u + expect(emojiRegex.test(HANDOFF_TEMPLATE)).toBe(false) + }) +}) diff --git a/src/features/builtin-commands/commands.ts b/src/features/builtin-commands/commands.ts index 998ce2531..aee5dc28a 100644 --- a/src/features/builtin-commands/commands.ts +++ b/src/features/builtin-commands/commands.ts @@ -5,6 +5,7 @@ import { RALPH_LOOP_TEMPLATE, CANCEL_RALPH_TEMPLATE } from "./templates/ralph-lo import { STOP_CONTINUATION_TEMPLATE } from "./templates/stop-continuation" import { REFACTOR_TEMPLATE } from "./templates/refactor" import { START_WORK_TEMPLATE } from "./templates/start-work" +import { HANDOFF_TEMPLATE } from "./templates/handoff" const BUILTIN_COMMAND_DEFINITIONS: Record> = { "init-deep": { @@ -77,6 +78,22 @@ $ARGUMENTS ${STOP_CONTINUATION_TEMPLATE} `, }, + handoff: { + description: "(builtin) Create a detailed context summary for continuing work in a new session", + template: ` +${HANDOFF_TEMPLATE} + + + +Session ID: $SESSION_ID +Timestamp: $TIMESTAMP + + + +$ARGUMENTS +`, + argumentHint: "[goal]", + }, } export function loadBuiltinCommands( diff --git a/src/features/builtin-commands/templates/handoff.ts b/src/features/builtin-commands/templates/handoff.ts new file mode 100644 index 000000000..d8010994d --- /dev/null +++ b/src/features/builtin-commands/templates/handoff.ts @@ -0,0 +1,177 @@ +export const HANDOFF_TEMPLATE = `# Handoff Command + +## Purpose + +Use /handoff when: +- The current session context is getting too long and quality is degrading +- You want to start fresh while preserving essential context from this session +- The context window is approaching capacity + +This creates a detailed context summary that can be used to continue work in a new session. + +--- + +# PHASE 0: VALIDATE REQUEST + +Before proceeding, confirm: +- [ ] There is meaningful work or context in this session to preserve +- [ ] The user wants to create a handoff summary (not just asking about it) + +If the session is nearly empty or has no meaningful context, inform the user there is nothing substantial to hand off. + +--- + +# PHASE 1: GATHER PROGRAMMATIC CONTEXT + +Execute these tools to gather concrete data: + +1. session_read({ session_id: "$SESSION_ID" }) — full session history +2. todoread() — current task progress +3. Bash({ command: "git diff --stat HEAD~10..HEAD" }) — recent file changes +4. Bash({ command: "git status --porcelain" }) — uncommitted changes + +Suggested execution order: + +\`\`\` +session_read({ session_id: "$SESSION_ID" }) +todoread() +Bash({ command: "git diff --stat HEAD~10..HEAD" }) +Bash({ command: "git status --porcelain" }) +\`\`\` + +Analyze the gathered outputs to understand: +- What the user asked for (exact wording) +- What work was completed +- What tasks remain incomplete (include todo state) +- What decisions were made +- What files were modified or discussed (include git diff/stat + status) +- What patterns, constraints, or preferences were established + +--- + +# PHASE 2: EXTRACT CONTEXT + +Write the context summary from first person perspective ("I did...", "I told you..."). + +Focus on: +- Capabilities and behavior, not file-by-file implementation details +- What matters for continuing the work +- Avoiding excessive implementation details (variable names, storage keys, constants) unless critical +- USER REQUESTS (AS-IS) must be verbatim (do not paraphrase) +- EXPLICIT CONSTRAINTS must be verbatim only (do not invent) + +Questions to consider when extracting: +- What did I just do or implement? +- What instructions did I already give which are still relevant (e.g. follow patterns in the codebase)? +- What files did I tell you are important or that I am working on? +- Did I provide a plan or spec that should be included? +- What did I already tell you that is important (libraries, patterns, constraints, preferences)? +- What important technical details did I discover (APIs, methods, patterns)? +- What caveats, limitations, or open questions did I find? + +--- + +# PHASE 3: FORMAT OUTPUT + +Generate a handoff summary using this exact format: + +\`\`\` +HANDOFF CONTEXT +=============== + +USER REQUESTS (AS-IS) +--------------------- +- [Exact verbatim user requests - NOT paraphrased] + +GOAL +---- +[One sentence describing what should be done next] + +WORK COMPLETED +-------------- +- [First person bullet points of what was done] +- [Include specific file paths when relevant] +- [Note key implementation decisions] + +CURRENT STATE +------------- +- [Current state of the codebase or task] +- [Build/test status if applicable] +- [Any environment or configuration state] + +PENDING TASKS +------------- +- [Tasks that were planned but not completed] +- [Next logical steps to take] +- [Any blockers or issues encountered] +- [Include current todo state from todoread()] + +KEY FILES +--------- +- [path/to/file1] - [brief role description] +- [path/to/file2] - [brief role description] +(Maximum 10 files, prioritized by importance) +- (Include files from git diff/stat and git status) + +IMPORTANT DECISIONS +------------------- +- [Technical decisions that were made and why] +- [Trade-offs that were considered] +- [Patterns or conventions established] + +EXPLICIT CONSTRAINTS +-------------------- +- [Verbatim constraints only - from user or existing AGENTS.md] +- If none, write: None + +CONTEXT FOR CONTINUATION +------------------------ +- [What the next session needs to know to continue] +- [Warnings or gotchas to be aware of] +- [References to documentation if relevant] +\`\`\` + +Rules for the summary: +- Plain text with bullets +- No markdown headers with # (use the format above with dashes) +- No bold, italic, or code fences within content +- Use workspace-relative paths for files +- Keep it focused - only include what matters for continuation +- Pick an appropriate length based on complexity +- USER REQUESTS (AS-IS) and EXPLICIT CONSTRAINTS must be verbatim only + +--- + +# PHASE 4: PROVIDE INSTRUCTIONS + +After generating the summary, instruct the user: + +\`\`\` +--- + +TO CONTINUE IN A NEW SESSION: + +1. Press 'n' in OpenCode TUI to open a new session, or run 'opencode' in a new terminal +2. Paste the HANDOFF CONTEXT above as your first message +3. Add your request: "Continue from the handoff context above. [Your next task]" + +The new session will have all context needed to continue seamlessly. +\`\`\` + +--- + +# IMPORTANT CONSTRAINTS + +- DO NOT attempt to programmatically create new sessions (no API available to agents) +- DO provide a self-contained summary that works without access to this session +- DO include workspace-relative file paths +- DO NOT include sensitive information (API keys, credentials, secrets) +- DO NOT exceed 10 files in the KEY FILES section +- DO keep the GOAL section to a single sentence or short paragraph + +--- + +# EXECUTE NOW + +Begin by gathering programmatic context, then synthesize the handoff summary. +` diff --git a/src/features/builtin-commands/types.ts b/src/features/builtin-commands/types.ts index 1b1487744..0c2624f12 100644 --- a/src/features/builtin-commands/types.ts +++ b/src/features/builtin-commands/types.ts @@ -1,6 +1,6 @@ import type { CommandDefinition } from "../claude-code-command-loader" -export type BuiltinCommandName = "init-deep" | "ralph-loop" | "cancel-ralph" | "ulw-loop" | "refactor" | "start-work" | "stop-continuation" +export type BuiltinCommandName = "init-deep" | "ralph-loop" | "cancel-ralph" | "ulw-loop" | "refactor" | "start-work" | "stop-continuation" | "handoff" export interface BuiltinCommandConfig { disabled_commands?: BuiltinCommandName[]