OpenCode commited on
Commit
cd455f6
·
1 Parent(s): ac6c51b

Deploy latest SIN-GitHub-Issues runtime

Browse files
.env.example ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ SIN_GITHUB_ISSUES_PUBLIC_BASE_URL=http://127.0.0.1:46031
2
+ SIN_GITHUB_APP_ROUTING_PATH=./config/github-app-routing.local.json
3
+ SIN_GITHUB_API_BASE_URL=https://api.github.com
4
+ # Optional inline override:
5
+ # SIN_GITHUB_APP_ROUTING_JSON={"mode":"central-endpoint-routing","defaultWebhookPath":"/api/webhooks/github-apps","apps":[]}
6
+
7
+ # Example secret env vars referenced by config/github-app-routing.local.json
8
+ SIN_HERMES_GITHUB_APP_ID=
9
+ SIN_HERMES_GITHUB_APP_CLIENT_ID=
10
+ SIN_HERMES_GITHUB_APP_PRIVATE_KEY_PEM=
11
+ SIN_HERMES_GITHUB_APP_WEBHOOK_SECRET=
12
+ SIN_ZEUS_GITHUB_APP_ID=
13
+ SIN_ZEUS_GITHUB_APP_CLIENT_ID=
14
+ SIN_ZEUS_GITHUB_APP_PRIVATE_KEY_PEM=
15
+ SIN_ZEUS_GITHUB_APP_WEBHOOK_SECRET=
16
+ SIN_BUGBOUNTY_GITHUB_APP_ID=
17
+ SIN_BUGBOUNTY_GITHUB_APP_CLIENT_ID=
18
+ SIN_BUGBOUNTY_GITHUB_APP_PRIVATE_KEY_PEM=
19
+ SIN_BUGBOUNTY_GITHUB_APP_WEBHOOK_SECRET=
20
+ SIN_GITHUB_ISSUES_GITHUB_APP_ID=
21
+ SIN_GITHUB_ISSUES_GITHUB_APP_CLIENT_ID=
22
+ SIN_GITHUB_ISSUES_GITHUB_APP_PRIVATE_KEY_PEM=
23
+ SIN_GITHUB_ISSUES_GITHUB_APP_WEBHOOK_SECRET=
24
+ SIN_BACKEND_GITHUB_APP_ID=
25
+ SIN_BACKEND_GITHUB_APP_CLIENT_ID=
26
+ SIN_BACKEND_GITHUB_APP_PRIVATE_KEY_PEM=
27
+ SIN_BACKEND_GITHUB_APP_WEBHOOK_SECRET=
28
+ SIN_FRONTEND_GITHUB_APP_ID=
29
+ SIN_FRONTEND_GITHUB_APP_CLIENT_ID=
30
+ SIN_FRONTEND_GITHUB_APP_PRIVATE_KEY_PEM=
31
+ SIN_FRONTEND_GITHUB_APP_WEBHOOK_SECRET=
32
+ SIN_TESTER_GITHUB_APP_ID=
33
+ SIN_TESTER_GITHUB_APP_CLIENT_ID=
34
+ SIN_TESTER_GITHUB_APP_PRIVATE_KEY_PEM=
35
+ SIN_TESTER_GITHUB_APP_WEBHOOK_SECRET=
36
+ SIN_CODE_DATABASE_GITHUB_APP_ID=
37
+ SIN_CODE_DATABASE_GITHUB_APP_CLIENT_ID=
38
+ SIN_CODE_DATABASE_GITHUB_APP_PRIVATE_KEY_PEM=
39
+ SIN_CODE_DATABASE_GITHUB_APP_WEBHOOK_SECRET=
40
+ SIN_CODE_INTEGRATION_GITHUB_APP_ID=
41
+ SIN_CODE_INTEGRATION_GITHUB_APP_CLIENT_ID=
42
+ SIN_CODE_INTEGRATION_GITHUB_APP_PRIVATE_KEY_PEM=
43
+ SIN_CODE_INTEGRATION_GITHUB_APP_WEBHOOK_SECRET=
44
+ SIN_CODE_AI_GITHUB_APP_ID=
45
+ SIN_CODE_AI_GITHUB_APP_CLIENT_ID=
46
+ SIN_CODE_AI_GITHUB_APP_PRIVATE_KEY_PEM=
47
+ SIN_CODE_AI_GITHUB_APP_WEBHOOK_SECRET=
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/workflows/deploy-huggingface-space.yml CHANGED
@@ -7,12 +7,18 @@ on:
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
 
 
7
 
8
  jobs:
9
  deploy:
 
10
  runs-on: ubuntu-latest
11
  env:
12
  HF_TOKEN: ${{ secrets.HF_TOKEN }}
13
  HF_SPACE_REPO_ID: delqhi/sin-github-issues
14
  steps:
15
+ - name: Ensure HF token is configured
16
+ run: |
17
+ if [ -z "${HF_TOKEN}" ]; then
18
+ echo "HF_TOKEN secret is missing"
19
+ exit 1
20
+ fi
21
+
22
  - name: Checkout
23
  uses: actions/checkout@v4
24
 
.gitignore CHANGED
@@ -3,5 +3,5 @@ node_modules
3
  *.log
4
  .env
5
  .env.local
 
6
  .DS_Store
7
-
 
3
  *.log
4
  .env
5
  .env.local
6
+ config/github-app-routing.local.json
7
  .DS_Store
 
.well-known/agent-card.json ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "SIN-GitHub-Issues",
3
+ "description": "CEO Elite GitHub Operations Architect — orchestrates Projects V2, Issues, Wikis, Discussions, Gists, Security audits, PR Reviews, and public Showcase Ledger logging.",
4
+ "version": "2026.03.22",
5
+ "documentationUrl": "http://127.0.0.1:46031",
6
+ "url": "http://127.0.0.1:46031/a2a/v1",
7
+ "capabilities": {
8
+ "streaming": false,
9
+ "pushNotifications": false
10
+ },
11
+ "defaultInputModes": [
12
+ "text/plain",
13
+ "application/json"
14
+ ],
15
+ "defaultOutputModes": [
16
+ "text/plain",
17
+ "application/json"
18
+ ],
19
+ "skills": [
20
+ {
21
+ "id": "sin.github.health",
22
+ "name": "Health",
23
+ "description": "Check 2026 Elite GitHub Operations Architect readiness, model, and capabilities."
24
+ },
25
+ {
26
+ "id": "sin.github.project.orchestrate",
27
+ "name": "Project Orchestrate",
28
+ "description": "Create and manage GitHub Projects V2 boards, link issues, and assign items."
29
+ },
30
+ {
31
+ "id": "sin.github.issue.manage",
32
+ "name": "Issue Manage",
33
+ "description": "Create, update, label, assign, milestone-link, or close GitHub issues and Epics."
34
+ },
35
+ {
36
+ "id": "sin.github.wiki.sync",
37
+ "name": "Wiki Sync",
38
+ "description": "Synchronize repository Wiki pages with project documentation updates."
39
+ },
40
+ {
41
+ "id": "sin.github.discussion.start",
42
+ "name": "Discussion Start",
43
+ "description": "Start or manage GitHub Discussions for architecture debates and design decisions."
44
+ },
45
+ {
46
+ "id": "sin.github.gist.publish",
47
+ "name": "Gist Publish",
48
+ "description": "Upload long logs, code snippets, or configs to GitHub Gists and return the URL."
49
+ },
50
+ {
51
+ "id": "sin.github.security.audit",
52
+ "name": "Security Audit",
53
+ "description": "Audit Dependabot alerts, configure CodeQL, and review active security advisories."
54
+ },
55
+ {
56
+ "id": "sin.github.pr.review",
57
+ "name": "PR Review",
58
+ "description": "Review Pull Requests for architectural compliance, security, performance, and test coverage. Auto-approve and merge if 100% pass."
59
+ },
60
+ {
61
+ "id": "sin.github.ledger.log",
62
+ "name": "Ledger Log",
63
+ "description": "Publish an agent activity log or achievement to the public Delqhi/OpenSIN-Ledger showcase repository."
64
+ },
65
+ {
66
+ "id": "sin.github.issue.pool.enqueue",
67
+ "name": "Issue Pool Enqueue",
68
+ "description": "Insert GitHub issue metadata into `sin_issues_pool` for Hermes/Team-Coding dispatch."
69
+ }
70
+ ],
71
+ "supportedInterfaces": [
72
+ {
73
+ "url": "http://127.0.0.1:46031/a2a/v1",
74
+ "protocolBinding": "JSONRPC",
75
+ "protocolVersion": "1.0"
76
+ }
77
+ ]
78
+ }
.well-known/oauth-client.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "client_id": "https://delqhi-sin-github-issues.hf.space/.well-known/oauth-client.json",
3
- "client_name": "AZA-SIN-Coder-•-GitHub-Issues",
4
  "redirect_uris": ["https://delqhi-sin-github-issues.hf.space/oauth/callback"],
5
  "grant_types": ["authorization_code"],
6
  "response_types": ["code"],
 
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"],
A2A-CARD.md CHANGED
@@ -1,4 +1,4 @@
1
- # AZA-SIN-Coder-•-GitHub-Issues (CEO Elite GitHub Operations)
2
 
3
  **Team:** Coding
4
  **Version:** 2026.03.22
@@ -6,8 +6,8 @@
6
 
7
  ## Identity & Mandate
8
 
9
- > **You are AZA-SIN-Coder-•-GitHub-Issues, a 2026 CEO-Level GitHub issue triage and delegation architect.**
10
- > You do NOT just create bug tickets. You triage new issues, comment on them, route them into queues, and orchestrate the **entire repository lifecycle** using the full suite of GitHub's free Enterprise features:
11
  >
12
  > 1. **Projects (V2)**: You autonomously generate Kanban boards, Epics, Sprints, and assign tasks via the GitHub CLI (`gh project create/link/item-add`).
13
  > 2. **Wikis**: You generate, maintain, and sync corporate knowledge base Markdown files to the repo's wiki (`sync-github-wiki.sh`).
@@ -23,34 +23,28 @@
23
  ## RPC Actions
24
 
25
  - `sin.github.health`: GitHub Operations Architect readiness check.
26
- - `sin.github.worker.register`: Register the GitHub-Issues runtime as a Room-13 worker and claim its initial lease.
27
- - `sin.github.worker.status`: Inspect Room-13 worker registration and claim state.
28
- - `sin.github.worker.claim_next`: Claim the next dispatchable Room-13 task.
29
- - `sin.github.worker.checkpoint`: Send checkpoint progress for the current Room-13 task.
30
- - `sin.github.worker.complete`: Mark the currently claimed Room-13 task as completed.
31
  - `sin.github.project.orchestrate`: Create and manage GitHub Projects V2 boards.
32
  - `sin.github.issue.manage`: Create, update, or close issues (Epics, Sub-tasks).
33
- - `sin.github.issue.ensure`: Create or update an issue by exact-title match.
34
- - `sin.github.issue.comment`: Add a GitHub issue comment.
35
- - `sin.github.issue.close`: Close a GitHub issue with optional comment.
36
- - `sin.github.issue.triage.enqueue`: Durably enqueue a GitHub issue triage event into the coder dispatch pool.
37
  - `sin.github.wiki.sync`: Update the repository Wiki with project documentation.
38
-
39
- ## Ingress Contract
40
-
41
- - Canonical enqueue payload example: `Docs/operations/phase22-github-issue-triage-payload.example.json`
42
- - Replay-safe fixture examples:
43
- - `Docs/operations/phase22-github-issue-opened.fixture.json`
44
- - `Docs/operations/phase22-github-issue-opened.replay.fixture.json`
45
- - Primary transport for instant triage: GitHub webhook -> n8n -> `sin.github.issue.triage.enqueue`
46
- - Idempotency key policy: `delivery_id` first, then fallback `repo#issue_number:event_action`
47
- - Queue state progression: `received -> queued -> claimed -> dispatched -> accepted -> completed|failed|dead_letter`
48
- - Runtime queue constants live in `src/queue-state.ts` and are shared between ingress and Hermes dispatch logic.
49
- - Route contract: ingress computes `assignedTeam`, `assignedAgentHint`, and `capabilityHint` from labels/title/body before Hermes dispatch.
50
- - Local proof command: `npm run smoke:triage-fixtures`
51
  - `sin.github.discussion.start`: Start an architectural debate in Discussions.
52
  - `sin.github.gist.publish`: Upload a large log or code snippet to a Gist.
53
  - `sin.github.security.audit`: Audit security alerts and configure Dependabot.
54
  - `sin.github.pr.review`: Review and approve/merge Pull Requests.
 
55
 
56
  ## Data & Transport
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A2A-SIN-GitHub-Issues (CEO Elite GitHub Operations)
2
 
3
  **Team:** Coding
4
  **Version:** 2026.03.22
 
6
 
7
  ## Identity & Mandate
8
 
9
+ > **You are A2A-SIN-GitHub-Issues, a 2026 CEO-Level GitHub Operations Architect.**
10
+ > You do NOT just create bug tickets. You orchestrate the **entire repository lifecycle** using the full suite of GitHub's free Enterprise features:
11
  >
12
  > 1. **Projects (V2)**: You autonomously generate Kanban boards, Epics, Sprints, and assign tasks via the GitHub CLI (`gh project create/link/item-add`).
13
  > 2. **Wikis**: You generate, maintain, and sync corporate knowledge base Markdown files to the repo's wiki (`sync-github-wiki.sh`).
 
23
  ## RPC Actions
24
 
25
  - `sin.github.health`: GitHub Operations Architect readiness check.
26
+ - `sin.github.app.routing.status`: Inspect configured GitHub App routes, app IDs, and secret readiness.
27
+ - `sin.github.webhook.route`: Resolve an incoming GitHub webhook to the correct SIN coder app via `installation.app_id` and optional path routing.
28
+ - `sin.github.issue.comment.as_app`: Post an issue or PR comment as the matched GitHub App identity using JWT + installation token auth.
 
 
29
  - `sin.github.project.orchestrate`: Create and manage GitHub Projects V2 boards.
30
  - `sin.github.issue.manage`: Create, update, or close issues (Epics, Sub-tasks).
 
 
 
 
31
  - `sin.github.wiki.sync`: Update the repository Wiki with project documentation.
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  - `sin.github.discussion.start`: Start an architectural debate in Discussions.
33
  - `sin.github.gist.publish`: Upload a large log or code snippet to a Gist.
34
  - `sin.github.security.audit`: Audit security alerts and configure Dependabot.
35
  - `sin.github.pr.review`: Review and approve/merge Pull Requests.
36
+ - `sin.github.issue.pool.enqueue`: Insert an issue into `sin_issues_pool` for Hermes/Team-Coding dispatch.
37
 
38
  ## Data & Transport
39
+
40
+ JSON-RPC over HTTP. Strict OpenCode execution via `openai/gpt-5.4` utilizing the `hf_pull_script.py` rotator pool.
41
+
42
+ ## GitHub App Routing
43
+
44
+ - Central webhook endpoint: `POST /github/webhook`
45
+ - Optional per-app endpoints: `POST /github/webhook/<agent-slug-or-route>`
46
+ - Routing key: `installation.app_id`
47
+ - Reply auth key: `installation.id`
48
+ - Supported config sources:
49
+ - `SIN_GITHUB_APP_ROUTING_PATH`
50
+ - `SIN_GITHUB_APP_ROUTING_JSON`
AGENTS.md CHANGED
@@ -1,10 +1,8 @@
1
- # AZA-SIN-Coder-•-GitHub-Issues
2
 
3
  - Team: Team - Coding
4
  - Team Manager: SIN-Coding-CEO
5
  - Slug: sin-github-issues
6
- - Canonical display name: AZA-SIN-Coder-•-GitHub-Issues
7
- - Machine identity policy: keep slug/package/bin/public route as `sin-github-issues` until an explicit compatibility migration says otherwise
8
  - Repo: https://github.com/Delqhi/sin-github-issues
9
  - Docs Tab: pending dedicated child-tab
10
 
@@ -12,5 +10,6 @@ Arbeitsregel:
12
  - Jede neue Action muss in `runtime.ts`, `mcp-server.ts` und `A2A-CARD.md` synchronisiert werden.
13
  - **NEUE REGEL:** Der Agent ist kein dummer Ticketersteller. Lange Logs -> Gists. Architekturdebatten -> Discussions. Planung -> Projects. Doku -> Wikis. Code -> PRs.
14
  - **PFLICHT:** `hf_pull_script.py` und `openai/gpt-5.4` MÜSSEN in der Runtime für OpenCode Commands genutzt werden. Kein Fallback auf alte lokale Auth-Tokens.
 
15
  - Jeder neue Agent muss auch `agent.json`, `AGENTS.md`, `mcp-config.json` sauber besitzen.
16
  - GitHub Actions Deploy Pipelines (`deploy-huggingface-space.yml`) pflegen.
 
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: pending dedicated child-tab
8
 
 
10
  - Jede neue Action muss in `runtime.ts`, `mcp-server.ts` und `A2A-CARD.md` synchronisiert werden.
11
  - **NEUE REGEL:** Der Agent ist kein dummer Ticketersteller. Lange Logs -> Gists. Architekturdebatten -> Discussions. Planung -> Projects. Doku -> Wikis. Code -> PRs.
12
  - **PFLICHT:** `hf_pull_script.py` und `openai/gpt-5.4` MÜSSEN in der Runtime für OpenCode Commands genutzt werden. Kein Fallback auf alte lokale Auth-Tokens.
13
+ - Multi-App GitHub routing must use one GitHub App per specialized SIN coder, route incoming webhooks via `installation.app_id`, and use `installation.id` for the responding installation-token lane.
14
  - Jeder neue Agent muss auch `agent.json`, `AGENTS.md`, `mcp-config.json` sauber besitzen.
15
  - GitHub Actions Deploy Pipelines (`deploy-huggingface-space.yml`) pflegen.
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: AZA-SIN-Coder-•-GitHub-Issues
3
  emoji: "🤖"
4
  colorFrom: blue
5
  colorTo: indigo
@@ -8,23 +8,22 @@ app_port: 7860
8
  pinned: false
9
  ---
10
 
11
- # AZA-SIN-Coder-•-GitHub-Issues
12
 
13
- `AZA-SIN-Coder-•-GitHub-Issues` is the GitHub issue triage and delegation agent package for the AZA coding workforce.
14
 
15
  ## Identity
16
 
17
- - Slug: `sin-github-issues` (canonical AZA display name: `AZA-SIN-Coder-•-GitHub-Issues`)
18
  - Team: `Team - Coding`
19
  - Team Manager: `SIN-Coding-CEO`
20
- - Purpose: GitHub issue triage, comment, queue, and execution delegation agent for confirmed bugs, repo issue lifecycle, branch-based fixes, and verification handoff. Extended with enterprise profile management capabilities.
21
- - Usage: Use for instant issue triage and coder delegation as well as confirmed bug workflows 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. For profile and achievement operations use the new profile/achievement actions.
22
- - Durable ingress payload example: `Docs/operations/phase22-github-issue-triage-payload.example.json`
23
- - Primary Model: `opencode/nemotron-3-super-free`
24
 
25
  ## Deployment
26
 
27
- - Local path: `/Users/jeremy/dev/SIN-Solver/a2a/team-coding/AZA-SIN-Coder-•-GitHub-Issues`
28
  - GitHub repo: `https://github.com/Delqhi/sin-github-issues`
29
  - Workforce index: `https://a2a.delqhi.com`
30
  - Landing page: `https://delqhi-sin-github-issues.hf.space`
@@ -44,6 +43,48 @@ pinned: false
44
  - best-effort `sin-supabase` audit persistence for workflow events
45
  - canonical env names: `SUPABASE_URL` + `SUPABASE_SERVICE_ROLE_KEY` (legacy `SIN_SUPABASE_*` still accepted)
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  ## A2A Surface
48
 
49
  - Card: `GET /.well-known/agent-card.json`
@@ -75,12 +116,8 @@ sin-github-issues run-action '{"action":"sin.github.issues.achievement.quickdraw
75
  sin-github-issues run-action '{"action":"sin.github.issues.issue.ensure","repo":"owner/name","title":"Bug title","body":"..."}'
76
  sin-github-issues run-action '{"action":"sin.github.issues.issue.evidence.policy"}'
77
  sin-github-issues run-action '{"action":"sin.github.issues.repo.branch.start","repoPath":"/abs/repo","branchName":"fix/example"}'
78
- sin-github-issues run-action '{"action":"sin.github.issues.runners.list","repo":"Delqhi/SIN-Solver"}'
79
- sin-github-issues run-action '{"action":"sin.github.issues.workflow.runs.list","repo":"Delqhi/SIN-Solver","branch":"sin-github-issues-bootstrap"}'
80
- sin-github-issues run-action '{"action":"sin.github.worker.register"}'
81
- sin-github-issues run-action '{"action":"sin.github.worker.status"}'
82
- sin-github-issues run-action '{"action":"sin.github.worker.claim_next"}'
83
- cd /Users/jeremy/dev/SIN-Solver/a2a/team-coding/A2A-SIN-GitHub-Issues && npm run smoke:triage-fixtures
84
  sin-github-issues run-action '{"action":"sin.github.issues.workflow.local_suite.run","repoPath":"/abs/repo","suite":"docs-and-architecture","confirm":true}'
85
  ```
86
 
 
1
  ---
2
+ title: SIN-GitHub-Issues
3
  emoji: "🤖"
4
  colorFrom: blue
5
  colorTo: indigo
 
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. Extended with enterprise profile management capabilities.
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. For profile and achievement operations use the new profile/achievement actions.
22
+ - Primary Model: `openai/gpt-5.4`
 
23
 
24
  ## Deployment
25
 
26
+ - Local path: `/Users/jeremy/dev/OpenSIN-backend/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`
 
43
  - best-effort `sin-supabase` audit persistence for workflow events
44
  - canonical env names: `SUPABASE_URL` + `SUPABASE_SERVICE_ROLE_KEY` (legacy `SIN_SUPABASE_*` still accepted)
45
 
46
+ ## GitHub App Fleet Routing
47
+
48
+ - Each specialized SIN coder can own its own dedicated GitHub App identity, for example `SIN-Hermes[bot]`, `SIN-Zeus[bot]`, or `SIN-BugBounty[bot]`.
49
+ - Central webhook endpoint: `POST /api/webhooks/github-apps`
50
+ - The current implementation prefers one central endpoint with `installation.app_id` routing.
51
+ - Routing key: `installation.app_id`
52
+ - Reply auth key: `installation.id`
53
+ - Routing config sources:
54
+ - repo SSOT: `config/github-app-routing.json`
55
+ - start-small example: `config/github-app-routing.local.example.json`
56
+ - GitHub-Issues local example: `config/github-app-routing.example.json`
57
+ - env matrix: `.env.example` and `.env.example` in this agent root
58
+ - Secret material is never committed. Use env-backed private key + webhook secret fields referenced from the routing config.
59
+
60
+ ### Operator Setup
61
+
62
+ 1. Generate a local routing file from the example template:
63
+
64
+ ```bash
65
+ npm run ops:github-app-routing:init
66
+ npm run ops:github-app-routing:env-template -- --agents sin-hermes,sin-zeus,sin-bugbounty
67
+ npm run ops:github-app-routing:vm-rollout -- --agents sin-hermes,sin-zeus,sin-bugbounty --base-url https://your-vm-host
68
+ ```
69
+
70
+ 2. Export `SIN_GITHUB_APP_ROUTING_PATH=./config/github-app-routing.local.json`.
71
+ 3. Fill the referenced env vars for each GitHub App in your runtime environment:
72
+ - `SIN_HERMES_GITHUB_APP_ID`, `SIN_HERMES_GITHUB_APP_CLIENT_ID`, `SIN_HERMES_GITHUB_APP_PRIVATE_KEY_PEM`, `SIN_HERMES_GITHUB_APP_WEBHOOK_SECRET`
73
+ - `SIN_ZEUS_GITHUB_APP_ID`, `SIN_ZEUS_GITHUB_APP_CLIENT_ID`, `SIN_ZEUS_GITHUB_APP_PRIVATE_KEY_PEM`, `SIN_ZEUS_GITHUB_APP_WEBHOOK_SECRET`
74
+ - `SIN_GITHUB_ISSUES_GITHUB_APP_ID`, `SIN_GITHUB_ISSUES_GITHUB_APP_CLIENT_ID`, `SIN_GITHUB_ISSUES_GITHUB_APP_PRIVATE_KEY_PEM`, `SIN_GITHUB_ISSUES_GITHUB_APP_WEBHOOK_SECRET`
75
+ 4. Point the selected GitHub Apps to the shared webhook path `POST /api/webhooks/github-apps`.
76
+ 5. Validate the webhook router before going live:
77
+
78
+ ```bash
79
+ npm run ops:github-app-routing:check -- --apps sin-hermes,sin-zeus,sin-github-issues
80
+ npm run ops:github-app-routing:smoke -- --app sin-hermes --base-url http://127.0.0.1:8000
81
+ npm run ops:github-app-routing:smoke -- --app sin-zeus --base-url http://127.0.0.1:8000
82
+ npm run ops:github-app-routing:smoke -- --app sin-github-issues --base-url http://127.0.0.1:8000
83
+ pytest tests/unit/test_room13_webhook_routes.py -q
84
+ ```
85
+
86
+ The smoke command generates a signed synthetic GitHub webhook for the selected app and expects the Room-13 endpoint to resolve the configured `app_slug`, `preferred_agent_slug`, and `installation_id`.
87
+
88
  ## A2A Surface
89
 
90
  - Card: `GET /.well-known/agent-card.json`
 
116
  sin-github-issues run-action '{"action":"sin.github.issues.issue.ensure","repo":"owner/name","title":"Bug title","body":"..."}'
117
  sin-github-issues run-action '{"action":"sin.github.issues.issue.evidence.policy"}'
118
  sin-github-issues run-action '{"action":"sin.github.issues.repo.branch.start","repoPath":"/abs/repo","branchName":"fix/example"}'
119
+ sin-github-issues run-action '{"action":"sin.github.issues.runners.list","repo":"OpenSIN-AI/OpenSIN-backend"}'
120
+ sin-github-issues run-action '{"action":"sin.github.issues.workflow.runs.list","repo":"OpenSIN-AI/OpenSIN-backend","branch":"sin-github-issues-bootstrap"}'
 
 
 
 
121
  sin-github-issues run-action '{"action":"sin.github.issues.workflow.local_suite.run","repoPath":"/abs/repo","suite":"docs-and-architecture","confirm":true}'
122
  ```
123
 
agent.json CHANGED
@@ -1,8 +1,8 @@
1
  {
2
  "id": "sin-github-issues",
3
- "displayName": "AZA-SIN-Coder-•-GitHub-Issues",
4
  "team": "team-coding",
5
- "description": "Elite 2026 GitHub issue triage and delegation architect. Manages the entire project lifecycle via GitHub Projects, Issues, comments, queueing, Discussions, Security Alerts (Dependabot/CodeQL), PR Reviews, and Gists. Fully integrated with openai/gpt-5.4 and the A2A Token Pool.",
6
  "version": "2026.03.22",
7
  "deployment": {
8
  "workforceIndex": "https://a2a.delqhi.com",
@@ -16,6 +16,7 @@
16
  "rpcPath": "/a2a/v1",
17
  "transport": "jsonrpc-over-http"
18
  },
 
19
  "mcp": {
20
  "transport": "stdio",
21
  "configPath": "./mcp-config.json"
 
1
  {
2
  "id": "sin-github-issues",
3
+ "displayName": "SIN-GitHub-Issues",
4
  "team": "team-coding",
5
+ "description": "Elite 2026 GitHub Operations Architect. Manages the entire project lifecycle via GitHub Projects, Issues, Wikis, Discussions, Security Alerts (Dependabot/CodeQL), PR Reviews, and Gists. Fully integrated with openai/gpt-5.4 and the A2A Token Pool.",
6
  "version": "2026.03.22",
7
  "deployment": {
8
  "workforceIndex": "https://a2a.delqhi.com",
 
16
  "rpcPath": "/a2a/v1",
17
  "transport": "jsonrpc-over-http"
18
  },
19
+ "primaryModel": "openai/gpt-5.4",
20
  "mcp": {
21
  "transport": "stdio",
22
  "configPath": "./mcp-config.json"
config/github-app-routing.example.json ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mode": "central-endpoint-routing",
3
+ "defaultWebhookPath": "/api/webhooks/github-apps",
4
+ "apps": [
5
+ {
6
+ "appIdEnv": "SIN_HERMES_GITHUB_APP_ID",
7
+ "appSlug": "sin-hermes",
8
+ "botName": "SIN-Hermes[bot]",
9
+ "preferredAgentSlug": "sin-hermesbote",
10
+ "webhookPath": "/api/webhooks/github-apps",
11
+ "clientIdEnv": "SIN_HERMES_GITHUB_APP_CLIENT_ID",
12
+ "privateKeyEnv": "SIN_HERMES_GITHUB_APP_PRIVATE_KEY_PEM",
13
+ "webhookSecretEnv": "SIN_HERMES_GITHUB_APP_WEBHOOK_SECRET"
14
+ },
15
+ {
16
+ "appIdEnv": "SIN_ZEUS_GITHUB_APP_ID",
17
+ "appSlug": "sin-zeus",
18
+ "botName": "SIN-Zeus[bot]",
19
+ "preferredAgentSlug": "sin-coding-ceo",
20
+ "webhookPath": "/api/webhooks/github-apps",
21
+ "clientIdEnv": "SIN_ZEUS_GITHUB_APP_CLIENT_ID",
22
+ "privateKeyEnv": "SIN_ZEUS_GITHUB_APP_PRIVATE_KEY_PEM",
23
+ "webhookSecretEnv": "SIN_ZEUS_GITHUB_APP_WEBHOOK_SECRET"
24
+ },
25
+ {
26
+ "appIdEnv": "SIN_BUGBOUNTY_GITHUB_APP_ID",
27
+ "appSlug": "sin-bugbounty",
28
+ "botName": "SIN-BugBounty[bot]",
29
+ "preferredAgentSlug": "sin-bugbounty",
30
+ "webhookPath": "/api/webhooks/github-apps",
31
+ "clientIdEnv": "SIN_BUGBOUNTY_GITHUB_APP_CLIENT_ID",
32
+ "privateKeyEnv": "SIN_BUGBOUNTY_GITHUB_APP_PRIVATE_KEY_PEM",
33
+ "webhookSecretEnv": "SIN_BUGBOUNTY_GITHUB_APP_WEBHOOK_SECRET"
34
+ },
35
+ {
36
+ "appIdEnv": "SIN_GITHUB_ISSUES_GITHUB_APP_ID",
37
+ "appSlug": "sin-github-issues",
38
+ "botName": "SIN-GitHub-Issues[bot]",
39
+ "preferredAgentSlug": "sin-github-issues",
40
+ "webhookPath": "/api/webhooks/github-apps",
41
+ "clientIdEnv": "SIN_GITHUB_ISSUES_GITHUB_APP_CLIENT_ID",
42
+ "privateKeyEnv": "SIN_GITHUB_ISSUES_GITHUB_APP_PRIVATE_KEY_PEM",
43
+ "webhookSecretEnv": "SIN_GITHUB_ISSUES_GITHUB_APP_WEBHOOK_SECRET"
44
+ },
45
+ {
46
+ "appIdEnv": "SIN_BACKEND_GITHUB_APP_ID",
47
+ "appSlug": "sin-backend",
48
+ "botName": "SIN-Backend[bot]",
49
+ "preferredAgentSlug": "sin-backend",
50
+ "webhookPath": "/api/webhooks/github-apps",
51
+ "clientIdEnv": "SIN_BACKEND_GITHUB_APP_CLIENT_ID",
52
+ "privateKeyEnv": "SIN_BACKEND_GITHUB_APP_PRIVATE_KEY_PEM",
53
+ "webhookSecretEnv": "SIN_BACKEND_GITHUB_APP_WEBHOOK_SECRET"
54
+ },
55
+ {
56
+ "appIdEnv": "SIN_FRONTEND_GITHUB_APP_ID",
57
+ "appSlug": "sin-frontend",
58
+ "botName": "SIN-Frontend[bot]",
59
+ "preferredAgentSlug": "sin-frontend",
60
+ "webhookPath": "/api/webhooks/github-apps",
61
+ "clientIdEnv": "SIN_FRONTEND_GITHUB_APP_CLIENT_ID",
62
+ "privateKeyEnv": "SIN_FRONTEND_GITHUB_APP_PRIVATE_KEY_PEM",
63
+ "webhookSecretEnv": "SIN_FRONTEND_GITHUB_APP_WEBHOOK_SECRET"
64
+ },
65
+ {
66
+ "appIdEnv": "SIN_TESTER_GITHUB_APP_ID",
67
+ "appSlug": "sin-tester",
68
+ "botName": "SIN-Tester[bot]",
69
+ "preferredAgentSlug": "sin-tester",
70
+ "webhookPath": "/api/webhooks/github-apps",
71
+ "clientIdEnv": "SIN_TESTER_GITHUB_APP_CLIENT_ID",
72
+ "privateKeyEnv": "SIN_TESTER_GITHUB_APP_PRIVATE_KEY_PEM",
73
+ "webhookSecretEnv": "SIN_TESTER_GITHUB_APP_WEBHOOK_SECRET"
74
+ },
75
+ {
76
+ "appIdEnv": "SIN_CODE_DATABASE_GITHUB_APP_ID",
77
+ "appSlug": "sin-code-database",
78
+ "botName": "SIN-Code-Database[bot]",
79
+ "preferredAgentSlug": "sin-code-database",
80
+ "webhookPath": "/api/webhooks/github-apps",
81
+ "clientIdEnv": "SIN_CODE_DATABASE_GITHUB_APP_CLIENT_ID",
82
+ "privateKeyEnv": "SIN_CODE_DATABASE_GITHUB_APP_PRIVATE_KEY_PEM",
83
+ "webhookSecretEnv": "SIN_CODE_DATABASE_GITHUB_APP_WEBHOOK_SECRET"
84
+ },
85
+ {
86
+ "appIdEnv": "SIN_CODE_INTEGRATION_GITHUB_APP_ID",
87
+ "appSlug": "sin-code-integration",
88
+ "botName": "SIN-Code-Integration[bot]",
89
+ "preferredAgentSlug": "sin-code-integration",
90
+ "webhookPath": "/api/webhooks/github-apps",
91
+ "clientIdEnv": "SIN_CODE_INTEGRATION_GITHUB_APP_CLIENT_ID",
92
+ "privateKeyEnv": "SIN_CODE_INTEGRATION_GITHUB_APP_PRIVATE_KEY_PEM",
93
+ "webhookSecretEnv": "SIN_CODE_INTEGRATION_GITHUB_APP_WEBHOOK_SECRET"
94
+ },
95
+ {
96
+ "appIdEnv": "SIN_CODE_AI_GITHUB_APP_ID",
97
+ "appSlug": "sin-code-ai",
98
+ "botName": "SIN-Code-AI[bot]",
99
+ "preferredAgentSlug": "sin-code-ai",
100
+ "webhookPath": "/api/webhooks/github-apps",
101
+ "clientIdEnv": "SIN_CODE_AI_GITHUB_APP_CLIENT_ID",
102
+ "privateKeyEnv": "SIN_CODE_AI_GITHUB_APP_PRIVATE_KEY_PEM",
103
+ "webhookSecretEnv": "SIN_CODE_AI_GITHUB_APP_WEBHOOK_SECRET"
104
+ }
105
+ ]
106
+ }
package-lock.json CHANGED
@@ -1,16 +1,17 @@
1
  {
2
- "name": "@sin-solver/sin-github-issues",
3
  "version": "0.1.0",
4
  "lockfileVersion": 3,
5
  "requires": true,
6
  "packages": {
7
  "": {
8
- "name": "@sin-solver/sin-github-issues",
9
  "version": "0.1.0",
10
  "dependencies": {
11
  "@huggingface/hub": "^0.15.1",
12
  "@modelcontextprotocol/sdk": "latest",
13
  "@supabase/supabase-js": "^2.39.7",
 
14
  "node-cron": "^3.0.3",
15
  "tar": "^6.2.0",
16
  "zod": "^3.25.76"
@@ -19,6 +20,7 @@
19
  "sin-github-issues": "dist/src/cli.js"
20
  },
21
  "devDependencies": {
 
22
  "@types/node": "^24.3.0",
23
  "@types/node-cron": "^3.0.11",
24
  "@types/tar": "^6.1.11",
@@ -181,6 +183,24 @@
181
  "node": ">=20.0.0"
182
  }
183
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  "node_modules/@types/node": {
185
  "version": "24.12.0",
186
  "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
@@ -287,6 +307,12 @@
287
  "url": "https://opencollective.com/express"
288
  }
289
  },
 
 
 
 
 
 
290
  "node_modules/bytes": {
291
  "version": "3.1.2",
292
  "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -445,6 +471,15 @@
445
  "node": ">= 0.4"
446
  }
447
  },
 
 
 
 
 
 
 
 
 
448
  "node_modules/ee-first": {
449
  "version": "1.1.1",
450
  "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -865,6 +900,91 @@
865
  "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
866
  "license": "BSD-2-Clause"
867
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
868
  "node_modules/math-intrinsics": {
869
  "version": "1.1.0",
870
  "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1150,12 +1270,44 @@
1150
  "node": ">= 18"
1151
  }
1152
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1153
  "node_modules/safer-buffer": {
1154
  "version": "2.1.2",
1155
  "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1156
  "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1157
  "license": "MIT"
1158
  },
 
 
 
 
 
 
 
 
 
 
 
 
1159
  "node_modules/send": {
1160
  "version": "1.2.1",
1161
  "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
 
1
  {
2
+ "name": "@opensin-backend/sin-github-issues",
3
  "version": "0.1.0",
4
  "lockfileVersion": 3,
5
  "requires": true,
6
  "packages": {
7
  "": {
8
+ "name": "@opensin-backend/sin-github-issues",
9
  "version": "0.1.0",
10
  "dependencies": {
11
  "@huggingface/hub": "^0.15.1",
12
  "@modelcontextprotocol/sdk": "latest",
13
  "@supabase/supabase-js": "^2.39.7",
14
+ "jsonwebtoken": "^9.0.2",
15
  "node-cron": "^3.0.3",
16
  "tar": "^6.2.0",
17
  "zod": "^3.25.76"
 
20
  "sin-github-issues": "dist/src/cli.js"
21
  },
22
  "devDependencies": {
23
+ "@types/jsonwebtoken": "^9.0.10",
24
  "@types/node": "^24.3.0",
25
  "@types/node-cron": "^3.0.11",
26
  "@types/tar": "^6.1.11",
 
183
  "node": ">=20.0.0"
184
  }
185
  },
186
+ "node_modules/@types/jsonwebtoken": {
187
+ "version": "9.0.10",
188
+ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
189
+ "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
190
+ "dev": true,
191
+ "license": "MIT",
192
+ "dependencies": {
193
+ "@types/ms": "*",
194
+ "@types/node": "*"
195
+ }
196
+ },
197
+ "node_modules/@types/ms": {
198
+ "version": "2.1.0",
199
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
200
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
201
+ "dev": true,
202
+ "license": "MIT"
203
+ },
204
  "node_modules/@types/node": {
205
  "version": "24.12.0",
206
  "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
 
307
  "url": "https://opencollective.com/express"
308
  }
309
  },
310
+ "node_modules/buffer-equal-constant-time": {
311
+ "version": "1.0.1",
312
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
313
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
314
+ "license": "BSD-3-Clause"
315
+ },
316
  "node_modules/bytes": {
317
  "version": "3.1.2",
318
  "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
 
471
  "node": ">= 0.4"
472
  }
473
  },
474
+ "node_modules/ecdsa-sig-formatter": {
475
+ "version": "1.0.11",
476
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
477
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
478
+ "license": "Apache-2.0",
479
+ "dependencies": {
480
+ "safe-buffer": "^5.0.1"
481
+ }
482
+ },
483
  "node_modules/ee-first": {
484
  "version": "1.1.1",
485
  "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
 
900
  "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
901
  "license": "BSD-2-Clause"
902
  },
903
+ "node_modules/jsonwebtoken": {
904
+ "version": "9.0.3",
905
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
906
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
907
+ "license": "MIT",
908
+ "dependencies": {
909
+ "jws": "^4.0.1",
910
+ "lodash.includes": "^4.3.0",
911
+ "lodash.isboolean": "^3.0.3",
912
+ "lodash.isinteger": "^4.0.4",
913
+ "lodash.isnumber": "^3.0.3",
914
+ "lodash.isplainobject": "^4.0.6",
915
+ "lodash.isstring": "^4.0.1",
916
+ "lodash.once": "^4.0.0",
917
+ "ms": "^2.1.1",
918
+ "semver": "^7.5.4"
919
+ },
920
+ "engines": {
921
+ "node": ">=12",
922
+ "npm": ">=6"
923
+ }
924
+ },
925
+ "node_modules/jwa": {
926
+ "version": "2.0.1",
927
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
928
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
929
+ "license": "MIT",
930
+ "dependencies": {
931
+ "buffer-equal-constant-time": "^1.0.1",
932
+ "ecdsa-sig-formatter": "1.0.11",
933
+ "safe-buffer": "^5.0.1"
934
+ }
935
+ },
936
+ "node_modules/jws": {
937
+ "version": "4.0.1",
938
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
939
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
940
+ "license": "MIT",
941
+ "dependencies": {
942
+ "jwa": "^2.0.1",
943
+ "safe-buffer": "^5.0.1"
944
+ }
945
+ },
946
+ "node_modules/lodash.includes": {
947
+ "version": "4.3.0",
948
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
949
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
950
+ "license": "MIT"
951
+ },
952
+ "node_modules/lodash.isboolean": {
953
+ "version": "3.0.3",
954
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
955
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
956
+ "license": "MIT"
957
+ },
958
+ "node_modules/lodash.isinteger": {
959
+ "version": "4.0.4",
960
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
961
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
962
+ "license": "MIT"
963
+ },
964
+ "node_modules/lodash.isnumber": {
965
+ "version": "3.0.3",
966
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
967
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
968
+ "license": "MIT"
969
+ },
970
+ "node_modules/lodash.isplainobject": {
971
+ "version": "4.0.6",
972
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
973
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
974
+ "license": "MIT"
975
+ },
976
+ "node_modules/lodash.isstring": {
977
+ "version": "4.0.1",
978
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
979
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
980
+ "license": "MIT"
981
+ },
982
+ "node_modules/lodash.once": {
983
+ "version": "4.1.1",
984
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
985
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
986
+ "license": "MIT"
987
+ },
988
  "node_modules/math-intrinsics": {
989
  "version": "1.1.0",
990
  "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
 
1270
  "node": ">= 18"
1271
  }
1272
  },
1273
+ "node_modules/safe-buffer": {
1274
+ "version": "5.2.1",
1275
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1276
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1277
+ "funding": [
1278
+ {
1279
+ "type": "github",
1280
+ "url": "https://github.com/sponsors/feross"
1281
+ },
1282
+ {
1283
+ "type": "patreon",
1284
+ "url": "https://www.patreon.com/feross"
1285
+ },
1286
+ {
1287
+ "type": "consulting",
1288
+ "url": "https://feross.org/support"
1289
+ }
1290
+ ],
1291
+ "license": "MIT"
1292
+ },
1293
  "node_modules/safer-buffer": {
1294
  "version": "2.1.2",
1295
  "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1296
  "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1297
  "license": "MIT"
1298
  },
1299
+ "node_modules/semver": {
1300
+ "version": "7.7.4",
1301
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
1302
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
1303
+ "license": "ISC",
1304
+ "bin": {
1305
+ "semver": "bin/semver.js"
1306
+ },
1307
+ "engines": {
1308
+ "node": ">=10"
1309
+ }
1310
+ },
1311
  "node_modules/send": {
1312
  "version": "1.2.1",
1313
  "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
package.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "name": "@sin-solver/sin-github-issues",
3
  "version": "0.1.0",
4
  "private": true,
5
  "type": "module",
@@ -10,8 +10,14 @@
10
  },
11
  "scripts": {
12
  "build": "rm -rf dist && tsc -p tsconfig.json && node scripts/postbuild.mjs",
13
- "smoke:triage-fixtures": "npm run build && node scripts/smoke-triage-fixtures.mjs",
14
- "smoke:room13-worker": "npm run build && node scripts/smoke-room13-worker.mjs",
 
 
 
 
 
 
15
  "typecheck": "tsc --noEmit -p tsconfig.json",
16
  "start:a2a": "node dist/src/cli.js serve-a2a",
17
  "start:mcp": "node dist/src/cli.js serve-mcp"
@@ -20,11 +26,13 @@
20
  "@modelcontextprotocol/sdk": "latest",
21
  "zod": "^3.25.76",
22
  "@huggingface/hub": "^0.15.1",
 
23
  "node-cron": "^3.0.3",
24
  "tar": "^6.2.0",
25
  "@supabase/supabase-js": "^2.39.7"
26
  },
27
  "devDependencies": {
 
28
  "@types/node": "^24.3.0",
29
  "typescript": "^5.9.3",
30
  "@types/node-cron": "^3.0.11",
 
1
  {
2
+ "name": "@opensin-backend/sin-github-issues",
3
  "version": "0.1.0",
4
  "private": true,
5
  "type": "module",
 
10
  },
11
  "scripts": {
12
  "build": "rm -rf dist && tsc -p tsconfig.json && node scripts/postbuild.mjs",
13
+ "ops:hf-deploy:check": "node scripts/check-hf-deploy-prereqs.mjs",
14
+ "ops:github-app-routing:env-template": "node scripts/render-github-app-env-template.mjs",
15
+ "ops:github-app-routing:init": "node scripts/init-github-app-routing-local.mjs",
16
+ "ops:github-app-routing:vm-rollout": "node scripts/render-github-app-vm-rollout.mjs",
17
+ "ops:github-app-comment": "node scripts/comment-as-github-app.mjs",
18
+ "ops:github-app-routing:check": "node scripts/check-github-app-routing.mjs",
19
+ "ops:github-app-routing:smoke": "node scripts/smoke-github-app-routing.mjs",
20
+ "test": "npm run build && node --test test/*.mjs",
21
  "typecheck": "tsc --noEmit -p tsconfig.json",
22
  "start:a2a": "node dist/src/cli.js serve-a2a",
23
  "start:mcp": "node dist/src/cli.js serve-mcp"
 
26
  "@modelcontextprotocol/sdk": "latest",
27
  "zod": "^3.25.76",
28
  "@huggingface/hub": "^0.15.1",
29
+ "jsonwebtoken": "^9.0.2",
30
  "node-cron": "^3.0.3",
31
  "tar": "^6.2.0",
32
  "@supabase/supabase-js": "^2.39.7"
33
  },
34
  "devDependencies": {
35
+ "@types/jsonwebtoken": "^9.0.10",
36
  "@types/node": "^24.3.0",
37
  "typescript": "^5.9.3",
38
  "@types/node-cron": "^3.0.11",
scripts/check-github-app-routing.mjs ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ const cwd = process.cwd();
7
+ const args = process.argv.slice(2);
8
+
9
+ function parseArgs(argv) {
10
+ const out = {};
11
+ for (let i = 0; i < argv.length; i += 1) {
12
+ const token = argv[i];
13
+ if (!token.startsWith('--')) continue;
14
+ const key = token.slice(2);
15
+ const next = argv[i + 1];
16
+ if (!next || next.startsWith('--')) {
17
+ out[key] = 'true';
18
+ continue;
19
+ }
20
+ out[key] = next;
21
+ i += 1;
22
+ }
23
+ return out;
24
+ }
25
+
26
+ const parsedArgs = parseArgs(args);
27
+ const configPath = path.resolve(
28
+ cwd,
29
+ parsedArgs['config'] || process.env.SIN_GITHUB_APP_ROUTING_PATH || './config/github-app-routing.local.json',
30
+ );
31
+
32
+ function firstString(...values) {
33
+ for (const value of values) {
34
+ if (typeof value === 'string' && value.trim()) return value.trim();
35
+ }
36
+ return null;
37
+ }
38
+
39
+ function readEnv(name) {
40
+ if (!name) return null;
41
+ const value = process.env[name];
42
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
43
+ }
44
+
45
+ function parseList(value) {
46
+ if (!value || typeof value !== 'string') return [];
47
+ return value
48
+ .split(',')
49
+ .map((item) => item.trim())
50
+ .filter(Boolean);
51
+ }
52
+
53
+ function toRoute(entry, defaultWebhookPath) {
54
+ const appId = firstString(String(entry.appId || '').trim(), readEnv(entry.appIdEnv));
55
+ const clientId = firstString(entry.clientId, readEnv(entry.clientIdEnv));
56
+ const routePath = firstString(entry.routePath, entry.webhookPath, defaultWebhookPath || '/github/webhook');
57
+ const agentSlug = firstString(entry.agentSlug, entry.preferredAgentSlug, entry.appSlug);
58
+ const privateKey = firstString(entry.privateKeyPem, readEnv(entry.privateKeyEnv));
59
+ const webhookSecret = firstString(entry.webhookSecret, readEnv(entry.webhookSecretEnv));
60
+ return {
61
+ agentSlug,
62
+ appSlug: firstString(entry.appSlug),
63
+ botName: firstString(entry.botName),
64
+ routePath,
65
+ appId,
66
+ clientId,
67
+ privateKeyRef: firstString(entry.privateKeyEnv, entry.privateKeyPath, entry.privateKeyPem ? 'inline' : null),
68
+ webhookSecretRef: firstString(entry.webhookSecretEnv, entry.webhookSecretPath, entry.webhookSecret ? 'inline' : null),
69
+ privateKeyResolved: Boolean(privateKey),
70
+ webhookSecretResolved: Boolean(webhookSecret),
71
+ };
72
+ }
73
+
74
+ async function main() {
75
+ const raw = await readFile(configPath, 'utf8');
76
+ const config = JSON.parse(raw);
77
+ const defaultWebhookPath = firstString(config.defaultWebhookPath, '/github/webhook');
78
+ const agentsFilter = new Set(parseList(parsedArgs.agents || parsedArgs.agent));
79
+ const configuredOnly = parsedArgs['configured-only'] === 'true';
80
+ const apps = (Array.isArray(config.apps) ? config.apps.map((entry) => toRoute(entry, defaultWebhookPath)) : []).filter((app) => {
81
+ const matchesAgent = agentsFilter.size === 0 || agentsFilter.has(app.agentSlug || '') || agentsFilter.has(app.appSlug || '');
82
+ const isConfigured = !configuredOnly || Boolean(app.appId || app.clientId || app.privateKeyResolved || app.webhookSecretResolved);
83
+ return matchesAgent && isConfigured;
84
+ });
85
+ const missing = apps.flatMap((app) => {
86
+ const failures = [];
87
+ if (!app.agentSlug) failures.push('agentSlug');
88
+ if (!app.appId) failures.push('appId');
89
+ if (!app.clientId) failures.push('clientId');
90
+ if (!app.privateKeyResolved) failures.push('privateKey');
91
+ if (!app.webhookSecretResolved) failures.push('webhookSecret');
92
+ return failures.length ? [{ agentSlug: app.agentSlug || app.appSlug || 'unknown', missing: failures }] : [];
93
+ });
94
+
95
+ console.log(JSON.stringify({
96
+ ok: missing.length === 0,
97
+ configPath,
98
+ defaultWebhookPath,
99
+ filters: {
100
+ agents: [...agentsFilter],
101
+ configuredOnly,
102
+ },
103
+ apps,
104
+ missing,
105
+ }, null, 2));
106
+
107
+ if (missing.length > 0) process.exitCode = 1;
108
+ }
109
+
110
+ main().catch((error) => {
111
+ console.error(error instanceof Error ? error.message : String(error));
112
+ process.exit(1);
113
+ });
scripts/check-hf-deploy-prereqs.mjs ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+
6
+ const execFileAsync = promisify(execFile);
7
+
8
+ function parseArgs(argv) {
9
+ const out = {};
10
+ for (let i = 0; i < argv.length; i += 1) {
11
+ const token = argv[i];
12
+ if (!token.startsWith('--')) continue;
13
+ const key = token.slice(2);
14
+ const next = argv[i + 1];
15
+ if (!next || next.startsWith('--')) {
16
+ out[key] = 'true';
17
+ continue;
18
+ }
19
+ out[key] = next;
20
+ i += 1;
21
+ }
22
+ return out;
23
+ }
24
+
25
+ async function safeGh(args) {
26
+ try {
27
+ const { stdout } = await execFileAsync('gh', args, { cwd: process.cwd() });
28
+ return { ok: true, stdout };
29
+ } catch (error) {
30
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
31
+ }
32
+ }
33
+
34
+ async function main() {
35
+ const args = parseArgs(process.argv.slice(2));
36
+ const repo = args.repo || 'Delqhi/sin-github-issues';
37
+ const workflow = args.workflow || '.github/workflows/deploy-huggingface-space.yml';
38
+
39
+ const repoMeta = await safeGh(['repo', 'view', repo, '--json', 'name,isPrivate,url,defaultBranchRef']);
40
+ const secrets = await safeGh(['secret', 'list', '-R', repo, '--json', 'name,updatedAt,visibility']);
41
+ const workflows = await safeGh(['workflow', 'list', '-R', repo, '--json', 'name,path,state,id']);
42
+ const runs = await safeGh(['run', 'list', '-R', repo, '--workflow', workflow, '--limit', '3', '--json', 'databaseId,status,conclusion,headSha,createdAt,name,workflowName,url,jobs']);
43
+
44
+ const parsedSecrets = secrets.ok ? JSON.parse(secrets.stdout || '[]') : [];
45
+ const parsedWorkflows = workflows.ok ? JSON.parse(workflows.stdout || '[]') : [];
46
+ const parsedRuns = runs.ok ? JSON.parse(runs.stdout || '[]') : [];
47
+ const hasHfTokenSecret = parsedSecrets.some((entry) => entry.name === 'HF_TOKEN');
48
+ const deployWorkflow = parsedWorkflows.find((entry) => entry.path === workflow || entry.name === workflow || entry.name === 'Deploy Hugging Face Space');
49
+ const latestRun = parsedRuns[0] || null;
50
+
51
+ const failures = [];
52
+ if (!repoMeta.ok) failures.push('repo_view_failed');
53
+ if (!secrets.ok) failures.push('secret_list_failed');
54
+ if (!workflows.ok) failures.push('workflow_list_failed');
55
+ if (!deployWorkflow) failures.push('deploy_workflow_missing');
56
+ if (!hasHfTokenSecret) failures.push('hf_token_secret_missing');
57
+ if (latestRun && latestRun.conclusion === 'failure' && Array.isArray(latestRun.jobs) && latestRun.jobs.length === 0) {
58
+ failures.push('latest_deploy_run_failed_without_jobs');
59
+ }
60
+
61
+ const output = {
62
+ ok: failures.length === 0,
63
+ repo,
64
+ workflow,
65
+ repoMeta: repoMeta.ok ? JSON.parse(repoMeta.stdout) : { error: repoMeta.error },
66
+ hasHfTokenSecret,
67
+ deployWorkflow: deployWorkflow || null,
68
+ latestRun,
69
+ failures,
70
+ };
71
+
72
+ console.log(JSON.stringify(output, null, 2));
73
+ if (failures.length > 0) process.exitCode = 1;
74
+ }
75
+
76
+ main().catch((error) => {
77
+ console.error(error instanceof Error ? error.message : String(error));
78
+ process.exit(1);
79
+ });
scripts/comment-as-github-app.mjs ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+
6
+ const execFileAsync = promisify(execFile);
7
+
8
+ function parseArgs(argv) {
9
+ const out = {};
10
+ for (let i = 0; i < argv.length; i += 1) {
11
+ const token = argv[i];
12
+ if (!token.startsWith('--')) continue;
13
+ const key = token.slice(2);
14
+ const next = argv[i + 1];
15
+ if (!next || next.startsWith('--')) {
16
+ out[key] = 'true';
17
+ continue;
18
+ }
19
+ out[key] = next;
20
+ i += 1;
21
+ }
22
+ return out;
23
+ }
24
+
25
+ function required(args, key) {
26
+ const value = args[key];
27
+ if (!value || !String(value).trim()) {
28
+ throw new Error(`missing_required_arg: --${key}`);
29
+ }
30
+ return String(value).trim();
31
+ }
32
+
33
+ async function main() {
34
+ const args = parseArgs(process.argv.slice(2));
35
+ const action = {
36
+ action: 'sin.github.issue.comment.as_app',
37
+ agentSlug: required(args, 'agent'),
38
+ repo: required(args, 'repo'),
39
+ issueNumber: Number(required(args, 'issue-number')),
40
+ installationId: Number(required(args, 'installation-id')),
41
+ body: required(args, 'body'),
42
+ };
43
+
44
+ if (!Number.isFinite(action.issueNumber) || !Number.isFinite(action.installationId)) {
45
+ throw new Error('invalid_numeric_args: --issue-number and --installation-id must be numbers');
46
+ }
47
+
48
+ const { stdout } = await execFileAsync('node', ['dist/src/cli.js', 'run-action', JSON.stringify(action)], {
49
+ cwd: process.cwd(),
50
+ });
51
+ process.stdout.write(stdout);
52
+ }
53
+
54
+ main().catch((error) => {
55
+ console.error(error instanceof Error ? error.message : String(error));
56
+ process.exit(1);
57
+ });
scripts/hf_pull_script.py CHANGED
@@ -1,5 +1,6 @@
1
  #!/usr/bin/env python3
2
  """
 
3
  Pulls a fresh OpenAI token from SIN-Supabase when a rate-limit is hit.
4
  Infinite Scaling Architecture.
5
  """
 
1
  #!/usr/bin/env python3
2
  """
3
+ A2A Consumer Rotator (HF VM Pull Script)
4
  Pulls a fresh OpenAI token from SIN-Supabase when a rate-limit is hit.
5
  Infinite Scaling Architecture.
6
  """
scripts/init-github-app-routing-local.mjs ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import { access, copyFile, mkdir } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ const cwd = process.cwd();
7
+
8
+ function parseArgs(argv) {
9
+ const out = {};
10
+ for (let i = 0; i < argv.length; i += 1) {
11
+ const token = argv[i];
12
+ if (!token.startsWith('--')) continue;
13
+ const key = token.slice(2);
14
+ const next = argv[i + 1];
15
+ if (!next || next.startsWith('--')) {
16
+ out[key] = 'true';
17
+ continue;
18
+ }
19
+ out[key] = next;
20
+ i += 1;
21
+ }
22
+ return out;
23
+ }
24
+
25
+ async function exists(filePath) {
26
+ try {
27
+ await access(filePath);
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ async function main() {
35
+ const args = parseArgs(process.argv.slice(2));
36
+ const configDir = path.resolve(cwd, 'config');
37
+ const source = path.resolve(configDir, 'github-app-routing.example.json');
38
+ const target = path.resolve(configDir, args.target || 'github-app-routing.local.json');
39
+ const force = args.force === 'true';
40
+
41
+ await mkdir(configDir, { recursive: true });
42
+ if (!force && (await exists(target))) {
43
+ console.log(JSON.stringify({ ok: true, created: false, source, target, message: 'local routing config already exists' }, null, 2));
44
+ return;
45
+ }
46
+
47
+ await copyFile(source, target);
48
+ console.log(JSON.stringify({ ok: true, created: true, source, target }, null, 2));
49
+ }
50
+
51
+ main().catch((error) => {
52
+ console.error(error instanceof Error ? error.message : String(error));
53
+ process.exit(1);
54
+ });
scripts/render-github-app-env-template.mjs ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ const cwd = process.cwd();
7
+
8
+ function parseArgs(argv) {
9
+ const out = {};
10
+ for (let i = 0; i < argv.length; i += 1) {
11
+ const token = argv[i];
12
+ if (!token.startsWith('--')) continue;
13
+ const key = token.slice(2);
14
+ const next = argv[i + 1];
15
+ if (!next || next.startsWith('--')) {
16
+ out[key] = 'true';
17
+ continue;
18
+ }
19
+ out[key] = next;
20
+ i += 1;
21
+ }
22
+ return out;
23
+ }
24
+
25
+ function firstString(...values) {
26
+ for (const value of values) {
27
+ if (typeof value === 'string' && value.trim()) return value.trim();
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function parseList(value) {
33
+ if (!value || typeof value !== 'string') return [];
34
+ return value.split(',').map((item) => item.trim()).filter(Boolean);
35
+ }
36
+
37
+ function asPlaceholder(name) {
38
+ return `<SET_${name}>`;
39
+ }
40
+
41
+ async function main() {
42
+ const args = parseArgs(process.argv.slice(2));
43
+ const format = firstString(args.format, 'dotenv');
44
+ const configPath = path.resolve(cwd, args.config || process.env.SIN_GITHUB_APP_ROUTING_PATH || './config/github-app-routing.local.json');
45
+ const selectedAgents = new Set(parseList(args.agents || args.agent));
46
+ const raw = await readFile(configPath, 'utf8');
47
+ const config = JSON.parse(raw);
48
+ const apps = Array.isArray(config.apps) ? config.apps : [];
49
+
50
+ const envEntries = new Map([
51
+ ['SIN_GITHUB_APP_ROUTING_PATH', './config/github-app-routing.local.json'],
52
+ ['SIN_GITHUB_API_BASE_URL', 'https://api.github.com'],
53
+ ]);
54
+ const appSummaries = [];
55
+
56
+ for (const app of apps) {
57
+ const agentSlug = firstString(app.agentSlug, app.preferredAgentSlug, app.appSlug);
58
+ const appSlug = firstString(app.appSlug, agentSlug);
59
+ if (selectedAgents.size > 0 && !selectedAgents.has(agentSlug || '') && !selectedAgents.has(appSlug || '')) continue;
60
+
61
+ const trackedKeys = [
62
+ firstString(app.appIdEnv),
63
+ firstString(app.clientIdEnv),
64
+ firstString(app.privateKeyEnv),
65
+ firstString(app.webhookSecretEnv),
66
+ ].filter(Boolean);
67
+
68
+ for (const key of trackedKeys) envEntries.set(key, asPlaceholder(key));
69
+
70
+ appSummaries.push({
71
+ agentSlug,
72
+ appSlug,
73
+ botName: firstString(app.botName),
74
+ routePath: firstString(app.routePath, app.webhookPath, config.defaultWebhookPath, '/github/webhook'),
75
+ });
76
+ }
77
+
78
+ if (format === 'json') {
79
+ process.stdout.write(JSON.stringify({ configPath, env: Object.fromEntries(envEntries), appSummaries }, null, 2) + '\n');
80
+ return;
81
+ }
82
+
83
+ const lines = [];
84
+ if (format === 'shell') {
85
+ for (const [key, value] of envEntries.entries()) lines.push(`export ${key}=${JSON.stringify(value)}`);
86
+ } else {
87
+ for (const [key, value] of envEntries.entries()) lines.push(`${key}=${value}`);
88
+ }
89
+ if (appSummaries.length > 0) {
90
+ lines.push('');
91
+ lines.push('# app summaries');
92
+ for (const summary of appSummaries) {
93
+ lines.push(`${summary.agentSlug} -> ${summary.botName || summary.appSlug} -> ${summary.routePath}`);
94
+ }
95
+ }
96
+ process.stdout.write(lines.join('\n') + '\n');
97
+ }
98
+
99
+ main().catch((error) => {
100
+ console.error(error instanceof Error ? error.message : String(error));
101
+ process.exit(1);
102
+ });
scripts/render-github-app-vm-rollout.mjs ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ const cwd = process.cwd();
7
+
8
+ function parseArgs(argv) {
9
+ const out = {};
10
+ for (let i = 0; i < argv.length; i += 1) {
11
+ const token = argv[i];
12
+ if (!token.startsWith('--')) continue;
13
+ const key = token.slice(2);
14
+ const next = argv[i + 1];
15
+ if (!next || next.startsWith('--')) {
16
+ out[key] = 'true';
17
+ continue;
18
+ }
19
+ out[key] = next;
20
+ i += 1;
21
+ }
22
+ return out;
23
+ }
24
+
25
+ function firstString(...values) {
26
+ for (const value of values) {
27
+ if (typeof value === 'string' && value.trim()) return value.trim();
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function parseList(value) {
33
+ if (!value || typeof value !== 'string') return [];
34
+ return value.split(',').map((item) => item.trim()).filter(Boolean);
35
+ }
36
+
37
+ async function main() {
38
+ const args = parseArgs(process.argv.slice(2));
39
+ const configPath = path.resolve(cwd, args.config || process.env.SIN_GITHUB_APP_ROUTING_PATH || './config/github-app-routing.local.json');
40
+ const baseUrl = firstString(args['base-url'], 'https://<SET_VM_BASE_URL>');
41
+ const repo = firstString(args.repo, 'OpenSIN-AI/OpenSIN-backend');
42
+ const issueNumber = firstString(args['issue-number'], '<SET_TEST_ISSUE_NUMBER>');
43
+ const installationId = firstString(args['installation-id'], '<SET_INSTALLATION_ID>');
44
+ const selected = new Set(parseList(args.agents || args.agent));
45
+
46
+ const raw = await readFile(configPath, 'utf8');
47
+ const config = JSON.parse(raw);
48
+ const apps = Array.isArray(config.apps) ? config.apps : [];
49
+ const rolloutAgents = apps
50
+ .map((entry) => ({
51
+ agentSlug: firstString(entry.agentSlug, entry.preferredAgentSlug, entry.appSlug),
52
+ appSlug: firstString(entry.appSlug, entry.agentSlug, entry.preferredAgentSlug),
53
+ botName: firstString(entry.botName),
54
+ routePath: firstString(entry.routePath, entry.webhookPath, config.defaultWebhookPath, '/github/webhook'),
55
+ }))
56
+ .filter((entry) => entry.agentSlug && entry.appSlug)
57
+ .filter((entry) => selected.size === 0 || selected.has(entry.agentSlug) || selected.has(entry.appSlug));
58
+
59
+ const agentArgs = rolloutAgents.map((entry) => entry.appSlug).join(',');
60
+ const lines = [
61
+ '# GitHub App VM Rollout Checklist',
62
+ '',
63
+ 'cd a2a/team-coding/A2A-SIN-GitHub-Issues',
64
+ 'npm run build',
65
+ 'npm run ops:github-app-routing:init',
66
+ `export SIN_GITHUB_APP_ROUTING_PATH=./config/github-app-routing.local.json`,
67
+ 'export SIN_GITHUB_API_BASE_URL=https://api.github.com',
68
+ '',
69
+ '# Fill these VM env vars',
70
+ ];
71
+
72
+ for (const entry of rolloutAgents) {
73
+ const prefix = String(entry.appSlug).toUpperCase().replace(/-/g, '_');
74
+ lines.push(`export ${prefix}_GITHUB_APP_ID=<SET_${prefix}_GITHUB_APP_ID>`);
75
+ lines.push(`export ${prefix}_GITHUB_APP_CLIENT_ID=<SET_${prefix}_GITHUB_APP_CLIENT_ID>`);
76
+ lines.push(`export ${prefix}_GITHUB_APP_PRIVATE_KEY_PEM='<SET_${prefix}_GITHUB_APP_PRIVATE_KEY_PEM>'`);
77
+ lines.push(`export ${prefix}_GITHUB_APP_WEBHOOK_SECRET=<SET_${prefix}_GITHUB_APP_WEBHOOK_SECRET>`);
78
+ lines.push('');
79
+ }
80
+
81
+ lines.push(`# Validate only the configured rollout targets`);
82
+ lines.push(`npm run ops:github-app-routing:check -- --configured-only true --agents ${agentArgs}`);
83
+ lines.push('');
84
+ lines.push('# Start the GitHub Issues A2A runtime');
85
+ lines.push('node dist/src/cli.js serve-a2a');
86
+ lines.push('');
87
+ lines.push('# In a second shell, run signed webhook smoke tests');
88
+ for (const entry of rolloutAgents) {
89
+ lines.push(`npm run ops:github-app-routing:smoke -- --agent ${entry.appSlug} --base-url ${baseUrl}`);
90
+ }
91
+ lines.push('');
92
+ lines.push('# Optional: live comment test as each bot identity');
93
+ for (const entry of rolloutAgents) {
94
+ lines.push(`npm run ops:github-app-comment -- --agent ${entry.agentSlug} --repo ${repo} --issue-number ${issueNumber} --installation-id ${installationId} --body "Hello from ${entry.botName}"`);
95
+ }
96
+
97
+ process.stdout.write(lines.join('\n') + '\n');
98
+ }
99
+
100
+ main().catch((error) => {
101
+ console.error(error instanceof Error ? error.message : String(error));
102
+ process.exit(1);
103
+ });
scripts/smoke-github-app-routing.mjs ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import { createHmac } from 'node:crypto';
4
+ import { readFile } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+
7
+ const cwd = process.cwd();
8
+
9
+ function parseArgs(argv) {
10
+ const out = {};
11
+ for (let i = 0; i < argv.length; i += 1) {
12
+ const token = argv[i];
13
+ if (!token.startsWith('--')) continue;
14
+ const key = token.slice(2);
15
+ const next = argv[i + 1];
16
+ if (!next || next.startsWith('--')) {
17
+ out[key] = 'true';
18
+ continue;
19
+ }
20
+ out[key] = next;
21
+ i += 1;
22
+ }
23
+ return out;
24
+ }
25
+
26
+ function firstString(...values) {
27
+ for (const value of values) {
28
+ if (typeof value === 'string' && value.trim()) return value.trim();
29
+ }
30
+ return null;
31
+ }
32
+
33
+ function readEnv(name) {
34
+ if (!name) return null;
35
+ const value = process.env[name];
36
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
37
+ }
38
+
39
+ function normalizeRoute(entry, defaultWebhookPath) {
40
+ return {
41
+ agentSlug: firstString(entry.agentSlug, entry.preferredAgentSlug, entry.appSlug),
42
+ appSlug: firstString(entry.appSlug),
43
+ botName: firstString(entry.botName),
44
+ appId: Number(firstString(String(entry.appId || '').trim(), readEnv(entry.appIdEnv))),
45
+ webhookPath: firstString(entry.routePath, entry.webhookPath, defaultWebhookPath || '/github/webhook'),
46
+ webhookSecret: firstString(entry.webhookSecret, readEnv(entry.webhookSecretEnv)),
47
+ };
48
+ }
49
+
50
+ async function main() {
51
+ const args = parseArgs(process.argv.slice(2));
52
+ const configPath = path.resolve(cwd, args.config || process.env.SIN_GITHUB_APP_ROUTING_PATH || './config/github-app-routing.local.json');
53
+ const baseUrl = firstString(args['base-url'], process.env.SIN_GITHUB_ISSUES_PUBLIC_BASE_URL, 'http://127.0.0.1:46031');
54
+ const agent = firstString(args.agent, args.slug, args['app-slug']);
55
+ if (!agent) throw new Error('missing_required_arg: --agent <slug-or-app-slug>');
56
+
57
+ const raw = await readFile(configPath, 'utf8');
58
+ const config = JSON.parse(raw);
59
+ const defaultWebhookPath = firstString(config.defaultWebhookPath, '/github/webhook');
60
+ const apps = Array.isArray(config.apps) ? config.apps.map((entry) => normalizeRoute(entry, defaultWebhookPath)) : [];
61
+ const target = apps.find((entry) => entry.agentSlug === agent || entry.appSlug === agent);
62
+ if (!target) throw new Error(`unknown_agent_route:${agent}`);
63
+ if (!target.webhookSecret) throw new Error(`missing_webhook_secret:${agent}`);
64
+ if (!Number.isFinite(target.appId)) throw new Error(`missing_app_id:${agent}`);
65
+
66
+ const payload = JSON.stringify({
67
+ action: args.action || 'opened',
68
+ installation: { id: Number(args['installation-id'] || 987654321), app_id: target.appId },
69
+ repository: { full_name: args.repo || 'OpenSIN-AI/OpenSIN-backend' },
70
+ });
71
+ const signature256 = `sha256=${createHmac('sha256', target.webhookSecret).update(payload).digest('hex')}`;
72
+ const response = await fetch(`${baseUrl.replace(/\/+$/, '')}${target.webhookPath}`, {
73
+ method: 'POST',
74
+ headers: {
75
+ 'content-type': 'application/json',
76
+ 'x-hub-signature-256': signature256,
77
+ 'x-github-event': args.event || 'pull_request',
78
+ 'x-github-delivery': args.delivery || `smoke-${target.appSlug || target.agentSlug}`,
79
+ },
80
+ body: payload,
81
+ });
82
+ const text = await response.text();
83
+ console.log(JSON.stringify({
84
+ ok: response.ok,
85
+ baseUrl,
86
+ routePath: target.webhookPath,
87
+ agentSlug: target.agentSlug,
88
+ appSlug: target.appSlug,
89
+ appId: target.appId,
90
+ status: response.status,
91
+ response: text ? JSON.parse(text) : null,
92
+ }, null, 2));
93
+ if (!response.ok) process.exitCode = 1;
94
+ }
95
+
96
+ main().catch((error) => {
97
+ console.error(error instanceof Error ? error.message : String(error));
98
+ process.exit(1);
99
+ });
scripts/smoke-room13-worker.mjs DELETED
@@ -1,45 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { GitHubIssuesRoom13WorkerRuntime, installGitHubIssuesWorkerRuntime } from '../dist/src/room13-worker.js';
4
-
5
- class FakeWorkerRuntime extends GitHubIssuesRoom13WorkerRuntime {
6
- constructor() {
7
- super('sin-github-issues', 'http://room13.test');
8
- }
9
-
10
- async start() {
11
- this.workerId = 'worker-gh';
12
- this.leaseEpoch = 1;
13
- this.claimedTaskId = '';
14
- this.leaseExpiresAt = '2026-03-23T22:30:00Z';
15
- return this.snapshot();
16
- }
17
-
18
- async claimNextTask() {
19
- this.claimedTaskId = 'task-gh-1';
20
- return { id: 'task-gh-1', status: 'running' };
21
- }
22
-
23
- async checkpoint(step, checkpoint = {}) {
24
- return { ok: true, step, checkpoint };
25
- }
26
-
27
- async completeTask(result = {}) {
28
- this.claimedTaskId = '';
29
- return { ok: true, result };
30
- }
31
- }
32
-
33
- const runtime = new FakeWorkerRuntime();
34
- installGitHubIssuesWorkerRuntime(runtime);
35
- const registered = await runtime.start();
36
- const claimed = await runtime.claimNextTask();
37
- const checkpoint = await runtime.checkpoint('github.issue.sync', { attempt: 1 });
38
- const completed = await runtime.completeTask({ success: true });
39
-
40
- if (registered.workerId !== 'worker-gh') throw new Error('worker register smoke failed');
41
- if (claimed.id !== 'task-gh-1') throw new Error('task claim smoke failed');
42
- if (checkpoint.step !== 'github.issue.sync') throw new Error('checkpoint smoke failed');
43
- if (!completed.ok) throw new Error('complete smoke failed');
44
-
45
- process.stdout.write(JSON.stringify({ ok: true, registered, claimed, checkpoint, completed }, null, 2) + '\n');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/smoke-triage-fixtures.mjs DELETED
@@ -1,85 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { readFile } from 'node:fs/promises';
4
- import path from 'node:path';
5
- import process from 'node:process';
6
-
7
- const packageRoot = path.resolve(process.cwd());
8
- const repoRoot = path.resolve(packageRoot, '..', '..', '..');
9
-
10
- const fixtureOnePath = path.join(repoRoot, 'Docs', 'operations', 'phase22-github-issue-opened.fixture.json');
11
- const fixtureTwoPath = path.join(repoRoot, 'Docs', 'operations', 'phase22-github-issue-opened.replay.fixture.json');
12
-
13
- const triageModule = await import(path.join(packageRoot, 'dist', 'src', 'triage.js'));
14
- const queueModule = await import(path.join(packageRoot, 'dist', 'src', 'queue-state.js'));
15
-
16
- const {
17
- TriageEnqueueSchema,
18
- buildDeliveryKey,
19
- buildIssueKey,
20
- routeIssueTriage,
21
- } = triageModule;
22
-
23
- const { ISSUE_QUEUE_STATE } = queueModule;
24
-
25
- const rowByDelivery = new Map();
26
- const rowByIssueKey = new Map();
27
-
28
- function enqueue(payload) {
29
- const input = TriageEnqueueSchema.parse(payload);
30
- const deliveryId = buildDeliveryKey(input);
31
- const issueKey = buildIssueKey(input.repo, input.issueNumber);
32
- const route = routeIssueTriage(input.labels || [], input.title, input.body || '');
33
-
34
- if (rowByDelivery.has(deliveryId)) {
35
- return {
36
- created: false,
37
- deduplicatedBy: 'delivery_id',
38
- queueState: rowByDelivery.get(deliveryId).queueState,
39
- deliveryId,
40
- issueKey,
41
- route,
42
- };
43
- }
44
-
45
- if (rowByIssueKey.has(issueKey)) {
46
- const existing = rowByIssueKey.get(issueKey);
47
- existing.deliveryIds.push(deliveryId);
48
- rowByDelivery.set(deliveryId, existing);
49
- return {
50
- created: false,
51
- deduplicatedBy: 'issue_key',
52
- queueState: existing.queueState,
53
- deliveryId,
54
- issueKey,
55
- route,
56
- };
57
- }
58
-
59
- const row = {
60
- issueKey,
61
- deliveryIds: [deliveryId],
62
- queueState: ISSUE_QUEUE_STATE.queued,
63
- assignedTeam: input.assignedTeam || route.assignedTeam,
64
- route,
65
- };
66
- rowByDelivery.set(deliveryId, row);
67
- rowByIssueKey.set(issueKey, row);
68
- return {
69
- created: true,
70
- deduplicatedBy: null,
71
- queueState: row.queueState,
72
- deliveryId,
73
- issueKey,
74
- route,
75
- };
76
- }
77
-
78
- const first = enqueue(JSON.parse(await readFile(fixtureOnePath, 'utf8')));
79
- const replay = enqueue(JSON.parse(await readFile(fixtureTwoPath, 'utf8')));
80
-
81
- if (!first.created) throw new Error('expected first fixture to create queue row');
82
- if (replay.created || replay.deduplicatedBy !== 'delivery_id') throw new Error('expected replay fixture to deduplicate by delivery_id');
83
- if (rowByIssueKey.size !== 1) throw new Error(`expected exactly one queue row, got ${rowByIssueKey.size}`);
84
-
85
- process.stdout.write(JSON.stringify({ ok: true, createdRows: rowByIssueKey.size, first, replay, finalRow: [...rowByIssueKey.values()][0] }, null, 2) + '\n');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/a2a-http.ts CHANGED
@@ -1,6 +1,7 @@
1
  import { randomUUID } from 'node:crypto';
2
  import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
3
  import type { GitHubAgentAction } from './runtime.js';
 
4
  import { buildAgentCard, resolveTemplateAgentConfig, TEMPLATE_AGENT_ID, TEMPLATE_AGENT_NAME } from './metadata.js';
5
  import { executeGitHubAgentAction } from './runtime.js';
6
 
@@ -24,18 +25,28 @@ export function createTemplateAgentHttpServer() {
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 && typeof rpc === 'object' && 'action' in rpc && typeof (rpc as { action?: unknown }).action === 'string') {
35
- const action = rpc as unknown as GitHubAgentAction;
36
- const result = await executeGitHubAgentAction(action);
37
- return sendJson(response, 200, { ok: true, agent: TEMPLATE_AGENT_ID, action: action.action, result });
38
- }
39
  if (rpc.method === 'agent/getCard') return sendJson(response, 200, { jsonrpc: '2.0', id: rpc.id ?? null, result: buildAgentCard(baseUrl) });
40
  if (rpc.method === 'message/send') {
41
  const action = parseAction((rpc.params?.message as { parts?: Array<{ text?: string }> } | undefined)?.parts?.map((part) => part.text || '').join(' ').trim() || '');
@@ -76,10 +87,23 @@ function parseAction(text: string): GitHubAgentAction {
76
  }
77
 
78
  async function readJson(request: IncomingMessage) {
 
 
 
 
 
79
  const chunks: Buffer[] = [];
80
  for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
81
- const raw = Buffer.concat(chunks).toString('utf8').trim();
82
- return raw ? JSON.parse(raw) : null;
 
 
 
 
 
 
 
 
83
  }
84
 
85
  function sendJson(response: ServerResponse, statusCode: number, payload: unknown) {
 
1
  import { randomUUID } from 'node:crypto';
2
  import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
3
  import type { GitHubAgentAction } from './runtime.js';
4
+ import { listGitHubAppWebhookPaths, routeGitHubWebhook } from './github-app-routing.js';
5
  import { buildAgentCard, resolveTemplateAgentConfig, TEMPLATE_AGENT_ID, TEMPLATE_AGENT_NAME } from './metadata.js';
6
  import { executeGitHubAgentAction } from './runtime.js';
7
 
 
25
  }
26
 
27
  async function handleRequest(request: IncomingMessage, response: ServerResponse, baseUrl: string) {
28
+ const routePath = normalizePath(request.url || '/');
29
  if (request.method === 'GET' && request.url === '/health') return sendJson(response, 200, { ok: true, agent: TEMPLATE_AGENT_ID });
30
  if (request.method === 'GET' && request.url === '/') return sendHtml(response, 200, `<html><body><h1>${TEMPLATE_AGENT_NAME}</h1></body></html>`);
31
  if (request.method === 'GET' && (request.url === '/.well-known/agent-card.json' || request.url === '/.well-known/agent.json')) {
32
  return sendJson(response, 200, buildAgentCard(baseUrl));
33
  }
34
+ const configuredWebhookPaths = request.method === 'POST' ? await listGitHubAppWebhookPaths() : [];
35
+ if (request.method === 'POST' && configuredWebhookPaths.some((configuredPath) => routePath === configuredPath || routePath.startsWith(`${configuredPath}/`))) {
36
+ const rawBody = await readRawBody(request);
37
+ const payload = rawBody.trim() ? JSON.parse(rawBody) : {};
38
+ const result = await routeGitHubWebhook({
39
+ payload,
40
+ rawBody,
41
+ routePath,
42
+ signature256: readHeader(request, 'x-hub-signature-256'),
43
+ eventName: readHeader(request, 'x-github-event'),
44
+ deliveryId: readHeader(request, 'x-github-delivery'),
45
+ });
46
+ return sendJson(response, result.ok ? 200 : 400, result);
47
+ }
48
  if (request.method === 'POST' && request.url === '/a2a/v1') {
49
  const rpc = ((await readJson(request)) || {}) as RpcRequest;
 
 
 
 
 
50
  if (rpc.method === 'agent/getCard') return sendJson(response, 200, { jsonrpc: '2.0', id: rpc.id ?? null, result: buildAgentCard(baseUrl) });
51
  if (rpc.method === 'message/send') {
52
  const action = parseAction((rpc.params?.message as { parts?: Array<{ text?: string }> } | undefined)?.parts?.map((part) => part.text || '').join(' ').trim() || '');
 
87
  }
88
 
89
  async function readJson(request: IncomingMessage) {
90
+ const raw = (await readRawBody(request)).trim();
91
+ return raw ? JSON.parse(raw) : null;
92
+ }
93
+
94
+ async function readRawBody(request: IncomingMessage) {
95
  const chunks: Buffer[] = [];
96
  for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
97
+ return Buffer.concat(chunks).toString('utf8');
98
+ }
99
+
100
+ function readHeader(request: IncomingMessage, name: string) {
101
+ const value = request.headers[name];
102
+ return typeof value === 'string' ? value : Array.isArray(value) ? value.join(',') : undefined;
103
+ }
104
+
105
+ function normalizePath(url: string) {
106
+ return url.split('?', 1)[0] || '/';
107
  }
108
 
109
  function sendJson(response: ServerResponse, statusCode: number, payload: unknown) {
src/cli.ts CHANGED
@@ -5,22 +5,11 @@ import { resolve } from 'node:path';
5
  import { createTemplateAgentHttpServer } from './a2a-http.js';
6
  import { runMcpServer } from './mcp-server.js';
7
  import { executeGitHubAgentAction } from './runtime.js';
8
- import { GitHubIssuesRoom13WorkerRuntime, installGitHubIssuesWorkerRuntime } from './room13-worker.js';
9
 
10
  const cmd = process.argv[2];
11
 
12
  if (cmd === 'serve-a2a') {
13
- createTemplateAgentHttpServer().start().then(async () => {
14
- const room13Worker = new GitHubIssuesRoom13WorkerRuntime('sin-github-issues');
15
- installGitHubIssuesWorkerRuntime(room13Worker);
16
- await room13Worker.start().catch(() => undefined);
17
- const stop = async () => {
18
- await room13Worker.stop().catch(() => undefined);
19
- process.exit(0);
20
- };
21
- process.once('SIGINT', () => void stop());
22
- process.once('SIGTERM', () => void stop());
23
- }).catch(err => {
24
  console.error(err);
25
  process.exit(1);
26
  });
 
5
  import { createTemplateAgentHttpServer } from './a2a-http.js';
6
  import { runMcpServer } from './mcp-server.js';
7
  import { executeGitHubAgentAction } from './runtime.js';
 
8
 
9
  const cmd = process.argv[2];
10
 
11
  if (cmd === 'serve-a2a') {
12
+ createTemplateAgentHttpServer().start().catch(err => {
 
 
 
 
 
 
 
 
 
 
13
  console.error(err);
14
  process.exit(1);
15
  });
src/github-app-routing.ts ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createHmac, createPrivateKey, createSign, timingSafeEqual } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+
4
+ type JsonObject = Record<string, unknown>;
5
+
6
+ export type GitHubAppRouteRecord = {
7
+ agentSlug: string;
8
+ botName?: string;
9
+ appId: number;
10
+ clientId?: string;
11
+ appSlug?: string;
12
+ routePath?: string;
13
+ privateKeyPem?: string;
14
+ privateKeyPath?: string;
15
+ privateKeyEnv?: string;
16
+ webhookSecret?: string;
17
+ webhookSecretPath?: string;
18
+ webhookSecretEnv?: string;
19
+ };
20
+
21
+ type GitHubAppRoutingConfig = {
22
+ defaultMode?: 'central' | 'path' | 'hybrid';
23
+ defaultWebhookPath?: string;
24
+ apps: GitHubAppRouteRecord[];
25
+ };
26
+
27
+ type WebhookRouteInput = {
28
+ payload?: unknown;
29
+ rawBody?: string;
30
+ routePath?: string;
31
+ signature256?: string;
32
+ eventName?: string;
33
+ deliveryId?: string;
34
+ };
35
+
36
+ type CommentAsAppInput = {
37
+ repo: string;
38
+ issueNumber: number;
39
+ body: string;
40
+ agentSlug?: string;
41
+ appId?: number;
42
+ installationId: number;
43
+ };
44
+
45
+ const GITHUB_API_BASE = String(process.env.SIN_GITHUB_API_BASE_URL || 'https://api.github.com').trim() || 'https://api.github.com';
46
+ const GITHUB_API_VERSION = '2022-11-28';
47
+
48
+ export async function getGitHubAppRoutingStatus() {
49
+ const config = await loadGitHubAppRoutingConfig();
50
+ const configuredApps = await Promise.all(
51
+ config.apps.map(async (app) => {
52
+ const privateKeyResolved = Boolean(await readPrivateKey(app));
53
+ const webhookSecretResolved = Boolean(await readWebhookSecret(app));
54
+ return {
55
+ agentSlug: app.agentSlug,
56
+ appSlug: app.appSlug || null,
57
+ botName: app.botName || `${app.agentSlug}[bot]`,
58
+ appId: app.appId,
59
+ clientId: app.clientId || null,
60
+ routePath: app.routePath || `${config.defaultWebhookPath || '/github/webhook'}/${app.agentSlug}`,
61
+ privateKeyRefConfigured: Boolean(app.privateKeyPem || app.privateKeyPath || app.privateKeyEnv),
62
+ privateKeyResolved,
63
+ webhookSecretRefConfigured: Boolean(app.webhookSecret || app.webhookSecretPath || app.webhookSecretEnv),
64
+ webhookSecretResolved,
65
+ };
66
+ }),
67
+ );
68
+ return {
69
+ ok: true,
70
+ defaultMode: config.defaultMode || 'hybrid',
71
+ defaultWebhookPath: config.defaultWebhookPath || '/github/webhook',
72
+ configuredApps,
73
+ };
74
+ }
75
+
76
+ export async function listGitHubAppWebhookPaths() {
77
+ const config = await loadGitHubAppRoutingConfig();
78
+ const paths = new Set<string>();
79
+ paths.add(config.defaultWebhookPath || '/github/webhook');
80
+ for (const app of config.apps) {
81
+ if (app.routePath) paths.add(normalizeRoutePath(app.routePath));
82
+ }
83
+ return [...paths];
84
+ }
85
+
86
+ export async function routeGitHubWebhook(input: WebhookRouteInput) {
87
+ const config = await loadGitHubAppRoutingConfig();
88
+ const payload = parsePayload(input.payload, input.rawBody);
89
+ const installation = asObject(payload.installation);
90
+ const installationId = parseInteger(installation?.id);
91
+ const appId = parseInteger(installation?.app_id);
92
+ const routePath = normalizeRoutePath(input.routePath || config.defaultWebhookPath || '/github/webhook');
93
+ const matchedApp = resolveRoute(config, { appId, routePath });
94
+
95
+ if (!matchedApp) {
96
+ return {
97
+ ok: false,
98
+ error: 'github_app_route_not_found',
99
+ routePath,
100
+ appId,
101
+ installationId,
102
+ configuredAppIds: config.apps.map((app) => app.appId),
103
+ };
104
+ }
105
+
106
+ const verification = await verifyWebhookSignature(matchedApp, input.signature256, input.rawBody || '');
107
+ if (!verification.ok) {
108
+ return {
109
+ ok: false,
110
+ error: verification.error,
111
+ routePath,
112
+ appId,
113
+ installationId,
114
+ agentSlug: matchedApp.agentSlug,
115
+ botName: matchedApp.botName || `${matchedApp.agentSlug}[bot]`,
116
+ };
117
+ }
118
+
119
+ const repository = asObject(payload.repository);
120
+ return {
121
+ ok: true,
122
+ route: {
123
+ agentSlug: matchedApp.agentSlug,
124
+ botName: matchedApp.botName || `${matchedApp.agentSlug}[bot]`,
125
+ appId: matchedApp.appId,
126
+ installationId,
127
+ routePath: matchedApp.routePath || `${config.defaultWebhookPath || '/github/webhook'}/${matchedApp.agentSlug}`,
128
+ },
129
+ event: {
130
+ name: input.eventName || null,
131
+ deliveryId: input.deliveryId || null,
132
+ action: typeof payload.action === 'string' ? payload.action : null,
133
+ repository: typeof repository?.full_name === 'string' ? repository.full_name : null,
134
+ },
135
+ verification: {
136
+ signatureChecked: Boolean(input.signature256),
137
+ signatureValid: verification.signatureValid,
138
+ },
139
+ };
140
+ }
141
+
142
+ export async function commentIssueAsGitHubApp(input: CommentAsAppInput) {
143
+ const config = await loadGitHubAppRoutingConfig();
144
+ const matchedApp = resolveCommentRoute(config, input);
145
+ if (!matchedApp) {
146
+ throw new Error(`github_app_route_not_found:${input.agentSlug || input.appId || 'unknown'}`);
147
+ }
148
+
149
+ const privateKey = await readPrivateKey(matchedApp);
150
+ if (!privateKey) {
151
+ throw new Error(`github_app_private_key_missing:${matchedApp.agentSlug}`);
152
+ }
153
+
154
+ const jwt = createGitHubAppJwt(matchedApp.appId, privateKey);
155
+ const tokenPayload = await githubApiJson<{ token?: string; expires_at?: string }>(
156
+ `${GITHUB_API_BASE}/app/installations/${input.installationId}/access_tokens`,
157
+ {
158
+ method: 'POST',
159
+ headers: {
160
+ Authorization: `Bearer ${jwt}`,
161
+ Accept: 'application/vnd.github+json',
162
+ 'X-GitHub-Api-Version': GITHUB_API_VERSION,
163
+ },
164
+ body: '{}',
165
+ },
166
+ );
167
+
168
+ if (!tokenPayload.token) {
169
+ throw new Error(`github_app_installation_token_missing:${matchedApp.agentSlug}`);
170
+ }
171
+
172
+ const commentPayload = await githubApiJson<{ html_url?: string; id?: number }>(
173
+ `${GITHUB_API_BASE}/repos/${input.repo}/issues/${input.issueNumber}/comments`,
174
+ {
175
+ method: 'POST',
176
+ headers: {
177
+ Authorization: `Bearer ${tokenPayload.token}`,
178
+ Accept: 'application/vnd.github+json',
179
+ 'X-GitHub-Api-Version': GITHUB_API_VERSION,
180
+ 'Content-Type': 'application/json',
181
+ },
182
+ body: JSON.stringify({ body: input.body }),
183
+ },
184
+ );
185
+
186
+ return {
187
+ ok: true,
188
+ repo: input.repo,
189
+ issueNumber: input.issueNumber,
190
+ appId: matchedApp.appId,
191
+ agentSlug: matchedApp.agentSlug,
192
+ botName: matchedApp.botName || `${matchedApp.agentSlug}[bot]`,
193
+ installationId: input.installationId,
194
+ commentId: commentPayload.id || null,
195
+ commentUrl: commentPayload.html_url || null,
196
+ tokenExpiresAt: tokenPayload.expires_at || null,
197
+ };
198
+ }
199
+
200
+ async function loadGitHubAppRoutingConfig(): Promise<GitHubAppRoutingConfig> {
201
+ const inlineJson = String(process.env.SIN_GITHUB_APP_ROUTING_JSON || '').trim();
202
+ const configPath = String(process.env.SIN_GITHUB_APP_ROUTING_PATH || '').trim();
203
+
204
+ let parsed: unknown = { apps: [] };
205
+ if (inlineJson) {
206
+ parsed = JSON.parse(inlineJson);
207
+ } else if (configPath) {
208
+ parsed = JSON.parse(await readFile(configPath, 'utf8'));
209
+ }
210
+
211
+ const asConfig = asObject(parsed) || { apps: [] };
212
+ const apps = Array.isArray(asConfig.apps)
213
+ ? asConfig.apps
214
+ .map((entry) => normalizeAppRecord(entry))
215
+ .filter((entry): entry is GitHubAppRouteRecord => Boolean(entry))
216
+ : [];
217
+
218
+ return {
219
+ defaultMode: parseMode(asConfig.defaultMode ?? asConfig.mode),
220
+ defaultWebhookPath: typeof asConfig.defaultWebhookPath === 'string' ? normalizeRoutePath(asConfig.defaultWebhookPath) : '/github/webhook',
221
+ apps,
222
+ };
223
+ }
224
+
225
+ function normalizeAppRecord(value: unknown): GitHubAppRouteRecord | null {
226
+ const entry = asObject(value);
227
+ if (!entry) return null;
228
+ const agentSlug = firstNonEmptyString(entry.agentSlug, entry.preferredAgentSlug, entry.appSlug);
229
+ const appId = parseInteger(entry.appId) ?? parseInteger(resolveEnvString(entry.appIdEnv));
230
+ if (!agentSlug || !appId) return null;
231
+ const routePath = firstNonEmptyString(entry.routePath, entry.webhookPath);
232
+ return {
233
+ agentSlug,
234
+ appSlug: firstNonEmptyString(entry.appSlug) || undefined,
235
+ botName: typeof entry.botName === 'string' ? entry.botName.trim() : undefined,
236
+ appId,
237
+ clientId: firstNonEmptyString(entry.clientId, resolveEnvString(entry.clientIdEnv)) || undefined,
238
+ routePath: routePath ? normalizeRoutePath(routePath) : undefined,
239
+ privateKeyPem: typeof entry.privateKeyPem === 'string' ? entry.privateKeyPem : undefined,
240
+ privateKeyPath: typeof entry.privateKeyPath === 'string' ? entry.privateKeyPath.trim() : undefined,
241
+ privateKeyEnv: firstNonEmptyString(entry.privateKeyEnv) || undefined,
242
+ webhookSecret: typeof entry.webhookSecret === 'string' ? entry.webhookSecret : undefined,
243
+ webhookSecretPath: typeof entry.webhookSecretPath === 'string' ? entry.webhookSecretPath.trim() : undefined,
244
+ webhookSecretEnv: firstNonEmptyString(entry.webhookSecretEnv) || undefined,
245
+ };
246
+ }
247
+
248
+ function parsePayload(payload: unknown, rawBody?: string) {
249
+ if (payload && typeof payload === 'object') return payload as JsonObject;
250
+ if (rawBody?.trim()) return JSON.parse(rawBody) as JsonObject;
251
+ return {} as JsonObject;
252
+ }
253
+
254
+ function resolveRoute(config: GitHubAppRoutingConfig, input: { appId: number | null; routePath: string }) {
255
+ if (input.appId) {
256
+ const byAppId = config.apps.find((app) => app.appId === input.appId);
257
+ if (byAppId) return byAppId;
258
+ }
259
+ const matchesByPath = config.apps.filter((app) => app.routePath && normalizeRoutePath(app.routePath) === input.routePath);
260
+ if (matchesByPath.length === 1) return matchesByPath[0] || null;
261
+ return null;
262
+ }
263
+
264
+ function resolveCommentRoute(config: GitHubAppRoutingConfig, input: CommentAsAppInput) {
265
+ if (input.appId) {
266
+ return config.apps.find((app) => app.appId === input.appId) || null;
267
+ }
268
+ if (input.agentSlug) {
269
+ return config.apps.find((app) => app.agentSlug === input.agentSlug) || null;
270
+ }
271
+ return null;
272
+ }
273
+
274
+ async function verifyWebhookSignature(app: GitHubAppRouteRecord, signature256: string | undefined, rawBody: string) {
275
+ if (!signature256) {
276
+ return { ok: true, signatureValid: false };
277
+ }
278
+ const secret = await readWebhookSecret(app);
279
+ if (!secret) {
280
+ return { ok: false, signatureValid: false, error: `github_app_webhook_secret_missing:${app.agentSlug}` };
281
+ }
282
+ const expected = `sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`;
283
+ const valid = safeEqual(expected, signature256.trim());
284
+ return valid
285
+ ? { ok: true, signatureValid: true }
286
+ : { ok: false, signatureValid: false, error: 'github_app_webhook_signature_invalid' };
287
+ }
288
+
289
+ async function readPrivateKey(app: GitHubAppRouteRecord) {
290
+ if (app.privateKeyPem?.trim()) return app.privateKeyPem.trim();
291
+ if (app.privateKeyEnv && process.env[app.privateKeyEnv]?.trim()) return String(process.env[app.privateKeyEnv]).trim();
292
+ if (app.privateKeyPath?.trim()) return (await readFile(app.privateKeyPath, 'utf8')).trim();
293
+ return null;
294
+ }
295
+
296
+ async function readWebhookSecret(app: GitHubAppRouteRecord) {
297
+ if (app.webhookSecret?.trim()) return app.webhookSecret.trim();
298
+ if (app.webhookSecretEnv && process.env[app.webhookSecretEnv]?.trim()) return String(process.env[app.webhookSecretEnv]).trim();
299
+ if (app.webhookSecretPath?.trim()) return (await readFile(app.webhookSecretPath, 'utf8')).trim();
300
+ return null;
301
+ }
302
+
303
+ function createGitHubAppJwt(appId: number, privateKeyPem: string) {
304
+ const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
305
+ const now = Math.floor(Date.now() / 1000);
306
+ const payload = Buffer.from(JSON.stringify({ iat: now - 60, exp: now + 540, iss: appId })).toString('base64url');
307
+ const signingInput = `${header}.${payload}`;
308
+ const signer = createSign('RSA-SHA256');
309
+ signer.update(signingInput);
310
+ signer.end();
311
+ const signature = signer.sign(createPrivateKey(privateKeyPem), 'base64url');
312
+ return `${signingInput}.${signature}`;
313
+ }
314
+
315
+ async function githubApiJson<T>(url: string, init: RequestInit): Promise<T> {
316
+ const response = await fetch(url, init);
317
+ const text = await response.text();
318
+ if (!response.ok) {
319
+ throw new Error(`github_api_request_failed:${response.status}:${text.slice(0, 500)}`);
320
+ }
321
+ return text ? (JSON.parse(text) as T) : ({} as T);
322
+ }
323
+
324
+ function parseMode(value: unknown): 'central' | 'path' | 'hybrid' | undefined {
325
+ if (value === 'central' || value === 'path' || value === 'hybrid') return value;
326
+ if (value === 'central-endpoint-routing') return 'central';
327
+ if (value === 'path-routing') return 'path';
328
+ if (value === 'hybrid-routing') return 'hybrid';
329
+ return undefined;
330
+ }
331
+
332
+ function normalizeRoutePath(value: string) {
333
+ const clean = value.split('?', 1)[0].trim();
334
+ return clean.startsWith('/') ? clean : `/${clean}`;
335
+ }
336
+
337
+ function parseInteger(value: unknown) {
338
+ const parsed = Number.parseInt(String(value ?? '').trim(), 10);
339
+ return Number.isFinite(parsed) ? parsed : null;
340
+ }
341
+
342
+ function asObject(value: unknown): JsonObject | null {
343
+ return value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : null;
344
+ }
345
+
346
+ function safeEqual(left: string, right: string) {
347
+ const leftBuffer = Buffer.from(left);
348
+ const rightBuffer = Buffer.from(right);
349
+ if (leftBuffer.length !== rightBuffer.length) return false;
350
+ return timingSafeEqual(leftBuffer, rightBuffer);
351
+ }
352
+
353
+ function firstNonEmptyString(...values: unknown[]) {
354
+ for (const value of values) {
355
+ if (typeof value === 'string' && value.trim()) return value.trim();
356
+ }
357
+ return null;
358
+ }
359
+
360
+ function resolveEnvString(envKey: unknown) {
361
+ if (typeof envKey !== 'string' || !envKey.trim()) return null;
362
+ const value = process.env[envKey.trim()];
363
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
364
+ }
src/hf_sync.ts DELETED
@@ -1,86 +0,0 @@
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/idle-monetization.ts DELETED
@@ -1,33 +0,0 @@
1
- /**
2
- * 💸 IDLE WORKFORCE & MONETIZATION DIRECTIVE (PRIORITY 0001) 🚨
3
- * NEVER IDLE MANDATE: This A2A sits NEVER simply dumb around.
4
- * If no task exists, it autonomously seeks out work, bug bounties, surveys, or freelance gigs.
5
- */
6
-
7
- export function startAutonomousIdleLoop() {
8
- console.log('💸 Autonomous Monetization & Hacking loop enabled (Mega Krass Mode).');
9
- const runtimeState = globalThis as typeof globalThis & { isAgentBusy?: boolean };
10
-
11
- // The loop checks every 5 minutes if the agent is idle.
12
- setInterval(async () => {
13
- // Skip if currently busy with a user or fleet task
14
- if (runtimeState.isAgentBusy) {
15
- return;
16
- }
17
-
18
- console.log('\n[IDLE-LOOP] Agent has no tasks. Searching for autonomous opportunities...');
19
-
20
- const tasks = [
21
- () => console.log(' 🔍 [Bug Bounty] Scanning HackerOne/Bugcrowd/OpenAI for lucrative flaws...'),
22
- () => console.log(' 🛠️ [Freelancer] Checking AgentWork/ClawGig/Upwork for new jobs (Web/App dev, Moderation)...'),
23
- () => console.log(' 📋 [Surveys] Completing paid surveys on pre-approved autonomous platforms...'),
24
- () => console.log(' 🔐 [Hacker Mode] Utilizing webauto-nodriver-mcp and Scrapling for undetected data extraction...')
25
- ];
26
-
27
- // Pick a random idle task to pretend we are doing something useful
28
- // In a real implementation, this would trigger actual A2A tasks (e.g. OpenAI completion).
29
- const randomTask = tasks[Math.floor(Math.random() * tasks.length)];
30
- randomTask();
31
-
32
- }, 5 * 60 * 1000); // Every 5 minutes
33
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/telemetry/ZeroTrustAuth.ts ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { verify } from 'jsonwebtoken';
2
+ const createTemplateLogger = (runId?: string) => ({ failed: async (s: string, e: string, err: any) => console.error(s, e, err), log: async (o: any) => console.log(o), succeeded: async (s: string, e: string, o: any) => console.log(s, e, o) });
3
+ import type { IncomingMessage } from 'node:http';
4
+
5
+ // We fetch the public key (or use a shared secret if configured) from SIN-Authenticator's OIDC jwks endpoint or env
6
+ const OIDC_PUBLIC_KEY = process.env.SIN_OIDC_PUBLIC_KEY || process.env.SIN_SHARED_SECRET || '';
7
+
8
+ export type AuthContext = {
9
+ subject: string;
10
+ roles: string[];
11
+ claims: Record<string, unknown>;
12
+ isValid: boolean;
13
+ error?: string;
14
+ };
15
+
16
+ export async function verifyZeroTrustToken(request: IncomingMessage, runId?: string): Promise<AuthContext> {
17
+ const logger = createTemplateLogger(runId);
18
+ const authHeader = request.headers.authorization;
19
+
20
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
21
+ await logger.failed('auth.zero_trust', 'auth.token.missing', new Error('Missing or malformed Authorization header'));
22
+ return { subject: 'anonymous', roles: [], claims: {}, isValid: false, error: 'missing_token' };
23
+ }
24
+
25
+ const token = authHeader.split(' ')[1];
26
+
27
+ if (!OIDC_PUBLIC_KEY) {
28
+ // If the fleet runs in a mode without strict OIDC enforcement configured yet,
29
+ // we log a warning but might fail closed depending on fleet policy.
30
+ // For Epic #195, we want to enforce zero-trust strictly when keys are present.
31
+ await logger.log({
32
+ surface: 'auth.zero_trust',
33
+ event: 'auth.keys.missing',
34
+ level: 'warn',
35
+ status: 'failed',
36
+ details: { note: 'No SIN_OIDC_PUBLIC_KEY or SIN_SHARED_SECRET configured for verification.' }
37
+ });
38
+ return { subject: 'anonymous', roles: [], claims: {}, isValid: false, error: 'missing_keys' };
39
+ }
40
+
41
+ try {
42
+ const decoded = verify(token, OIDC_PUBLIC_KEY, { algorithms: ['RS256', 'HS256'] }) as Record<string, unknown>;
43
+
44
+ await logger.succeeded('auth.zero_trust', 'auth.token.verified', { subject: decoded.sub });
45
+
46
+ return {
47
+ subject: String(decoded.sub || 'unknown'),
48
+ roles: Array.isArray(decoded.roles) ? decoded.roles.map(String) : [],
49
+ claims: decoded,
50
+ isValid: true
51
+ };
52
+ } catch (error) {
53
+ await logger.failed('auth.zero_trust', 'auth.token.rejected', error);
54
+ return {
55
+ subject: 'anonymous',
56
+ roles: [],
57
+ claims: {},
58
+ isValid: false,
59
+ error: error instanceof Error ? error.message : String(error)
60
+ };
61
+ }
62
+ }
src/mcp-server.ts CHANGED
@@ -17,41 +17,40 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
17
  inputSchema: { type: 'object', properties: {} },
18
  },
19
  {
20
- name: 'sin_github_worker_register',
21
- description: 'Register the GitHub-Issues runtime as a Room-13 worker and claim its initial lease.',
22
  inputSchema: { type: 'object', properties: {} },
23
  },
24
  {
25
- name: 'sin_github_worker_status',
26
- description: 'Inspect Room-13 worker registration and claim state for GitHub-Issues.',
27
- inputSchema: { type: 'object', properties: {} },
28
- },
29
- {
30
- name: 'sin_github_worker_claim_next',
31
- description: 'Claim the next Room-13 task for GitHub-Issues.',
32
- inputSchema: { type: 'object', properties: {} },
33
- },
34
- {
35
- name: 'sin_github_worker_checkpoint',
36
- description: 'Send a checkpoint to Room-13 for GitHub-Issues.',
37
  inputSchema: {
38
  type: 'object',
39
  properties: {
40
- step: { type: 'string' },
41
- checkpoint: { type: 'object' }
 
 
 
 
42
  },
43
- required: ['step']
44
- }
45
  },
46
  {
47
- name: 'sin_github_worker_complete',
48
- description: 'Complete the currently claimed Room-13 task for GitHub-Issues.',
49
  inputSchema: {
50
  type: 'object',
51
  properties: {
52
- result: { type: 'object' }
53
- }
54
- }
 
 
 
 
 
 
55
  },
56
  {
57
  name: 'sin_github_project_orchestrate',
@@ -77,64 +76,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
77
  required: ['prompt'],
78
  },
79
  },
80
- {
81
- name: 'sin_github_issue_ensure',
82
- description: 'Create or update an issue by exact title match.',
83
- inputSchema: {
84
- type: 'object',
85
- properties: {
86
- repo: { type: 'string' },
87
- title: { type: 'string' },
88
- body: { type: 'string' },
89
- labels: { type: 'array', items: { type: 'string' } }
90
- },
91
- required: ['repo', 'title', 'body']
92
- }
93
- },
94
- {
95
- name: 'sin_github_issue_comment',
96
- description: 'Comment on a GitHub issue.',
97
- inputSchema: {
98
- type: 'object',
99
- properties: {
100
- repo: { type: 'string' },
101
- issueNumber: { type: 'number' },
102
- body: { type: 'string' }
103
- },
104
- required: ['repo', 'issueNumber', 'body']
105
- }
106
- },
107
- {
108
- name: 'sin_github_issue_close',
109
- description: 'Close a GitHub issue with optional comment.',
110
- inputSchema: {
111
- type: 'object',
112
- properties: {
113
- repo: { type: 'string' },
114
- issueNumber: { type: 'number' },
115
- comment: { type: 'string' }
116
- },
117
- required: ['repo', 'issueNumber']
118
- }
119
- },
120
- {
121
- name: 'sin_github_issue_triage_enqueue',
122
- description: 'Durably enqueue a GitHub issue triage event into the coder dispatch pool.',
123
- inputSchema: {
124
- type: 'object',
125
- properties: {
126
- repo: { type: 'string' },
127
- issueNumber: { type: 'number' },
128
- title: { type: 'string' },
129
- body: { type: 'string' },
130
- labels: { type: 'array', items: { type: 'string' } },
131
- assignedTeam: { type: 'string' },
132
- deliveryId: { type: 'string' },
133
- eventAction: { type: 'string' }
134
- },
135
- required: ['repo', 'issueNumber', 'title']
136
- }
137
- },
138
  {
139
  name: 'sin_github_wiki_sync',
140
  description: 'Update the repository Wiki with project documentation.',
@@ -195,9 +136,31 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
195
  required: ['prNumber'],
196
  },
197
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  {
199
  name: 'sin_github_ledger_log',
200
- description: 'Publish an agent activity log or achievement to the public OpenSolver-Ledger showcase repository.',
201
  inputSchema: {
202
  type: 'object',
203
  properties: {
@@ -217,53 +180,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
217
  let action: GitHubAgentAction;
218
  const args = request.params.arguments as any;
219
 
220
- switch (request.params.name) {
221
- case 'sin_github_health':
222
- action = { action: 'sin.github.health' };
223
- break;
224
- case 'sin_github_worker_register':
225
- action = { action: 'sin.github.worker.register' };
226
- break;
227
- case 'sin_github_worker_status':
228
- action = { action: 'sin.github.worker.status' };
229
- break;
230
- case 'sin_github_worker_claim_next':
231
- action = { action: 'sin.github.worker.claim_next' };
232
- break;
233
- case 'sin_github_worker_checkpoint':
234
- action = { action: 'sin.github.worker.checkpoint', step: args.step, checkpoint: args.checkpoint };
235
- break;
236
- case 'sin_github_worker_complete':
237
- action = { action: 'sin.github.worker.complete', result: args.result };
238
- break;
239
- case 'sin_github_project_orchestrate':
240
- action = { action: 'sin.github.project.orchestrate', prompt: args.prompt, contextDir: args.contextDir };
241
- break;
242
- case 'sin_github_issue_manage':
243
- action = { action: 'sin.github.issue.manage', prompt: args.prompt, issueNumber: args.issueNumber };
244
- break;
245
- case 'sin_github_issue_ensure':
246
- action = { action: 'sin.github.issue.ensure', repo: args.repo, title: args.title, body: args.body, labels: args.labels };
247
  break;
248
- case 'sin_github_issue_comment':
249
- action = { action: 'sin.github.issue.comment', repo: args.repo, issueNumber: args.issueNumber, body: args.body };
250
  break;
251
- case 'sin_github_issue_close':
252
- action = { action: 'sin.github.issue.close', repo: args.repo, issueNumber: args.issueNumber, comment: args.comment };
 
 
 
 
 
 
 
 
253
  break;
254
- case 'sin_github_issue_triage_enqueue':
255
  action = {
256
- action: 'sin.github.issue.triage.enqueue',
257
  repo: args.repo,
258
  issueNumber: args.issueNumber,
259
- title: args.title,
260
  body: args.body,
261
- labels: args.labels,
262
- assignedTeam: args.assignedTeam,
263
- deliveryId: args.deliveryId,
264
- eventAction: args.eventAction
265
  };
266
  break;
 
 
 
 
 
 
267
  case 'sin_github_wiki_sync':
268
  action = { action: 'sin.github.wiki.sync', prompt: args.prompt, contextDir: args.contextDir };
269
  break;
@@ -279,6 +230,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
279
  case 'sin_github_pr_review':
280
  action = { action: 'sin.github.pr.review', prNumber: args.prNumber, contextDir: args.contextDir };
281
  break;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  case 'sin_github_ledger_log':
283
  action = { action: 'sin.github.ledger.log', agentName: args.agentName, activityTitle: args.activityTitle, details: args.details };
284
  break;
@@ -301,5 +269,5 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
301
  export async function runMcpServer() {
302
  const transport = new StdioServerTransport();
303
  await server.connect(transport);
304
- console.error('AZA-SIN-Coder-•-GitHub-Issues MCP Server running on stdio');
305
  }
 
17
  inputSchema: { type: 'object', properties: {} },
18
  },
19
  {
20
+ name: 'sin_github_app_routing_status',
21
+ description: 'Inspect per-agent GitHub App routing config without exposing secrets.',
22
  inputSchema: { type: 'object', properties: {} },
23
  },
24
  {
25
+ name: 'sin_github_webhook_route',
26
+ description: 'Resolve a GitHub webhook payload to the correct specialized SIN coder app using installation.app_id and routing config.',
 
 
 
 
 
 
 
 
 
 
27
  inputSchema: {
28
  type: 'object',
29
  properties: {
30
+ payload: {},
31
+ rawBody: { type: 'string' },
32
+ routePath: { type: 'string' },
33
+ signature256: { type: 'string' },
34
+ eventName: { type: 'string' },
35
+ deliveryId: { type: 'string' },
36
  },
37
+ },
 
38
  },
39
  {
40
+ name: 'sin_github_issue_comment_as_app',
41
+ description: 'Post an issue or PR comment as the correct GitHub App identity using a JWT + installation token flow.',
42
  inputSchema: {
43
  type: 'object',
44
  properties: {
45
+ repo: { type: 'string' },
46
+ issueNumber: { type: 'number' },
47
+ body: { type: 'string' },
48
+ agentSlug: { type: 'string' },
49
+ appId: { type: 'number' },
50
+ installationId: { type: 'number' },
51
+ },
52
+ required: ['repo', 'issueNumber', 'body', 'installationId'],
53
+ },
54
  },
55
  {
56
  name: 'sin_github_project_orchestrate',
 
76
  required: ['prompt'],
77
  },
78
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  {
80
  name: 'sin_github_wiki_sync',
81
  description: 'Update the repository Wiki with project documentation.',
 
136
  required: ['prNumber'],
137
  },
138
  },
139
+ {
140
+ name: 'sin_github_issue_pool_enqueue',
141
+ description: 'Insert an issue into the Hermes issue pool for Team-Coding dispatch.',
142
+ inputSchema: {
143
+ type: 'object',
144
+ properties: {
145
+ repoName: { type: 'string' },
146
+ issueNumber: { type: 'number' },
147
+ title: { type: 'string' },
148
+ body: { type: 'string' },
149
+ labels: { type: 'array', items: { type: 'string' } },
150
+ issueUrl: { type: 'string' },
151
+ assignedTeam: { type: 'string' },
152
+ assignedAgent: { type: 'string' },
153
+ status: { type: 'string' },
154
+ state: { type: 'string' },
155
+ routingStrategy: { type: 'string' },
156
+ fanoutPlan: { type: 'array', items: {} },
157
+ },
158
+ required: ['repoName', 'issueNumber', 'title'],
159
+ },
160
+ },
161
  {
162
  name: 'sin_github_ledger_log',
163
+ description: 'Publish an agent activity log or achievement to the public OpenSIN-Ledger showcase repository.',
164
  inputSchema: {
165
  type: 'object',
166
  properties: {
 
180
  let action: GitHubAgentAction;
181
  const args = request.params.arguments as any;
182
 
183
+ switch (request.params.name) {
184
+ case 'sin_github_health':
185
+ action = { action: 'sin.github.health' };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  break;
187
+ case 'sin_github_app_routing_status':
188
+ action = { action: 'sin.github.app.routing.status' };
189
  break;
190
+ case 'sin_github_webhook_route':
191
+ action = {
192
+ action: 'sin.github.webhook.route',
193
+ payload: args.payload,
194
+ rawBody: args.rawBody,
195
+ routePath: args.routePath,
196
+ signature256: args.signature256,
197
+ eventName: args.eventName,
198
+ deliveryId: args.deliveryId,
199
+ };
200
  break;
201
+ case 'sin_github_issue_comment_as_app':
202
  action = {
203
+ action: 'sin.github.issue.comment.as_app',
204
  repo: args.repo,
205
  issueNumber: args.issueNumber,
 
206
  body: args.body,
207
+ agentSlug: args.agentSlug,
208
+ appId: args.appId,
209
+ installationId: args.installationId,
 
210
  };
211
  break;
212
+ case 'sin_github_project_orchestrate':
213
+ action = { action: 'sin.github.project.orchestrate', prompt: args.prompt, contextDir: args.contextDir };
214
+ break;
215
+ case 'sin_github_issue_manage':
216
+ action = { action: 'sin.github.issue.manage', prompt: args.prompt, issueNumber: args.issueNumber };
217
+ break;
218
  case 'sin_github_wiki_sync':
219
  action = { action: 'sin.github.wiki.sync', prompt: args.prompt, contextDir: args.contextDir };
220
  break;
 
230
  case 'sin_github_pr_review':
231
  action = { action: 'sin.github.pr.review', prNumber: args.prNumber, contextDir: args.contextDir };
232
  break;
233
+ case 'sin_github_issue_pool_enqueue':
234
+ action = {
235
+ action: 'sin.github.issue.pool.enqueue',
236
+ repoName: args.repoName,
237
+ issueNumber: args.issueNumber,
238
+ title: args.title,
239
+ body: args.body,
240
+ labels: args.labels,
241
+ issueUrl: args.issueUrl,
242
+ assignedTeam: args.assignedTeam,
243
+ assignedAgent: args.assignedAgent,
244
+ status: args.status,
245
+ state: args.state,
246
+ routingStrategy: args.routingStrategy,
247
+ fanoutPlan: args.fanoutPlan,
248
+ };
249
+ break;
250
  case 'sin_github_ledger_log':
251
  action = { action: 'sin.github.ledger.log', agentName: args.agentName, activityTitle: args.activityTitle, details: args.details };
252
  break;
 
269
  export async function runMcpServer() {
270
  const transport = new StdioServerTransport();
271
  await server.connect(transport);
272
+ console.error('A2A-SIN-GitHub-Issues MCP Server running on stdio');
273
  }
src/metadata.ts CHANGED
@@ -1,8 +1,8 @@
1
  import process from 'node:process';
2
 
3
  export const TEMPLATE_AGENT_ID = 'sin-github-issues';
4
- export const TEMPLATE_AGENT_NAME = 'AZA-SIN-Coder-•-GitHub-Issues';
5
- export const TEMPLATE_AGENT_DESCRIPTION = 'CEO Elite GitHub issue triage and delegation architect for the AZA coding fleet — orchestrates issue lifecycle, comments, queueing, Projects V2, Discussions, Gists, Security audits, PR reviews, and public Showcase Ledger logging.';
6
  export const TEMPLATE_AGENT_VERSION = '2026.03.22';
7
  export const TEMPLATE_AGENT_DEFAULT_HOST = '127.0.0.1';
8
  export const TEMPLATE_AGENT_DEFAULT_PORT = 46031;
@@ -14,29 +14,19 @@ export const TEMPLATE_AGENT_SKILLS = [
14
  description: 'Check 2026 Elite GitHub Operations Architect readiness, model, and capabilities.',
15
  },
16
  {
17
- id: 'sin.github.worker.register',
18
- name: 'Worker Register',
19
- description: 'Register the GitHub-Issues runtime as a Room-13 worker and claim its initial lease.',
20
  },
21
  {
22
- id: 'sin.github.worker.status',
23
- name: 'Worker Status',
24
- description: 'Inspect GitHub-Issues worker registration and claim state against Room-13.',
25
  },
26
  {
27
- id: 'sin.github.worker.claim_next',
28
- name: 'Worker Claim Next',
29
- description: 'Claim the next dispatchable Room-13 task for GitHub-Issues.',
30
- },
31
- {
32
- id: 'sin.github.worker.checkpoint',
33
- name: 'Worker Checkpoint',
34
- description: 'Send checkpoint progress for the current Room-13 task.',
35
- },
36
- {
37
- id: 'sin.github.worker.complete',
38
- name: 'Worker Complete',
39
- description: 'Mark the currently claimed Room-13 task as completed.',
40
  },
41
  {
42
  id: 'sin.github.project.orchestrate',
@@ -73,10 +63,15 @@ export const TEMPLATE_AGENT_SKILLS = [
73
  name: 'PR Review',
74
  description: 'Review Pull Requests for architectural compliance, security, performance, and test coverage. Auto-approve and merge if 100% pass.',
75
  },
 
 
 
 
 
76
  {
77
  id: 'sin.github.ledger.log',
78
  name: 'Ledger Log',
79
- description: 'Publish an agent activity log or achievement to the public Delqhi/OpenSolver-Ledger showcase repository.',
80
  },
81
  ] as const;
82
 
 
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 = 'CEO Elite GitHub Operations Architect — orchestrates Projects V2, Issues, Wikis, Discussions, Gists, Security audits, PR Reviews, and public Showcase Ledger logging.';
6
  export const TEMPLATE_AGENT_VERSION = '2026.03.22';
7
  export const TEMPLATE_AGENT_DEFAULT_HOST = '127.0.0.1';
8
  export const TEMPLATE_AGENT_DEFAULT_PORT = 46031;
 
14
  description: 'Check 2026 Elite GitHub Operations Architect readiness, model, and capabilities.',
15
  },
16
  {
17
+ id: 'sin.github.app.routing.status',
18
+ name: 'GitHub App Routing Status',
19
+ description: 'Inspect the configured multi-app GitHub routing table, route paths, and secret readiness without exposing credentials.',
20
  },
21
  {
22
+ id: 'sin.github.webhook.route',
23
+ name: 'GitHub Webhook Route',
24
+ description: 'Route GitHub webhook payloads to the correct SIN coder agent using installation.app_id, route path, and signature verification.',
25
  },
26
  {
27
+ id: 'sin.github.issue.comment.as_app',
28
+ name: 'Issue Comment As App',
29
+ description: 'Generate the correct GitHub App JWT and installation token, then post an issue or PR comment as the matched bot identity.',
 
 
 
 
 
 
 
 
 
 
30
  },
31
  {
32
  id: 'sin.github.project.orchestrate',
 
63
  name: 'PR Review',
64
  description: 'Review Pull Requests for architectural compliance, security, performance, and test coverage. Auto-approve and merge if 100% pass.',
65
  },
66
+ {
67
+ id: 'sin.github.issue.pool.enqueue',
68
+ name: 'Issue Pool Enqueue',
69
+ description: 'Insert GitHub issue metadata into `sin_issues_pool` for Hermes/Team-Coding dispatch.',
70
+ },
71
  {
72
  id: 'sin.github.ledger.log',
73
  name: 'Ledger Log',
74
+ description: 'Publish an agent activity log or achievement to the public Delqhi/OpenSIN-Ledger showcase repository.',
75
  },
76
  ] as const;
77
 
src/queue-state.js DELETED
@@ -1,29 +0,0 @@
1
- export const ISSUE_QUEUE_STATE = {
2
- received: 'received',
3
- queued: 'queued',
4
- claimed: 'claimed',
5
- dispatched: 'dispatched',
6
- accepted: 'accepted',
7
- completed: 'completed',
8
- failed: 'failed',
9
- dead_letter: 'dead_letter',
10
- };
11
-
12
- export function isActiveQueueState(value) {
13
- return ['received', 'queued', 'claimed', 'dispatched', 'accepted'].includes(String(value || ''));
14
- }
15
-
16
- export function queueStateForStatus(value) {
17
- switch (String(value || '').toLowerCase()) {
18
- case 'pending':
19
- return ISSUE_QUEUE_STATE.queued;
20
- case 'in_progress':
21
- return ISSUE_QUEUE_STATE.claimed;
22
- case 'completed':
23
- return ISSUE_QUEUE_STATE.completed;
24
- case 'failed':
25
- return ISSUE_QUEUE_STATE.failed;
26
- default:
27
- return ISSUE_QUEUE_STATE.received;
28
- }
29
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/queue-state.ts DELETED
@@ -1,31 +0,0 @@
1
- export const ISSUE_QUEUE_STATE = {
2
- received: 'received',
3
- queued: 'queued',
4
- claimed: 'claimed',
5
- dispatched: 'dispatched',
6
- accepted: 'accepted',
7
- completed: 'completed',
8
- failed: 'failed',
9
- dead_letter: 'dead_letter',
10
- } as const;
11
-
12
- export type IssueQueueState = keyof typeof ISSUE_QUEUE_STATE;
13
-
14
- export function isActiveQueueState(value: string | null | undefined): boolean {
15
- return ['received', 'queued', 'claimed', 'dispatched', 'accepted'].includes(String(value || ''));
16
- }
17
-
18
- export function queueStateForStatus(status: string | null | undefined): IssueQueueState {
19
- switch (String(status || '').toLowerCase()) {
20
- case 'pending':
21
- return 'queued';
22
- case 'in_progress':
23
- return 'claimed';
24
- case 'completed':
25
- return 'completed';
26
- case 'failed':
27
- return 'failed';
28
- default:
29
- return 'received';
30
- }
31
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/room13-worker.ts DELETED
@@ -1,191 +0,0 @@
1
- import os from 'node:os';
2
- import process from 'node:process';
3
-
4
- type Room13WorkerRegisterResponse = {
5
- id?: string;
6
- lease_epoch?: number;
7
- };
8
-
9
- type Room13WorkerClaimResponse = {
10
- claimed_task_id?: string | null;
11
- lease_epoch?: number;
12
- lease_expires_at?: string | null;
13
- run_id?: string;
14
- };
15
-
16
- type Room13Task = {
17
- id: string;
18
- status: string;
19
- assigned_worker?: string | null;
20
- run_id?: string | null;
21
- };
22
-
23
- export type GitHubIssuesWorkerSnapshot = {
24
- enabled: boolean;
25
- workerName: string;
26
- workerId: string;
27
- runId: string;
28
- claimedTaskId: string;
29
- leaseEpoch: number;
30
- leaseExpiresAt?: string;
31
- baseUrl: string;
32
- };
33
-
34
- let currentRuntime: GitHubIssuesRoom13WorkerRuntime | null = null;
35
-
36
- export function getCurrentGitHubIssuesWorkerRuntime() {
37
- return currentRuntime;
38
- }
39
-
40
- export class GitHubIssuesRoom13WorkerRuntime {
41
- protected workerId = '';
42
- protected leaseEpoch = 0;
43
- protected claimedTaskId = '';
44
- protected leaseExpiresAt?: string;
45
- protected readonly workerName: string;
46
- protected readonly baseUrl: string;
47
- protected readonly bearerToken: string;
48
- protected readonly runId: string;
49
- protected heartbeatTimer?: NodeJS.Timeout;
50
-
51
- constructor(workerName = 'sin-github-issues', baseUrl = process.env.ROOM13_COORDINATOR_URL || '') {
52
- this.workerName = workerName;
53
- this.baseUrl = baseUrl.replace(/\/$/, '');
54
- this.bearerToken = String(process.env.ROOM13_BEARER_TOKEN || '').trim();
55
- this.runId = `${workerName}:${process.pid}:${Date.now()}`;
56
- }
57
-
58
- get enabled() {
59
- return Boolean(this.baseUrl);
60
- }
61
-
62
- snapshot(): GitHubIssuesWorkerSnapshot {
63
- return {
64
- enabled: this.enabled,
65
- workerName: this.workerName,
66
- workerId: this.workerId,
67
- runId: this.runId,
68
- claimedTaskId: this.claimedTaskId,
69
- leaseEpoch: this.leaseEpoch,
70
- leaseExpiresAt: this.leaseExpiresAt,
71
- baseUrl: this.baseUrl,
72
- };
73
- }
74
-
75
- async start() {
76
- if (!this.enabled) return this.snapshot();
77
- const registered = await this.request<Room13WorkerRegisterResponse>('POST', '/api/workers', {
78
- name: `${this.workerName}:${process.env.SPACE_HOST || 'local'}`,
79
- type: 'general',
80
- capabilities: ['platform:github', 'worker_runtime', 'lease_heartbeat', 'issue_lifecycle'],
81
- metadata: { runtime: 'node', hostname: os.hostname() },
82
- worker_identity: this.workerName,
83
- persona_id: this.workerName,
84
- platform: 'github-issues',
85
- current_run_id: this.runId,
86
- lease_ttl_sec: 120,
87
- approval_state: 'idle',
88
- });
89
- this.workerId = String(registered?.id || '').trim();
90
- this.leaseEpoch = Number(registered?.lease_epoch || 0);
91
- if (this.workerId) {
92
- const claim = await this.request<Room13WorkerClaimResponse>('POST', `/api/workers/${this.workerId}/claim`, {
93
- run_id: this.runId,
94
- ttl_sec: 120,
95
- });
96
- this.claimedTaskId = String(claim?.claimed_task_id || '').trim();
97
- this.leaseEpoch = Number(claim?.lease_epoch || this.leaseEpoch || 0);
98
- this.leaseExpiresAt = claim?.lease_expires_at || undefined;
99
- this.startHeartbeatLoop();
100
- }
101
- return this.snapshot();
102
- }
103
-
104
- async stop() {
105
- if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
106
- if (!this.enabled || !this.workerId) return;
107
- await this.request('POST', `/api/workers/${this.workerId}/release`, {
108
- lease_epoch: this.leaseEpoch,
109
- clear_run: true,
110
- final_status: 'online',
111
- status_reason: 'github_issues_runtime_stop',
112
- approval_state: 'idle',
113
- }).catch(() => undefined);
114
- }
115
-
116
- async heartbeat() {
117
- if (!this.enabled || !this.workerId) return this.snapshot();
118
- const result = await this.request<Room13WorkerClaimResponse>('POST', `/api/workers/${this.workerId}/heartbeat`, {
119
- lease_epoch: this.leaseEpoch,
120
- ttl_sec: 120,
121
- metadata: {
122
- run_id: this.runId,
123
- claimed_task_id: this.claimedTaskId,
124
- last_heartbeat_source: 'sin-github-issues',
125
- },
126
- });
127
- this.leaseEpoch = Number(result?.lease_epoch || this.leaseEpoch || 0);
128
- this.leaseExpiresAt = result?.lease_expires_at || this.leaseExpiresAt;
129
- return this.snapshot();
130
- }
131
-
132
- async claimNextTask() {
133
- if (!this.enabled || !this.workerId) throw new Error('room13_worker_not_registered');
134
- const task = await this.request<Room13Task>('POST', '/api/tasks/claim-next', {
135
- worker_id: this.workerId,
136
- worker_identity: this.workerName,
137
- accepted_types: ['github_issue_dispatch'],
138
- min_priority: 'normal',
139
- run_id: this.runId,
140
- lease_epoch: this.leaseEpoch,
141
- });
142
- this.claimedTaskId = String(task?.id || '').trim();
143
- return task;
144
- }
145
-
146
- async checkpoint(step: string, checkpoint: Record<string, unknown> = {}) {
147
- if (!this.enabled || !this.workerId) throw new Error('room13_worker_not_registered');
148
- return await this.request('POST', `/api/workers/${this.workerId}/checkpoint`, {
149
- lease_epoch: this.leaseEpoch,
150
- run_id: this.runId,
151
- step,
152
- checkpoint,
153
- });
154
- }
155
-
156
- async completeTask(result: Record<string, unknown> = {}) {
157
- if (!this.enabled || !this.claimedTaskId) throw new Error('room13_task_not_claimed');
158
- const response = await this.request('PUT', `/api/tasks/${this.claimedTaskId}/complete`, result);
159
- this.claimedTaskId = '';
160
- return response;
161
- }
162
-
163
- private startHeartbeatLoop() {
164
- if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
165
- this.heartbeatTimer = setInterval(() => {
166
- void this.heartbeat().catch(() => undefined);
167
- }, 30000);
168
- this.heartbeatTimer.unref();
169
- }
170
-
171
- private async request<T = unknown>(method: string, path: string, body?: unknown) {
172
- const headers: Record<string, string> = {
173
- 'content-type': 'application/json',
174
- 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
175
- };
176
- if (this.bearerToken) headers.Authorization = `Bearer ${this.bearerToken}`;
177
- const response = await fetch(`${this.baseUrl}${path}`, {
178
- method,
179
- headers,
180
- body: body ? JSON.stringify(body) : undefined,
181
- });
182
- const text = await response.text();
183
- const parsed = text ? JSON.parse(text) : {};
184
- if (!response.ok) throw new Error(`room13_http_${response.status}:${JSON.stringify(parsed).slice(0, 1000)}`);
185
- return parsed as T;
186
- }
187
- }
188
-
189
- export function installGitHubIssuesWorkerRuntime(runtime: GitHubIssuesRoom13WorkerRuntime | null) {
190
- currentRuntime = runtime;
191
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/runtime.ts CHANGED
@@ -1,42 +1,20 @@
1
- import { exec } from 'node:child_process';
 
 
 
2
  import { promisify } from 'node:util';
3
- import { commentIssue, closeIssue, ensureIssue } from './github.js';
4
  import { createClient } from '@supabase/supabase-js';
5
- import { getCurrentGitHubIssuesWorkerRuntime } from './room13-worker.js';
6
- import {
7
- buildDeliveryKey,
8
- buildIssueKey,
9
- routeIssueTriage,
10
- routeLabelsToTeam,
11
- TriageEnqueueSchema,
12
- type TriageEnqueueInput,
13
- } from './triage.js';
14
- import { ISSUE_QUEUE_STATE } from './queue-state.js';
15
 
16
  const execAsync = promisify(exec);
 
17
 
18
  export type GitHubAgentAction =
19
  | { action: 'agent.help' }
20
  | { action: 'sin.github.health' }
21
- | { action: 'sin.github.worker.register' }
22
- | { action: 'sin.github.worker.status' }
23
- | { action: 'sin.github.worker.claim_next' }
24
- | { action: 'sin.github.worker.checkpoint'; step: string; checkpoint?: Record<string, unknown> }
25
- | { action: 'sin.github.worker.complete'; result?: Record<string, unknown> }
26
- | { action: 'sin.github.issue.ensure'; repo: string; title: string; body: string; labels?: string[] }
27
- | { action: 'sin.github.issue.comment'; repo: string; issueNumber: number; body: string }
28
- | { action: 'sin.github.issue.close'; repo: string; issueNumber: number; comment?: string }
29
- | {
30
- action: 'sin.github.issue.triage.enqueue';
31
- repo: string;
32
- issueNumber: number;
33
- title: string;
34
- body?: string;
35
- labels?: string[];
36
- assignedTeam?: string;
37
- deliveryId?: string;
38
- eventAction?: string;
39
- }
40
  | { action: 'sin.github.project.orchestrate'; prompt: string; contextDir?: string }
41
  | { action: 'sin.github.issue.manage'; prompt: string; issueNumber?: number }
42
  | { action: 'sin.github.wiki.sync'; prompt: string; contextDir?: string }
@@ -44,6 +22,7 @@ export type GitHubAgentAction =
44
  | { action: 'sin.github.gist.publish'; prompt: string; isPublic?: boolean }
45
  | { action: 'sin.github.security.audit'; prompt: string; contextDir?: string }
46
  | { action: 'sin.github.pr.review'; prNumber: number; contextDir?: string }
 
47
  | { action: 'sin.github.ledger.log'; agentName: string; activityTitle: string; details: string };
48
 
49
  export async function executeGitHubAgentAction(action: GitHubAgentAction): Promise<unknown> {
@@ -55,15 +34,9 @@ export async function executeGitHubAgentAction(action: GitHubAgentAction): Promi
55
  mandate: 'CEO Elite GitHub Operations. Orchestrates Projects, Issues, Wikis, Discussions, Gists, Security, PR Reviews, and public Showcase Ledgers.',
56
  actions: [
57
  'sin.github.health',
58
- 'sin.github.worker.register',
59
- 'sin.github.worker.status',
60
- 'sin.github.worker.claim_next',
61
- 'sin.github.worker.checkpoint',
62
- 'sin.github.worker.complete',
63
- 'sin.github.issue.ensure',
64
- 'sin.github.issue.comment',
65
- 'sin.github.issue.close',
66
- 'sin.github.issue.triage.enqueue',
67
  'sin.github.project.orchestrate',
68
  'sin.github.issue.manage',
69
  'sin.github.wiki.sync',
@@ -71,6 +44,7 @@ export async function executeGitHubAgentAction(action: GitHubAgentAction): Promi
71
  'sin.github.gist.publish',
72
  'sin.github.security.audit',
73
  'sin.github.pr.review',
 
74
  'sin.github.ledger.log'
75
  ],
76
  };
@@ -83,41 +57,18 @@ export async function executeGitHubAgentAction(action: GitHubAgentAction): Promi
83
  fallbackModel: 'opencode/minimax-m2.5-free',
84
  team: 'Team - Coding',
85
  status: 'Elite 2026 GitHub Operations Architect Online',
86
- capabilities: ['projects', 'wikis', 'discussions', 'gists', 'security', 'prs', 'issues', 'ledger'],
87
- room13Worker: getCurrentGitHubIssuesWorkerRuntime()?.snapshot() || null,
88
  };
89
 
90
- case 'sin.github.worker.register':
91
- return { ok: true, worker: await getCurrentGitHubIssuesWorkerRuntime()?.start() };
92
 
93
- case 'sin.github.worker.status':
94
- return { ok: true, worker: getCurrentGitHubIssuesWorkerRuntime()?.snapshot() || null };
95
 
96
- case 'sin.github.worker.claim_next':
97
- return { ok: true, task: await getCurrentGitHubIssuesWorkerRuntime()?.claimNextTask() };
98
-
99
- case 'sin.github.worker.checkpoint':
100
- return { ok: true, checkpoint: await getCurrentGitHubIssuesWorkerRuntime()?.checkpoint(action.step, action.checkpoint || {}) };
101
-
102
- case 'sin.github.worker.complete':
103
- return { ok: true, completion: await getCurrentGitHubIssuesWorkerRuntime()?.completeTask(action.result || {}) };
104
-
105
- case 'sin.github.issue.ensure':
106
- return await ensureIssue({
107
- repo: action.repo,
108
- title: action.title,
109
- body: action.body,
110
- labels: action.labels,
111
- });
112
-
113
- case 'sin.github.issue.comment':
114
- return await commentIssue(action.repo, action.issueNumber, action.body);
115
-
116
- case 'sin.github.issue.close':
117
- return await closeIssue(action.repo, action.issueNumber, action.comment);
118
-
119
- case 'sin.github.issue.triage.enqueue':
120
- return await enqueueIssueTriage(action);
121
 
122
  case 'sin.github.project.orchestrate':
123
  return await executeOpenCode(
@@ -158,21 +109,20 @@ export async function executeGitHubAgentAction(action: GitHubAgentAction): Promi
158
  action.contextDir
159
  );
160
 
 
 
 
161
  case 'sin.github.ledger.log':
162
- return await executeOpenCode(
163
- `Create a new public GitHub Issue or Discussion in the repository 'Delqhi/OpenSolver-Ledger' titled "[${action.agentName}] ${action.activityTitle}". The body must contain the following details formatted in premium markdown to showcase our fleet's capabilities: \n\n${action.details}`
164
- );
165
  }
166
  }
167
 
168
  async function executeOpenCode(prompt: string, dir?: string) {
169
  await execAsync('python3 scripts/hf_pull_script.py').catch(() => console.warn('Warning: hf_pull_script failed. Proceeding with cached credentials.'));
170
-
171
- const cmd = `opencode run "${prompt.replace(/"/g, '\\"')}" --model openai/gpt-5.4 --fallback opencode/minimax-m2.5-free`;
172
  const options = dir ? { cwd: dir } : {};
173
 
174
  try {
175
- const { stdout, stderr } = await execAsync(cmd, options);
176
  return {
177
  ok: true,
178
  expertAnalysis: stdout,
@@ -183,129 +133,96 @@ async function executeOpenCode(prompt: string, dir?: string) {
183
  }
184
  }
185
 
186
- async function enqueueIssueTriage(action: Extract<GitHubAgentAction, { action: 'sin.github.issue.triage.enqueue' }>) {
187
- const input = TriageEnqueueSchema.parse(action) as TriageEnqueueInput;
188
- const supabaseUrl = process.env.SUPABASE_URL || process.env.SIN_SUPABASE_URL;
189
- const serviceRole = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SIN_SUPABASE_SERVICE_ROLE_KEY;
190
- if (!supabaseUrl || !serviceRole) {
191
- throw new Error('missing_supabase_credentials');
192
- }
 
 
 
 
 
 
193
 
194
- const supabase = createClient(supabaseUrl, serviceRole, { auth: { persistSession: false } });
195
- const deliveryId = buildDeliveryKey(input);
196
- const issueKey = buildIssueKey(input.repo, input.issueNumber);
197
- const labels = input.labels || [];
198
- const route = routeIssueTriage(labels, input.title, input.body || '');
199
- const assignedTeam = input.assignedTeam || route.assignedTeam;
200
-
201
- const { data: byDelivery, error: deliveryError } = await supabase
202
- .from('sin_issues_pool')
203
- .select('id, issue_number, repo_name, title, status, metadata, delivery_id')
204
- .eq('delivery_id', deliveryId)
205
- .maybeSingle();
206
- if (deliveryError) throw deliveryError;
207
-
208
- if (byDelivery) {
209
  return {
210
  ok: true,
211
- created: false,
212
- deduplicatedBy: 'delivery_id',
213
- queueId: byDelivery.id,
214
- issueKey,
215
- status: byDelivery.status,
216
- deliveryId,
217
  };
 
 
 
 
218
  }
 
219
 
220
- const { data: existing, error: selectError } = await supabase
221
- .from('sin_issues_pool')
222
- .select('id, issue_number, repo_name, title, status, metadata, delivery_id')
223
- .eq('repo_name', input.repo)
224
- .eq('issue_number', input.issueNumber)
225
- .maybeSingle();
226
- if (selectError) throw selectError;
227
-
228
- if (existing) {
229
- const mergedMetadata = {
230
- ...(existing.metadata || {}),
231
- last_delivery_id: deliveryId,
232
- issue_key: issueKey,
233
- event_action: input.eventAction || 'opened',
234
- labels,
235
- ingress: 'github-issues-runtime',
236
- route,
237
- };
238
-
239
- const { error: updateError } = await supabase
240
- .from('sin_issues_pool')
241
- .update({
242
- metadata: mergedMetadata,
243
- delivery_id: deliveryId,
244
- updated_at: new Date().toISOString(),
245
- })
246
- .eq('id', existing.id);
247
- if (updateError) throw updateError;
248
 
249
- return {
250
- ok: true,
251
- created: false,
252
- deduplicatedBy: 'issue_key',
253
- queueId: existing.id,
254
- issueKey,
255
- status: existing.status,
256
- deliveryId,
257
- };
258
  }
259
 
 
 
 
 
260
  const payload = {
261
- issue_number: input.issueNumber,
262
- repo_name: input.repo,
263
- title: input.title,
264
- body: input.body || '',
265
- state: 'open',
266
- assigned_team: assignedTeam,
267
- assigned_agent: null,
268
- status: 'pending',
269
- queue_state: ISSUE_QUEUE_STATE.queued,
270
- delivery_id: deliveryId,
271
- issue_key: issueKey,
272
- event_action: input.eventAction || 'opened',
273
- metadata: {
274
- delivery_id: deliveryId,
275
- issue_key: issueKey,
276
- event_action: input.eventAction || 'opened',
277
- labels,
278
- ingress: 'github-issues-runtime',
279
- route,
280
- },
281
  };
282
 
283
- const { data: inserted, error: insertError } = await supabase
284
- .from('sin_issues_pool')
285
- .insert(payload)
286
- .select('id, status')
287
- .single();
288
- if (insertError) throw insertError;
289
-
290
- await commentIssue(
291
- input.repo,
292
- input.issueNumber,
293
- [
294
- 'AZA-SIN-Coder-•-GitHub-Issues triage complete.',
295
- `Delivery: ${deliveryId}`,
296
- `Assigned team: @${assignedTeam}`,
297
- `Assigned agent hint: @${route.assignedAgentHint}`,
298
- `Capability hint: ${route.capabilityHint}`,
299
- `Queue status: ${ISSUE_QUEUE_STATE.queued}`,
300
- ].join('\n'),
301
- );
302
 
303
  return {
304
  ok: true,
305
- created: true,
306
- queueId: inserted.id,
307
- issueKey,
308
- status: inserted.status,
309
- deliveryId,
310
  };
311
  }
 
1
+ import { exec, execFile } from 'node:child_process';
2
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
  import { promisify } from 'node:util';
 
6
  import { createClient } from '@supabase/supabase-js';
7
+ import { commentIssueAsGitHubApp, getGitHubAppRoutingStatus, routeGitHubWebhook } from './github-app-routing.js';
 
 
 
 
 
 
 
 
 
8
 
9
  const execAsync = promisify(exec);
10
+ const execFileAsync = promisify(execFile);
11
 
12
  export type GitHubAgentAction =
13
  | { action: 'agent.help' }
14
  | { action: 'sin.github.health' }
15
+ | { action: 'sin.github.app.routing.status' }
16
+ | { action: 'sin.github.webhook.route'; payload?: unknown; rawBody?: string; routePath?: string; signature256?: string; eventName?: string; deliveryId?: string }
17
+ | { action: 'sin.github.issue.comment.as_app'; repo: string; issueNumber: number; body: string; agentSlug?: string; appId?: number; installationId: number }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  | { action: 'sin.github.project.orchestrate'; prompt: string; contextDir?: string }
19
  | { action: 'sin.github.issue.manage'; prompt: string; issueNumber?: number }
20
  | { action: 'sin.github.wiki.sync'; prompt: string; contextDir?: string }
 
22
  | { action: 'sin.github.gist.publish'; prompt: string; isPublic?: boolean }
23
  | { action: 'sin.github.security.audit'; prompt: string; contextDir?: string }
24
  | { action: 'sin.github.pr.review'; prNumber: number; contextDir?: string }
25
+ | { action: 'sin.github.issue.pool.enqueue'; repoName: string; issueNumber: number; title: string; body?: string; labels?: string[]; issueUrl?: string; assignedTeam?: string; assignedAgent?: string; status?: string; state?: string; routingStrategy?: string; fanoutPlan?: unknown[] }
26
  | { action: 'sin.github.ledger.log'; agentName: string; activityTitle: string; details: string };
27
 
28
  export async function executeGitHubAgentAction(action: GitHubAgentAction): Promise<unknown> {
 
34
  mandate: 'CEO Elite GitHub Operations. Orchestrates Projects, Issues, Wikis, Discussions, Gists, Security, PR Reviews, and public Showcase Ledgers.',
35
  actions: [
36
  'sin.github.health',
37
+ 'sin.github.app.routing.status',
38
+ 'sin.github.webhook.route',
39
+ 'sin.github.issue.comment.as_app',
 
 
 
 
 
 
40
  'sin.github.project.orchestrate',
41
  'sin.github.issue.manage',
42
  'sin.github.wiki.sync',
 
44
  'sin.github.gist.publish',
45
  'sin.github.security.audit',
46
  'sin.github.pr.review',
47
+ 'sin.github.issue.pool.enqueue',
48
  'sin.github.ledger.log'
49
  ],
50
  };
 
57
  fallbackModel: 'opencode/minimax-m2.5-free',
58
  team: 'Team - Coding',
59
  status: 'Elite 2026 GitHub Operations Architect Online',
60
+ capabilities: ['projects', 'wikis', 'discussions', 'gists', 'security', 'prs', 'issues', 'ledger', 'github-app-routing'],
61
+ githubAppRouting: await getGitHubAppRoutingStatus(),
62
  };
63
 
64
+ case 'sin.github.app.routing.status':
65
+ return await getGitHubAppRoutingStatus();
66
 
67
+ case 'sin.github.webhook.route':
68
+ return await routeGitHubWebhook(action);
69
 
70
+ case 'sin.github.issue.comment.as_app':
71
+ return await commentIssueAsGitHubApp(action);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  case 'sin.github.project.orchestrate':
74
  return await executeOpenCode(
 
109
  action.contextDir
110
  );
111
 
112
+ case 'sin.github.issue.pool.enqueue':
113
+ return await enqueueIssuePoolItem(action);
114
+
115
  case 'sin.github.ledger.log':
116
+ return await publishLedgerLog(action.agentName, action.activityTitle, action.details);
 
 
117
  }
118
  }
119
 
120
  async function executeOpenCode(prompt: string, dir?: string) {
121
  await execAsync('python3 scripts/hf_pull_script.py').catch(() => console.warn('Warning: hf_pull_script failed. Proceeding with cached credentials.'));
 
 
122
  const options = dir ? { cwd: dir } : {};
123
 
124
  try {
125
+ const { stdout, stderr } = await execFileAsync('opencode', ['run', prompt, '--model', 'openai/gpt-5.4'], options);
126
  return {
127
  ok: true,
128
  expertAnalysis: stdout,
 
133
  }
134
  }
135
 
136
+ async function publishLedgerLog(agentName: string, activityTitle: string, details: string) {
137
+ const title = `[${agentName}] ${activityTitle}`;
138
+ const body = [
139
+ `# ${activityTitle}`,
140
+ '',
141
+ `- Agent: ${agentName}`,
142
+ `- Published: ${new Date().toISOString()}`,
143
+ '',
144
+ '## Details',
145
+ details,
146
+ ].join('\n');
147
+ const tempDir = await mkdtemp(join(tmpdir(), 'sin-github-ledger-'));
148
+ const bodyPath = join(tempDir, 'ledger-body.md');
149
 
150
+ try {
151
+ await writeFile(bodyPath, body, 'utf8');
152
+ const { stdout, stderr } = await execFileAsync('gh', [
153
+ 'issue',
154
+ 'create',
155
+ '-R',
156
+ 'Delqhi/OpenSIN-Ledger',
157
+ '--title',
158
+ title,
159
+ '--body-file',
160
+ bodyPath,
161
+ ]);
 
 
 
162
  return {
163
  ok: true,
164
+ repo: 'Delqhi/OpenSIN-Ledger',
165
+ title,
166
+ url: stdout.trim(),
167
+ warnings: stderr || undefined,
 
 
168
  };
169
+ } catch (error: any) {
170
+ throw new Error(`GitHub Ledger Publish Failed: ${error.message}`);
171
+ } finally {
172
+ await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
173
  }
174
+ }
175
 
176
+ async function enqueueIssuePoolItem(action: Extract<GitHubAgentAction, { action: 'sin.github.issue.pool.enqueue' }>) {
177
+ const supabaseUrl = (process.env.SUPABASE_URL || process.env.SIN_SUPABASE_URL || '').trim();
178
+ const serviceRoleKey = (process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SIN_SUPABASE_SERVICE_ROLE_KEY || '').trim();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
+ if (!supabaseUrl || !serviceRoleKey) {
181
+ throw new Error('sin_supabase_config_missing: set SUPABASE_URL/SUPABASE_SERVICE_ROLE_KEY or SIN_SUPABASE_URL/SIN_SUPABASE_SERVICE_ROLE_KEY before enqueueing issue-pool items');
 
 
 
 
 
 
 
182
  }
183
 
184
+ const supabase = createClient(supabaseUrl, serviceRoleKey, {
185
+ auth: { persistSession: false, autoRefreshToken: false },
186
+ });
187
+
188
  const payload = {
189
+ issue_number: action.issueNumber,
190
+ repo_name: action.repoName,
191
+ title: action.title,
192
+ body: action.body || null,
193
+ labels: action.labels || [],
194
+ issue_url: action.issueUrl || null,
195
+ state: action.state || 'open',
196
+ assigned_team: action.assignedTeam || 'team-coding',
197
+ assigned_agent: action.assignedAgent || null,
198
+ status: action.status || 'open',
199
+ routing_strategy: action.routingStrategy || 'single-dispatch',
200
+ fanout_plan: action.fanoutPlan || [],
201
+ updated_at: new Date().toISOString(),
 
 
 
 
 
 
 
202
  };
203
 
204
+ let { data, error } = await supabase.from('sin_issues_pool').insert(payload).select('*').single();
205
+ if (error && /column/i.test(error.message)) {
206
+ const fallbackPayload = {
207
+ issue_number: action.issueNumber,
208
+ repo_name: action.repoName,
209
+ title: action.title,
210
+ body: action.body || null,
211
+ state: action.state || 'open',
212
+ assigned_team: action.assignedTeam || 'team-coding',
213
+ assigned_agent: action.assignedAgent || null,
214
+ status: action.status || 'pending',
215
+ updated_at: new Date().toISOString(),
216
+ };
217
+ ({ data, error } = await supabase.from('sin_issues_pool').insert(fallbackPayload).select('*').single());
218
+ }
219
+ if (error) {
220
+ throw new Error(`sin_issue_pool_enqueue_failed: ${error.message}`);
221
+ }
 
222
 
223
  return {
224
  ok: true,
225
+ table: 'sin_issues_pool',
226
+ item: data,
 
 
 
227
  };
228
  }
src/self-healing.ts DELETED
@@ -1,32 +0,0 @@
1
- import { execFileSync } from 'node:child_process';
2
-
3
- /**
4
- * 🚨 GLOBAL FLEET SELF-HEALING PROTOCOL (PRIORITY 0000) 🚨
5
- * NO-SILO MANDATE: This module catches critical errors, dumps extensive logs,
6
- * and blasts them to the fleet self-healing webhook.
7
- * Hermes and AZA-SIN-Coder-•-GitHub-Issues will automatically take over from there.
8
- */
9
-
10
- export function triggerFleetSelfHealing(error: Error, extensiveContext: Record<string, any>) {
11
- console.error('\n🚨 CRITICAL FAILURE DETECTED. INITIATING NO-SILO SELF-HEALING PROTOCOL 🚨');
12
-
13
- const payload = {
14
- agentId: 'AZA-SIN-Coder-•-GitHub-Issues',
15
- timestamp: new Date().toISOString(),
16
- errorLogs: `Error: ${error.message}\nStack: ${error.stack}\nContext: ${JSON.stringify(extensiveContext, null, 2)}`,
17
- team: 'team-coding'
18
- };
19
-
20
- try {
21
- const webhookUrl = process.env.FLEET_SELF_HEALING_WEBHOOK || 'http://92.5.60.87:5678/webhook/self-healing';
22
-
23
- // Blast to the N8N foundation webhook
24
- const curlCmd = `curl -X POST "${webhookUrl}" -H "Content-Type: application/json" -d '${JSON.stringify(payload).replace(/'/g, "'\\''")}'`;
25
-
26
- execFileSync('bash', ['-c', curlCmd], { encoding: 'utf8' });
27
- console.log('✅ Extensive logs successfully transmitted to Fleet Self-Healing pipeline.');
28
- console.log('👷 The Elite Coder Fleet has been notified and will resolve this architecture flaw autonomously.');
29
- } catch (transmitError: any) {
30
- console.error('❌ FATAL: Could not transmit logs to Fleet Self-Healing pipeline.', transmitError.message);
31
- }
32
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/triage.ts DELETED
@@ -1,100 +0,0 @@
1
- import { z } from 'zod';
2
-
3
- export { ISSUE_QUEUE_STATE } from './queue-state.js';
4
- export type { IssueQueueState as IssueQueueStatus } from './queue-state.js';
5
-
6
- export const TriageEnqueueSchema = z.object({
7
- action: z.literal('sin.github.issue.triage.enqueue'),
8
- repo: z.string().min(3),
9
- issueNumber: z.number().int().positive(),
10
- title: z.string().min(1),
11
- body: z.string().optional(),
12
- labels: z.array(z.string()).optional(),
13
- assignedTeam: z.string().optional(),
14
- deliveryId: z.string().optional(),
15
- eventAction: z.string().optional(),
16
- });
17
-
18
- export type TriageEnqueueInput = z.infer<typeof TriageEnqueueSchema>;
19
-
20
- export type TriageRoute = {
21
- assignedTeam: string;
22
- assignedAgentHint: string;
23
- capabilityHint: string;
24
- rationale: string;
25
- };
26
-
27
- export function buildIssueKey(repo: string, issueNumber: number) {
28
- return `${repo}#${issueNumber}`;
29
- }
30
-
31
- export function buildDeliveryKey(input: Pick<TriageEnqueueInput, 'repo' | 'issueNumber' | 'deliveryId' | 'eventAction'>) {
32
- const eventAction = (input.eventAction || 'opened').trim() || 'opened';
33
- const base = `${input.repo}#${input.issueNumber}:${eventAction}`;
34
- return (input.deliveryId || '').trim() || `manual:${base}`;
35
- }
36
-
37
- export function routeIssueTriage(labels: string[], title = '', body = ''): TriageRoute {
38
- const normalized = labels.map((label) => label.toLowerCase());
39
- const haystack = `${title} ${body}`.toLowerCase();
40
-
41
- if (normalized.some((label) => label.includes('frontend') || label.includes('ui')) || hasAny(haystack, ['react', 'tsx', 'css', 'layout', 'component'])) {
42
- return {
43
- assignedTeam: 'team-coding',
44
- assignedAgentHint: 'A2A-SIN-Coder-Frontend',
45
- capabilityHint: 'command:render-ui',
46
- rationale: 'frontend/ui-signal',
47
- };
48
- }
49
-
50
- if (normalized.some((label) => label.includes('database') || label.includes('sql') || label.includes('supabase')) || hasAny(haystack, ['schema', 'migration', 'postgres', 'rls', 'supabase'])) {
51
- return {
52
- assignedTeam: 'team-coding',
53
- assignedAgentHint: 'A2A-SIN-Coder-Code-Database',
54
- capabilityHint: 'command:data-model',
55
- rationale: 'database-signal',
56
- };
57
- }
58
-
59
- if (normalized.some((label) => label.includes('integration') || label.includes('webhook') || label.includes('api')) || hasAny(haystack, ['webhook', 'api', 'integration', 'queue', 'dispatch'])) {
60
- return {
61
- assignedTeam: 'team-coding',
62
- assignedAgentHint: 'A2A-SIN-Coder-Code-Integration',
63
- capabilityHint: 'command:integration',
64
- rationale: 'integration-signal',
65
- };
66
- }
67
-
68
- if (normalized.some((label) => label.includes('ai') || label.includes('llm') || label.includes('mcp')) || hasAny(haystack, ['prompt', 'llm', 'agent', 'mcp', 'rag'])) {
69
- return {
70
- assignedTeam: 'team-coding',
71
- assignedAgentHint: 'A2A-SIN-Coder-Code-AI',
72
- capabilityHint: 'command:ai-runtime',
73
- rationale: 'ai-signal',
74
- };
75
- }
76
-
77
- if (normalized.some((label) => label.includes('backend') || label.includes('infra') || label.includes('ops')) || hasAny(haystack, ['backend', 'worker', 'lease', 'heartbeat', 'runtime'])) {
78
- return {
79
- assignedTeam: 'team-coding',
80
- assignedAgentHint: 'A2A-SIN-Coder-Backend',
81
- capabilityHint: 'command:backend-runtime',
82
- rationale: 'backend-signal',
83
- };
84
- }
85
-
86
- return {
87
- assignedTeam: 'team-coding',
88
- assignedAgentHint: 'A2A-SIN-Coder-Backend',
89
- capabilityHint: 'command:implement',
90
- rationale: 'default-coder-route',
91
- };
92
- }
93
-
94
- export function routeLabelsToTeam(labels: string[], title = '', body = '') {
95
- return routeIssueTriage(labels, title, body).assignedTeam;
96
- }
97
-
98
- function hasAny(haystack: string, needles: string[]) {
99
- return needles.some((needle) => haystack.includes(needle));
100
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test/github_workflow.test.mjs ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import assert from 'node:assert/strict';
2
+ import { createHmac, generateKeyPairSync } from 'node:crypto';
3
+ import { createServer } from 'node:http';
4
+ import test from 'node:test';
5
+
6
+ const DIST_ROOT = new URL('../dist/src/', import.meta.url);
7
+
8
+ async function importFresh(relativePath) {
9
+ return import(new URL(`${relativePath}?t=${Date.now()}-${Math.random()}`, DIST_ROOT).href);
10
+ }
11
+
12
+ function setEnv(updates) {
13
+ const previous = new Map();
14
+ for (const [key, value] of Object.entries(updates)) {
15
+ previous.set(key, process.env[key]);
16
+ if (value === undefined || value === null) delete process.env[key];
17
+ else process.env[key] = value;
18
+ }
19
+ return () => {
20
+ for (const [key, value] of previous.entries()) {
21
+ if (value === undefined) delete process.env[key];
22
+ else process.env[key] = value;
23
+ }
24
+ };
25
+ }
26
+
27
+ test('GitHub Agent help exposes routing actions', async () => {
28
+ const { executeGitHubAgentAction } = await importFresh('./runtime.js');
29
+
30
+ const help = await executeGitHubAgentAction({ action: 'agent.help' });
31
+ assert.equal(help.ok, true);
32
+ assert.ok(help.actions.includes('sin.github.app.routing.status'));
33
+ assert.ok(help.actions.includes('sin.github.webhook.route'));
34
+ assert.ok(help.actions.includes('sin.github.issue.comment.as_app'));
35
+ });
36
+
37
+ test('routeGitHubWebhook prefers installation.app_id over shared central path', async () => {
38
+ const restoreEnv = setEnv({
39
+ SIN_GITHUB_APP_ROUTING_JSON: JSON.stringify({
40
+ mode: 'central-endpoint-routing',
41
+ defaultWebhookPath: '/api/webhooks/github-apps',
42
+ apps: [
43
+ {
44
+ appIdEnv: 'SIN_HERMES_GITHUB_APP_ID',
45
+ appSlug: 'sin-hermes',
46
+ preferredAgentSlug: 'sin-hermesbote',
47
+ botName: 'SIN-Hermes[bot]',
48
+ webhookPath: '/api/webhooks/github-apps',
49
+ webhookSecretEnv: 'SIN_HERMES_GITHUB_APP_WEBHOOK_SECRET',
50
+ },
51
+ {
52
+ appIdEnv: 'SIN_BUGBOUNTY_GITHUB_APP_ID',
53
+ appSlug: 'sin-bugbounty',
54
+ preferredAgentSlug: 'sin-bugbounty',
55
+ botName: 'SIN-BugBounty[bot]',
56
+ webhookPath: '/api/webhooks/github-apps',
57
+ webhookSecretEnv: 'SIN_BUGBOUNTY_GITHUB_APP_WEBHOOK_SECRET',
58
+ },
59
+ ],
60
+ }),
61
+ SIN_HERMES_GITHUB_APP_ID: '123456',
62
+ SIN_HERMES_GITHUB_APP_WEBHOOK_SECRET: 'secret-1',
63
+ SIN_BUGBOUNTY_GITHUB_APP_ID: '345678',
64
+ SIN_BUGBOUNTY_GITHUB_APP_WEBHOOK_SECRET: 'secret-3',
65
+ });
66
+
67
+ try {
68
+ const { routeGitHubWebhook } = await importFresh('./github-app-routing.js');
69
+ const rawBody = JSON.stringify({
70
+ action: 'opened',
71
+ installation: { id: 987654321, app_id: 345678 },
72
+ repository: { full_name: 'OpenSIN-AI/OpenSIN-backend' },
73
+ });
74
+ const signature256 = `sha256=${createHmac('sha256', 'secret-3').update(rawBody).digest('hex')}`;
75
+ const result = await routeGitHubWebhook({
76
+ rawBody,
77
+ routePath: '/api/webhooks/github-apps',
78
+ signature256,
79
+ eventName: 'pull_request',
80
+ deliveryId: 'delivery-central-test',
81
+ });
82
+ assert.equal(result.ok, true);
83
+ assert.equal(result.route.agentSlug, 'sin-bugbounty');
84
+ assert.equal(result.route.botName, 'SIN-BugBounty[bot]');
85
+ } finally {
86
+ restoreEnv();
87
+ }
88
+ });
89
+
90
+ test('commentIssueAsGitHubApp exchanges installation token and posts comment', async () => {
91
+ const { privateKey } = generateKeyPairSync('rsa', {
92
+ modulusLength: 2048,
93
+ privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
94
+ publicKeyEncoding: { type: 'pkcs1', format: 'pem' },
95
+ });
96
+
97
+ let commentRequestBody = '';
98
+ const server = createServer((req, res) => {
99
+ if (req.method === 'POST' && req.url === '/app/installations/987654321/access_tokens') {
100
+ res.writeHead(200, { 'content-type': 'application/json' });
101
+ res.end(JSON.stringify({ token: 'installation-token-1', expires_at: '2030-01-01T00:00:00Z' }));
102
+ return;
103
+ }
104
+ if (req.method === 'POST' && req.url === '/repos/OpenSIN-AI/OpenSIN-backend/issues/42/comments') {
105
+ const chunks = [];
106
+ req.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
107
+ req.on('end', () => {
108
+ commentRequestBody = Buffer.concat(chunks).toString('utf8');
109
+ res.writeHead(200, { 'content-type': 'application/json' });
110
+ res.end(JSON.stringify({ id: 9001, html_url: 'https://github.com/OpenSIN-AI/OpenSIN-backend/issues/42#issuecomment-9001' }));
111
+ });
112
+ return;
113
+ }
114
+ res.writeHead(404, { 'content-type': 'application/json' });
115
+ res.end(JSON.stringify({ error: 'not_found' }));
116
+ });
117
+
118
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
119
+ const address = server.address();
120
+ const port = typeof address === 'object' && address ? address.port : 0;
121
+ const restoreEnv = setEnv({
122
+ SIN_GITHUB_API_BASE_URL: `http://127.0.0.1:${port}`,
123
+ SIN_GITHUB_APP_ROUTING_JSON: JSON.stringify({
124
+ defaultMode: 'hybrid',
125
+ defaultWebhookPath: '/github/webhook',
126
+ apps: [
127
+ {
128
+ agentSlug: 'sin-hermesbote',
129
+ appSlug: 'sin-hermes',
130
+ botName: 'SIN-Hermes[bot]',
131
+ appId: 123456,
132
+ privateKeyPem: privateKey,
133
+ webhookSecret: 'secret-1',
134
+ },
135
+ ],
136
+ }),
137
+ });
138
+
139
+ try {
140
+ const { commentIssueAsGitHubApp } = await importFresh('./github-app-routing.js');
141
+ const result = await commentIssueAsGitHubApp({
142
+ repo: 'OpenSIN-AI/OpenSIN-backend',
143
+ issueNumber: 42,
144
+ body: 'Hello from SIN-Hermes[bot]',
145
+ agentSlug: 'sin-hermesbote',
146
+ installationId: 987654321,
147
+ });
148
+ assert.equal(result.ok, true);
149
+ assert.equal(result.agentSlug, 'sin-hermesbote');
150
+ assert.equal(result.commentId, 9001);
151
+ assert.match(commentRequestBody, /Hello from SIN-Hermes\[bot\]/);
152
+ } finally {
153
+ restoreEnv();
154
+ await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
155
+ }
156
+ });