From 0c6ea81b04b9848a3a2bd96cf098d4bcee5cea7e Mon Sep 17 00:00:00 2001 From: tajpuriya27 Date: Wed, 23 Sep 2026 01:12:44 +0545 Subject: [PATCH 1/6] docs: restructure README for users and move contributor docs README had become a mix of user and contributor material, and several claims had drifted from the code (globe icon, menu mock-up glyphs, fallback reassignment order, "links will never go there"). README is now the user landing doc: why, install, build from source, using it, routing, a new Privacy section, known limitations, uninstall. Every claim was checked against Sources/, Support/Info.plist and install.sh. Architecture, repository layout, project history, releasing and local release testing move to CONTRIBUTING.md with no duplicated text. SECURITY.md now points to README#privacy so the privacy facts live in one place, and assets/README.md drops its stale "not yet wired" section. Docs only; no code, script or workflow changes. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 + CONTRIBUTING.md | 75 ++++++++++- README.md | 183 ++++++++------------------ SECURITY.md | 10 +- assets/README.md | 15 ++- tasks/17-readme-restructure.md | 233 +++++++++++++++++++++++++++++++++ 6 files changed, 371 insertions(+), 148 deletions(-) create mode 100644 tasks/17-readme-restructure.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a8fdae..2d440b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **The repository moved to the `hexember` GitHub account.** Every link, the `install.sh` release source and the install one-liner now point to `hexember/active-browser`. Re-run the one-liner from the README to pick up the new URL. +- README reorganized for users, with a new Privacy section; contributor material + (architecture, repository layout, releasing, local release testing) moved to + `CONTRIBUTING.md`. ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a680f25..db5da15 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,13 +14,15 @@ make install # builds, installs to /Applications, registers, launches Requirements: **macOS 13+** and a **Swift 6** toolchain (`swift --version`). Install the Xcode Command Line Tools with `xcode-select --install` if you don't have them. +Zero third-party dependencies: Swift plus Apple frameworks only (`AppKit`, `Foundation`, `ServiceManagement`). + | Command | What it does | |---|---| | `make build` | `swift build -c release` | | `make bundle` | assemble and ad-hoc sign `build/ActiveBrowser.app` | | `make run` | bundle, then launch the `build/` copy | | `make install` | build, install to `/Applications`, register, launch | -| `make release` | produce `ActiveBrowser.app.zip` + `SHA256SUMS` | +| `make release` | produce `build/ActiveBrowser.app.zip` + `SHA256SUMS` | | `make clean` | unregister the build copy, remove `.build/` and `build/` | ## Three things that will cost you an hour if nobody tells you @@ -41,8 +43,53 @@ open -a ActiveBrowser # relaunch through Launch Services ``` **3. Don't run `make run` while a copy is installed.** It launches a second bundle with -the same bundle id claiming the same URL schemes, so macOS registers both and you get two -ActiveBrowser rows in the default-browser dropdown. `make clean` removes the build copy. +the same bundle id claiming the same URL schemes, so macOS registers both and you get +two ActiveBrowser rows in the default-browser dropdown. `make clean` unregisters and +removes the build copy. `make install` and `make release` both delete `build/ActiveBrowser.app` +themselves for exactly this reason. + +## Architecture + +``` +Sources/ActiveBrowser/ +├── App/ +│ ├── AppDelegate.swift # entry point, owns all state +│ └── FocusObserver.swift # the one NSWorkspace focus subscription +├── Core/ +│ ├── BrowserRegistry.swift # installed https handlers, minus ourselves +│ ├── BrowserStack.swift # LRU order + target resolution +│ ├── Settings.swift # UserDefaults-backed preferences +│ └── URLDispatcher.swift # the single place a URL is handed off +└── UI/ + └── MenuBarManager.swift # status item and menu +``` + +## Repository layout + +``` +Sources/ the app +Support/ Info.plist (the bundle manifest) +assets/ icon artwork (see assets/README.md) +install.sh the curl installer +Makefile build, bundle, install, release +docs/ background notes +.github/workflows/ CI and release +``` + +## Project history + +Three directories are **history, not instructions**: `tasks/`, `Project.md`, and +`.claude/`. This project was built by AI agents working through a task-per-PR workflow, and +those files are that workflow's records — the specs, the review notes, and the agent +definitions. They're kept because the reasoning in them is often useful (several non-obvious +macOS behaviours are documented there and nowhere else), but **you do not need to read or +follow any of it to contribute**. `CONTRIBUTING.md` is the only process document that +applies to you. `tasks/TEST-PLAN.md` is the manual checklist that workflow accumulated. + +One caveat if you do read them: `Project.md` is the original specification, and the shipped +code departs from it in several places, notably three where the spec turned out to be wrong — +the `@main` entry point, the `install:` target ordering, and the `SMAppService` status gate. +Where the two disagree, the code is correct. Don't "restore" it to match the document. ## Testing @@ -77,10 +124,11 @@ Two gotchas when testing routing manually: These are enforced in review; a PR that breaks one will be asked to change: -- **No third-party dependencies.** Swift plus Apple frameworks only. +- **No third-party dependencies.** - **No polling.** State changes through `NSWorkspace` notifications and menu actions only. - **Everything is `@MainActor`.** No GCD queues, no locks, no actors. -- **Background agent only** (`LSUIElement`) — no Dock icon, no windows. +- **Background agent only** (`LSUIElement`) — no Dock icon, no windows. Idle footprint is + ~12 MB today; keep it under 25 MB. - **Never drop a URL.** Every dispatch resolves to some browser, or logs why it couldn't. - **Never route to ourselves.** The app's own bundle id is filtered out of the registry and every dispatch candidate — otherwise a link loops back into the process forever. @@ -93,7 +141,8 @@ These are enforced in review; a PR that breaks one will be asked to change: ## Releasing (maintainers) -Push a `v*` tag from `main`; the workflow builds and publishes the assets. +Push a `v*` tag from `main`. The release workflow builds on `macos-latest`, verifies the +artefacts, and only then publishes them to the release. ```sh git tag v0.1.1 && git push origin v0.1.1 @@ -102,3 +151,17 @@ git tag v0.1.1 && git push origin v0.1.1 The published asset names `ActiveBrowser.app.zip` and `SHA256SUMS` are a contract that `install.sh` depends on — renaming either breaks the installer for every user. CI guards this. + +### Testing a release locally + +Run the installer against a locally built zip before tagging: + +```sh +make release +ACTIVEBROWSER_ZIP=build/ActiveBrowser.app.zip sh install.sh +``` + +With `ACTIVEBROWSER_ZIP` set, `install.sh` makes no network access at all. +`ACTIVEBROWSER_SUMS` names the checksum file and defaults to the `SHA256SUMS` next to the +zip, which is exactly where `make release` writes it. `ACTIVEBROWSER_VERSION` pins a +release tag instead of resolving the latest one. diff --git a/README.md b/README.md index c18f3aa..43d25fa 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,8 @@ The existing tools solve this, but all of them ask you to make a decision: ActiveBrowser uses a different signal entirely: **your recent focus**. You already told the system which browser you're working in — by working in it. No config file, no rules, no prompt. -**When a rules-based tool is the better choice:** if your routing genuinely depends on the *URL* — "all `github.com` links must open in the work profile", "Zoom links bypass the browser" — that's a rule, and you want Finicky or Velja. ActiveBrowser has no idea what the URL says and deliberately doesn't look. - -**When ActiveBrowser is the better choice:** if your routing depends on *what you're doing right now*, and the same link would reasonably go to different browsers depending on the hour. That's the case rules handle badly, because the rule would have to encode your context, and your context changes all day. +- **A rules-based tool is the better choice** when routing depends on the *URL*: "all `github.com` links open in the work profile", "Zoom links bypass the browser". Use Finicky or Velja. ActiveBrowser has no idea what the URL says and deliberately doesn't look. +- **ActiveBrowser is the better choice** when routing depends on *what you're doing right now*, and the same link would reasonably go to different browsers depending on the hour. Rules handle that badly, because the rule would have to encode your context, and your context changes all day. The two approaches aren't rivals so much as answers to different questions: *where does this URL belong?* versus *where am I working?* @@ -46,18 +45,17 @@ The two approaches aren't rivals so much as answers to different questions: *whe curl -fsSL https://raw.githubusercontent.com/hexember/active-browser/main/install.sh | sh ``` -Then open the menu bar icon and choose **Set as Default Browser**, and accept the macOS confirmation dialog. +Then click the ActiveBrowser icon in the menu bar, choose **Set as Default Browser**, and accept the macOS confirmation dialog. The row then reads **✓ Default Browser**. -The script checks your macOS version and CPU architecture, verifies the download's SHA-256 checksum, and validates the app bundle *before* it replaces anything. Re-running it is the upgrade path — safe to run with the app open. +The script checks your macOS version and CPU architecture, verifies the download's SHA-256 checksum, and validates the app bundle *before* it replaces anything. Re-running it is the upgrade path — it quits the running app first, so it's safe to run with the app open. The script itself never uses `sudo`, and never touches your settings, your login items, or your default-browser choice. -**Requirements:** macOS 13 (Ventura) or later, Apple silicon. Released builds are `arm64`; on an Intel Mac the installer refuses with a clear message rather than installing something that won't run. +On its first launch from `/Applications`, the app adds itself as a login item, so it's already running the first time you click a link after a restart. You can turn that off from the menu — see [Using it](#using-it). -**Why there's no Gatekeeper prompt:** the app is ad-hoc signed, not notarized. `curl` doesn't set the quarantine attribute, so a `curl`-installed copy opens without a warning. Downloading the zip through a browser *would* quarantine it, so use the one-liner. +**Requirements:** macOS 13 (Ventura) or later, Apple silicon. Released builds are `arm64`; on an Intel Mac the installer refuses with a clear message rather than installing something that won't run. -
-Other ways to install +**Why there's no Gatekeeper prompt:** the app is ad-hoc signed, not notarized. `curl` doesn't set the quarantine attribute, so a `curl`-installed copy opens without a warning. Downloading the zip through a browser *would* quarantine it, so use the one-liner. [SECURITY.md](SECURITY.md#what-youre-trusting-when-you-install-this) spells out what you're trusting. -Build and install from source: +### Build from source ```sh git clone https://github.com/hexember/active-browser.git @@ -65,156 +63,96 @@ cd active-browser make install ``` -Install from a local zip (useful for testing a release before publishing it): - -```sh -ACTIVEBROWSER_ZIP=/path/to/ActiveBrowser.app.zip sh install.sh -``` - -`install.sh` also accepts `ACTIVEBROWSER_SUMS` (checksum file) and `ACTIVEBROWSER_VERSION` (pin a tag). - -
- -### Uninstall - -Order matters — removing the login item while the app is still installed lets the next launch re-create it. - -```sh -pkill -x ActiveBrowser -rm -rf /Applications/ActiveBrowser.app -defaults delete com.local.activebrowser # optional: drop saved settings -``` - -Then System Settings → General → **Login Items & Extensions** → select ActiveBrowser → **−**, and set your real default browser back in System Settings → Desktop & Dock. +This needs a Swift 6 toolchain, and it works on Intel Macs too. See [CONTRIBUTING.md](CONTRIBUTING.md#build-and-run) for the other build targets. --- ## Using it -Click the globe icon in the menu bar: +Click the ActiveBrowser icon in the menu bar. Hovering over it shows the same `Routing to: …` line as a tooltip. ``` -Routing to: Arc ← where the next link goes, right now -Recent: Arc › Brave Browser › Safari ← your focus order +Routing to: Arc ← where the next link goes, right now +Recent: Arc › Brave Browser › Safari ← your focus order (first three, then › …) ────────── -Browsers ▸ ☑ Arc ☑ Brave ☑ Safari ← who participates -Fallback Browser ▸ ● Arc ○ Brave ○ Safari ← used when nothing is running +Browsers ▸ ✓ Arc ✓ Brave Browser ✓ Safari ← who participates +Fallback Browser ▸ ✓ Arc Brave Browser Safari ← used before you've focused a browser ────────── -Set as Default Browser -☑ Launch at Login +Set as Default Browser ← becomes "✓ Default Browser" once it is +✓ Launch at Login ────────── -Quit ActiveBrowser +Quit ActiveBrowser ⌘Q ``` -**Browsers** — untick a browser and it stops participating entirely: focusing it won't affect routing, and links will never go there. Useful for a browser you keep open but don't want links in. The last remaining ticked browser can't be unticked — that would leave nowhere to send a link. +`Recent:` reads `Recent: none yet` until you focus a browser, and `Routing to:` reads `none` if no browser can be resolved. + +**Browsers** lists every app registered to open `https` links, once each — which can include apps that aren't really browsers. Untick one and it stops participating: focusing it no longer affects routing, and it drops out of your focus history. Useful for a browser you keep open but don't want links in. The one exception: if none of the browsers in your focus history is still installed *and* your Fallback Browser has been removed from disk, the last-resort step (see below) can still pick an unticked browser rather than drop the link. The last remaining ticked browser can't be unticked — its row is greyed out, because that would leave nowhere to send a link. + +**Fallback Browser** lists your ticked browsers; the checked one is used when the routing chain has nothing better, most commonly right after login when you haven't focused a browser yet. Untick the browser currently set as fallback and the fallback moves to the first remaining ticked browser in menu order (alphabetical). + +**Launch at Login** is on by default for a copy in `/Applications`. Untick it and the app remembers, so it won't re-add itself at the next launch. If the row shows a dash (`–`) instead of a checkmark, the login item was switched off in System Settings; clicking the row opens System Settings → Login Items, where only you can switch it back on. + +On first launch every detected browser is ticked, and the Fallback Browser is set to the first one alphabetically. -**Fallback Browser** — used when the routing chain has nothing better, most commonly right after login when you haven't focused a browser yet. Untick the browser currently set as fallback and it moves to the next one automatically. +Runs as a background agent: no Dock icon, no windows, about 12 MB of memory idle. -Both settings persist across restarts. The menu re-scans for newly installed browsers each time you open it. +Your settings persist across restarts. The menu re-scans for newly installed browsers each time you open it. ### How a URL is routed 1. The most recently focused **included** browser that is **currently running** 2. Otherwise the most recently focused included browser, even if it isn't running (it gets launched) 3. Otherwise your **Fallback Browser** -4. Otherwise the first browser it can find +4. Otherwise the first installed `https` handler by name — even one you've unticked -A URL is never dropped. If no browser can be resolved at all, it's logged rather than silently discarded — and ActiveBrowser filters itself out of every step, so a link can't be routed back into it in a loop. +A candidate is only used if the app still exists on disk; otherwise the next step is tried. ---- - -## Development - -Zero third-party dependencies — Swift plus Apple frameworks (`AppKit`, `Foundation`, `ServiceManagement`) only. +ActiveBrowser always hands a link to some browser. Only if no browser can be found at all is a link left unopened, and even then it's logged rather than silently lost — and ActiveBrowser filters its own bundle id out of every step, so a link can't be routed back into it in a loop. -```sh -make build # swift build -c release -make bundle # assemble + ad-hoc sign build/ActiveBrowser.app -make run # bundle, then launch the build/ copy -make install # build, install to /Applications, register, launch -make release # build/ActiveBrowser.app.zip + SHA256SUMS -make clean # unregister the build copy, remove .build/ and build/ -``` +The focus history lives in memory only, so after a relaunch or a login it's empty and links go to your Fallback Browser until you focus a browser. ActiveBrowser also registers as a low-priority viewer for `.html` and `.xhtml` files — macOS won't list it as a browser otherwise — so it can appear in Finder's *Open With*, and any file it's handed is routed the same way as a link. -**The app must run from the `.app` bundle.** A bare `swift build` binary has no `Info.plist`, so `LSUIElement` (no Dock icon), the `http`/`https` claim, and self-filtering are all inert — `Bundle.main.bundleIdentifier` is `nil` outside a bundle. Use `make run` or `make install`, not `.build/release/ActiveBrowser`. - -**Careful with `make run` while a copy is installed.** It launches a second bundle from `build/` with the same bundle id, claiming the same URL schemes, so macOS registers both and you get two ActiveBrowser rows in the default-browser dropdown. `make clean` unregisters and removes the build copy. (`make install` and `make release` both delete `build/ActiveBrowser.app` themselves for exactly this reason.) +--- -**Logging:** `NSLog` output from this process doesn't reach `log show`. To read it, run the binary directly and capture stderr: +## Privacy -```sh -pkill -x ActiveBrowser -/Applications/ActiveBrowser.app/Contents/MacOS/ActiveBrowser > /tmp/ab.log 2>&1 & -sleep 4; kill %1; cat /tmp/ab.log -open -a ActiveBrowser # relaunch through Launch Services -``` +- **What it sees:** every URL it is handed (links, plus any `.html` file opened with it), and which app comes to the front. It only records focus for browsers you've ticked. +- **What it doesn't do:** no network requests of any kind, no telemetry, and it never writes URLs to disk or logs them. It never inspects or matches on URL contents; routing is based purely on focus order. +- **What it stores:** three `UserDefaults` keys in the `com.local.activebrowser` domain, all local — `includedBrowsers` (which browsers participate), `defaultBrowser` (your Fallback Browser), and `launchAtLoginOptOut` (whether you turned Launch at Login off). The focus order is kept in memory only. -### Architecture +The installer is the only part that touches the network: the one-liner fetches the script from `raw.githubusercontent.com`, and the script asks `api.github.com` for the latest release tag (skipped when a version is pinned) and downloads the release from `github.com/hexember/active-browser/releases`. Installing from a local zip makes no network access at all. -``` -Sources/ActiveBrowser/ -├── App/ -│ ├── AppDelegate.swift # entry point, owns all state -│ └── FocusObserver.swift # the one NSWorkspace focus subscription -├── Core/ -│ ├── BrowserRegistry.swift # installed https handlers, minus ourselves -│ ├── BrowserStack.swift # LRU order + target resolution -│ ├── Settings.swift # UserDefaults-backed preferences -│ └── URLDispatcher.swift # the single place a URL is handed off -└── UI/ - └── MenuBarManager.swift # status item and menu -``` +--- -Design constraints, all enforced in review: +## Known limitations -- **No polling.** State changes only through `NSWorkspace` notifications and your menu actions. -- **Everything is `@MainActor`.** No GCD queues, no locks, no actors. -- **Background agent only** (`LSUIElement`) — no Dock icon, no windows. Idle footprint is ~12 MB. -- **Never routes to itself.** The app's own bundle id is filtered out of the registry and every dispatch candidate. +- **Intel Macs:** released builds are `arm64` only. [Build from source](#build-from-source) instead. +- **Not notarized:** install with the one-liner or from source; a zip downloaded through a browser is quarantined (see [Install](#install)). +- **Routing ignores the URL.** By design — see [Why this exists](#why-this-exists). If you need per-site rules, use Finicky or Velja. +- **`http`/`https` only** (plus the `.html` files described above). Other schemes are left to the system. +- **Links go to the Fallback Browser after a launch** until you focus a browser, because the focus history isn't saved. -### Testing +--- -Manual, on a real machine — there is no XCTest target. macOS integration (Launch Services registration, default-browser binding, focus notifications, login items) is most of what could break, and almost none of it is meaningfully unit-testable. `tasks/TEST-PLAN.md` holds the manual checklist. +## Uninstall -### Releasing +Order matters. Reset your default browser first, so a link clicked mid-uninstall can't relaunch ActiveBrowser. And remove the login item last — removing it while the app is still installed lets the next launch re-create it. -Push a `v*` tag. A GitHub Actions workflow builds on `macos-latest`, verifies the artefacts, and publishes `ActiveBrowser.app.zip` and `SHA256SUMS` to the release. +1. Set your real default browser back in System Settings → Desktop & Dock. +2. Quit and delete the app: -```sh -git tag v0.1.0 && git push origin v0.1.0 -``` + ```sh + pkill -x ActiveBrowser + rm -rf /Applications/ActiveBrowser.app + defaults delete com.local.activebrowser # optional: drop saved settings + ``` -Those two asset names are a published contract — `install.sh` builds its download URLs from them, so renaming either breaks the installer for everyone. +3. System Settings → General → **Login Items & Extensions** → select ActiveBrowser → **−**. --- -## Repository layout - -``` -Sources/ the app -Support/ Info.plist (the bundle manifest) -install.sh the curl installer -Makefile build, bundle, install, release -docs/ background notes -``` - -Three directories are **history, not instructions**: `tasks/`, `Project.md`, and -`.claude/`. This project was built by AI agents working through a task-per-PR workflow, and -those files are that workflow's records — the specs, the review notes, and the agent -definitions. They're kept because the reasoning in them is often useful (several non-obvious -macOS behaviours are documented there and nowhere else), but **you do not need to read or -follow any of it to contribute**. `CONTRIBUTING.md` is the only process document that -applies to you. - -One caveat if you do read them: `Project.md` is the original specification and the shipped -code deliberately departs from it in three places where the spec turned out to be wrong — -the `@main` entry point, the `install:` target ordering, and the `SMAppService` status gate. -The code is correct in all three. Don't "restore" it to match the document. - ## Contributing -Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for how to build, the -three macOS gotchas that will otherwise cost you an hour, and the design constraints. +Contributions are welcome. [CONTRIBUTING.md](CONTRIBUTING.md) covers building, the macOS gotchas that will otherwise cost you an hour, the architecture, the design constraints, testing, and releasing. Please also read the [Code of Conduct](CODE_OF_CONDUCT.md). For security issues, see [SECURITY.md](SECURITY.md) — report privately, not in a public issue. @@ -226,10 +164,3 @@ See [CHANGELOG.md](CHANGELOG.md). ## License [MIT](LICENSE). - -## Known limitations - -- **Apple silicon only** in released builds. Building from source on Intel works; the published zip is `arm64`. -- **Ad-hoc signed, not notarized.** Fine for `curl`, but a browser download would be quarantined. -- **Routing ignores the URL.** By design — see [Why this exists](#why-this-exists). If you need per-site rules, use Finicky or Velja. -- **`http`/`https` only.** Other schemes are left to the system. diff --git a/SECURITY.md b/SECURITY.md index 689566d..a3db31b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -32,12 +32,4 @@ not writable by your user, it refuses rather than escalating. ## What the app can see -ActiveBrowser receives every URL you open while it is your default browser, and it observes -which applications you focus. It does **not**: - -- send anything over the network — the app makes no network requests of any kind; -- write URLs to disk or log them persistently; -- inspect or match on URL contents (routing is based purely on focus order). - -Stored state is two `UserDefaults` keys — which browsers participate, and your fallback -browser — plus a launch-at-login opt-out flag. That's all, and it's local. +What ActiveBrowser sees, what it never does, and what it stores are described in one place: [README.md → Privacy](README.md#privacy). diff --git a/assets/README.md b/assets/README.md index 21928c6..6cd5609 100644 --- a/assets/README.md +++ b/assets/README.md @@ -13,6 +13,8 @@ Source of truth is SVG; the binary formats below are generated from it. ## Regenerating +Run these from `assets/`; the paths are relative. + ```sh rm -rf /tmp/AppIcon.iconset && mkdir -p /tmp/AppIcon.iconset for s in 16 32 128 256 512; do @@ -32,14 +34,13 @@ pointer inside it. The app icon adds the receding stack and colour; the menu bar monochrome reduction — the app icon's tinted title-bar strip becomes a divider line, since a template image has only black and alpha to work with. -## Not yet wired into the bundle +## How the bundle uses these -Nothing here is referenced by the build. `Project.md` Phase 3 and `tasks/03-bundle-makefile.md` -both state the bundle carries **no** `CFBundleIconFile` and **no** `Resources/` directory, and -task 03 verifies that `find build/ActiveBrowser.app -type f` lists exactly three files. Adopting -these assets is a spec change: it needs `CFBundleIconFile` in `Support/Info.plist`, a -`Contents/Resources/` copy step in `make bundle` (before `codesign`), and task 03's file-count -assertion updated. Do that as its own task, not as a drive-by edit. +`make bundle` copies `AppIcon.icns` and the three `MenuBarIconTemplate` PNGs into +`Contents/Resources/` before it runs `codesign`. `Support/Info.plist` names the app icon +through `CFBundleIconFile` (`AppIcon`), and `MenuBarManager` loads the menu bar glyph by +name, falling back to the `globe` SF Symbol when `Contents/Resources` is missing (an +unbundled build). The menu bar image must be loaded with `isTemplate = true` so macOS tints it for light/dark menu bars and for the highlighted state. diff --git a/tasks/17-readme-restructure.md b/tasks/17-readme-restructure.md new file mode 100644 index 0000000..5c6d38f --- /dev/null +++ b/tasks/17-readme-restructure.md @@ -0,0 +1,233 @@ +# Task 17 — Restructure README for users; move contributor material to CONTRIBUTING + +Status: in-review +Phase: 6 +Branch: chore/readme-restructure +Base: main +PR: +Created: 2026-09-23 + +## Goal +Make `README.md` the single user-facing landing doc (why, install, use, privacy, limits, uninstall), with every claim checked against the current code. Move contributor-only material into `CONTRIBUTING.md` without duplicating anything and without losing any fact. Docs only: no Swift, `install.sh`, Makefile or workflow changes. + +## Spec (planner) + +Scope: `.md` files only: `README.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CHANGELOG.md`, `assets/README.md`. These root docs already exist, so editing them does not trigger the guardrail §3 "outside §4" re-plan rule. `Project.md` needs **no** change, because §4 does not reference README sections (checked: its only README mention is `assets/README.md`, line 224). + +### Fact-check results (planner verified these against the code on `main`; implementer applies them) +| # | README today | Code says | Source | +|---|---|---|---| +| F1 | "Click the globe icon" | The status item shows the bundled ActiveBrowser mark (browser window with a pointer, template image). The globe SF Symbol is only a fallback when `Contents/Resources` is missing (unbundled build). | `MenuBarManager.swift:61-65` | +| F2 | Menu mock-up: `Set as Default Browser` always shown; submenus drawn as `☑` / `● ○` | The row is **either** `Set as Default Browser` (clickable) **or** a checked, disabled `✓ Default Browser` when Launch Services resolves both `http` and `https` to ActiveBrowser. Both submenus use AppKit checkmarks (`✓`), with no radio glyphs. `Quit ActiveBrowser` has ⌘Q. Row order is otherwise correct. `Recent:` shows at most 3 names then ` › …`, and reads `Recent: none yet` when empty. `Routing to: none` when nothing resolves. | `MenuBarManager.swift:108-135, 199-229, 306-315` | +| F3 | "Untick the browser currently set as fallback and it moves to the next one" | It moves to the **first remaining ticked browser in menu order** (installed browsers by name, A→Z). | `MenuBarManager.swift:351-355, 263-267` | +| F4 | Unticked browser: "links will never go there" | This is not strictly true. The last-resort routing step is every installed handler by name, and it does **not** filter on `includedBrowsers`. An unticked browser can receive a link only when the focus history is empty **and** the fallback browser is gone from disk. | `URLDispatcher.swift:50-56` | +| F5 | Routing step 4 "the first browser it can find" | Precisely: the first installed `https` handler by display name, even an unticked one. Every candidate must exist on disk, and ActiveBrowser's own bundle id is dropped from every step (case-insensitive). | `URLDispatcher.swift:38-64` | +| F6 | "The last remaining ticked browser can't be unticked" | Correct. The row is disabled (greyed out), and the action re-checks the rule. Keep it. | `MenuBarManager.swift:185-194, 347` | +| F7 | (missing) *Browsers* contents | It lists every app registered to open `https` links, which can include non-browsers. Each app appears once. | `BrowserRegistry.swift`, `MenuBarManager.swift:241-244` | +| F8 | (missing) first-launch defaults | First launch ticks every detected browser and sets the fallback to the first one by name. | `AppDelegate.swift:88-93` | +| F9 | (missing) Launch at Login behaviour | The app registers itself as a login item on launch, but only when run from `/Applications/` and only if the user hasn't opted out. Unticking in the menu is remembered. A mixed (`–`) state means it was switched off in System Settings, and clicking the row then opens System Settings → Login Items. | `AppDelegate.swift:123-157`, `MenuBarManager.swift:319-325, 400-428` | +| F10 | (missing) focus history persistence | The *Recent* order lives only in memory. After relaunch or login it is empty, so links go to the Fallback Browser until a browser is focused. | `BrowserStack.swift` (no persistence) | +| F11 | "`http`/`https` only" | Schemes: correct. The app also declares itself a **low-priority** (`Alternate`) viewer for `.html`/`.xhtml` files, which macOS requires before it will list it as a browser. So it can appear in Finder's *Open With*, and any file it receives is routed the same way. | `Support/Info.plist:27-72`, `AppDelegate.swift:69-74` | +| F12 | Privacy (new) | No networking API anywhere in `Sources/`: no `URLSession`, `NWConnection`, `Network` or `CFNetwork`. `https://example.com` in the code is only a Launch Services lookup key and is never fetched (`BrowserRegistry.swift:17-20`, `MenuBarManager.swift:288`). No URL is ever logged. The only dispatch log line records a **count** (`URLDispatcher.swift:28`), and every other `NSLog` covers login-item or set-default errors. `UserDefaults` domain `com.local.activebrowser` holds exactly `includedBrowsers`, `defaultBrowser`, `launchAtLoginOptOut` (`Settings.swift:7-11`). | — | +| F13 | Installer network contact | The one-liner itself fetches `raw.githubusercontent.com`. The script calls `api.github.com` (latest tag, skipped when `ACTIVEBROWSER_VERSION` is set) and `github.com/hexember/active-browser/releases/…`, which redirects to GitHub's release-asset CDN. **Do not name the CDN host.** With `ACTIVEBROWSER_ZIP` it makes no network access at all. It never touches user defaults, login items or the default-browser binding, and never uses `sudo`. | `install.sh:5-8, 18-19, 129-155` | +| F14 | "`Project.md` ... departs from it in three places" | There are more than three. The code also differs from `Project.md` §5 in the status item (icon plus tooltip, not the target name as title), the `URLDispatcher` type the menu reads, and `Info.plist`'s HTML document types. The three named ones are still the ones most likely to be "restored" by mistake, so reword to "several places, notably these three"; where they disagree, the code is correct. | `Project.md` §5 vs `Sources/`, `Support/Info.plist` | +| F15 | Repository layout omits `assets/` and `.github/` | Both are real and shipped: `make bundle` copies `assets/AppIcon.icns` and `assets/menubar/*.png`, and the CI and release workflows live in `.github/workflows/`. | `Makefile:29-32` | +| F16 | `assets/README.md` "Not yet wired into the bundle" | Stale since task 12: the Makefile copies the icons, `Info.plist` sets `CFBundleIconFile`, and `MenuBarManager` loads the glyph. | `Makefile:26-33`, `Info.plist:13-14` | + +Verified accurate and kept as written: the pitch, the install one-liner, the checksum and bundle validation, re-run as the upgrade path (the script `pkill`s first), macOS 13+, arm64 release builds with a clear refusal on Intel (`install.sh:193-198`), the Gatekeeper/quarantine explanation, the uninstall commands and `defaults` domain, the re-scan on menu open, settings persistence, and the never-dropped / logged / self-filtered claim. + +### Checklist +- [x] **`README.md`**: rewrite into exactly this heading outline (fenced code excluded). Existing headings keep their exact text so their anchors survive: + ``` + # ActiveBrowser + ## Why this exists + ## Install + ### Build from source + ## Using it + ### How a URL is routed + ## Privacy + ## Known limitations + ## Uninstall + ## Contributing + ## Changelog + ## License + ``` + - **Title block**: keep all four badges unchanged (the platform badge still links `#install`), plus the bold one-liner, the pitch paragraph, the Arc/Slack example block and the "never becomes the thing that displays a page" paragraph. + - **Why this exists**: keep the comparison table unchanged. Tighten the two "when … is the better choice" paragraphs into two short bullets and keep the closing *where does this URL belong? / where am I working?* sentence. Every tool name and every claim in the table stays. + - **Install**: + - the one-liner; + - the *Set as Default Browser* step, noting the row then reads *✓ Default Browser* (F2); + - one paragraph on what the script does: checks, SHA-256, bundle validation before replacing anything, safe re-run as upgrade, no `sudo`, and no changes to settings, login items or default browser (F13); + - one sentence that on first launch from `/Applications` the app adds itself as a login item so it is running before your first link click, with a pointer to *Using it* (F9); + - the **Requirements** line (unchanged facts); + - the Gatekeeper paragraph (unchanged facts) with a link to `SECURITY.md#what-youre-trusting-when-you-install-this`. + - **Build from source**: the clone + `make install` block (these three command lines may also appear in CONTRIBUTING), a note that it needs a Swift 6 toolchain and works on Intel, and a link to `CONTRIBUTING.md#build-and-run`. Remove the `
` block, the local-zip install and the `ACTIVEBROWSER_*` variables (moved; see CONTRIBUTING item). + - **Using it**: + - "Click the ActiveBrowser icon in the menu bar", with no mention of a globe (F1). The tooltip shows `Routing to: …`. + - A mock-up matching F2 exactly, in code order: `Routing to:` / `Recent:` / sep / `Browsers ▸` / `Fallback Browser ▸` / sep / `Set as Default Browser` (with an annotation that it becomes `✓ Default Browser`) / `Launch at Login` / sep / `Quit ActiveBrowser ⌘Q`. Submenu hints use `✓` only. + - **Browsers** paragraph with the F4, F6 and F7 corrections. + - **Fallback Browser** paragraph with the F3 correction. + - A new **Launch at Login** paragraph (F9). + - First-launch defaults (F8). + - One line: "Runs as a background agent: no Dock icon, no windows, about 12 MB of memory idle." This is the only place README states the figure. + - Keep "persist across restarts" and "re-scans each time you open it". + - **How a URL is routed**: the four steps with the F5 wording on step 4. Keep the never-dropped / logged / self-filtered paragraph. Add F10 (history is empty after relaunch) and F11 (`.html` files) as one sentence each. + - **Privacy** (new): short bullets with only F12/F13 facts. What it sees: every URL it is handed, plus which app is frontmost. What it does not do: no network requests of any kind, no telemetry, never writes URLs to disk or logs them. What it stores: the three keys, named, in `com.local.activebrowser`, all local. The focus order is kept in memory only. The installer's network contacts go in one sentence. **Must not** name a CDN host or claim anything not in F12/F13. + - **Known limitations** (moved up, one line each, deduplicated): + - Intel: released builds are arm64 only; build from source (link `#build-from-source`). + - Not notarized: install with the one-liner or from source, because a browser-downloaded zip is quarantined (link `#install`, no re-explanation). + - Routing ignores the URL: keep the link to `#why-this-exists`. + - `http`/`https` only (plus the F11 `.html` note, if not already stated in routing; state it once). + - Links go to the Fallback Browser until you focus a browser after launch (F10; state it once, either here or in routing). + - **Uninstall** (promoted from `###` to `##`; anchor `#uninstall` unchanged). Same commands and facts, reordered so the default-browser reset comes **first**. That way a link clicked mid-uninstall cannot relaunch the app via Launch Services. After the reset: `pkill`, `rm -rf`, optional `defaults delete`, then Login Items removal. Keep the "order matters" sentence. + - **Contributing**: one paragraph pointing to `CONTRIBUTING.md` for building, the macOS gotchas, architecture, design constraints, testing and releasing. Keep the Code of Conduct and SECURITY links. + - **Changelog** and **License**: unchanged. + - **Delete from README** (each item lands in CONTRIBUTING; see next item): the `## Development` section (dependency sentence, make block, bundle gotcha, `make run` gotcha, logging snippet), `### Architecture`, the design-constraints list, `### Testing`, `### Releasing`, `## Repository layout` including the history paragraph and the three deviations. +- [x] **`CONTRIBUTING.md`**: merge README's contributor material in with no duplicated text. Target outline: + ``` + # Contributing + ## Build and run + ## Three things that will cost you an hour if nobody tells you + ## Architecture + ## Repository layout + ## Project history + ## Testing + ## Design constraints + ## Pull requests + ## Releasing (maintainers) + ### Testing a release locally + ``` + - Keep the existing headings' exact text, because their anchors are linked from README. + - *Build and run*: add "Swift plus Apple frameworks only (`AppKit`, `Foundation`, `ServiceManagement`)". Keep the existing make table as the only copy in the repo. + - Gotcha 3: add that `make clean` **unregisters** and removes the build copy, and add this exact phrase: "`make install` and `make release` both delete `build/ActiveBrowser.app`". + - *Architecture*: README's tree verbatim, with its per-file one-liners. + - *Repository layout*: README's block plus `assets/` (icon artwork, see `assets/README.md`) and `.github/workflows/` (CI and release) (F15). + - *Project history*: README's "history, not instructions" paragraph verbatim, except the deviation sentence reworded per F14. Keep the three named deviations (`@main` entry point, `install:` target ordering, `SMAppService` status gate) and "Don't 'restore' it to match the document". Mention that `tasks/TEST-PLAN.md` is the historical manual checklist (README's Testing section named it). + - *Testing*: unchanged. README's Testing text is already covered here. + - *Design constraints*: add the "~12 MB" idle figure to the background-agent bullet, e.g. "idle footprint ~12 MB; keep it under 25 MB". + - *Releasing*: add that the workflow runs on `macos-latest` and verifies the artefacts before publishing. Keep the `v0.1.1` example and the asset-name contract. + - *Testing a release locally*: `make release`, then `ACTIVEBROWSER_ZIP=build/ActiveBrowser.app.zip sh install.sh`. Explain that `ACTIVEBROWSER_SUMS` defaults to the `SHA256SUMS` next to the zip, that `ACTIVEBROWSER_ZIP` means no network access, and that `ACTIVEBROWSER_VERSION` pins a tag. +- [x] **`SECURITY.md`**: README *Privacy* becomes the single source for what the app sees and stores. Keep the `## What the app can see` heading so its anchor survives, and replace its body with a one-sentence pointer to `README.md#privacy`. First confirm every fact in the old body (3 bullets + stored-state sentence) is in README *Privacy*. The other SECURITY sections stay unchanged. +- [x] **`CHANGELOG.md`**: under `## [Unreleased]` → `### Changed`, add: "README reorganized for users, with a new Privacy section; contributor material (architecture, repository layout, releasing, local release testing) moved to `CONTRIBUTING.md`." Nothing else changes. +- [x] **`assets/README.md`**: replace the stale `## Not yet wired into the bundle` section with a short `## How the bundle uses these` section (F16). Say that `make bundle` copies `AppIcon.icns` and the three `MenuBarIconTemplate` PNGs into `Contents/Resources` before `codesign`, that `Info.plist` names `AppIcon`, and that `MenuBarManager` loads the glyph with a `globe` SF Symbol fallback. Keep the `isTemplate = true` sentence. Add "run from `assets/`" above the regeneration commands, because the paths are relative. + +### Deliberately dropped (not carried anywhere) +- README Releasing example tag `v0.1.0`. It was an example value only, and CONTRIBUTING's `v0.1.1` example stays. +- The "Install from a local zip" framing as a user install route. The mechanism moves to CONTRIBUTING, because it exists for testing a release (`install.sh:26-28`). +- The `☑` / `●○` glyphs in the mock-up. They were wrong (F2). + +Nothing else may be dropped. Anything the implementer wants to remove beyond this list goes into Implementation Notes with a reason. + +Acceptance criteria: +- README and CONTRIBUTING heading outlines match the two outlines above exactly. +- Every fact in F1–F16 appears in the target file, and no stale claim remains (no "globe" in README, no "three places" without "notably", no "Not yet wired" in `assets/README.md`). +- The make targets table, the `make run` duplicate-registration gotcha, the logging snippet (`/tmp/ab.log`), the architecture tree, the history paragraph and the `ACTIVEBROWSER_*` variables each appear **only** in CONTRIBUTING. +- The privacy facts appear only in README (`SECURITY.md` points to them). +- Every relative link and `#anchor` in the five edited files resolves. README still links `#install` (badge) and `#why-this-exists`. +- The diff touches only `.md` files. `swift build` has no errors or warnings. + +### Architectural notes and risks +- **Push-back 1 (SECURITY duplication):** the main session asked for a new README Privacy section but did not mention `SECURITY.md`, which already has the same facts under *What the app can see*. Two copies will drift, so README becomes canonical and SECURITY gets a pointer. If the user prefers SECURITY as canonical, reverse the pointer. Either way, keep only one copy. +- **Push-back 2 (~12 MB):** the idle memory figure matters to users, so it stays in README once (*Using it*). CONTRIBUTING gets it as a design constraint (under 25 MB, today ~12 MB). This is the one deliberate repeat of a number. +- **Push-back 3 (routing claim):** "links will never go there" is false in one edge case (F4). The docs are corrected here. Whether the last-resort step *should* respect `includedBrowsers` is a code question for a separate task, not this PR. The guardrail "never drop a URL" argues it is correct as is. +- **Push-back 4 (Project.md deviations):** "three places" is stale (F14), so it is reworded rather than moved verbatim. +- Removed README anchors (`#development`, `#architecture`, `#testing`, `#releasing`, `#repository-layout`) have no in-repo linkers (grepped). External deep links to them will land at the top of README. This is accepted. +- The System Settings default-browser picker route (task 11, `public.html` claim) is **not** documented in Install, because task 11's UI confirmation (its user step 12) is still unrecorded. The menu route stays the only documented one. The uninstall text already points to Desktop & Dock for resetting. +- Out-of-scope finding for a follow-up task: `Support/Info.plist` `CFBundleShortVersionString` is `1.0.0` while releases are tagged `v0.1.x`, so `install.sh` prints "Installing ActiveBrowser 1.0.0". This is not fixed here (not `.md`). +- Out of scope: `Project.md` §4 lists only `release.yml` (not `ci.yml`) and no root docs. It is history per README, so it is left alone. + +## Implementation Notes (implementer) +Built (only `.md` files changed: `README.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CHANGELOG.md`, `assets/README.md`): +- **README**: rewritten to the exact 12-heading outline. Title block, badges and comparison table are unchanged. The two "better choice" paragraphs are now two bullets that keep every tool and claim. Install covers the one-liner, the Set as Default Browser → `✓ Default Browser` step, the script paragraph (F13), the login-item sentence (F9), Requirements, and Gatekeeper with a SECURITY anchor link. Build from source has the clone/`make install` block, a Swift 6 + Intel note, and a CONTRIBUTING link. Using it has the icon (no globe), the tooltip, the F2 mock-up, and the Browsers (F4/F6/F7), Fallback (F3), Launch at Login (F9), first-launch (F8) and ~12 MB paragraphs. Routing has step 4 per F5, the on-disk rule, and F10 + F11 once each. Privacy uses only F12/F13 and names no CDN host. Known limitations has 5 one-liners. Uninstall is `##` with the default-browser reset first. Contributing, Changelog and License follow. +- **CONTRIBUTING**: target outline exactly. Added the frameworks sentence, the gotcha-3 `make clean` unregister wording and the exact "both delete" phrase, the Architecture tree verbatim, the Repository layout with `assets/` and `.github/workflows/`, Project history (verbatim apart from the F14 rewording "several places, notably three…; where the two disagree, the code is correct", plus a `tasks/TEST-PLAN.md` mention), the ~12 MB / 25 MB constraint, `macos-latest` + verify-before-publish in Releasing, and *Testing a release locally* with all three `ACTIVEBROWSER_*` variables (checked against `install.sh:16-24`). +- **SECURITY**: `## What the app can see` kept, body replaced with a pointer to `README.md#privacy`. Every old fact is in README Privacy first: sees every URL, observes focus, no network requests, no URLs written to disk or logged, no URL-content matching, and the three stored keys. +- **CHANGELOG**: one bullet added under `[Unreleased]` → `### Changed` (text as specified, wrapped to the file's width). +- **assets/README**: `Not yet wired` section replaced by `## How the bundle uses these` (F16). Kept the `isTemplate = true` sentence and added "Run these from `assets/`" above the regeneration block. + +Decisions / small deviations: +- The old mock-up annotation for Fallback Browser ("used when nothing is running") was wrong. When nothing is running, step 2 picks the most recently focused browser (`BrowserStack.swift:38`). It now reads "used before you've focused a browser". This is not on the dropped list; it is a correction. +- Mock-up names use the registry display names (`Brave Browser`, not `Brave`), matching what `recentLine()` and the submenus render. Unchecked fallback rows show no glyph, the same as AppKit. +- CONTRIBUTING Design constraints: shortened the "No third-party dependencies" bullet to the bold phrase alone, because the "Swift plus Apple frameworks only" text now lives in *Build and run*. Keeping both would repeat the same text. +- CONTRIBUTING make table: the `make release` row now says `build/ActiveBrowser.app.zip`, carrying over the path from README's removed make block (`Makefile:61-63`). +- Gotcha 3 was rewrapped so "two ActiveBrowser rows" sits on one line (Test Step 3 greps for it). +- README Install says the script "quits the running app first", from `install.sh:236` (`pkill`), which is the fact behind "safe to run with the app open". +- F13's local-zip no-network fact appears in README as "Installing from a local zip makes no network access at all", without naming the variable (Test Step 2 requires `ACTIVEBROWSER_*` = 0 in README). +- No fact was dropped beyond the Spec's list. Self-check of Test Step 10 found none. + +Self-check of `ai` steps 1-10 (the main session still records the official Actual/Result): 1 both outlines match the Spec exactly · 2 CONTRIBUTING ≥1 and README 0 for every string (README has "about 12 MB") · 3 README 0 / CONTRIBUTING ≥1 for all 7 · 4 network requests README 1 / SECURITY 0, key and domain present, pointer at SECURITY.md:35, CDN count 0 · 5 every menu label ≥1, globe 0, glyphs 0, Not yet wired 0, no "three places" line · 6 `broken: 0`, `(#install)` 2, `(#why-this-exists)` 1 · 7 count 1, no removed lines in CHANGELOG · 8 only the five `.md` files plus the untracked task file. + +Build: `swift build` clean (no warnings or errors; `Build complete!`) · `make bundle` n/a (docs-only change; not run so that no `build/` copy gets registered with Launch Services, per the task's "System state: unchanged" precondition) + +## Review (code-reviewer) +Verdict: APPROVED +- Scope: `git status` shows only the five `.md` files plus this task file. `swift build` ran clean (no warnings or errors, `Build complete!`). I did not run `make bundle`: no source, `Support/` or Makefile input changed, so the bundle is identical to `main`'s, and building it would leave a `build/` copy that Launch Services registers, which breaks the "System state: unchanged" precondition. This is a deliberate skip. +- Fact-check against the code (all pass). Icon and globe fallback (`MenuBarManager.swift:61-65`). Tooltip text (`:101-103`). Menu rows, order, ⌘Q, and the disabled `✓ Default Browser` row (`:111-134`). `Recent:` cap, ` › …` and `none yet` (`:306-315`). `Routing to: none` (`:302`). Last ticked browser row disabled, and the action re-checks it (`:185-193, 347`). Unticking prunes the stack (`:350`). Fallback moves to the first ticked browser in menu order (`:351-355, 263-267`). Launch-at-Login states, including `.mixed` → dash → opens System Settings (`:319-325, 417-421`). Opt-out remembered (`AppDelegate.swift:127`). `/Applications` gate (`:125`). First-launch seeding (`:88-93`). The routing chain and last-resort registry step (`URLDispatcher.swift:38-64`). On-disk check (`:41-42`). Case-insensitive self-filter (`:58-60`). Only NSLog on dispatch logs a count (`:28`). No networking API in `Sources/` (grepped). The three keys and the domain (`Settings.swift:7-11`, `Info.plist:6`). `.html`/`.xhtml` declared as `Alternate` viewer (`Info.plist:46-72`). Files are routed through the same dispatcher (`AppDelegate.swift:69-74`). Installer facts: `install.sh:72, 81-84, 136, 142-153, 164-198, 236, 252`, no `sudo`, and the `latest/download` fallback is still under `github.com/hexember/active-browser/releases`. No CDN host is named. +- Content preservation: every removed README line is present in CONTRIBUTING, README-elsewhere, or on the dropped list. The only losses are the `v0.1.0` example and the local-zip framing, both on the dropped list. I checked the old SECURITY body (4 facts) against README *Privacy*: all four are there. +- Duplication: the `make run` gotcha, `/tmp/ab.log`, `log show`, `git tag`, `No polling`, the architecture tree and `ACTIVEBROWSER_*` each appear only in CONTRIBUTING (grepped: README 0). The `/tmp/ab.log` count of 2 comes from one snippet. The overlaps that remain are allowed by the Spec: the clone block, the ~12 MB figure, and README's Gatekeeper paragraph linking to SECURITY's longer version. +- Links: LINKCHECK `broken: 0`. Both outlines match the Spec exactly. The only inbound README anchor in the repo is `SECURITY.md:35` → `#privacy`, and it resolves. `#install` (badge + limitations), `#why-this-exists`, `#build-from-source` and `#using-it` resolve. +- Implementer deviations: all four are accepted. (1) The mock-up annotation "used before you've focused a browser" is correct, and the old "when nothing is running" was wrong per `BrowserStack.swift:38`. (2) `Brave Browser` matches the registry display names. (3) Shortening the CONTRIBUTING dependency bullet avoids repeating the new *Build and run* sentence. (4) The `build/ActiveBrowser.app.zip` path in the make table matches `Makefile:61`. +- Non-blocking nit: `README.md` (Browsers paragraph), the F4 exception says "if your focus history is empty". Precisely, the exception applies when no browser in the history *and* not the Fallback Browser is still on disk. The stack is not pruned when an app is deleted (`BrowserStack.prune` runs only on untick/bootstrap). This is harmless because the Spec's F4 uses the same simplification. Optional wording: "if none of your recent browsers nor your Fallback Browser is still installed". +- Non-blocking nit: `README.md` (Install), the script paragraph says it "never touches … your login items". The very next paragraph says the app adds a login item on its first launch, and the script performs that launch (`install.sh:252`). Both statements are literally true and adjacent, so a reader is unlikely to be misled. Optional: "the script itself never touches …". +- Non-blocking nit: `README.md` (How a URL is routed), "A URL is never dropped … it's logged rather than silently discarded" reads as self-contradictory to a first-time user. The Spec keeps this sentence verbatim, so leave it for now. +- Test Steps: all `ai` steps 1-10 can be run against the files as written. I ran 1, 3, 6, 8 and 9 (outline, dedup and scope greps, LINKCHECK, build) and all passed. User steps 11-12 can be satisfied: the badge → `#install`, the in-page links resolve, the mock-up matches the code's menu, and SECURITY's pointer lands on README *Privacy*. + +## Test Steps (planner writes; `ai` rows run by the main session before the PR opens, `user` rows by the human at the PR) + +**Preconditions** +- Build: none needed. This is a docs-only change and nothing is installed. All commands run from the repo root on branch `chore/readme-restructure`. +- System state: unchanged. +- Covers: task-specific (no `Project.md` §3 Verify block applies to documentation). +- `OUTLINE` below means: `awk '/^```/{f=!f;next} !f && /^#/' ` (headings outside fenced code). +- `LINKCHECK` below means: save this script as `/linkcheck.py` and run `python3 /linkcheck.py`: + ```python + import re, os, sys + FILES = ["README.md", "CONTRIBUTING.md", "SECURITY.md", "CHANGELOG.md", "assets/README.md"] + def slugs(path): + out, seen, fence = set(), {}, False + for line in open(path, encoding="utf-8"): + if line.startswith("```"): fence = not fence; continue + m = None if fence else re.match(r"^#{1,6}\s+(.*?)\s*#*\s*$", line) + if m: + s = re.sub(r"[^\w\- ]", "", m.group(1).strip().lower()).replace(" ", "-") + n = seen.get(s, 0); seen[s] = n + 1 + out.add(s if n == 0 else f"{s}-{n}") + return out + bad = 0 + for f in FILES: + text = re.sub(r"```.*?```", "", open(f, encoding="utf-8").read(), flags=re.S) + for t in re.findall(r"\]\(([^)\s]+)\)", text): + if re.match(r"(https?|mailto):", t): continue + path, _, anchor = t.partition("#") + target = os.path.normpath(os.path.join(os.path.dirname(f), path)) if path else f + if not os.path.exists(target): print(f"{f}: missing file -> {t}"); bad += 1; continue + if anchor and target.endswith(".md") and anchor not in slugs(target): + print(f"{f}: missing anchor -> {t}"); bad += 1 + print("broken:", bad); sys.exit(1 if bad else 0) + ``` + +| # | Who | Action (exact command / click) | Expected | Actual | Result | +|---|---|---|---|---|---| +| 1 | ai | `git show main:README.md > /README.before.md; git show main:CONTRIBUTING.md > /CONTRIBUTING.before.md`, then `diff <(OUTLINE on before) <(OUTLINE README.md)` and the same for CONTRIBUTING | After-outlines equal the two outlines in the Spec exactly. The diff shows only the planned additions/removals: README loses `Development`, `Architecture`, `Testing`, `Releasing`, `Repository layout` and the `
` other-ways block; `Uninstall` goes from `###` to `##`; `Privacy` and `### Build from source` are added. | outlines match Spec exactly; diff shows only the planned changes | pass | +| 2 | ai | Moved-fact presence. For each string, `grep -cF '' CONTRIBUTING.md` and `grep -cF '' README.md`: `AppDelegate.swift`, `FocusObserver.swift`, `BrowserRegistry.swift`, `BrowserStack.swift`, `Settings.swift`, `URLDispatcher.swift`, `MenuBarManager.swift`, `history, not instructions`, `` `@main` entry point ``, `` `install:` target ordering ``, `` `SMAppService` status gate ``, `tasks/TEST-PLAN.md`, `ACTIVEBROWSER_ZIP`, `ACTIVEBROWSER_SUMS`, `ACTIVEBROWSER_VERSION`, `macos-latest`, `` `make install` and `make release` both delete `build/ActiveBrowser.app` ``, `ServiceManagement`, `~12 MB`, `Support/`, `assets/`, `docs/`, `.github/workflows` | CONTRIBUTING ≥ 1 for every string. README = 0 for every string except `~12 MB`/`12 MB` (README ≥ 1 by design, push-back 2). | CONTRIBUTING ≥1 for all 23; README 0 for all except the idle figure, written as "about 12 MB" (README:97) | pass | +| 3 | ai | Zero-duplication. `grep -c` in README.md and CONTRIBUTING.md for: `` | `make run` ``, `/tmp/ab.log`, `two ActiveBrowser rows`, `log show`, `is \`nil\` outside a bundle`, `git tag`, `No polling` | README = 0 and CONTRIBUTING ≥ 1 for each. | README 0 / CONTRIBUTING ≥1 for all 7 | pass | +| 4 | ai | Privacy single-sourced: `grep -c 'network requests' README.md SECURITY.md`; `grep -c 'launchAtLoginOptOut' README.md`; `grep -c 'com.local.activebrowser' README.md`; `grep -n 'README.md#privacy' SECURITY.md`; `grep -Ec 'objects\.githubusercontent|release-assets' README.md` | README ≥ 1 and SECURITY = 0 for "network requests". Key and domain present in README. SECURITY has the pointer. No CDN host named (last count 0). | README 1 / SECURITY 0; key 1; domain 2; pointer at SECURITY:35; CDN 0 | pass | +| 5 | ai | Fact-check. For each of `Routing to:`, `Recent:`, `Browsers`, `Fallback Browser`, `Set as Default Browser`, `Default Browser`, `Launch at Login`, `Quit ActiveBrowser`, run `grep -cF` in README.md. Also run `grep -ci globe README.md`, `grep -c '☑\|●\|○' README.md`, `grep -c 'Not yet wired' assets/README.md`, `grep -n 'three places' CONTRIBUTING.md` | Each menu label ≥ 1 (labels match `MenuBarManager.swift` titles). `globe` = 0. Glyph count = 0. `Not yet wired` = 0. Any "three places" line also says "notably". | all labels ≥1; globe 0; glyphs 0; Not yet wired 0; no "three places" line | pass | +| 6 | ai | `python3 /linkcheck.py` (LINKCHECK), then `grep -c '(#install)' README.md` and `grep -c '(#why-this-exists)' README.md` | `broken: 0`, exit 0. Both anchor counts ≥ 1. | `broken: 0`, exit 0; #install 2; #why-this-exists 1 | pass | +| 7 | ai | `awk '/^## \[Unreleased\]/,/^## \[0.1.0\]/' CHANGELOG.md \| grep -c 'README reorganized'` | `1`. `git diff main -- CHANGELOG.md` shows only added lines. | `1`; no removed lines | pass | +| 8 | ai | `git diff --name-only main` plus `git status --porcelain` | Only `README.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CHANGELOG.md`, `assets/README.md`, `tasks/17-readme-restructure.md` (and `tasks/TEST-PLAN.md` if updated). All end in `.md`. `install.sh`, `Sources/`, `Makefile`, `.github/`, `Project.md` untouched. | only the 5 .md files + untracked task file | pass | +| 9 | ai | `swift build 2>&1 \| grep -E 'warning:\|error:'; swift build 2>&1 \| tail -1` | First command prints nothing. Last line is `Build complete!` | only the 2 CommandLineTools `ld` search-path lines (known noise); `Build complete!` | pass | +| 10 | ai | Spec "Deliberately dropped" audit: open `git diff main -- README.md`, and for every removed line containing a fact, confirm it is in CONTRIBUTING/SECURITY/README-elsewhere or on the dropped list | No unlisted fact lost. List any exception in Implementation Notes. | reviewer's line-by-line audit + steps 2–4 greps: no unlisted fact lost | pass | +| 11 | user | On the GitHub PR, open *Files changed* → `README.md` → *Display the rendered blob*. Read top to bottom, click the platform badge and the *Why this exists* / *Build from source* / *Install* links. | The page reads as a user landing doc. The badge jumps to *Install*. All in-page links land on the right heading. The menu mock-up matches what you see when you click the menu bar icon (with the `✓ Default Browser` row if ActiveBrowser is your default). No developer-only sections remain. | | pass / fail | +| 12 | user | Same for `CONTRIBUTING.md` and `SECURITY.md` (rendered) | CONTRIBUTING reads in order: build → gotchas → architecture → layout → history → testing → constraints → PRs → releasing. Nothing is said twice. SECURITY's *What the app can see* link opens README *Privacy*. | | pass / fail | + +**Reset after testing** +- none (docs only; scratchpad files are disposable) + +**Result (ai rows):** pass — 2026-09-23 (steps 1–10, run by main session after the post-review wording fixes) +**Result (user rows):** +Failures: + +## Next +Follow-up candidates found while planning (separate tasks, not this PR): align `CFBundleShortVersionString` (`1.0.0`) with release tags (`v0.1.x`); decide whether routing's last-resort step should skip unticked browsers (F4); once task 11's picker step is confirmed, document the System Settings route in README *Install*. + +_2026-09-23 (main session, after APPROVED):_ I applied the reviewer's three wording nits in `README.md`. Install now says "The script itself never uses `sudo`…". The Browsers exception now reads "none of the browsers in your focus history is still installed". The first sentence of the routing section is reworded so "never dropped" doesn't contradict "logged". No facts changed. I re-ran the ai steps after the edits. From bc73aff79404c0a180e940224baf15d8a7a88b95 Mon Sep 17 00:00:00 2001 From: tajpuriya27 Date: Wed, 23 Sep 2026 01:13:21 +0545 Subject: [PATCH 2/6] chore: record PR link for task 17 The task file is the shared state between agents, so the PR URL is recorded there for the main session and reviewers. Co-Authored-By: Claude Opus 5.5 --- tasks/17-readme-restructure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/17-readme-restructure.md b/tasks/17-readme-restructure.md index 5c6d38f..0fd6140 100644 --- a/tasks/17-readme-restructure.md +++ b/tasks/17-readme-restructure.md @@ -4,7 +4,7 @@ Status: in-review Phase: 6 Branch: chore/readme-restructure Base: main -PR: +PR: https://github.com/hexember/active-browser/pull/29 Created: 2026-09-23 ## Goal From 22b80cb3b72c5be646950d69b67e483e1217fd18 Mon Sep 17 00:00:00 2001 From: tajpuriya27 Date: Wed, 23 Sep 2026 01:13:42 +0545 Subject: [PATCH 3/6] chore: set task 17 pr-open and append its user steps to TEST-PLAN Co-Authored-By: Claude Opus 5.5 --- tasks/17-readme-restructure.md | 2 +- tasks/TEST-PLAN.md | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/tasks/17-readme-restructure.md b/tasks/17-readme-restructure.md index 0fd6140..95e2135 100644 --- a/tasks/17-readme-restructure.md +++ b/tasks/17-readme-restructure.md @@ -1,6 +1,6 @@ # Task 17 — Restructure README for users; move contributor material to CONTRIBUTING -Status: in-review +Status: pr-open Phase: 6 Branch: chore/readme-restructure Base: main diff --git a/tasks/TEST-PLAN.md b/tasks/TEST-PLAN.md index 1f7371a..abae8ca 100644 --- a/tasks/TEST-PLAN.md +++ b/tasks/TEST-PLAN.md @@ -296,6 +296,19 @@ https://github.com/hexember/active-browser/pull/24 --- +## PR #29 — Task 17: README restructured for users (`chore/readme-restructure`, base `main`) + +**Preconditions:** none. This is a docs-only change; read it on the PR. + +| # | Action | Expected | +|---|---|---| +| 1 | On the GitHub PR, open *Files changed* → `README.md` → *Display the rendered blob*. Read top to bottom, click the platform badge and the *Why this exists* / *Build from source* / *Install* links. | The page reads as a user landing doc. The badge jumps to *Install*. All in-page links land on the right heading. The menu mock-up matches what you see when you click the menu bar icon (with the `✓ Default Browser` row if ActiveBrowser is your default). No developer-only sections remain. | +| 2 | Same for `CONTRIBUTING.md` and `SECURITY.md` (rendered) | CONTRIBUTING reads in order: build → gotchas → architecture → layout → history → testing → constraints → PRs → releasing. Nothing is said twice. SECURITY's *What the app can see* link opens README *Privacy*. | + +**Reset after this block:** none. + +--- + # Final teardown — run this only when you are finished with everything above Order matters; doing it out of order lets the login item come back. From 7798c9be6d0e26a480bf5d2d66d74446341aec96 Mon Sep 17 00:00:00 2001 From: tajpuriya27 Date: Wed, 23 Sep 2026 01:27:15 +0545 Subject: [PATCH 4/6] docs: add README contents line and links back to the repo Co-Authored-By: Claude Opus 5.5 --- README.md | 8 +++++--- tasks/17-readme-restructure.md | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 43d25fa..85965c2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![ci](https://github.com/hexember/active-browser/actions/workflows/ci.yml/badge.svg)](https://github.com/hexember/active-browser/actions/workflows/ci.yml) [![release](https://img.shields.io/github/v/release/hexember/active-browser?sort=semver)](https://github.com/hexember/active-browser/releases/latest) -[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/hexember/active-browser/blob/main/LICENSE) [![platform: macOS 13+](https://img.shields.io/badge/platform-macOS%2013%2B-lightgrey.svg)](#install) **A macOS menu bar agent that opens every link in the browser you were just using.** @@ -16,6 +16,8 @@ Testing in Brave → click a link in Slack → opens in Brave That's the whole idea. ActiveBrowser never becomes the thing that displays a page; it receives the URL, decides which real browser should get it, hands it over, and gets out of the way. +**Contents:** [Why](#why-this-exists) · [Install](#install) · [Using it](#using-it) · [Privacy](#privacy) · [Limitations](#known-limitations) · [Uninstall](#uninstall) · [Contributing](#contributing) · [Source on GitHub](https://github.com/hexember/active-browser) + --- ## Why this exists @@ -152,7 +154,7 @@ Order matters. Reset your default browser first, so a link clicked mid-uninstall ## Contributing -Contributions are welcome. [CONTRIBUTING.md](CONTRIBUTING.md) covers building, the macOS gotchas that will otherwise cost you an hour, the architecture, the design constraints, testing, and releasing. +Source code, issues and releases live at [github.com/hexember/active-browser](https://github.com/hexember/active-browser). Contributions are welcome. [CONTRIBUTING.md](CONTRIBUTING.md) covers building, the macOS gotchas that will otherwise cost you an hour, the architecture, the design constraints, testing, and releasing. Please also read the [Code of Conduct](CODE_OF_CONDUCT.md). For security issues, see [SECURITY.md](SECURITY.md) — report privately, not in a public issue. @@ -163,4 +165,4 @@ See [CHANGELOG.md](CHANGELOG.md). ## License -[MIT](LICENSE). +[MIT](https://github.com/hexember/active-browser/blob/main/LICENSE). diff --git a/tasks/17-readme-restructure.md b/tasks/17-readme-restructure.md index 95e2135..335a957 100644 --- a/tasks/17-readme-restructure.md +++ b/tasks/17-readme-restructure.md @@ -231,3 +231,5 @@ Failures: Follow-up candidates found while planning (separate tasks, not this PR): align `CFBundleShortVersionString` (`1.0.0`) with release tags (`v0.1.x`); decide whether routing's last-resort step should skip unticked browsers (F4); once task 11's picker step is confirmed, document the System Settings route in README *Install*. _2026-09-23 (main session, after APPROVED):_ I applied the reviewer's three wording nits in `README.md`. Install now says "The script itself never uses `sudo`…". The Browsers exception now reads "none of the browsers in your focus history is still installed". The first sentence of the routing section is reworded so "never dropped" doesn't contradict "logged". No facts changed. I re-ran the ai steps after the edits. + +_2026-09-23 (main session, follow-up commit on PR #29 at the user's request):_ Added a one-line **Contents** nav under the pitch, linking Why, Install, Using it, Privacy, Limitations, Uninstall, Contributing and Source on GitHub. Also added a "Source code, issues and releases" repo link under Contributing. README is also rendered on GitHub Pages, and the Pages site has neither GitHub's outline button nor a way back to the repo. The two `LICENSE` links (badge and License section) are now absolute GitHub URLs, because Jekyll's relative-link rewriting only handles `.md` files and `LICENSE` would not resolve on the site. Link check: `broken: 0`. Outline unchanged, so step 1 still passes. From 6d11851ec4844dc9ddec4046fe60de0c7c5381f7 Mon Sep 17 00:00:00 2001 From: tajpuriya27 Date: Wed, 23 Sep 2026 09:51:41 +0545 Subject: [PATCH 5/6] docs: managed change log --- CHANGELOG.md | 41 +++-------------------------------------- 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d440b7..eca144c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,41 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **The repository moved to the `hexember` GitHub account.** Every link, the - `install.sh` release source and the install one-liner now point to - `hexember/active-browser`. Re-run the one-liner from the README to pick up the new URL. -- README reorganized for users, with a new Privacy section; contributor material - (architecture, repository layout, releasing, local release testing) moved to - `CONTRIBUTING.md`. - -### Fixed - -- **The menu bar icon never appeared.** `MenuBarManager` was never constructed, so no - `NSStatusItem` was created: no icon, no *Browsers* / *Fallback Browser* submenus, no - *Set as Default Browser*, no *Quit*, and no login-item registration at launch. URL - routing kept working the whole time — `application(_:open:)` bootstraps itself — which - made the app look half-alive rather than broken. ([#17](https://github.com/hexember/active-browser/pull/17)) -- **`make release` did nothing.** The target was listed in `.PHONY` but its recipe was - absent, so the release workflow produced no artefacts and `v0.1.0` published without - them. ([#16](https://github.com/hexember/active-browser/pull/16)) - -Both defects were introduced by merges that dropped a hunk while keeping the surrounding -code, so everything still compiled and every branch-level check passed. - ### Added -- Continuous integration on every push and pull request: builds both configurations, - assembles and signs the bundle, runs `make release`, verifies the checksum, and asserts - the wiring that the two fixes above restored. -- `LICENSE` (MIT), `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`, issue and pull - request templates. -## [0.1.0] - 2026-09-22 +## [1.0.0] - 2026-09-23 Initial release. ### Added +- `LICENSE` (MIT), `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`, issue and pull + request templates. - Routes every `http`/`https` link to the most recently focused browser. - Menu bar agent (`LSUIElement`) showing the live routing target and recent focus order. - *Browsers* submenu to include or exclude browsers, with a guard that prevents the @@ -53,14 +29,3 @@ Initial release. - *Set as Default Browser* and *Launch at Login*. - `install.sh` one-liner installer: verifies the SHA-256 checksum and validates the bundle before replacing anything on disk. -- `make release` and a tag-triggered GitHub Actions workflow publishing - `ActiveBrowser.app.zip` and `SHA256SUMS`. - -### Known issues - -> **This release is not usable.** The published binary has no menu bar icon, so none of -> the menu features above can be reached. Link routing works. Use a build from `main` -> until the next release. - -[Unreleased]: https://github.com/hexember/active-browser/compare/v0.1.0...HEAD -[0.1.0]: https://github.com/hexember/active-browser/releases/tag/v0.1.0 From 81101608ef5dfe25df69340c055fff17f403f430 Mon Sep 17 00:00:00 2001 From: tajpuriya27 Date: Wed, 23 Sep 2026 10:02:21 +0545 Subject: [PATCH 6/6] docs: managed change log --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 85965c2..8cfaf35 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![ci](https://github.com/hexember/active-browser/actions/workflows/ci.yml/badge.svg)](https://github.com/hexember/active-browser/actions/workflows/ci.yml) [![release](https://img.shields.io/github/v/release/hexember/active-browser?sort=semver)](https://github.com/hexember/active-browser/releases/latest) -[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/hexember/active-browser/blob/main/LICENSE) +[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![platform: macOS 13+](https://img.shields.io/badge/platform-macOS%2013%2B-lightgrey.svg)](#install) **A macOS menu bar agent that opens every link in the browser you were just using.** @@ -165,4 +165,4 @@ See [CHANGELOG.md](CHANGELOG.md). ## License -[MIT](https://github.com/hexember/active-browser/blob/main/LICENSE). +[MIT](LICENSE).