import type { AppState, Feature, FeatureColumn, PulseEvent } from '../../store/types' import { formatDateTime } from '../../utils/format' const columnLabels: Record = { now: 'Now', next: 'Next', later: 'Later', done: 'Done', } const groupFeatures = (features: Feature[]) => ({ now: features.filter((feature) => feature.column === 'now'), next: features.filter((feature) => feature.column === 'next'), later: features.filter((feature) => feature.column === 'later'), done: features.filter((feature) => feature.column === 'done'), }) const getFeatureLabel = (features: Feature[], featureId?: string) => { if (!featureId) return '—' return features.find((feature) => feature.id === featureId)?.title ?? `${featureId} (missing feature)` } const renderFeature = (feature: Feature) => { const criteria = feature.acceptance_criteria.length ? feature.acceptance_criteria.map((item) => ` - ${item}`).join('\n') : ' - None yet' return [ `- **${feature.title}**`, ` - Description: ${feature.description || '—'}`, ` - Priority: ${feature.priority}`, ` - Status: ${feature.status}`, ' - Acceptance Criteria:', criteria, ` - Scope Notes: ${feature.scope_notes || '—'}`, ].join('\n') } const sortPulsesNewestFirst = (pulses: PulseEvent[]) => [...pulses].sort((a, b) => b.timestamp.localeCompare(a.timestamp)) export const createAgentSessionPrompt = ( state: AppState, options?: { featureId?: string target?: string }, ) => { const grouped = groupFeatures(state.features) const target = options?.target || 'AI coding agent' const focusFeature = options?.featureId ? state.features.find((feature) => feature.id === options.featureId) ?? null : grouped.now[0] ?? null const relatedPulses = sortPulsesNewestFirst(state.pulses) .filter((pulse) => (focusFeature ? pulse.feature_id === focusFeature.id : true)) .slice(0, 6) const renderFeatureBlock = (feature: Feature | null) => { if (!feature) return '- No specific feature selected yet. Choose the smallest useful next move.' return [ `- Title: ${feature.title}`, `- Description: ${feature.description || '—'}`, `- Column: ${columnLabels[feature.column]}`, `- Priority: ${feature.priority}`, `- Status: ${feature.status}`, `- Acceptance criteria: ${feature.acceptance_criteria.length ? feature.acceptance_criteria.join('; ') : 'None yet'}`, `- Scope notes: ${feature.scope_notes || '—'}`, ].join('\n') } return [ `You are the ${target} for ${state.project.name}.`, '', 'Read the project context below and ship the smallest high-quality improvement that satisfies the focus feature without scope creep.', '', '## Project', `- Name: ${state.project.name}`, `- Pitch: ${state.project.one_line_pitch || '—'}`, `- Current goal: ${state.project.current_goal || '—'}`, `- Notes: ${state.project.notes || '—'}`, '', '## Focus Feature', renderFeatureBlock(focusFeature), '', '## Other Active Features', grouped.now.filter((feature) => feature.id !== focusFeature?.id).length ? grouped.now .filter((feature) => feature.id !== focusFeature?.id) .map((feature) => `- ${feature.title} (${feature.status}, ${feature.priority})`) .join('\n') : '- None beyond the focus feature.', '', '## Next Up', grouped.next.length ? grouped.next.map((feature) => `- ${feature.title} (${feature.status})`).join('\n') : '- Nothing queued yet.', '', '## Parking Lot / Do Not Implement Yet', state.parking_lot.length ? state.parking_lot.map((item) => `- ${item.title} — ${item.reason_parked || item.description || 'Parked for later.'}`).join('\n') : '- Nothing parked yet.', '', '## Recent Relevant Pulses', relatedPulses.length ? relatedPulses.map((pulse) => `- ${formatDateTime(pulse.timestamp)} · ${pulse.pulse_type} · ${pulse.message}`).join('\n') : '- No relevant pulse events yet.', '', '## Rules', '- Stay inside the focus feature unless a tiny supporting fix is required.', '- Do not implement Parking Lot items.', '- Preserve working behavior and avoid needless rewrites.', '- Prefer the smallest shippable step with clear evidence.', '', '## Deliver back', '- What changed', '- Files touched', '- How you verified it', '- Any blocker or follow-up you intentionally parked', ].join('\n') } export const createJsonExport = (state: AppState) => JSON.stringify( { schema_version: state.schema_version, exported_at: new Date().toISOString(), project: state.project, features: state.features, parking_lot: state.parking_lot, pulses: state.pulses, settings: state.settings, }, null, 2, ) export const createPulseJsonl = (state: AppState) => state.pulses.map((pulse) => JSON.stringify(pulse)).join('\n') export const createMarkdownPackage = (state: AppState) => { const grouped = groupFeatures(state.features) const recentPulses = sortPulsesNewestFirst(state.pulses).slice(0, 8) const projectSummary = `# Project Summary\n\n## Name\n${state.project.name}\n\n## One-Line Pitch\n${state.project.one_line_pitch || '—'}\n\n## Description\n${state.project.description || '—'}\n\n## Current Goal\n${state.project.current_goal || '—'}\n\n## Notes\n${state.project.notes || '—'}\n` const featurePlan = ['# Feature Plan'] ;(['now', 'next', 'later', 'done'] as FeatureColumn[]).forEach((column) => { featurePlan.push(`\n## ${columnLabels[column]}`) const features = grouped[column] if (!features.length) { featurePlan.push('\n_No features in this column yet._') return } featurePlan.push('', ...features.map(renderFeature)) }) const parkingLot = [ '# Parking Lot', '', ...(state.parking_lot.length ? state.parking_lot.map( (item) => `- **${item.title}**\n - Description: ${item.description || '—'}\n - Reason parked: ${item.reason_parked || '—'}\n - Possible future placement: ${item.possible_future_placement || '—'}\n - Risk level: ${item.risk_level}`, ) : ['_No parked ideas yet._']), ].join('\n') const pulseLog = [ '# Pulse Log', '', ...(sortPulsesNewestFirst(state.pulses).length ? sortPulsesNewestFirst(state.pulses).map( (pulse) => `- **${formatDateTime(pulse.timestamp)}** — ${pulse.pulse_type}\n - Feature: ${getFeatureLabel(state.features, pulse.feature_id)}\n - Source/Agent: ${pulse.source} / ${pulse.agent_id}\n - Message: ${pulse.message}\n - Confidence: ${pulse.confidence_score}\n - Evidence: ${pulse.evidence_refs.length ? pulse.evidence_refs.join('; ') : '—'}`, ) : ['_No pulse events yet._']), ].join('\n') const claudeContext = [ '# AI Coding Context', '', '## Project', `${state.project.name} — ${state.project.one_line_pitch || '—'}`, '', '## Current Goal', state.project.current_goal || '—', '', '## Active Features', grouped.now.length ? grouped.now.map(renderFeature).join('\n\n') : '_None yet._', '', '## Next Features', grouped.next.length ? grouped.next.map(renderFeature).join('\n\n') : '_None yet._', '', '## Done Features', grouped.done.length ? grouped.done.map(renderFeature).join('\n\n') : '_None yet._', '', '## Parking Lot / Do Not Implement Yet', state.parking_lot.length ? state.parking_lot .map((item) => `- ${item.title} — ${item.reason_parked || item.description || 'Parked for later.'}`) .join('\n') : '_No parked ideas yet._', '', '## Recent Pulse Events', recentPulses.length ? recentPulses .map( (pulse) => `- ${formatDateTime(pulse.timestamp)} — ${pulse.pulse_type} — ${pulse.message} (${getFeatureLabel(state.features, pulse.feature_id)})`, ) .join('\n') : '_No recent pulse events yet._', '', '## Instructions for AI Developer', '- Only work on the selected feature.', '- Do not implement Parking Lot items.', '- Preserve working behavior.', '- Report changes and test steps.', ].join('\n') return { 'PROJECT_SUMMARY.md': projectSummary, 'FEATURE_PLAN.md': featurePlan.join('\n'), 'PARKING_LOT.md': parkingLot, 'PULSE_LOG.md': pulseLog, 'CLAUDE_CONTEXT.md': claudeContext, } }