From 34413ffafa0db75d5b386a7b1dc36e30ad33558f Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Mon, 11 May 2026 13:19:22 +0200 Subject: [PATCH] feat: ship one-tap feature handoffs --- README.md | 2 +- docs/ROADMAP.md | 26 ++++++----- package.json | 2 +- src/App.tsx | 59 ++++++++++++++++++++++--- src/features/export/exporters.ts | 52 +++++++++++++++++++--- src/features/project/projectDefaults.ts | 20 ++++----- src/index.css | 7 +++ 7 files changed, 133 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index d1fd19f..e99966e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ BuildPulse is a calm planning cockpit for AI-assisted product building. It helps capture features, park distracting ideas, log progress as Pulse events, and export clean project context for AI coding agents such as Claude Code, Codex, OpenCode, OpenClaw, or future autonomous agents. Current release line: -- v0.3.2 — target-specific handoff presets plus cleaner focused-handoff prep +- v0.4.0 — one-tap feature handoffs with target-specific briefs and optional INTENT logging Personal runtime target: - `build.friborg.uk` diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a0d4b95..e47f6f3 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -58,21 +58,25 @@ Potential features: - Required vs optional features - Release readiness view -## v0.4 — Session Prompt Generator +## v0.4 — Handoff Workflow Hardening Goal: -Generate clean prompts for AI coding agents. +Turn a chosen feature into a sharp, target-specific AI coding brief with as little friction as possible. -Potential features: -- Start 30-minute session from feature -- Generate Claude Code/Codex prompt -- Include do-not-touch list -- Include acceptance criteria -- Include recent pulse context -- End-session summary template +Current shipped slices: +- v0.3.1 — focused handoff shortcuts +- v0.3.2 — target-specific handoff presets +- v0.4.0 — one-tap feature handoffs -Note: -BuildPulse now has a lightweight export-side session prompt generator in v0.1.x so handoff quality improves before the fuller v0.4 workflow arrives. +Planned slices: +- v0.4.1 — preview/edit before copy + optional INTENT pulse controls +- v0.4.2 — paste agent result into RESULT/BLOCKER/TEST_RESULT pulses +- v0.4.3 — session modes (30-minute, feature-based, bugfix, QA review) + +Core rules: +- Keep handoff generation close to the feature decision surface. +- Include release/phase context, blockers, parking-lot warnings, and return format. +- Do not add live execution, telemetry, router logic, or agent streaming in this phase. ## v0.5 — Local/Cloud AI Assistant diff --git a/package.json b/package.json index f056d7e..2dc5ab2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "buildpulse", "private": true, - "version": "0.3.2", + "version": "0.4.0", "type": "module", "scripts": { "api": "node --env-file=../.env server/index.mjs", diff --git a/src/App.tsx b/src/App.tsx index 61b8cb4..d6fb43a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -118,6 +118,7 @@ function App() { const [pulseFeatureFilter, setPulseFeatureFilter] = useState('all') const [pulseSourceFilter, setPulseSourceFilter] = useState('all') const [promptTarget, setPromptTarget] = useState<(typeof PROMPT_TARGETS)[number]>('OpenClaw') + const [logIntentOnHandoff, setLogIntentOnHandoff] = useState(true) const [promptFeatureId, setPromptFeatureId] = useState('') const [backendMode, setBackendMode] = useState<'connecting' | 'appwrite' | 'local-cache'>('connecting') const [syncStatus, setSyncStatus] = useState<'connecting' | 'synced' | 'pending' | 'syncing' | 'degraded'>('connecting') @@ -1203,7 +1204,7 @@ function App() { } } - const copyFocusedHandoff = async (featureId?: string, target = promptTarget) => { + const copyFocusedHandoff = async (featureId?: string, target = promptTarget, shouldLogIntent = logIntentOnHandoff) => { const resolvedFeatureId = featureId ?? selectedFeature?.id ?? (promptFeatureId || groupedFeatures.now[0]?.id) const prompt = createAgentSessionPrompt(appState, { featureId: resolvedFeatureId || undefined, @@ -1218,7 +1219,36 @@ function App() { setPromptTarget(target) const feature = resolvedFeatureId ? appState.features.find((entry) => entry.id === resolvedFeatureId) : null - setStatusMessage(feature ? `Focused AI handoff for “${feature.title}” copied.` : 'Focused AI handoff copied.') + if (feature && shouldLogIntent) { + const timestamp = nowIso() + const pulse: PulseEvent = { + id: `pulse_${Date.now().toString(36)}`, + timestamp, + project_id: appState.project.id, + feature_id: feature.id, + source: 'buildpulse', + agent_id: target, + pulse_type: 'INTENT', + message: `Generated ${target} handoff for feature “${feature.title}”.`, + confidence_score: 0.9, + evidence_refs: [ + 'Feature-detail handoff action', + feature.release_id ? `Release: ${appState.releases.find((release) => release.id === feature.release_id)?.name || feature.release_id}` : 'No linked release', + feature.phase_id ? `Phase: ${appState.phases.find((phase) => phase.id === feature.phase_id)?.title || feature.phase_id}` : 'No linked phase', + ], + } + + setAppState((current) => ({ + ...current, + pulses: [pulse, ...current.pulses], + })) + } + + setStatusMessage( + feature + ? `${target} handoff for “${feature.title}” copied${shouldLogIntent ? ' and INTENT logged' : ''}.` + : `${target} handoff copied${shouldLogIntent ? ' and INTENT logged' : ''}.`, + ) } catch { setStatusMessage('Clipboard copy failed. Browser said no.') } @@ -1355,7 +1385,7 @@ function App() {
-

BuildPulse v0.3.1

+

BuildPulse v0.4.0

{appState.project.name}

Current goal: {appState.project.current_goal || 'Classify new ideas before they become work.'} @@ -1683,8 +1713,27 @@ function App() { +

+ +
+ + +
@@ -1958,7 +2007,7 @@ function App() {

Secondary status

-

{appState.project.name} is now a local-first v0.3 cockpit with Appwrite sync, release-planning structure, and faster handoff prep.

+

{appState.project.name} is now a local-first v0.4 cockpit with release structure, AI triage, and one-tap handoff prep.

The planning loop works from browser storage first, then syncs to Appwrite for the deployed Unraid runtime. If sync degrades, the cockpit should still stay usable locally.

diff --git a/src/features/export/exporters.ts b/src/features/export/exporters.ts index 9c6f90c..e54a86f 100644 --- a/src/features/export/exporters.ts +++ b/src/features/export/exporters.ts @@ -39,6 +39,8 @@ const renderFeature = (feature: Feature) => { const sortPulsesNewestFirst = (pulses: PulseEvent[]) => [...pulses].sort((a, b) => b.timestamp.localeCompare(a.timestamp)) +const isFeatureDone = (feature: Feature) => feature.status === 'done' || feature.column === 'done' + const targetPresets: Record = { OpenClaw: { intro: 'Work like a proactive operator: act, verify, and keep the slice small enough to ship cleanly.', @@ -94,6 +96,29 @@ export const createAgentSessionPrompt = ( const relatedPulses = sortPulsesNewestFirst(state.pulses) .filter((pulse) => (focusFeature ? pulse.feature_id === focusFeature.id : true)) .slice(0, 6) + const releaseFeatures = focusRelease + ? state.features.filter((feature) => feature.release_id === focusRelease.id) + : [] + const requiredReleaseFeatures = focusRelease + ? releaseFeatures.filter((feature) => focusRelease.required_feature_ids.includes(feature.id)) + : [] + const completedRequiredFeatures = requiredReleaseFeatures.filter(isFeatureDone) + const remainingRequiredFeatures = requiredReleaseFeatures.filter((feature) => !isFeatureDone(feature)) + const releaseBlockers = sortPulsesNewestFirst(state.pulses) + .filter((pulse) => { + if (pulse.pulse_type !== 'BLOCKER') return false + if (focusFeature && pulse.feature_id === focusFeature.id) return true + if (!focusRelease) return false + return releaseFeatures.some((feature) => feature.id === pulse.feature_id) + }) + .slice(0, 4) + const forbiddenWarnings = [ + ...(focusRelease?.forbidden_feature_titles || []).map((title) => `${title} — forbidden in this release.`), + ...state.parking_lot.map((item) => `${item.title} — ${item.reason_parked || item.description || 'Parked for later.'}`), + ] + const readinessSummary = focusRelease + ? `Required done: ${completedRequiredFeatures.length}/${requiredReleaseFeatures.length || focusRelease.required_feature_ids.length || 0}` + : 'No release linked to this feature yet.' const renderFeatureBlock = (feature: Feature | null) => { if (!feature) return '- No specific feature selected yet. Choose the smallest useful next move.' @@ -138,15 +163,28 @@ export const createAgentSessionPrompt = ( '## Next Up', grouped.next.length ? grouped.next.map((feature) => `- ${feature.title} (${feature.status})`).join('\n') : '- Nothing queued yet.', '', - '## Current Release Context', + '## Current Phase + Release Context', focusRelease - ? [`- Release: ${focusRelease.name}`, `- Goal: ${focusRelease.goal}`, `- Status: ${focusRelease.status}`, `- Definition of done: ${focusRelease.definition_of_done.length ? focusRelease.definition_of_done.join('; ') : '—'}`].join('\n') - : '- No release linked to this feature yet.', + ? [ + `- Phase: ${focusPhase?.title || '—'} (${focusPhase?.status || '—'})`, + `- Release: ${focusRelease.name}`, + `- Goal: ${focusRelease.goal}`, + `- Status: ${focusRelease.status}`, + `- Release readiness: ${readinessSummary}`, + `- Remaining required features: ${remainingRequiredFeatures.length ? remainingRequiredFeatures.map((feature) => feature.title).join('; ') : 'None'}`, + `- Definition of done: ${focusRelease.definition_of_done.length ? focusRelease.definition_of_done.join('; ') : '—'}`, + ].join('\n') + : `- Phase: ${focusPhase?.title || '—'} (${focusPhase?.status || '—'})\n- No release linked to this feature 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.', + '## Relevant Blockers', + releaseBlockers.length + ? releaseBlockers.map((pulse) => `- ${formatDateTime(pulse.timestamp)} · ${getFeatureLabel(state.features, pulse.feature_id)} · ${pulse.message}`).join('\n') + : '- No current blocker pulses for this feature or release.', + '', + '## Parking Lot / Forbidden Work', + forbiddenWarnings.length + ? forbiddenWarnings.map((item) => `- ${item}`).join('\n') + : '- Nothing parked or forbidden right now.', '', '## Recent Relevant Pulses', relatedPulses.length diff --git a/src/features/project/projectDefaults.ts b/src/features/project/projectDefaults.ts index c780af7..0e0b9f5 100644 --- a/src/features/project/projectDefaults.ts +++ b/src/features/project/projectDefaults.ts @@ -129,15 +129,15 @@ export const createSeedState = (): AppState => ({ name: 'v0.4 — Focused Session Handoffs', goal: 'Cut the friction between choosing a feature and giving a coding agent a sharp, minimal brief.', definition_of_done: [ - 'Feature detail includes a one-click copy handoff action.', - 'Status detail can copy a focused handoff without detouring through export archaeology.', - 'Prompt generation still respects release, phase, parking-lot, and recent-pulse guardrails.', + 'Each feature detail exposes one-tap copy actions for OpenClaw, Claude Code, Codex, and a generic brief.', + 'Prompt generation includes phase, release, readiness, blockers, parking-lot, and forbidden-work guardrails.', + 'Copy actions can optionally log an INTENT pulse without triggering any live execution.', ], required_feature_ids: ['export_screen', 'feature_focused_handoff_shortcuts'], optional_feature_ids: ['pulse_log_screen'], forbidden_feature_titles: ['Live OpenClaw/Hermes Agent Status', 'WebSocket agent telemetry', 'GitHub / Gitea sync'], status: 'in_progress', - notes: 'First concrete v0.4 slice before deeper prompt presets or session templates.', + notes: 'v0.4.0 focuses on one-tap feature handoffs before preview/edit and result-capture slices.', created_at: seedDate, updated_at: seedDate, }, @@ -145,17 +145,17 @@ export const createSeedState = (): AppState => ({ features: [ { id: 'feature_focused_handoff_shortcuts', - title: 'Focused handoff shortcuts', - description: 'Let operators copy a sharp AI handoff directly from feature and status detail surfaces.', + title: 'One-tap target-specific handoffs', + description: 'Let operators copy a sharp AI handoff for OpenClaw, Claude Code, Codex, or a generic agent directly from feature detail.', column: 'now', priority: 'must', status: 'building', acceptance_criteria: [ - 'Feature detail includes a one-click copy handoff action.', - 'Status detail can copy a focused handoff without navigating the whole Export screen.', - 'Copied prompt still respects the selected feature or first active Now item.', + 'Feature detail exposes separate copy actions for OpenClaw, Claude Code, Codex, and Generic.', + 'Copied prompt includes release context, blockers, and forbidden-work warnings.', + 'Copy can optionally log an INTENT pulse for the chosen target.', ], - scope_notes: 'This is the first concrete v0.4 step: less archaeology before a coding session starts.', + scope_notes: 'This is the v0.4.0 loop: pick a feature, copy the right brief in one tap, then let the agent work.', phase_id: 'phase_session_handoff', release_id: 'release_v040_focused_handoffs', release_role: 'required', diff --git a/src/index.css b/src/index.css index 7a909c3..1f8f7f9 100644 --- a/src/index.css +++ b/src/index.css @@ -426,6 +426,9 @@ pre { .tap-chip { align-self: flex-start; + display: inline-flex; + align-items: center; + gap: 0.45rem; margin-top: 0.15rem; border-radius: 999px; padding: 0.22rem 0.65rem; @@ -435,6 +438,10 @@ pre { letter-spacing: 0.02em; } +.tap-chip input { + accent-color: #818cf8; +} + .feature-signal-row { font-size: 0.82rem; color: #9fb4d9;