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
122 changes: 122 additions & 0 deletions .github/workflows/jules-push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: Jules Push Monitor

on:
push:
branches:
- '**'

jobs:
# ── Detect whether any commit in this push is from JulesWyrm ──────────────
detect-jules:
runs-on: ubuntu-latest
outputs:
jules_pushed: ${{ steps.check.outputs.jules_pushed }}
commit_range: ${{ steps.check.outputs.commit_range }}
steps:
- name: Check commit authors
id: check
env:
COMMITS: ${{ toJson(github.event.commits) }}
BEFORE: ${{ github.event.before }}
AFTER: ${{ github.event.after }}
run: |
JULES_FOUND=$(echo "$COMMITS" | python3 -c "
import json, sys
commits = json.load(sys.stdin)
found = any(c.get('author', {}).get('username') == 'JulesWyrm' for c in commits)
print('true' if found else 'false')
")
Comment on lines +16 to +28
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

The github.event.commits payload isn’t guaranteed to include all commits in a push (it can be truncated on larger pushes), so this author check can miss JulesWyrm and skip the workflow. A more reliable approach is to checkout and run git log ${BEFORE}..${AFTER} (or use the GitHub compare API) to scan authors across the full commit range.

Suggested change
- name: Check commit authors
id: check
env:
COMMITS: ${{ toJson(github.event.commits) }}
BEFORE: ${{ github.event.before }}
AFTER: ${{ github.event.after }}
run: |
JULES_FOUND=$(echo "$COMMITS" | python3 -c "
import json, sys
commits = json.load(sys.stdin)
found = any(c.get('author', {}).get('username') == 'JulesWyrm' for c in commits)
print('true' if found else 'false')
")
- name: Check out repository history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check commit authors
id: check
env:
BEFORE: ${{ github.event.before }}
AFTER: ${{ github.event.after }}
run: |
AUTHOR_LINES=$(git log --format='%an%n%ae' "${BEFORE}..${AFTER}")
JULES_FOUND=$(printf '%s\n' "$AUTHOR_LINES" | grep -Eixq 'JulesWyrm|.*<juleswyrm>|juleswyrm@.*' && echo true || echo false)

Copilot uses AI. Check for mistakes.
echo "jules_pushed=$JULES_FOUND" >> "$GITHUB_OUTPUT"
echo "commit_range=${BEFORE}..${AFTER}" >> "$GITHUB_OUTPUT"

# ── Notify via Beeper ─────────────────────────────────────────────────────
notify:
needs: detect-jules
if: needs.detect-jules.outputs.jules_pushed == 'true'
runs-on: ubuntu-latest
steps:
- name: Send Beeper notification
# TODO: Confirm the following before enabling:
# 1. Base URL — if this is the Beeper Desktop API (localhost), it is not
# reachable from GitHub Actions. Confirm whether a cloud endpoint exists,
# or set up a proxy (e.g. a Vercel edge function that forwards to desktop).
# 2. Auth header — replace "Bearer" with the correct scheme if different.
# 3. accountID — add as a repo secret (BEEPER_ACCOUNT_ID).
# 4. user.id — your own Beeper user ID to start a self-DM (BEEPER_SELF_USER_ID).
#
# API shape (from https://developers.beeper.com/desktop-api-reference/resources/chats/methods/create):
# POST /v1/chats
# {
# "accountID": "<your account id>",
# "mode": "start",
# "user": { "id": "<your user id>" },
# "messageText": "<notification text>"
# }
env:
BEEPER_API_KEY: ${{ secrets.BEEPER_API_KEY }}
BEEPER_BASE_URL: ${{ vars.BEEPER_BASE_URL }}
BEEPER_ACCOUNT_ID: ${{ secrets.BEEPER_ACCOUNT_ID }}
BEEPER_SELF_USER_ID: ${{ secrets.BEEPER_SELF_USER_ID }}
BRANCH: ${{ github.ref_name }}
SHA: ${{ github.event.after }}
run: |
MESSAGE="JulesWyrm pushed to ${BRANCH} (${SHA:0:7}). A wiring-review issue has been opened."
curl -sf -X POST "${BEEPER_BASE_URL}/v1/chats" \
-H "Authorization: Bearer ${BEEPER_API_KEY}" \
Comment on lines +55 to +65
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

Because this step uses curl -sf, it will fail (and mark the workflow red) when Beeper config/secrets aren’t set yet (the comments indicate it’s still TBD). Consider gating the step/job on required vars being present, or set continue-on-error: true so issue creation still runs successfully even if notifications aren’t configured.

Copilot uses AI. Check for mistakes.
-H "Content-Type: application/json" \
-d "{
\"accountID\": \"${BEEPER_ACCOUNT_ID}\",
\"mode\": \"start\",
\"user\": { \"id\": \"${BEEPER_SELF_USER_ID}\" },
\"messageText\": \"${MESSAGE}\"
}"
Comment on lines +64 to +72
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

The JSON payload for Beeper is built via shell string interpolation; if BRANCH or MESSAGE contains quotes/newlines, the JSON becomes invalid (and can potentially change the request body). Generate the JSON with a proper encoder (e.g., python -c 'import json; print(json.dumps(...))' or jq -n) before passing it to curl.

Suggested change
curl -sf -X POST "${BEEPER_BASE_URL}/v1/chats" \
-H "Authorization: Bearer ${BEEPER_API_KEY}" \
-H "Content-Type: application/json" \
-d "{
\"accountID\": \"${BEEPER_ACCOUNT_ID}\",
\"mode\": \"start\",
\"user\": { \"id\": \"${BEEPER_SELF_USER_ID}\" },
\"messageText\": \"${MESSAGE}\"
}"
PAYLOAD=$(MESSAGE="$MESSAGE" \
BEEPER_ACCOUNT_ID="$BEEPER_ACCOUNT_ID" \
BEEPER_SELF_USER_ID="$BEEPER_SELF_USER_ID" \
python3 -c '
import json
import os
print(json.dumps({
"accountID": os.environ["BEEPER_ACCOUNT_ID"],
"mode": "start",
"user": {"id": os.environ["BEEPER_SELF_USER_ID"]},
"messageText": os.environ["MESSAGE"],
}))
')
curl -sf -X POST "${BEEPER_BASE_URL}/v1/chats" \
-H "Authorization: Bearer ${BEEPER_API_KEY}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"

Copilot uses AI. Check for mistakes.

# ── Analyse diff and open issue assigned to Copilot ───────────────────────
create-issue:
needs: detect-jules
if: needs.detect-jules.outputs.jules_pushed == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Checkout full history
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Compute diff of Jules' commits
env:
BEFORE: ${{ github.event.before }}
AFTER: ${{ github.event.after }}
run: |
# If BEFORE is all-zeros the branch is new; diff against the first commit's parent.
if [[ "$BEFORE" == "0000000000000000000000000000000000000000" ]]; then
git diff "${AFTER}^..${AFTER}" > /tmp/jules-diff.patch || \
git show "$AFTER" > /tmp/jules-diff.patch
else
git diff "${BEFORE}..${AFTER}" > /tmp/jules-diff.patch
fi
echo "Diff size: $(wc -c < /tmp/jules-diff.patch) bytes"

- name: Analyse diff and build issue body
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: node scripts/analyze-diff.mjs < /tmp/jules-diff.patch > /tmp/issue-body.md

- name: Open GitHub issue assigned to Copilot
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: ${{ github.ref_name }}
SHA: ${{ github.event.after }}
run: |
SHORT_SHA="${SHA:0:7}"
gh issue create \
--title "Jules push on ${BRANCH} (${SHORT_SHA}): wiring review needed" \
--body-file /tmp/issue-body.md \
--assignee "@copilot"
38 changes: 38 additions & 0 deletions dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,41 @@ Run with `npm test`. 20 tests across 3 suites, all passing.
| B001 | Fixed | Circuit suite iframe state loss on tab switch | Fixed by rendering both iframes simultaneously, toggling with CSS display:none |
| B002 | Fixed | Electromagnet coil flat rendering (no depth) | Fixed draw order: back arches → rod (white fill) → front arches |
| B003 | Reverted | Electromagnet integration removed from circuit tools | Symbol style did not match required exam format (hatched steel rod + stacked cell symbols). To be redesigned before re-integrating. |

---

## Planned: Co-contributor Push Automation

### Problem
JulesWyrm (co-contributor) pushes raw HTML tool additions/enhancements directly. These changes need Next.js wiring (new page, `tool-id`, tracker meta, tests) before they are production-ready. Currently this wiring is done manually after noticing her push.

### Goal
Automate detection, notification, and issue creation whenever JulesWyrm pushes, so nothing falls through the cracks.

### Design

**Trigger:** GitHub Actions workflow on `push` to any branch.

**Job 1 — Notify**
- Check if any commit author in the push is `JulesWyrm`
- If yes, call Beeper API to send a push notification to Kahhow

**Job 2 — Auto-issue (runs only on JulesWyrm pushes)**
- Generate a diff of her changes using the full push range (`${before}..${after}`), not just the last commit; if `before` is all zeros (new branch), handle that case separately so the diff still covers everything introduced by the push
- Send the diff to Claude API (Anthropic) with a structured prompt that checks for:
- New HTML files in `public/tools/` → needs a Next.js page + `tool-id` added to `VALID_TOOLS`
- Missing `<meta name="tool-id">` or `<script src="/tracker.js">` in any HTML file
- New tool → needs Jest test suite added under `__tests__/canvas/`
- Changes to existing canvas logic → flag tests may need updating
- Claude returns a structured issue body (markdown) summarising what changed and what wiring is needed
- Open a GitHub Issue via `gh issue create --assignee "@copilot"` so Copilot automatically attempts the wiring and opens a PR

**Why Claude for analysis, Copilot for implementation:**
Claude handles open-ended diff analysis and writing the issue spec. Copilot handles the mechanical Next.js wiring (creating page files, adding tool-id, etc.) once given a clear spec.

**Secrets required:**
- `ANTHROPIC_API_KEY` — for Claude diff analysis
- `BEEPER_*` — Beeper API auth (TBD once API docs confirmed)
- `GITHUB_TOKEN` — already available in Actions

**Status:** Planned — not yet implemented.
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

This section marks the automation as “Planned — not yet implemented,” but this PR adds the workflow + analysis script. Update the status (and clarify what remains TODO, e.g. Beeper endpoint/auth) so the docs match the repo state.

Suggested change
**Status:** Planned — not yet implemented.
**Status:** Partially implemented — the GitHub Actions workflow and diff-analysis script are now in the repo. Remaining TODOs: finalize the Beeper endpoint/auth configuration and complete any notification wiring that depends on those credentials.

Copilot uses AI. Check for mistakes.
129 changes: 129 additions & 0 deletions scripts/analyze-diff.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env node
/**
* analyze-diff.mjs
*
* Reads a git diff from stdin, calls the Anthropic Messages API
* (claude-sonnet-4-6), and writes a markdown GitHub issue body to stdout.
*
* Usage:
* git diff <before>..<after> | node scripts/analyze-diff.mjs
*
* Required env:
* ANTHROPIC_API_KEY
*/

// ── 1. Read diff from stdin ───────────────────────────────────────────────────
const diff = await readStdin()

if (!diff.trim()) {
process.stdout.write('<!-- analyze-diff: empty diff, nothing to review -->\n')
process.exit(0)
}

// ── 2. Build the prompt ───────────────────────────────────────────────────────
const SYSTEM_PROMPT = `\
You are a code-review assistant for a Next.js 14 (App Router) project that hosts
standalone HTML canvas tools in \`public/tools/\`. Your job is to review a git diff
and identify any Next.js wiring work that a developer needs to do next.

Project conventions:
- Every HTML tool file in \`public/tools/\` needs:
1. A \`<meta name="tool-id" content="<id>" />\` tag in \`<head>\`
2. A \`<script src="/tracker.js"></script>\` tag before \`</body>\`
- Every new tool also needs:
3. A new route at \`app/tools/<name>/page.tsx\` that renders a full-viewport iframe
4. A new tool-id string added to the VALID_TOOLS Set in \`app/api/event/route.ts\`
5. A Jest test suite in \`__tests__/canvas/<name>.test.js\` using
\`loadCircuitScript\` (non-IIFE) or \`loadIifeScript\` (IIFE scripts)
from \`__tests__/canvas/helpers.js\`
- When canvas logic functions change (snap, gcd, componentSize, getComponentNodes,
rotatePoint, getLocalNodes), the corresponding __tests__/canvas/ suite likely
needs updating.
- GRID=28, COMPONENT_SCALE=0.8 for circuit_diagram_creatorv2.html
- GRID=22.4 for circuit_diagram_secjc.html; includes transistor (3-node) and transformer (2-node)
- object_circuitv2.html: COMPONENT_SCALE=0.8 for battery/switch, BULB_SCALE=1 (bulb nodes unscaled)
`

// Truncate at 48 000 chars to avoid runaway costs on huge diffs
const diffSnippet = diff.length > 48_000
? diff.slice(0, 48_000) + '\n\n[... diff truncated at 48 000 chars ...]'
: diff

const USER_PROMPT = `\
Below is a git diff from a push by JulesWyrm. Analyse it and produce a GitHub
issue body in Markdown describing the Next.js wiring work needed.

Structure your response as:
1. A short (2-3 sentence) **Summary** of what changed.
2. A **Checklist** of actionable tasks (GitHub task-list syntax: \`- [ ] ...\`),
covering where relevant:
- New \`public/tools/\` HTML files that need a Next.js page
- Missing or incorrect \`<meta name="tool-id">\` tags
- Missing \`<script src="/tracker.js">\` tags
- New tool-ids to add to VALID_TOOLS in \`app/api/event/route.ts\`
- New Jest test suites to create in \`__tests__/canvas/\`
- Existing canvas test suites that need updating due to logic changes
3. A **Files changed** section listing every file touched in the diff.

If nothing in the diff requires Next.js wiring work, say so clearly and keep the
issue body short.

\`\`\`diff
${diffSnippet}
\`\`\`
`

// ── 3. Call the Anthropic Messages API ───────────────────────────────────────
const apiKey = process.env.ANTHROPIC_API_KEY
if (!apiKey) {
process.stderr.write('ERROR: ANTHROPIC_API_KEY is not set\n')
process.exit(1)
}

const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
'content-type': 'application/json'
},
body: JSON.stringify({
model: 'claude-sonnet-4-6',
max_tokens: 2048,
system: SYSTEM_PROMPT,
messages: [{ role: 'user', content: USER_PROMPT }]
})
})

if (!response.ok) {
const errText = await response.text()
process.stderr.write(`ERROR: Anthropic API returned ${response.status}: ${errText}\n`)
process.exit(1)
}

const data = await response.json()
const issueBody = data?.content?.[0]?.text ?? ''

if (!issueBody) {
process.stderr.write('ERROR: Unexpected response shape from Anthropic API\n')
process.stderr.write(JSON.stringify(data, null, 2) + '\n')
process.exit(1)
}

// ── 4. Write issue body to stdout ─────────────────────────────────────────────
const footer = [
'',
'---',
'*Auto-generated by [analyze-diff.mjs](../scripts/analyze-diff.mjs) · model: claude-sonnet-4-6*'
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

This footer link is relative (../scripts/analyze-diff.mjs) and won’t resolve correctly inside a GitHub Issue. Use an absolute GitHub URL (ideally pinned to the commit SHA) or remove the link to avoid a broken reference.

Suggested change
'*Auto-generated by [analyze-diff.mjs](../scripts/analyze-diff.mjs) · model: claude-sonnet-4-6*'
'*Auto-generated by analyze-diff.mjs · model: claude-sonnet-4-6*'

Copilot uses AI. Check for mistakes.
].join('\n')

process.stdout.write(issueBody + footer + '\n')

// ── Helpers ───────────────────────────────────────────────────────────────────
async function readStdin () {
const chunks = []
for await (const chunk of process.stdin) {
chunks.push(chunk)
}
return Buffer.concat(chunks).toString('utf8')
}
Loading