From 8d80c73faa838d59c22187437ffe183b0155a892 Mon Sep 17 00:00:00 2001 From: jbotwina Date: Tue, 23 Dec 2025 11:02:17 -0500 Subject: [PATCH] feat: Add task dependencies and spawn sub-task functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add edge dragging to create dependencies in graph view - Add spawn sub-task action available in graph view and kanban board - Implement ancestor context selection when spawning tasks - Add dependency validation (circular, self, duplicate prevention) - Include ancestor context in spawned task descriptions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 --- apps/ui/package.json | 1 + apps/ui/src/components/ui/collapsible.tsx | 10 + apps/ui/src/components/views/board-view.tsx | 23 +- .../components/kanban-card/card-header.tsx | 57 ++- .../components/kanban-card/kanban-card.tsx | 3 + .../board-view/dialogs/add-feature-dialog.tsx | 95 +++- .../board-view/hooks/use-board-actions.ts | 2 + .../views/board-view/kanban-board.tsx | 3 + .../shared/ancestor-context-section.tsx | 201 +++++++++ .../views/board-view/shared/index.ts | 1 + .../views/graph-view/components/task-node.tsx | 11 + .../views/graph-view/graph-canvas.tsx | 17 + .../views/graph-view/graph-view.tsx | 58 ++- .../views/graph-view/hooks/use-graph-nodes.ts | 5 + .../graph-view/utils/ancestor-context.ts | 93 ++++ .../graph-view/utils/dependency-validation.ts | 51 +++ .../views/graph-view/utils/index.ts | 2 + package-lock.json | 33 +- pnpm-lock.yaml | 407 ++++++++++++++++++ 19 files changed, 1057 insertions(+), 16 deletions(-) create mode 100644 apps/ui/src/components/ui/collapsible.tsx create mode 100644 apps/ui/src/components/views/board-view/shared/ancestor-context-section.tsx create mode 100644 apps/ui/src/components/views/graph-view/utils/ancestor-context.ts create mode 100644 apps/ui/src/components/views/graph-view/utils/dependency-validation.ts create mode 100644 apps/ui/src/components/views/graph-view/utils/index.ts create mode 100644 pnpm-lock.yaml diff --git a/apps/ui/package.json b/apps/ui/package.json index 410e63c1..ce6edcbf 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -44,6 +44,7 @@ "@dnd-kit/utilities": "^3.2.2", "@lezer/highlight": "^1.2.3", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", diff --git a/apps/ui/src/components/ui/collapsible.tsx b/apps/ui/src/components/ui/collapsible.tsx new file mode 100644 index 00000000..dfe74fc4 --- /dev/null +++ b/apps/ui/src/components/ui/collapsible.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; + +const Collapsible = CollapsiblePrimitive.Root; + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 0459e68b..66fa50d0 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -107,6 +107,9 @@ export function BoardView() { // State for viewing plan in read-only mode const [viewPlanFeature, setViewPlanFeature] = useState(null); + // State for spawn task mode + const [spawnParentFeature, setSpawnParentFeature] = useState(null); + // Worktree dialog states const [showCreateWorktreeDialog, setShowCreateWorktreeDialog] = useState(false); const [showDeleteWorktreeDialog, setShowDeleteWorktreeDialog] = useState(false); @@ -1021,6 +1024,10 @@ export function BoardView() { onImplement={handleStartImplementation} onViewPlan={(feature) => setViewPlanFeature(feature)} onApprovePlan={handleOpenApprovalDialog} + onSpawnTask={(feature) => { + setSpawnParentFeature(feature); + setShowAddDialog(true); + }} featuresWithContext={featuresWithContext} runningAutoTasks={runningAutoTasks} shortcuts={shortcuts} @@ -1043,6 +1050,13 @@ export function BoardView() { onStartTask={handleStartImplementation} onStopTask={handleForceStopFeature} onResumeTask={handleResumeFeature} + onUpdateFeature={(featureId, updates) => { + handleUpdateFeature(featureId, updates); + }} + onSpawnTask={(feature) => { + setSpawnParentFeature(feature); + setShowAddDialog(true); + }} /> )} @@ -1077,7 +1091,12 @@ export function BoardView() { {/* Add Feature Dialog */} { + setShowAddDialog(open); + if (!open) { + setSpawnParentFeature(null); + } + }} onAdd={handleAddFeature} categorySuggestions={categorySuggestions} branchSuggestions={branchSuggestions} @@ -1088,6 +1107,8 @@ export function BoardView() { isMaximized={isMaximized} showProfilesOnly={showProfilesOnly} aiProfiles={aiProfiles} + parentFeature={spawnParentFeature} + allFeatures={hookFeatures} /> {/* Edit Feature Dialog */} diff --git a/apps/ui/src/components/views/board-view/components/kanban-card/card-header.tsx b/apps/ui/src/components/views/board-view/components/kanban-card/card-header.tsx index 52b34417..6f486caa 100644 --- a/apps/ui/src/components/views/board-view/components/kanban-card/card-header.tsx +++ b/apps/ui/src/components/views/board-view/components/kanban-card/card-header.tsx @@ -19,6 +19,7 @@ import { ChevronDown, ChevronUp, Cpu, + GitFork, } from 'lucide-react'; import { CountUpTimer } from '@/components/ui/count-up-timer'; import { formatModelName, DEFAULT_MODEL } from '@/lib/agent-context-parser'; @@ -31,6 +32,7 @@ interface CardHeaderProps { onEdit: () => void; onDelete: () => void; onViewOutput?: () => void; + onSpawnTask?: () => void; } export function CardHeaderSection({ @@ -40,6 +42,7 @@ export function CardHeaderSection({ onEdit, onDelete, onViewOutput, + onSpawnTask, }: CardHeaderProps) { const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); @@ -92,6 +95,17 @@ export function CardHeaderSection({ Edit + { + e.stopPropagation(); + onSpawnTask?.(); + }} + data-testid={`spawn-running-${feature.id}`} + className="text-xs" + > + + Spawn Sub-Task + {/* Model info in dropdown */}
@@ -106,7 +120,21 @@ export function CardHeaderSection({ {/* Backlog header */} {!isCurrentAutoTask && feature.status === 'backlog' && ( -
+
+ + {onViewOutput && ( + +
+
+ +

+ Select ancestors to include their context in the new task's prompt. +

+ +
+ {allAncestorItems.map((item) => { + const isSelected = selectedAncestorIds.has(item.id); + const isExpanded = expandedIds.has(item.id); + const hasContent = + item.description || + ('spec' in item && item.spec) || + ('summary' in item && item.summary); + const displayTitle = + item.title || + item.description.slice(0, 50) + (item.description.length > 50 ? '...' : ''); + + return ( + +
+ toggleSelected(item.id)} + className="mt-0.5" + /> +
+
+ {hasContent && ( + + + + )} + +
+ + +
+ {item.description && ( +
+ Description: +

{item.description}

+
+ )} + {'spec' in item && item.spec && ( +
+ Specification: +

{item.spec}

+
+ )} + {'summary' in item && item.summary && ( +
+ Summary: +

{item.summary}

+
+ )} +
+
+
+
+
+ ); + })} + + {ancestors.length === 0 && ( +

+ Parent task has no additional ancestors +

+ )} +
+
+ ); +} diff --git a/apps/ui/src/components/views/board-view/shared/index.ts b/apps/ui/src/components/views/board-view/shared/index.ts index 730f5d1a..304d4c89 100644 --- a/apps/ui/src/components/views/board-view/shared/index.ts +++ b/apps/ui/src/components/views/board-view/shared/index.ts @@ -6,3 +6,4 @@ export * from './testing-tab-content'; export * from './priority-selector'; export * from './branch-selector'; export * from './planning-mode-selector'; +export * from './ancestor-context-section'; diff --git a/apps/ui/src/components/views/graph-view/components/task-node.tsx b/apps/ui/src/components/views/graph-view/components/task-node.tsx index 8ea4f319..1c190df7 100644 --- a/apps/ui/src/components/views/graph-view/components/task-node.tsx +++ b/apps/ui/src/components/views/graph-view/components/task-node.tsx @@ -14,6 +14,7 @@ import { GitBranch, Terminal, RotateCcw, + GitFork, } from 'lucide-react'; import { TaskNodeData } from '../hooks/use-graph-nodes'; import { Button } from '@/components/ui/button'; @@ -266,6 +267,16 @@ export const TaskNode = memo(function TaskNode({ data, selected }: TaskNodeProps Resume Task )} + { + e.stopPropagation(); + data.onSpawnTask?.(); + }} + > + + Spawn Sub-Task +
diff --git a/apps/ui/src/components/views/graph-view/graph-canvas.tsx b/apps/ui/src/components/views/graph-view/graph-canvas.tsx index 283de400..88173511 100644 --- a/apps/ui/src/components/views/graph-view/graph-canvas.tsx +++ b/apps/ui/src/components/views/graph-view/graph-canvas.tsx @@ -11,6 +11,7 @@ import { SelectionMode, ConnectionMode, Node, + Connection, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; @@ -53,6 +54,7 @@ interface GraphCanvasProps { onSearchQueryChange: (query: string) => void; onNodeDoubleClick?: (featureId: string) => void; nodeActionCallbacks?: NodeActionCallbacks; + onCreateDependency?: (sourceId: string, targetId: string) => Promise; backgroundStyle?: React.CSSProperties; className?: string; } @@ -64,6 +66,7 @@ function GraphCanvasInner({ onSearchQueryChange, onNodeDoubleClick, nodeActionCallbacks, + onCreateDependency, backgroundStyle, className, }: GraphCanvasProps) { @@ -138,6 +141,19 @@ function GraphCanvasInner({ [onNodeDoubleClick] ); + // Handle edge connection (creating dependencies) + const handleConnect = useCallback( + async (connection: Connection) => { + if (!connection.source || !connection.target) return; + + // In React Flow, dragging from source handle to target handle means: + // - source = the node being dragged FROM (the prerequisite/dependency) + // - target = the node being dragged TO (the dependent task) + await onCreateDependency?.(connection.source, connection.target); + }, + [onCreateDependency] + ); + // MiniMap node color based on status const minimapNodeColor = useCallback((node: Node) => { const data = node.data as TaskNodeData | undefined; @@ -165,6 +181,7 @@ function GraphCanvasInner({ onNodesChange={isLocked ? undefined : onNodesChange} onEdgesChange={onEdgesChange} onNodeDoubleClick={handleNodeDoubleClick} + onConnect={handleConnect} nodeTypes={nodeTypes} edgeTypes={edgeTypes} fitView diff --git a/apps/ui/src/components/views/graph-view/graph-view.tsx b/apps/ui/src/components/views/graph-view/graph-view.tsx index 9ef01bce..33874dc9 100644 --- a/apps/ui/src/components/views/graph-view/graph-view.tsx +++ b/apps/ui/src/components/views/graph-view/graph-view.tsx @@ -3,6 +3,8 @@ import { Feature, useAppStore } from '@/store/app-store'; import { GraphCanvas } from './graph-canvas'; import { useBoardBackground } from '../board-view/hooks'; import { NodeActionCallbacks } from './hooks'; +import { wouldCreateCircularDependency, dependencyExists } from './utils'; +import { toast } from 'sonner'; interface GraphViewProps { features: Feature[]; @@ -17,6 +19,8 @@ interface GraphViewProps { onStartTask?: (feature: Feature) => void; onStopTask?: (feature: Feature) => void; onResumeTask?: (feature: Feature) => void; + onUpdateFeature?: (featureId: string, updates: Partial) => void; + onSpawnTask?: (feature: Feature) => void; } export function GraphView({ @@ -32,6 +36,8 @@ export function GraphView({ onStartTask, onStopTask, onResumeTask, + onUpdateFeature, + onSpawnTask, }: GraphViewProps) { const { currentProject } = useAppStore(); @@ -74,6 +80,49 @@ export function GraphView({ [features, onEditFeature] ); + // Handle creating a dependency via edge connection + const handleCreateDependency = useCallback( + async (sourceId: string, targetId: string): Promise => { + // Prevent self-dependency + if (sourceId === targetId) { + toast.error('A task cannot depend on itself'); + return false; + } + + // Check if dependency already exists + if (dependencyExists(features, sourceId, targetId)) { + toast.info('Dependency already exists'); + return false; + } + + // Check for circular dependency + if (wouldCreateCircularDependency(features, sourceId, targetId)) { + toast.error('Cannot create circular dependency', { + description: 'This would create a dependency cycle', + }); + return false; + } + + // Get target feature and update its dependencies + const targetFeature = features.find((f) => f.id === targetId); + if (!targetFeature) { + toast.error('Target task not found'); + return false; + } + + const currentDeps = targetFeature.dependencies || []; + + // Add the dependency + onUpdateFeature?.(targetId, { + dependencies: [...currentDeps, sourceId], + }); + + toast.success('Dependency created'); + return true; + }, + [features, onUpdateFeature] + ); + // Node action callbacks for dropdown menu const nodeActionCallbacks: NodeActionCallbacks = useMemo( () => ({ @@ -107,8 +156,14 @@ export function GraphView({ onResumeTask?.(feature); } }, + onSpawnTask: (featureId: string) => { + const feature = features.find((f) => f.id === featureId); + if (feature) { + onSpawnTask?.(feature); + } + }, }), - [features, onViewOutput, onEditFeature, onStartTask, onStopTask, onResumeTask] + [features, onViewOutput, onEditFeature, onStartTask, onStopTask, onResumeTask, onSpawnTask] ); return ( @@ -120,6 +175,7 @@ export function GraphView({ onSearchQueryChange={onSearchQueryChange} onNodeDoubleClick={handleNodeDoubleClick} nodeActionCallbacks={nodeActionCallbacks} + onCreateDependency={handleCreateDependency} backgroundStyle={backgroundImageStyle} className="h-full" /> diff --git a/apps/ui/src/components/views/graph-view/hooks/use-graph-nodes.ts b/apps/ui/src/components/views/graph-view/hooks/use-graph-nodes.ts index 2b83a4a4..e216462d 100644 --- a/apps/ui/src/components/views/graph-view/hooks/use-graph-nodes.ts +++ b/apps/ui/src/components/views/graph-view/hooks/use-graph-nodes.ts @@ -24,6 +24,7 @@ export interface TaskNodeData extends Feature { onStartTask?: () => void; onStopTask?: () => void; onResumeTask?: () => void; + onSpawnTask?: () => void; } export type TaskNode = Node; @@ -40,6 +41,7 @@ export interface NodeActionCallbacks { onStartTask?: (featureId: string) => void; onStopTask?: (featureId: string) => void; onResumeTask?: (featureId: string) => void; + onSpawnTask?: (featureId: string) => void; } interface UseGraphNodesProps { @@ -112,6 +114,9 @@ export function useGraphNodes({ onResumeTask: actionCallbacks?.onResumeTask ? () => actionCallbacks.onResumeTask!(feature.id) : undefined, + onSpawnTask: actionCallbacks?.onSpawnTask + ? () => actionCallbacks.onSpawnTask!(feature.id) + : undefined, }, }; diff --git a/apps/ui/src/components/views/graph-view/utils/ancestor-context.ts b/apps/ui/src/components/views/graph-view/utils/ancestor-context.ts new file mode 100644 index 00000000..d0f2abaf --- /dev/null +++ b/apps/ui/src/components/views/graph-view/utils/ancestor-context.ts @@ -0,0 +1,93 @@ +import { Feature } from '@/store/app-store'; + +export interface AncestorContext { + id: string; + title?: string; + description: string; + spec?: string; + summary?: string; + depth: number; // 0 = immediate parent, 1 = grandparent, etc. +} + +/** + * Traverses the dependency graph to find all ancestors of a feature. + * Returns ancestors ordered by depth (closest first). + * + * @param feature - The feature to find ancestors for + * @param allFeatures - All features in the system + * @param maxDepth - Maximum depth to traverse (prevents infinite loops) + * @returns Array of ancestor contexts, sorted by depth (closest first) + */ +export function getAncestors( + feature: Feature, + allFeatures: Feature[], + maxDepth: number = 10 +): AncestorContext[] { + const featureMap = new Map(allFeatures.map((f) => [f.id, f])); + const ancestors: AncestorContext[] = []; + const visited = new Set(); + + function traverse(featureId: string, depth: number) { + if (depth > maxDepth || visited.has(featureId)) return; + visited.add(featureId); + + const f = featureMap.get(featureId); + if (!f?.dependencies) return; + + for (const depId of f.dependencies) { + const dep = featureMap.get(depId); + if (dep && !visited.has(depId)) { + ancestors.push({ + id: dep.id, + title: dep.title, + description: dep.description, + spec: dep.spec, + summary: dep.summary, + depth, + }); + traverse(depId, depth + 1); + } + } + } + + traverse(feature.id, 0); + + // Sort by depth (closest ancestors first) + return ancestors.sort((a, b) => a.depth - b.depth); +} + +/** + * Formats ancestor context for inclusion in a task description. + * + * @param ancestors - Array of ancestor contexts (including parent) + * @param selectedIds - Set of selected ancestor IDs to include + * @returns Formatted markdown string with ancestor context + */ +export function formatAncestorContextForPrompt( + ancestors: AncestorContext[], + selectedIds: Set +): string { + const selectedAncestors = ancestors.filter((a) => selectedIds.has(a.id)); + if (selectedAncestors.length === 0) return ''; + + const sections = selectedAncestors.map((ancestor) => { + const parts: string[] = []; + const title = ancestor.title || `Task (${ancestor.id.slice(0, 8)})`; + + parts.push(`### ${title}`); + + if (ancestor.description) { + parts.push(`**Description:** ${ancestor.description}`); + } + if (ancestor.spec) { + parts.push(`**Specification:**\n${ancestor.spec}`); + } + if (ancestor.summary) { + parts.push(`**Summary:** ${ancestor.summary}`); + } + + return parts.join('\n\n'); + }); + + return `## Ancestor Context\n\n${sections.join('\n\n---\n\n')}`; +} diff --git a/apps/ui/src/components/views/graph-view/utils/dependency-validation.ts b/apps/ui/src/components/views/graph-view/utils/dependency-validation.ts new file mode 100644 index 00000000..e3da76f8 --- /dev/null +++ b/apps/ui/src/components/views/graph-view/utils/dependency-validation.ts @@ -0,0 +1,51 @@ +import { Feature } from '@/store/app-store'; + +/** + * Checks if adding a dependency from sourceId to targetId would create a circular dependency. + * Uses DFS to detect if targetId can reach sourceId through existing dependencies. + * + * @param features - All features in the system + * @param sourceId - The feature that would become a dependency (the prerequisite) + * @param targetId - The feature that would depend on sourceId + * @returns true if adding this dependency would create a cycle + */ +export function wouldCreateCircularDependency( + features: Feature[], + sourceId: string, + targetId: string +): boolean { + const featureMap = new Map(features.map((f) => [f.id, f])); + const visited = new Set(); + + function canReach(currentId: string, targetId: string): boolean { + if (currentId === targetId) return true; + if (visited.has(currentId)) return false; + + visited.add(currentId); + const feature = featureMap.get(currentId); + if (!feature?.dependencies) return false; + + for (const depId of feature.dependencies) { + if (canReach(depId, targetId)) return true; + } + return false; + } + + // Check if source can reach target through existing dependencies + // If so, adding target -> source would create a cycle + return canReach(sourceId, targetId); +} + +/** + * Checks if a dependency already exists between two features. + * + * @param features - All features in the system + * @param sourceId - The potential dependency (prerequisite) + * @param targetId - The feature that might depend on sourceId + * @returns true if targetId already depends on sourceId + */ +export function dependencyExists(features: Feature[], sourceId: string, targetId: string): boolean { + const targetFeature = features.find((f) => f.id === targetId); + if (!targetFeature?.dependencies) return false; + return targetFeature.dependencies.includes(sourceId); +} diff --git a/apps/ui/src/components/views/graph-view/utils/index.ts b/apps/ui/src/components/views/graph-view/utils/index.ts new file mode 100644 index 00000000..c3c1a6fd --- /dev/null +++ b/apps/ui/src/components/views/graph-view/utils/index.ts @@ -0,0 +1,2 @@ +export * from './dependency-validation'; +export * from './ancestor-context'; diff --git a/package-lock.json b/package-lock.json index 2072c523..0f569f29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,6 +81,7 @@ "@dnd-kit/utilities": "^3.2.2", "@lezer/highlight": "^1.2.3", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", @@ -1216,7 +1217,7 @@ }, "node_modules/@electron/node-gyp": { "version": "10.2.0-electron.1", - "resolved": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", "integrity": "sha512-4MSBTT8y07YUDqf69/vSh80Hh791epYqGtWHO3zSKhYFwQg+gx9wi1PqbqP6YqC4WMsNxZ5l9oDmnWdK5pfCKQ==", "dev": true, "license": "MIT", @@ -3747,6 +3748,36 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..c8245ff1 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,407 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + cross-spawn: + specifier: ^7.0.6 + version: 7.0.6 + rehype-sanitize: + specifier: ^6.0.0 + version: 6.0.0 + tree-kill: + specifier: ^1.2.2 + version: 1.2.2 + devDependencies: + husky: + specifier: ^9.1.7 + version: 9.1.7 + lint-staged: + specifier: ^16.2.7 + version: 16.2.7 + prettier: + specifier: ^3.7.4 + version: 3.7.4 + +packages: + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + ansi-escapes@7.2.0: + resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==} + engines: {node: '>=18'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-truncate@5.1.1: + resolution: {integrity: sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==} + engines: {node: '>=20'} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + commander@14.0.2: + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} + engines: {node: '>=20'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + get-east-asian-width@1.4.0: + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} + engines: {node: '>=18'} + + hast-util-sanitize@5.0.2: + resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==} + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + lint-staged@16.2.7: + resolution: {integrity: sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==} + engines: {node: '>=20.17'} + hasBin: true + + listr2@9.0.5: + resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} + engines: {node: '>=20.0.0'} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + nano-spawn@2.0.0: + resolution: {integrity: sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==} + engines: {node: '>=20.17'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + engines: {node: '>=14'} + hasBin: true + + rehype-sanitize@6.0.0: + resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.1.0: + resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==} + engines: {node: '>=20'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + +snapshots: + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.3.0': {} + + ansi-escapes@7.2.0: + dependencies: + environment: 1.1.0 + + ansi-regex@6.2.2: {} + + ansi-styles@6.2.3: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@5.1.1: + dependencies: + slice-ansi: 7.1.2 + string-width: 8.1.0 + + colorette@2.0.20: {} + + commander@14.0.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + emoji-regex@10.6.0: {} + + environment@1.1.0: {} + + eventemitter3@5.0.1: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + get-east-asian-width@1.4.0: {} + + hast-util-sanitize@5.0.2: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.3.0 + unist-util-position: 5.0.0 + + husky@9.1.7: {} + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.4.0 + + is-number@7.0.0: {} + + isexe@2.0.0: {} + + lint-staged@16.2.7: + dependencies: + commander: 14.0.2 + listr2: 9.0.5 + micromatch: 4.0.8 + nano-spawn: 2.0.0 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.8.2 + + listr2@9.0.5: + dependencies: + cli-truncate: 5.1.1 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.2 + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.2.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.1.2 + wrap-ansi: 9.0.2 + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-function@5.0.1: {} + + nano-spawn@2.0.0: {} + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + path-key@3.1.1: {} + + picomatch@2.3.1: {} + + pidtree@0.6.0: {} + + prettier@3.7.4: {} + + rehype-sanitize@6.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-sanitize: 5.0.2 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + rfdc@1.4.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + string-argv@0.3.2: {} + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 + + string-width@8.1.0: + dependencies: + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tree-kill@1.2.2: {} + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.1.2 + + yaml@2.8.2: {}