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:
parent
bfe4f853f0
commit
c7c3a25457
27 changed files with 2376 additions and 137 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue