Development
How the PABLO repository is organised, how the engine is built, and how to run the quality checks before sending a change. The engine is a Symfony application in app/; quality tools run through Castor.
Repository layout#
pablo/
├── app/ # the Symfony application (Composer package "pablo")
│ ├── bin/pablo # the pablo command (PABLO commands only)
│ ├── bin/console # framework maintenance: cache:clear, lint:twig, debug:router…
│ ├── public/index.php # the dashboard's front controller (pablo web)
│ ├── config/ # bundles, services, routes, packages/
│ ├── src/ # the engine
│ ├── templates/ # Twig: dashboard pages and components
│ ├── assets/ # dashboard JS, CSS, fonts, icons, vendored libraries
│ └── tests/ # PHPUnit, mirroring src/
├── bin/install.sh # installer run from a checkout (and uninstall.sh)
├── examples/ # example global and project configuration
├── opencode/ # agent templates, the commit command, the skill (the single source for every runtime)
├── claude/ # generated, git-ignored: the Claude Code rendering of the command
├── copilot/ # generated, git-ignored: the Copilot agent files and skill
├── systemd/, launchd/ # scheduler and dashboard service definitions
├── site/ # this website; site/installer is the curl | bash installer, served at /installer
└── castor.php # quality tasksNothing is ever written outside the repository except the links the installer creates and the data in ~/.pablo/.
Engine modules#
Namespaces mirror folders (Pablo\ is app/src/).
| Folder | Responsibility |
|---|---|
App/ | the kernel and the console application |
Command/ | one Symfony Console command per subcommand, by topic: Task/, Sync/, Report/, System/, Internal/ |
Config/ | loading project YAML and merging defaults key by key |
Domain/ | Task and Issue records, the State and Agent enums, Time, value objects such as DisplayCache, PrBadge, AgentActivity |
StateMachine/ | the data-driven state machine and its single entry handler |
Store/ | the state store and per-task locks |
Poller/ | state polling and merge auto-close |
Provider/ | external tools: Gh/ (pull requests, CI, reviews), Git/ (worktrees, sync), Confluence/, Tracker/ (GitHub, Jira, Linear) |
Agents/ | agent launchers (Orca, OpenChamber, headless) behind one interface, chosen by agent_backend |
Analytics/ | the JSONL event log, reader and aggregator |
Listing/ | the terminal tables |
Dashboard/, Controller/, Twig/Components/ | the dashboard |
Doctor/, Dispatch/, Backup/, Support/ | preflight checks, the background job, backups, helpers |
Architecture notes#
- One kernel, three entry points.
bin/pablo,bin/consoleandpublic/index.phpshare one FrameworkBundle kernel, whose container is cached inapp/var/cache/<env>.PABLO_ENVdefaults todevso the cache is revalidated after agit pull. - Never resolve a
PABLO_*variable (orHOME) when the container is compiled: the value would be frozen into the cache.StoreandAgentsresolve their paths in their constructors for this reason. pablo --helplists PABLO commands only: the console application collects thepablo.commandtag, notconsole.command.- Add a state: a case in
Domain/State.phpand a row inStateMachine::states(). Always change state throughStateMachine::enterState(), never by setting the field. - Add an issue tracker: implement
ProviderInterfaceand register it inProviderRegistry. - Terminal and dashboard can't drift: both render the same value objects (
PrBadge,AgentActivity) and use the same rules (Listing::isWaitingForFeedback(),Listing::stateRank()). - The dashboard never calls external tools while rendering: it reads the poller's
DisplayCache. Analytics come fromAnalyticsAggregator, shared withpablo stats. - Portability: the engine runs on Linux and macOS.
tests/PortabilityTest.phpfails ifsrc/mentionssystemctl,journalctl,/etc/,/proc/or/opt/.
Quality checks#
Run from the repository root, with the global Castor binary. The PHP tools are downloaded on first use.
castor qa:test # PHPUnit
castor qa:phpstan # static analysis, level 8
castor qa:cs-fixer # fix coding style (@Symfony + @Symfony:risky)
castor qa:cs:check # check only (what CI runs)
castor qa:twig-cs-fixer # Twig templates
(cd app && composer exec phpunit tests/StateMachineTest.php) # one test file
php app/bin/console lint:twig app/templatesPHPStan runs at level 8 over src, tests, config and public. Raise the level by fixing what it reports, never by ignoring errors or adding them to the baseline. php-cs-fixer runs with --path-mode intersection: a new top-level folder has to be added to both castor.php and app/.php-cs-fixer.php.
CI (.github/workflows/ci.yml) runs the tests, PHPStan and the coding-style check on every push to main and every pull request.
Tests#
- Tests mirror the source tree:
app/src/Store/Store.phpis tested byapp/tests/Store/StoreTest.php. - The engine uses dependency injection everywhere. Collaborators that touch the outside world sit behind interfaces (
GhPrInterface,GitRepoInterface,ProcessRunnerInterface,ProviderRegistryInterface,AnalyticsInterface,AgentLauncherInterface), with fakes beside the tests:FakeAgents,FakeGit,FakeGhPr,FakeProcessRunner,StubProviders,FakeAnalytics. FakeAgentsis injected wherever a real agent launcher could be reached, so a test can never start a realopencode run,claude -porcopilot -p(a real, paid model call).tests/Command/CommandTestBed.phpwires the fake graph for command tests;tests/EngineGraph.phpdoes the same for the poller, listing and dispatcher.- A test that boots the kernel must use the
RestoresErrorHandlerstrait around the boot, or PHPUnit flags it as risky.
Dashboard assets#
AssetMapper and an importmap: no Node, no build step, nothing fetched at runtime.
php app/bin/console importmap:install # after changing app/importmap.php
php app/bin/console ux:icons:import lucide:name # add an icon, then commit itapp/assets/vendor/must stay committed: the installer never runsimportmap:install, so a missing file means a dashboard without JavaScript. (The root.gitignoreonly ignores/app/vendor/for that reason.)- Icons are Lucide icons committed in
app/assets/icons/, with online downloads disabled. - The fonts (Josefin Sans, JetBrains Mono, SIL Open Font License) are committed in
app/assets/fonts/. app/assets/styles/app.cssthemes Bulma after this website. Colour classes returned byTaskViewandRebaseLogView(is-danger,is-success…) are mapped to soft tones in both themes; never use a baretag is-lightoris-grey(a test guards it).
This website#
The site lives in site/: the home page and assets as plain files, and one HTML fragment per documentation page in site/content/, listed in site/content/nav.yaml. A small PHP builder in site/build/ wraps each page in the layout, adds heading anchors and the “On this page” column, highlights code, checks every internal link, and writes the search index.
castor site:build # build into _site/
castor site:serve # build and preview on http://127.0.0.1:8000Inside a page, link to another page with href="page:states#reviews" (a typo fails the build), to a repository file with href="repo:app/src/Poller/Poller.php", and to a site asset with asset:assets/dashboard.png. Callouts are <aside class="callout note|tip|warning">. Pushing to main deploys the site with GitHub Pages.
Git discipline#
PABLO's own agents never commit or push on their own initiative; the only sanctioned paths are an explicit request from you, or /pablo-commit-and-pr. Contributions follow the same spirit: changes are delivered as a working tree or a pull request, and you decide when they land.