Vibecode Shotomatic
track this build4 phases, 12 steps, beginner friendly0%A personal timed screenshot loop is a realistic weekend build. Matching reliable capture across Mac apps, permission recovery, website batches, guide editing, and polished exports is a much larger product.
You are building a lean indie version of Shotomatic. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Shotomatic · indie build A screenshot automation tool whose entire value is determinism: the same page captured twice must produce the same bytes, or the output cannot be diffed, reviewed or trusted, and almost everything about a modern page conspires against that. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A headless browser with a pinned version and fixed viewport | a browser update changes rendering and therefore every screenshot | | Determinism | Frozen clock, disabled animation, blocked external resources | these are the four things that make a page render differently each time | | Framing | Device frames and annotations applied after capture | the capture should be the page, with decoration as a separate step | | Localisation | The same script run per locale with the locale set properly | translated screenshots are the main reason to automate this at all | | Runtime | Python 3.12 with a job queue | batches are long and individual captures fail | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Orchestrates capture, comparison and decoration. - Get it: python.org or your package manager, then a virtual environment. - Verify: python3 --version prints 3.12 or higher - [ ] **A headless browser with a pinned version** · free - Why: A browser update changes font rendering and layout subtly, which invalidates every baseline at once and looks like your site changed. - Get it: A browser automation library with a pinned browser build, recorded in your configuration. - Verify: The browser version is pinned and reported in every run - [ ] **Five pages that are hard to capture** · free - Why: Determinism only fails on real pages, and the failures are specific: animation, a live clock, lazy images, web fonts and third-party content. - Get it: Collect pages exhibiting each, from a site you own or have permission to capture. - Verify: All five exhibit their problem on a naive capture - [ ] **An application in four languages** · free - Why: Localised capture is the main reason to automate screenshots, and half-translated output is the failure that looks correct. - Get it: A real application with genuine translations, including one language substantially longer than English. - Verify: Four locales render with translated dates and numbers, not only text - [ ] **Your approval process for baselines** · free - Why: A baseline updated automatically makes the entire comparison meaningless, and deciding the process after the first noisy run means deciding it while annoyed. - Get it: Decide who approves a baseline change and what tolerance is acceptable, and write it down. - Verify: A written process exists and tolerance is configured ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Full visual testing with component snapshots. - Interaction recording as video. - Cross-browser rendering comparison. - Semantic change detection. - polished display, window, and custom-area selection - reliable macOS permission setup and recovery - Action Capture guide editing and annotations - batch website capture with per-URL settings - integrated PDF, ZIP, and MP4 export workflows If one of those is essential to you, that is the reason to keep paying for Shotomatic, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Shotomatic The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me automated screenshots that come out identical every time you run them to replace Shotomatic. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Capture: A headless browser with a pinned version and fixed viewport · a browser update changes rendering and therefore every screenshot - Determinism: Frozen clock, disabled animation, blocked external resources · these are the four things that make a page render differently each time - Framing: Device frames and annotations applied after capture · the capture should be the page, with decoration as a separate step - Localisation: The same script run per locale with the locale set properly · translated screenshots are the main reason to automate this at all - Runtime: Python 3.12 with a job queue · batches are long and individual captures fail ### Data model (create this before Phase 1) - scripts(id, name, steps_json, viewport_json, wait_strategy, created_at) - targets(id, script_id, url, locale, theme, device_profile) - captures(id, target_id, run_id, image_path, content_hash, captured_at, attempt, stable) - runs(id, started_at, finished_at, browser_version, succeeded, failed, non_deterministic_count) - baselines(target_id, content_hash, approved_at, approved_by) · what the output is supposed to look like - diffs(capture_id, baseline_hash, changed_pixels, changed_regions_json, verdict) - A capture records whether it was stable, meaning two consecutive captures produced identical bytes, because an unstable capture cannot be compared to anything. ### Phase 1 · The same page twice, identically Build: Freeze the clock, disable animations and transitions, block external resources, and pin the browser version. Capture each target twice and compare, marking a target unstable if the two differ. Trap: Everything on a modern page fights determinism: a timestamp, a rotating advertisement, a fade-in that has not finished, a font arriving late, a randomised placeholder. Capture twice and compare on every run, so an unstable target is detected rather than producing a diff you spend an hour investigating. Done when: A page with animations, a clock and web fonts captures byte-identically twice. An unstable target is detected and reported rather than producing a spurious diff. The browser version is pinned and recorded per run. Blocking external resources does not break pages that depend on them, or the failure is reported clearly. Do not build yet: No framing, localisation or diffing. ### Phase 2 · Knowing when the page is ready Build: Wait on real readiness signals: network idle, fonts loaded, a required element present. Support a per-script wait strategy. Fail a capture that never becomes ready rather than capturing a partial page. Trap: Waiting a fixed number of seconds is either too short, producing captures of half-loaded pages, or too long, making a batch take hours, and it is both on different pages in the same run. Wait on signals, and treat a page that never settles as a failure rather than capturing whatever was there at the timeout. Done when: Captures wait on font loading and network idle rather than a fixed delay. A required element can be specified per script and is waited for. A page that never becomes ready fails with a reason rather than producing a partial capture. Batch duration is proportional to real page readiness rather than to a fixed timeout. Do not build yet: No framing, localisation or diffs yet. ### Phase 3 · The same shot in every language Build: Run each script per locale, setting the locale properly rather than only a query parameter. Detect text overflow and clipping caused by longer translations. Keep locale outputs separately comparable. Trap: Setting a language through a query parameter while the browser still reports a different locale produces pages that are half translated, with dates and numbers in the original language, and it looks correct enough to ship. Set the browser locale, the accept header and any application preference together. Done when: A script run in four locales produces genuinely localised output including dates and numbers. Text overflow or clipping caused by a longer translation is detected and reported. Each locale's captures are compared against that locale's own baseline. A locale with a missing translation is reported rather than silently falling back. Do not build yet: No framing or diff review yet. ### Phase 4 · Diffs a person can review quickly Build: Compare against an approved baseline, reporting changed regions rather than a percentage. Support a tolerance for genuinely acceptable differences. Require explicit approval to update a baseline. Trap: Reporting a diff as a percentage of changed pixels tells you nothing about whether the change matters, since a one percent change might be an entire button disappearing or an antialiasing difference along one edge. Report bounding regions and show them, so a reviewer looks at the change rather than at a number. Done when: Diffs report changed regions with their locations rather than only a percentage. A tolerance can be set and its effect is visible in the report. Updating a baseline requires explicit approval and records who approved it. A diff caused by an unstable target is identified as instability rather than as a change. Do not build yet: No framing or export yet. ### Phase 5 · Frames, annotations and batches Build: Apply device frames, captions and highlights after capture, from a declarative description. Run large batches with per-target failure isolation. Keep the undecorated capture so decoration can be redone without recapturing. Trap: Baking a caption into the capture means changing the wording requires recapturing every screenshot, which for a localised app store set is hundreds of captures and a browser version that may have changed since. Keep the raw capture and apply decoration as a separate reproducible step. Done when: Decoration is applied after capture and can be changed without recapturing, verified by rewording a caption. A batch of three hundred targets runs unattended with per-target failure isolation. The same script and decoration description produce identical output on a rerun. Failed targets are reported individually with their reasons. Do not build yet: Nothing after this. ### Out of scope (and why) - Visual testing as a full discipline, including component-level snapshots and cross-browser matrices, which is a larger project. - Recording interactions as video, which has different determinism problems entirely. - Cross-browser and cross-platform rendering comparison, which needs infrastructure per platform. - Automatic detection of what changed semantically, which is where visual diffing tools stop being reliable. ### README must contain - That determinism requires freezing the clock, disabling animation, blocking external resources and pinning the browser - That every target is captured twice and compared, so an unstable target is detected rather than producing a false diff - That waiting is on real readiness signals rather than a fixed delay - That locale is set in the browser, the header and the application together, or the output is half translated - That decoration is applied after capture so wording changes do not require recapturing ===== AGENTS.md ===== # Agent instructions · Shotomatic indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A headless browser with a pinned version and fixed viewport, Frozen clock, disabled animation, blocked external resources, Device frames and annotations applied after capture, The same script run per locale with the locale set properly, Python 3.12 with a job queue. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Everything on a modern page fights determinism: a timestamp, a rotating advertisement, a fade-in that has not finished, a font arriving late, a randomised placeholder. Capture twice and compare on every run, so an unstable target is detected rather than producing a diff you spend an hour investigating. - Waiting a fixed number of seconds is either too short, producing captures of half-loaded pages, or too long, making a batch take hours, and it is both on different pages in the same run. Wait on signals, and treat a page that never settles as a failure rather than capturing whatever was there at the timeout. - Setting a language through a query parameter while the browser still reports a different locale produces pages that are half translated, with dates and numbers in the original language, and it looks correct enough to ship. Set the browser locale, the accept header and any application preference together. - Reporting a diff as a percentage of changed pixels tells you nothing about whether the change matters, since a one percent change might be an entire button disappearing or an antialiasing difference along one edge. Report bounding regions and show them, so a reviewer looks at the change rather than at a number. ===== BUILD_PLAN.md ===== # Build plan · Shotomatic A screenshot automation tool whose entire value is determinism: the same page captured twice must produce the same bytes, or the output cannot be diffed, reviewed or trusted, and almost everything about a modern page conspires against that. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · The same page twice, identically Capturing a page twice produces byte-identical images. ### Steps 1. Freeze time and disable animation Two of the four things that make a page render differently each run. Files: `src/determinism.py` 2. Block external resources Advertisements and third-party widgets are never the same twice. Files: `src/determinism.py` 3. Capture twice and compare The stability check is what makes every later diff meaningful. Files: `src/capture.py`, `tests/test_stability.py` ### Done when - [ ] A page with animation and fonts captures identically twice - [ ] Unstable targets are reported rather than diffed - [ ] The browser version is pinned and recorded - [ ] Blocked resources fail clearly rather than silently ### Watch out - Everything on a modern page fights determinism: a timestamp, a rotating advertisement, a fade-in that has not finished, a font arriving late, a randomised placeholder. Capture twice and compare on every run, so an unstable target is detected rather than producing a diff you spend an hour investigating. ## Phase 2 · Knowing when the page is ready Captures happen after the page has finished, not after a guessed delay. ### Steps 1. Wait on real signals Fonts and network, not a number of seconds. Files: `src/wait.py` 2. Allow a required element per script The page knows when it is ready better than any generic rule. Files: `src/scripts.py` 3. Fail rather than capture partial A screenshot of a loading spinner is worse than a failure. Files: `src/capture.py` ### Done when - [ ] Captures wait on fonts and network idle - [ ] A required element can be specified and is waited for - [ ] A never-ready page fails with a reason - [ ] Batch duration reflects real readiness ### Watch out - Waiting a fixed number of seconds is either too short, producing captures of half-loaded pages, or too long, making a batch take hours, and it is both on different pages in the same run. Wait on signals, and treat a page that never settles as a failure rather than capturing whatever was there at the timeout. ## Phase 3 · The same shot in every language A script runs across locales and the text is genuinely translated. ### Steps 1. Set locale everywhere it is read Browser locale, header and application preference together. Files: `src/locale.py` 2. Detect overflow and clipping German is longer and the button is not. Files: `src/overflow.py` 3. Baseline per locale Comparing a German screenshot to an English baseline finds nothing useful. Files: `src/baselines.py` ### Done when - [ ] Four locales produce genuinely localised output - [ ] Overflow and clipping are detected - [ ] Each locale compares against its own baseline - [ ] Missing translations are reported ### Watch out - Setting a language through a query parameter while the browser still reports a different locale produces pages that are half translated, with dates and numbers in the original language, and it looks correct enough to ship. Set the browser locale, the accept header and any application preference together. ## Phase 4 · Diffs a person can review quickly A change is shown as a region rather than a pixel count. ### Steps 1. Report regions, not percentages A percentage cannot distinguish a missing button from antialiasing. Files: `src/diff.py` 2. Require approval to update a baseline Automatic baseline updates make the whole comparison pointless. Files: `src/baselines.py` 3. Attribute instability correctly An unstable target's diff is not a change and should not be reviewed as one. Files: `src/diff.py` ### Done when - [ ] Diffs report changed regions with locations - [ ] Tolerance is configurable and visible in reports - [ ] Baseline updates require recorded approval - [ ] Instability is distinguished from change ### Watch out - Reporting a diff as a percentage of changed pixels tells you nothing about whether the change matters, since a one percent change might be an entire button disappearing or an antialiasing difference along one edge. Report bounding regions and show them, so a reviewer looks at the change rather than at a number. ## Not in this build - Full visual testing with component snapshots. - Interaction recording as video. - Cross-browser rendering comparison. - Semantic change detection. ## After v1, if you want it - Capturing on a schedule to detect unannounced third-party changes, which is a real use - Store-ready output sizes per platform, which is the tedious part of a release - A review interface for approving a batch of baseline changes at once ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Recorded per run. A browser update invalidates every baseline at once. BROWSER_VERSION=pinned build identifier # Required. Raw captures and decorated outputs, kept separately. OUTPUT_DIR=./captures # Required. A fixed time, so any visible clock renders identically. FREEZE_CLOCK_AT=2026-01-01T12:00:00Z # Required. Present as a constant. Third-party content is never the same twice. BLOCK_EXTERNAL_RESOURCES=always # Required. Each target captured this many times and compared before being used. STABILITY_CAPTURES=2 # Required. Zero by default. Tolerance hides the changes you are looking for. DIFF_TOLERANCE_PIXELS=0
You are building a lean indie version of Shotomatic. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Shotomatic · indie build A screenshot automation tool whose entire value is determinism: the same page captured twice must produce the same bytes, or the output cannot be diffed, reviewed or trusted, and almost everything about a modern page conspires against that. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A headless browser with a pinned version and fixed viewport | a browser update changes rendering and therefore every screenshot | | Determinism | Frozen clock, disabled animation, blocked external resources | these are the four things that make a page render differently each time | | Framing | Device frames and annotations applied after capture | the capture should be the page, with decoration as a separate step | | Localisation | The same script run per locale with the locale set properly | translated screenshots are the main reason to automate this at all | | Runtime | Python 3.12 with a job queue | batches are long and individual captures fail | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Orchestrates capture, comparison and decoration. - Get it: python.org or your package manager, then a virtual environment. - Verify: python3 --version prints 3.12 or higher - [ ] **A headless browser with a pinned version** · free - Why: A browser update changes font rendering and layout subtly, which invalidates every baseline at once and looks like your site changed. - Get it: A browser automation library with a pinned browser build, recorded in your configuration. - Verify: The browser version is pinned and reported in every run - [ ] **Five pages that are hard to capture** · free - Why: Determinism only fails on real pages, and the failures are specific: animation, a live clock, lazy images, web fonts and third-party content. - Get it: Collect pages exhibiting each, from a site you own or have permission to capture. - Verify: All five exhibit their problem on a naive capture - [ ] **An application in four languages** · free - Why: Localised capture is the main reason to automate screenshots, and half-translated output is the failure that looks correct. - Get it: A real application with genuine translations, including one language substantially longer than English. - Verify: Four locales render with translated dates and numbers, not only text - [ ] **Your approval process for baselines** · free - Why: A baseline updated automatically makes the entire comparison meaningless, and deciding the process after the first noisy run means deciding it while annoyed. - Get it: Decide who approves a baseline change and what tolerance is acceptable, and write it down. - Verify: A written process exists and tolerance is configured ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Full visual testing with component snapshots. - Interaction recording as video. - Cross-browser rendering comparison. - Semantic change detection. - polished display, window, and custom-area selection - reliable macOS permission setup and recovery - Action Capture guide editing and annotations - batch website capture with per-URL settings - integrated PDF, ZIP, and MP4 export workflows If one of those is essential to you, that is the reason to keep paying for Shotomatic, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Shotomatic The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me automated screenshots that come out identical every time you run them to replace Shotomatic. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Capture: A headless browser with a pinned version and fixed viewport · a browser update changes rendering and therefore every screenshot - Determinism: Frozen clock, disabled animation, blocked external resources · these are the four things that make a page render differently each time - Framing: Device frames and annotations applied after capture · the capture should be the page, with decoration as a separate step - Localisation: The same script run per locale with the locale set properly · translated screenshots are the main reason to automate this at all - Runtime: Python 3.12 with a job queue · batches are long and individual captures fail ### Data model (create this before Phase 1) - scripts(id, name, steps_json, viewport_json, wait_strategy, created_at) - targets(id, script_id, url, locale, theme, device_profile) - captures(id, target_id, run_id, image_path, content_hash, captured_at, attempt, stable) - runs(id, started_at, finished_at, browser_version, succeeded, failed, non_deterministic_count) - baselines(target_id, content_hash, approved_at, approved_by) · what the output is supposed to look like - diffs(capture_id, baseline_hash, changed_pixels, changed_regions_json, verdict) - A capture records whether it was stable, meaning two consecutive captures produced identical bytes, because an unstable capture cannot be compared to anything. ### Phase 1 · The same page twice, identically Build: Freeze the clock, disable animations and transitions, block external resources, and pin the browser version. Capture each target twice and compare, marking a target unstable if the two differ. Trap: Everything on a modern page fights determinism: a timestamp, a rotating advertisement, a fade-in that has not finished, a font arriving late, a randomised placeholder. Capture twice and compare on every run, so an unstable target is detected rather than producing a diff you spend an hour investigating. Done when: A page with animations, a clock and web fonts captures byte-identically twice. An unstable target is detected and reported rather than producing a spurious diff. The browser version is pinned and recorded per run. Blocking external resources does not break pages that depend on them, or the failure is reported clearly. Do not build yet: No framing, localisation or diffing. ### Phase 2 · Knowing when the page is ready Build: Wait on real readiness signals: network idle, fonts loaded, a required element present. Support a per-script wait strategy. Fail a capture that never becomes ready rather than capturing a partial page. Trap: Waiting a fixed number of seconds is either too short, producing captures of half-loaded pages, or too long, making a batch take hours, and it is both on different pages in the same run. Wait on signals, and treat a page that never settles as a failure rather than capturing whatever was there at the timeout. Done when: Captures wait on font loading and network idle rather than a fixed delay. A required element can be specified per script and is waited for. A page that never becomes ready fails with a reason rather than producing a partial capture. Batch duration is proportional to real page readiness rather than to a fixed timeout. Do not build yet: No framing, localisation or diffs yet. ### Phase 3 · The same shot in every language Build: Run each script per locale, setting the locale properly rather than only a query parameter. Detect text overflow and clipping caused by longer translations. Keep locale outputs separately comparable. Trap: Setting a language through a query parameter while the browser still reports a different locale produces pages that are half translated, with dates and numbers in the original language, and it looks correct enough to ship. Set the browser locale, the accept header and any application preference together. Done when: A script run in four locales produces genuinely localised output including dates and numbers. Text overflow or clipping caused by a longer translation is detected and reported. Each locale's captures are compared against that locale's own baseline. A locale with a missing translation is reported rather than silently falling back. Do not build yet: No framing or diff review yet. ### Phase 4 · Diffs a person can review quickly Build: Compare against an approved baseline, reporting changed regions rather than a percentage. Support a tolerance for genuinely acceptable differences. Require explicit approval to update a baseline. Trap: Reporting a diff as a percentage of changed pixels tells you nothing about whether the change matters, since a one percent change might be an entire button disappearing or an antialiasing difference along one edge. Report bounding regions and show them, so a reviewer looks at the change rather than at a number. Done when: Diffs report changed regions with their locations rather than only a percentage. A tolerance can be set and its effect is visible in the report. Updating a baseline requires explicit approval and records who approved it. A diff caused by an unstable target is identified as instability rather than as a change. Do not build yet: No framing or export yet. ### Phase 5 · Frames, annotations and batches Build: Apply device frames, captions and highlights after capture, from a declarative description. Run large batches with per-target failure isolation. Keep the undecorated capture so decoration can be redone without recapturing. Trap: Baking a caption into the capture means changing the wording requires recapturing every screenshot, which for a localised app store set is hundreds of captures and a browser version that may have changed since. Keep the raw capture and apply decoration as a separate reproducible step. Done when: Decoration is applied after capture and can be changed without recapturing, verified by rewording a caption. A batch of three hundred targets runs unattended with per-target failure isolation. The same script and decoration description produce identical output on a rerun. Failed targets are reported individually with their reasons. Do not build yet: Nothing after this. ### Out of scope (and why) - Visual testing as a full discipline, including component-level snapshots and cross-browser matrices, which is a larger project. - Recording interactions as video, which has different determinism problems entirely. - Cross-browser and cross-platform rendering comparison, which needs infrastructure per platform. - Automatic detection of what changed semantically, which is where visual diffing tools stop being reliable. ### README must contain - That determinism requires freezing the clock, disabling animation, blocking external resources and pinning the browser - That every target is captured twice and compared, so an unstable target is detected rather than producing a false diff - That waiting is on real readiness signals rather than a fixed delay - That locale is set in the browser, the header and the application together, or the output is half translated - That decoration is applied after capture so wording changes do not require recapturing ===== AGENTS.md ===== # Agent instructions · Shotomatic indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A headless browser with a pinned version and fixed viewport, Frozen clock, disabled animation, blocked external resources, Device frames and annotations applied after capture, The same script run per locale with the locale set properly, Python 3.12 with a job queue. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Everything on a modern page fights determinism: a timestamp, a rotating advertisement, a fade-in that has not finished, a font arriving late, a randomised placeholder. Capture twice and compare on every run, so an unstable target is detected rather than producing a diff you spend an hour investigating. - Waiting a fixed number of seconds is either too short, producing captures of half-loaded pages, or too long, making a batch take hours, and it is both on different pages in the same run. Wait on signals, and treat a page that never settles as a failure rather than capturing whatever was there at the timeout. - Setting a language through a query parameter while the browser still reports a different locale produces pages that are half translated, with dates and numbers in the original language, and it looks correct enough to ship. Set the browser locale, the accept header and any application preference together. - Reporting a diff as a percentage of changed pixels tells you nothing about whether the change matters, since a one percent change might be an entire button disappearing or an antialiasing difference along one edge. Report bounding regions and show them, so a reviewer looks at the change rather than at a number. ===== BUILD_PLAN.md ===== # Build plan · Shotomatic A screenshot automation tool whose entire value is determinism: the same page captured twice must produce the same bytes, or the output cannot be diffed, reviewed or trusted, and almost everything about a modern page conspires against that. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · The same page twice, identically Capturing a page twice produces byte-identical images. ### Steps 1. Freeze time and disable animation Two of the four things that make a page render differently each run. Files: `src/determinism.py` 2. Block external resources Advertisements and third-party widgets are never the same twice. Files: `src/determinism.py` 3. Capture twice and compare The stability check is what makes every later diff meaningful. Files: `src/capture.py`, `tests/test_stability.py` ### Done when - [ ] A page with animation and fonts captures identically twice - [ ] Unstable targets are reported rather than diffed - [ ] The browser version is pinned and recorded - [ ] Blocked resources fail clearly rather than silently ### Watch out - Everything on a modern page fights determinism: a timestamp, a rotating advertisement, a fade-in that has not finished, a font arriving late, a randomised placeholder. Capture twice and compare on every run, so an unstable target is detected rather than producing a diff you spend an hour investigating. ## Phase 2 · Knowing when the page is ready Captures happen after the page has finished, not after a guessed delay. ### Steps 1. Wait on real signals Fonts and network, not a number of seconds. Files: `src/wait.py` 2. Allow a required element per script The page knows when it is ready better than any generic rule. Files: `src/scripts.py` 3. Fail rather than capture partial A screenshot of a loading spinner is worse than a failure. Files: `src/capture.py` ### Done when - [ ] Captures wait on fonts and network idle - [ ] A required element can be specified and is waited for - [ ] A never-ready page fails with a reason - [ ] Batch duration reflects real readiness ### Watch out - Waiting a fixed number of seconds is either too short, producing captures of half-loaded pages, or too long, making a batch take hours, and it is both on different pages in the same run. Wait on signals, and treat a page that never settles as a failure rather than capturing whatever was there at the timeout. ## Phase 3 · The same shot in every language A script runs across locales and the text is genuinely translated. ### Steps 1. Set locale everywhere it is read Browser locale, header and application preference together. Files: `src/locale.py` 2. Detect overflow and clipping German is longer and the button is not. Files: `src/overflow.py` 3. Baseline per locale Comparing a German screenshot to an English baseline finds nothing useful. Files: `src/baselines.py` ### Done when - [ ] Four locales produce genuinely localised output - [ ] Overflow and clipping are detected - [ ] Each locale compares against its own baseline - [ ] Missing translations are reported ### Watch out - Setting a language through a query parameter while the browser still reports a different locale produces pages that are half translated, with dates and numbers in the original language, and it looks correct enough to ship. Set the browser locale, the accept header and any application preference together. ## Phase 4 · Diffs a person can review quickly A change is shown as a region rather than a pixel count. ### Steps 1. Report regions, not percentages A percentage cannot distinguish a missing button from antialiasing. Files: `src/diff.py` 2. Require approval to update a baseline Automatic baseline updates make the whole comparison pointless. Files: `src/baselines.py` 3. Attribute instability correctly An unstable target's diff is not a change and should not be reviewed as one. Files: `src/diff.py` ### Done when - [ ] Diffs report changed regions with locations - [ ] Tolerance is configurable and visible in reports - [ ] Baseline updates require recorded approval - [ ] Instability is distinguished from change ### Watch out - Reporting a diff as a percentage of changed pixels tells you nothing about whether the change matters, since a one percent change might be an entire button disappearing or an antialiasing difference along one edge. Report bounding regions and show them, so a reviewer looks at the change rather than at a number. ## Not in this build - Full visual testing with component snapshots. - Interaction recording as video. - Cross-browser rendering comparison. - Semantic change detection. ## After v1, if you want it - Capturing on a schedule to detect unannounced third-party changes, which is a real use - Store-ready output sizes per platform, which is the tedious part of a release - A review interface for approving a batch of baseline changes at once ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Recorded per run. A browser update invalidates every baseline at once. BROWSER_VERSION=pinned build identifier # Required. Raw captures and decorated outputs, kept separately. OUTPUT_DIR=./captures # Required. A fixed time, so any visible clock renders identically. FREEZE_CLOCK_AT=2026-01-01T12:00:00Z # Required. Present as a constant. Third-party content is never the same twice. BLOCK_EXTERNAL_RESOURCES=always # Required. Each target captured this many times and compared before being used. STABILITY_CAPTURES=2 # Required. Zero by default. Tolerance hides the changes you are looking for. DIFF_TOLERANCE_PIXELS=0
You are building a production product version of Shotomatic. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Shotomatic · product brief ## Problem A personal timed screenshot loop is a realistic weekend build. Matching reliable capture across Mac apps, permission recovery, website batches, guide editing, and polished exports is a much larger product. ## Product outcome Screenshot automation whose output is identical run to run, so a diff means the page changed rather than that the tool is noisy. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS 14 or later - Xcode command line tools - Screen Recording permission - Accessibility permission for optional keypress automation ## Explicit non-goals for v1 - Full visual testing with component snapshots. - Interaction recording as video. - Cross-browser rendering comparison. - Semantic change detection. - polished display, window, and custom-area selection - reliable macOS permission setup and recovery - Action Capture guide editing and annotations - batch website capture with per-URL settings - integrated PDF, ZIP, and MP4 export workflows ## Success criteria - A page with animation and fonts is confirmed to capture identically twice - Unstable targets are confirmed reported rather than diffed - Captures are confirmed to wait on real readiness signals - Four locales are confirmed genuinely localised - Decoration is confirmed changeable without recapturing ===== BRIEF.md ===== # Build brief · Shotomatic The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me automated screenshots that come out identical every time you run them to replace Shotomatic. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Capture: A headless browser with a pinned version and fixed viewport · a browser update changes rendering and therefore every screenshot - Determinism: Frozen clock, disabled animation, blocked external resources · these are the four things that make a page render differently each time - Framing: Device frames and annotations applied after capture · the capture should be the page, with decoration as a separate step - Localisation: The same script run per locale with the locale set properly · translated screenshots are the main reason to automate this at all - Runtime: Python 3.12 with a job queue · batches are long and individual captures fail ### Data model (create this before Phase 1) - scripts(id, name, steps_json, viewport_json, wait_strategy, created_at) - targets(id, script_id, url, locale, theme, device_profile) - captures(id, target_id, run_id, image_path, content_hash, captured_at, attempt, stable) - runs(id, started_at, finished_at, browser_version, succeeded, failed, non_deterministic_count) - baselines(target_id, content_hash, approved_at, approved_by) · what the output is supposed to look like - diffs(capture_id, baseline_hash, changed_pixels, changed_regions_json, verdict) - A capture records whether it was stable, meaning two consecutive captures produced identical bytes, because an unstable capture cannot be compared to anything. ### Phase 1 · The same page twice, identically Build: Freeze the clock, disable animations and transitions, block external resources, and pin the browser version. Capture each target twice and compare, marking a target unstable if the two differ. Trap: Everything on a modern page fights determinism: a timestamp, a rotating advertisement, a fade-in that has not finished, a font arriving late, a randomised placeholder. Capture twice and compare on every run, so an unstable target is detected rather than producing a diff you spend an hour investigating. Done when: A page with animations, a clock and web fonts captures byte-identically twice. An unstable target is detected and reported rather than producing a spurious diff. The browser version is pinned and recorded per run. Blocking external resources does not break pages that depend on them, or the failure is reported clearly. Do not build yet: No framing, localisation or diffing. ### Phase 2 · Knowing when the page is ready Build: Wait on real readiness signals: network idle, fonts loaded, a required element present. Support a per-script wait strategy. Fail a capture that never becomes ready rather than capturing a partial page. Trap: Waiting a fixed number of seconds is either too short, producing captures of half-loaded pages, or too long, making a batch take hours, and it is both on different pages in the same run. Wait on signals, and treat a page that never settles as a failure rather than capturing whatever was there at the timeout. Done when: Captures wait on font loading and network idle rather than a fixed delay. A required element can be specified per script and is waited for. A page that never becomes ready fails with a reason rather than producing a partial capture. Batch duration is proportional to real page readiness rather than to a fixed timeout. Do not build yet: No framing, localisation or diffs yet. ### Phase 3 · The same shot in every language Build: Run each script per locale, setting the locale properly rather than only a query parameter. Detect text overflow and clipping caused by longer translations. Keep locale outputs separately comparable. Trap: Setting a language through a query parameter while the browser still reports a different locale produces pages that are half translated, with dates and numbers in the original language, and it looks correct enough to ship. Set the browser locale, the accept header and any application preference together. Done when: A script run in four locales produces genuinely localised output including dates and numbers. Text overflow or clipping caused by a longer translation is detected and reported. Each locale's captures are compared against that locale's own baseline. A locale with a missing translation is reported rather than silently falling back. Do not build yet: No framing or diff review yet. ### Phase 4 · Diffs a person can review quickly Build: Compare against an approved baseline, reporting changed regions rather than a percentage. Support a tolerance for genuinely acceptable differences. Require explicit approval to update a baseline. Trap: Reporting a diff as a percentage of changed pixels tells you nothing about whether the change matters, since a one percent change might be an entire button disappearing or an antialiasing difference along one edge. Report bounding regions and show them, so a reviewer looks at the change rather than at a number. Done when: Diffs report changed regions with their locations rather than only a percentage. A tolerance can be set and its effect is visible in the report. Updating a baseline requires explicit approval and records who approved it. A diff caused by an unstable target is identified as instability rather than as a change. Do not build yet: No framing or export yet. ### Phase 5 · Frames, annotations and batches Build: Apply device frames, captions and highlights after capture, from a declarative description. Run large batches with per-target failure isolation. Keep the undecorated capture so decoration can be redone without recapturing. Trap: Baking a caption into the capture means changing the wording requires recapturing every screenshot, which for a localised app store set is hundreds of captures and a browser version that may have changed since. Keep the raw capture and apply decoration as a separate reproducible step. Done when: Decoration is applied after capture and can be changed without recapturing, verified by rewording a caption. A batch of three hundred targets runs unattended with per-target failure isolation. The same script and decoration description produce identical output on a rerun. Failed targets are reported individually with their reasons. Do not build yet: Nothing after this. ### Out of scope (and why) - Visual testing as a full discipline, including component-level snapshots and cross-browser matrices, which is a larger project. - Recording interactions as video, which has different determinism problems entirely. - Cross-browser and cross-platform rendering comparison, which needs infrastructure per platform. - Automatic detection of what changed semantically, which is where visual diffing tools stop being reliable. ### README must contain - That determinism requires freezing the clock, disabling animation, blocking external resources and pinning the browser - That every target is captured twice and compared, so an unstable target is detected rather than producing a false diff - That waiting is on real readiness signals rather than a fixed delay - That locale is set in the browser, the header and the application together, or the output is half translated - That decoration is applied after capture so wording changes do not require recapturing ===== ARCHITECTURE.md ===== # Architecture · Shotomatic ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A headless browser with a pinned version and fixed viewport | a browser update changes rendering and therefore every screenshot | | Determinism | Frozen clock, disabled animation, blocked external resources | these are the four things that make a page render differently each time | | Framing | Device frames and annotations applied after capture | the capture should be the page, with decoration as a separate step | | Localisation | The same script run per locale with the locale set properly | translated screenshots are the main reason to automate this at all | | Runtime | Python 3.12 with a job queue | batches are long and individual captures fail | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Determinism | Clock, animation, resources and browser version. | Not swappable. Without all four the output cannot be compared to anything. | | Readiness | Signal-based waiting. | Any strategy, provided it is not a fixed delay. | | Localisation | Setting locale everywhere it is read. | Any implementation, provided it is not only a query parameter. | | Diffing | Region reporting and baseline approval. | Any comparison, provided baselines are not updated automatically. | | Decoration | Post-capture framing and captions. | Any renderer, provided the raw capture is kept. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `BROWSER_VERSION` · required · Recorded per run. A browser update invalidates every baseline at once. - `OUTPUT_DIR` · required · Raw captures and decorated outputs, kept separately. - `FREEZE_CLOCK_AT` · required · A fixed time, so any visible clock renders identically. - `BLOCK_EXTERNAL_RESOURCES` · required · Present as a constant. Third-party content is never the same twice. - `STABILITY_CAPTURES` · required · Each target captured this many times and compared before being used. - `DIFF_TOLERANCE_PIXELS` · required · Zero by default. Tolerance hides the changes you are looking for. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Shotomatic product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A headless browser with a pinned version and fixed viewport, Frozen clock, disabled animation, blocked external resources, Device frames and annotations applied after capture, The same script run per locale with the locale set properly, Python 3.12 with a job queue. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Everything on a modern page fights determinism: a timestamp, a rotating advertisement, a fade-in that has not finished, a font arriving late, a randomised placeholder. Capture twice and compare on every run, so an unstable target is detected rather than producing a diff you spend an hour investigating. - Waiting a fixed number of seconds is either too short, producing captures of half-loaded pages, or too long, making a batch take hours, and it is both on different pages in the same run. Wait on signals, and treat a page that never settles as a failure rather than capturing whatever was there at the timeout. - Setting a language through a query parameter while the browser still reports a different locale produces pages that are half translated, with dates and numbers in the original language, and it looks correct enough to ship. Set the browser locale, the accept header and any application preference together. - Reporting a diff as a percentage of changed pixels tells you nothing about whether the change matters, since a one percent change might be an entire button disappearing or an antialiasing difference along one edge. Report bounding regions and show them, so a reviewer looks at the change rather than at a number. - Baking a caption into the capture means changing the wording requires recapturing every screenshot, which for a localised app store set is hundreds of captures and a browser version that may have changed since. Keep the raw capture and apply decoration as a separate reproducible step. ===== MILESTONES.md ===== # Delivery milestones · Shotomatic Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · The same page twice, identically Capturing a page twice produces byte-identical images. ### Steps 1. Freeze time and disable animation Two of the four things that make a page render differently each run. Files: `src/determinism.py` 2. Block external resources Advertisements and third-party widgets are never the same twice. Files: `src/determinism.py` 3. Capture twice and compare The stability check is what makes every later diff meaningful. Files: `src/capture.py`, `tests/test_stability.py` ### Done when - [ ] A page with animation and fonts captures identically twice - [ ] Unstable targets are reported rather than diffed - [ ] The browser version is pinned and recorded - [ ] Blocked resources fail clearly rather than silently ### Watch out - Everything on a modern page fights determinism: a timestamp, a rotating advertisement, a fade-in that has not finished, a font arriving late, a randomised placeholder. Capture twice and compare on every run, so an unstable target is detected rather than producing a diff you spend an hour investigating. ## M2 · Knowing when the page is ready Captures happen after the page has finished, not after a guessed delay. ### Steps 1. Wait on real signals Fonts and network, not a number of seconds. Files: `src/wait.py` 2. Allow a required element per script The page knows when it is ready better than any generic rule. Files: `src/scripts.py` 3. Fail rather than capture partial A screenshot of a loading spinner is worse than a failure. Files: `src/capture.py` ### Done when - [ ] Captures wait on fonts and network idle - [ ] A required element can be specified and is waited for - [ ] A never-ready page fails with a reason - [ ] Batch duration reflects real readiness ### Watch out - Waiting a fixed number of seconds is either too short, producing captures of half-loaded pages, or too long, making a batch take hours, and it is both on different pages in the same run. Wait on signals, and treat a page that never settles as a failure rather than capturing whatever was there at the timeout. ## M3 · The same shot in every language A script runs across locales and the text is genuinely translated. ### Steps 1. Set locale everywhere it is read Browser locale, header and application preference together. Files: `src/locale.py` 2. Detect overflow and clipping German is longer and the button is not. Files: `src/overflow.py` 3. Baseline per locale Comparing a German screenshot to an English baseline finds nothing useful. Files: `src/baselines.py` ### Done when - [ ] Four locales produce genuinely localised output - [ ] Overflow and clipping are detected - [ ] Each locale compares against its own baseline - [ ] Missing translations are reported ### Watch out - Setting a language through a query parameter while the browser still reports a different locale produces pages that are half translated, with dates and numbers in the original language, and it looks correct enough to ship. Set the browser locale, the accept header and any application preference together. ## M4 · Diffs a person can review quickly A change is shown as a region rather than a pixel count. ### Steps 1. Report regions, not percentages A percentage cannot distinguish a missing button from antialiasing. Files: `src/diff.py` 2. Require approval to update a baseline Automatic baseline updates make the whole comparison pointless. Files: `src/baselines.py` 3. Attribute instability correctly An unstable target's diff is not a change and should not be reviewed as one. Files: `src/diff.py` ### Done when - [ ] Diffs report changed regions with locations - [ ] Tolerance is configurable and visible in reports - [ ] Baseline updates require recorded approval - [ ] Instability is distinguished from change ### Watch out - Reporting a diff as a percentage of changed pixels tells you nothing about whether the change matters, since a one percent change might be an entire button disappearing or an antialiasing difference along one edge. Report bounding regions and show them, so a reviewer looks at the change rather than at a number. ## M5 · Frames, annotations and batches (production only) Hundreds of decorated screenshots produce unattended and reproducibly. ### Steps 1. Keep the raw capture So a wording change is a re-render rather than a recapture. Files: `src/decorate.py` 2. Describe decoration declaratively Reproducible, and reviewable as text. Files: `src/decorate.py` 3. Isolate per-target failures One bad page must not end a three hundred target run. Files: `src/batch.py` ### Done when - [ ] Decoration changes without recapturing - [ ] Three hundred targets run unattended - [ ] A rerun produces identical output - [ ] Failures are reported per target ### Watch out - Baking a caption into the capture means changing the wording requires recapturing every screenshot, which for a localised app store set is hundreds of captures and a browser version that may have changed since. Keep the raw capture and apply decoration as a separate reproducible step. ===== OPERATIONS.md ===== # Operations · Shotomatic ## Backup Baselines and scripts are the valuable artefacts, since captures regenerate. Keep the browser version recorded with each baseline, because a baseline is only meaningful against the browser that produced it. ## Restore Restore and confirm baselines match their recorded browser version, since restoring baselines produced by a different browser build will make every subsequent run report differences that are not real. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring The non-deterministic capture count per run is the health metric. A rise usually means a page gained a third-party widget rather than that the tool degraded. ## Incident checklist A run reporting hundreds of diffs almost always means the browser version changed rather than the site, and the pinned version recorded per run is what distinguishes the two in seconds rather than an afternoon. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A page with animation and fonts is confirmed to capture identically twice - [ ] Unstable targets are confirmed reported rather than diffed - [ ] Captures are confirmed to wait on real readiness signals - [ ] Four locales are confirmed genuinely localised - [ ] Decoration is confirmed changeable without recapturing ## Launch constraint Do not market omitted Shotomatic capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Recorded per run. A browser update invalidates every baseline at once. BROWSER_VERSION=pinned build identifier # Required. Raw captures and decorated outputs, kept separately. OUTPUT_DIR=./captures # Required. A fixed time, so any visible clock renders identically. FREEZE_CLOCK_AT=2026-01-01T12:00:00Z # Required. Present as a constant. Third-party content is never the same twice. BLOCK_EXTERNAL_RESOURCES=always # Required. Each target captured this many times and compared before being used. STABILITY_CAPTURES=2 # Required. Zero by default. Tolerance hides the changes you are looking for. DIFF_TOLERANCE_PIXELS=0
# Shotomatic · indie build A screenshot automation tool whose entire value is determinism: the same page captured twice must produce the same bytes, or the output cannot be diffed, reviewed or trusted, and almost everything about a modern page conspires against that. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A headless browser with a pinned version and fixed viewport | a browser update changes rendering and therefore every screenshot | | Determinism | Frozen clock, disabled animation, blocked external resources | these are the four things that make a page render differently each time | | Framing | Device frames and annotations applied after capture | the capture should be the page, with decoration as a separate step | | Localisation | The same script run per locale with the locale set properly | translated screenshots are the main reason to automate this at all | | Runtime | Python 3.12 with a job queue | batches are long and individual captures fail | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Orchestrates capture, comparison and decoration. - Get it: python.org or your package manager, then a virtual environment. - Verify: python3 --version prints 3.12 or higher - [ ] **A headless browser with a pinned version** · free - Why: A browser update changes font rendering and layout subtly, which invalidates every baseline at once and looks like your site changed. - Get it: A browser automation library with a pinned browser build, recorded in your configuration. - Verify: The browser version is pinned and reported in every run - [ ] **Five pages that are hard to capture** · free - Why: Determinism only fails on real pages, and the failures are specific: animation, a live clock, lazy images, web fonts and third-party content. - Get it: Collect pages exhibiting each, from a site you own or have permission to capture. - Verify: All five exhibit their problem on a naive capture - [ ] **An application in four languages** · free - Why: Localised capture is the main reason to automate screenshots, and half-translated output is the failure that looks correct. - Get it: A real application with genuine translations, including one language substantially longer than English. - Verify: Four locales render with translated dates and numbers, not only text - [ ] **Your approval process for baselines** · free - Why: A baseline updated automatically makes the entire comparison meaningless, and deciding the process after the first noisy run means deciding it while annoyed. - Get it: Decide who approves a baseline change and what tolerance is acceptable, and write it down. - Verify: A written process exists and tolerance is configured ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Full visual testing with component snapshots. - Interaction recording as video. - Cross-browser rendering comparison. - Semantic change detection. - polished display, window, and custom-area selection - reliable macOS permission setup and recovery - Action Capture guide editing and annotations - batch website capture with per-URL settings - integrated PDF, ZIP, and MP4 export workflows If one of those is essential to you, that is the reason to keep paying for Shotomatic, and the README should say so rather than pretend.
# Build brief · Shotomatic The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me automated screenshots that come out identical every time you run them to replace Shotomatic. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Capture: A headless browser with a pinned version and fixed viewport · a browser update changes rendering and therefore every screenshot - Determinism: Frozen clock, disabled animation, blocked external resources · these are the four things that make a page render differently each time - Framing: Device frames and annotations applied after capture · the capture should be the page, with decoration as a separate step - Localisation: The same script run per locale with the locale set properly · translated screenshots are the main reason to automate this at all - Runtime: Python 3.12 with a job queue · batches are long and individual captures fail ### Data model (create this before Phase 1) - scripts(id, name, steps_json, viewport_json, wait_strategy, created_at) - targets(id, script_id, url, locale, theme, device_profile) - captures(id, target_id, run_id, image_path, content_hash, captured_at, attempt, stable) - runs(id, started_at, finished_at, browser_version, succeeded, failed, non_deterministic_count) - baselines(target_id, content_hash, approved_at, approved_by) · what the output is supposed to look like - diffs(capture_id, baseline_hash, changed_pixels, changed_regions_json, verdict) - A capture records whether it was stable, meaning two consecutive captures produced identical bytes, because an unstable capture cannot be compared to anything. ### Phase 1 · The same page twice, identically Build: Freeze the clock, disable animations and transitions, block external resources, and pin the browser version. Capture each target twice and compare, marking a target unstable if the two differ. Trap: Everything on a modern page fights determinism: a timestamp, a rotating advertisement, a fade-in that has not finished, a font arriving late, a randomised placeholder. Capture twice and compare on every run, so an unstable target is detected rather than producing a diff you spend an hour investigating. Done when: A page with animations, a clock and web fonts captures byte-identically twice. An unstable target is detected and reported rather than producing a spurious diff. The browser version is pinned and recorded per run. Blocking external resources does not break pages that depend on them, or the failure is reported clearly. Do not build yet: No framing, localisation or diffing. ### Phase 2 · Knowing when the page is ready Build: Wait on real readiness signals: network idle, fonts loaded, a required element present. Support a per-script wait strategy. Fail a capture that never becomes ready rather than capturing a partial page. Trap: Waiting a fixed number of seconds is either too short, producing captures of half-loaded pages, or too long, making a batch take hours, and it is both on different pages in the same run. Wait on signals, and treat a page that never settles as a failure rather than capturing whatever was there at the timeout. Done when: Captures wait on font loading and network idle rather than a fixed delay. A required element can be specified per script and is waited for. A page that never becomes ready fails with a reason rather than producing a partial capture. Batch duration is proportional to real page readiness rather than to a fixed timeout. Do not build yet: No framing, localisation or diffs yet. ### Phase 3 · The same shot in every language Build: Run each script per locale, setting the locale properly rather than only a query parameter. Detect text overflow and clipping caused by longer translations. Keep locale outputs separately comparable. Trap: Setting a language through a query parameter while the browser still reports a different locale produces pages that are half translated, with dates and numbers in the original language, and it looks correct enough to ship. Set the browser locale, the accept header and any application preference together. Done when: A script run in four locales produces genuinely localised output including dates and numbers. Text overflow or clipping caused by a longer translation is detected and reported. Each locale's captures are compared against that locale's own baseline. A locale with a missing translation is reported rather than silently falling back. Do not build yet: No framing or diff review yet. ### Phase 4 · Diffs a person can review quickly Build: Compare against an approved baseline, reporting changed regions rather than a percentage. Support a tolerance for genuinely acceptable differences. Require explicit approval to update a baseline. Trap: Reporting a diff as a percentage of changed pixels tells you nothing about whether the change matters, since a one percent change might be an entire button disappearing or an antialiasing difference along one edge. Report bounding regions and show them, so a reviewer looks at the change rather than at a number. Done when: Diffs report changed regions with their locations rather than only a percentage. A tolerance can be set and its effect is visible in the report. Updating a baseline requires explicit approval and records who approved it. A diff caused by an unstable target is identified as instability rather than as a change. Do not build yet: No framing or export yet. ### Phase 5 · Frames, annotations and batches Build: Apply device frames, captions and highlights after capture, from a declarative description. Run large batches with per-target failure isolation. Keep the undecorated capture so decoration can be redone without recapturing. Trap: Baking a caption into the capture means changing the wording requires recapturing every screenshot, which for a localised app store set is hundreds of captures and a browser version that may have changed since. Keep the raw capture and apply decoration as a separate reproducible step. Done when: Decoration is applied after capture and can be changed without recapturing, verified by rewording a caption. A batch of three hundred targets runs unattended with per-target failure isolation. The same script and decoration description produce identical output on a rerun. Failed targets are reported individually with their reasons. Do not build yet: Nothing after this. ### Out of scope (and why) - Visual testing as a full discipline, including component-level snapshots and cross-browser matrices, which is a larger project. - Recording interactions as video, which has different determinism problems entirely. - Cross-browser and cross-platform rendering comparison, which needs infrastructure per platform. - Automatic detection of what changed semantically, which is where visual diffing tools stop being reliable. ### README must contain - That determinism requires freezing the clock, disabling animation, blocking external resources and pinning the browser - That every target is captured twice and compared, so an unstable target is detected rather than producing a false diff - That waiting is on real readiness signals rather than a fixed delay - That locale is set in the browser, the header and the application together, or the output is half translated - That decoration is applied after capture so wording changes do not require recapturing
# Agent instructions · Shotomatic indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A headless browser with a pinned version and fixed viewport, Frozen clock, disabled animation, blocked external resources, Device frames and annotations applied after capture, The same script run per locale with the locale set properly, Python 3.12 with a job queue. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Everything on a modern page fights determinism: a timestamp, a rotating advertisement, a fade-in that has not finished, a font arriving late, a randomised placeholder. Capture twice and compare on every run, so an unstable target is detected rather than producing a diff you spend an hour investigating. - Waiting a fixed number of seconds is either too short, producing captures of half-loaded pages, or too long, making a batch take hours, and it is both on different pages in the same run. Wait on signals, and treat a page that never settles as a failure rather than capturing whatever was there at the timeout. - Setting a language through a query parameter while the browser still reports a different locale produces pages that are half translated, with dates and numbers in the original language, and it looks correct enough to ship. Set the browser locale, the accept header and any application preference together. - Reporting a diff as a percentage of changed pixels tells you nothing about whether the change matters, since a one percent change might be an entire button disappearing or an antialiasing difference along one edge. Report bounding regions and show them, so a reviewer looks at the change rather than at a number.
# Build plan · Shotomatic A screenshot automation tool whose entire value is determinism: the same page captured twice must produce the same bytes, or the output cannot be diffed, reviewed or trusted, and almost everything about a modern page conspires against that. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · The same page twice, identically Capturing a page twice produces byte-identical images. ### Steps 1. Freeze time and disable animation Two of the four things that make a page render differently each run. Files: `src/determinism.py` 2. Block external resources Advertisements and third-party widgets are never the same twice. Files: `src/determinism.py` 3. Capture twice and compare The stability check is what makes every later diff meaningful. Files: `src/capture.py`, `tests/test_stability.py` ### Done when - [ ] A page with animation and fonts captures identically twice - [ ] Unstable targets are reported rather than diffed - [ ] The browser version is pinned and recorded - [ ] Blocked resources fail clearly rather than silently ### Watch out - Everything on a modern page fights determinism: a timestamp, a rotating advertisement, a fade-in that has not finished, a font arriving late, a randomised placeholder. Capture twice and compare on every run, so an unstable target is detected rather than producing a diff you spend an hour investigating. ## Phase 2 · Knowing when the page is ready Captures happen after the page has finished, not after a guessed delay. ### Steps 1. Wait on real signals Fonts and network, not a number of seconds. Files: `src/wait.py` 2. Allow a required element per script The page knows when it is ready better than any generic rule. Files: `src/scripts.py` 3. Fail rather than capture partial A screenshot of a loading spinner is worse than a failure. Files: `src/capture.py` ### Done when - [ ] Captures wait on fonts and network idle - [ ] A required element can be specified and is waited for - [ ] A never-ready page fails with a reason - [ ] Batch duration reflects real readiness ### Watch out - Waiting a fixed number of seconds is either too short, producing captures of half-loaded pages, or too long, making a batch take hours, and it is both on different pages in the same run. Wait on signals, and treat a page that never settles as a failure rather than capturing whatever was there at the timeout. ## Phase 3 · The same shot in every language A script runs across locales and the text is genuinely translated. ### Steps 1. Set locale everywhere it is read Browser locale, header and application preference together. Files: `src/locale.py` 2. Detect overflow and clipping German is longer and the button is not. Files: `src/overflow.py` 3. Baseline per locale Comparing a German screenshot to an English baseline finds nothing useful. Files: `src/baselines.py` ### Done when - [ ] Four locales produce genuinely localised output - [ ] Overflow and clipping are detected - [ ] Each locale compares against its own baseline - [ ] Missing translations are reported ### Watch out - Setting a language through a query parameter while the browser still reports a different locale produces pages that are half translated, with dates and numbers in the original language, and it looks correct enough to ship. Set the browser locale, the accept header and any application preference together. ## Phase 4 · Diffs a person can review quickly A change is shown as a region rather than a pixel count. ### Steps 1. Report regions, not percentages A percentage cannot distinguish a missing button from antialiasing. Files: `src/diff.py` 2. Require approval to update a baseline Automatic baseline updates make the whole comparison pointless. Files: `src/baselines.py` 3. Attribute instability correctly An unstable target's diff is not a change and should not be reviewed as one. Files: `src/diff.py` ### Done when - [ ] Diffs report changed regions with locations - [ ] Tolerance is configurable and visible in reports - [ ] Baseline updates require recorded approval - [ ] Instability is distinguished from change ### Watch out - Reporting a diff as a percentage of changed pixels tells you nothing about whether the change matters, since a one percent change might be an entire button disappearing or an antialiasing difference along one edge. Report bounding regions and show them, so a reviewer looks at the change rather than at a number. ## Not in this build - Full visual testing with component snapshots. - Interaction recording as video. - Cross-browser rendering comparison. - Semantic change detection. ## After v1, if you want it - Capturing on a schedule to detect unannounced third-party changes, which is a real use - Store-ready output sizes per platform, which is the tedious part of a release - A review interface for approving a batch of baseline changes at once
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Recorded per run. A browser update invalidates every baseline at once. BROWSER_VERSION=pinned build identifier # Required. Raw captures and decorated outputs, kept separately. OUTPUT_DIR=./captures # Required. A fixed time, so any visible clock renders identically. FREEZE_CLOCK_AT=2026-01-01T12:00:00Z # Required. Present as a constant. Third-party content is never the same twice. BLOCK_EXTERNAL_RESOURCES=always # Required. Each target captured this many times and compared before being used. STABILITY_CAPTURES=2 # Required. Zero by default. Tolerance hides the changes you are looking for. DIFF_TOLERANCE_PIXELS=0
# Shotomatic · product brief ## Problem A personal timed screenshot loop is a realistic weekend build. Matching reliable capture across Mac apps, permission recovery, website batches, guide editing, and polished exports is a much larger product. ## Product outcome Screenshot automation whose output is identical run to run, so a diff means the page changed rather than that the tool is noisy. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS 14 or later - Xcode command line tools - Screen Recording permission - Accessibility permission for optional keypress automation ## Explicit non-goals for v1 - Full visual testing with component snapshots. - Interaction recording as video. - Cross-browser rendering comparison. - Semantic change detection. - polished display, window, and custom-area selection - reliable macOS permission setup and recovery - Action Capture guide editing and annotations - batch website capture with per-URL settings - integrated PDF, ZIP, and MP4 export workflows ## Success criteria - A page with animation and fonts is confirmed to capture identically twice - Unstable targets are confirmed reported rather than diffed - Captures are confirmed to wait on real readiness signals - Four locales are confirmed genuinely localised - Decoration is confirmed changeable without recapturing
# Build brief · Shotomatic The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me automated screenshots that come out identical every time you run them to replace Shotomatic. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Capture: A headless browser with a pinned version and fixed viewport · a browser update changes rendering and therefore every screenshot - Determinism: Frozen clock, disabled animation, blocked external resources · these are the four things that make a page render differently each time - Framing: Device frames and annotations applied after capture · the capture should be the page, with decoration as a separate step - Localisation: The same script run per locale with the locale set properly · translated screenshots are the main reason to automate this at all - Runtime: Python 3.12 with a job queue · batches are long and individual captures fail ### Data model (create this before Phase 1) - scripts(id, name, steps_json, viewport_json, wait_strategy, created_at) - targets(id, script_id, url, locale, theme, device_profile) - captures(id, target_id, run_id, image_path, content_hash, captured_at, attempt, stable) - runs(id, started_at, finished_at, browser_version, succeeded, failed, non_deterministic_count) - baselines(target_id, content_hash, approved_at, approved_by) · what the output is supposed to look like - diffs(capture_id, baseline_hash, changed_pixels, changed_regions_json, verdict) - A capture records whether it was stable, meaning two consecutive captures produced identical bytes, because an unstable capture cannot be compared to anything. ### Phase 1 · The same page twice, identically Build: Freeze the clock, disable animations and transitions, block external resources, and pin the browser version. Capture each target twice and compare, marking a target unstable if the two differ. Trap: Everything on a modern page fights determinism: a timestamp, a rotating advertisement, a fade-in that has not finished, a font arriving late, a randomised placeholder. Capture twice and compare on every run, so an unstable target is detected rather than producing a diff you spend an hour investigating. Done when: A page with animations, a clock and web fonts captures byte-identically twice. An unstable target is detected and reported rather than producing a spurious diff. The browser version is pinned and recorded per run. Blocking external resources does not break pages that depend on them, or the failure is reported clearly. Do not build yet: No framing, localisation or diffing. ### Phase 2 · Knowing when the page is ready Build: Wait on real readiness signals: network idle, fonts loaded, a required element present. Support a per-script wait strategy. Fail a capture that never becomes ready rather than capturing a partial page. Trap: Waiting a fixed number of seconds is either too short, producing captures of half-loaded pages, or too long, making a batch take hours, and it is both on different pages in the same run. Wait on signals, and treat a page that never settles as a failure rather than capturing whatever was there at the timeout. Done when: Captures wait on font loading and network idle rather than a fixed delay. A required element can be specified per script and is waited for. A page that never becomes ready fails with a reason rather than producing a partial capture. Batch duration is proportional to real page readiness rather than to a fixed timeout. Do not build yet: No framing, localisation or diffs yet. ### Phase 3 · The same shot in every language Build: Run each script per locale, setting the locale properly rather than only a query parameter. Detect text overflow and clipping caused by longer translations. Keep locale outputs separately comparable. Trap: Setting a language through a query parameter while the browser still reports a different locale produces pages that are half translated, with dates and numbers in the original language, and it looks correct enough to ship. Set the browser locale, the accept header and any application preference together. Done when: A script run in four locales produces genuinely localised output including dates and numbers. Text overflow or clipping caused by a longer translation is detected and reported. Each locale's captures are compared against that locale's own baseline. A locale with a missing translation is reported rather than silently falling back. Do not build yet: No framing or diff review yet. ### Phase 4 · Diffs a person can review quickly Build: Compare against an approved baseline, reporting changed regions rather than a percentage. Support a tolerance for genuinely acceptable differences. Require explicit approval to update a baseline. Trap: Reporting a diff as a percentage of changed pixels tells you nothing about whether the change matters, since a one percent change might be an entire button disappearing or an antialiasing difference along one edge. Report bounding regions and show them, so a reviewer looks at the change rather than at a number. Done when: Diffs report changed regions with their locations rather than only a percentage. A tolerance can be set and its effect is visible in the report. Updating a baseline requires explicit approval and records who approved it. A diff caused by an unstable target is identified as instability rather than as a change. Do not build yet: No framing or export yet. ### Phase 5 · Frames, annotations and batches Build: Apply device frames, captions and highlights after capture, from a declarative description. Run large batches with per-target failure isolation. Keep the undecorated capture so decoration can be redone without recapturing. Trap: Baking a caption into the capture means changing the wording requires recapturing every screenshot, which for a localised app store set is hundreds of captures and a browser version that may have changed since. Keep the raw capture and apply decoration as a separate reproducible step. Done when: Decoration is applied after capture and can be changed without recapturing, verified by rewording a caption. A batch of three hundred targets runs unattended with per-target failure isolation. The same script and decoration description produce identical output on a rerun. Failed targets are reported individually with their reasons. Do not build yet: Nothing after this. ### Out of scope (and why) - Visual testing as a full discipline, including component-level snapshots and cross-browser matrices, which is a larger project. - Recording interactions as video, which has different determinism problems entirely. - Cross-browser and cross-platform rendering comparison, which needs infrastructure per platform. - Automatic detection of what changed semantically, which is where visual diffing tools stop being reliable. ### README must contain - That determinism requires freezing the clock, disabling animation, blocking external resources and pinning the browser - That every target is captured twice and compared, so an unstable target is detected rather than producing a false diff - That waiting is on real readiness signals rather than a fixed delay - That locale is set in the browser, the header and the application together, or the output is half translated - That decoration is applied after capture so wording changes do not require recapturing
# Architecture · Shotomatic ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A headless browser with a pinned version and fixed viewport | a browser update changes rendering and therefore every screenshot | | Determinism | Frozen clock, disabled animation, blocked external resources | these are the four things that make a page render differently each time | | Framing | Device frames and annotations applied after capture | the capture should be the page, with decoration as a separate step | | Localisation | The same script run per locale with the locale set properly | translated screenshots are the main reason to automate this at all | | Runtime | Python 3.12 with a job queue | batches are long and individual captures fail | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Determinism | Clock, animation, resources and browser version. | Not swappable. Without all four the output cannot be compared to anything. | | Readiness | Signal-based waiting. | Any strategy, provided it is not a fixed delay. | | Localisation | Setting locale everywhere it is read. | Any implementation, provided it is not only a query parameter. | | Diffing | Region reporting and baseline approval. | Any comparison, provided baselines are not updated automatically. | | Decoration | Post-capture framing and captions. | Any renderer, provided the raw capture is kept. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `BROWSER_VERSION` · required · Recorded per run. A browser update invalidates every baseline at once. - `OUTPUT_DIR` · required · Raw captures and decorated outputs, kept separately. - `FREEZE_CLOCK_AT` · required · A fixed time, so any visible clock renders identically. - `BLOCK_EXTERNAL_RESOURCES` · required · Present as a constant. Third-party content is never the same twice. - `STABILITY_CAPTURES` · required · Each target captured this many times and compared before being used. - `DIFF_TOLERANCE_PIXELS` · required · Zero by default. Tolerance hides the changes you are looking for. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Shotomatic product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A headless browser with a pinned version and fixed viewport, Frozen clock, disabled animation, blocked external resources, Device frames and annotations applied after capture, The same script run per locale with the locale set properly, Python 3.12 with a job queue. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Everything on a modern page fights determinism: a timestamp, a rotating advertisement, a fade-in that has not finished, a font arriving late, a randomised placeholder. Capture twice and compare on every run, so an unstable target is detected rather than producing a diff you spend an hour investigating. - Waiting a fixed number of seconds is either too short, producing captures of half-loaded pages, or too long, making a batch take hours, and it is both on different pages in the same run. Wait on signals, and treat a page that never settles as a failure rather than capturing whatever was there at the timeout. - Setting a language through a query parameter while the browser still reports a different locale produces pages that are half translated, with dates and numbers in the original language, and it looks correct enough to ship. Set the browser locale, the accept header and any application preference together. - Reporting a diff as a percentage of changed pixels tells you nothing about whether the change matters, since a one percent change might be an entire button disappearing or an antialiasing difference along one edge. Report bounding regions and show them, so a reviewer looks at the change rather than at a number. - Baking a caption into the capture means changing the wording requires recapturing every screenshot, which for a localised app store set is hundreds of captures and a browser version that may have changed since. Keep the raw capture and apply decoration as a separate reproducible step.
# Delivery milestones · Shotomatic Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · The same page twice, identically Capturing a page twice produces byte-identical images. ### Steps 1. Freeze time and disable animation Two of the four things that make a page render differently each run. Files: `src/determinism.py` 2. Block external resources Advertisements and third-party widgets are never the same twice. Files: `src/determinism.py` 3. Capture twice and compare The stability check is what makes every later diff meaningful. Files: `src/capture.py`, `tests/test_stability.py` ### Done when - [ ] A page with animation and fonts captures identically twice - [ ] Unstable targets are reported rather than diffed - [ ] The browser version is pinned and recorded - [ ] Blocked resources fail clearly rather than silently ### Watch out - Everything on a modern page fights determinism: a timestamp, a rotating advertisement, a fade-in that has not finished, a font arriving late, a randomised placeholder. Capture twice and compare on every run, so an unstable target is detected rather than producing a diff you spend an hour investigating. ## M2 · Knowing when the page is ready Captures happen after the page has finished, not after a guessed delay. ### Steps 1. Wait on real signals Fonts and network, not a number of seconds. Files: `src/wait.py` 2. Allow a required element per script The page knows when it is ready better than any generic rule. Files: `src/scripts.py` 3. Fail rather than capture partial A screenshot of a loading spinner is worse than a failure. Files: `src/capture.py` ### Done when - [ ] Captures wait on fonts and network idle - [ ] A required element can be specified and is waited for - [ ] A never-ready page fails with a reason - [ ] Batch duration reflects real readiness ### Watch out - Waiting a fixed number of seconds is either too short, producing captures of half-loaded pages, or too long, making a batch take hours, and it is both on different pages in the same run. Wait on signals, and treat a page that never settles as a failure rather than capturing whatever was there at the timeout. ## M3 · The same shot in every language A script runs across locales and the text is genuinely translated. ### Steps 1. Set locale everywhere it is read Browser locale, header and application preference together. Files: `src/locale.py` 2. Detect overflow and clipping German is longer and the button is not. Files: `src/overflow.py` 3. Baseline per locale Comparing a German screenshot to an English baseline finds nothing useful. Files: `src/baselines.py` ### Done when - [ ] Four locales produce genuinely localised output - [ ] Overflow and clipping are detected - [ ] Each locale compares against its own baseline - [ ] Missing translations are reported ### Watch out - Setting a language through a query parameter while the browser still reports a different locale produces pages that are half translated, with dates and numbers in the original language, and it looks correct enough to ship. Set the browser locale, the accept header and any application preference together. ## M4 · Diffs a person can review quickly A change is shown as a region rather than a pixel count. ### Steps 1. Report regions, not percentages A percentage cannot distinguish a missing button from antialiasing. Files: `src/diff.py` 2. Require approval to update a baseline Automatic baseline updates make the whole comparison pointless. Files: `src/baselines.py` 3. Attribute instability correctly An unstable target's diff is not a change and should not be reviewed as one. Files: `src/diff.py` ### Done when - [ ] Diffs report changed regions with locations - [ ] Tolerance is configurable and visible in reports - [ ] Baseline updates require recorded approval - [ ] Instability is distinguished from change ### Watch out - Reporting a diff as a percentage of changed pixels tells you nothing about whether the change matters, since a one percent change might be an entire button disappearing or an antialiasing difference along one edge. Report bounding regions and show them, so a reviewer looks at the change rather than at a number. ## M5 · Frames, annotations and batches (production only) Hundreds of decorated screenshots produce unattended and reproducibly. ### Steps 1. Keep the raw capture So a wording change is a re-render rather than a recapture. Files: `src/decorate.py` 2. Describe decoration declaratively Reproducible, and reviewable as text. Files: `src/decorate.py` 3. Isolate per-target failures One bad page must not end a three hundred target run. Files: `src/batch.py` ### Done when - [ ] Decoration changes without recapturing - [ ] Three hundred targets run unattended - [ ] A rerun produces identical output - [ ] Failures are reported per target ### Watch out - Baking a caption into the capture means changing the wording requires recapturing every screenshot, which for a localised app store set is hundreds of captures and a browser version that may have changed since. Keep the raw capture and apply decoration as a separate reproducible step.
# Operations · Shotomatic ## Backup Baselines and scripts are the valuable artefacts, since captures regenerate. Keep the browser version recorded with each baseline, because a baseline is only meaningful against the browser that produced it. ## Restore Restore and confirm baselines match their recorded browser version, since restoring baselines produced by a different browser build will make every subsequent run report differences that are not real. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring The non-deterministic capture count per run is the health metric. A rise usually means a page gained a third-party widget rather than that the tool degraded. ## Incident checklist A run reporting hundreds of diffs almost always means the browser version changed rather than the site, and the pinned version recorded per run is what distinguishes the two in seconds rather than an afternoon. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A page with animation and fonts is confirmed to capture identically twice - [ ] Unstable targets are confirmed reported rather than diffed - [ ] Captures are confirmed to wait on real readiness signals - [ ] Four locales are confirmed genuinely localised - [ ] Decoration is confirmed changeable without recapturing ## Launch constraint Do not market omitted Shotomatic capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Recorded per run. A browser update invalidates every baseline at once. BROWSER_VERSION=pinned build identifier # Required. Raw captures and decorated outputs, kept separately. OUTPUT_DIR=./captures # Required. A fixed time, so any visible clock renders identically. FREEZE_CLOCK_AT=2026-01-01T12:00:00Z # Required. Present as a constant. Third-party content is never the same twice. BLOCK_EXTERNAL_RESOURCES=always # Required. Each target captured this many times and compared before being used. STABILITY_CAPTURES=2 # Required. Zero by default. Tolerance hides the changes you are looking for. DIFF_TOLERANCE_PIXELS=0
$ choose a build depth, inspect the files, then open the complete pack in your agent
They want repeatable capture across Mac apps without maintaining native permission handling, automation edge cases, session history, and several export pipelines themselves.
xpolished display, window, and custom-area selection
xreliable macOS permission setup and recovery
xAction Capture guide editing and annotations
xbatch website capture with per-URL settings
xintegrated PDF, ZIP, and MP4 export workflows
Shotomatic pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| pro monthly | $7.99 | — | Up to 3 Macs; unlimited screenshots per session. |
| pro yearly | — | $4.08 | Up to 3 Macs; unlimited screenshots per session. |
| pro lifetime | custom | — | Up to 3 Macs; unlimited screenshots per session. |
free tierno free tier
billingMonthly + annual + one-time lifetime license; no permanent free plan.
hidden costsThe lifetime purchase is a separate one-time entitlement rather than an annual subscription; the public page offers a 14-day money-back period.
verified 2026-08-12 · source ↗
Vibecode Shotomatic
Kinda. The core of Shotomatic is buildable in a weekend with the prompt on this page, but there are real gaps: polished display, window, and custom-area selection, reliable macOS permission setup and recovery. Read the honest list above before committing.
How much does Shotomatic cost?
Shotomatic costs about $7.99/month (Pro Monthly, checked 2026-07-30), which is $95.88 per year.
What do I lose by replacing Shotomatic?
Honestly: polished display, window, and custom-area selection; reliable macOS permission setup and recovery; Action Capture guide editing and annotations; batch website capture with per-URL settings; integrated PDF, ZIP, and MP4 export workflows. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Shotomatic?
Yes: Flameshot (mature cross-platform screenshot capture, but not a timed Mac automation workflow). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.