- Move from top-level domain-skills/tasksquad.ai/ to the canonical agent-workspace/domain-skills/tasksquad-ai/ location, switching to kebab-case to match sibling dirs (booking-com, dev-to, archive-org). - Replace find_text_coords(...) with self-contained inline Python helpers (_coords_by_text, _coords_by_selector) that wrap querySelector + getBoundingClientRect. find_text_coords does not exist in the harness or agent_helpers — every call site was a NameError waiting to happen. - Fix the compose-task snippet: drop the dead subject_input = js(...) line, focus the subject input via _coords_by_selector before typing, and add a placeholder click for the agent dropdown item. - Fix the polling-interval contradiction: the inbox section now defers to the gotchas, and the gotchas list the Free (5s) / Pro (2s) cadence in one place.
3.0 KiB
TaskSquad — Authentication
Field-tested against tasksquad.ai on 2026-05-03 using a Chrome session.
URLs
https://tasksquad.ai/auth # login page (redirects to /dashboard if already signed in)
https://tasksquad.ai/auth/cli # CLI token auth page
Login page
The login page is a centered card with two OAuth buttons. No email/password form exists — SSO only.
goto_url("https://tasksquad.ai/auth")
wait_for_load()
Sign-in buttons
The card contains exactly two buttons:
| Visible label | Provider |
|---|---|
| "Continue with Google" | |
| "Continue with GitHub" | GitHub |
Both are <button type="button"> inside a <div class="px-6 pb-6 flex flex-col gap-3">. Locate by label text and click coordinates returned from the rendered element:
import json
def click_button_by_text(label):
"""Find a <button> by exact innerText and click its center."""
sel = json.dumps(label)
result = js(f"""
var b = Array.from(document.querySelectorAll("button"))
.find(e => (e.innerText || "").trim() === {sel});
if (!b) return null;
var r = b.getBoundingClientRect();
return JSON.stringify({{x: Math.round(r.x + r.width/2), y: Math.round(r.y + r.height/2)}});
""")
if result is None:
raise RuntimeError(f"button not found: {label!r} — has the React app finished hydrating?")
pos = json.loads(result)
click_at_xy(pos["x"], pos["y"])
# Google
click_button_by_text("Continue with Google")
# GitHub
click_button_by_text("Continue with GitHub")
After clicking, a browser popup opens for the OAuth flow. Stop and ask the user to complete sign-in — do not attempt to interact with the OAuth popup window.
Auth-wall pattern
Any route under /dashboard/* redirects to /auth when the user is not signed in. If you navigate to a dashboard URL and land on /auth, the session is unauthenticated — ask the user to sign in before proceeding.
Redirect after sign-in
Firebase Auth resolves the session client-side. After the OAuth popup closes, React's onAuthStateChanged fires and automatically navigates the user to /dashboard. Do not click anything — just wait_for_load() and verify with page_info().
Sign-out
There is no dedicated sign-out page. The sign-out action is triggered from the sidebar in /dashboard. Look for a LogOut icon button in the bottom-left sidebar area.
Gotchas
- The page is a React SPA. Navigating to
/authwhile the app is still hydrating returns the bare<div id="root">with no buttons yet. Always callwait_for_load()and verify the "Continue with Google" text is present before clicking. - No error routes exist for failed OAuth. On failure, the card renders an inline
<p class="text-sm text-red-500 text-center">error message below the buttons. authed === nullloading state. While Firebase resolves the session on/auth, the<Login>component is not rendered (the route rendersnull). Wait for buttons to appear before clicking.