diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 5403ee68..b890925e 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -335,6 +335,12 @@ {"id":"responsible-vibe-31.4.1","title":"Code cleanup: check for debug/temp artifacts","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-03T09:54:03.20756+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-03T09:54:12.432805+02:00","closed_at":"2026-04-03T09:54:12.432805+02:00","close_reason":"Closed","dependencies":[{"issue_id":"responsible-vibe-31.4.1","depends_on_id":"responsible-vibe-31.4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} {"id":"responsible-vibe-31.4.2","title":"Final test run","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-03T09:54:03.404412+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-03T10:00:00.880469+02:00","closed_at":"2026-04-03T10:00:00.880469+02:00","close_reason":"Closed","dependencies":[{"issue_id":"responsible-vibe-31.4.2","depends_on_id":"responsible-vibe-31.4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} {"id":"responsible-vibe-31.4.3","title":"Commit changes","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-03T09:54:03.597606+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-03T10:00:00.995887+02:00","closed_at":"2026-04-03T10:00:00.995887+02:00","close_reason":"Closed","dependencies":[{"issue_id":"responsible-vibe-31.4.3","depends_on_id":"responsible-vibe-31.4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-32","title":"responsible-vibe: minor (development-plan-continue-after-compact-2.md)","description":"Responsible vibe engineering session using minor workflow for responsible-vibe","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-21T11:12:19.686403+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-21T11:12:19.686403+02:00"} +{"id":"responsible-vibe-32.1","title":"Explore","description":"Understand the problem, analyze existing patterns, and design your approach. Consider the scope and impact of the change. **STEP 1: Analyze Requirements** - If exists: Use it to understand the required changes - Otherwise: Document requirements in your task management system **STEP 2: Review Design Approach** - If exists: Respect the design approach documented in - Otherwise: Design your approach based on the problem analysis **STEP 3: Document Decisions** - Document your analysis and design decisions - Create tasks to guide implementation - Focus on analysis and design only - do not write any code yet","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-21T11:12:19.962361+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-21T11:12:19.962361+02:00","dependencies":[{"issue_id":"responsible-vibe-32.1","depends_on_id":"responsible-vibe-32","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-32.1.1","title":"Research: confirm promptAsync vs prompt behavior","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-21T12:56:13.271134+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-21T12:58:58.330246+02:00","closed_at":"2026-04-21T12:58:58.330246+02:00","close_reason":"Closed","dependencies":[{"issue_id":"responsible-vibe-32.1.1","depends_on_id":"responsible-vibe-32.1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-32.1.2","title":"Design: inject full phase instructions into compacting hook","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-21T12:56:13.49865+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-21T12:58:58.513881+02:00","closed_at":"2026-04-21T12:58:58.513881+02:00","close_reason":"Closed","dependencies":[{"issue_id":"responsible-vibe-32.1.2","depends_on_id":"responsible-vibe-32.1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-32.2","title":"Implement","description":"Write clean, focused code for the minor enhancement, test your changes, and prepare for commit. **STEP 1: Review Design and Requirements** - If exists: Follow your design from - Otherwise: Elaborate design options and present them to the user - If exists: Ensure the relevant requirements from are met - Otherwise: Ensure existing requirements are met based on your task context **STEP 2: Implement Changes** - Write clean, focused code for the minor enhancement - Test your changes to ensure they work correctly and don't break existing functionality **STEP 3: Prepare for Finalization** - Update task progress as needed - Prepare documentation and commit when ready","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-21T11:12:20.251568+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-21T11:12:20.251568+02:00","dependencies":[{"issue_id":"responsible-vibe-32.2","depends_on_id":"responsible-vibe-32","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"responsible-vibe-32.2","depends_on_id":"responsible-vibe-32.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-32.3","title":"Finalize","description":"Ensure code quality and documentation accuracy through systematic cleanup and review. **STEP 1: Code Cleanup** Systematically clean up development artifacts: - **Remove Debug Output**: Search for and remove all temporary debug output statements used during development. Look for language-specific debug output methods (console logging, print statements, debug output functions). Remove any debugging statements that were added for development purposes. - **Review TODO/FIXME Comments**: - Address each TODO/FIXME comment by either implementing the solution or documenting why it's deferred - Remove completed TODOs - Convert remaining TODOs to proper issue tracking if needed - **Remove Debugging Code Blocks**: - Remove temporary debugging code, test code blocks, and commented-out code - Clean up any experimental code that's no longer needed - Ensure proper error handling replaces temporary debug logging **STEP 2: Documentation Review** Review and update documentation to reflect final implementation: - **Update Long-Term Memory Documents**: Based on what was actually implemented: - If exists: Update if requirements changed during development - If exists: Update if design details were refined or changed - **Compare Against Implementation**: Review documentation against actual implemented functionality - **Update Changed Sections**: Only modify documentation sections that have functional changes - **Remove Development Progress**: Remove references to development iterations, progress notes, and temporary decisions - **Focus on Final State**: Ensure documentation describes the final implemented state, not the development process - **Ask User to Review Document Updates** **STEP 3: Final Validation** - Run existing tests to ensure cleanup didn't break functionality - Verify documentation accuracy with a final review - Ensure minor enhancement is ready for delivery - Update task progress and mark completed work as you finalize the minor enhancement","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-21T11:12:20.567032+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-21T11:12:20.567032+02:00","dependencies":[{"issue_id":"responsible-vibe-32.3","depends_on_id":"responsible-vibe-32","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"responsible-vibe-32.3","depends_on_id":"responsible-vibe-32.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} {"id":"responsible-vibe-4","title":"Responsible-Vibe Development: responsible-vibe","description":"Development session using minor workflow for responsible-vibe","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-01-20T08:45:26.033247+01:00","created_by":"Oliver Jägle","updated_at":"2026-01-20T08:45:26.033247+01:00"} {"id":"responsible-vibe-4.1","title":"Explore","description":"minor workflow explore phase tasks","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-01-20T08:45:26.241377+01:00","created_by":"Oliver Jägle","updated_at":"2026-01-20T08:45:26.241377+01:00","dependencies":[{"issue_id":"responsible-vibe-4.1","depends_on_id":"responsible-vibe-4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} {"id":"responsible-vibe-4.1.1","title":"Analyze current verbose task management sections in plan file templates","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-01-20T08:46:16.960883+01:00","created_by":"Oliver Jägle","updated_at":"2026-01-20T08:46:58.154323+01:00","closed_at":"2026-01-20T08:46:58.154323+01:00","close_reason":"Closed","dependencies":[{"issue_id":"responsible-vibe-4.1.1","depends_on_id":"responsible-vibe-4.1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} diff --git a/.beads/last-touched b/.beads/last-touched index 2c5986f5..88e50fe6 100644 --- a/.beads/last-touched +++ b/.beads/last-touched @@ -1 +1 @@ -responsible-vibe-31.4.3 +responsible-vibe-32.1.2 diff --git a/.vibe/development-plan-continue-after-compact-2.md b/.vibe/development-plan-continue-after-compact-2.md new file mode 100644 index 00000000..0b4e316c --- /dev/null +++ b/.vibe/development-plan-continue-after-compact-2.md @@ -0,0 +1,75 @@ +# Development Plan: responsible-vibe (continue-after-compact-2 branch) + +*Generated on 2026-04-21 by Vibe Feature MCP* +*Workflow: [minor](https://mrsimpson.github.io/responsible-vibe-mcp/workflows/minor)* + +## Goal +After OpenCode context compaction, the AI should automatically continue with full workflow phase context. Two scenarios: +1. **Auto compaction** (`auto: true`): OpenCode creates a synthetic "Continue if you have next steps..." message, but it bypasses `chat.message` hook → phase instructions not injected. +2. **Manual compaction** (`auto: false`, triggered via UI): No synthetic continue message is created at all. Our plugin fires `client.session.prompt()` but the loop exits immediately without making an LLM call. + +## Key Decisions + +### Root cause findings (2026-04-21 deep dive) + +**Manual compaction (`auto: false`):** +- The compaction task part in the DB has `auto=0` — all observed compactions were triggered via the OpenCode TUI with `auto: false` +- Since `auto: false`, the synthetic "Continue if you have next steps..." message is NEVER created +- `session.compacted` is still published (it fires regardless of auto) +- Our plugin calls `client.session.prompt()` → POST `/session/{id}/message` → returns with `duration=0` +- The loop exits IMMEDIATELY without calling the LLM — confirmed by absence of `session.prompt step=0 loop` logs and no LLM activity in the ~1m46s window before user's manual message +- The exact cause of immediate loop exit is unclear but likely related to race conditions between the runner state machine and our prompt call timing + +**Auto compaction (`auto: true`):** +- Synthetic "Continue..." message IS created (with `synthetic: true` on the text part) +- The `chat.message` hook is NOT triggered for synthetic messages — this is the original stated problem +- AI responds but without phase context + +**Session.idle timing issue:** +- `session.idle` fires SIMULTANEOUSLY with `session.compacted` (same millisecond in logs) +- This is because after auto compaction, `session.idle` fires at the compaction step boundary +- For manual compaction: the loop at step=2 exits in 2ms after the compaction summary is created, then `session.idle` fires + +### Proposed fix approach + +**Two-pronged strategy:** + +1. **Enhance `experimental.session.compacting` hook** to inject FULL phase instructions into the compaction context/prompt. This makes the summary self-sufficient — it embeds the phase context so the AI knows what to continue even without `chat.message` firing. + +2. **Fix the post-compaction prompt injection:** + - Switch from `client.session.prompt()` (synchronous, blocks) to `client.session.promptAsync()` (fires-and-forgets, non-blocking) + - Add a small delay (e.g., 500ms) before calling to avoid race conditions with runner state transitions + - Use `client.session.promptAsync()` to inject "Continue with the current phase." so the loop runs properly without our code blocking on the HTTP response + +### Why `experimental.compaction.autocontinue` is not the solution +Setting `enabled: false` would prevent the synthetic message entirely and force reliance on our prompt — but this doesn't solve the immediate loop exit issue. + +## Notes +- All 3 compaction tasks in the DB have `auto=0` — this project uses manual compaction (via TUI) +- Session ID `ses_2516d00b9ffesNfmx4bVKPm4Hh` is a long-running session with messages from multiple days +- The `duration=0` for the HTTP `/session/{id}/message` response is NORMAL for streaming endpoints (measures time to first byte, not full response) +- The `chat.message` hook DOES fire for our `client.session.prompt()` messages — confirmed by synthetic phase instructions appearing in DB parts +- After the loop exits at step=2 (2ms), there's 1m46s of silence before user's manual message — our `promptAsync` call must have had no effect + +## Explore + +### Tasks + +*Auto-synced — do not edit here, use `bd` CLI instead.* + +- [x] `responsible-vibe-32.1.1` Research: confirm promptAsync vs prompt behavior +- [x] `responsible-vibe-32.1.2` Design: inject full phase instructions into compacting hook + +## Implement + +### Tasks + +*Auto-synced — do not edit here, use `bd` CLI instead.* + + +## Finalize + +### Tasks + +*Auto-synced — do not edit here, use `bd` CLI instead.* + diff --git a/packages/core/src/instruction-generator.ts b/packages/core/src/instruction-generator.ts index 7bd4b799..c525b546 100644 --- a/packages/core/src/instruction-generator.ts +++ b/packages/core/src/instruction-generator.ts @@ -107,10 +107,15 @@ export class InstructionGenerator implements IInstructionGenerator { const phaseName = capitalizePhase(phase); + // IMPORTANT: Directive markers to make instructions stand out from context let workflowSection = `--- -**Read \`${conversationContext.planFilePath}\`** for context. -- Focus on "${phaseName}" tasks, log decisions in "Key Decisions" -- Do NOT use other task/todo tools - use only the plan file for task tracking`; +### YOU MUST FOLLOW THESE INSTRUCTIONS: + +**IMPORTANT: Read \`${conversationContext.planFilePath}\`** for context. + +**ACTION REQUIRED: Focus on "${phaseName}" tasks** and log decisions in "Key Decisions" + +**CRITICAL: Do NOT use other task/todo tools** - use only the plan file for task tracking`; // Add file restriction guidance if patterns are restricted if ( diff --git a/packages/opencode-plugin/src/plugin.ts b/packages/opencode-plugin/src/plugin.ts index a8891037..88c183ce 100644 --- a/packages/opencode-plugin/src/plugin.ts +++ b/packages/opencode-plugin/src/plugin.ts @@ -162,6 +162,10 @@ export const WorkflowsPlugin: Plugin = async ( // Consumed and cleared by the next chat.message hook call. let bufferedInstructions: BufferedInstructions | null = null; + // Tracks sessions that just completed compaction and need a phase-aware + // continue message once the session becomes idle. + let postCompactionSession: string | null = null; + // Last-known model from chat.message hook. Cached so proceed_to_phase can // pass providerID + modelID to the summarize API (which requires them). let lastKnownModel: { providerID: string; modelID: string } | null = null; @@ -473,8 +477,10 @@ ACTION REQUIRED: Use transition_phase tool to move to a phase that allows editin /** * Hook 3: experimental.session.compacting - * Fires when session is being compacted. We provide minimal guidance on what - * to preserve and instruct the summary to end with phase continuation. + * Fires when session is being compacted. We provide full phase instructions + * so the compaction summary is self-sufficient — the AI knows exactly what + * to continue even if the chat.message hook doesn't fire for the synthetic + * auto-compaction "continue" message. */ 'experimental.session.compacting': async (hookInput, output) => { logger.debug('experimental.session.compacting hook fired', { @@ -487,14 +493,105 @@ ACTION REQUIRED: Use transition_phase tool to move to a phase that allows editin return; } + // Get full phase instructions to embed in compaction context + let phaseInstructions: string | null = null; + try { + const serverContext = await getServerContext(); + const handler = new WhatsNextHandler(); + const handlerResult = await handler.handle({}, serverContext); + if (handlerResult.success && handlerResult.data) { + phaseInstructions = stripWhatsNextReferences( + handlerResult.data.instructions + ); + } + } catch (_err) { + // Fall back to minimal guidance if instructions can't be fetched + } + output.context.push( 'Preserve: user intents, key decisions, significant changes and the reasoning why they were made. Remove tool calls, intermediate thoughts, and minor details.' ); - output.context.push( - `End summary with: "Continue ${state.phase} phase. ${state.phaseDescription || ''}"` - ); - logger.info('Injected compaction guidance', { phase: state.phase }); + if (phaseInstructions) { + output.context.push( + `Current workflow phase: ${state.phase}. After compaction, resume with full phase context:\n\n${phaseInstructions}` + ); + } else { + output.context.push( + `End summary with: "Continue ${state.phase} phase. ${state.phaseDescription || ''}"` + ); + } + + logger.info('Injected compaction guidance', { + phase: state.phase, + fullInstructions: phaseInstructions !== null, + }); + }, + + /** + * Hook 4: event + * Listens for bus events. When a session compaction completes we record it, + * then when the session becomes idle we send a real user message so the + * normal chat.message hook fires and injects phase instructions — giving the + * AI full workflow context to continue after the compaction. + * + * We intentionally do NOT suppress the default synthetic "continue" message + * (experimental.compaction.autocontinue). It may produce a first generic AI + * response, but the idle trigger below ensures a proper phase-aware follow-up. + */ + event: async ({ event }) => { + logger.debug('event hook fired', { type: event.type }); + + if (event.type === 'session.compacted') { + postCompactionSession = event.properties.sessionID as string; + logger.info('session.compacted: pending phase-aware continue', { + sessionID: postCompactionSession, + }); + return; + } + + if ( + event.type === 'session.idle' && + postCompactionSession === (event.properties.sessionID as string) + ) { + const sessionID = postCompactionSession; + postCompactionSession = null; + logger.info( + 'session.idle after compaction: sending phase-aware continue', + { sessionID } + ); + + // Wait a short time to allow the OpenCode runner state machine to + // fully settle after compaction before we fire the follow-up prompt. + await new Promise(resolve => setTimeout(resolve, 500)); + + try { + const client = input.client as { + session: { + promptAsync(params: { + path: { id: string }; + body: { parts: Array<{ type: string; text: string }> }; + }): Promise; + }; + }; + await client.session.promptAsync({ + path: { id: sessionID }, + body: { + parts: [ + { type: 'text', text: 'Continue with the current phase.' }, + ], + }, + }); + logger.info('session.idle: phase-aware continue sent (async)', { + sessionID, + }); + } catch (err) { + logger.error('session.idle: failed to send phase-aware continue', { + sessionID, + error: err instanceof Error ? err.message : String(err), + }); + } + } }, /** diff --git a/packages/opencode-plugin/src/types.ts b/packages/opencode-plugin/src/types.ts index 66e4e555..b6ea0036 100644 --- a/packages/opencode-plugin/src/types.ts +++ b/packages/opencode-plugin/src/types.ts @@ -74,9 +74,24 @@ export type ToolDefinition = { execute(args: unknown, context: ToolContext): Promise; }; +// Minimal Event types from @opencode-ai/sdk needed for the event hook +export type SessionCompactedEvent = { + type: 'session.compacted'; + properties: { sessionID: string }; +}; +export type SessionIdleEvent = { + type: 'session.idle'; + properties: { sessionID: string }; +}; +export type OtherEvent = { + type: string; + properties: Record; +}; +export type BusEvent = SessionCompactedEvent | SessionIdleEvent | OtherEvent; + // All available hooks export interface Hooks { - event?: (input: { event: unknown }) => Promise; + event?: (input: { event: BusEvent }) => Promise; config?: (input: unknown) => Promise; tool?: { [key: string]: ToolDefinition;