Skip to content

Commit b688090

Browse files
authored
feat(keystone): add keystone-workflows package and devcontainer integration (#5)
Add BMAD keystone workflow orchestration to the devcontainer image: - Create @zookanalytics/keystone-workflows package with: - bmad-story.yaml: Single story workflow (dev → review → verify → commit) - bmad-epic.yaml: Epic iterator using sprint-status.yaml - bmad-epic-status.yaml: Epic iterator using BMAD workflow-status - Default keystone config with providers and engine allowlist - Postinstall script for symlinks to ~/.keystone/workflows/ - Update Dockerfile to: - Install bun runtime (required for keystone-cli) - Install keystone-cli from ZookAnalytics fork - Copy and run keystone-workflows postinstall - Add container scripts: - update-keystone.sh: Auto-update packages with graceful fallback - devcontainer-sanity-check.sh: Extensible health check - Update post-create.sh with new steps (6/11 and 10/11) - Include tech specs for implementation reference
1 parent 2567ae5 commit b688090

12 files changed

Lines changed: 1732 additions & 13 deletions

docs/implementation/tech-spec-keystone-bmad-workflows.md

Lines changed: 454 additions & 0 deletions
Large diffs are not rendered by default.

docs/implementation/tech-spec-keystone-devcontainer-integration.md

Lines changed: 562 additions & 0 deletions
Large diffs are not rendered by default.

image/Dockerfile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,19 @@ RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/
112112
# Setup pnpm
113113
RUN pnpm setup
114114

115+
# Install bun (required for keystone-cli)
116+
# Must run as node user to install to ~/.bun
117+
RUN curl -fsSL https://bun.sh/install | bash
118+
ENV BUN_INSTALL="/home/node/.bun"
119+
ENV PATH="$BUN_INSTALL/bin:$PATH"
120+
121+
# Install keystone-cli globally via bun (must run as node user for PATH)
122+
RUN bun install -g github:ZookAnalytics/keystone-cli
123+
124+
# Copy keystone-workflows package and run postinstall to set up symlinks and config
125+
COPY --chown=node:node packages/keystone-workflows /home/node/.local/lib/keystone-workflows
126+
RUN /home/node/.local/lib/keystone-workflows/scripts/postinstall.sh
127+
115128
USER root
116129

117130
# Copy configuration files
@@ -136,6 +149,8 @@ COPY image/scripts/setup-instance-isolation.sh /usr/local/bin/
136149
COPY image/scripts/setup-claude-auth-sharing.sh /usr/local/bin/
137150
COPY image/scripts/search-history.sh /usr/local/bin/
138151
COPY image/scripts/fix-shared-data-permissions.sh /usr/local/bin/
152+
COPY image/scripts/update-keystone.sh /usr/local/bin/
153+
COPY image/scripts/devcontainer-sanity-check.sh /usr/local/bin/
139154

140155
# Make scripts executable and configure sudo
141156
RUN chmod 755 /usr/local/bin/init-firewall.sh && \
@@ -154,6 +169,8 @@ RUN chmod 755 /usr/local/bin/init-firewall.sh && \
154169
chmod 755 /usr/local/bin/setup-claude-auth-sharing.sh && \
155170
chmod 755 /usr/local/bin/search-history.sh && \
156171
chmod 755 /usr/local/bin/fix-shared-data-permissions.sh && \
172+
chmod 755 /usr/local/bin/update-keystone.sh && \
173+
chmod 755 /usr/local/bin/devcontainer-sanity-check.sh && \
157174
echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-commands && \
158175
echo "node ALL=(root) NOPASSWD: /usr/local/bin/assemble-managed-settings.sh" >> /etc/sudoers.d/node-commands && \
159176
echo "node ALL=(root) NOPASSWD: /usr/local/bin/fix-node-modules-ownership.sh" >> /etc/sudoers.d/node-commands && \
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/bin/bash
2+
# DevContainer sanity check - extensible script verifying container expectations
3+
# Add checks as container capabilities grow
4+
set -u # Fail on undefined variables
5+
6+
PASS=0
7+
FAIL=0
8+
9+
check() {
10+
if eval "$2" > /dev/null 2>&1; then
11+
echo "$1"
12+
PASS=$((PASS + 1))
13+
else
14+
echo "$1"
15+
FAIL=$((FAIL + 1))
16+
fi
17+
}
18+
19+
echo "Running DevContainer sanity checks..."
20+
echo "---"
21+
22+
# Core runtimes
23+
check "bun runtime" "bun --version"
24+
check "node runtime" "node --version"
25+
check "pnpm runtime" "pnpm --version"
26+
27+
# Keystone
28+
check "keystone binary" "keystone --version"
29+
check "keystone workflows exist" "ls ~/.keystone/workflows/bmad-*.yaml"
30+
check "keystone workflow discovery" "keystone run --list 2>/dev/null | grep -q bmad"
31+
check "keystone config exists" "test -f ~/.config/keystone/config.yaml"
32+
33+
# AI CLIs
34+
check "claude cli" "command -v claude"
35+
check "gemini cli" "command -v gemini"
36+
37+
# DevContainer tools
38+
check "git available" "git --version"
39+
check "gh cli available" "gh --version"
40+
41+
echo "---"
42+
echo "Passed: $PASS, Failed: $FAIL"
43+
[ $FAIL -eq 0 ]

image/scripts/post-create.sh

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ echo "==============================================="
1717

1818
# Step 1: Assemble Claude Code managed settings
1919
echo ""
20-
echo "[1/9] Assembling Claude Code managed settings..."
20+
echo "[1/11] Assembling Claude Code managed settings..."
2121
sudo /usr/local/bin/assemble-managed-settings.sh
2222
echo "✓ Managed settings assembled"
2323

2424
# Step 2: DevPod instance isolation (if applicable)
2525
echo ""
26-
echo "[2/9] Checking for DevPod instance isolation..."
26+
echo "[2/11] Checking for DevPod instance isolation..."
2727

2828
# Fix shared-data volume permissions if needed (Docker creates volumes as root)
2929
if [ -n "${SHARED_DATA_DIR:-}" ] && [ -d "$SHARED_DATA_DIR" ] && [ ! -w "$SHARED_DATA_DIR" ]; then
@@ -59,19 +59,19 @@ fi
5959

6060
# Step 3: Check for package updates (daily)
6161
echo ""
62-
echo "[3/9] Checking for package updates..."
62+
echo "[3/11] Checking for package updates..."
6363
/usr/local/bin/check-daily-updates.sh
6464
echo "✓ Package update check complete"
6565

6666
# Step 4: Fix node_modules ownership
6767
echo ""
68-
echo "[4/9] Fixing node_modules ownership..."
68+
echo "[4/11] Fixing node_modules ownership..."
6969
sudo /usr/local/bin/fix-node-modules-ownership.sh
7070
echo "✓ Node modules ownership fixed"
7171

7272
# Step 5: Install CLI tools
7373
echo ""
74-
echo "[5/9] Installing CLI tools..."
74+
echo "[5/11] Installing CLI tools..."
7575

7676
# Install Claude Code via official installer (https://claude.ai/install.sh)
7777
# Security note: Piping to bash is the official install method. The script is served
@@ -106,26 +106,41 @@ pnpm install -g "@google/gemini-cli@${GEMINI_CLI_VERSION}"
106106

107107
echo "✓ CLI tools installed"
108108

109-
# Step 6: Start dnsmasq for DNS logging
109+
# Step 6: Update keystone packages
110110
echo ""
111-
echo "[6/9] Starting dnsmasq DNS forwarder..."
111+
echo "[6/11] Updating keystone packages..."
112+
/usr/local/bin/update-keystone.sh
113+
echo "✓ Keystone packages updated"
114+
115+
# Step 7: Start dnsmasq for DNS logging
116+
echo ""
117+
echo "[7/11] Starting dnsmasq DNS forwarder..."
112118
sudo /usr/local/bin/start-dnsmasq.sh
113119

114-
# Step 7: Start ulogd for firewall logging
120+
# Step 8: Start ulogd for firewall logging
115121
echo ""
116-
echo "[7/9] Starting ulogd firewall logger..."
122+
echo "[8/11] Starting ulogd firewall logger..."
117123
sudo /usr/local/bin/start-ulogd.sh
118124
echo "✓ ulogd started"
119125

120-
# Step 8: Initialize firewall
126+
# Step 9: Initialize firewall
121127
echo ""
122-
echo "[8/9] Initializing firewall rules..."
128+
echo "[9/11] Initializing firewall rules..."
123129
sudo /usr/local/bin/init-firewall.sh
124130
echo "✓ Firewall initialized"
125131

126-
# Step 9: Run project-specific post-create if it exists
132+
# Step 10: Run sanity check
133+
echo ""
134+
echo "[10/11] Running sanity check..."
135+
if /usr/local/bin/devcontainer-sanity-check.sh; then
136+
echo "✓ Sanity check passed"
137+
else
138+
echo "⚠ Sanity check reported failures (see above) - container continues"
139+
fi
140+
141+
# Step 11: Run project-specific post-create if it exists
127142
echo ""
128-
echo "[9/9] Running project-specific setup..."
143+
echo "[11/11] Running project-specific setup..."
129144
PROJECT_POST_CREATE="$WORKSPACE_ROOT/.devcontainer/post-create-project.sh"
130145
if [ -f "$PROJECT_POST_CREATE" ]; then
131146
echo "Running $PROJECT_POST_CREATE..."

image/scripts/update-keystone.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
# Track update failures (non-fatal - container continues with existing versions)
5+
UPDATE_FAILURES=0
6+
7+
echo "Updating keystone packages..."
8+
# Note: bun global installs may change symlink targets from ~/.local/lib to ~/.bun/install/global
9+
# This is acceptable - postinstall.sh uses ln -sf to cleanly overwrite existing symlinks
10+
11+
if ! bun install -g github:ZookAnalytics/keystone-cli; then
12+
echo "⚠ keystone-cli update failed, using existing version"
13+
UPDATE_FAILURES=$((UPDATE_FAILURES + 1))
14+
fi
15+
16+
if ! bun install -g github:ZookAnalytics/claude-devcontainer#packages/keystone-workflows; then
17+
echo "⚠ keystone-workflows update failed, using existing version"
18+
UPDATE_FAILURES=$((UPDATE_FAILURES + 1))
19+
fi
20+
21+
# Log versions for debugging
22+
echo "---"
23+
echo "keystone-cli: $(keystone --version 2>/dev/null || echo 'not installed')"
24+
# Workflows version from package.json (if installed locally) or symlink target
25+
WORKFLOWS_PKG="$HOME/.local/lib/keystone-workflows/package.json"
26+
if [ -f "$WORKFLOWS_PKG" ]; then
27+
echo "keystone-workflows: $(grep '"version"' "$WORKFLOWS_PKG" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')"
28+
else
29+
echo "keystone-workflows: (installed via bun global)"
30+
fi
31+
echo "workflows path: $(ls -la ~/.keystone/workflows/*.yaml 2>/dev/null | head -1 || echo 'not found')"
32+
33+
# Report update status (exit 0 for graceful degradation per AC5)
34+
if [ "$UPDATE_FAILURES" -gt 0 ]; then
35+
echo "$UPDATE_FAILURES update(s) failed - using existing versions"
36+
fi
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Keystone Configuration
2+
# Default configuration for BMAD workflows in claude-devcontainer
3+
default_provider: openai
4+
5+
providers:
6+
# Install with: npm install @ai-sdk/openai @ai-sdk/anthropic
7+
openai:
8+
package: "@ai-sdk/openai"
9+
api_key_env: OPENAI_API_KEY
10+
default_model: gpt-4o
11+
anthropic:
12+
package: "@ai-sdk/anthropic"
13+
api_key_env: ANTHROPIC_API_KEY
14+
default_model: claude-sonnet-4-20250514
15+
16+
# Example: OpenAI-compatible provider (Groq)
17+
# groq:
18+
# package: "@ai-sdk/openai"
19+
# base_url: https://api.groq.com/openai/v1
20+
# api_key_env: GROQ_API_KEY
21+
# default_model: llama-3.3-70b-versatile
22+
23+
model_mappings:
24+
"gpt-*": openai
25+
"claude-*": anthropic
26+
"o1-*": openai
27+
28+
# mcp_servers:
29+
# filesystem:
30+
# command: npx
31+
# args: ["-y", "@modelcontextprotocol/server-filesystem", "."]
32+
33+
engines:
34+
allowlist:
35+
claude:
36+
command: claude
37+
version: "*"
38+
versionArgs: ["--version"]
39+
gemini:
40+
command: gemini
41+
version: "*"
42+
versionArgs: ["--version"]
43+
44+
defaults:
45+
timeout: 900000 # 15 minutes for long CLI operations
46+
47+
storage:
48+
retention_days: 30
49+
50+
expression:
51+
strict: false
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "@zookanalytics/keystone-workflows",
3+
"version": "0.1.0",
4+
"description": "BMAD keystone workflows for claude-devcontainer",
5+
"files": [
6+
"workflows/",
7+
"config/",
8+
"scripts/"
9+
],
10+
"scripts": {
11+
"postinstall": "./scripts/postinstall.sh"
12+
},
13+
"keywords": [
14+
"keystone",
15+
"bmad",
16+
"workflows",
17+
"claude-devcontainer"
18+
],
19+
"license": "MIT",
20+
"repository": {
21+
"type": "git",
22+
"url": "https://github.com/zookanalytics/claude-devcontainer.git",
23+
"directory": "packages/keystone-workflows"
24+
}
25+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
4+
PACKAGE_DIR="$SCRIPT_DIR/.."
5+
WORKFLOWS_DIR="$PACKAGE_DIR/workflows"
6+
CONFIG_DIR="$PACKAGE_DIR/config"
7+
8+
# Validate package structure
9+
if [ ! -d "$WORKFLOWS_DIR" ]; then
10+
echo "ERROR: Workflows directory not found at $WORKFLOWS_DIR" >&2
11+
exit 1
12+
fi
13+
if [ ! -d "$CONFIG_DIR" ]; then
14+
echo "ERROR: Config directory not found at $CONFIG_DIR" >&2
15+
exit 1
16+
fi
17+
18+
# 1. Install workflows to ~/.keystone/workflows/
19+
# Uses ln -sf to force-overwrite existing symlinks (handles updates cleanly)
20+
WORKFLOW_TARGET="$HOME/.keystone/workflows"
21+
mkdir -p "$WORKFLOW_TARGET"
22+
WORKFLOW_COUNT=0
23+
for f in "$WORKFLOWS_DIR"/*.yaml; do
24+
[ -e "$f" ] || continue # Handle no matches
25+
ln -sf "$f" "$WORKFLOW_TARGET/$(basename "$f")" # -f overwrites existing
26+
WORKFLOW_COUNT=$((WORKFLOW_COUNT + 1))
27+
done
28+
if [ "$WORKFLOW_COUNT" -eq 0 ]; then
29+
echo "WARNING: No workflow files found in $WORKFLOWS_DIR" >&2
30+
else
31+
echo "Keystone workflows installed to $WORKFLOW_TARGET ($WORKFLOW_COUNT files)"
32+
fi
33+
34+
# 2. Install default config to ~/.config/keystone/ (only if not exists)
35+
CONFIG_TARGET="$HOME/.config/keystone"
36+
if [ ! -f "$CONFIG_TARGET/config.yaml" ]; then
37+
mkdir -p "$CONFIG_TARGET"
38+
if [ -f "$CONFIG_DIR/keystone-config.yaml" ]; then
39+
cp "$CONFIG_DIR/keystone-config.yaml" "$CONFIG_TARGET/config.yaml"
40+
echo "Default keystone config installed to $CONFIG_TARGET/config.yaml"
41+
else
42+
echo "WARNING: Default config not found at $CONFIG_DIR/keystone-config.yaml" >&2
43+
fi
44+
else
45+
echo "Keystone config already exists at $CONFIG_TARGET/config.yaml (skipped)"
46+
fi

0 commit comments

Comments
 (0)