Configuring Horizon
coollabsio/coolify
A skill your agent uses whenever the user mentions Horizon by name in a Laravel context.
Photo upload, tagging, verification status, summary generation, XP calculation, AddTagsToPhotoAction, UploadPhotoController, and the VerificationStatus enum.
$ npx skills add OpenLitterMap/openlittermap-web --skill photo-pipeline -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install OpenLitterMap/openlittermap-web photo-pipeline --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/OpenLitterMap/openlittermap-web.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.ai/skills/photo-pipeline .claude/skills/photo-pipeline && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "photo-pipeline" agent skill from https://github.com/OpenLitterMap/openlittermap-web/tree/master/.ai/skills/photo-pipeline into .claude/skills/photo-pipeline/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "photo-pipeline", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/OpenLitterMap/openlittermap-web/tree/master/.ai/skills/photo-pipelineType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add OpenLitterMap/openlittermap-web --skill photo-pipeline -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install OpenLitterMap/openlittermap-web photo-pipeline --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/OpenLitterMap/openlittermap-web.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.ai/skills/photo-pipeline .agents/skills/photo-pipeline && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "photo-pipeline" agent skill from https://github.com/OpenLitterMap/openlittermap-web/tree/master/.ai/skills/photo-pipeline into .agents/skills/photo-pipeline/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "photo-pipeline", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add OpenLitterMap/openlittermap-web --skill photo-pipeline -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install OpenLitterMap/openlittermap-web photo-pipeline --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/OpenLitterMap/openlittermap-web.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.ai/skills/photo-pipeline .cursor/skills/photo-pipeline && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "photo-pipeline" agent skill from https://github.com/OpenLitterMap/openlittermap-web/tree/master/.ai/skills/photo-pipeline into .cursor/skills/photo-pipeline/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "photo-pipeline", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/OpenLitterMap/openlittermap-web.git --path .ai/skills/photo-pipeline--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add OpenLitterMap/openlittermap-web --skill photo-pipeline -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install OpenLitterMap/openlittermap-web photo-pipeline --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/OpenLitterMap/openlittermap-web.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.ai/skills/photo-pipeline .gemini/skills/photo-pipeline && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "photo-pipeline" agent skill from https://github.com/OpenLitterMap/openlittermap-web/tree/master/.ai/skills/photo-pipeline into .gemini/skills/photo-pipeline/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "photo-pipeline", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install OpenLitterMap/openlittermap-web photo-pipelineInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add OpenLitterMap/openlittermap-web --skill photo-pipeline -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/OpenLitterMap/openlittermap-web.git skills-src && mkdir -p .github/skills && cp -r skills-src/.ai/skills/photo-pipeline .github/skills/photo-pipeline && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "photo-pipeline" agent skill from https://github.com/OpenLitterMap/openlittermap-web/tree/master/.ai/skills/photo-pipeline into .github/skills/photo-pipeline/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "photo-pipeline", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add OpenLitterMap/openlittermap-web --skill photo-pipeline -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install OpenLitterMap/openlittermap-web photo-pipeline --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/OpenLitterMap/openlittermap-web.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.ai/skills/photo-pipeline .opencode/skills/photo-pipeline && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "photo-pipeline" agent skill from https://github.com/OpenLitterMap/openlittermap-web/tree/master/.ai/skills/photo-pipeline into .opencode/skills/photo-pipeline/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "photo-pipeline", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
photo-pipelinePhoto upload, tagging, verification status, summary generation, XP calculation, AddTagsToPhotoAction, UploadPhotoController, and the VerificationStatus enum.
Photo Pipeline is an agent skill from OpenLitterMap/openlittermap-web. Photo upload, tagging, verification status, summary generation, XP calculation, AddTagsToPhotoAction, UploadPhotoController, and the VerificationStatus enum.
Its SKILL.md is about 5.3k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It works with PHP. The repository describes itself as: https://opengeospatialdata.springeropen.com/articles/10.1186/s40965-018-0050-y. The licence is GPL-3.0.
2 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit ac688aa. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
No scripts in the folder and no shell commands in SKILL.md (its code samples are php and json).
From the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Photo Pipeline loads about 5.3k tokens when it runs. Until then it costs about 43 tokens; SKILL.md has 1,968 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from OpenLitterMap/openlittermap-web at commit ac688aa, republished under its GPL-3.0 licence (© OpenLitterMap). 1,968 words, ~5,301 tokens.
.claude/skills/photo-pipeline/SKILL.md (or your agent's skills folder).Photos flow through three phases: Upload (observation only) -> Tag (summary + XP) -> Verify (metrics). Each phase is independent and idempotent.
app/Http/Controllers/Uploads/UploadPhotoController.php — Web upload entry pointapp/Http/Requests/UploadPhotoRequest.php — Web upload validation (EXIF datetime, GPS). Dedup is NOT here — it's an idempotent lookup in the controller. HEIC files skip the image/dimensions rules AND the raw exif_read_data() GPS/datetime checks (both detected via MakeImageAction::isHeic()) — PHP can't read HEIC EXIF pre-conversion, so that validation is deferred to the controller, which reads the converted JPEG's EXIF.app/Http/Controllers/API/Tags/PhotoTagsController.php — V5 tagging endpoint (POST /api/v3/tags add, PUT /api/v3/tags replace)app/Actions/Tags/AddTagsToPhotoAction.php — Core tagging logic (v5)app/Actions/Photos/MakeImageAction.php — Image processing + EXIF extraction. Converts HEIC→JPEG by shelling out to heif-convert (from libheif; NOT ImageMagick — IM's HEIC delegate is unreliable across variants). The binary path comes from config('services.heif_convert.path') (env HEIF_CONVERT_PATH, default heif-convert resolved via PATH). Set an absolute path when the web server's PATH excludes it (e.g. local Valet php-fpm under launchd lacks /opt/homebrew/bin).app/Actions/Photos/UploadPhotoAction.php — S3 storage (requires non-null Carbon datetime)app/Services/Tags/GeneratePhotoSummaryService.php — Builds summary JSON + calculates XPapp/Services/Tags/XpCalculator.php — XP scoring rulesapp/Enums/VerificationStatus.php — Photo verification state machineapp/Enums/XpScore.php — XP values per tag typeapp/Http/Requests/Api/PhotoTagsRequest.php — V5 tag request validation (POST — blocks already-verified photos)app/Http/Requests/Api/ReplacePhotoTagsRequest.php — V5 replace tag request validation (PUT — ownership only, no verification gate)app/Http/Controllers/API/GetUntaggedUploadController.php — Mobile untagged photos (supports ?platform=web|mobile filter)app/Observers/PhotoObserver.php — Sets is_public = false for school team photosapp/Helpers/helpers.php — getDateTimeForPhoto(), getCoordinatesFromPhoto(), dmsToDec()tests/Feature/UploadValidationTest.php — 11 tests (EXIF datetime, GPS DMS conversion, edge cases)tests/Feature/Tags/ReplacePhotoTagsTest.php — 5 tests (replace tags, ownership, auth, extra tags cleanup)UploadPhotoRequest rejects images without EXIF datetime. Controller has ?? Carbon::now() safety fallback. UploadPhotoAction::run() type-hints Carbon $datetime — null will crash. Mobile uploads send explicit lat, lon, date — EXIF validation is skipped.dmsToDec() validates all 6 denominator values before dividing. Returns null on malformed data.lat=0, lon=0 get 422 (Null Island guard). Web uploads accept 0,0 from EXIF.GeneratePhotoSummaryService::run() MUST run regardless of trust level. School photos need a summary at tag time so it exists when the teacher approves later. Gating summary behind a trust check causes null summary at approval = zero metrics.TagsVerifiedByAdmin fires for ALL non-school users. This ensures all users get immediate leaderboard credit. Trusted users also get ADMIN_APPROVED (visible on map). Non-trusted users stay at verified=0 (not on map). School students' photos stop at VERIFIED(1) and wait for teacher approval — event does NOT fire for them.$photo->verified returns the enum, not an int. Use ->value for >=/< comparisons, === for equality checks. Never compare enum to raw int.remaining is deprecated and NO LONGER read for picked-up. DB column is photos.remaining (tinyint(1) NOT NULL DEFAULT 1). The Photo::getPickedUpAttribute() accessor (appended via $appends) now derives picked-up from the first tag — data_get($this->summary, 'tags.0.picked_up') — returning ?bool (null for untagged), NOT !$this->remaining. So $photo->picked_up reflects per-tag edits everywhere (map popups, profile, uploads, team views). Requires summary to be loaded (all readers select it). photos.remaining is now only written at upload (a user default) and hidden from API serialization ($hidden = ['geom', 'remaining']) — it is no longer returned in any response. Clients use picked_up. Per-tag photo_tags.picked_up is the source of truth (nullable true/false/null). users.picked_up defaults to true for new users; column stays nullable tri-state.photo_tags.category_litter_object_id, category_id, and litter_object_id are now NULLABLE. Extra-tag-only tags (brands, materials, custom tags) can exist as standalone PhotoTags without a litter object. AddTagsToPhotoAction::createExtraTagOnly() creates these with null CLO fields. GeneratePhotoSummaryService only counts objects when objectId > 0 (variable renamed $totalLitter → $totalObjects). XpCalculator only awards object XP when object_id > 0.PUT /api/v3/tags with tags: [] clears all tags on a photo (resets summary, XP, verified). ReplacePhotoTagsRequest validates tags as present|array (not required|array|min:1).is_public = false per photo or globally via users.public_photos default. Private-by-choice photos still receive immediate upload XP — the metrics gate in recordUploadMetrics() uses a school team check ($photo->team_id && $team->isSchool()), NOT an is_public check. Never change the gate to $photo->is_public === false — that would incorrectly defer metrics for private-by-choice photos.enum VerificationStatus: int
{
case UNVERIFIED = 0; // Uploaded, no tags
case VERIFIED = 1; // Tagged (school students land here, awaiting teacher)
case ADMIN_APPROVED = 2; // Verified by admin/trusted user OR teacher-approved
case BBOX_APPLIED = 3; // Bounding boxes drawn
case BBOX_VERIFIED = 4; // Bounding boxes verified
case AI_READY = 5; // Ready for OpenLitterAI training
public function isPublicReady(): bool // >= ADMIN_APPROVED
public function isVerified(): bool // >= VERIFIED
}UploadPhotoRequest::after() validates before controller runs:
dmsToDec() must succeed (guards zero denominators)Duplicate handling is NOT validation. Dedup (user_id + datetime) lives in UploadPhotoController::__invoke(), before any S3 write / Photo::create / XP. A duplicate is idempotent success, not a 422: it returns { success: true, photo_id: <existing>, already_uploaded: true, tagged: <bool>, xp_awarded: 0 } (pure lookup, zero side effects). Skipped for participant uploads (students share the facilitator's user_id and may share a datetime). tagged = existing photo has a non-null summary.
Error contract on UploadPhotoRequest: failedValidation() returns a structured response:
{ "success": false, "error": "<code>", "message": "<human string>", "errors": {} }resolveErrorCode() maps failures to typed string codes:
no_exif — image has no readable EXIF datano_gps — image has no GPS coordinatesno_datetime — image has no datetime in EXIFinvalid_coordinates — GPS coordinates failed parsing (zero denominators, etc.)validation_error — generic Laravel validation failure (wrong file type, size, etc.)Mobile clients should read the error field for programmatic handling. Note: Handler::unauthenticated() returns { message: "Unauthenticated." } without an error code field (inconsistency — not yet fixed).
UploadPhotoController::__invoke() flow:
MakeImageAction::run($file) — extract EXIFgetDateTimeForPhoto($exif) ?? Carbon::now() — EXIF datetime with safety fallbackUploadPhotoAction::run() x2 — S3 full image + bbox thumbnailgetCoordinatesFromPhoto($exif) → ResolveLocationAction::run($lat, $lon) — Country/State/City FKsPhoto::create() — observation record with FKs only. For participant uploads: team_id from participant's team, participant_id from participant slotevent(new ImageUploaded(...)) — real-time broadcastPOST is append-only + has an idempotent guard. store() first checks the target photo's summary: if non-null (already tagged), it returns an idempotent no-op { success: true, already_tagged: true, photoTags: [...] } WITHOUT re-adding — because POST appends, a retried POST would otherwise double-tag/double-count (the verified >= 1 authorize gate does NOT catch ordinary users, who stay verified = 0). To re-tag/edit an already-tagged photo, use PUT /api/v3/tags (replace).
PhotoTagsController::store() -> AddTagsToPhotoAction::run():
// AddTagsToPhotoAction::run() is wrapped in DB::transaction() — all tag creation,
// summary generation, and verification update are atomic.
public function run(int $userId, int $photoId, array $tags): array
{
$photoTags = $this->addTagsToPhoto($userId, $photoId, $tags);
// Creates PhotoTag + PhotoTagExtraTags (materials, brands, custom)
// Handles 4 tag types: object, custom-only, brand-only, material-only
$photo->generateSummary();
// ALWAYS — generates summary JSON from PhotoTag records
$photo->xp = $this->calculateXp($photoTags);
// ALWAYS — uses XpScore enum multipliers (Upload=5, Object=1, Brand=3, Material=2, Custom=1)
$this->updateVerification($userId, $photo);
// Routes to trusted path or school-pending path
}The web frontend sends 4 distinct tag types. resolveTag() handles each:
{ object: { id, key }, quantity, materials?, brands? }. Category auto-resolved from object->categories()->first().{ custom: true, key: "dirty-bench", quantity }. Uses $tag['key'] (not $tag['custom']).{ brand_only: true, brand: { id, key }, quantity }. PhotoTag with null category/object.{ material_only: true, material: { id, key }, quantity }. Same as brand-only pattern.protected function updateVerification(int $userId, Photo $photo): void
{
$user = User::find($userId);
$isSchoolStudent = false;
if ($user->verification_required) {
$photo->verification = 0.1;
if ($photo->team_id) {
$team = Team::find($photo->team_id);
if ($team && $team->isSchool()) {
$photo->verified = VerificationStatus::VERIFIED->value;
$isSchoolStudent = true;
}
}
} else {
// Trusted user — immediate approval + map visibility
$photo->verification = 1;
$photo->verified = VerificationStatus::ADMIN_APPROVED->value;
}
$photo->save();
// ALL users get leaderboard credit immediately (except school students).
// Non-trusted photos stay at verified=0 (not on map) but metrics are processed.
if (! $isSchoolStudent) {
event(new TagsVerifiedByAdmin(...));
}
}Key distinction: TagsVerifiedByAdmin fires for ALL non-school users. Trusted users also get verified = ADMIN_APPROVED (photo visible on map). Non-trusted users stay at verified = 0 (photo NOT on map, but user IS on leaderboard).
// XpScore enum values:
Upload => 5 // Base for every photo
Object => 1 // Per litter item (default)
Material => 2 // Per material tag
Brand => 3 // Per brand tag
CustomTag => 1 // Per custom tag
PickedUp => 5 // Bonus if picked_up = true
Small => 10 // Special objects: 'dumping_small'
Medium => 25 // Special objects: 'dumping_medium'
Large => 50 // Special objects: 'dumping_large'
BagsLitter => 10 // Special objects: 'bags_litter'PhotoTagsController::update() handles PUT /api/v3/tags for replacing all tags on an already-tagged photo. The entire operation is wrapped in DB::transaction():
summary=null, xp=0, verified=0AddTagsToPhotoAction::run() — regenerates summary, XP, fires TagsVerifiedByAdminMetricsService::processPhoto() detects prior processing (has processed_at), calls doUpdate() which calculates deltas between old processed_tags and new summary, applies adjustments to all metricsonboarding_completed_at on first tag submission (parity with store()), guarded to non-empty tags. PUT-first-time == POST-first-time: on a never-tagged photo the reset is a no-op and the same AddTagsToPhotoAction::run(..., skipVerification=false) runs → identical verified/XP/metrics for trusted/school/ordinary users. The mobile auto-upload flow tags exclusively via PUT (idempotent).Frontend edit mode: /tag?photo=<id> loads a specific photo. If it has existing tags, isEditMode=true → uses PUT. If untagged, uses POST. convertExistingTags() transforms API new_tags format back to frontend format (including litter_object_type_id for the type dimension).
Frontend guards: Double-submit prevention via isSubmitting ref. After success, REFRESH_USER() updates the nav XP bar (non-blocking). Stats and photos refresh in parallel via Promise.all().
Security: ReplacePhotoTagsRequest checks $photo->user_id === $this->user()->id. GET_SINGLE_PHOTO calls /api/v3/user/photos which filters by authenticated user.
GeneratePhotoSummaryService::run() still populates result_string from the summary keys for backward compatibility. Format: category.object qty,category.object qty,... (e.g., smoking.butts 3,food.wrapper 2,). However, no public-facing endpoint reads result_string anymore — all map endpoints (GlobalMapController, DisplayTagsOnMapController, TeamsClusterController, PointsController, FilterPhotosByGeoHashTrait) were updated to select and return summary instead. Both result_string and total_litter columns are now write-only and scheduled for eventual removal.
total_litter → total_tags: All active endpoints now read total_tags instead of total_litter. Fixed: CommunityController, ContributorAggregator, TimeSeriesAggregator, ProfileController (global litter fallback), JoinTeamAction (team pivot). Safe (location-level Redis, not photo column): GlobalStatsController, WorldCupController. Safe (correct fallback): CreateCSVExport. Console commands that read these columns (CompileResultsString, ResetResultString) have been deleted. Dead jobs deleted: Api/AddTags, Photos/AddTagsToPhoto (both wrote total_litter + verification float).
{
"tags": {
"2": {
"65": {
"quantity": 5,
"materials": {"16": 3, "15": 2},
"brands": {"12": 3}
}
}
},
"totals": {
"total_tags": 15, "total_objects": 5,
"by_category": {"2": 10},
"materials": 8, "brands": 3, "custom_tags": 0
},
"keys": {
"categories": {"2": "smoking"},
"objects": {"65": "wrapper"},
"materials": {"16": "plastic"},
"brands": {"12": "marlboro"}
}
}protected $hidden = ['geom']; // Binary spatial data — breaks JSON serializationAlways ensure geom stays in $hidden. If you need coordinates, use lat/lon columns.
$photo->verified >= 2 fails. Use $photo->verified->value >= VerificationStatus::ADMIN_APPROVED->value.TagsVerifiedByAdmin for school students. School photos must wait for teacher approval. Only trusted users get immediate dispatch.geom in API responses. Binary spatial data. Keep it in $hidden.$photo->toArray() for queue responses. The Location model's updatedAtDiffForHumans accessor crashes on null updated_at. Build response arrays manually when including country relation. See AdminQueueController for pattern.UploadPhotoAction::run(). The method type-hints Carbon $datetime. If EXIF has no datetime, getDateTimeForPhoto() returns null. Validation must reject first; controller has ?? Carbon::now() safety fallback.dmsToDec() against zero denominators. EXIF GPS values are "numerator/denominator" format. If denominator is 0 in any of the 6 components (degrees/minutes/seconds for lat and lon), division crashes. The function now returns null instead.0,0 — only reject null.city_id in factory. PhotoFactory doesn't include city_id by default. Add 'city_id' => City::factory() when testing location-dependent features.category_litter_object_id with category_id. Phase 1 adds category_litter_object_id (FK to category_litter_object pivot) and litter_object_type_id (FK to litter_object_types) to photo_tags. Both are nullable in Phase 1. The existing category_id and litter_object_id columns remain and are still the authoritative source until Phase 3.'tags' instead of 'new_tags' in upload controller. UsersUploadsController must return tags under the key 'new_tags' — the Uploads.vue frontend reads photo.new_tags for tag counts and objects list.where('verified', 0) or doesntHave('photoTags') for untagged filter. Use whereNull('summary') — summary is set by GeneratePhotoSummaryService when tags are added, regardless of verification status. After "leaderboard immediate credit," untrusted users' verified stays at 0 after tagging, so where('verified', 0) includes tagged photos.litter_object_type_id in photo response. UsersUploadsController::getNewTags() must include litter_object_type_id so the frontend can preserve the type dimension on edit round-trips.DB::transaction(). If AddTagsToPhotoAction::run() fails after old tags are deleted, the photo loses all tag data. The entire delete-reset-add sequence must be atomic. AddTagsToPhotoAction::run() itself is also wrapped in a transaction — both operations are independently protected.getNewTags() conditionally includes category/object. For extra-tag-only PhotoTags (brand/material/custom-only), category and object fields are null. The serializer only includes them when both category_id and litter_object_id are non-null. Frontend must handle null category/object gracefully.UploadPhotoRequest errors to match Laravel's default shape. UploadPhotoRequest overrides failedValidation() to return a custom { success, error, message, errors } shape with typed error codes. Do not assert the standard Laravel { errors: { field: [...] } } shape for upload failures.image/dimensions back unconditionally, running raw exif_read_data() on HEIC, or swapping the converter to ImageMagick. Laravel's image rule excludes HEIC and dimensions (getimagesize()) returns false for HEIC — UploadPhotoRequest::rules() drops both for HEIC (detected via MakeImageAction::isHeic()), keeping mimes+max. after() also skips the raw exif_read_data() GPS/datetime checks for HEIC (PHP can't read HEIC EXIF pre-conversion → false no_exif); the controller validates GPS from the converted JPEG instead and returns no_gps if genuinely missing. And MakeImageAction converts via heif-convert (libheif; path from config('services.heif_convert.path')), NOT ImageMagick convert/magick — IM's HEIC delegate is unreliable across variants. Reintroducing any of these breaks HEIC upload (silent client/server rejection, or conversion failure).© OpenLitterMap, GPL-3.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in .ai/skills/photo-pipeline of OpenLitterMap/openlittermap-web.
Open the folder on GitHubat commit ac688aa
Photo Pipeline next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Photo Pipeline this skillOpenLitterMap/openlittermap-web | 134 | — | ~5.3k | Automated safety check: Pass | GPL-3.0 | |
| Configuring Horizoncoollabsio/coolify | 63k | 4 repos | ~898 | Automated safety check: Pass | MIT | |
| Tailwindcss Developmentanonaddy/anonaddy | 4.9k | 10 repos | ~865 | Automated safety check: Pass | MIT | |
| Fortify Developmentcoollabsio/coolify | 63k | 4 repos | ~1.9k | Automated safety check: Pass | MIT | |
| MCP Developmentcoollabsio/coolify | 63k | 1 repos | ~949 | Automated safety check: Pass | MIT | |
| WooCommerce Code Reviewwoocommerce/woocommerce | 11k | 3 repos | ~1.1k | Automated safety check: Pass | Custom licence |
coollabsio/coolify
A skill your agent uses whenever the user mentions Horizon by name in a Laravel context.
anonaddy/anonaddy
Always invoke when the user's message includes 'tailwind' in any form.
coollabsio/coolify
ACTIVATE when the user works on authentication in Laravel. An agent skill from coollabsio/coolify.
coollabsio/coolify
A skill your agent uses for Laravel MCP development. An agent skill from coollabsio/coolify.
woocommerce/woocommerce
Reviews WooCommerce code changes against the project's standards, flagging backend PHP architecture, naming, documentation, data integrity and testing violations.
symfony/symfony
Synchronize translation catalogs across maintained Symfony branches: find messages that newer branches added to the English catalogs but that are still missing from the oldest maintained branch…
OpenLitterMap/openlittermap-web
Styles applications using Tailwind CSS v3 utilities. An agent skill from OpenLitterMap/openlittermap-web.
OpenLitterMap/openlittermap-web
OpenLitterMap v5 architecture reference. An agent skill from OpenLitterMap/openlittermap-web.
OpenLitterMap/openlittermap-web
AchievementEngine, AchievementRepository, milestone checkers, AchievementsSeeder, userachievements pivot, AchievementsController API, and achievement evaluation flow.
OpenLitterMap/openlittermap-web
AdminController, photo approval, tag editing, deletion, MetricsService integration, admin middleware, verification queue, and admin XP.
OpenLitterMap/openlittermap-web
REST API endpoints, route structure, auth guards, request/response contracts, error patterns, and the full API surface for web SPA and mobile clients.
OpenLitterMap/openlittermap-web
ClusteringService, tile keys, dirty tiles/teams, clustering commands, ClusterController GeoJSON API, PhotoObserver dirty marking, and map cluster rendering.
Works with
Photo upload, tagging, verification status, summary generation, XP calculation, AddTagsToPhotoAction, UploadPhotoController, and the VerificationStatus enum. Photo Pipeline is an agent skill from OpenLitterMap/openlittermap-web. Photo upload, tagging, verification status, summary generation, XP calculation, AddTagsToPhotoAction, UploadPhotoController, and the VerificationStatus enum.
Run `npx skills add OpenLitterMap/openlittermap-web --skill photo-pipeline -a claude-code`. Or copy the skill folder (.ai/skills/photo-pipeline in OpenLitterMap/openlittermap-web) into .claude/skills/photo-pipeline in your project. Claude Code loads it when a task matches its description.
Run `npx skills add OpenLitterMap/openlittermap-web --skill photo-pipeline -a codex`. Or copy the skill folder (.ai/skills/photo-pipeline in OpenLitterMap/openlittermap-web) into .agents/skills/photo-pipeline in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add OpenLitterMap/openlittermap-web --skill photo-pipeline -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/photo-pipeline, .gemini/skills/photo-pipeline, .github/skills/photo-pipeline and .opencode/skills/photo-pipeline in your project.
SKILL.md names no scripts, command-line tools or credentials: Photo Pipeline is instructions for the agent only.
SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Photo Pipeline is published under the GPL-3.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 5.3k tokens (SKILL.md is roughly 21k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Photo Pipeline: Configuring Horizon (coollabsio/coolify, 63k stars), Tailwindcss Development (anonaddy/anonaddy, 4.9k stars), Fortify Development (coollabsio/coolify, 63k stars) and MCP Development (coollabsio/coolify, 63k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
OpenLitterMap (a GitHub organization) maintains it in OpenLitterMap/openlittermap-web, which has 134 GitHub stars. The repository holds 16 skills in this directory. The repository was last updated on September 14, 2026.
Source: OpenLitterMap/openlittermap-web on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.