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
This commit is contained in:
committed by
YeonGyu-Kim
parent
0743855b40
commit
a5489718f9
138
src/features/builtin-commands/commands.test.ts
Normal file
138
src/features/builtin-commands/commands.test.ts
Normal file
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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<BuiltinCommandName, Omit<CommandDefinition, "name">> = {
|
||||
"init-deep": {
|
||||
@@ -77,6 +78,22 @@ $ARGUMENTS
|
||||
${STOP_CONTINUATION_TEMPLATE}
|
||||
</command-instruction>`,
|
||||
},
|
||||
handoff: {
|
||||
description: "(builtin) Create a detailed context summary for continuing work in a new session",
|
||||
template: `<command-instruction>
|
||||
${HANDOFF_TEMPLATE}
|
||||
</command-instruction>
|
||||
|
||||
<session-context>
|
||||
Session ID: $SESSION_ID
|
||||
Timestamp: $TIMESTAMP
|
||||
</session-context>
|
||||
|
||||
<user-request>
|
||||
$ARGUMENTS
|
||||
</user-request>`,
|
||||
argumentHint: "[goal]",
|
||||
},
|
||||
}
|
||||
|
||||
export function loadBuiltinCommands(
|
||||
|
||||
177
src/features/builtin-commands/templates/handoff.ts
Normal file
177
src/features/builtin-commands/templates/handoff.ts
Normal file
@@ -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.
|
||||
`
|
||||
@@ -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[]
|
||||
|
||||
Reference in New Issue
Block a user