resources/boost/skills/flowforge-development/SKILL.md
Builds Kanban board interfaces for Eloquent models with drag-and-drop functionality. Use when creating board pages, configuring columns and cards, implementing drag-and-drop positioning, working with Filament board pages or standalone Livewire boards, or troubleshooting position-related issues.
npx skillsauth add relaticle/flowforge flowforge-developmentInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use when:
use Illuminate\Database\Schema\Blueprint;
Schema::table('tasks', function (Blueprint $table) {
$table->flowforgePositionColumn(); // DECIMAL(20,10) nullable
$table->unique(['status', 'position']);
});
php artisan flowforge:make-board TaskBoard
use Relaticle\Flowforge\BoardPage;
use Relaticle\Flowforge\Board;
use Relaticle\Flowforge\Column;
class TaskBoard extends BoardPage
{
protected static ?string $navigationIcon = 'heroicon-o-view-columns';
public function board(Board $board): Board
{
return $board
->query(Task::query())
->columnIdentifier('status')
->positionIdentifier('position')
->recordTitleAttribute('title')
->columns([
Column::make('todo', 'To Do')
->icon('heroicon-o-clipboard'),
Column::make('in_progress', 'In Progress')
->icon('heroicon-o-play'),
Column::make('done', 'Done')
->icon('heroicon-o-check'),
]);
}
}
use Relaticle\Flowforge\BoardPage;
class TaskBoard extends BoardPage
{
protected static ?string $navigationIcon = 'heroicon-o-view-columns';
protected static ?string $navigationGroup = 'Tasks';
public function board(Board $board): Board
{
return $board
->query(Task::query()->where('team_id', auth()->user()->team_id))
->columnIdentifier('status')
->positionIdentifier('position')
->columns([...]);
}
}
use Relaticle\Flowforge\BoardResourcePage;
class TaskBoardPage extends BoardResourcePage
{
protected static string $resource = TaskResource::class;
public function board(Board $board): Board
{
return $board
->query($this->getResource()::getEloquentQuery())
->columnIdentifier('status')
->positionIdentifier('position')
->columns([...]);
}
}
Register in resource:
public static function getPages(): array
{
return [
'index' => Pages\ListTasks::route('/'),
'board' => Pages\TaskBoardPage::route('/board'),
];
}
use Livewire\Component;
use Relaticle\Flowforge\Board;
use Relaticle\Flowforge\Contracts\HasBoard;
use Relaticle\Flowforge\Concerns\InteractsWithBoard;
class TaskBoard extends Component implements HasBoard
{
use InteractsWithBoard;
public function board(Board $board): Board
{
return $board
->query(Task::query())
->columnIdentifier('status')
->positionIdentifier('position')
->columns([...]);
}
public function render()
{
return view('livewire.task-board');
}
}
Blade view:
<div>
{{ $this->board }}
</div>
use Relaticle\Flowforge\Column;
->columns([
Column::make('backlog', 'Backlog')
->icon('heroicon-o-inbox')
->color('gray'),
Column::make('todo', 'To Do')
->icon('heroicon-o-clipboard')
->color('info'),
Column::make('in_progress', 'In Progress')
->icon('heroicon-o-play')
->color('warning'),
Column::make('review', 'Review')
->icon('heroicon-o-eye')
->color('primary'),
Column::make('done', 'Done')
->icon('heroicon-o-check-circle')
->color('success'),
])
Use Filament's Schema builder for rich card layouts:
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Components\ImageEntry;
use Filament\Schemas\Components\Grid;
->cardSchema([
Grid::make(2)
->schema([
TextEntry::make('title')
->weight('bold'),
TextEntry::make('priority')
->badge()
->color(fn ($state) => match ($state) {
'high' => 'danger',
'medium' => 'warning',
default => 'gray',
}),
]),
TextEntry::make('assignee.name')
->icon('heroicon-o-user'),
TextEntry::make('due_date')
->date()
->icon('heroicon-o-calendar'),
])
->cardsPerColumn(20) // Cards loaded initially
->cardsIncrement(10) // Cards loaded on "Load More"
->searchable(['title', 'description'])
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\TernaryFilter;
->filters([
SelectFilter::make('priority')
->options([
'low' => 'Low',
'medium' => 'Medium',
'high' => 'High',
]),
SelectFilter::make('assignee_id')
->relationship('assignee', 'name')
->searchable()
->preload(),
TernaryFilter::make('is_overdue')
->label('Overdue'),
])
Record Actions (per card):
use Filament\Actions\Action;
use Filament\Actions\EditAction;
use Filament\Actions\DeleteAction;
->recordActions([
EditAction::make()
->url(fn ($record) => route('tasks.edit', $record)),
Action::make('archive')
->icon('heroicon-o-archive-box')
->action(fn ($record) => $record->archive()),
DeleteAction::make(),
])
Column Actions (per column header):
->columnActions([
Action::make('add')
->icon('heroicon-o-plus')
->action(function (array $arguments) {
// $arguments['column'] contains column identifier
Task::create([
'status' => $arguments['column'],
'position' => DecimalPosition::forEmptyColumn(),
]);
}),
])
Flowforge uses DECIMAL(20,10) positions with BCMath precision for reliable ordering.
use Relaticle\Flowforge\Services\DecimalPosition;
// Position between two cards (includes cryptographic jitter)
$position = DecimalPosition::between($afterPosition, $beforePosition);
// Exact midpoint (deterministic, for testing)
$position = DecimalPosition::betweenExact($afterPosition, $beforePosition);
// Position before first card
$position = DecimalPosition::before($firstPosition);
// Position after last card
$position = DecimalPosition::after($lastPosition);
// Initial position for empty column
$position = DecimalPosition::forEmptyColumn();
// Smart positioning (handles nulls)
$position = DecimalPosition::calculate($afterPos, $beforePos);
// Check if rebalancing needed
if (DecimalPosition::needsRebalancing($posA, $posB)) {
// Gap is < 0.0001
}
// Generate evenly-spaced sequence
$positions = DecimalPosition::generateSequence(count: 100);
// In your Livewire component
public function moveCard(
int|string $recordId,
string $toColumn,
?string $afterRecordId = null,
?string $beforeRecordId = null
): void {
// Parent handles position calculation and saving
parent::moveCard($recordId, $toColumn, $afterRecordId, $beforeRecordId);
// Add custom logic after move
$this->dispatch('card-moved');
}
php artisan flowforge:make-board TaskBoard
php artisan flowforge:make-board TaskBoard --resource # For resource page
php artisan flowforge:diagnose-positions "App\Models\Task" status position
Checks for:
php artisan flowforge:rebalance-positions "App\Models\Task" status position
php artisan flowforge:rebalance-positions "App\Models\Task" status position --column=in_progress
php artisan flowforge:repair-positions "App\Models\Task" status position
Offers multiple repair strategies:
Publish config:
php artisan vendor:publish --tag=flowforge-config
config/flowforge.php:
return [
'columns' => [
'default_limit' => 50,
],
'kanban' => [
'initial_cards_count' => 20,
'cards_increment' => 10,
],
'ui' => [
'show_item_counts' => true,
],
];
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('tasks', function (Blueprint $table) {
$table->flowforgePositionColumn();
$table->unique(['status', 'position']);
});
}
};
Important: The unique constraint on [column_identifier, position] is required for concurrent safety.
public function board(Board $board): Board
{
return $board
->query(Task::query()->where('team_id', auth()->user()->team_id))
// ...
}
public function board(Board $board): Board
{
$statuses = Status::ordered()->get();
return $board
->query(Task::query())
->columnIdentifier('status_id')
->positionIdentifier('position')
->columns(
$statuses->map(fn ($status) =>
Column::make($status->id, $status->name)
->icon($status->icon)
->color($status->color)
)->toArray()
);
}
public function board(Board $board): Board
{
return $board
->query(Task::query()->with(['assignee', 'tags', 'project']))
// ...
}
->recordActions([
Action::make('view')
->url(fn ($record) => TaskResource::getUrl('view', ['record' => $record]))
->openUrlInNewTab(),
])
ext-bcmathDECIMAL(20,10) with unique constraintdevelopment
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.
development
End-to-end Parallels smoke, upgrade, and rerun workflow for OpenClaw across macOS, Windows, and Linux guests. Use when Codex needs to run, rerun, debug, or interpret VM-based install, onboarding, gateway smoke tests, latest-release-to-main upgrade checks, fresh snapshot retests, or optional Discord roundtrip verification under Parallels.