Skip to content

Commit 51fba9e

Browse files
authored
Merge pull request gigsmart#122 from TheBushidoCollective/feat/github-releases
Add GitHub releases, tags, and artifacts
2 parents 953ec26 + ed1fc76 commit 51fba9e

File tree

9 files changed

+142
-35
lines changed

9 files changed

+142
-35
lines changed

.ai-dlc/settings.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
git:
2+
change_strategy: intent
3+
default_branch: main
4+
auto_merge: true
5+
elaboration_review: true
6+
7+
visual_review: false
8+
9+
providers:
10+
ticketing:
11+
type: github-issues
12+
13+
review_agents:
14+
accessibility: true

.github/workflows/bump-plugin-version.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,12 @@ jobs:
106106
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
107107
108108
- name: Commit version bump
109+
id: commit
109110
run: |
110111
# Check if there are any changes to commit
111112
if git diff --quiet; then
112113
echo "No version changes to commit"
114+
echo "released=false" >> $GITHUB_OUTPUT
113115
exit 0
114116
fi
115117
@@ -126,8 +128,55 @@ jobs:
126128
echo "Rebase conflict detected - another workflow likely pushed first"
127129
echo "Aborting rebase and exiting cleanly (next push will retry)"
128130
git rebase --abort 2>/dev/null || true
131+
echo "released=false" >> $GITHUB_OUTPUT
129132
exit 0
130133
fi
131134
132135
# Push the rebased commit
133136
git push origin main
137+
echo "released=true" >> $GITHUB_OUTPUT
138+
139+
- name: Create and push git tag
140+
if: steps.commit.outputs.released == 'true'
141+
run: |
142+
TAG="v${{ steps.bump-version.outputs.new_version }}"
143+
git tag -f "$TAG"
144+
git push -f origin "$TAG"
145+
146+
- name: Package plugin
147+
if: steps.commit.outputs.released == 'true'
148+
run: |
149+
VERSION="${{ steps.bump-version.outputs.new_version }}"
150+
zip -r "ai-dlc-plugin-${VERSION}.zip" plugin/ \
151+
-x "plugin/node_modules/*" \
152+
-x "plugin/*/node_modules/*" \
153+
-x "plugin/*/.tsbuildinfo"
154+
155+
- name: Extract release notes
156+
if: steps.commit.outputs.released == 'true'
157+
run: |
158+
VERSION="${{ steps.bump-version.outputs.new_version }}"
159+
# Extract changelog section for this version
160+
awk -v ver="$VERSION" '
161+
/^## \[/ {
162+
if (found) exit
163+
if (index($0, "[" ver "]") > 0) { found=1; next }
164+
}
165+
found { print }
166+
' CHANGELOG.md > /tmp/release-notes.md
167+
# Convert relative commit links to absolute URLs for release context
168+
sed -i "s|../../commit/|https://github.com/${{ github.repository }}/commit/|g" /tmp/release-notes.md
169+
170+
- name: Create GitHub Release
171+
if: steps.commit.outputs.released == 'true'
172+
env:
173+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
174+
run: |
175+
VERSION="${{ steps.bump-version.outputs.new_version }}"
176+
TAG="v${VERSION}"
177+
# Delete existing release on retry to ensure idempotency
178+
gh release delete "$TAG" -y 2>/dev/null || true
179+
gh release create "$TAG" \
180+
--title "$TAG" \
181+
--notes-file /tmp/release-notes.md \
182+
"ai-dlc-plugin-${VERSION}.zip#AI-DLC Plugin (zip)"

bun.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugin/.mcp.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"command": "bash",
55
"args": [
66
"-c",
7-
"command -v bun >/dev/null 2>&1 || { echo 'Error: Bun is required but not installed. Install it with: curl -fsSL https://bun.sh/install | bash' >&2; exit 1; }; exec bun run --cwd \"${CLAUDE_PLUGIN_ROOT}/mcp-server\" src/server.ts"
7+
"cd \"${CLAUDE_PLUGIN_ROOT}/mcp-server\" && exec npx tsx src/server.ts"
88
]
99
}
1010
}

plugin/mcp-server/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"zod": "^3.23.0"
1313
},
1414
"devDependencies": {
15-
"@types/bun": "^1.2.0",
15+
"@types/node": "^22.0.0",
16+
"tsx": "^4.19.0",
1617
"typescript": "^5.7.0"
1718
}
1819
}

plugin/mcp-server/src/http.ts

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { realpath } from "node:fs/promises"
1+
import { createServer, type Server as HttpServer } from "node:http"
2+
import { readFile, realpath } from "node:fs/promises"
23
import { extname, join, resolve } from "node:path"
34
import type { Server } from "@modelcontextprotocol/sdk/server/index.js"
45
import { z } from "zod"
56
import { getSession, updateQuestionSession, updateSession } from "./sessions.js"
67
import type { QuestionAnswer } from "./sessions.js"
78

8-
let httpServer: ReturnType<typeof Bun.serve> | null = null
9+
let httpServer: HttpServer | null = null
910
let actualPort: number | null = null
1011

1112
/** Dependency-injected MCP server reference */
@@ -115,19 +116,16 @@ async function handleMockupGet(
115116
}
116117

117118
try {
118-
const file = Bun.file(resolved)
119-
if (!(await file.exists())) {
120-
return new Response("Not found", { status: 404 })
121-
}
122119
// Symlink-safe check: ensure resolved real path stays within base dir
123120
const realResolved = await realpath(resolved).catch(() => null)
124121
const realBase = await realpath(mockupsDir).catch(() => resolve(mockupsDir))
125122
if (!realResolved || !realResolved.startsWith(realBase)) {
126123
return new Response("Forbidden", { status: 403 })
127124
}
125+
const data = await readFile(resolved)
128126
const ext = extname(resolved).toLowerCase()
129127
const contentType = MIME_TYPES[ext] ?? "application/octet-stream"
130-
return new Response(file, {
128+
return new Response(data, {
131129
headers: { "Content-Type": contentType },
132130
})
133131
} catch {
@@ -152,10 +150,6 @@ async function handleWireframeGet(
152150
}
153151

154152
try {
155-
const file = Bun.file(resolved)
156-
if (!(await file.exists())) {
157-
return new Response("Not found", { status: 404 })
158-
}
159153
// Symlink-safe check: ensure resolved real path stays within base dir
160154
const realResolved = await realpath(resolved).catch(() => null)
161155
const realBase = await realpath(session.intent_dir).catch(() =>
@@ -164,9 +158,10 @@ async function handleWireframeGet(
164158
if (!realResolved || !realResolved.startsWith(realBase)) {
165159
return new Response("Forbidden", { status: 403 })
166160
}
161+
const data = await readFile(resolved)
167162
const ext = extname(resolved).toLowerCase()
168163
const contentType = MIME_TYPES[ext] ?? "application/octet-stream"
169-
return new Response(file, {
164+
return new Response(data, {
170165
headers: { "Content-Type": contentType },
171166
})
172167
} catch {
@@ -281,22 +276,18 @@ function handleRequest(req: Request): Response | Promise<Response> {
281276
return new Response("Not Found", { status: 404 })
282277
}
283278

284-
export function startHttpServer(): number {
279+
export async function startHttpServer(): Promise<number> {
285280
if (httpServer && actualPort !== null) {
286281
return actualPort
287282
}
288283

289284
const basePort = Number.parseInt(process.env.AI_DLC_REVIEW_PORT ?? "8789", 10)
290-
let port = basePort
291285
const maxAttempts = 10
292286

293287
for (let i = 0; i < maxAttempts; i++) {
288+
const port = basePort + i
294289
try {
295-
httpServer = Bun.serve({
296-
port,
297-
hostname: "127.0.0.1",
298-
fetch: handleRequest,
299-
})
290+
await listenOnPort(port)
300291
actualPort = port
301292
console.error(`Review HTTP server listening on http://127.0.0.1:${port}`)
302293
return port
@@ -306,7 +297,6 @@ export function startHttpServer(): number {
306297
"code" in err &&
307298
(err as NodeJS.ErrnoException).code === "EADDRINUSE"
308299
) {
309-
port++
310300
continue
311301
}
312302
throw err
@@ -317,3 +307,61 @@ export function startHttpServer(): number {
317307
`Could not find available port (tried ${basePort}-${basePort + maxAttempts - 1})`,
318308
)
319309
}
310+
311+
function listenOnPort(port: number): Promise<void> {
312+
return new Promise((resolve, reject) => {
313+
const server = createServer(async (req, res) => {
314+
// Build a Web API Request from the incoming Node request
315+
const url = `http://127.0.0.1:${port}${req.url ?? "/"}`
316+
const headers = new Headers()
317+
for (const [key, value] of Object.entries(req.headers)) {
318+
if (value) {
319+
if (Array.isArray(value)) {
320+
for (const v of value) headers.append(key, v)
321+
} else {
322+
headers.set(key, value)
323+
}
324+
}
325+
}
326+
327+
let body: ArrayBuffer | null = null
328+
if (req.method !== "GET" && req.method !== "HEAD") {
329+
const chunks: Buffer[] = []
330+
for await (const chunk of req) {
331+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk)
332+
}
333+
const buf = Buffer.concat(chunks)
334+
body = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)
335+
}
336+
337+
const webRequest = new Request(url, {
338+
method: req.method ?? "GET",
339+
headers,
340+
body,
341+
})
342+
343+
try {
344+
const webResponse = await handleRequest(webRequest)
345+
res.writeHead(
346+
webResponse.status,
347+
Object.fromEntries(webResponse.headers.entries()),
348+
)
349+
const responseBody = await webResponse.arrayBuffer()
350+
res.end(Buffer.from(responseBody))
351+
} catch (err) {
352+
console.error("HTTP handler error:", err)
353+
res.writeHead(500)
354+
res.end("Internal Server Error")
355+
}
356+
})
357+
358+
server.once("error", (err) => {
359+
reject(err)
360+
})
361+
362+
server.listen(port, "127.0.0.1", () => {
363+
httpServer = server
364+
resolve()
365+
})
366+
})
367+
}

plugin/mcp-server/src/server.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { spawn } from "node:child_process"
12
import { readdir } from "node:fs/promises"
23
import { join, resolve } from "node:path"
34
import {
@@ -314,7 +315,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
314315
})
315316

316317
// Start HTTP server (idempotent)
317-
const port = startHttpServer()
318+
const port = await startHttpServer()
318319
const reviewUrl = `http://127.0.0.1:${port}/review/${session.session_id}`
319320

320321
// Open browser
@@ -323,7 +324,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
323324
process.platform === "darwin"
324325
? ["open", reviewUrl]
325326
: ["xdg-open", reviewUrl]
326-
Bun.spawn(cmd, { stdio: ["ignore", "ignore", "ignore"] })
327+
spawn(cmd[0], cmd.slice(1), { stdio: "ignore", detached: true }).unref()
327328
} catch (err) {
328329
console.error("Failed to open browser:", err)
329330
}
@@ -418,7 +419,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
418419
})
419420

420421
// Start HTTP server (idempotent)
421-
const port = startHttpServer()
422+
const port = await startHttpServer()
422423
const questionUrl = `http://127.0.0.1:${port}/question/${session.session_id}`
423424

424425
// Open browser
@@ -427,7 +428,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
427428
process.platform === "darwin"
428429
? ["open", questionUrl]
429430
: ["xdg-open", questionUrl]
430-
Bun.spawn(cmd, { stdio: ["ignore", "ignore", "ignore"] })
431+
spawn(cmd[0], cmd.slice(1), { stdio: "ignore", detached: true }).unref()
431432
} catch (err) {
432433
console.error("Failed to open browser:", err)
433434
}

plugin/schemas/settings.schema.json

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,6 @@
2121
"$ref": "#/definitions/providersConfig",
2222
"description": "External system providers for spec, ticketing, design, and comms"
2323
},
24-
"workflow_mode": {
25-
"type": "string",
26-
"enum": ["interactive", "autonomous"],
27-
"default": "interactive",
28-
"description": "interactive: pause for user approval at key decision points. autonomous: proceed automatically, only pause on blockers."
29-
},
3024
"granularity": {
3125
"type": "string",
3226
"enum": ["coarse", "standard", "fine"],

plugin/skills/autopilot/SKILL.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ Autopilot is designed for straightforward features. It pauses and returns contro
3939
1. **Pause on blockers or ambiguity** - Never guess. If elaboration or execution encounters something unclear, stop and ask.
4040
2. **Pause if elaboration generates more than 5 units** - Confirm scope with the user before proceeding to execution. More than 5 units suggests the feature may be too large or complex for autopilot.
4141
3. **Pause before creating PR** - Always confirm with the user before delivery. Show a summary of what was built across all units.
42-
4. **Config-aware mode** - Uses `workflow_mode: autonomous` from project config (`.ai-dlc/settings.yml`) if available. Falls back to interactive mode for any decision that could go wrong.
43-
5. **No silent failures** - If any phase fails, stop immediately and report what happened. Do not attempt to recover autonomously from phase-level failures.
42+
4. **No silent failures** - If any phase fails, stop immediately and report what happened. Do not attempt to recover autonomously from phase-level failures.
4443

4544
---
4645

0 commit comments

Comments
 (0)