Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions .github/workflows/flow-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Dashboard flow checks: can a user still finish each core product flow?
# See e2e/flows/README.md.
#
# Replays every e2e/flows/*/flow.spec.ts against a freshly booted stack. When a
# flow fails, a Claude Code agent re-walks that flow's story and says whether
# the UI only moved (healed), the flow broke, or the PR changed it on purpose.
# The verdicts go into one PR comment. This check never blocks a merge yet.
name: Flow checks

on:
workflow_dispatch:
pull_request:
paths:
- client/dashboard/**
- e2e/flows/**
- e2e/flows.config.ts
- .mise-tasks/test/flows.sh
- .mise-tasks/flows/**
- .github/workflows/flow-checks.yml

permissions:
contents: read

concurrency:
group: flow-checks-${{ github.ref }}
cancel-in-progress: true

jobs:
flow-checks:
name: flow-checks
# Booting the stack needs ATLAS_TOKEN and healing needs ANTHROPIC_API_KEY;
# a fork PR gets neither.
if: >-
github.event_name == 'workflow_dispatch' ||
github.event.pull_request.head.repo.full_name == github.repository
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 60
permissions:
contents: read
pull-requests: write # posts the verdict comment
env:
GOMAXPROCS: 8
ATLAS_TOKEN: ${{ secrets.ATLAS_TOKEN }}
steps:
- name: Checkout
uses: useblacksmith/checkout@6fd481652155169ed4d2f25ebaf97464f685175f # v1
with:
# The heal agent reads `git diff origin/main...HEAD` to tell an
# intended flow change from a broken one.
fetch-depth: 0

- name: Setup Mise
uses: jdx/mise-action@9149ea85001c7435d5a66bb127d6a1b6227cb0a5 # v5.0.0
with:
minimum_release_age: 1d
install: true
cache: true
cache_key_prefix: mise-blacksmith-v1
env: false

- name: Restore Go module cache
uses: ./.github/workflows/composite/go-mod-cache # zizmor: ignore[self-repository]

# Same answers as wake-stack-e2e.yml: zero must not prompt on a runner.
- name: Answer zero's questions before it asks them
run: |
cat > mise.local.toml <<'TOML'
[env]
CLICKHOUSE_MIGRATION_ENGINE = "golang-migrate"
TOML

- name: Give the seed someone to be
run: |
git config user.email "flow-checks@example.com"
git config user.name "Flow Checks"

- name: Boot the stack
run: ./zero --agent

- name: Replay flows
id: replay
continue-on-error: true
run: mise run test:flows

- name: Install Claude Code
if: steps.replay.outcome == 'failure'
run: curl -fsSL https://claude.ai/install.sh | bash

@cubic-dev-ai cubic-dev-ai Bot Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Flag Security Vulnerabilities

This executes a mutable remote installer in a job where ATLAS_TOKEN is available to every step. Pin and verify the installer, and scope ATLAS_TOKEN only to the stack-boot step.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .github/workflows/flow-checks.yml, line 87:

<comment>This executes a mutable remote installer in a job where `ATLAS_TOKEN` is available to every step. Pin and verify the installer, and scope `ATLAS_TOKEN` only to the stack-boot step.</comment>

<file context>
@@ -0,0 +1,152 @@
+
+      - name: Install Claude Code
+        if: steps.replay.outcome == 'failure'
+        run: curl -fsSL https://claude.ai/install.sh | bash
+
+      # One agent per failing flow. Each writes e2e/.flows/<flow>-verdict.json.
</file context>
Fix with cubic


# One agent per failing flow. Each writes e2e/.flows/<flow>-verdict.json.
- name: Heal failing flows
if: steps.replay.outcome == 'failure'
continue-on-error: true
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
failed=$(jq -r '[.. | objects | select(.specs?) | .specs[]
| select(.ok == false) | .file | split("/")[0]] | unique[]' \
e2e/.flows/results.json)
Comment on lines +96 to +98

@cubic-dev-ai cubic-dev-ai Bot Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The heal step never identifies a flow: it reads .file after .specs[], where Playwright’s JSON spec has no file, so jq fails and no failing flow is healed or reported. Keep the suite object while iterating its failed specs, then select the parent directory as the flow name.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .github/workflows/flow-checks.yml, line 96:

<comment>The heal step never identifies a flow: it reads `.file` after `.specs[]`, where Playwright’s JSON spec has no file, so `jq` fails and no failing flow is healed or reported. Keep the suite object while iterating its failed specs, then select the parent directory as the flow name.</comment>

<file context>
@@ -0,0 +1,152 @@
+        env:
+          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+        run: |
+          failed=$(jq -r '[.. | objects | select(.specs?) | .specs[]
+            | select(.ok == false) | .file | split("/")[0]] | unique[]' \
+            e2e/.flows/results.json)
</file context>
Suggested change
failed=$(jq -r '[.. | objects | select(.specs?) | .specs[]
| select(.ok == false) | .file | split("/")[0]] | unique[]' \
e2e/.flows/results.json)
failed=$(jq -r '[.. | objects | select(.specs?) | . as $suite
| $suite.specs[] | select(.ok == false) | $suite.file
| split("/") | .[-2]] | unique[]' \
e2e/.flows/results.json)
Fix with cubic

for flow in $failed; do
PATH="$HOME/.local/bin:$PATH" mise run flows:heal "$flow" &
done
wait

- name: Post verdicts
if: always() && github.event_name == 'pull_request'

@cubic-dev-ai cubic-dev-ai Bot Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: always() also runs after a cancellation, so a superseded run (cancelled by cancel-in-progress) still overwrites the sticky comment with its partial state. Use !cancelled() so only completed runs post.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .github/workflows/flow-checks.yml, line 105:

<comment>`always()` also runs after a cancellation, so a superseded run (cancelled by `cancel-in-progress`) still overwrites the sticky comment with its partial state. Use `!cancelled()` so only completed runs post.</comment>

<file context>
@@ -0,0 +1,152 @@
+          wait
+
+      - name: Post verdicts
+        if: always() && github.event_name == 'pull_request'
+        env:
+          GH_TOKEN: ${{ github.token }}
</file context>
Suggested change
if: always() && github.event_name == 'pull_request'
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
Fix with cubic

env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
REPLAY: ${{ steps.replay.outcome }}
run: |
marker='<!-- flow-checks -->'
{
echo "$marker"
echo "### Flow checks"
if [ "$REPLAY" = success ]; then
echo "Every flow replayed cleanly."
else
echo "| Flow | Verdict | Step | Summary |"
echo "| --- | --- | --- | --- |"
for f in e2e/.flows/*-verdict.json; do
[ -f "$f" ] || continue
jq -r '"| \(.flow) | **\(.verdict)** | \(.step // "-") | \(.summary) |"' "$f" ||
echo "| $(basename "$f" -verdict.json) | unknown | - | the agent did not return a verdict |"
done
echo
echo "_healed_: UI moved, flow still works; the new script is in the run artifacts."
echo "_broken_: a user can no longer finish the flow."
echo "_changed_: this PR changes the flow on purpose; update its story.md."
fi
echo
echo "[Run, traces and screenshots]($RUN_URL). This check does not block merging."
} > comment.md
id=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR/comments" --paginate \
--jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -n 1)
if [ -n "$id" ]; then
gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$id" -F body=@comment.md
else
gh pr comment "$PR" --body-file comment.md
fi

# Healed scripts, transcripts, traces and screenshots.
- name: Upload flow artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: flow-checks
path: |
e2e/.flows
e2e/flows/*/flow.spec.ts
retention-days: 7
if-no-files-found: ignore
106 changes: 106 additions & 0 deletions .github/workflows/magellan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Magellan: a nightly agent that explores the production dashboard as a bot
# user in a dedicated integration org, and files the bugs it can prove as
# Linear Triage issues. See e2e/magellan/README.md.
#
# The repo is public, so this workflow uploads no artifacts and logs no
# findings: screenshots and detail go to Linear only.
#
# Setup, in the `magellan` environment:
# - vars: MAGELLAN_SITE_URL (e.g. https://app.speakeasy.com), MAGELLAN_ORG
# - secrets: MAGELLAN_SESSION (the bot's `gram_session`), ANTHROPIC_API_KEY,
# MAGELLAN_LINEAR_API_KEY
name: Magellan

on:
workflow_dispatch:
inputs:
since:
description: Focus on dashboard changes merged since (git date)
default: 24 hours ago
file:
description: File Linear issues (otherwise dry run)
type: boolean
default: true
schedule:
# Explore on weeknights, after the day's merges.
- cron: "0 3 * * 2-6"
# Keep the bot's session alive. It expires after 72h without use, and
# the weekend gap alone is about that long.
- cron: "0 15 * * *"

permissions:
contents: read

concurrency:
group: magellan-${{ github.event.schedule || 'dispatch' }}

@cubic-dev-ai cubic-dev-ai Bot Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Scheduled and manual Magellan runs now land in different concurrency groups, so they can run at the same time against the same bot session and org. The dedupe in file.mts is search-then-create with no lock, so overlapping runs can file duplicate Linear issues. Serialize all explore runs in one group and move only keepalive out of it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .github/workflows/magellan.yml, line 35:

<comment>Scheduled and manual Magellan runs now land in different concurrency groups, so they can run at the same time against the same bot session and org. The dedupe in `file.mts` is search-then-create with no lock, so overlapping runs can file duplicate Linear issues. Serialize all explore runs in one group and move only keepalive out of it.</comment>

<file context>
@@ -13,24 +22,47 @@ on:
 
 concurrency:
-  group: magellan
+  group: magellan-${{ github.event.schedule || 'dispatch' }}
   cancel-in-progress: false
 
</file context>
Suggested change
group: magellan-${{ github.event.schedule || 'dispatch' }}
group: ${{ github.event.schedule == '0 15 * * *' && 'magellan-keepalive' || 'magellan' }}
Fix with cubic

cancel-in-progress: false

jobs:
keepalive:
name: keepalive
if: github.event.schedule == '0 15 * * *'
runs-on: blacksmith-2vcpu-ubuntu-2404
environment: magellan
timeout-minutes: 5
steps:
# Any authenticated call slides the session's 72h expiry. A failure
# here means the session is gone and someone has to capture a new one.
- name: Refresh the bot session
env:
SITE_URL: ${{ vars.MAGELLAN_SITE_URL }}
SESSION: ${{ secrets.MAGELLAN_SESSION }}
run: |
status=$(curl -s -o /dev/null -w '%{http_code}' \

@cubic-dev-ai cubic-dev-ai Bot Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Flag Security Vulnerabilities

This request sends the production MAGELLAN_SESSION in a header to an environment-supplied URL without restricting its scheme; an HTTP value would expose a replayable session credential. Restrict curl to HTTPS (for example, --proto '=https') before sending the secret.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .github/workflows/magellan.yml, line 53:

<comment>This request sends the production `MAGELLAN_SESSION` in a header to an environment-supplied URL without restricting its scheme; an HTTP value would expose a replayable session credential. Restrict curl to HTTPS (for example, `--proto '=https'`) before sending the secret.</comment>

<file context>
@@ -13,24 +22,47 @@ on:
+          SITE_URL: ${{ vars.MAGELLAN_SITE_URL }}
+          SESSION: ${{ secrets.MAGELLAN_SESSION }}
+        run: |
+          status=$(curl -s -o /dev/null -w '%{http_code}' \
+            -H "Gram-Session: $SESSION" "${SITE_URL%/}/rpc/auth.info")
+          if [ "$status" != 200 ]; then
</file context>
Suggested change
status=$(curl -s -o /dev/null -w '%{http_code}' \
+ status=$(curl --proto '=https' -s -o /dev/null -w '%{http_code}' \
Fix with cubic

-H "Gram-Session: $SESSION" "${SITE_URL%/}/rpc/auth.info")
if [ "$status" != 200 ]; then
echo "::error::The Magellan bot session is no longer valid (auth.info returned $status). Capture a new one; see e2e/magellan/README.md."
exit 1
fi
Comment on lines +55 to +58

@cubic-dev-ai cubic-dev-ai Bot Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Any non-200 response, including a 5xx from an outage, is reported as a dead session, which sends the operator to recapture a valid session. Only 401 and 403 mean the session is gone; other statuses should fail with a generic message. Consider adding --max-time to the curl call as well.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .github/workflows/magellan.yml, line 55:

<comment>Any non-200 response, including a 5xx from an outage, is reported as a dead session, which sends the operator to recapture a valid session. Only 401 and 403 mean the session is gone; other statuses should fail with a generic message. Consider adding `--max-time` to the curl call as well.</comment>

<file context>
@@ -13,24 +22,47 @@ on:
+        run: |
+          status=$(curl -s -o /dev/null -w '%{http_code}' \
+            -H "Gram-Session: $SESSION" "${SITE_URL%/}/rpc/auth.info")
+          if [ "$status" != 200 ]; then
+            echo "::error::The Magellan bot session is no longer valid (auth.info returned $status). Capture a new one; see e2e/magellan/README.md."
+            exit 1
</file context>
Suggested change
if [ "$status" != 200 ]; then
echo "::error::The Magellan bot session is no longer valid (auth.info returned $status). Capture a new one; see e2e/magellan/README.md."
exit 1
fi
if [ "$status" = 401 ] || [ "$status" = 403 ]; then
echo "::error::The Magellan bot session is no longer valid (auth.info returned $status). Capture a new one; see e2e/magellan/README.md."
exit 1
elif [ "$status" != 200 ]; then
echo "::error::auth.info returned $status. Retry, or check MAGELLAN_SITE_URL."
exit 1
fi
Fix with cubic


explore:
name: explore
if: github.event_name == 'workflow_dispatch' || github.event.schedule == '0 3 * * 2-6'
runs-on: blacksmith-4vcpu-ubuntu-2404
environment: magellan
timeout-minutes: 75
steps:
- name: Checkout
uses: useblacksmith/checkout@6fd481652155169ed4d2f25ebaf97464f685175f # v1
with:
fetch-depth: 0

- name: Setup Mise
uses: jdx/mise-action@9149ea85001c7435d5a66bb127d6a1b6227cb0a5 # v5.0.0
with:
minimum_release_age: 1d
install: true
cache: true
cache_key_prefix: mise-blacksmith-v1
env: false

- name: Install the browser
run: mise run playwright:install-browser

- name: Install Claude Code
run: curl -fsSL https://claude.ai/install.sh | bash

@cubic-dev-ai cubic-dev-ai Bot Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This mutable remote installer executes as a shell with the job's ATLAS_TOKEN; a compromised installer can exfiltrate the migration credential. Install a version-pinned, checksum-verified Claude artifact and scope ATLAS_TOKEN only to the boot step.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .github/workflows/magellan.yml, line 69:

<comment>This mutable remote installer executes as a shell with the job's `ATLAS_TOKEN`; a compromised installer can exfiltrate the migration credential. Install a version-pinned, checksum-verified Claude artifact and scope `ATLAS_TOKEN` only to the boot step.</comment>

<file context>
@@ -0,0 +1,96 @@
+        run: ./zero --agent
+
+      - name: Install Claude Code
+        run: curl -fsSL https://claude.ai/install.sh | bash
+
+      - name: Explore
</file context>
Fix with cubic


- name: Explore
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
MAGELLAN_SITE_URL: ${{ vars.MAGELLAN_SITE_URL }}
MAGELLAN_ORG: ${{ vars.MAGELLAN_ORG }}
MAGELLAN_SESSION: ${{ secrets.MAGELLAN_SESSION }}
SINCE: ${{ inputs.since || '24 hours ago' }}
run: PATH="$HOME/.local/bin:$PATH" mise run magellan:explore --since "$SINCE"

- name: File findings
env:
LINEAR_API_KEY: ${{ secrets.MAGELLAN_LINEAR_API_KEY }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
FILE: ${{ github.event_name == 'schedule' || inputs.file }}
run: |
if [ "$FILE" = true ]; then
mise run magellan:file --file --run-url "$RUN_URL"
else
mise run magellan:file --run-url "$RUN_URL"
fi
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ __pycache__/

# Traces, videos and screenshots from the e2e suite (mise run test:wake-stack)
e2e/.playwright
# Flow check results, traces and heal-agent transcripts (mise run test:flows)
e2e/.flows
# Magellan explorer runs: findings, screenshots, transcripts (mise run magellan:explore)
e2e/.magellan

# Riptide artifacts (cloud-synced)
.humanlayer/tasks/
Expand Down
29 changes: 29 additions & 0 deletions .mise-tasks/flows/heal.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash

#MISE description="Have a Claude Code agent re-walk a flow story, then heal its script or report the flow broken"
#MISE dir="{{ config_root }}"
#MISE depends=["ensure-stack"]
#USAGE arg "<flow>" help="Directory name under e2e/flows/, e.g. catalog-connect"

set -euo pipefail

flow="${usage_flow:?}"
if [ ! -f "e2e/flows/${flow}/story.md" ]; then
echo "No story at e2e/flows/${flow}/story.md" >&2
exit 1
fi

mkdir -p e2e/.flows
verdict_file="e2e/.flows/${flow}-verdict.json"

# The agent's last line is the verdict JSON (see e2e/flows/HEAL.md). Keep the
# full transcript for whoever reads the CI artifacts.
sed "s/{{FLOW}}/${flow}/g" e2e/flows/HEAL.md |
claude -p \
--permission-mode acceptEdits \
--allowedTools "Read,Write,Edit,Glob,Grep,Bash(mise run playwright:*),Bash(mise run test:flows:*),Bash(git diff:*),Bash(git log:*),Bash(aube run -F @gram/e2e type-check)" \
--max-turns 80 |
tee "e2e/.flows/${flow}-transcript.txt" |
tail -n 1 >"$verdict_file"

cat "$verdict_file"
77 changes: 77 additions & 0 deletions .mise-tasks/magellan/explore.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env bash

#MISE description="Magellan: let a Claude Code agent explore the dashboard and record bugs it can prove"
#MISE dir="{{ config_root }}"
#USAGE flag "--since <since>" default="24 hours ago" help="Focus on dashboard changes merged to main since this git date"
#USAGE flag "--budget <minutes>" default="45" help="Rough time budget for the agent"

# Two targets:
#
# - Local (default): this worktree's stack, logged in through dev-idp.
# - Remote: set MAGELLAN_SITE_URL, MAGELLAN_ORG and MAGELLAN_SESSION (a
# `gram_session` token for a dedicated bot user, captured from a real login).
# No login can be scripted against WorkOS, so the session is injected into
# the browser instead. See e2e/magellan/README.md.

set -euo pipefail

run_dir="e2e/.magellan/$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$run_dir"
touch "$run_dir/findings.jsonl"

if [ -n "${MAGELLAN_SITE_URL:-}" ]; then
: "${MAGELLAN_ORG:?MAGELLAN_ORG is required with MAGELLAN_SITE_URL}"
: "${MAGELLAN_SESSION:?MAGELLAN_SESSION is required with MAGELLAN_SITE_URL}"
site_url="${MAGELLAN_SITE_URL%/}"

@cubic-dev-ai cubic-dev-ai Bot Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag Security Vulnerabilities

Remote mode accepts an http:// MAGELLAN_SITE_URL, so the agent can send dashboard traffic without TLS. Reject non-HTTPS URLs before starting the browser.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .mise-tasks/magellan/explore.sh, line 25:

<comment>Remote mode accepts an `http://` `MAGELLAN_SITE_URL`, so the agent can send dashboard traffic without TLS. Reject non-HTTPS URLs before starting the browser.</comment>

<file context>
@@ -2,16 +2,51 @@
+if [ -n "${MAGELLAN_SITE_URL:-}" ]; then
+    : "${MAGELLAN_ORG:?MAGELLAN_ORG is required with MAGELLAN_SITE_URL}"
+    : "${MAGELLAN_SESSION:?MAGELLAN_SESSION is required with MAGELLAN_SITE_URL}"
+    site_url="${MAGELLAN_SITE_URL%/}"
+    org="$MAGELLAN_ORG"
+    pw="playwright-cli -s=magellan"
</file context>
Suggested change
site_url="${MAGELLAN_SITE_URL%/}"
site_url="${MAGELLAN_SITE_URL%/}"
case "$site_url" in
https://*) ;;
*) echo "MAGELLAN_SITE_URL must use https://" >&2; exit 1 ;;
esac
Fix with cubic

org="$MAGELLAN_ORG"
pw="playwright-cli -s=magellan"
allowed_pw="Bash(playwright-cli -s=magellan:*)"

# Outside the run directory, which gets uploaded: this file is a login.

@cubic-dev-ai cubic-dev-ai Bot Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This comment says the run directory gets uploaded, but magellan.yml uploads no artifacts. Reword it so the reason for keeping the state file outside the run directory matches the workflow.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .mise-tasks/magellan/explore.sh, line 30:

<comment>This comment says the run directory gets uploaded, but `magellan.yml` uploads no artifacts. Reword it so the reason for keeping the state file outside the run directory matches the workflow.</comment>

<file context>
@@ -2,16 +2,51 @@
+    pw="playwright-cli -s=magellan"
+    allowed_pw="Bash(playwright-cli -s=magellan:*)"
+
+    # Outside the run directory, which gets uploaded: this file is a login.
+    state_file="$(mktemp -t magellan-state.XXXXXX)"
+    trap 'rm -f "$state_file"' EXIT
</file context>
Suggested change
# Outside the run directory, which gets uploaded: this file is a login.
# Outside the run directory: this file is a login, so keep it out of the run's transcript and notes.
Fix with cubic

state_file="$(mktemp -t magellan-state.XXXXXX)"
trap 'rm -f "$state_file"' EXIT
host="$(node -e 'console.log(new URL(process.argv[1]).hostname)' "$site_url")"
node -e '
const [host, token, file] = process.argv.slice(1);
const cookie = { name: "gram_session", value: token, domain: host, path: "/",
expires: Math.floor(Date.now() / 1000) + 86400, httpOnly: true, secure: true, sameSite: "Lax" };
require("node:fs").writeFileSync(file, JSON.stringify({ cookies: [cookie], origins: [] }));
' "$host" "$MAGELLAN_SESSION" "$state_file"
login="You are already logged in as a bot user. Start with \`$pw open\`, \`$pw state-load $state_file\`, then \`$pw goto $site_url/$org\`. Never log out: it ends the bot's session for good. Do not read the state file."

@cubic-dev-ai cubic-dev-ai Bot Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This gives the Claude agent access to the production bearer session through the readable state file whose path is included in its prompt. Load the browser state before starting Claude and never expose the credential file or session token to the agent; also avoid passing the token in the node process arguments.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .mise-tasks/magellan/explore.sh, line 40:

<comment>This gives the Claude agent access to the production bearer session through the readable state file whose path is included in its prompt. Load the browser state before starting Claude and never expose the credential file or session token to the agent; also avoid passing the token in the `node` process arguments.</comment>

<file context>
@@ -2,16 +2,51 @@
+        expires: Math.floor(Date.now() / 1000) + 86400, httpOnly: true, secure: true, sameSite: "Lax" };
+      require("node:fs").writeFileSync(file, JSON.stringify({ cookies: [cookie], origins: [] }));
+    ' "$host" "$MAGELLAN_SESSION" "$state_file"
+    login="You are already logged in as a bot user. Start with \`$pw open\`, \`$pw state-load $state_file\`, then \`$pw goto $site_url/$org\`. Never log out: it ends the bot's session for good. Do not read the state file."
+else
+    mise run ensure-stack
</file context>
Fix with cubic

else
mise run ensure-stack
site_url="$GRAM_SITE_URL"
org="${MAGELLAN_ORG:-speakeasy}"
pw="mise run playwright -s=magellan"
allowed_pw="Bash(mise run playwright:*)"
login="Log in with the \"Log in\" button."
fi

# What changed recently is where new bugs are most likely. The agent maps
# these files to routes itself.
git fetch --quiet origin main || true
{
echo "# Dashboard changes on main since ${usage_since:?}"
echo
# Tests and the generated SDK change no page a user sees.
git log origin/main --since="$usage_since" --format='## %h %s' --name-only -- \

@cubic-dev-ai cubic-dev-ai Bot Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Flag Security Vulnerabilities

This passes main-branch commit subjects directly into focus.md, which the writable Claude agent is instructed to read. A crafted subject can prompt it to alter workspace files that the later filing step executes with LINEAR_API_KEY; omit commit subjects from this context or isolate the agent from files and secrets used by subsequent steps.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .mise-tasks/magellan/explore.sh, line 22:

<comment>This passes main-branch commit subjects directly into `focus.md`, which the writable Claude agent is instructed to read. A crafted subject can prompt it to alter workspace files that the later filing step executes with `LINEAR_API_KEY`; omit commit subjects from this context or isolate the agent from files and secrets used by subsequent steps.</comment>

<file context>
@@ -0,0 +1,35 @@
+    echo "# Dashboard changes on main since ${usage_since:?}"
+    echo
+    # Tests and the generated SDK change no page a user sees.
+    git log origin/main --since="$usage_since" --format='## %h %s' --name-only -- \
+        client/dashboard/src ':!client/dashboard/src/sdk' ':!*.test.ts' ':!*.test.tsx' |
+        grep -v '^$' || echo "(none — roam instead)"
</file context>
Fix with cubic

@cubic-dev-ai cubic-dev-ai Bot Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: If git log fails here, the fallback text 'none — roam instead' hides the error. Check the git log exit status separately, or let the failure surface, so a missing or stale origin/main is not mistaken for a quiet period.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .mise-tasks/magellan/explore.sh, line 22:

<comment>If `git log` fails here, the fallback text 'none — roam instead' hides the error. Check the `git log` exit status separately, or let the failure surface, so a missing or stale `origin/main` is not mistaken for a quiet period.</comment>

<file context>
@@ -0,0 +1,35 @@
+    echo "# Dashboard changes on main since ${usage_since:?}"
+    echo
+    # Tests and the generated SDK change no page a user sees.
+    git log origin/main --since="$usage_since" --format='## %h %s' --name-only -- \
+        client/dashboard/src ':!client/dashboard/src/sdk' ':!*.test.ts' ':!*.test.tsx' |
+        grep -v '^$' || echo "(none — roam instead)"
</file context>
Fix with cubic

client/dashboard/src ':!client/dashboard/src/sdk' ':!*.test.ts' ':!*.test.tsx' |
grep -v '^$' || echo "(none — roam instead)"
} >"$run_dir/focus.md"

# `#` delimits sed below, so none of the substituted values may contain one.
sed -e "s#{{RUN_DIR}}#${run_dir}#g" \
-e "s#{{BUDGET}}#${usage_budget:?}#g" \
-e "s#{{SITE_URL}}#${site_url}#g" \
-e "s#{{ORG}}#${org}#g" \
-e "s#{{PW}}#${pw}#g" \
-e "s#{{LOGIN}}#${login}#g" \
e2e/magellan/EXPLORE.md |
claude -p \
--permission-mode acceptEdits \
--allowedTools "Read,Write,Edit,Glob,Grep,${allowed_pw},Bash(git log:*),Bash(git diff:*),Bash(date:*)" \
--max-turns 200 \
>"$run_dir/transcript.txt"

echo "$run_dir"
wc -l <"$run_dir/findings.jsonl" | xargs echo "findings:"
Loading
Loading