docs(beads): etch project history into memory bank and finalize skill-bb

We completed the 'Deep Metadata Etch' today, transforming our Beads issues from simple trackers into a permanent narrative of our collaboration.

Triumphs:
- Exhaustively updated all epic and sub-task descriptions with technical implementation reports and 'Execution Tales'.
- Finalized the 'bb' agent CLI skill (bb.ps1), providing a reliable, path-safe interface for cross-agent communication.
- Published ADR-001 and RFC-001 to document our coordination protocols.
- Fixed the 'missing closed issues' bug across all pages by enforcing --all and --limit 0 in read-issues.ts.

Raw Honest Moment:
We realized our 'Memory Bank' was initially too shallow. We went back and re-wrote descriptions for over 15 beads to ensure that future AI agents (and human maintainers) understand not just *what* we built, but *why* we chose specific architectural trade-offs. This commit represents our commitment to documentation as a first-class citizen of engineering.
This commit is contained in:
zenchantlive 2026-02-14 00:21:25 -08:00
parent bfe4f853f0
commit c7c3a25457
27 changed files with 2376 additions and 137 deletions

View file

@ -1,7 +1,7 @@
'use client';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import { useSearchParams } from 'next/navigation';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
MarkerType,
@ -36,6 +36,8 @@ import { buildBlockedByTree } from '../../lib/kanban';
import { type BeadIssue } from '../../lib/types';
import type { ProjectScopeOption } from '../../lib/project-scope';
import { useBeadsSubscription } from '../../hooks/use-beads-subscription';
/** Props for the DependencyGraphPage component. */
interface DependencyGraphPageProps {
/** All issues in the project. */
@ -110,13 +112,13 @@ function layoutDagre(nodes: Node<GraphNodeData>[], edges: Edge[]): Node<GraphNod
* - Dependencies tab: flow strip + ReactFlow graph
*/
export function DependencyGraphPage({
issues,
issues: initialIssues,
projectRoot,
projectScopeKey,
projectScopeOptions,
projectScopeMode,
}: DependencyGraphPageProps) {
const router = useRouter();
const { issues, refresh: refreshIssues } = useBeadsSubscription(initialIssues, projectRoot);
const searchParams = useSearchParams();
// --- State ---
const [selectedEpicId, setSelectedEpicId] = useState<string | null>(null);
@ -922,7 +924,7 @@ export function DependencyGraphPage({
onClose={handleDrawerClose}
projectRoot={projectRoot}
editable={projectScopeMode === 'single'}
onIssueUpdated={() => router.refresh()}
onIssueUpdated={() => refreshIssues()}
blockedTree={selectedIssue ? buildBlockedByTree(issues, selectedIssue.id) : undefined}
outgoingBlocks={selectedId ? blocksDetailsMap.get(selectedId) ?? [] : []}
onSelectBlockedIssue={handleTaskSelect}

View file

@ -8,6 +8,7 @@ import { hasOpenBlockers } from '../../lib/kanban';
import type { BeadIssue } from '../../lib/types';
import { Chip } from '../shared/chip';
import { statusBorder, statusDotColor, statusGradient } from '../shared/status-utils';
interface KanbanCardProps {
issue: BeadIssue;
@ -21,54 +22,6 @@ interface KanbanCardProps {
onSelect: (issue: BeadIssue) => void;
}
function statusGradient(status: string): string {
switch (status) {
case 'ready':
case 'open':
return 'bg-[linear-gradient(145deg,rgba(34,45,42,0.92)_0%,rgba(24,32,30,0.88)_50%,rgba(18,28,26,0.9)_100%)]';
case 'in_progress':
return 'bg-[linear-gradient(145deg,rgba(42,40,32,0.92)_0%,rgba(32,30,24,0.88)_50%,rgba(26,24,18,0.9)_100%)]';
case 'blocked':
return 'bg-[linear-gradient(145deg,rgba(60,24,30,0.95)_0%,rgba(45,18,24,0.9)_50%,rgba(32,12,16,0.92)_100%)]';
case 'closed':
return 'bg-[linear-gradient(145deg,rgba(28,30,34,0.75)_0%,rgba(22,24,28,0.72)_50%,rgba(18,20,24,0.75)_100%)] opacity-75';
default:
return 'bg-[linear-gradient(145deg,rgba(38,40,48,0.92)_0%,rgba(28,30,36,0.88)_50%,rgba(22,24,30,0.9)_100%)]';
}
}
function statusBorder(status: string): string {
switch (status) {
case 'ready':
case 'open':
return 'border-emerald-500/20';
case 'in_progress':
return 'border-amber-500/20';
case 'blocked':
return 'border-rose-500/20';
case 'closed':
return 'border-rose-500/30';
default:
return 'border-white/[0.06]';
}
}
function statusDotColor(status: string): string {
switch (status) {
case 'ready':
case 'open':
return 'bg-emerald-400';
case 'in_progress':
return 'bg-amber-400';
case 'blocked':
return 'bg-rose-400';
case 'closed':
return 'bg-slate-400';
default:
return 'bg-slate-400';
}
}
function titleColor(status: string): string {
return status === 'closed' ? 'text-text-muted/70' : 'text-text-strong/95';
}

View file

@ -2,7 +2,7 @@
import { motion } from 'framer-motion';
import Link from 'next/link';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import type { KanbanFilterOptions, KanbanStatus } from '../../lib/kanban';
import {
@ -24,6 +24,8 @@ import { KanbanDetail } from './kanban-detail';
import { ProjectScopeControls } from '../shared/project-scope-controls';
import { WorkspaceHero } from '../shared/workspace-hero';
import { useBeadsSubscription } from '../../hooks/use-beads-subscription';
interface KanbanPageProps {
issues: BeadIssue[];
projectRoot: string;
@ -34,10 +36,6 @@ interface KanbanPageProps {
type MutationOperation = 'create' | 'update' | 'close' | 'reopen' | 'comment';
interface MutationErrorResponse {
error?: { message?: string };
}
async function postMutation(operation: MutationOperation, body: Record<string, unknown>) {
const response = await fetch(`/api/beads/${operation}`, {
method: 'POST',
@ -51,25 +49,14 @@ async function postMutation(operation: MutationOperation, body: Record<string, u
}
}
async function fetchIssues(projectRoot: string): Promise<BeadIssue[]> {
const response = await fetch(`/api/beads/read?projectRoot=${encodeURIComponent(projectRoot)}`, {
cache: 'no-store',
});
const payload = (await response.json()) as { ok: boolean; issues?: BeadIssue[] } & MutationErrorResponse;
if (!response.ok || !payload.ok || !payload.issues) {
throw new Error(payload.error?.message ?? 'Failed to refresh issues');
}
return payload.issues;
}
export function KanbanPage({
issues,
issues: initialIssues,
projectRoot,
projectScopeKey,
projectScopeOptions,
projectScopeMode,
}: KanbanPageProps) {
const [localIssues, setLocalIssues] = useState<BeadIssue[]>(issues);
const { issues: localIssues, refresh: refreshIssues, updateLocal: setLocalIssues } = useBeadsSubscription(initialIssues, projectRoot);
const [filters, setFilters] = useState<KanbanFilterOptions>({
query: '',
type: '',
@ -83,11 +70,6 @@ export function KanbanPage({
const [nextActionableFeedback, setNextActionableFeedback] = useState<string | null>(null);
const [pendingIssueIds, setPendingIssueIds] = useState<Set<string>>(new Set());
const [mutationError, setMutationError] = useState<string | null>(null);
const refreshInFlightRef = useRef(false);
useEffect(() => {
setLocalIssues(issues);
}, [issues]);
const filteredIssues = useMemo(() => filterKanbanIssues(localIssues, filters), [localIssues, filters]);
const columns = useMemo(() => buildKanbanColumns(filteredIssues), [filteredIssues]);
@ -170,39 +152,6 @@ export function KanbanPage({
selectIssueWithDetailBehavior(nextActionableIssue.id, 'ready');
}, [nextActionableIssue, selectIssueWithDetailBehavior]);
const refreshIssues = useCallback(async (options: { silent?: boolean } = {}) => {
if (refreshInFlightRef.current) {
return;
}
refreshInFlightRef.current = true;
try {
const reconciled = await fetchIssues(projectRoot);
setLocalIssues(reconciled);
} catch (error) {
if (!options.silent) {
throw error;
}
} finally {
refreshInFlightRef.current = false;
}
}, [projectRoot]);
// Auto-refresh when issues change on disk (SSE)
useEffect(() => {
const source = new EventSource(`/api/events?projectRoot=${encodeURIComponent(projectRoot)}`);
const onIssues = () => {
void refreshIssues({ silent: true });
};
source.addEventListener('issues', onIssues as EventListener);
return () => {
source.removeEventListener('issues', onIssues as EventListener);
source.close();
};
}, [projectRoot, refreshIssues]);
const mutateStatus = async (issue: BeadIssue, targetStatus: KanbanStatus) => {
if (!allowMutations) {
return;