226 lines
8.4 KiB
TypeScript
226 lines
8.4 KiB
TypeScript
import type { AppState, Feature, FeatureColumn, PulseEvent } from '../../store/types'
|
|
import { formatDateTime } from '../../utils/format'
|
|
|
|
const columnLabels: Record<FeatureColumn, string> = {
|
|
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,
|
|
}
|
|
}
|