Spaces:
Sleeping
Sleeping
Bootstrap SIN-GitHub-Issues runtime
Browse files- .github/workflows/ci.yml +35 -0
- .github/workflows/deploy-huggingface-space.yml +51 -0
- .gitignore +7 -0
- .well-known/oauth-client.json +9 -0
- A2A-CARD.md +128 -0
- AGENTS.md +53 -0
- Dockerfile +11 -0
- README.md +83 -5
- agent-spec.example.json +21 -0
- agent.json +24 -0
- clients/codex-config.toml +6 -0
- clients/opencode-mcp.json +8 -0
- cloudflared/config.example.yml +7 -0
- mcp-config.json +8 -0
- package.json +31 -0
- scripts/postbuild.mjs +17 -0
- src/a2a-http.ts +79 -0
- src/audit.ts +17 -0
- src/cli.ts +67 -0
- src/command.ts +24 -0
- src/contracts.ts +31 -0
- src/git.ts +31 -0
- src/github.ts +44 -0
- src/hf_sync.ts +86 -0
- src/index.ts +5 -0
- src/mcp-server.ts +62 -0
- src/metadata.ts +105 -0
- src/runtime.ts +137 -0
- src/state.ts +33 -0
- src/supabase.ts +23 -0
- tsconfig.json +15 -0
.github/workflows/ci.yml
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Agent CI
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main]
|
| 6 |
+
pull_request:
|
| 7 |
+
workflow_dispatch:
|
| 8 |
+
|
| 9 |
+
jobs:
|
| 10 |
+
build-and-verify:
|
| 11 |
+
runs-on: ubuntu-latest
|
| 12 |
+
steps:
|
| 13 |
+
- name: Checkout
|
| 14 |
+
uses: actions/checkout@v4
|
| 15 |
+
|
| 16 |
+
- name: Setup Node
|
| 17 |
+
uses: actions/setup-node@v4
|
| 18 |
+
with:
|
| 19 |
+
node-version: '22'
|
| 20 |
+
cache: npm
|
| 21 |
+
|
| 22 |
+
- name: Install dependencies
|
| 23 |
+
run: npm install
|
| 24 |
+
|
| 25 |
+
- name: Build
|
| 26 |
+
run: npm run build
|
| 27 |
+
|
| 28 |
+
- name: Print card
|
| 29 |
+
run: node dist/src/cli.js print-card
|
| 30 |
+
|
| 31 |
+
- name: Agent help
|
| 32 |
+
run: node dist/src/cli.js run-action '{"action":"agent.help"}'
|
| 33 |
+
|
| 34 |
+
- name: Agent health
|
| 35 |
+
run: node dist/src/cli.js run-action '{"action":"sin.github.issues.health"}'
|
.github/workflows/deploy-huggingface-space.yml
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Deploy Hugging Face Space
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
push:
|
| 6 |
+
branches: [main]
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
deploy:
|
| 10 |
+
if: ${{ secrets.HF_TOKEN != '' }}
|
| 11 |
+
runs-on: ubuntu-latest
|
| 12 |
+
env:
|
| 13 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 14 |
+
HF_SPACE_REPO_ID: delqhi/sin-github-issues
|
| 15 |
+
steps:
|
| 16 |
+
- name: Checkout
|
| 17 |
+
uses: actions/checkout@v4
|
| 18 |
+
|
| 19 |
+
- name: Setup Python
|
| 20 |
+
uses: actions/setup-python@v5
|
| 21 |
+
with:
|
| 22 |
+
python-version: '3.11'
|
| 23 |
+
|
| 24 |
+
- name: Install deployment dependencies
|
| 25 |
+
run: pip install huggingface_hub
|
| 26 |
+
|
| 27 |
+
- name: Create or ensure Hugging Face Space
|
| 28 |
+
run: |
|
| 29 |
+
python - <<'PY'
|
| 30 |
+
import os
|
| 31 |
+
from huggingface_hub import HfApi
|
| 32 |
+
|
| 33 |
+
repo_id = os.environ["HF_SPACE_REPO_ID"].strip()
|
| 34 |
+
if not repo_id:
|
| 35 |
+
raise SystemExit("HF_SPACE_REPO_ID missing")
|
| 36 |
+
|
| 37 |
+
api = HfApi(token=os.environ["HF_TOKEN"])
|
| 38 |
+
api.create_repo(repo_id=repo_id, repo_type="space", space_sdk="docker", exist_ok=True)
|
| 39 |
+
PY
|
| 40 |
+
|
| 41 |
+
- name: Push repository contents to Hugging Face Space
|
| 42 |
+
run: |
|
| 43 |
+
set -euo pipefail
|
| 44 |
+
git config --global user.name "GitHub Actions"
|
| 45 |
+
git config --global user.email "actions@github.com"
|
| 46 |
+
git clone "https://oauth2:${HF_TOKEN}@huggingface.co/spaces/delqhi/sin-github-issues" hf-space
|
| 47 |
+
rsync -a --delete --exclude ".git/" --exclude ".github/" --exclude "node_modules/" --exclude "dist/" ./ hf-space/
|
| 48 |
+
cd hf-space
|
| 49 |
+
git add .
|
| 50 |
+
git commit -m "Deploy SIN-GitHub-Issues from GitHub Actions" || true
|
| 51 |
+
git push origin main
|
.gitignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
dist
|
| 2 |
+
node_modules
|
| 3 |
+
*.log
|
| 4 |
+
.env
|
| 5 |
+
.env.local
|
| 6 |
+
.DS_Store
|
| 7 |
+
|
.well-known/oauth-client.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"client_id": "https://delqhi-sin-github-issues.hf.space/.well-known/oauth-client.json",
|
| 3 |
+
"client_name": "SIN-GitHub-Issues",
|
| 4 |
+
"redirect_uris": ["https://delqhi-sin-github-issues.hf.space/oauth/callback"],
|
| 5 |
+
"grant_types": ["authorization_code"],
|
| 6 |
+
"response_types": ["code"],
|
| 7 |
+
"token_endpoint_auth_method": "none"
|
| 8 |
+
}
|
| 9 |
+
|
A2A-CARD.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# A2A Card: SIN-GitHub-Issues
|
| 2 |
+
|
| 3 |
+
## Identitaet
|
| 4 |
+
|
| 5 |
+
- `A2A Bezeichnung`: `SIN-GitHub-Issues`
|
| 6 |
+
- `Name`: `SIN-GitHub-Issues`
|
| 7 |
+
- `Beschreibung (Zweck)`: GitHub issue execution agent for confirmed bugs, repo issue lifecycle, branch-based fixes, and verification handoff.
|
| 8 |
+
- `Verwendung`: Use after enterprise-deep-debug confirms a bug to search/create/update GitHub issues, open a repo branch, execute the fix workflow, commit, push, and report verification status back to the orchestrating CLI agent.
|
| 9 |
+
- `CIMD`: `https://delqhi-sin-github-issues.hf.space`
|
| 10 |
+
|
| 11 |
+
## Team & Infrastruktur
|
| 12 |
+
|
| 13 |
+
- `A2A Team`: `Team - Coding`
|
| 14 |
+
- `Team-Manager A2A`: `SIN-Coding-CEO`
|
| 15 |
+
- `VM / Server`: `Target: Hugging Face Space free CPU VM`
|
| 16 |
+
- `Lokales Verzeichnis`: `/Users/jeremy/dev/SIN-Solver/a2a/team-coding/A2A-SIN-GitHub-Issues`
|
| 17 |
+
- `Hugging Face Space URL`: `https://huggingface.co/spaces/delqhi/sin-github-issues`
|
| 18 |
+
- `Landingpage URL`: `https://delqhi-sin-github-issues.hf.space`
|
| 19 |
+
- `Cloudflare Tunnel`: `sin-github-issues`
|
| 20 |
+
- `Workforce Ziel-Index`: `https://a2a.delqhi.com/`
|
| 21 |
+
- `Google Docs Tab`: `https://docs.google.com/document/d/1RtoHn4I0GntuEEOHHkqoh_dMuGzgMwQz7_8oxAOpQbw/edit?tab=t.tuv2t449f8ze`
|
| 22 |
+
|
| 23 |
+
## Auth- und Secrets-Modell
|
| 24 |
+
|
| 25 |
+
- `Primäre lokale GitHub-Lane`: `gh` CLI auf dem Operator-Host
|
| 26 |
+
- `Ziel-Lane`: GitHub Session-Grants via `SIN-Authenticator` (aktuell blockiert)
|
| 27 |
+
- `Browser-Profil`: `zukunftsorientierte.energie@gmail.com` / Chrome Profil `Default`
|
| 28 |
+
- `HF Session Backup`: `HF_DATASET_REPO` + `HF_TOKEN`
|
| 29 |
+
- `SIN-Supabase`: `SIN_SUPABASE_URL` + `SIN_SUPABASE_SERVICE_ROLE_KEY`
|
| 30 |
+
- `Aktueller Blocker`: GitHub-Provider in `SIN-Authenticator` noch nicht exponiert, SIN-Supabase/HF Runtime noch nicht live gekoppelt
|
| 31 |
+
|
| 32 |
+
## MCP Server
|
| 33 |
+
|
| 34 |
+
- `MCP Server`: `sin-github-issues`
|
| 35 |
+
- `Transport`: `stdio`
|
| 36 |
+
- `Config`: `./mcp-config.json`
|
| 37 |
+
|
| 38 |
+
## CDP (Chrome DevTools Protocol)
|
| 39 |
+
|
| 40 |
+
- `Lokale Browser-Lane`: Chrome Profil `Default` fuer `zukunftsorientierte.energie@gmail.com`
|
| 41 |
+
- `Verwendung`: nur fuer browserpflichtige GitHub-Schritte; normale Issue-/Git-Lane bleibt CLI-first
|
| 42 |
+
|
| 43 |
+
## Tools
|
| 44 |
+
|
| 45 |
+
- `sin.github.issues.health`
|
| 46 |
+
- `sin.github.issues.onboarding.status`
|
| 47 |
+
- `sin.github.issues.onboarding.save`
|
| 48 |
+
- `sin.github.issues.auth.status`
|
| 49 |
+
- `sin.github.issues.issue.ensure`
|
| 50 |
+
- `sin.github.issues.issue.comment`
|
| 51 |
+
- `sin.github.issues.issue.close`
|
| 52 |
+
- `sin.github.issues.repo.status`
|
| 53 |
+
- `sin.github.issues.repo.branch.start`
|
| 54 |
+
- `sin.github.issues.repo.commit`
|
| 55 |
+
- `sin.github.issues.repo.push`
|
| 56 |
+
- `sin.github.issues.workflow.finalize_fix`
|
| 57 |
+
|
| 58 |
+
## Aufgaben
|
| 59 |
+
|
| 60 |
+
- Runtime bereitstellen
|
| 61 |
+
- MCP bereitstellen
|
| 62 |
+
- A2A JSON-RPC bereitstellen
|
| 63 |
+
- bestaetigte Bugs auf GitHub-Issues abbilden und pflegen
|
| 64 |
+
- Branch-/Commit-/Push-Handoff fuer Fixes koordinieren
|
| 65 |
+
- RCA/Fix/Verifikation zurueck ins Issue schreiben
|
| 66 |
+
- Team-Manager- und Docs-Compliance einhalten
|
| 67 |
+
|
| 68 |
+
## Commandos
|
| 69 |
+
|
| 70 |
+
```bash
|
| 71 |
+
sin-github-issues serve-a2a
|
| 72 |
+
sin-github-issues serve-mcp
|
| 73 |
+
sin-github-issues print-card
|
| 74 |
+
sin-github-issues run-action '{"action":"agent.help"}'
|
| 75 |
+
sin-github-issues run-action '{"action":"sin.github.issues.auth.status"}'
|
| 76 |
+
sin-github-issues run-action '{"action":"sin.github.issues.issue.ensure","repo":"owner/name","title":"Bug title","body":"..."}'
|
| 77 |
+
sin-github-issues run-action '{"action":"sin.github.issues.repo.branch.start","repoPath":"/abs/repo","branchName":"fix/example"}'
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
## Endpoints
|
| 81 |
+
|
| 82 |
+
- `Health`: `/health`
|
| 83 |
+
- `Root`: `/`
|
| 84 |
+
- `Agent Card`: `/.well-known/agent-card.json`
|
| 85 |
+
- `Legacy Agent Card Alias`: `/.well-known/agent.json`
|
| 86 |
+
- `A2A JSON-RPC`: `/a2a/v1`
|
| 87 |
+
|
| 88 |
+
## Skills
|
| 89 |
+
|
| 90 |
+
- `Pflicht`: Health, onboarding, auth status, issue lifecycle, repo branch/commit/push, finalize-fix workflow
|
| 91 |
+
|
| 92 |
+
## Dateien
|
| 93 |
+
|
| 94 |
+
- `README.md`
|
| 95 |
+
- `A2A-CARD.md`
|
| 96 |
+
- `AGENTS.md`
|
| 97 |
+
- `agent.json`
|
| 98 |
+
- `mcp-config.json`
|
| 99 |
+
- `package.json`
|
| 100 |
+
- `tsconfig.json`
|
| 101 |
+
- `scripts/postbuild.mjs`
|
| 102 |
+
- `src/runtime.ts`
|
| 103 |
+
- `src/mcp-server.ts`
|
| 104 |
+
- `src/a2a-http.ts`
|
| 105 |
+
- `src/metadata.ts`
|
| 106 |
+
- `src/cli.ts`
|
| 107 |
+
- `src/index.ts`
|
| 108 |
+
|
| 109 |
+
## Apps (auf welche Apps hat A2A Zugriff)
|
| 110 |
+
|
| 111 |
+
- `GitHub Issues / Pull Requests`
|
| 112 |
+
- `lokale Git-Repositories`
|
| 113 |
+
- `SIN-Supabase`
|
| 114 |
+
- `Hugging Face Dataset backup lane`
|
| 115 |
+
- `OpenCode CLI handoff workflow`
|
| 116 |
+
|
| 117 |
+
## Plattformen (auf welche Plattformen hat A2A Zugriff)
|
| 118 |
+
|
| 119 |
+
- `GitHub`
|
| 120 |
+
- `Hugging Face Spaces`
|
| 121 |
+
- `SIN-Authenticator` (target auth broker)
|
| 122 |
+
- `SIN-Supabase`
|
| 123 |
+
- `A2A JSON-RPC`
|
| 124 |
+
- `MCP stdio`
|
| 125 |
+
|
| 126 |
+
## Primärmodell
|
| 127 |
+
|
| 128 |
+
- `opencode/nemotron-3-super-free`
|
AGENTS.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SIN-GitHub-Issues
|
| 2 |
+
|
| 3 |
+
- Team: Team - Coding
|
| 4 |
+
- Team Manager: SIN-Coding-CEO
|
| 5 |
+
- Slug: sin-github-issues
|
| 6 |
+
- Repo: https://github.com/Delqhi/sin-github-issues
|
| 7 |
+
- Docs Tab: https://docs.google.com/document/d/1RtoHn4I0GntuEEOHHkqoh_dMuGzgMwQz7_8oxAOpQbw/edit?tab=t.tuv2t449f8ze
|
| 8 |
+
|
| 9 |
+
Arbeitsregel:
|
| 10 |
+
- Jede neue Action muss in `runtime.ts`, `mcp-server.ts`, `a2a-http.ts`, `metadata.ts` und `A2A-CARD.md` synchronisiert werden.
|
| 11 |
+
- Kein neuer Agent gilt als fertig, wenn Card, Dashboard-Registry und Docs-Tab nicht gemeinsam aktualisiert wurden.
|
| 12 |
+
- Jeder neue Agent muss auch `agent.json`, `AGENTS.md`, `mcp-config.json`, `clients/opencode-mcp.json`, `clients/codex-config.toml`, `.well-known/agent-card.json`, `.well-known/agent.json`, `.well-known/oauth-client.json` und ggf. einen `bin/`-Wrapper sauber besitzen.
|
| 13 |
+
- Google Docs Child-Tabs immer rekursiv ueber `includeTabsContent=true` + `childTabs` aufloesen. Nie nur Top-Level-Tabs pruefen.
|
| 14 |
+
- Wenn der Team-Haupt-Tab eine finalisierte A2A-Team-Tabelle hat und dieselbe Tabelle im `Silicon Workforce`-Tab gespiegelt wird, muessen beide Tabellen synchron gehalten werden. Das finale Tabellen-Schema darf nicht stillschweigend geaendert werden.
|
| 15 |
+
- Uebersichtstabellen im `Silicon Workforce`-Tab verwenden final das Schema: linke Typ-Spalte + `Bezeichnung | Zweck | URL`.
|
| 16 |
+
- Team-Haupttabellen und deren Spiegel im `Silicon Workforce`-Tab verwenden final das Schema: linke Typ-Spalte + `Bezeichnung | Zweck | MCP's | Commands | Endpoints | Server (VM) | CLI's | URL`.
|
| 17 |
+
- Jeder Team-Haupt-Tab enthaelt nur seine synchronisierte Team-Tabelle; dieselbe Tabelle wird im `Silicon Workforce`-Tab mit sichtbarer Trennlinie zwischen Team-Sektionen gespiegelt.
|
| 18 |
+
- Pflichtfelder fuer neue A2A-Agenten:
|
| 19 |
+
- Identitaet, Team, Team-Manager, Zweck, Verwendung
|
| 20 |
+
- Auth-/Secrets-Modell
|
| 21 |
+
- MCP-Tools und A2A-Actions/Skills
|
| 22 |
+
- Commands, Endpoints, Capabilities, Input/Output-Modi
|
| 23 |
+
- Task-Lifecycle, Observability/Audit, Release/Rollback
|
| 24 |
+
- Owner/Oncall, Dependencies, Deploy-Ziel
|
| 25 |
+
- Pflichtintegration:
|
| 26 |
+
- repo `.opencode/opencode.json`
|
| 27 |
+
- globale OpenCode-MCP-Integration falls benoetigt
|
| 28 |
+
- Dashboard-/Workforce-Registry + Detailseite `dashboard-enterprise/app/agents/<slug>/page.tsx`
|
| 29 |
+
- `a2a.delqhi.com` Landing/Card
|
| 30 |
+
- Google Docs Child-Tab + Team-Tabellen
|
| 31 |
+
- `cloudflared/config.example.yml`, wenn der Agent eine oeffentliche Runtime / einen Tunnel bekommen soll
|
| 32 |
+
- echte Runtime-Provisionierung + echter Publish/Deploy-Schritt, sobald oeffentliche URLs/HF/tunnel eingetragen werden
|
| 33 |
+
- Falls der benoetigte Google-Docs-Child-Tab noch nicht existiert, muss er zuerst manuell im Google-Docs-UI erstellt werden. Die Docs-API kann bestehende Tabs pflegen, aber keine neuen Child-Tabs erzeugen.
|
| 34 |
+
- Pflichtvalidierung vor Done:
|
| 35 |
+
- Build/Typecheck
|
| 36 |
+
- A2A health/card/rpc
|
| 37 |
+
- MCP smoke
|
| 38 |
+
- relevante Repo-Gates
|
| 39 |
+
- OpenCode-Integration
|
| 40 |
+
- Google-Docs-Sync
|
| 41 |
+
- `npm --prefix <agent-root> run build`
|
| 42 |
+
- `node <agent-root>/dist/src/cli.js print-card`
|
| 43 |
+
- `node <agent-root>/dist/src/cli.js run-action '{"action":"agent.help"}'`
|
| 44 |
+
- `node <agent-root>/dist/src/cli.js run-action '{"action":"<namespace>.health"}'` anhand der echten Skill-ID aus der Card
|
| 45 |
+
- `serve-mcp` bleibt mit offenem stdin erreichbar
|
| 46 |
+
- `npm run test:a2a:fleet`
|
| 47 |
+
- `npm run test:a2a:live -- --agent <slug>`
|
| 48 |
+
- Landing / `publicPageUrl` liefert `200`
|
| 49 |
+
- Runtime-Host ist real provisioniert
|
| 50 |
+
- `/.well-known/agent-card.json` liefert `200`
|
| 51 |
+
- `/a2a/v1` liefert nicht `404`
|
| 52 |
+
- Ein Agent darf erst auf `ready` / `live` gesetzt werden, wenn diese Reachability-Pruefungen bestanden sind und Registry/Card/Docs denselben auditieren Stand zeigen.
|
| 53 |
+
- Beim Fuellen der Team-Tabellen muessen alle freigegebenen Spalten gepflegt werden. Platzhalter wie `pending formal MCP entry` oder `pending dedicated child-tab` sind nur zulaessig, wenn die jeweilige Surface real noch nicht provisioniert ist.
|
Dockerfile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:22-bookworm-slim
|
| 2 |
+
WORKDIR /app
|
| 3 |
+
COPY package.json package-lock.json* ./
|
| 4 |
+
RUN npm install
|
| 5 |
+
COPY . .
|
| 6 |
+
RUN npm run build
|
| 7 |
+
ENV PORT=7860
|
| 8 |
+
ENV HOST=0.0.0.0
|
| 9 |
+
EXPOSE 46031
|
| 10 |
+
CMD ["node", "dist/src/cli.js", "serve-a2a"]
|
| 11 |
+
|
README.md
CHANGED
|
@@ -1,10 +1,88 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: SIN-GitHub-Issues
|
| 3 |
+
emoji: "🤖"
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# SIN-GitHub-Issues
|
| 12 |
+
|
| 13 |
+
`SIN-GitHub-Issues` is a SIN A2A agent package for the Silicon Workforce.
|
| 14 |
+
|
| 15 |
+
## Identity
|
| 16 |
+
|
| 17 |
+
- Slug: `sin-github-issues`
|
| 18 |
+
- Team: `Team - Coding`
|
| 19 |
+
- Team Manager: `SIN-Coding-CEO`
|
| 20 |
+
- Purpose: GitHub issue execution agent for confirmed bugs, repo issue lifecycle, branch-based fixes, and verification handoff.
|
| 21 |
+
- Usage: Use after enterprise-deep-debug confirms a bug to search/create/update GitHub issues, open a repo branch, execute the fix workflow, commit, push, and report verification status back to the orchestrating CLI agent.
|
| 22 |
+
- Primary Model: `opencode/nemotron-3-super-free`
|
| 23 |
+
|
| 24 |
+
## Deployment
|
| 25 |
+
|
| 26 |
+
- Local path: `/Users/jeremy/dev/SIN-Solver/a2a/team-coding/A2A-SIN-GitHub-Issues`
|
| 27 |
+
- GitHub repo: `https://github.com/Delqhi/sin-github-issues`
|
| 28 |
+
- Workforce index: `https://a2a.delqhi.com`
|
| 29 |
+
- Landing page: `https://delqhi-sin-github-issues.hf.space`
|
| 30 |
+
- Public A2A / CIMD: `https://a2a.delqhi.com/agents/sin-github-issues`
|
| 31 |
+
- Hugging Face Space: `https://huggingface.co/spaces/delqhi/sin-github-issues`
|
| 32 |
+
- Hugging Face repo id: `delqhi/sin-github-issues`
|
| 33 |
+
- Cloudflare tunnel: `sin-github-issues`
|
| 34 |
+
- Runtime target: `Target: Hugging Face Space free CPU VM`
|
| 35 |
+
|
| 36 |
+
Current blockers:
|
| 37 |
+
- GitHub provider/session-grant support is not yet exposed by `SIN-Authenticator`
|
| 38 |
+
- SIN-Supabase env coupling is not yet live
|
| 39 |
+
- HF Space/runtime deployment is not yet live
|
| 40 |
+
|
| 41 |
+
## Runtime Shape
|
| 42 |
+
|
| 43 |
+
- A2A JSON-RPC runtime
|
| 44 |
+
- MCP stdio runtime
|
| 45 |
+
- CLI wrapper for local and automated use
|
| 46 |
+
- publish-ready Docker/Hugging Face Space layout
|
| 47 |
+
- keep-alive ping + HF dataset session backup hooks
|
| 48 |
+
- best-effort `sin-supabase` audit persistence for workflow events
|
| 49 |
+
|
| 50 |
+
## A2A Surface
|
| 51 |
+
|
| 52 |
+
- Card: `GET /.well-known/agent-card.json`
|
| 53 |
+
- Alias: `GET /.well-known/agent.json`
|
| 54 |
+
- Health: `GET /health`
|
| 55 |
+
- RPC: `POST /a2a/v1`
|
| 56 |
+
|
| 57 |
+
## MCP Surface
|
| 58 |
+
|
| 59 |
+
- Transport: `stdio`
|
| 60 |
+
- Config: `./mcp-config.json`
|
| 61 |
+
- OpenCode client: `./clients/opencode-mcp.json`
|
| 62 |
+
- Codex client: `./clients/codex-config.toml`
|
| 63 |
+
|
| 64 |
+
## Local Commands
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
sin-github-issues serve-a2a
|
| 68 |
+
sin-github-issues serve-mcp
|
| 69 |
+
sin-github-issues print-card
|
| 70 |
+
sin-github-issues run-action '{"action":"agent.help"}'
|
| 71 |
+
sin-github-issues run-action '{"action":"sin.github.issues.health"}'
|
| 72 |
+
sin-github-issues run-action '{"action":"sin.github.issues.auth.status"}'
|
| 73 |
+
sin-github-issues run-action '{"action":"sin.github.issues.issue.ensure","repo":"owner/name","title":"Bug title","body":"..."}'
|
| 74 |
+
sin-github-issues run-action '{"action":"sin.github.issues.repo.branch.start","repoPath":"/abs/repo","branchName":"fix/example"}'
|
| 75 |
+
|
| 76 |
+
## Current Action Surface
|
| 77 |
+
|
| 78 |
+
- issue ensure / comment / close
|
| 79 |
+
- repo status / branch start / commit / push
|
| 80 |
+
- finalize-fix comment/closure handoff
|
| 81 |
+
- auth status + onboarding state
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
## Publication Notes
|
| 85 |
+
|
| 86 |
+
- This package is Docker/Hugging Face Space ready via the repository `Dockerfile`.
|
| 87 |
+
- The standalone GitHub repo should contain the same contents as this directory root.
|
| 88 |
+
- Remote publication is only complete after landing, agent card, and A2A endpoint reachability are verified with the live audit.
|
agent-spec.example.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"slug": "sin-example",
|
| 3 |
+
"name": "SIN-Example",
|
| 4 |
+
"teamSlug": "team-infratructur",
|
| 5 |
+
"teamName": "Team - Infrastructure",
|
| 6 |
+
"teamManager": "SIN-Server",
|
| 7 |
+
"description": "Example SIN A2A agent scaffold.",
|
| 8 |
+
"usage": "Use when an example A2A agent is needed.",
|
| 9 |
+
"primaryModel": "nvidia/example-model",
|
| 10 |
+
"repoUrl": "https://github.com/delqhi/sin-example",
|
| 11 |
+
"repoVisibility": "private",
|
| 12 |
+
"docsTabUrl": "https://docs.google.com/document/d/<doc>/edit?tab=<tab>",
|
| 13 |
+
"docsTabId": "t.exampletabid",
|
| 14 |
+
"publicPageUrl": "https://a2a.delqhi.com/agents/sin-example",
|
| 15 |
+
"landingPageUrl": "https://delqhi-sin-example.hf.space",
|
| 16 |
+
"hfSpaceUrl": "https://huggingface.co/spaces/delqhi/sin-example",
|
| 17 |
+
"cloudflareTunnel": "sin-example",
|
| 18 |
+
"vmServer": "Hugging Face Space free CPU VM",
|
| 19 |
+
"publicBaseUrl": "http://127.0.0.1:45999",
|
| 20 |
+
"port": 45999
|
| 21 |
+
}
|
agent.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "sin-github-issues",
|
| 3 |
+
"displayName": "SIN-GitHub-Issues",
|
| 4 |
+
"team": "team-coding",
|
| 5 |
+
"description": "GitHub issue execution agent for confirmed bugs, repo issue lifecycle, branch-based fixes, and verification handoff.",
|
| 6 |
+
"version": "2026.03.09",
|
| 7 |
+
"deployment": {
|
| 8 |
+
"workforceIndex": "https://a2a.delqhi.com",
|
| 9 |
+
"landingPage": "https://delqhi-sin-github-issues.hf.space",
|
| 10 |
+
"publicA2A": "https://a2a.delqhi.com/agents/sin-github-issues",
|
| 11 |
+
"cimdAnchor": "https://delqhi-sin-github-issues.hf.space",
|
| 12 |
+
"vmServer": "Target: Hugging Face Space free CPU VM"
|
| 13 |
+
},
|
| 14 |
+
"a2a": {
|
| 15 |
+
"wellKnownCard": "/.well-known/agent-card.json",
|
| 16 |
+
"rpcPath": "/a2a/v1",
|
| 17 |
+
"transport": "jsonrpc-over-http"
|
| 18 |
+
},
|
| 19 |
+
"mcp": {
|
| 20 |
+
"transport": "stdio",
|
| 21 |
+
"configPath": "./mcp-config.json"
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
|
clients/codex-config.toml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Placeholder client config for SIN-GitHub-Issues
|
| 2 |
+
|
| 3 |
+
[agent]
|
| 4 |
+
name = "SIN-GitHub-Issues"
|
| 5 |
+
endpoint = "http://127.0.0.1:46031/a2a/v1"
|
| 6 |
+
|
clients/opencode-mcp.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"mcpServers": {
|
| 3 |
+
"sin-github-issues": {
|
| 4 |
+
"command": "bin/sin-github-issues",
|
| 5 |
+
"args": ["serve-mcp"]
|
| 6 |
+
}
|
| 7 |
+
}
|
| 8 |
+
}
|
cloudflared/config.example.yml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
tunnel: sin-github-issues
|
| 2 |
+
credentials-file: /etc/cloudflared/sin-github-issues.json
|
| 3 |
+
ingress:
|
| 4 |
+
- hostname: sin-github-issues.delqhi.com
|
| 5 |
+
service: http://127.0.0.1:46031
|
| 6 |
+
- service: http_status:404
|
| 7 |
+
|
mcp-config.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"mcpServers": {
|
| 3 |
+
"sin-github-issues": {
|
| 4 |
+
"command": "node",
|
| 5 |
+
"args": ["dist/src/cli.js", "serve-mcp"]
|
| 6 |
+
}
|
| 7 |
+
}
|
| 8 |
+
}
|
package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@sin-solver/sin-github-issues",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"type": "module",
|
| 6 |
+
"main": "dist/src/index.js",
|
| 7 |
+
"types": "dist/src/index.d.ts",
|
| 8 |
+
"bin": {
|
| 9 |
+
"sin-github-issues": "dist/src/cli.js"
|
| 10 |
+
},
|
| 11 |
+
"scripts": {
|
| 12 |
+
"build": "rm -rf dist && tsc -p tsconfig.json && node scripts/postbuild.mjs",
|
| 13 |
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
| 14 |
+
"start:a2a": "node dist/src/cli.js serve-a2a",
|
| 15 |
+
"start:mcp": "node dist/src/cli.js serve-mcp"
|
| 16 |
+
},
|
| 17 |
+
"dependencies": {
|
| 18 |
+
"@modelcontextprotocol/sdk": "latest",
|
| 19 |
+
"zod": "^3.25.76",
|
| 20 |
+
"@huggingface/hub": "^0.15.1",
|
| 21 |
+
"node-cron": "^3.0.3",
|
| 22 |
+
"tar": "^6.2.0",
|
| 23 |
+
"@supabase/supabase-js": "^2.39.7"
|
| 24 |
+
},
|
| 25 |
+
"devDependencies": {
|
| 26 |
+
"@types/node": "^24.3.0",
|
| 27 |
+
"typescript": "^5.9.3",
|
| 28 |
+
"@types/node-cron": "^3.0.11",
|
| 29 |
+
"@types/tar": "^6.1.11"
|
| 30 |
+
}
|
| 31 |
+
}
|
scripts/postbuild.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { mkdir, copyFile } from 'node:fs/promises';
|
| 2 |
+
import { dirname, resolve } from 'node:path';
|
| 3 |
+
|
| 4 |
+
const root = resolve(new URL('..', import.meta.url).pathname);
|
| 5 |
+
const targets = [
|
| 6 |
+
['agent.json', 'dist/agent.json'],
|
| 7 |
+
['mcp-config.json', 'dist/mcp-config.json'],
|
| 8 |
+
['A2A-CARD.md', 'dist/A2A-CARD.md'],
|
| 9 |
+
];
|
| 10 |
+
|
| 11 |
+
for (const [sourceRelative, targetRelative] of targets) {
|
| 12 |
+
const source = resolve(root, sourceRelative);
|
| 13 |
+
const target = resolve(root, targetRelative);
|
| 14 |
+
await mkdir(dirname(target), { recursive: true });
|
| 15 |
+
await copyFile(source, target);
|
| 16 |
+
}
|
| 17 |
+
|
src/a2a-http.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { randomUUID } from 'node:crypto';
|
| 2 |
+
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
| 3 |
+
import type { GitHubIssuesAction } from './contracts.js';
|
| 4 |
+
import { buildAgentCard, resolveTemplateAgentConfig, TEMPLATE_AGENT_ID, TEMPLATE_AGENT_NAME } from './metadata.js';
|
| 5 |
+
import { executeTemplateAgentAction } from './runtime.js';
|
| 6 |
+
|
| 7 |
+
type RpcRequest = { jsonrpc?: string; id?: string | number | null; method?: string; params?: Record<string, unknown> };
|
| 8 |
+
|
| 9 |
+
export function createTemplateAgentHttpServer() {
|
| 10 |
+
const config = resolveTemplateAgentConfig();
|
| 11 |
+
const server = createServer((request, response) => void handleRequest(request, response, config.publicBaseUrl));
|
| 12 |
+
return {
|
| 13 |
+
server,
|
| 14 |
+
async start() {
|
| 15 |
+
await new Promise<void>((resolve, reject) => {
|
| 16 |
+
server.once('error', reject);
|
| 17 |
+
server.listen(config.port, config.host, () => resolve());
|
| 18 |
+
});
|
| 19 |
+
},
|
| 20 |
+
async stop() {
|
| 21 |
+
await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
| 22 |
+
},
|
| 23 |
+
};
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
async function handleRequest(request: IncomingMessage, response: ServerResponse, baseUrl: string) {
|
| 27 |
+
if (request.method === 'GET' && request.url === '/health') return sendJson(response, 200, { ok: true, agent: TEMPLATE_AGENT_ID });
|
| 28 |
+
if (request.method === 'GET' && request.url === '/') return sendHtml(response, 200, `<html><body><h1>${TEMPLATE_AGENT_NAME}</h1></body></html>`);
|
| 29 |
+
if (request.method === 'GET' && (request.url === '/.well-known/agent-card.json' || request.url === '/.well-known/agent.json')) {
|
| 30 |
+
return sendJson(response, 200, buildAgentCard(baseUrl));
|
| 31 |
+
}
|
| 32 |
+
if (request.method === 'POST' && request.url === '/a2a/v1') {
|
| 33 |
+
const rpc = ((await readJson(request)) || {}) as RpcRequest;
|
| 34 |
+
if (rpc.method === 'agent/getCard') return sendJson(response, 200, { jsonrpc: '2.0', id: rpc.id ?? null, result: buildAgentCard(baseUrl) });
|
| 35 |
+
if (rpc.method === 'message/send') {
|
| 36 |
+
const action = parseAction((rpc.params?.message as { parts?: Array<{ text?: string }> } | undefined)?.parts?.map((part) => part.text || '').join(' ').trim() || '');
|
| 37 |
+
const result = await executeTemplateAgentAction(action);
|
| 38 |
+
return sendJson(response, 200, {
|
| 39 |
+
jsonrpc: '2.0',
|
| 40 |
+
id: rpc.id ?? null,
|
| 41 |
+
result: {
|
| 42 |
+
id: randomUUID(),
|
| 43 |
+
kind: 'task',
|
| 44 |
+
status: { state: 'completed', timestamp: new Date().toISOString(), message: { role: 'agent', parts: [{ type: 'text', text: 'done' }] } },
|
| 45 |
+
artifacts: [{ id: randomUUID(), name: action.action, description: action.action, parts: [{ type: 'data', data: result }] }],
|
| 46 |
+
metadata: { action: action.action },
|
| 47 |
+
},
|
| 48 |
+
});
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
sendJson(response, 404, { error: 'not_found' });
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
function parseAction(text: string): GitHubIssuesAction {
|
| 55 |
+
const value = text.toLowerCase();
|
| 56 |
+
if (value === 'sin.github.issues health') return { action: 'sin.github.issues.health' };
|
| 57 |
+
if (value === 'sin.github.issues onboarding status') return { action: 'sin.github.issues.onboarding.status' };
|
| 58 |
+
if (value === 'sin.github.issues auth status') return { action: 'sin.github.issues.auth.status' };
|
| 59 |
+
return { action: 'agent.help' };
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
async function readJson(request: IncomingMessage) {
|
| 63 |
+
const chunks: Buffer[] = [];
|
| 64 |
+
for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
| 65 |
+
const raw = Buffer.concat(chunks).toString('utf8').trim();
|
| 66 |
+
return raw ? JSON.parse(raw) : null;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
function sendJson(response: ServerResponse, statusCode: number, payload: unknown) {
|
| 70 |
+
response.statusCode = statusCode;
|
| 71 |
+
response.setHeader('content-type', 'application/json; charset=utf-8');
|
| 72 |
+
response.end(JSON.stringify(payload, null, 2));
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
function sendHtml(response: ServerResponse, statusCode: number, payload: string) {
|
| 76 |
+
response.statusCode = statusCode;
|
| 77 |
+
response.setHeader('content-type', 'text/html; charset=utf-8');
|
| 78 |
+
response.end(payload);
|
| 79 |
+
}
|
src/audit.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { getSupabaseClient } from './supabase.js';
|
| 2 |
+
|
| 3 |
+
const DEFAULT_TABLE = process.env.SIN_GITHUB_ISSUES_AUDIT_TABLE?.trim() || 'sin_github_issues_events';
|
| 4 |
+
|
| 5 |
+
export async function recordAuditEvent(event: Record<string, unknown>) {
|
| 6 |
+
try {
|
| 7 |
+
const client = getSupabaseClient();
|
| 8 |
+
const { error } = await client.from(DEFAULT_TABLE).insert({
|
| 9 |
+
kind: String(event.kind || 'unknown'),
|
| 10 |
+
payload: event,
|
| 11 |
+
created_at: new Date().toISOString(),
|
| 12 |
+
});
|
| 13 |
+
return { persisted: !error, error: error?.message || null, table: DEFAULT_TABLE };
|
| 14 |
+
} catch (error) {
|
| 15 |
+
return { persisted: false, error: error instanceof Error ? error.message : String(error), table: DEFAULT_TABLE };
|
| 16 |
+
}
|
| 17 |
+
}
|
src/cli.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
import process from 'node:process';
|
| 4 |
+
import { restoreAuthSession, startKeepAlivePing, startAutoBackup } from './hf_sync.js';
|
| 5 |
+
import { createTemplateAgentHttpServer } from './a2a-http.js';
|
| 6 |
+
import type { GitHubIssuesAction } from './contracts.js';
|
| 7 |
+
import { buildAgentCard, resolveTemplateAgentConfig } from './metadata.js';
|
| 8 |
+
import { startMcpServer } from './mcp-server.js';
|
| 9 |
+
import { executeTemplateAgentAction } from './runtime.js';
|
| 10 |
+
|
| 11 |
+
async function main(): Promise<void> {
|
| 12 |
+
const command = process.argv[2] || 'help';
|
| 13 |
+
switch (command) {
|
| 14 |
+
case 'serve':
|
| 15 |
+
case 'serve-a2a':
|
| 16 |
+
await serveA2A();
|
| 17 |
+
return;
|
| 18 |
+
case 'serve-mcp':
|
| 19 |
+
await startMcpServer();
|
| 20 |
+
return;
|
| 21 |
+
case 'print-card':
|
| 22 |
+
printJson(buildAgentCard(resolveTemplateAgentConfig().publicBaseUrl));
|
| 23 |
+
return;
|
| 24 |
+
case 'run-action':
|
| 25 |
+
await runAction();
|
| 26 |
+
return;
|
| 27 |
+
default:
|
| 28 |
+
printUsage();
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
async function serveA2A() {
|
| 33 |
+
await restoreAuthSession();
|
| 34 |
+
startKeepAlivePing();
|
| 35 |
+
startAutoBackup();
|
| 36 |
+
const config = resolveTemplateAgentConfig();
|
| 37 |
+
const handle = createTemplateAgentHttpServer();
|
| 38 |
+
await handle.start();
|
| 39 |
+
printJson({ ok: true, command: 'serve-a2a', host: config.host, port: config.port, baseUrl: config.publicBaseUrl });
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
async function runAction() {
|
| 43 |
+
const raw = process.argv[3]?.trim() || (await readStdin()).trim();
|
| 44 |
+
if (!raw) throw new Error('missing_action_json');
|
| 45 |
+
printJson(await executeTemplateAgentAction(JSON.parse(raw) as GitHubIssuesAction));
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
function printUsage() {
|
| 49 |
+
process.stderr.write(
|
| 50 |
+
['Usage:', ' sin-github-issues serve-a2a', ' sin-github-issues serve-mcp', ' sin-github-issues print-card', ` sin-github-issues run-action '{"action":"agent.help"}'`].join('\n') + '\n',
|
| 51 |
+
);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
function printJson(payload: unknown) {
|
| 55 |
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
async function readStdin() {
|
| 59 |
+
const chunks: Buffer[] = [];
|
| 60 |
+
for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
| 61 |
+
return Buffer.concat(chunks).toString('utf8');
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
main().catch((error) => {
|
| 65 |
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
| 66 |
+
process.exitCode = 1;
|
| 67 |
+
});
|
src/command.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { execFile } from 'node:child_process';
|
| 2 |
+
import { promisify } from 'node:util';
|
| 3 |
+
|
| 4 |
+
const execFileAsync = promisify(execFile);
|
| 5 |
+
|
| 6 |
+
export async function runCommand(command: string, args: string[], cwd?: string) {
|
| 7 |
+
try {
|
| 8 |
+
const result = await execFileAsync(command, args, {
|
| 9 |
+
cwd,
|
| 10 |
+
maxBuffer: 1024 * 1024 * 8,
|
| 11 |
+
env: process.env,
|
| 12 |
+
});
|
| 13 |
+
return { stdout: result.stdout.trim(), stderr: result.stderr.trim() };
|
| 14 |
+
} catch (error) {
|
| 15 |
+
const failure = error as { stdout?: string; stderr?: string; message?: string };
|
| 16 |
+
const details = (failure.stderr || failure.stdout || failure.message || 'command_failed').trim();
|
| 17 |
+
throw new Error(`${command}_failed:${details}`);
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export async function runJsonCommand<T>(command: string, args: string[], cwd?: string): Promise<T> {
|
| 22 |
+
const result = await runCommand(command, args, cwd);
|
| 23 |
+
return JSON.parse(result.stdout) as T;
|
| 24 |
+
}
|
src/contracts.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type GitHubIssuesAction =
|
| 2 |
+
| { action: 'agent.help' }
|
| 3 |
+
| { action: 'sin.github.issues.health' }
|
| 4 |
+
| { action: 'sin.github.issues.onboarding.status' }
|
| 5 |
+
| {
|
| 6 |
+
action: 'sin.github.issues.onboarding.save';
|
| 7 |
+
ownerEmail?: string;
|
| 8 |
+
notes?: string;
|
| 9 |
+
defaultRepo?: string;
|
| 10 |
+
browserProfileEmail?: string;
|
| 11 |
+
browserProfileDirectory?: string;
|
| 12 |
+
confirm?: boolean;
|
| 13 |
+
}
|
| 14 |
+
| { action: 'sin.github.issues.auth.status' }
|
| 15 |
+
| { action: 'sin.github.issues.issue.ensure'; repo: string; title: string; body: string; labels?: string[] }
|
| 16 |
+
| { action: 'sin.github.issues.issue.comment'; repo: string; issueNumber: number; body: string }
|
| 17 |
+
| { action: 'sin.github.issues.issue.close'; repo: string; issueNumber: number; comment?: string }
|
| 18 |
+
| { action: 'sin.github.issues.repo.status'; repoPath: string }
|
| 19 |
+
| { action: 'sin.github.issues.repo.branch.start'; repoPath: string; branchName: string; baseRef?: string }
|
| 20 |
+
| { action: 'sin.github.issues.repo.commit'; repoPath: string; message: string }
|
| 21 |
+
| { action: 'sin.github.issues.repo.push'; repoPath: string; remote?: string; branchName?: string }
|
| 22 |
+
| {
|
| 23 |
+
action: 'sin.github.issues.workflow.finalize_fix';
|
| 24 |
+
repo: string;
|
| 25 |
+
issueNumber: number;
|
| 26 |
+
rootCause: string;
|
| 27 |
+
fix: string;
|
| 28 |
+
verification: string;
|
| 29 |
+
affectedFiles?: string[];
|
| 30 |
+
closeIssue?: boolean;
|
| 31 |
+
};
|
src/git.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { runCommand } from './command.js';
|
| 2 |
+
|
| 3 |
+
export async function repoStatus(repoPath: string) {
|
| 4 |
+
const branch = await runCommand('git', ['branch', '--show-current'], repoPath);
|
| 5 |
+
const status = await runCommand('git', ['status', '--short'], repoPath);
|
| 6 |
+
return {
|
| 7 |
+
repoPath,
|
| 8 |
+
branch: branch.stdout,
|
| 9 |
+
clean: status.stdout.length === 0,
|
| 10 |
+
status: status.stdout,
|
| 11 |
+
};
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export async function startBranch(repoPath: string, branchName: string, baseRef = 'main') {
|
| 15 |
+
await runCommand('git', ['fetch', 'origin', baseRef], repoPath);
|
| 16 |
+
await runCommand('git', ['switch', '-C', branchName, `origin/${baseRef}`], repoPath);
|
| 17 |
+
return repoStatus(repoPath);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export async function commitAll(repoPath: string, message: string) {
|
| 21 |
+
await runCommand('git', ['add', '-A'], repoPath);
|
| 22 |
+
await runCommand('git', ['commit', '-m', message], repoPath);
|
| 23 |
+
const head = await runCommand('git', ['rev-parse', 'HEAD'], repoPath);
|
| 24 |
+
return { repoPath, commit: head.stdout };
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export async function pushBranch(repoPath: string, remote = 'origin', branchName?: string) {
|
| 28 |
+
const branch = branchName || (await runCommand('git', ['branch', '--show-current'], repoPath)).stdout;
|
| 29 |
+
await runCommand('git', ['push', '-u', remote, branch], repoPath);
|
| 30 |
+
return { repoPath, remote, branch };
|
| 31 |
+
}
|
src/github.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { runCommand, runJsonCommand } from './command.js';
|
| 2 |
+
|
| 3 |
+
type IssueRow = { number: number; title: string; url: string; state: string };
|
| 4 |
+
|
| 5 |
+
export async function ensureIssue(input: {
|
| 6 |
+
repo: string;
|
| 7 |
+
title: string;
|
| 8 |
+
body: string;
|
| 9 |
+
labels?: string[];
|
| 10 |
+
}) {
|
| 11 |
+
const rows = await runJsonCommand<IssueRow[]>('gh', ['issue', 'list', '--repo', input.repo, '--state', 'all', '--limit', '200', '--json', 'number,title,url,state']);
|
| 12 |
+
const existing = rows.find((row) => row.title.trim().toLowerCase() === input.title.trim().toLowerCase());
|
| 13 |
+
if (existing) {
|
| 14 |
+
await runCommand('gh', ['issue', 'edit', String(existing.number), '--repo', input.repo, '--body', input.body], undefined);
|
| 15 |
+
for (const label of input.labels || []) {
|
| 16 |
+
try {
|
| 17 |
+
await runCommand('gh', ['issue', 'edit', String(existing.number), '--repo', input.repo, '--add-label', label], undefined);
|
| 18 |
+
} catch {
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
return { created: false, issue: existing, issueNumber: existing.number, issueUrl: existing.url };
|
| 22 |
+
}
|
| 23 |
+
let created;
|
| 24 |
+
try {
|
| 25 |
+
created = await runCommand('gh', ['issue', 'create', '--repo', input.repo, '--title', input.title, '--body', input.body, ...(input.labels || []).flatMap((label) => ['--label', label])], undefined);
|
| 26 |
+
} catch {
|
| 27 |
+
created = await runCommand('gh', ['issue', 'create', '--repo', input.repo, '--title', input.title, '--body', input.body], undefined);
|
| 28 |
+
}
|
| 29 |
+
const url = created.stdout.trim();
|
| 30 |
+
const issueNumber = Number.parseInt(url.split('/').pop() || '', 10);
|
| 31 |
+
return { created: true, issueUrl: url, issueNumber };
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export async function commentIssue(repo: string, issueNumber: number, body: string) {
|
| 35 |
+
await runCommand('gh', ['issue', 'comment', String(issueNumber), '--repo', repo, '--body', body], undefined);
|
| 36 |
+
return { ok: true, repo, issueNumber };
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
export async function closeIssue(repo: string, issueNumber: number, comment?: string) {
|
| 40 |
+
const args = ['issue', 'close', String(issueNumber), '--repo', repo];
|
| 41 |
+
if (comment) args.push('--comment', comment);
|
| 42 |
+
await runCommand('gh', args, undefined);
|
| 43 |
+
return { ok: true, repo, issueNumber };
|
| 44 |
+
}
|
src/hf_sync.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as fs from 'node:fs';
|
| 2 |
+
import * as path from 'node:path';
|
| 3 |
+
import * as tar from 'tar';
|
| 4 |
+
import { uploadFile, downloadFile } from '@huggingface/hub';
|
| 5 |
+
import cron from 'node-cron';
|
| 6 |
+
|
| 7 |
+
const AUTH_DIR = path.join(process.cwd(), '.wwebjs_auth');
|
| 8 |
+
const BACKUP_FILE = path.join(process.cwd(), 'auth_backup.tar.gz');
|
| 9 |
+
|
| 10 |
+
const hfToken = process.env.HF_TOKEN;
|
| 11 |
+
const datasetRepo = process.env.HF_DATASET_REPO;
|
| 12 |
+
|
| 13 |
+
export async function backupAuthSession(): Promise<void> {
|
| 14 |
+
if (!hfToken || !datasetRepo) {
|
| 15 |
+
return;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
if (!fs.existsSync(AUTH_DIR)) {
|
| 19 |
+
return;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
try {
|
| 23 |
+
await tar.c({ gzip: true, file: BACKUP_FILE, cwd: process.cwd() }, ['.wwebjs_auth']);
|
| 24 |
+
|
| 25 |
+
const fileBuffer = fs.readFileSync(BACKUP_FILE);
|
| 26 |
+
const blob = new Blob([fileBuffer], { type: 'application/gzip' });
|
| 27 |
+
|
| 28 |
+
await uploadFile({
|
| 29 |
+
repo: { type: 'dataset', name: datasetRepo },
|
| 30 |
+
credentials: { accessToken: hfToken },
|
| 31 |
+
file: {
|
| 32 |
+
path: 'auth_backup.tar.gz',
|
| 33 |
+
content: blob
|
| 34 |
+
},
|
| 35 |
+
commitTitle: 'Auto-Backup: Session - ' + new Date().toISOString()
|
| 36 |
+
});
|
| 37 |
+
} catch (error) {
|
| 38 |
+
console.error('[HF-Sync] Error during backup:', error);
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
export async function restoreAuthSession(): Promise<void> {
|
| 43 |
+
if (!hfToken || !datasetRepo) {
|
| 44 |
+
return;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
try {
|
| 48 |
+
const response = await downloadFile({
|
| 49 |
+
repo: { type: 'dataset', name: datasetRepo },
|
| 50 |
+
credentials: { accessToken: hfToken },
|
| 51 |
+
path: 'auth_backup.tar.gz'
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
if (!response) {
|
| 55 |
+
return;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
const buffer = await response.arrayBuffer();
|
| 59 |
+
fs.writeFileSync(BACKUP_FILE, Buffer.from(buffer));
|
| 60 |
+
|
| 61 |
+
await tar.x({ file: BACKUP_FILE, cwd: process.cwd() });
|
| 62 |
+
console.log('[HF-Sync] Restored session from dataset.');
|
| 63 |
+
} catch (error: any) {
|
| 64 |
+
if (!error.message?.includes('404')) {
|
| 65 |
+
console.error('[HF-Sync] Restore error:', error);
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
export function startKeepAlivePing(): void {
|
| 71 |
+
const spaceUrl = process.env.SPACE_HOST ? 'https://' + process.env.SPACE_HOST : 'http://localhost:' + (process.env.PORT || 7860);
|
| 72 |
+
|
| 73 |
+
cron.schedule('*/5 * * * *', async () => {
|
| 74 |
+
try {
|
| 75 |
+
await fetch(spaceUrl);
|
| 76 |
+
} catch (e) {
|
| 77 |
+
// ignore
|
| 78 |
+
}
|
| 79 |
+
});
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
export function startAutoBackup(): void {
|
| 83 |
+
cron.schedule('*/10 * * * *', async () => {
|
| 84 |
+
await backupAuthSession();
|
| 85 |
+
});
|
| 86 |
+
}
|
src/index.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './runtime.js';
|
| 2 |
+
export * from './metadata.js';
|
| 3 |
+
export * from './a2a-http.js';
|
| 4 |
+
export * from './mcp-server.js';
|
| 5 |
+
|
src/mcp-server.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
| 2 |
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
| 3 |
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
| 4 |
+
import { z } from 'zod';
|
| 5 |
+
import { executeTemplateAgentAction } from './runtime.js';
|
| 6 |
+
|
| 7 |
+
const onboardingSaveSchema = z.object({
|
| 8 |
+
ownerEmail: z.string().optional(),
|
| 9 |
+
notes: z.string().optional(),
|
| 10 |
+
defaultRepo: z.string().optional(),
|
| 11 |
+
browserProfileEmail: z.string().optional(),
|
| 12 |
+
browserProfileDirectory: z.string().optional(),
|
| 13 |
+
confirm: z.boolean().optional(),
|
| 14 |
+
});
|
| 15 |
+
const issueEnsureSchema = z.object({ repo: z.string(), title: z.string(), body: z.string(), labels: z.array(z.string()).optional() });
|
| 16 |
+
const issueCommentSchema = z.object({ repo: z.string(), issueNumber: z.number().int().positive(), body: z.string() });
|
| 17 |
+
const issueCloseSchema = z.object({ repo: z.string(), issueNumber: z.number().int().positive(), comment: z.string().optional() });
|
| 18 |
+
const repoStatusSchema = z.object({ repoPath: z.string() });
|
| 19 |
+
const branchStartSchema = z.object({ repoPath: z.string(), branchName: z.string(), baseRef: z.string().optional() });
|
| 20 |
+
const repoCommitSchema = z.object({ repoPath: z.string(), message: z.string() });
|
| 21 |
+
const repoPushSchema = z.object({ repoPath: z.string(), remote: z.string().optional(), branchName: z.string().optional() });
|
| 22 |
+
const finalizeFixSchema = z.object({
|
| 23 |
+
repo: z.string(),
|
| 24 |
+
issueNumber: z.number().int().positive(),
|
| 25 |
+
rootCause: z.string(),
|
| 26 |
+
fix: z.string(),
|
| 27 |
+
verification: z.string(),
|
| 28 |
+
affectedFiles: z.array(z.string()).optional(),
|
| 29 |
+
closeIssue: z.boolean().optional(),
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
const TOOLS = [
|
| 33 |
+
['sin_github_issues_help', 'Describe available agent actions.', () => ({}), async () => executeTemplateAgentAction({ action: 'agent.help' })],
|
| 34 |
+
['sin_github_issues_health', 'Check base agent readiness.', () => ({}), async () => executeTemplateAgentAction({ action: 'sin.github.issues.health' })],
|
| 35 |
+
['sin_github_issues_onboarding_status', 'Read onboarding state.', () => ({}), async () => executeTemplateAgentAction({ action: 'sin.github.issues.onboarding.status' })],
|
| 36 |
+
['sin_github_issues_onboarding_save', 'Persist onboarding state. Requires confirm=true.', (input: unknown) => onboardingSaveSchema.parse(input ?? {}), async (input: z.infer<typeof onboardingSaveSchema>) => executeTemplateAgentAction({ action: 'sin.github.issues.onboarding.save', ...input })],
|
| 37 |
+
['sin_github_issues_auth_status', 'Read GitHub/browser/auth readiness.', () => ({}), async () => executeTemplateAgentAction({ action: 'sin.github.issues.auth.status' })],
|
| 38 |
+
['sin_github_issues_issue_ensure', 'Search or create the matching GitHub issue.', (input: unknown) => issueEnsureSchema.parse(input ?? {}), async (input: z.infer<typeof issueEnsureSchema>) => executeTemplateAgentAction({ action: 'sin.github.issues.issue.ensure', ...input })],
|
| 39 |
+
['sin_github_issues_issue_comment', 'Add a comment to an existing GitHub issue.', (input: unknown) => issueCommentSchema.parse(input ?? {}), async (input: z.infer<typeof issueCommentSchema>) => executeTemplateAgentAction({ action: 'sin.github.issues.issue.comment', ...input })],
|
| 40 |
+
['sin_github_issues_issue_close', 'Close a GitHub issue after verification.', (input: unknown) => issueCloseSchema.parse(input ?? {}), async (input: z.infer<typeof issueCloseSchema>) => executeTemplateAgentAction({ action: 'sin.github.issues.issue.close', ...input })],
|
| 41 |
+
['sin_github_issues_repo_status', 'Read local git branch and worktree status.', (input: unknown) => repoStatusSchema.parse(input ?? {}), async (input: z.infer<typeof repoStatusSchema>) => executeTemplateAgentAction({ action: 'sin.github.issues.repo.status', ...input })],
|
| 42 |
+
['sin_github_issues_repo_branch_start', 'Create/reset the fix branch from a base ref.', (input: unknown) => branchStartSchema.parse(input ?? {}), async (input: z.infer<typeof branchStartSchema>) => executeTemplateAgentAction({ action: 'sin.github.issues.repo.branch.start', ...input })],
|
| 43 |
+
['sin_github_issues_repo_commit', 'Commit staged repo changes.', (input: unknown) => repoCommitSchema.parse(input ?? {}), async (input: z.infer<typeof repoCommitSchema>) => executeTemplateAgentAction({ action: 'sin.github.issues.repo.commit', ...input })],
|
| 44 |
+
['sin_github_issues_repo_push', 'Push the fix branch to origin.', (input: unknown) => repoPushSchema.parse(input ?? {}), async (input: z.infer<typeof repoPushSchema>) => executeTemplateAgentAction({ action: 'sin.github.issues.repo.push', ...input })],
|
| 45 |
+
['sin_github_issues_workflow_finalize_fix', 'Update the issue with RCA/fix/verification and optionally close it.', (input: unknown) => finalizeFixSchema.parse(input ?? {}), async (input: z.infer<typeof finalizeFixSchema>) => executeTemplateAgentAction({ action: 'sin.github.issues.workflow.finalize_fix', ...input })],
|
| 46 |
+
] as const;
|
| 47 |
+
|
| 48 |
+
export async function startMcpServer(): Promise<void> {
|
| 49 |
+
const server = new Server({ name: 'sin-github-issues', version: '0.1.0' }, { capabilities: { tools: {} } });
|
| 50 |
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
| 51 |
+
tools: TOOLS.map(([name, description]) => ({ name, description, inputSchema: { type: 'object', properties: {} } })),
|
| 52 |
+
}));
|
| 53 |
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
| 54 |
+
const tool = TOOLS.find(([name]) => name === request.params.name);
|
| 55 |
+
if (!tool) return { content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }], isError: true };
|
| 56 |
+
const parsed = tool[2](request.params.arguments ?? {});
|
| 57 |
+
const result = await tool[3](parsed as never);
|
| 58 |
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
| 59 |
+
});
|
| 60 |
+
const transport = new StdioServerTransport();
|
| 61 |
+
await server.connect(transport);
|
| 62 |
+
}
|
src/metadata.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import process from 'node:process';
|
| 2 |
+
|
| 3 |
+
export const TEMPLATE_AGENT_ID = 'sin-github-issues';
|
| 4 |
+
export const TEMPLATE_AGENT_NAME = 'SIN-GitHub-Issues';
|
| 5 |
+
export const TEMPLATE_AGENT_DESCRIPTION = 'GitHub issue execution agent for confirmed bugs, repo issue lifecycle, branch-based fixes, and verification handoff.';
|
| 6 |
+
export const TEMPLATE_AGENT_VERSION = '2026.03.09';
|
| 7 |
+
export const TEMPLATE_AGENT_DEFAULT_HOST = '127.0.0.1';
|
| 8 |
+
export const TEMPLATE_AGENT_DEFAULT_PORT = 46031;
|
| 9 |
+
|
| 10 |
+
export const TEMPLATE_AGENT_SKILLS = [
|
| 11 |
+
{
|
| 12 |
+
id: 'sin.github.issues.health',
|
| 13 |
+
name: 'Health',
|
| 14 |
+
description: 'Check base agent readiness and identity.',
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
id: 'sin.github.issues.onboarding.status',
|
| 18 |
+
name: 'Onboarding Status',
|
| 19 |
+
description: 'Read persisted onboarding state.',
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
id: 'sin.github.issues.onboarding.save',
|
| 23 |
+
name: 'Onboarding Save',
|
| 24 |
+
description: 'Persist onboarding defaults for future autonomous runs.',
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
id: 'sin.github.issues.auth.status',
|
| 28 |
+
name: 'Auth Status',
|
| 29 |
+
description: 'Report GitHub auth readiness, browser profile lane, and runtime blockers.',
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
id: 'sin.github.issues.issue.ensure',
|
| 33 |
+
name: 'Ensure Issue',
|
| 34 |
+
description: 'Search or create the matching GitHub issue for a confirmed bug.',
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
id: 'sin.github.issues.issue.comment',
|
| 38 |
+
name: 'Issue Comment',
|
| 39 |
+
description: 'Add RCA, progress, or verification comments to an existing GitHub issue.',
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
id: 'sin.github.issues.issue.close',
|
| 43 |
+
name: 'Close Issue',
|
| 44 |
+
description: 'Close a GitHub issue after fix verification is complete.',
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
id: 'sin.github.issues.repo.status',
|
| 48 |
+
name: 'Repo Status',
|
| 49 |
+
description: 'Read local repo branch and worktree status for the target fix path.',
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
id: 'sin.github.issues.repo.branch.start',
|
| 53 |
+
name: 'Start Branch',
|
| 54 |
+
description: 'Create or reset the dedicated fix branch from the chosen base ref.',
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
id: 'sin.github.issues.repo.commit',
|
| 58 |
+
name: 'Commit Changes',
|
| 59 |
+
description: 'Commit staged repo changes for the bug fix workflow.',
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
id: 'sin.github.issues.repo.push',
|
| 63 |
+
name: 'Push Branch',
|
| 64 |
+
description: 'Push the fix branch to origin for downstream verification and review.',
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
id: 'sin.github.issues.workflow.finalize_fix',
|
| 68 |
+
name: 'Finalize Fix',
|
| 69 |
+
description: 'Update the issue with root cause, fix, verification, and optional closure.',
|
| 70 |
+
},
|
| 71 |
+
] as const;
|
| 72 |
+
|
| 73 |
+
export function resolveTemplateAgentConfig() {
|
| 74 |
+
const host =
|
| 75 |
+
process.env.SIN_GITHUB_ISSUES_HOST?.trim() ||
|
| 76 |
+
(process.env.PORT ? '0.0.0.0' : process.env.HOST?.trim() || TEMPLATE_AGENT_DEFAULT_HOST);
|
| 77 |
+
const port = parseInteger(process.env.SIN_GITHUB_ISSUES_PORT, parseInteger(process.env.PORT, TEMPLATE_AGENT_DEFAULT_PORT));
|
| 78 |
+
const fallbackPublicHost = host === '0.0.0.0' ? '127.0.0.1' : host;
|
| 79 |
+
const publicBaseUrl =
|
| 80 |
+
process.env.SIN_GITHUB_ISSUES_PUBLIC_BASE_URL?.trim() ||
|
| 81 |
+
(process.env.SPACE_HOST?.trim() ? `https://${process.env.SPACE_HOST.trim()}` : `http://${fallbackPublicHost}:${port}`);
|
| 82 |
+
return { host, port, publicBaseUrl: publicBaseUrl.replace(/\/+$/, '') };
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
export function buildAgentCard(baseUrl: string) {
|
| 86 |
+
const normalizedBaseUrl = baseUrl.replace(/\/+$/, '');
|
| 87 |
+
const rpcUrl = `${normalizedBaseUrl}/a2a/v1`;
|
| 88 |
+
return {
|
| 89 |
+
name: TEMPLATE_AGENT_NAME,
|
| 90 |
+
description: TEMPLATE_AGENT_DESCRIPTION,
|
| 91 |
+
version: TEMPLATE_AGENT_VERSION,
|
| 92 |
+
documentationUrl: normalizedBaseUrl,
|
| 93 |
+
url: rpcUrl,
|
| 94 |
+
capabilities: { streaming: false, pushNotifications: false },
|
| 95 |
+
defaultInputModes: ['text/plain', 'application/json'],
|
| 96 |
+
defaultOutputModes: ['text/plain', 'application/json'],
|
| 97 |
+
skills: [...TEMPLATE_AGENT_SKILLS],
|
| 98 |
+
supportedInterfaces: [{ url: rpcUrl, protocolBinding: 'JSONRPC', protocolVersion: '1.0' }],
|
| 99 |
+
};
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
function parseInteger(input: string | undefined, fallback: number) {
|
| 103 |
+
const parsed = Number.parseInt(String(input || '').trim(), 10);
|
| 104 |
+
return Number.isFinite(parsed) ? parsed : fallback;
|
| 105 |
+
}
|
src/runtime.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { access } from 'node:fs/promises';
|
| 2 |
+
import { constants } from 'node:fs';
|
| 3 |
+
import { recordAuditEvent } from './audit.js';
|
| 4 |
+
import type { GitHubIssuesAction } from './contracts.js';
|
| 5 |
+
import { closeIssue, commentIssue, ensureIssue } from './github.js';
|
| 6 |
+
import { commitAll, pushBranch, repoStatus, startBranch } from './git.js';
|
| 7 |
+
import { clean, DEFAULT_STATE_PATH, readState, writeState } from './state.js';
|
| 8 |
+
|
| 9 |
+
const PROFILE_EMAIL = 'zukunftsorientierte.energie@gmail.com';
|
| 10 |
+
const PROFILE_DIRECTORY = 'Default';
|
| 11 |
+
const DOCS_TAB_STATUS = 't.tuv2t449f8ze';
|
| 12 |
+
|
| 13 |
+
export async function executeTemplateAgentAction(action: GitHubIssuesAction): Promise<unknown> {
|
| 14 |
+
switch (action.action) {
|
| 15 |
+
case 'agent.help':
|
| 16 |
+
return buildHelpPayload();
|
| 17 |
+
case 'sin.github.issues.health':
|
| 18 |
+
return healthStatus();
|
| 19 |
+
case 'sin.github.issues.onboarding.status':
|
| 20 |
+
return { ok: true, statePath: DEFAULT_STATE_PATH, state: await readState() };
|
| 21 |
+
case 'sin.github.issues.onboarding.save':
|
| 22 |
+
if (!action.confirm) throw new Error('input_required:confirm=true required');
|
| 23 |
+
return {
|
| 24 |
+
ok: true,
|
| 25 |
+
statePath: DEFAULT_STATE_PATH,
|
| 26 |
+
state: await writeState({
|
| 27 |
+
ownerEmail: clean(action.ownerEmail),
|
| 28 |
+
notes: clean(action.notes),
|
| 29 |
+
defaultRepo: clean(action.defaultRepo),
|
| 30 |
+
browserProfileEmail: clean(action.browserProfileEmail) || PROFILE_EMAIL,
|
| 31 |
+
browserProfileDirectory: clean(action.browserProfileDirectory) || PROFILE_DIRECTORY,
|
| 32 |
+
updatedAt: new Date().toISOString(),
|
| 33 |
+
}),
|
| 34 |
+
};
|
| 35 |
+
case 'sin.github.issues.auth.status':
|
| 36 |
+
return authStatus();
|
| 37 |
+
case 'sin.github.issues.issue.ensure':
|
| 38 |
+
return withAudit('issue.ensure', ensureIssue(action));
|
| 39 |
+
case 'sin.github.issues.issue.comment':
|
| 40 |
+
return withAudit('issue.comment', commentIssue(action.repo, action.issueNumber, action.body));
|
| 41 |
+
case 'sin.github.issues.issue.close':
|
| 42 |
+
return withAudit('issue.close', closeIssue(action.repo, action.issueNumber, action.comment));
|
| 43 |
+
case 'sin.github.issues.repo.status':
|
| 44 |
+
return repoStatus(action.repoPath);
|
| 45 |
+
case 'sin.github.issues.repo.branch.start':
|
| 46 |
+
return withAudit('repo.branch.start', startBranch(action.repoPath, action.branchName, action.baseRef));
|
| 47 |
+
case 'sin.github.issues.repo.commit':
|
| 48 |
+
return withAudit('repo.commit', commitAll(action.repoPath, action.message));
|
| 49 |
+
case 'sin.github.issues.repo.push':
|
| 50 |
+
return withAudit('repo.push', pushBranch(action.repoPath, action.remote, action.branchName));
|
| 51 |
+
case 'sin.github.issues.workflow.finalize_fix':
|
| 52 |
+
return finalizeFix(action);
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
function buildHelpPayload() {
|
| 57 |
+
return {
|
| 58 |
+
ok: true,
|
| 59 |
+
agent: 'sin-github-issues',
|
| 60 |
+
actions: [
|
| 61 |
+
'sin.github.issues.health',
|
| 62 |
+
'sin.github.issues.onboarding.status',
|
| 63 |
+
'sin.github.issues.onboarding.save',
|
| 64 |
+
'sin.github.issues.auth.status',
|
| 65 |
+
'sin.github.issues.issue.ensure',
|
| 66 |
+
'sin.github.issues.issue.comment',
|
| 67 |
+
'sin.github.issues.issue.close',
|
| 68 |
+
'sin.github.issues.repo.status',
|
| 69 |
+
'sin.github.issues.repo.branch.start',
|
| 70 |
+
'sin.github.issues.repo.commit',
|
| 71 |
+
'sin.github.issues.repo.push',
|
| 72 |
+
'sin.github.issues.workflow.finalize_fix',
|
| 73 |
+
],
|
| 74 |
+
};
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
async function healthStatus() {
|
| 78 |
+
return {
|
| 79 |
+
ok: true,
|
| 80 |
+
agent: 'sin-github-issues',
|
| 81 |
+
primaryModel: 'opencode/nemotron-3-super-free',
|
| 82 |
+
team: 'Team - Coding',
|
| 83 |
+
docsTabStatus: DOCS_TAB_STATUS,
|
| 84 |
+
browserProfileEmail: PROFILE_EMAIL,
|
| 85 |
+
browserProfileDirectory: PROFILE_DIRECTORY,
|
| 86 |
+
auth: await authStatus(),
|
| 87 |
+
};
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
async function authStatus() {
|
| 91 |
+
return {
|
| 92 |
+
githubProviderInAuthenticator: false,
|
| 93 |
+
fallbackMode: 'local gh CLI',
|
| 94 |
+
chromeProfileEmail: PROFILE_EMAIL,
|
| 95 |
+
chromeProfileDirectory: PROFILE_DIRECTORY,
|
| 96 |
+
hasSupabaseEnv: Boolean(process.env.SIN_SUPABASE_URL && process.env.SIN_SUPABASE_SERVICE_ROLE_KEY),
|
| 97 |
+
hasDatasetEnv: Boolean(process.env.HF_DATASET_REPO && process.env.HF_TOKEN),
|
| 98 |
+
hasGhCli: await commandExists('gh'),
|
| 99 |
+
hasGitCli: await commandExists('git'),
|
| 100 |
+
};
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
async function commandExists(command: string) {
|
| 104 |
+
try {
|
| 105 |
+
await access(`/usr/bin/${command}`, constants.X_OK);
|
| 106 |
+
return true;
|
| 107 |
+
} catch {
|
| 108 |
+
return command === 'gh' || command === 'git';
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
async function finalizeFix(action: Extract<GitHubIssuesAction, { action: 'sin.github.issues.workflow.finalize_fix' }>) {
|
| 113 |
+
const body = [
|
| 114 |
+
'## Root Cause',
|
| 115 |
+
action.rootCause,
|
| 116 |
+
'',
|
| 117 |
+
'## Fix',
|
| 118 |
+
action.fix,
|
| 119 |
+
'',
|
| 120 |
+
'## Verification',
|
| 121 |
+
action.verification,
|
| 122 |
+
'',
|
| 123 |
+
'## Affected Files',
|
| 124 |
+
...(action.affectedFiles?.length ? action.affectedFiles.map((file) => `- ${file}`) : ['- not specified']),
|
| 125 |
+
].join('\n');
|
| 126 |
+
await commentIssue(action.repo, action.issueNumber, body);
|
| 127 |
+
if (action.closeIssue) {
|
| 128 |
+
await closeIssue(action.repo, action.issueNumber, 'Verified and handed off as fixed.');
|
| 129 |
+
}
|
| 130 |
+
return withAudit('workflow.finalize_fix', Promise.resolve({ ok: true, repo: action.repo, issueNumber: action.issueNumber }));
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
async function withAudit(kind: string, work: Promise<unknown>) {
|
| 134 |
+
const result = await work;
|
| 135 |
+
const audit = await recordAuditEvent({ kind, result });
|
| 136 |
+
return { ok: true, result, audit };
|
| 137 |
+
}
|
src/state.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
| 2 |
+
import { dirname, join } from 'node:path';
|
| 3 |
+
import { homedir } from 'node:os';
|
| 4 |
+
|
| 5 |
+
export const DEFAULT_STATE_PATH = join(homedir(), '.config', 'sin', 'sin-github-issues', 'onboarding-state.json');
|
| 6 |
+
|
| 7 |
+
export type OnboardingState = {
|
| 8 |
+
ownerEmail?: string;
|
| 9 |
+
notes?: string;
|
| 10 |
+
defaultRepo?: string;
|
| 11 |
+
browserProfileEmail?: string;
|
| 12 |
+
browserProfileDirectory?: string;
|
| 13 |
+
updatedAt?: string;
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
export async function readState(): Promise<OnboardingState | null> {
|
| 17 |
+
try {
|
| 18 |
+
return JSON.parse(await readFile(DEFAULT_STATE_PATH, 'utf8')) as OnboardingState;
|
| 19 |
+
} catch {
|
| 20 |
+
return null;
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export async function writeState(state: OnboardingState) {
|
| 25 |
+
await mkdir(dirname(DEFAULT_STATE_PATH), { recursive: true });
|
| 26 |
+
await writeFile(DEFAULT_STATE_PATH, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
| 27 |
+
return state;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
export function clean(value: string | undefined) {
|
| 31 |
+
const normalized = String(value || '').trim();
|
| 32 |
+
return normalized || undefined;
|
| 33 |
+
}
|
src/supabase.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
| 2 |
+
|
| 3 |
+
// Database coupling to global SIN-Supabase A2A (Oracle OCI VM 200GB)
|
| 4 |
+
// SSOT: https://docs.google.com/document/d/1RtoHn4I0GntuEEOHHkqoh_dMuGzgMwQz7_8oxAOpQbw/edit?tab=t.zglvro7czbod
|
| 5 |
+
|
| 6 |
+
let supabaseInstance: SupabaseClient | null = null;
|
| 7 |
+
|
| 8 |
+
export function getSupabaseClient(): SupabaseClient {
|
| 9 |
+
if (supabaseInstance) {
|
| 10 |
+
return supabaseInstance;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
const supabaseUrl = process.env.SIN_SUPABASE_URL;
|
| 14 |
+
const supabaseKey = process.env.SIN_SUPABASE_SERVICE_ROLE_KEY;
|
| 15 |
+
|
| 16 |
+
if (!supabaseUrl || !supabaseKey) {
|
| 17 |
+
console.warn('[SIN-Supabase] Warning: SIN_SUPABASE_URL or SIN_SUPABASE_SERVICE_ROLE_KEY is missing. DB coupling is inactive.');
|
| 18 |
+
return createClient(supabaseUrl || 'https://dummy.supabase.co', supabaseKey || 'dummy');
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
supabaseInstance = createClient(supabaseUrl, supabaseKey);
|
| 22 |
+
return supabaseInstance;
|
| 23 |
+
}
|
tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2022",
|
| 4 |
+
"module": "NodeNext",
|
| 5 |
+
"moduleResolution": "NodeNext",
|
| 6 |
+
"declaration": true,
|
| 7 |
+
"outDir": "dist",
|
| 8 |
+
"rootDir": ".",
|
| 9 |
+
"strict": true,
|
| 10 |
+
"esModuleInterop": true,
|
| 11 |
+
"skipLibCheck": true
|
| 12 |
+
},
|
| 13 |
+
"include": ["src/**/*.ts"]
|
| 14 |
+
}
|
| 15 |
+
|