Skip to content
Open
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
21 changes: 21 additions & 0 deletions .github/workflows/bonedigger.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: bonedigger

on:
issues:
types: [opened, labeled, closed]
issue_comment:
types: [created]
schedule:
- cron: '0 9 * * *'

permissions:
issues: write
contents: read

jobs:
bonedigger:
uses: projectbluefin/bonedigger/.github/workflows/lifecycle.yml@main
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pin the reusable workflow to a commit SHA or release tag.

The workflow references @main, which can change without notice. This creates supply chain risk and prevents reproducible builds. Best practice is to pin to a specific commit SHA.

🔒 Recommended fix
-    uses: projectbluefin/bonedigger/.github/workflows/lifecycle.yml@main
+    uses: projectbluefin/bonedigger/.github/workflows/lifecycle.yml@<commit-sha>

Replace <commit-sha> with the full commit hash from the bonedigger repository's main branch, or use a release tag like @v1.0.0 if available.

🧰 Tools
🪛 zizmor (1.25.2)

[error] 17-17: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[warning] 17-17: secrets unconditionally inherited by called workflow (secrets-inherit): this reusable workflow

(secrets-inherit)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/bonedigger.yml at line 17, The workflow currently imports
the reusable workflow with the mutable ref "uses:
projectbluefin/bonedigger/.github/workflows/lifecycle.yml@main"; update that ref
to a fixed commit SHA or a released tag (e.g., replace "`@main`" with
"@<full-commit-sha>" or "`@vX.Y.Z`") so the reusable workflow import is pinned and
builds are reproducible and auditable.

with:
brand_name: "Common"
brand_emoji: "⚙️"
secrets: inherit
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Explicitly pass only required secrets instead of inheriting all secrets.

secrets: inherit grants the reusable workflow access to all repository secrets, violating the principle of least privilege. If the external workflow is compromised, all secrets are exposed.

🛡️ Recommended approach

First, verify which secrets the reusable workflow actually requires, then pass them explicitly:

-    secrets: inherit
+    secrets:
+      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      # Add other required secrets explicitly
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
secrets: inherit
secrets:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Add other required secrets explicitly
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/bonedigger.yml at line 21, Replace the broad "secrets:
inherit" usage with an explicit secrets map: determine which repository secrets
the reusable workflow actually needs, remove "secrets: inherit", and add a
"secrets:" block that lists each required secret by name and maps it to the
corresponding repository secret (e.g., SECRET_NAME: ${{ secrets.SECRET_NAME }}).
Update the reusable workflow call site that currently contains "secrets:
inherit" so it only passes those specific secret keys, ensuring least-privilege
access.

342 changes: 342 additions & 0 deletions .github/workflows/hive-status-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,342 @@
name: Hive Status Sync

on:
push:
branches: [main]
schedule:
- cron: '0 * * * *'
workflow_dispatch:

jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- name: Fetch Hive snapshot and post project status
env:
GH_TOKEN: ${{ secrets.PROJECT_TOKEN }}
run: |
Comment on lines +17 to +20
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move the missing-token guard to the top of the step.

This only skips at Line 225, after the workflow has already fetched the external snapshot and queried GitHub. During the documented post-merge window before PROJECT_TOKEN is added, any failure there turns an intended no-op into a red run.

Proposed fix
           import re, json, subprocess, urllib.request, sys, os
           from datetime import datetime, timezone

+          if not os.environ.get("GH_TOKEN"):
+              print("WARNING: PROJECT_TOKEN secret is not set — skipping project status post", file=sys.stderr)
+              sys.exit(0)
+
           SNAPSHOT_URL  = "https://raw.githubusercontent.com/kubestellar/docs/main/public/live/hive/bluefin/index.html"
           PROJECT_ID    = "PVT_kwDOCCE0ds4BLZBC"
           DASHBOARD_URL = "https://kubestellar.io/live/hive/bluefin/"
@@
-          # Guard: skip posting if PROJECT_TOKEN is not configured
-          if not os.environ.get("GH_TOKEN"):
-              print("WARNING: PROJECT_TOKEN secret is not set — skipping project status post", file=sys.stderr)
-              sys.exit(0)
-
           # Post to GitHub Project status update

Also applies to: 31-40, 103-130, 224-227

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/hive-status-sync.yml around lines 17 - 20, The step "Fetch
Hive snapshot and post project status" currently checks for the presence of
PROJECT_TOKEN late in the script, after external snapshot fetches and GH API
queries; move that missing-token guard to the top-level of the step so the whole
step is skipped before any run actions when secrets.PROJECT_TOKEN is absent.
Update the step's top-level condition (the step that declares name: "Fetch Hive
snapshot and post project status" and env: GH_TOKEN) to short-circuit execution
if PROJECT_TOKEN is missing, and apply the same change to the other steps with
the same pattern (the blocks around lines 31-40, 103-130, and 224-227) so no
network calls or API queries happen when PROJECT_TOKEN is not provided.

python3 << 'PYEOF'
import re, json, subprocess, urllib.request, sys, os
from datetime import datetime, timezone

SNAPSHOT_URL = "https://raw.githubusercontent.com/kubestellar/docs/main/public/live/hive/bluefin/index.html"
PROJECT_ID = "PVT_kwDOCCE0ds4BLZBC"
DASHBOARD_URL = "https://kubestellar.io/live/hive/bluefin/"
REPO = "projectbluefin/common"
ISSUES_URL = f"https://github.com/{REPO}/issues"

# Fetch snapshot
req = urllib.request.Request(SNAPSHOT_URL, headers={"User-Agent": "hive-status-sync/1.0"})
with urllib.request.urlopen(req, timeout=30) as r:
html = r.read().decode("utf-8", errors="replace")

# Extract embedded agents JSON
m = re.search(r'"agents":\s*(\[.*?\])\s*,\s*"(?:governor|repos|token)', html, re.DOTALL)
if not m:
print("ERROR: could not find agents JSON in snapshot", file=sys.stderr)
sys.exit(1)

agents = json.loads(m.group(1))

# Summarize agent states
active = [a for a in agents if a.get("state") == "running"]
working = [a for a in active if a.get("busy") == "working"]
stopped = [a for a in agents if a.get("state") != "running"]
total = len(agents)
n_active = len(active)

# Determine project status
if n_active >= 3:
status = "ON_TRACK"
elif n_active >= 1:
status = "AT_RISK"
else:
status = "OFF_TRACK"

# Formation health bar (colorblind-safe: shape + fill)
filled = round((n_active / total) * 10) if total > 0 else 0
health_bar = "█" * filled + "░" * (10 - filled)

# Formation headline
if n_active >= 3:
formation_status = "Formation coherent"
elif n_active >= 1:
formation_status = "Coverage reduced"
else:
formation_status = "Formation broken"

# Relative time helper
def rel_time(ts_str):
if not ts_str:
return "—"
try:
ts = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
delta = datetime.now(timezone.utc) - ts
s = int(delta.total_seconds())
if s < 60: return f"{s}s ago"
if s < 3600: return f"{s // 60}m ago"
if s < 86400: return f"{s // 3600}h ago"
return f"{s // 86400}d ago"
except Exception:
return ts_str

# Clean liveSummary (strip box-drawing, compress blank lines)
def clean_summary(text):
if not text:
return ""
cleaned = re.sub(r'[┃│╔╗╚╝╠╣═─┌┐└┘├┤┬┴┼|]', '', text)
cleaned = re.sub(r'/data/agents/\S+', '', cleaned)
cleaned = re.sub(r'─+\s*\n\s*❯\s*\n\s*─+', '', cleaned)
cleaned = re.sub(r'v[\d.]+ available.*', '', cleaned)
cleaned = re.sub(r'\n{3,}', '\n\n', cleaned)
lines = [l.rstrip() for l in cleaned.splitlines()]
lines = [l for l in lines if len(l.strip()) > 2]
return '\n'.join(lines).strip()

# Get supervisor summary (most informative)
supervisor = next((a for a in agents if a.get("role") == "supervisor"), None)
summary_text = clean_summary(supervisor.get("liveSummary", "")) if supervisor else ""

# Fetch CI status and issue counts
def gh_json(*args):
r = subprocess.run(["gh"] + list(args), capture_output=True, text=True)
if r.returncode != 0:
return None
try:
return json.loads(r.stdout)
except Exception:
return None

ci_label = "⏳ pending"
ci_runs = gh_json("run", "list", "--repo", REPO, "--workflow", "build.yml",
"--limit", "1", "--json", "conclusion,url")
if ci_runs:
run = ci_runs[0]
conclusion = run.get("conclusion", "")
url = run.get("url", "")
if conclusion == "success":
ci_label = f"[✅ build stable]({url})"
elif conclusion in ("failure", "startup_failure"):
ci_label = f"[❌ build degraded]({url})"
else:
ci_label = f"[⏳ build running]({url})"
Comment on lines +114 to +125
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh run list --help | sed -n '/--branch/,+2p;/--event/,+2p'

Repository: projectbluefin/common

Length of output: 443


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/hive-status-sync.yml"
echo "== Target sections =="
nl -ba "$FILE" | sed -n '90,150p'
echo
nl -ba "$FILE" | sed -n '250,330p'

echo "== Find gh run list calls =="
rg -n 'gh_json\("run", "list"|gh\("run", "list"|workflow", "build\.yml"|build\.yml' "$FILE"

Repository: projectbluefin/common

Length of output: 148


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/hive-status-sync.yml"
echo "== Target sections =="
cat -n "$FILE" | sed -n '90,160p'
echo
cat -n "$FILE" | sed -n '250,330p'

echo
echo "== Find gh run list calls =="
rg -n 'gh_json\("run", "list"|gh\("run", "list"|' "$FILE"
rg -n '"workflow", *"build\.yml"' "$FILE"
rg -n '--branch|--event' "$FILE"

Repository: projectbluefin/common

Length of output: 21785


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/build.yml"
echo "== build.yml triggers =="
if [ -f "$FILE" ]; then
  cat -n "$FILE" | sed -n '1,160p'
else
  echo "Missing $FILE"
fi

Repository: projectbluefin/common

Length of output: 4085


Scope CI lookup to main (avoid picking the newest PR build).

Both CI lookups in .github/workflows/hive-status-sync.yml list the newest build.yml run repo-wide (no --branch/--event), so a failing PR/feature-branch run can flip the status body and project title even when main is still green.

Proposed fix
-          ci_runs  = gh_json("run", "list", "--repo", REPO, "--workflow", "build.yml",
-                             "--limit", "1", "--json", "conclusion,url")
+          ci_runs  = gh_json("run", "list", "--repo", REPO, "--workflow", "build.yml",
+                             "--branch", "main", "--event", "push",
+                             "--limit", "1", "--json", "conclusion,url")
-          ci_raw = gh("run", "list", "--repo", REPO, "--workflow", "build.yml",
-                      "--limit", "1", "--json", "conclusion")
+          ci_raw = gh("run", "list", "--repo", REPO, "--workflow", "build.yml",
+                      "--branch", "main", "--event", "push",
+                      "--limit", "1", "--json", "conclusion")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ci_runs = gh_json("run", "list", "--repo", REPO, "--workflow", "build.yml",
"--limit", "1", "--json", "conclusion,url")
if ci_runs:
run = ci_runs[0]
conclusion = run.get("conclusion", "")
url = run.get("url", "")
if conclusion == "success":
ci_label = f"[✅ build stable]({url})"
elif conclusion in ("failure", "startup_failure"):
ci_label = f"[❌ build degraded]({url})"
else:
ci_label = f"[⏳ build running]({url})"
ci_runs = gh_json("run", "list", "--repo", REPO, "--workflow", "build.yml",
"--branch", "main", "--event", "push",
"--limit", "1", "--json", "conclusion,url")
if ci_runs:
run = ci_runs[0]
conclusion = run.get("conclusion", "")
url = run.get("url", "")
if conclusion == "success":
ci_label = f"[✅ build stable]({url})"
elif conclusion in ("failure", "startup_failure"):
ci_label = f"[❌ build degraded]({url})"
else:
ci_label = f"[⏳ build running]({url})"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/hive-status-sync.yml around lines 114 - 125, The CI lookup
currently grabs the latest workflow run repo-wide and can pick PR or
feature-branch builds; update the gh_json("run", "list", ...) call that
populates ci_runs so it is scoped to the default branch by adding the branch
filter (e.g., --branch main) or equivalent flag, ensuring ci_runs (and the
subsequent run/conclusion/url logic) only considers runs from main rather than
the newest repo-wide run.


def count_label(label):
raw = gh_json("issue", "list", "--repo", REPO,
"--label", label, "--state", "open", "--json", "number")
return len(raw) if raw else 0
Comment on lines +127 to +130
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh issue list --help | sed -n '/--limit/,+2p'

Repository: projectbluefin/common

Length of output: 247


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/hive-status-sync.yml"
echo "== file =="
wc -l "$FILE"

echo "== lines 110-170 =="
nl -ba "$FILE" | sed -n '110,170p'

echo "== lines 270-330 =="
nl -ba "$FILE" | sed -n '270,330p'

echo "== occurrences of issue list + json number =="
rg -n 'gh(_json)?\("issue",\s*"list"' "$FILE" || true
rg -n '--limit' "$FILE" || true
rg -n 'paginate' "$FILE" || true

Repository: projectbluefin/common

Length of output: 200


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/hive-status-sync.yml"

echo "== lines 110-170 =="
sed -n '110,170p' "$FILE" | cat -n

echo "== lines 270-330 =="
sed -n '270,330p' "$FILE" | cat -n

echo "== search for issue list calls =="
rg -n 'gh(_json)?\("issue",\s*"list"' "$FILE" || true
rg -n 'gh(_json)?\("issue",\s*"list"' "$FILE" -n || true

echo "== search for --limit usage =="
rg -n -- '--limit' "$FILE" || true

echo "== search for --paginate usage =="
rg -n -- '--paginate' "$FILE" || true

echo "== search for helper wrapper definitions (gh_json/gh) =="
rg -n 'function\s+gh_json|gh_json\s*\(\)|function\s+gh\b|gh\s*\(\)|REPO=' "$FILE" || true

Repository: projectbluefin/common

Length of output: 6506


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE=".github/workflows/hive-status-sync.yml"

echo "== search for gh_json implementation =="
rg -n --hidden --no-messages 'gh_json' "$FILE" || true

echo "== search for subprocess.run calls that include gh arguments =="
rg -n --hidden --no-messages 'subprocess\.run\(\["gh"' "$FILE" || true

echo "== show broader context around first gh_json occurrence =="
first=$(rg -n 'gh_json' "$FILE" | head -n 1 | cut -d: -f1)
start=$(( first-40 ))
end=$(( first+40 ))
sed -n "${start},${end}p" "$FILE" | cat -n

Repository: projectbluefin/common

Length of output: 4846


🌐 Web query:

gh issue list --paginate --json number output behavior default --limit 30

💡 Result:

The command gh issue list --paginate --json number --limit 30 functions by fetching issues from the specified repository while applying the following behaviors: 1. Limit: The --limit 30 flag sets the total number of issues to fetch [1][2]. Even though 30 is the default limit for gh issue list, explicitly setting it ensures the command retrieves exactly that many results [1][3]. 2. Pagination: The --paginate flag instructs the GitHub CLI to automatically follow pagination links (API responses) to fetch all pages of results until the specified limit is reached [4][5]. While --limit 30 typically requires only one API request, --paginate ensures that if the API's default page size were smaller than the limit (or if you increased the limit beyond a single page's capacity), the CLI would continue requesting subsequent pages until the limit is met [5]. 3. JSON Output: The --json number flag constrains the command's output to a JSON array containing only the number field for each issue [4]. This is useful for scripts or tools (like jq) that require structured data rather than the default text-based display [4]. In summary, this command will output a JSON array of up to 30 issue numbers, automatically handling any necessary pagination to reach that total count [4][5].

Citations:


🌐 Web query:

gh issue list --limit 1000 without --paginate fetch more than first page

💡 Result:

When you use gh issue list with a specific --limit (such as 1000), the GitHub CLI automatically handles the pagination required to fetch that many results for you [1][2]. There is no --paginate flag for the gh issue list command itself; it is not necessary because the command internally performs the sequence of API requests needed to fulfill the requested limit [1][2]. Key details regarding this behavior include: * Automatic Pagination: When you set a limit (e.g., --limit 1000), the CLI executes as many sequential API requests as necessary to retrieve that number of issues [1][2]. You do not need to manually handle pages or use a separate flag to enable this [2]. * Performance Considerations: Fetching a large number of items will take longer because it requires multiple network requests [1][2]. For example, fetching 1,000 items involves at least 10 API calls, which may result in a noticeable delay [1]. * Limits: While you can set a high limit (e.g., --limit 9999) to retrieve a large set of issues, be aware of underlying API rate limits and the time required to complete the request [2][3]. * Alternative for Custom APIs: The --paginate flag exists specifically for the gh api command [4][5][3], which allows you to make raw API requests and automatically iterate through all pages of results until no more are available [4][1]. This is distinct from gh issue list, which abstracts this process away through the --limit parameter [1][2]. In summary, you should simply use --limit 1000 (or your desired number) with gh issue list, and the CLI will manage the underlying pagination automatically [1][2].

Citations:


🌐 Web query:

gh issue list --limit maximum value

💡 Result:

The GitHub CLI (gh) command gh issue list does not have a strict "maximum value" for the --limit flag [1]. The flag accepts any positive integer [1]. While there is no enforced software ceiling on the number you can pass, setting an exceptionally high limit does not bypass the limitations of the underlying GitHub API [1][2]. The command performs pagination automatically to fetch the requested number of items, which means that requesting a very large number of issues will result in multiple API calls, potentially slowing down the command or hitting API rate limits [1][2]. There is no "unlimited" or "all" setting for this flag [1]. If you need to retrieve all issues regardless of count, you must specify a number sufficiently high to cover the total, though this practice is generally discouraged in favor of working with paginated results or smaller, targeted batches [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE=".github/workflows/hive-status-sync.yml"

echo "== usages of count_label results =="
rg -n 'n_ready|n_claimed|n_p0|queue/agent-ready|queue/claimed|P0 🔥|todo\.projectbluefin\.io' "$FILE" || true

echo "== show section where title is built (around todo.projectbluefin.io) =="
line=$(rg -n 'todo\.projectbluefin\.io' "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
  start=$(( line-40 )); end=$(( line+20 ))
  sed -n "${start},${end}p" "$FILE" | cat -n
fi

Repository: projectbluefin/common

Length of output: 3348


Prevent truncated queue counts when a label has >30 open issues

count_label() uses gh issue list ... --json number and returns len(...). gh issue list defaults to --limit 30, so any label with more than 30 open issues is undercounted, causing the posted status/title to drift.

Proposed fix
-          raw = gh_json("issue", "list", "--repo", REPO,
-                        "--label", label, "--state", "open", "--json", "number")
+          raw = gh_json("issue", "list", "--repo", REPO,
+                        "--label", label, "--state", "open", "--limit", "1000", "--json", "number")
-              raw = gh("issue", "list", "--repo", REPO,
-                       "--label", label, "--state", "open", "--json", "number")
+              raw = gh("issue", "list", "--repo", REPO,
+                       "--label", label, "--state", "open", "--limit", "1000", "--json", "number")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def count_label(label):
raw = gh_json("issue", "list", "--repo", REPO,
"--label", label, "--state", "open", "--json", "number")
return len(raw) if raw else 0
def count_label(label):
raw = gh_json("issue", "list", "--repo", REPO,
"--label", label, "--state", "open", "--limit", "1000", "--json", "number")
return len(raw) if raw else 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/hive-status-sync.yml around lines 127 - 130, count_label()
undercounts because gh issue list defaults to --limit 30; update the gh_json
call in count_label to include an explicit high limit (e.g. add "--limit",
"1000") so the full set of open issues for the given label is returned before
taking len(raw); change the call in the count_label function that invokes
gh_json("issue", "list", "--repo", REPO, "--label", label, "--state", "open",
"--json", "number") to include the --limit argument to prevent truncation.


n_p0 = count_label("P0")

# Fireteam — unique contributors from merged PRs (last 50), excluding bots
BOT_SUFFIXES = ("[bot]", "-bot", "copilot", "renovate", "dependabot")
fireteam_raw = gh_json("pr", "list", "--repo", REPO, "--state", "merged",
"--limit", "50", "--json", "author,body")
ghosts = set()
humans = set()
if fireteam_raw:
for pr in fireteam_raw:
login = (pr.get("author") or {}).get("login", "")
if not login:
continue
low = login.lower()
if any(low.endswith(s) or s in low for s in BOT_SUFFIXES):
continue
body = pr.get("body") or ""
if "[x] I am using an agent" in body or "[X] I am using an agent" in body:
ghosts.add(login)
else:
humans.add(login)
humans -= ghosts
fireteam_names = (
[f"👻 {u}" for u in sorted(ghosts)] +
[u for u in sorted(humans)]
)

# Agent roster table
roster_rows = []
for a in sorted(agents, key=lambda x: x.get("sortOrder", 99)):
emoji = a.get("emoji", "")
name = a.get("displayName", a.get("name", ""))
state = a.get("state", "unknown")
busy = a.get("busy", "")
model = a.get("model", "")
last_seen = rel_time(a.get("lastKick", ""))
next_ck = a.get("nextKick", "—")

if state == "running" and busy == "working":
status_icon = "🔵 ▶ active"
elif state == "running":
status_icon = "🟡 ⏸ standby"
else:
status_icon = "⬛ ✕ offline"

roster_rows.append(f"| {emoji} {name} | {status_icon} | {model} | {last_seen} | {next_ck} |")

roster_table = "\n".join([
"| Agent | Status | Model | Last Active | Next |",
"|---|---|---|---|---|",
] + roster_rows)

# Timestamp
now_utc = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")

# Status bar
p0_fragment = f" · ▲ {n_p0} P0" if n_p0 > 0 else ""
status_bar = f"`[{health_bar}]` {formation_status} · {n_active}/{total} active · {ci_label}{p0_fragment}"

# Active agent names for header
header = f"**{n_active}/{total} active**"
if working:
working_names = " · ".join(
f"{a.get('emoji', '')} {a.get('name', '')}" for a in working
)
header += f" — {working_names} working"

# Assemble body
body_parts = [status_bar, "", header, ""]

if summary_text:
body_parts += ["### What the team is working on", "", summary_text, ""]

body_parts += ["### Agent roster", "", roster_table, ""]

if fireteam_names:
fireteam_line = "**The Fireteam:** " + " · ".join(fireteam_names)
if ghosts:
fireteam_line += " _(👻 agent-assisted)_"
body_parts += ["", fireteam_line]

body_parts += [
f"_Updated hourly · {now_utc} · [Live dashboard]({DASHBOARD_URL}) · [Issue queue]({ISSUES_URL})_",
]

body = "\n".join(body_parts)

print(f"Status: {status}")
print(f"Body preview ({len(body)} chars):")
print(body[:500])
print("...")

# Guard: skip posting if PROJECT_TOKEN is not configured
if not os.environ.get("GH_TOKEN"):
print("WARNING: PROJECT_TOKEN secret is not set — skipping project status post", file=sys.stderr)
sys.exit(0)

# Post to GitHub Project status update
mutation = """
mutation($projectId: ID!, $body: String!, $status: ProjectV2StatusUpdateStatus!) {
createProjectV2StatusUpdate(input: {
projectId: $projectId
body: $body
status: $status
}) {
statusUpdate {
id
createdAt
}
}
}
"""

result = subprocess.run(
["gh", "api", "graphql",
"-f", f"query={mutation}",
"-f", f"projectId={PROJECT_ID}",
"-f", f"body={body}",
"-f", f"status={status}"],
capture_output=True, text=True
)

if result.returncode != 0 or '"errors"' in result.stdout:
print("ERROR posting status update:", file=sys.stderr)
print(result.stdout, file=sys.stderr)
print(result.stderr, file=sys.stderr)
sys.exit(1)

resp = json.loads(result.stdout)
update_id = resp["data"]["createProjectV2StatusUpdate"]["statusUpdate"]["id"]
print(f"Posted status update: {update_id}")
PYEOF
shell: bash

- name: Update project title with live stats
env:
GH_TOKEN: ${{ secrets.PROJECT_TOKEN }}
run: |
python3 << 'PYEOF'
import json, subprocess, sys, os

PROJECT_ID = "PVT_kwDOCCE0ds4BLZBC"
REPO = "projectbluefin/common"

if not os.environ.get("GH_TOKEN"):
print("WARNING: PROJECT_TOKEN not set — skipping title update", file=sys.stderr)
sys.exit(0)

def gh(*args):
r = subprocess.run(["gh"] + list(args), capture_output=True, text=True)
if r.returncode != 0:
print(f"ERROR: gh {' '.join(args)}\n{r.stderr}", file=sys.stderr)
sys.exit(1)
return r.stdout.strip()

# CI status: latest completed build.yml run
ci_raw = gh("run", "list", "--repo", REPO, "--workflow", "build.yml",
"--limit", "1", "--json", "conclusion")
ci_runs = json.loads(ci_raw)
if ci_runs and ci_runs[0].get("conclusion") == "success":
ci = "CI ✅"
elif ci_runs and ci_runs[0].get("conclusion") in ("failure", "startup_failure"):
ci = "CI ❌"
else:
ci = "CI ⏳"

# Issue queue counts
def count_label(label):
raw = gh("issue", "list", "--repo", REPO,
"--label", label, "--state", "open", "--json", "number")
return len(json.loads(raw))

n_ready = count_label("queue/agent-ready")
n_claimed = count_label("queue/claimed")
n_p0 = count_label("P0")

# Build title
parts = [ci, f"{n_ready} ready", f"{n_claimed} claimed"]
if n_p0 > 0:
parts.append(f"{n_p0} P0 🔥")
title = "todo.projectbluefin.io — " + " · ".join(parts)

print(f"Setting title: {title}")

mutation = """
mutation($projectId: ID!, $title: String!) {
updateProjectV2(input: { projectId: $projectId, title: $title }) {
projectV2 { title }
}
}
"""

result = subprocess.run(
["gh", "api", "graphql",
"-f", f"query={mutation}",
"-f", f"projectId={PROJECT_ID}",
"-f", f"title={title}"],
capture_output=True, text=True
)

if result.returncode != 0 or '"errors"' in result.stdout:
print("ERROR updating title:", file=sys.stderr)
print(result.stdout, file=sys.stderr)
print(result.stderr, file=sys.stderr)
sys.exit(1)

resp = json.loads(result.stdout)
new_title = resp["data"]["updateProjectV2"]["projectV2"]["title"]
print(f"Title updated to: {new_title}")
PYEOF
shell: bash
Loading