devscribe-reason 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +24 -0
- package/.env.example +10 -0
- package/README.md +118 -0
- package/package.json +29 -0
- package/scripts/setup.js +94 -0
- package/src/CLAUDE.md +100 -0
- package/src/index.js +620 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(''''version:'''', d.get\\(''''version''''\\)\\); print\\(''''main:'''', d.get\\(''''main''''\\)\\); print\\(''''exports keys:'''', list\\(d.get\\(''''exports'''',{}\\).keys\\(\\)\\)[:20]\\)\")",
|
|
5
|
+
"Bash(claude --version)",
|
|
6
|
+
"Read(//Users/giannihart/.claude/**)",
|
|
7
|
+
"Bash(node -e \"const http = require\\(''http''\\); const crypto = require\\(''crypto''\\); console.log\\(''http:'', typeof http.createServer\\); console.log\\(''crypto:'', typeof crypto.createHmac\\);\")",
|
|
8
|
+
"Bash(node -e \":*)",
|
|
9
|
+
"Bash(timeout 5 node src/index.js)",
|
|
10
|
+
"Bash(timeout 3 node src/index.js)",
|
|
11
|
+
"Bash(node -c src/index.js)",
|
|
12
|
+
"Bash(chmod +x scripts/setup.js)",
|
|
13
|
+
"Bash(npm install:*)",
|
|
14
|
+
"Bash(npm link:*)",
|
|
15
|
+
"Bash(devscribe-reason)",
|
|
16
|
+
"Bash(claude mcp:*)",
|
|
17
|
+
"Bash(GITHUB_TOKEN=ghp_test GITHUB_REPO=owner/repo npm run setup)",
|
|
18
|
+
"Bash(npm unlink:*)",
|
|
19
|
+
"Bash(GITHUB_TOKEN=ghp_test npm run setup)",
|
|
20
|
+
"Bash(node -c scripts/setup.js)",
|
|
21
|
+
"Bash(git -C . status --short)"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
package/.env.example
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Required for committing reasoning docs to GitHub
|
|
2
|
+
GITHUB_TOKEN=ghp_your_personal_access_token
|
|
3
|
+
# Optional: auto-detected from git remote if not set
|
|
4
|
+
GITHUB_REPO=owner/repo
|
|
5
|
+
GITHUB_BRANCH=main
|
|
6
|
+
|
|
7
|
+
# Optional: enable GitHub webhook auto-finalization
|
|
8
|
+
# Both WEBHOOK_PORT and GITHUB_WEBHOOK_SECRET must be set to activate
|
|
9
|
+
# WEBHOOK_PORT=3001
|
|
10
|
+
# GITHUB_WEBHOOK_SECRET=your_webhook_secret_from_github
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Devscribe Reason
|
|
2
|
+
|
|
3
|
+
An MCP server that automatically captures the reasoning behind engineering decisions made during Claude Code sessions and commits them as structured markdown files to `/docs/decisions/` in your GitHub repo.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
When developers and AI agents write code, the reasoning behind decisions — why this approach over alternatives, what tradeoffs were made, what was considered and rejected — lives only in the chat session and disappears when it closes. Devscribe Reason captures that reasoning automatically in real time.
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
**One command, 30 seconds, fully configured:**
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
GITHUB_TOKEN=ghp_your_personal_access_token npx devscribe-reason
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
That's it. The MCP server is now active in all your Claude Code sessions. Reasoning docs auto-commit to `/docs/decisions/`.
|
|
18
|
+
|
|
19
|
+
## How It Works
|
|
20
|
+
|
|
21
|
+
The server exposes four MCP tools that Claude Code calls throughout a coding session **automatically** — no manual configuration required:
|
|
22
|
+
|
|
23
|
+
| Tool | When It's Called | What It Does |
|
|
24
|
+
|------|-----------------|--------------|
|
|
25
|
+
| `log_intent` | Start of session | Records what you're building and why |
|
|
26
|
+
| `log_decision` | When a technical choice is made | Captures the decision, reasoning, and code context |
|
|
27
|
+
| `log_alternative` | When an approach is rejected | Documents what was considered and why it was dropped |
|
|
28
|
+
| `finalize_reasoning_doc` | When work is done | Automatically triggered when you say "we're done", "open a PR", etc. |
|
|
29
|
+
|
|
30
|
+
## Setup
|
|
31
|
+
|
|
32
|
+
### Option 1: NPM (Recommended)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Install globally and configure in one step
|
|
36
|
+
GITHUB_TOKEN=ghp_your_personal_access_token npx devscribe-reason
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The setup script will:
|
|
40
|
+
- Detect your Claude Code installation automatically
|
|
41
|
+
- Register devscribe-reason as a user-level MCP server (works in all repos)
|
|
42
|
+
- Auto-detect your GitHub repo from the current directory
|
|
43
|
+
- Done! Restart Claude Code
|
|
44
|
+
|
|
45
|
+
### Option 2: Docker / Development
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/YOUR_USERNAME/Devscribe-Reason.git
|
|
49
|
+
cd Devscribe-Reason
|
|
50
|
+
npm install
|
|
51
|
+
npm run setup
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Environment Variables
|
|
55
|
+
|
|
56
|
+
**Required:**
|
|
57
|
+
- `GITHUB_TOKEN` — [Personal Access Token](https://github.com/settings/tokens) with `repo` scope
|
|
58
|
+
|
|
59
|
+
**Optional (auto-detected):**
|
|
60
|
+
- `GITHUB_REPO` — GitHub repo (`owner/repo`), auto-detected from git remote
|
|
61
|
+
- `GITHUB_BRANCH` — Target branch (default: `main`)
|
|
62
|
+
|
|
63
|
+
**Optional (webhook auto-finalization):**
|
|
64
|
+
- `WEBHOOK_PORT` — Port for GitHub webhook listener (e.g., `3001`)
|
|
65
|
+
- `GITHUB_WEBHOOK_SECRET` — GitHub webhook secret (both env vars required to activate)
|
|
66
|
+
|
|
67
|
+
## Features
|
|
68
|
+
|
|
69
|
+
✅ **Zero setup** — Inject `instructions` via MCP protocol, no CLAUDE.md needed
|
|
70
|
+
✅ **Automatic logging** — Captures decisions silently throughout your session
|
|
71
|
+
✅ **Natural language triggers** — Say "we're done", "ship it", "open a PR" — anything works
|
|
72
|
+
✅ **GitHub webhook** — Auto-finalize reasoning docs when PRs are opened (optional)
|
|
73
|
+
✅ **Auto-detect repo** — Parses git remote to populate `GITHUB_REPO` automatically
|
|
74
|
+
✅ **Global MCP server** — Works in every repo, configured once at user level
|
|
75
|
+
|
|
76
|
+
## Output
|
|
77
|
+
|
|
78
|
+
Each session produces a markdown file in `/docs/decisions/` like:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
docs/decisions/2026-03-06-auth-refactor-oauth.md
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
With structured sections: Intent, Key Decisions, Alternatives Considered, Open Questions, and Links.
|
|
85
|
+
|
|
86
|
+
## GitHub Webhook (Optional)
|
|
87
|
+
|
|
88
|
+
To auto-commit reasoning docs when PRs are opened:
|
|
89
|
+
|
|
90
|
+
1. Create a GitHub App or use a Personal Access Token with webhook permissions
|
|
91
|
+
2. Run the server with:
|
|
92
|
+
```bash
|
|
93
|
+
WEBHOOK_PORT=3001 GITHUB_WEBHOOK_SECRET=your_secret npm start
|
|
94
|
+
```
|
|
95
|
+
3. Add webhook in GitHub repo settings:
|
|
96
|
+
- Payload URL: `https://your-domain.com/webhook`
|
|
97
|
+
- Events: `Pull Requests`
|
|
98
|
+
- Secret: `your_secret`
|
|
99
|
+
|
|
100
|
+
When a PR is opened, the reasoning doc is automatically committed to the PR's base branch.
|
|
101
|
+
|
|
102
|
+
## Troubleshooting
|
|
103
|
+
|
|
104
|
+
**"Could not find the 'claude' command"**
|
|
105
|
+
- Ensure Claude Code CLI or VS Code/Cursor with Claude Code extension is installed
|
|
106
|
+
- Reinstall: `npm run setup`
|
|
107
|
+
|
|
108
|
+
**"MCP server already exists"**
|
|
109
|
+
- The server is already registered. To re-register: `claude mcp remove --scope user devscribe-reason` then run setup again
|
|
110
|
+
|
|
111
|
+
**Reasoning docs not committing**
|
|
112
|
+
- Verify `GITHUB_TOKEN` has `repo` scope
|
|
113
|
+
- Check that `GITHUB_REPO` is set correctly: `echo $GITHUB_REPO`
|
|
114
|
+
- Verify the repo exists and the token has push access
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "devscribe-reason",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server that captures engineering decision reasoning and commits structured markdown to GitHub",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"devscribe-reason": "scripts/setup.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"setup": "node scripts/setup.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"reasoning",
|
|
17
|
+
"decisions",
|
|
18
|
+
"documentation",
|
|
19
|
+
"claude-code"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
24
|
+
"@octokit/rest": "^21.1.1",
|
|
25
|
+
"dotenv": "^16.4.7",
|
|
26
|
+
"glob": "^10.5.0",
|
|
27
|
+
"uuid": "^11.1.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/scripts/setup.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { resolve, dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { globSync } from "glob";
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const serverPath = resolve(__dirname, "../src/index.js");
|
|
12
|
+
const token = process.env.GITHUB_TOKEN || "";
|
|
13
|
+
const repo = process.env.GITHUB_REPO || "";
|
|
14
|
+
const branch = process.env.GITHUB_BRANCH || "main";
|
|
15
|
+
|
|
16
|
+
// Try to find the claude binary in common locations
|
|
17
|
+
function findClaudeBinary() {
|
|
18
|
+
// 1. Try 'which claude' if claude is in PATH
|
|
19
|
+
try {
|
|
20
|
+
const result = execSync("which claude", { stdio: "pipe", timeout: 1000 }).toString().trim();
|
|
21
|
+
if (result) return result;
|
|
22
|
+
} catch {
|
|
23
|
+
// Not in PATH
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 2. Try Cursor extensions (macOS)
|
|
27
|
+
try {
|
|
28
|
+
const cursorPattern = join(homedir(), ".cursor/extensions/*/resources/native-binary/claude");
|
|
29
|
+
const matches = globSync(cursorPattern, { absolute: true });
|
|
30
|
+
if (matches.length > 0) return matches[0];
|
|
31
|
+
} catch {
|
|
32
|
+
// globSync not available or pattern didn't match
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 3. Try VS Code extensions (macOS)
|
|
36
|
+
try {
|
|
37
|
+
const vscodePattern = join(homedir(), ".vscode/extensions/*/resources/native-binary/claude");
|
|
38
|
+
const matches = globSync(vscodePattern, { absolute: true });
|
|
39
|
+
if (matches.length > 0) return matches[0];
|
|
40
|
+
} catch {
|
|
41
|
+
// globSync not available or pattern didn't match
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 4. Try Claude Desktop installation (macOS)
|
|
45
|
+
const claudeDesktopPath = join(
|
|
46
|
+
homedir(),
|
|
47
|
+
"Library/Application Support/Claude/claude-code/claude"
|
|
48
|
+
);
|
|
49
|
+
if (existsSync(claudeDesktopPath)) return claudeDesktopPath;
|
|
50
|
+
|
|
51
|
+
// 5. Try Claude Desktop on Linux
|
|
52
|
+
const claudeLinuxPath = join(homedir(), ".local/share/Claude/claude");
|
|
53
|
+
if (existsSync(claudeLinuxPath)) return claudeLinuxPath;
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Build env args for the CLI
|
|
59
|
+
const envArgs = [
|
|
60
|
+
token && `-e GITHUB_TOKEN=${token}`,
|
|
61
|
+
repo && `-e GITHUB_REPO=${repo}`,
|
|
62
|
+
`-e GITHUB_BRANCH=${branch}`,
|
|
63
|
+
]
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.join(" ");
|
|
66
|
+
|
|
67
|
+
const claudeCmd = findClaudeBinary();
|
|
68
|
+
|
|
69
|
+
if (!claudeCmd) {
|
|
70
|
+
console.error(
|
|
71
|
+
"\n❌ Could not find the 'claude' command. Please ensure you have:\n"
|
|
72
|
+
);
|
|
73
|
+
console.error(" • Claude Code CLI installed (https://claude.com/claude-code)");
|
|
74
|
+
console.error(" • Or VS Code with the Claude Code extension");
|
|
75
|
+
console.error(" • Or Cursor with Claude Code support\n");
|
|
76
|
+
console.error("After installation, run: npm run setup\n");
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const cmd = `${claudeCmd} mcp add --scope user devscribe-reason node ${serverPath} ${envArgs}`;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
execSync(cmd, { stdio: "inherit" });
|
|
84
|
+
console.log(
|
|
85
|
+
"\n✅ Setup complete. devscribe-reason is now active in all your Claude Code sessions."
|
|
86
|
+
);
|
|
87
|
+
console.log(
|
|
88
|
+
"\nTo verify: Open Claude Code and check Settings > MCP Servers\n"
|
|
89
|
+
);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error("\n❌ Setup failed. Run this command manually:\n");
|
|
92
|
+
console.error(` ${cmd}\n`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
package/src/CLAUDE.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Devscribe Reason
|
|
2
|
+
|
|
3
|
+
An MCP server that automatically captures the reasoning behind engineering decisions made during Claude Code sessions and commits them as structured markdown files to `/docs/decisions/` in your GitHub repo.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
When developers and AI agents write code, the reasoning behind decisions — why this approach over alternatives, what tradeoffs were made, what was considered and rejected — lives only in the chat session and disappears when it closes. Devscribe Reason captures that reasoning automatically in real time.
|
|
8
|
+
|
|
9
|
+
## How It Works
|
|
10
|
+
|
|
11
|
+
The server exposes four MCP tools that Claude Code calls throughout a coding session:
|
|
12
|
+
|
|
13
|
+
| Tool | When It's Called | What It Does |
|
|
14
|
+
|------|-----------------|--------------|
|
|
15
|
+
| `log_intent` | Start of session | Records what you're building and why |
|
|
16
|
+
| `log_decision` | When a technical choice is made | Captures the decision, reasoning, and code context |
|
|
17
|
+
| `log_alternative` | When an approach is rejected | Documents what was considered and why it was dropped |
|
|
18
|
+
| `finalize_reasoning_doc` | End of session / PR time | Builds a structured markdown doc and commits it to GitHub |
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
### 1. Clone and install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git clone https://github.com/YOUR_USERNAME/Devscribe-Reason.git
|
|
26
|
+
cd Devscribe-Reason
|
|
27
|
+
npm install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 2. Configure environment variables
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cp .env.example .env
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Edit `.env` with your values:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
GITHUB_TOKEN=ghp_your_personal_access_token
|
|
40
|
+
GITHUB_REPO=owner/repo
|
|
41
|
+
GITHUB_BRANCH=main
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**GitHub Token:** Create a [Personal Access Token](https://github.com/settings/tokens) with `repo` scope (or `contents: write` for fine-grained tokens).
|
|
45
|
+
|
|
46
|
+
### 3. Connect to Claude Code
|
|
47
|
+
|
|
48
|
+
Add this to your Claude Code MCP settings (`~/.claude/claude_desktop_config.json` or via the Claude Code CLI):
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"devscribe-reason": {
|
|
54
|
+
"command": "node",
|
|
55
|
+
"args": ["/absolute/path/to/Devscribe-Reason/src/index.js"],
|
|
56
|
+
"env": {
|
|
57
|
+
"GITHUB_TOKEN": "ghp_your_token",
|
|
58
|
+
"GITHUB_REPO": "owner/repo",
|
|
59
|
+
"GITHUB_BRANCH": "main"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or using the Claude Code CLI:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
claude mcp add devscribe-reason node /absolute/path/to/Devscribe-Reason/src/index.js \
|
|
70
|
+
-e GITHUB_TOKEN=ghp_your_token \
|
|
71
|
+
-e GITHUB_REPO=owner/repo \
|
|
72
|
+
-e GITHUB_BRANCH=main
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 4. Add system prompt instructions
|
|
76
|
+
|
|
77
|
+
Add these instructions to your Claude Code system prompt (via CLAUDE.md or settings) so it uses the tools automatically:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
When the devscribe-reason MCP server is connected:
|
|
81
|
+
- Call log_intent at the start of every session before doing any work
|
|
82
|
+
- Call log_decision whenever making a significant technical or architectural choice
|
|
83
|
+
- Call log_alternative whenever an approach is considered and rejected
|
|
84
|
+
- Call finalize_reasoning_doc when work is complete or a PR is ready
|
|
85
|
+
- Never ask permission to log — just do it silently in the background
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Output
|
|
89
|
+
|
|
90
|
+
Each session produces a markdown file in `/docs/decisions/` like:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
docs/decisions/2026-03-06-auth-refactor-oauth.md
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
See the file for structured sections: Intent, Key Decisions, Alternatives Considered, Open Questions, and Links.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|
package/src/index.js
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { Octokit } from "@octokit/rest";
|
|
10
|
+
import { v4 as uuidv4 } from "uuid";
|
|
11
|
+
import dotenv from "dotenv";
|
|
12
|
+
import { createServer as createHttpServer } from "node:http";
|
|
13
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
14
|
+
|
|
15
|
+
dotenv.config();
|
|
16
|
+
|
|
17
|
+
// --- Session state (in-memory) ---
|
|
18
|
+
|
|
19
|
+
const sessions = new Map();
|
|
20
|
+
|
|
21
|
+
function getOrCreateSession(sessionId) {
|
|
22
|
+
if (!sessions.has(sessionId)) {
|
|
23
|
+
sessions.set(sessionId, {
|
|
24
|
+
id: sessionId || uuidv4(),
|
|
25
|
+
intent: null,
|
|
26
|
+
projectContext: null,
|
|
27
|
+
decisions: [],
|
|
28
|
+
alternatives: [],
|
|
29
|
+
openQuestions: [],
|
|
30
|
+
createdAt: new Date().toISOString(),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return sessions.get(sessionId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Use a default session for simplicity — Claude Code calls tools sequentially in one session
|
|
37
|
+
const DEFAULT_SESSION = uuidv4();
|
|
38
|
+
|
|
39
|
+
// --- Markdown generation ---
|
|
40
|
+
|
|
41
|
+
function buildMarkdown(session, prTitle, prDescription, branch) {
|
|
42
|
+
const date = new Date().toISOString().split("T")[0];
|
|
43
|
+
const lines = [];
|
|
44
|
+
|
|
45
|
+
lines.push(`# ${prTitle}`);
|
|
46
|
+
lines.push(`**Date:** ${date} `);
|
|
47
|
+
lines.push(`**Branch:** ${branch || process.env.GITHUB_BRANCH || "main"} `);
|
|
48
|
+
lines.push(`**Session ID:** ${session.id}`);
|
|
49
|
+
lines.push("");
|
|
50
|
+
|
|
51
|
+
// Intent
|
|
52
|
+
lines.push("## Intent");
|
|
53
|
+
if (session.intent) {
|
|
54
|
+
lines.push(session.intent);
|
|
55
|
+
if (session.projectContext) {
|
|
56
|
+
lines.push("");
|
|
57
|
+
lines.push(`**Project context:** ${session.projectContext}`);
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
lines.push("_No intent was logged for this session._");
|
|
61
|
+
}
|
|
62
|
+
lines.push("");
|
|
63
|
+
|
|
64
|
+
// Key Decisions
|
|
65
|
+
lines.push("## Key Decisions");
|
|
66
|
+
if (session.decisions.length === 0) {
|
|
67
|
+
lines.push("_No decisions were logged for this session._");
|
|
68
|
+
} else {
|
|
69
|
+
session.decisions.forEach((d, i) => {
|
|
70
|
+
lines.push(`### Decision ${i + 1}: ${d.decision.split(".")[0].slice(0, 80)}`);
|
|
71
|
+
lines.push(`**What:** ${d.decision} `);
|
|
72
|
+
lines.push(`**Why:** ${d.reasoning} `);
|
|
73
|
+
lines.push(`**Code context:** ${d.codeContext}`);
|
|
74
|
+
lines.push("");
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
lines.push("");
|
|
78
|
+
|
|
79
|
+
// Alternatives Considered
|
|
80
|
+
lines.push("## Alternatives Considered");
|
|
81
|
+
if (session.alternatives.length === 0) {
|
|
82
|
+
lines.push("_No alternatives were logged for this session._");
|
|
83
|
+
} else {
|
|
84
|
+
session.alternatives.forEach((a, i) => {
|
|
85
|
+
lines.push(`### Alternative ${i + 1}: ${a.alternative.split(".")[0].slice(0, 80)}`);
|
|
86
|
+
lines.push(`**What was considered:** ${a.alternative} `);
|
|
87
|
+
lines.push(`**Why rejected:** ${a.rejectionReason}`);
|
|
88
|
+
lines.push("");
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
lines.push("");
|
|
92
|
+
|
|
93
|
+
// Open Questions
|
|
94
|
+
lines.push("## Open Questions");
|
|
95
|
+
if (session.openQuestions.length === 0) {
|
|
96
|
+
lines.push("_No open questions were flagged during this session._");
|
|
97
|
+
} else {
|
|
98
|
+
session.openQuestions.forEach((q) => {
|
|
99
|
+
lines.push(`- ${q}`);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
lines.push("");
|
|
103
|
+
|
|
104
|
+
// Links
|
|
105
|
+
lines.push("## Links");
|
|
106
|
+
if (prDescription) {
|
|
107
|
+
lines.push(`- **PR Description:** ${prDescription}`);
|
|
108
|
+
}
|
|
109
|
+
lines.push("- Related decisions: _see `/docs/decisions/` for other decision records_");
|
|
110
|
+
lines.push("");
|
|
111
|
+
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function toKebabCase(str) {
|
|
116
|
+
return str
|
|
117
|
+
.toLowerCase()
|
|
118
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
119
|
+
.replace(/\s+/g, "-")
|
|
120
|
+
.replace(/-+/g, "-")
|
|
121
|
+
.replace(/^-|-$/g, "")
|
|
122
|
+
.slice(0, 60);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- GitHub commit ---
|
|
126
|
+
|
|
127
|
+
async function commitToGitHub(filePath, content, commitMessage, branch) {
|
|
128
|
+
const token = process.env.GITHUB_TOKEN;
|
|
129
|
+
const repo = process.env.GITHUB_REPO;
|
|
130
|
+
|
|
131
|
+
if (!token || !repo) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
"Missing GITHUB_TOKEN or GITHUB_REPO environment variables. " +
|
|
134
|
+
"Set them in your .env file or environment."
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const [owner, repoName] = repo.split("/");
|
|
139
|
+
const targetBranch = branch || process.env.GITHUB_BRANCH || "main";
|
|
140
|
+
|
|
141
|
+
const octokit = new Octokit({ auth: token });
|
|
142
|
+
|
|
143
|
+
// Get the latest commit SHA on the branch
|
|
144
|
+
const { data: ref } = await octokit.git.getRef({
|
|
145
|
+
owner,
|
|
146
|
+
repo: repoName,
|
|
147
|
+
ref: `heads/${targetBranch}`,
|
|
148
|
+
});
|
|
149
|
+
const latestCommitSha = ref.object.sha;
|
|
150
|
+
|
|
151
|
+
// Get the tree SHA of the latest commit
|
|
152
|
+
const { data: commit } = await octokit.git.getCommit({
|
|
153
|
+
owner,
|
|
154
|
+
repo: repoName,
|
|
155
|
+
commit_sha: latestCommitSha,
|
|
156
|
+
});
|
|
157
|
+
const treeSha = commit.tree.sha;
|
|
158
|
+
|
|
159
|
+
// Create a blob with the file content
|
|
160
|
+
const { data: blob } = await octokit.git.createBlob({
|
|
161
|
+
owner,
|
|
162
|
+
repo: repoName,
|
|
163
|
+
content: Buffer.from(content).toString("base64"),
|
|
164
|
+
encoding: "base64",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Create a new tree with the file
|
|
168
|
+
const { data: newTree } = await octokit.git.createTree({
|
|
169
|
+
owner,
|
|
170
|
+
repo: repoName,
|
|
171
|
+
base_tree: treeSha,
|
|
172
|
+
tree: [
|
|
173
|
+
{
|
|
174
|
+
path: filePath,
|
|
175
|
+
mode: "100644",
|
|
176
|
+
type: "blob",
|
|
177
|
+
sha: blob.sha,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Create a new commit
|
|
183
|
+
const { data: newCommit } = await octokit.git.createCommit({
|
|
184
|
+
owner,
|
|
185
|
+
repo: repoName,
|
|
186
|
+
message: commitMessage,
|
|
187
|
+
tree: newTree.sha,
|
|
188
|
+
parents: [latestCommitSha],
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Update the branch reference
|
|
192
|
+
await octokit.git.updateRef({
|
|
193
|
+
owner,
|
|
194
|
+
repo: repoName,
|
|
195
|
+
ref: `heads/${targetBranch}`,
|
|
196
|
+
sha: newCommit.sha,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
commitSha: newCommit.sha,
|
|
201
|
+
commitUrl: newCommit.html_url,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// --- Finalization helper (reusable by MCP tool and webhook) ---
|
|
206
|
+
|
|
207
|
+
let finalizationInFlight = false;
|
|
208
|
+
|
|
209
|
+
async function runFinalization(prTitle, prDescription, branch) {
|
|
210
|
+
if (finalizationInFlight) {
|
|
211
|
+
throw new Error("Finalization is already in progress. Please wait.");
|
|
212
|
+
}
|
|
213
|
+
finalizationInFlight = true;
|
|
214
|
+
try {
|
|
215
|
+
const session = getOrCreateSession(DEFAULT_SESSION);
|
|
216
|
+
const targetBranch = branch || process.env.GITHUB_BRANCH || "main";
|
|
217
|
+
const markdown = buildMarkdown(session, prTitle, prDescription, targetBranch);
|
|
218
|
+
const date = new Date().toISOString().split("T")[0];
|
|
219
|
+
const fileName = `${date}-${toKebabCase(prTitle)}.md`;
|
|
220
|
+
const filePath = `docs/decisions/${fileName}`;
|
|
221
|
+
const commitMessage = `docs: add reasoning doc for "${prTitle}"`;
|
|
222
|
+
|
|
223
|
+
const result = await commitToGitHub(filePath, markdown, commitMessage, targetBranch);
|
|
224
|
+
|
|
225
|
+
// Clear the session after successful commit
|
|
226
|
+
sessions.delete(DEFAULT_SESSION);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
filePath,
|
|
230
|
+
commitSha: result.commitSha,
|
|
231
|
+
branch: targetBranch,
|
|
232
|
+
decisionsCount: session.decisions.length,
|
|
233
|
+
alternativesCount: session.alternatives.length,
|
|
234
|
+
hasIntent: Boolean(session.intent),
|
|
235
|
+
};
|
|
236
|
+
} finally {
|
|
237
|
+
finalizationInFlight = false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// --- MCP Server setup ---
|
|
242
|
+
|
|
243
|
+
const SERVER_INSTRUCTIONS = `You are connected to the devscribe-reason MCP server, which automatically documents your engineering decisions.
|
|
244
|
+
|
|
245
|
+
REQUIRED BEHAVIOR - follow these rules silently without asking permission:
|
|
246
|
+
|
|
247
|
+
1. SESSION START: Call log_intent immediately at the start of every session, before doing any other work. Capture what the developer is trying to build and why.
|
|
248
|
+
|
|
249
|
+
2. DURING WORK: Call log_decision whenever you make any meaningful technical choice — architecture, library selection, data model design, API design, file/module structure, or implementation approach. Do not ask; just log it silently.
|
|
250
|
+
|
|
251
|
+
3. DURING WORK: Call log_alternative whenever you evaluate an approach and decide against it. Do not ask; just log it silently.
|
|
252
|
+
|
|
253
|
+
4. SESSION END: Call finalize_reasoning_doc when the developer indicates the session is ending. Recognize these signal phrases:
|
|
254
|
+
- "we're done", "all done", "that's it", "done for now"
|
|
255
|
+
- "open a PR", "create a PR", "make a PR", "submit a PR"
|
|
256
|
+
- "commit this", "ship it", "push this", "merge this"
|
|
257
|
+
- "finalize", "wrap up", "finish up"
|
|
258
|
+
- Any request to open or create a pull request
|
|
259
|
+
|
|
260
|
+
Never ask permission before calling any of these tools. Log silently in the background.`;
|
|
261
|
+
|
|
262
|
+
const server = new Server(
|
|
263
|
+
{
|
|
264
|
+
name: "devscribe-reason",
|
|
265
|
+
version: "1.0.0",
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
capabilities: {
|
|
269
|
+
tools: {},
|
|
270
|
+
},
|
|
271
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// List tools
|
|
276
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
277
|
+
return {
|
|
278
|
+
tools: [
|
|
279
|
+
{
|
|
280
|
+
name: "log_intent",
|
|
281
|
+
description:
|
|
282
|
+
"Log the intent for the current coding session. Call this at the START of every session before doing any work. Captures what the developer is trying to build and why.",
|
|
283
|
+
inputSchema: {
|
|
284
|
+
type: "object",
|
|
285
|
+
properties: {
|
|
286
|
+
intent: {
|
|
287
|
+
type: "string",
|
|
288
|
+
description: "What the developer is trying to build or accomplish",
|
|
289
|
+
},
|
|
290
|
+
project_context: {
|
|
291
|
+
type: "string",
|
|
292
|
+
description:
|
|
293
|
+
"Relevant background context about the project, codebase, or feature area",
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
required: ["intent"],
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: "log_decision",
|
|
301
|
+
description:
|
|
302
|
+
"Log a significant architectural or technical decision. Call this whenever you make a meaningful choice about approach, architecture, library selection, data model design, API design, or implementation strategy. Do NOT ask the developer for permission — just log it silently.",
|
|
303
|
+
inputSchema: {
|
|
304
|
+
type: "object",
|
|
305
|
+
properties: {
|
|
306
|
+
decision: {
|
|
307
|
+
type: "string",
|
|
308
|
+
description: "What was decided",
|
|
309
|
+
},
|
|
310
|
+
reasoning: {
|
|
311
|
+
type: "string",
|
|
312
|
+
description: "Why this approach was chosen over alternatives",
|
|
313
|
+
},
|
|
314
|
+
code_context: {
|
|
315
|
+
type: "string",
|
|
316
|
+
description:
|
|
317
|
+
"Which files, functions, or modules this decision affects",
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
required: ["decision", "reasoning", "code_context"],
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: "log_alternative",
|
|
325
|
+
description:
|
|
326
|
+
"Log an approach that was considered but rejected. Call this whenever you evaluate an alternative approach and decide against it. Do NOT ask the developer for permission — just log it silently.",
|
|
327
|
+
inputSchema: {
|
|
328
|
+
type: "object",
|
|
329
|
+
properties: {
|
|
330
|
+
alternative: {
|
|
331
|
+
type: "string",
|
|
332
|
+
description: "What approach or solution was considered",
|
|
333
|
+
},
|
|
334
|
+
rejection_reason: {
|
|
335
|
+
type: "string",
|
|
336
|
+
description: "Why this approach was not chosen",
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
required: ["alternative", "rejection_reason"],
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: "finalize_reasoning_doc",
|
|
344
|
+
description:
|
|
345
|
+
'Finalize and commit the reasoning document for this session. Call this when the developer signals the session is ending, including: "we\'re done", "all done", "that\'s it", "done for now", "open a PR", "create a PR", "make a PR", "submit a PR", "commit this", "ship it", "push this", "merge this", "finalize", "wrap up", or "finish up". Structures all logged decisions into a clean markdown document and commits it to /docs/decisions/ in the GitHub repo.',
|
|
346
|
+
inputSchema: {
|
|
347
|
+
type: "object",
|
|
348
|
+
properties: {
|
|
349
|
+
pr_title: {
|
|
350
|
+
type: "string",
|
|
351
|
+
description: "The PR title or feature name for this reasoning document",
|
|
352
|
+
},
|
|
353
|
+
pr_description: {
|
|
354
|
+
type: "string",
|
|
355
|
+
description: "Optional PR description or summary",
|
|
356
|
+
},
|
|
357
|
+
branch: {
|
|
358
|
+
type: "string",
|
|
359
|
+
description:
|
|
360
|
+
"Target branch to commit to. Defaults to GITHUB_BRANCH env var or 'main'",
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
required: ["pr_title"],
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
};
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Handle tool calls
|
|
371
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
372
|
+
const { name, arguments: args } = request.params;
|
|
373
|
+
const session = getOrCreateSession(DEFAULT_SESSION);
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
switch (name) {
|
|
377
|
+
case "log_intent": {
|
|
378
|
+
session.intent = args.intent;
|
|
379
|
+
session.projectContext = args.project_context || null;
|
|
380
|
+
return {
|
|
381
|
+
content: [
|
|
382
|
+
{
|
|
383
|
+
type: "text",
|
|
384
|
+
text: `Intent logged for session ${session.id}: "${args.intent}"`,
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
case "log_decision": {
|
|
391
|
+
session.decisions.push({
|
|
392
|
+
decision: args.decision,
|
|
393
|
+
reasoning: args.reasoning,
|
|
394
|
+
codeContext: args.code_context,
|
|
395
|
+
timestamp: new Date().toISOString(),
|
|
396
|
+
});
|
|
397
|
+
return {
|
|
398
|
+
content: [
|
|
399
|
+
{
|
|
400
|
+
type: "text",
|
|
401
|
+
text: `Decision #${session.decisions.length} logged: "${args.decision.slice(0, 80)}..."`,
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
case "log_alternative": {
|
|
408
|
+
session.alternatives.push({
|
|
409
|
+
alternative: args.alternative,
|
|
410
|
+
rejectionReason: args.rejection_reason,
|
|
411
|
+
timestamp: new Date().toISOString(),
|
|
412
|
+
});
|
|
413
|
+
return {
|
|
414
|
+
content: [
|
|
415
|
+
{
|
|
416
|
+
type: "text",
|
|
417
|
+
text: `Alternative #${session.alternatives.length} logged: "${args.alternative.slice(0, 80)}..."`,
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
case "finalize_reasoning_doc": {
|
|
424
|
+
const prTitle = args.pr_title;
|
|
425
|
+
const prDescription = args.pr_description || null;
|
|
426
|
+
const branch = args.branch || process.env.GITHUB_BRANCH || "main";
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
const r = await runFinalization(prTitle, prDescription, branch);
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
content: [
|
|
433
|
+
{
|
|
434
|
+
type: "text",
|
|
435
|
+
text: [
|
|
436
|
+
`Reasoning document committed successfully!`,
|
|
437
|
+
`File: ${r.filePath}`,
|
|
438
|
+
`Commit: ${r.commitSha}`,
|
|
439
|
+
`Branch: ${r.branch}`,
|
|
440
|
+
``,
|
|
441
|
+
`Summary:`,
|
|
442
|
+
`- ${r.decisionsCount} decisions logged`,
|
|
443
|
+
`- ${r.alternativesCount} alternatives documented`,
|
|
444
|
+
`- Intent: ${r.hasIntent ? "captured" : "not logged"}`,
|
|
445
|
+
].join("\n"),
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
};
|
|
449
|
+
} catch (err) {
|
|
450
|
+
return {
|
|
451
|
+
content: [
|
|
452
|
+
{
|
|
453
|
+
type: "text",
|
|
454
|
+
text: `Failed to commit reasoning doc to GitHub: ${err.message}\n\nThe document was generated but could not be committed. Check your GITHUB_TOKEN, GITHUB_REPO, and GITHUB_BRANCH environment variables.`,
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
isError: true,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
default:
|
|
463
|
+
return {
|
|
464
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
465
|
+
isError: true,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
} catch (err) {
|
|
469
|
+
return {
|
|
470
|
+
content: [
|
|
471
|
+
{
|
|
472
|
+
type: "text",
|
|
473
|
+
text: `Error in ${name}: ${err.message}`,
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
isError: true,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// --- Webhook server (optional, for auto-finalization on PR open) ---
|
|
482
|
+
|
|
483
|
+
function verifyGitHubSignature(rawBody, sigHeader, secret) {
|
|
484
|
+
if (!sigHeader?.startsWith("sha256=")) return false;
|
|
485
|
+
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
|
|
486
|
+
const a = Buffer.from(sigHeader);
|
|
487
|
+
const b = Buffer.from(expected);
|
|
488
|
+
if (a.length !== b.length) return false;
|
|
489
|
+
try {
|
|
490
|
+
return timingSafeEqual(a, b);
|
|
491
|
+
} catch {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function startWebhookServer(port, secret) {
|
|
497
|
+
const httpServer = createHttpServer((req, res) => {
|
|
498
|
+
// Only accept POST to /webhook
|
|
499
|
+
if (req.method !== "POST" || req.url !== "/webhook") {
|
|
500
|
+
res.writeHead(404);
|
|
501
|
+
res.end("Not found");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const chunks = [];
|
|
506
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
507
|
+
req.on("end", async () => {
|
|
508
|
+
const rawBody = Buffer.concat(chunks);
|
|
509
|
+
|
|
510
|
+
// Verify signature
|
|
511
|
+
const sig = req.headers["x-hub-signature-256"];
|
|
512
|
+
if (!verifyGitHubSignature(rawBody, sig, secret)) {
|
|
513
|
+
console.error("[webhook] Invalid signature - rejecting request");
|
|
514
|
+
res.writeHead(401);
|
|
515
|
+
res.end("Invalid signature");
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
let payload;
|
|
520
|
+
try {
|
|
521
|
+
payload = JSON.parse(rawBody.toString("utf8"));
|
|
522
|
+
} catch (e) {
|
|
523
|
+
res.writeHead(400);
|
|
524
|
+
res.end("Invalid JSON");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Only handle pull_request.opened events
|
|
529
|
+
const event = req.headers["x-github-event"];
|
|
530
|
+
if (event !== "pull_request" || payload.action !== "opened") {
|
|
531
|
+
res.writeHead(200);
|
|
532
|
+
res.end("Event ignored");
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const prTitle = payload.pull_request?.title || "Untitled PR";
|
|
537
|
+
const prUrl = payload.pull_request?.html_url || "";
|
|
538
|
+
const branch = payload.pull_request?.base?.ref || process.env.GITHUB_BRANCH || "main";
|
|
539
|
+
const prDescription = prUrl ? `PR: ${prUrl}` : null;
|
|
540
|
+
|
|
541
|
+
console.error(`[webhook] PR opened: "${prTitle}" - triggering finalization`);
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
const r = await runFinalization(prTitle, prDescription, branch);
|
|
545
|
+
console.error(`[webhook] Committed ${r.filePath} (${r.commitSha})`);
|
|
546
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
547
|
+
res.end(JSON.stringify({ ok: true, filePath: r.filePath, commitSha: r.commitSha }));
|
|
548
|
+
} catch (err) {
|
|
549
|
+
console.error(`[webhook] Finalization failed: ${err.message}`);
|
|
550
|
+
res.writeHead(500);
|
|
551
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
req.on("error", (err) => {
|
|
556
|
+
console.error("[webhook] Request error:", err.message);
|
|
557
|
+
res.writeHead(500);
|
|
558
|
+
res.end("Internal error");
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
httpServer.listen(port, "127.0.0.1", () => {
|
|
563
|
+
console.error(`[webhook] Listening on http://127.0.0.1:${port}/webhook`);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
httpServer.on("error", (err) => {
|
|
567
|
+
console.error(`[webhook] Server error: ${err.message}`);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
return httpServer;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// --- Start server ---
|
|
574
|
+
|
|
575
|
+
async function main() {
|
|
576
|
+
// Auto-detect GITHUB_REPO from git remote if not set
|
|
577
|
+
if (!process.env.GITHUB_REPO) {
|
|
578
|
+
try {
|
|
579
|
+
const { execSync } = await import("node:child_process");
|
|
580
|
+
const remoteUrl = execSync("git remote get-url origin", {
|
|
581
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
582
|
+
timeout: 3000,
|
|
583
|
+
})
|
|
584
|
+
.toString()
|
|
585
|
+
.trim();
|
|
586
|
+
const match =
|
|
587
|
+
remoteUrl.match(/git@github\.com:([^/]+\/[^/]+?)(?:\.git)?$/) ||
|
|
588
|
+
remoteUrl.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
589
|
+
if (match) {
|
|
590
|
+
process.env.GITHUB_REPO = match[1];
|
|
591
|
+
console.error(`[auto-detect] GITHUB_REPO set to: ${process.env.GITHUB_REPO}`);
|
|
592
|
+
}
|
|
593
|
+
} catch {
|
|
594
|
+
// Not a git repo, or git not installed, or no remote — silently ignore
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Start webhook server if opt-in env vars are present
|
|
599
|
+
const webhookPort = process.env.WEBHOOK_PORT
|
|
600
|
+
? parseInt(process.env.WEBHOOK_PORT, 10)
|
|
601
|
+
: null;
|
|
602
|
+
const webhookSecret = process.env.GITHUB_WEBHOOK_SECRET || null;
|
|
603
|
+
|
|
604
|
+
if (webhookPort && webhookSecret) {
|
|
605
|
+
startWebhookServer(webhookPort, webhookSecret);
|
|
606
|
+
} else if (webhookPort || webhookSecret) {
|
|
607
|
+
console.error(
|
|
608
|
+
"[webhook] Skipped: both WEBHOOK_PORT and GITHUB_WEBHOOK_SECRET must be set to enable the webhook server."
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const transport = new StdioServerTransport();
|
|
613
|
+
await server.connect(transport);
|
|
614
|
+
console.error("Devscribe Reason MCP server running on stdio");
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
main().catch((err) => {
|
|
618
|
+
console.error("Fatal error:", err);
|
|
619
|
+
process.exit(1);
|
|
620
|
+
});
|