Skip to content

Public launch readiness: gallery + MCP server + GitHub Action + article - #43

Merged
PetriLahdelma merged 9 commits into
mainfrom
claude/focused-montalcini-80c4dc
May 2, 2026
Merged

Public launch readiness: gallery + MCP server + GitHub Action + article#43
PetriLahdelma merged 9 commits into
mainfrom
claude/focused-montalcini-80c4dc

Conversation

@PetriLahdelma

@PetriLahdelma PetriLahdelma commented Apr 23, 2026

Copy link
Copy Markdown
Owner

Summary

Public launch readiness pass for the OSS CLI, marketing assets, MCP surface, GitHub Action, and supporting article/docs.

Validation

  • npm run typecheck
  • npm test
  • npm run build
  • npm run assets:check
  • site build checks

PetriLahdelma and others added 9 commits April 19, 2026 18:22
- Add docs/launch/ph.md: tagline options, maker comment drafts,
  5-slot gallery plan with capture commands, version-signal analysis
  (recommends cutting 0.10.0 over 1.0.0-beta.0), and day-of checklist.
- Bump JSON-LD softwareVersion on the marketing page to match the
  shipped npm release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@types/diff@8 is now a stub redirecting to the types bundled with
diff@9 itself. npm install -g project-spine was emitting:

  npm warn deprecated @types/diff@8.0.0: This is a stub types
  definition. diff provides its own type definitions, so you do
  not need this installed.

Also fixes an unrelated packaging mistake — it was declared under
"dependencies" rather than "devDependencies", so the types were
shipping to every global installer.

Build + 121 tests green after removal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- hero.png (110 KB) — headless-Chromium capture of projectspine.dev
  above-the-fold, dev toolbar excluded.
- demo.gif (1.1 MB, 1100x700) — copy of docs/demo/demo.gif; PH
  accepts animated GIFs in the gallery so no re-render needed.
- drift-diff.png (42 KB) — VHS tape; shows `spine drift diff` output
  after a hand-edit to AGENTS.md. Tomorrow Night palette to match
  Ghostty's default.
- tree.png (51 KB) — VHS tape; lists the 3 tool-discovery files at
  repo root + 11 compiled exports under .project-spine/exports/.

Tape files are committed alongside so the terminal slots can be
re-rendered deterministically whenever spine's output format changes.

Slot 5 (AGENTS.md rendered in Claude Code / Cursor) still needs a
real editor screenshot from your machine — documented in ph.md §3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The npm package now ships a second bin, spine-mcp, that speaks the
Model Context Protocol over stdio. Any MCP-speaking client (Claude
Code, Cursor, Continue, etc.) can drive Spine without the user
switching to a terminal.

Tools exposed:
  - spine_compile        (brief + repo + optional tokens/template)
  - spine_drift_check    (structured drift report; --json round-tripped
                          as structuredContent)
  - spine_drift_diff     (unified diffs of hand-edited exports)
  - spine_init           (scaffold brief.md from template)
  - spine_tokens_pull    (Figma Variables API; Enterprise-gated)

Resource exposed:
  - spine://manifest     (current cwd .project-spine/export-manifest.json)

Implementation is a thin wrapper: each tool shells out to the sibling
spine binary via process.execPath, so CLI behavior is preserved 1:1
including error messages, exit codes, and --json output. Resolution of
the CLI path falls through: installed-sibling, built-from-source,
SPINE_MCP_CLI override. In-memory transport tests cover tools/list and
two tool invocations (init happy path, drift-check on empty repo).

Dep added: @modelcontextprotocol/sdk@^1.29. All 124 tests green
(121 pre-existing + 3 new).

Setup notes: docs/mcp.md. README gets a short pointer section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"Why your AI agent writes padding: 13px, and how to stop it."
~900-word article framing the two-tool pattern: Spine compiles the
design-system contract into AGENTS.md / CLAUDE.md so agents know what
the tokens mean; Rhythmguard enforces token usage at lint time so
off-scale values fail CI.

Canonical target: digitaltableteur.com/articles/agent-padding-13px
Cross-post plan: dev.to (frontmatter included in file), LinkedIn
(700-char cut included at the bottom).

Markdown canonical form kept in this repo so it's versioned where
the code lives; Petri will port to DT's TS blog-post module format
at publish time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third-party consumers can now fail their own CI when AGENTS.md /
CLAUDE.md / copilot-instructions.md drift from the brief or tokens:

    - uses: PetriLahdelma/project-spine/.github/actions/drift-check@v0.9.2-alpha.0
      with:
        fail-on: any

Composite action (no Docker). Sets up Node 20, installs
project-spine@<version> globally, runs spine drift check, exposes a
`clean` output. Inputs: version, node-version, repo-path, fail-on,
json. Full reference in the action's own README.

No workflow file added to this repo's own CI — per the "no actions
usage for myself" constraint, the Action ships for external use only.
Marketplace publication is deferred.

Also added: docs/launch/placements.md with prepared submission
content for the MCP servers registry, awesome-mcp-servers,
awesome-claude-code, cursor.directory, and awesome-copilot. Order-
of-operations note says to file these after the MCP server hits npm
in a public release.

README gets a short pointer section for the Action next to the MCP
section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lates

Replaces the VHS-simulated terminal frames with authentic Ghostty
captures via screencapture -l on the Ghostty window id. Content is
byte-identical in meaning but the frames now show real Ghostty
chrome (macOS traffic lights, real font rendering, the user's actual
terminal emulator) instead of a synthesised approximation.

Gallery is now 6 assets for 5 PH slots — pick 5:

- hero.png          (unchanged — Chromium on projectspine.dev)
- demo.gif          (unchanged — the 60-s VHS demo GIF)
- drift-diff.png    (NEW, Ghostty — spine drift diff output)
- tree.png          (NEW, Ghostty — 3 discovery files + 11 exports)
- templates.png     (NEW, Ghostty — spine template list, 6 templates)
- changelog.png     (NEW, Chromium — /changelog page)
- claude.png        (NEW, Ghostty + claude CLI — "Read AGENTS.md
                     and summarise" round-tripping to a real
                     3-bullet answer; closes the brief → compile →
                     AGENTS.md → agent-consumes-it loop)

Recommended upload order documented in ph.md §3. claude.png is the
distinctive asset — it proves the downstream value instead of
describing it, which is the slot the gallery was missing.

Capture scripts are checked in next to their outputs
(capture-*.sh) so any slot can be regenerated deterministically.
The legacy VHS .tape and intermediate .gif files are removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Tagline: #5, "The missing context layer for software delivery"
  (48 chars, same as site tagline — keep voice consistent).
- Mobile LCP on production: 2.6 s (2026-04-23), down from 3.5 s
  before PRs #39 + #41. 0.1 s over the 2.5 s Good cutoff but overall
  Performance 97, A11y 96, Best Practices 96, SEO 100. Shipping
  without the WebP fallback; revisit post-launch if needed.

Open items in ph.md: version decision (alpha vs 0.10.0 vs 1.0.0-beta)
and maker comment (A vs B).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Maker comment: Draft A (warm, ~155 words). Post within 15 min.
- Version: cut 0.10.0 on launch-day morning via `npm version minor`,
  not before — keeps the readiness PRs simple and drops the alpha
  drag right when it matters.
- Hunter: self-hunt. Matches OSS-only posture.
- Target launch: Wed 2026-04-29, 00:01 PT. 6 days out, Tue/Wed is
  the highest-converting PH window.

All §7 open questions resolved. ph.md is now ready-to-execute.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 23, 2026 15:30
@vercel

vercel Bot commented Apr 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
project-spine Ready Ready Preview, Comment Apr 23, 2026 3:30pm

Request Review

@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The PR introduces a new MCP (Model Context Protocol) server that exposes Project Spine CLI operations as tools and resources, adds a GitHub Action for drift checking in CI workflows, and includes comprehensive documentation, tests, and launch materials for the new spine-mcp binary.

Changes

Cohort / File(s) Summary
GitHub Drift-Check Action
.github/actions/drift-check/action.yml, .github/actions/drift-check/README.md
Introduces a composite GitHub Action that installs project-spine, runs spine drift check with configurable options (version, node-version, repo-path, fail-on, json), and outputs a clean status based on exit code. Includes documentation with minimal and advanced workflow examples.
MCP Server Implementation
src/mcp/spawn.ts, src/mcp/server.ts
Implements a new MCP server that wraps the spine CLI, exposing tools (spine_compile, spine_drift_check, spine_drift_diff, spine_init, spine_tokens_pull) with structured JSON output handling, and a spine://manifest resource for accessing export manifests. Includes CLI resolution, process spawning, timeout enforcement, and JSON parsing utilities.
MCP Server Testing
src/mcp/server.test.ts
Adds end-to-end smoke tests for the MCP server via in-memory transport, covering tool listing (with read-only/destructive hints), project initialization, and drift checking with error assertion on empty repos.
Core & MCP Documentation
README.md, docs/mcp.md
Updates main README with new sections on spine-mcp MCP server and GitHub Action drift-check integration. Adds comprehensive docs/mcp.md guide documenting MCP tools, resource manifests, setup/config for multiple clients (Claude Code, Cursor, Continue), usage workflows, and troubleshooting.
Launch Planning & Strategy
docs/launch/ph.md, docs/launch/placements.md
Introduces Product Hunt launch plan with tagline, first comment, gallery strategy, versioning approach (0.10.0 release), and day-by-day operational checklist. Adds registry placement guide for MCP server distribution across official registries, awesome-* lists, and community directories with PR templates.
Gallery Automation Scripts
docs/launch/gallery/capture-claude.sh, docs/launch/gallery/capture-drift-diff.sh, docs/launch/gallery/capture-templates.sh, docs/launch/gallery/capture-tree.sh
Adds five shell scripts that provision isolated Spine workspaces and execute CLI commands to generate gallery/demo artifacts (Claude invocations, drift diffs, template listings, compiled outputs).
Design System & Agent Padding Article
docs/articles/agent-padding-13px.md
Introduces draft article explaining how AI agents introduce off-scale CSS values, documents the design-system "contract" concept, proposes an enforceable workflow using Project Spine + Rhythmguard for CI drift detection and autofix, with configuration examples and step-by-step setup.
Package Configuration & Version
package.json, site/app/(marketing)/page.tsx
Adds new CLI entry point spine-mcp running ./dist/mcp/server.js. Replaces @types/diff with @modelcontextprotocol/sdk dependency. Updates landing page JSON-LD softwareVersion from 0.9.1-alpha.0 to 0.9.2-alpha.0.

Sequence Diagram(s)

sequenceDiagram
    participant Client as MCP Client<br/>(Claude/Cursor)
    participant MCPServer as MCP Server<br/>(spine-mcp)
    participant SpineCLI as Spine CLI
    participant FileSystem as File System

    Client->>MCPServer: Call spine_compile tool
    activate MCPServer
    MCPServer->>SpineCLI: spawn("spine compile --repo ...")
    activate SpineCLI
    SpineCLI->>FileSystem: Read brief.md, tokens, repo
    FileSystem-->>SpineCLI: File contents
    SpineCLI->>FileSystem: Write compiled outputs to .project-spine/
    FileSystem-->>SpineCLI: Success
    SpineCLI-->>MCPServer: Exit code 0, stdout (--json payload)
    deactivate SpineCLI
    MCPServer->>MCPServer: shapeResult(): Parse JSON, set isError=false
    MCPServer-->>Client: Response with structured + text content
    deactivate MCPServer

    Client->>MCPServer: Access spine://manifest resource
    activate MCPServer
    MCPServer->>FileSystem: Read .project-spine/export-manifest.json
    FileSystem-->>MCPServer: JSON manifest
    MCPServer-->>Client: Manifest content
    deactivate MCPServer

    Client->>MCPServer: Call spine_drift_check tool
    activate MCPServer
    MCPServer->>SpineCLI: spawn("spine drift check --repo ... --json --fail-on any")
    activate SpineCLI
    SpineCLI->>FileSystem: Read exports, compare to source
    FileSystem-->>SpineCLI: File comparison result
    SpineCLI-->>MCPServer: Exit code 1, stdout (drift report JSON)
    deactivate SpineCLI
    MCPServer->>MCPServer: shapeResult(): Parse JSON, set isError=true
    MCPServer-->>Client: Response with drift errors + structured content
    deactivate MCPServer
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

The MCP server implementation spans two files with 392 lines of new logic including MCP protocol integration, multiple tool definitions with JSON handling, resource routing, and error shaping. While documentation and shell scripts are straightforward, the server code requires careful review of MCP SDK usage, CLI argument assembly, process spawning/timeout enforcement, and test coverage validation. Moderate heterogeneity with concentrated complexity in the core MCP implementation.

Poem

🐰 A shiny new server hops onto the scene,
Spinning spine-mcp so graceful and clean!
Drift checks in CI, MCP tools take flight,
Claude and Cursor now see the full light—
Launch day approaches with balloons and cheer,
Project Spine bounds into the new year! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title comprehensively summarizes the main PR objectives: Product Hunt launch preparation with five major components (kit, gallery, MCP server, GitHub Action, article).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/focused-montalcini-80c4dc

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR bundles Product Hunt launch readiness updates together with a new MCP server surface and a reusable GitHub Action, expanding how Project Spine can be driven from AI clients and CI.

Changes:

  • Add a new spine-mcp stdio MCP server that wraps key CLI commands as MCP tools, with tests and docs.
  • Add a composite GitHub Action to run spine drift check in downstream CI.
  • Add launch kit docs/assets (PH plan, placements, gallery capture scripts) plus a marketing JSON-LD version bump and README updates.

Reviewed changes

Copilot reviewed 16 out of 24 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/mcp/spawn.ts Child-process runner for invoking the Spine CLI from the MCP server.
src/mcp/server.ts MCP server implementation exposing Spine CLI capabilities as MCP tools/resources.
src/mcp/server.test.ts Vitest smoke tests for MCP tool registration and a couple of core tool flows.
package.json Adds spine-mcp bin and MCP SDK dependency; removes deprecated @types/diff.
package-lock.json Lockfile updates for MCP SDK dependency tree.
docs/mcp.md End-user setup and usage documentation for the MCP server.
README.md Adds MCP server and drift-check GitHub Action documentation sections.
.github/actions/drift-check/action.yml Composite Action definition for running spine drift check in CI.
.github/actions/drift-check/README.md Usage docs for the drift-check composite action.
site/app/(marketing)/page.tsx Bumps JSON-LD softwareVersion on the marketing landing page.
docs/launch/ph.md Product Hunt launch kit planning doc (tagline, gallery plan, checklist, date).
docs/launch/placements.md Prepared registry/awesome-list submission copy for post-release distribution.
docs/launch/gallery/drift-diff.png New gallery asset for PH.
docs/launch/gallery/capture-tree.sh Script to regenerate gallery “tree” capture.
docs/launch/gallery/capture-templates.sh Script to regenerate template list capture.
docs/launch/gallery/capture-drift-diff.sh Script to regenerate drift-diff capture.
docs/launch/gallery/capture-claude.sh Script to regenerate Claude “money shot” capture.
docs/articles/agent-padding-13px.md Draft article content for ecosystem marketing/cross-posting.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/mcp/server.ts
}

// Only run when invoked as a binary. Allows importing buildServer() from tests.
const invokedDirectly = import.meta.url === `file://${process.argv[1]}`;

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The invokedDirectly check is comparing import.meta.url to a manually constructed file://${process.argv[1]} string. When process.argv[1] is an absolute path (typical), this produces file:////... (extra slash) and won’t match import.meta.url, so main() may never run when spine-mcp is invoked as a binary. Use fileURLToPath(import.meta.url) === process.argv[1] or compare against pathToFileURL(process.argv[1]).href instead.

Suggested change
const invokedDirectly = import.meta.url === `file://${process.argv[1]}`;
const invokedDirectly = process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1];

Copilot uses AI. Check for mistakes.
Comment thread src/mcp/server.ts
Comment on lines +50 to +52
if (parsed && typeof parsed === "object" && parsed !== null) {
block.structuredContent = parsed as Record<string, unknown>;
}

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shapeResult only attaches structuredContent when parsed JSON is a non-null object. Valid JSON stdout can also be an array (or a primitive), and in those cases the parsed payload is silently dropped even when jsonStdout: true. Consider allowing arrays too (e.g., Array.isArray(parsed)), or attaching any successfully parsed JSON value.

Copilot uses AI. Check for mistakes.
Comment thread src/mcp/spawn.ts
Comment on lines +72 to +75
const timer = setTimeout(() => {
timedOut = true;
child.kill("SIGTERM");
}, timeoutMs);

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timeout handling sets timedOut = true and sends SIGTERM, but the promise only rejects on the child's close event. If the child ignores SIGTERM (or is stuck), close may never fire and the MCP tool call can hang indefinitely despite the timeout. Consider rejecting immediately on timeout and/or escalating to SIGKILL after a short grace period to guarantee termination.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +57
FLAGS="--repo '${{ inputs.repo-path }}' --fail-on '${{ inputs.fail-on }}'"
if [ "${{ inputs.json }}" = "true" ]; then
FLAGS="$FLAGS --json"
fi
# shellcheck disable=SC2086
eval spine drift check $FLAGS

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This action builds a shell command via a string and executes it with eval. Because repo-path / fail-on / json are interpolated into that string, this pattern is vulnerable to argument-breaking and command injection if any input contains shell metacharacters (including single quotes). Prefer invoking spine directly with properly quoted arguments (and conditional flags) rather than using eval.

Suggested change
FLAGS="--repo '${{ inputs.repo-path }}' --fail-on '${{ inputs.fail-on }}'"
if [ "${{ inputs.json }}" = "true" ]; then
FLAGS="$FLAGS --json"
fi
# shellcheck disable=SC2086
eval spine drift check $FLAGS
if [ "${{ inputs.json }}" = "true" ]; then
spine drift check --repo "${{ inputs.repo-path }}" --fail-on "${{ inputs.fail-on }}" --json
else
spine drift check --repo "${{ inputs.repo-path }}" --fail-on "${{ inputs.fail-on }}"
fi

Copilot uses AI. Check for mistakes.
Comment thread README.md
Comment on lines +220 to +226
Fail your own CI when `AGENTS.md` / `CLAUDE.md` / `copilot-instructions.md` drift from the brief or tokens. No release to Marketplace yet — reference by full repo path:

```yaml
- uses: PetriLahdelma/project-spine/.github/actions/drift-check@v0.9.2-alpha.0
with:
fail-on: any
```

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This README snippet pins the composite action reference to @v0.9.2-alpha.0, but the PR description states third-party consumers should reference it as .../drift-check@v0.10.0. Please align the documented ref with the intended release/tag strategy (or clarify why the README should point at 0.9.2).

Copilot uses AI. Check for mistakes.
#!/bin/bash
# Gallery slot 4 — generated files, captured inside real Ghostty
set -e
export PATH="/Users/petrilahdelma/.nvm/versions/node/v20.19.2/bin:$PATH"

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The capture script hardcodes a user-specific PATH (/Users/petrilahdelma/...). This makes regeneration non-portable for other contributors and leaks a local machine path into the repo. Prefer relying on the caller’s existing PATH (and documenting prerequisites), or resolve node/spine dynamically (e.g., command -v spine) without embedding an absolute home directory path.

Suggested change
export PATH="/Users/petrilahdelma/.nvm/versions/node/v20.19.2/bin:$PATH"
command -v spine >/dev/null 2>&1 || {
echo "Error: 'spine' must be available on PATH." >&2
exit 1
}
command -v node >/dev/null 2>&1 || {
echo "Error: 'node' must be available on PATH." >&2
exit 1
}

Copilot uses AI. Check for mistakes.
#!/bin/bash
# Gallery — template catalog
set -e
export PATH="/Users/petrilahdelma/.nvm/versions/node/v20.19.2/bin:$PATH"

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This capture script hardcodes a user-specific PATH (/Users/petrilahdelma/...). For reproducibility, avoid absolute home-directory paths and instead rely on the environment’s PATH (with documented prerequisites) or discover binaries dynamically.

Suggested change
export PATH="/Users/petrilahdelma/.nvm/versions/node/v20.19.2/bin:$PATH"
# Require `spine` to be available on PATH.
command -v spine >/dev/null 2>&1 || {
echo "Error: 'spine' must be installed and available on PATH." >&2
exit 1
}

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +23
export PATH="/Users/petrilahdelma/.nvm/versions/node/v20.19.2/bin:/Users/petrilahdelma/.local/bin:$PATH"

WORK=$(mktemp -d)
cd "$WORK"
spine init --template saas-marketing >/dev/null 2>&1
printf '{"name":"acme-payroll","dependencies":{"next":"14.0.0"}}' > package.json
mkdir -p app && printf '{"compilerOptions":{"strict":true}}' > tsconfig.json
spine compile --brief ./brief.md --repo . --template saas-marketing >/dev/null 2>&1

clear
# Show the prompt, then ask Claude — print mode keeps the output static
# so the terminal frame can be photographed cleanly.
echo 'acme-payroll $ claude -p "Read AGENTS.md and summarise what this project wants in 3 bullets."'
echo
claude -p "Read AGENTS.md in the current directory and summarise what this project wants in exactly 3 short bullets. Keep it under 60 words total. Start with 'Project wants to:'."

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This capture script hardcodes a user-specific PATH (/Users/petrilahdelma/...). That makes the script non-portable and leaks a local machine path into the repo. Prefer relying on PATH + documented prerequisites, or dynamically locating spine/claude with command -v.

Suggested change
export PATH="/Users/petrilahdelma/.nvm/versions/node/v20.19.2/bin:/Users/petrilahdelma/.local/bin:$PATH"
WORK=$(mktemp -d)
cd "$WORK"
spine init --template saas-marketing >/dev/null 2>&1
printf '{"name":"acme-payroll","dependencies":{"next":"14.0.0"}}' > package.json
mkdir -p app && printf '{"compilerOptions":{"strict":true}}' > tsconfig.json
spine compile --brief ./brief.md --repo . --template saas-marketing >/dev/null 2>&1
clear
# Show the prompt, then ask Claude — print mode keeps the output static
# so the terminal frame can be photographed cleanly.
echo 'acme-payroll $ claude -p "Read AGENTS.md and summarise what this project wants in 3 bullets."'
echo
claude -p "Read AGENTS.md in the current directory and summarise what this project wants in exactly 3 short bullets. Keep it under 60 words total. Start with 'Project wants to:'."
SPINE_BIN=$(command -v spine) || {
echo "Error: spine must be installed and available on PATH." >&2
exit 1
}
CLAUDE_BIN=$(command -v claude) || {
echo "Error: claude must be installed and available on PATH." >&2
exit 1
}
WORK=$(mktemp -d)
cd "$WORK"
"$SPINE_BIN" init --template saas-marketing >/dev/null 2>&1
printf '{"name":"acme-payroll","dependencies":{"next":"14.0.0"}}' > package.json
mkdir -p app && printf '{"compilerOptions":{"strict":true}}' > tsconfig.json
"$SPINE_BIN" compile --brief ./brief.md --repo . --template saas-marketing >/dev/null 2>&1
clear
# Show the prompt, then ask Claude — print mode keeps the output static
# so the terminal frame can be photographed cleanly.
echo 'acme-payroll $ claude -p "Read AGENTS.md and summarise what this project wants in 3 bullets."'
echo
"$CLAUDE_BIN" -p "Read AGENTS.md in the current directory and summarise what this project wants in exactly 3 short bullets. Keep it under 60 words total. Start with 'Project wants to:'."

Copilot uses AI. Check for mistakes.
#!/bin/bash
# Gallery slot 3 — drift diff, captured inside real Ghostty
set -e
export PATH="/Users/petrilahdelma/.nvm/versions/node/v20.19.2/bin:$PATH"

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This capture script hardcodes a user-specific PATH (/Users/petrilahdelma/...). For reproducibility across machines/CI, avoid embedding absolute home paths; rely on the caller’s PATH (with prerequisites documented) or locate spine dynamically.

Suggested change
export PATH="/Users/petrilahdelma/.nvm/versions/node/v20.19.2/bin:$PATH"

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
src/mcp/spawn.ts (1)

40-75: Two small runtime concerns on the child-process plumbing.

  1. SpineRunOptions.env types values as string | undefined, but the spread on L64 passes those undefined values straight into spawn's env. Node does not strip undefined keys from the child's env — depending on version/platform you get a literal "undefined" string. Either filter or narrow the type.
  2. Timeout only sends SIGTERM. If the spine CLI ever ignores or delays termination (e.g., stuck in a long sync step), the timer fires, timedOut=true, but close may never arrive promptly. An escalating SIGKILL after a grace window is cheap insurance.
♻️ Proposed refactor
-    const child = spawn(process.execPath, [SPINE_CLI, ...args], {
-      cwd: opts.cwd,
-      env: { ...process.env, ...opts.env },
-      stdio: ["ignore", "pipe", "pipe"],
-    });
+    const mergedEnv: Record<string, string> = { ...process.env } as Record<string, string>;
+    for (const [k, v] of Object.entries(opts.env ?? {})) {
+      if (v === undefined) delete mergedEnv[k];
+      else mergedEnv[k] = v;
+    }
+    const child = spawn(process.execPath, [SPINE_CLI, ...args], {
+      cwd: opts.cwd,
+      env: mergedEnv,
+      stdio: ["ignore", "pipe", "pipe"],
+    });
@@
-    const timer = setTimeout(() => {
-      timedOut = true;
-      child.kill("SIGTERM");
-    }, timeoutMs);
+    let killTimer: NodeJS.Timeout | undefined;
+    const timer = setTimeout(() => {
+      timedOut = true;
+      child.kill("SIGTERM");
+      killTimer = setTimeout(() => child.kill("SIGKILL"), 5_000);
+    }, timeoutMs);
@@
-    child.on("close", (code) => {
-      clearTimeout(timer);
+    child.on("close", (code) => {
+      clearTimeout(timer);
+      if (killTimer) clearTimeout(killTimer);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp/spawn.ts` around lines 40 - 75, SpineRunOptions.env may contain
undefined values which get passed through to spawn and can turn into the string
"undefined", and runSpine's timeout only sends SIGTERM once; update runSpine to
(1) sanitize opts.env by removing keys whose values are undefined before
spreading into spawn (reference SpineRunOptions and the spawn call that builds
env), and (2) implement an escalation: after the initial setTimeout handler sets
timedOut and calls child.kill("SIGTERM"), start a short secondary timer (e.g.,
1–5s grace) that calls child.kill("SIGKILL") if the process hasn't exited;
ensure you clear both timers when the child exits/close and preserve existing
stdout/stderr/resolve logic (reference variables timer, timedOut, and child.kill
usage in runSpine).
docs/launch/gallery/capture-claude.sh (1)

9-9: Hardcoded per-user absolute path makes the script non-portable.

/Users/petrilahdelma/.nvm/... and /Users/petrilahdelma/.local/bin tie this to one workstation. ph.md documents that capture scripts are run locally by the maintainer, so this is intentional — but if anyone else (future you, a collaborator, CI smoke) tries to regenerate, it silently falls through to whatever spine/claude happen to be on the default PATH. Consider dropping the hardcoded prefix and letting the invoking shell supply PATH, or guarding with command -v spine claude || { echo "…"; exit 1; }. Same comment applies to capture-tree.sh and capture-drift-diff.sh.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/launch/gallery/capture-claude.sh` at line 9, The script export hardcodes
per-user absolute paths (the export PATH line) which makes capture-claude.sh
(and similarly capture-tree.sh and capture-drift-diff.sh) non-portable; remove
the fixed /Users/petrilahdelma/... prefixes and either rely on the invoking
shell's PATH or, if a specific user-local bin is required, reference $HOME
(e.g., $HOME/.nvm/... or $HOME/.local/bin) instead of the literal path, and add
a runtime guard at the script start (use command -v spine claude || { echo
"spine/claude required"; exit 1; }) to fail fast when required binaries are
missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/actions/drift-check/action.yml:
- Around line 43-64: Replace direct interpolation of GitHub inputs into the bash
script and remove eval: pass inputs via environment variables in the "Run spine
drift check" step (e.g., set env PROJECT_SPINE_VERSION, REPO_PATH, FAIL_ON,
JSON_FLAG) and reference them inside the script using "$PROJECT_SPINE_VERSION",
"$REPO_PATH", "$FAIL_ON", "$JSON_FLAG" to avoid code injection, update the
"Install project-spine" step to use the env var instead of `${{ inputs.version
}}`, build FLAGS using safe quoted variable expansions (e.g., FLAGS="--repo
\"$REPO_PATH\" --fail-on \"$FAIL_ON\"" and append --json only if "$JSON_FLAG" =
"true"), and invoke the command directly as spine drift check $FLAGS (no eval)
in the step with id "run" to eliminate the injection vector introduced by eval.

In `@docs/articles/agent-padding-13px.md`:
- Around line 105-108: The fenced code block showing the Cards.module.css lint
output triggers markdownlint MD040 because it lacks a language; update the block
to include a language token (e.g., change the opening triple backticks to
```text or ```console) so it becomes "```text" and keep the block contents
unchanged; ensure the same change is applied to any other similar unlabeled
fenced blocks in docs/articles/agent-padding-13px.md.

In `@docs/launch/gallery/capture-templates.sh`:
- Line 7: The export of HOME masks mktemp errors; change the single-line "export
HOME=$(mktemp -d)" to first run mktemp -d into a temp variable (e.g.,
tmp_dir=$(mktemp -d)) check its exit/status is non-empty, then assign and export
HOME (export HOME="$tmp_dir"); update the capture-templates.sh usage of HOME
accordingly and mirror this pattern where HOME is set to ensure failures aren't
silently ignored.

In `@docs/launch/ph.md`:
- Around line 54-66: Update the heading "Shipped — 6 assets for 5 slots" to
reflect the seven files listed in the table (hero.png, demo.gif, drift-diff.png,
tree.png, templates.png, changelog.png, claude.png); change the header text to
"Shipped — 7 assets for 5 slots" (or equivalent phrasing that includes
claude.png as the money-shot) so the header matches the table and PR
description.

In `@src/mcp/server.ts`:
- Around line 274-281: The direct-invocation check using import.meta.url ===
`file://${process.argv[1]}` is brittle on Windows, paths with spaces, and
symlinks; change the logic that computes invokedDirectly to resolve and compare
canonical file paths: convert import.meta.url to a path with
fileURLToPath(import.meta.url) and canonicalize both that path and
process.argv[1] with fs.realpathSync (or use import.meta.filename on Node>=22)
and then compare those resolved strings before calling main(). Ensure you update
the invokedDirectly variable and keep the existing main().catch(...) error
handling unchanged.

---

Nitpick comments:
In `@docs/launch/gallery/capture-claude.sh`:
- Line 9: The script export hardcodes per-user absolute paths (the export PATH
line) which makes capture-claude.sh (and similarly capture-tree.sh and
capture-drift-diff.sh) non-portable; remove the fixed /Users/petrilahdelma/...
prefixes and either rely on the invoking shell's PATH or, if a specific
user-local bin is required, reference $HOME (e.g., $HOME/.nvm/... or
$HOME/.local/bin) instead of the literal path, and add a runtime guard at the
script start (use command -v spine claude || { echo "spine/claude required";
exit 1; }) to fail fast when required binaries are missing.

In `@src/mcp/spawn.ts`:
- Around line 40-75: SpineRunOptions.env may contain undefined values which get
passed through to spawn and can turn into the string "undefined", and runSpine's
timeout only sends SIGTERM once; update runSpine to (1) sanitize opts.env by
removing keys whose values are undefined before spreading into spawn (reference
SpineRunOptions and the spawn call that builds env), and (2) implement an
escalation: after the initial setTimeout handler sets timedOut and calls
child.kill("SIGTERM"), start a short secondary timer (e.g., 1–5s grace) that
calls child.kill("SIGKILL") if the process hasn't exited; ensure you clear both
timers when the child exits/close and preserve existing stdout/stderr/resolve
logic (reference variables timer, timedOut, and child.kill usage in runSpine).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 19945593-fe90-4b51-880f-5d0e16de9d05

📥 Commits

Reviewing files that changed from the base of the PR and between 54e1b6a and a8d0aa9.

⛔ Files ignored due to path filters (8)
  • docs/launch/gallery/changelog.png is excluded by !**/*.png
  • docs/launch/gallery/claude.png is excluded by !**/*.png
  • docs/launch/gallery/demo.gif is excluded by !**/*.gif
  • docs/launch/gallery/drift-diff.png is excluded by !**/*.png
  • docs/launch/gallery/hero.png is excluded by !**/*.png
  • docs/launch/gallery/templates.png is excluded by !**/*.png
  • docs/launch/gallery/tree.png is excluded by !**/*.png
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (16)
  • .github/actions/drift-check/README.md
  • .github/actions/drift-check/action.yml
  • README.md
  • docs/articles/agent-padding-13px.md
  • docs/launch/gallery/capture-claude.sh
  • docs/launch/gallery/capture-drift-diff.sh
  • docs/launch/gallery/capture-templates.sh
  • docs/launch/gallery/capture-tree.sh
  • docs/launch/ph.md
  • docs/launch/placements.md
  • docs/mcp.md
  • package.json
  • site/app/(marketing)/page.tsx
  • src/mcp/server.test.ts
  • src/mcp/server.ts
  • src/mcp/spawn.ts

Comment on lines +43 to +64
- name: Install project-spine
shell: bash
run: npm install -g "project-spine@${{ inputs.version }}"

- name: Run spine drift check
id: run
shell: bash
run: |
set +e
FLAGS="--repo '${{ inputs.repo-path }}' --fail-on '${{ inputs.fail-on }}'"
if [ "${{ inputs.json }}" = "true" ]; then
FLAGS="$FLAGS --json"
fi
# shellcheck disable=SC2086
eval spine drift check $FLAGS
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
echo "clean=true" >> "$GITHUB_OUTPUT"
else
echo "clean=false" >> "$GITHUB_OUTPUT"
fi
exit $EXIT_CODE

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

GitHub Actions security hardening: script injection via ${{ }} expressions in run steps

💡 Result:

Script injection via ${{ }} expressions in GitHub Actions run steps occurs when untrusted inputs from contexts like github.event.pull_request.title are directly interpolated into shell scripts. GitHub Actions evaluates expressions before writing a temporary script file for execution, allowing attackers to inject commands. Vulnerable example: - name: Check PR title run: | title="${{ github.event.pull_request.title }}" if [[ $title =~ ^octocat ]]; then echo "Valid" fi An attacker sets PR title to a"; ls $GITHUB_WORKSPACE;#, injecting ls command. Hardening steps: 1. Use environment variables: Set untrusted values in env, reference with shell syntax (e.g., $TITLE). - env: TITLE: ${{ github.event.pull_request.title }} run: | if [[ "$TITLE" =~ ^octocat ]]; then echo "Valid" fi This stores value in memory, avoiding script injection. 2. Prefer custom actions over inline run scripts for processing untrusted input, passing as arguments. 3. Avoid ${{ }} in run/script blocks; safe in env/args. 4. Use tools like GitHub CodeQL, Scorecards, or ades to scan workflows. 5. Set GITHUB_TOKEN to read-only defaults, pin actions, limit permissions. Official guidance unchanged as of 2026 per docs and recent discussions.

Citations:


Critical security issue: script injection via ${{ inputs.* }} directly into bash scripts.

GitHub Actions expands ${{ }} expressions before the script executes, so untrusted inputs containing quotes or shell metacharacters will break out and inject arbitrary commands. An attacker can pass version: "'; malicious_command; #" or repo-path: "'; rm -rf ~; '" to execute commands on the runner. This affects lines 45 (npm install), 52–57 (FLAGS construction), and 57 (eval call).

Additionally, eval is unnecessary here and amplifies the injection risk. Use environment variables to safely pass untrusted input, as documented in GitHub's security hardening guide for Actions.

🛡️ Proposed fix: pass inputs via env, drop eval
     - name: Install project-spine
       shell: bash
-      run: npm install -g "project-spine@${{ inputs.version }}"
+      env:
+        SPINE_VERSION: ${{ inputs.version }}
+      run: npm install -g "project-spine@${SPINE_VERSION}"
 
     - name: Run spine drift check
       id: run
       shell: bash
+      env:
+        REPO_PATH: ${{ inputs.repo-path }}
+        FAIL_ON: ${{ inputs.fail-on }}
+        EMIT_JSON: ${{ inputs.json }}
       run: |
         set +e
-        FLAGS="--repo '${{ inputs.repo-path }}' --fail-on '${{ inputs.fail-on }}'"
-        if [ "${{ inputs.json }}" = "true" ]; then
-          FLAGS="$FLAGS --json"
+        args=(drift check --repo "$REPO_PATH" --fail-on "$FAIL_ON")
+        if [ "$EMIT_JSON" = "true" ]; then
+          args+=(--json)
         fi
-        # shellcheck disable=SC2086
-        eval spine drift check $FLAGS
+        spine "${args[@]}"
         EXIT_CODE=$?
         if [ $EXIT_CODE -eq 0 ]; then
           echo "clean=true" >> "$GITHUB_OUTPUT"
         else
           echo "clean=false" >> "$GITHUB_OUTPUT"
         fi
         exit $EXIT_CODE
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/actions/drift-check/action.yml around lines 43 - 64, Replace direct
interpolation of GitHub inputs into the bash script and remove eval: pass inputs
via environment variables in the "Run spine drift check" step (e.g., set env
PROJECT_SPINE_VERSION, REPO_PATH, FAIL_ON, JSON_FLAG) and reference them inside
the script using "$PROJECT_SPINE_VERSION", "$REPO_PATH", "$FAIL_ON",
"$JSON_FLAG" to avoid code injection, update the "Install project-spine" step to
use the env var instead of `${{ inputs.version }}`, build FLAGS using safe
quoted variable expansions (e.g., FLAGS="--repo \"$REPO_PATH\" --fail-on
\"$FAIL_ON\"" and append --json only if "$JSON_FLAG" = "true"), and invoke the
command directly as spine drift check $FLAGS (no eval) in the step with id "run"
to eliminate the injection vector introduced by eval.

Comment on lines +105 to +108
```
Cards.module.css:12:3 × Use a value on the rhythmic-4 scale.
Expected 12 or 16, got 13. Run `--fix` to normalize.
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a language to the fenced code block.

markdownlint MD040 flags this block. Use text (or console) so cross-posts to dev.to render consistently.

📝 Proposed fix
-```
+```text
 Cards.module.css:12:3 × Use a value on the rhythmic-4 scale.
   Expected 12 or 16, got 13. Run `--fix` to normalize.
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 105-105: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/articles/agent-padding-13px.md` around lines 105 - 108, The fenced code
block showing the Cards.module.css lint output triggers markdownlint MD040
because it lacks a language; update the block to include a language token (e.g.,
change the opening triple backticks to ```text or ```console) so it becomes
"```text" and keep the block contents unchanged; ensure the same change is
applied to any other similar unlabeled fenced blocks in
docs/articles/agent-padding-13px.md.

export PATH="/Users/petrilahdelma/.nvm/versions/node/v20.19.2/bin:$PATH"
# Use a clean HOME so user-local templates under ~/.project-spine/templates
# don't leak into the capture.
export HOME=$(mktemp -d)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

SC2155: mktemp -d exit status is masked by export.

Because export always returns 0, a failure of mktemp -d would silently set HOME to empty and defeat set -e. Split the assignment.

📝 Proposed fix
-export HOME=$(mktemp -d)
+HOME=$(mktemp -d)
+export HOME

The same pattern applies in capture-claude.sh / capture-tree.sh (WORK=$(mktemp -d) there is already separate, so fine — only HOME needs fixing here).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export HOME=$(mktemp -d)
HOME=$(mktemp -d)
export HOME
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 7-7: Declare and assign separately to avoid masking return values.

(SC2155)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/launch/gallery/capture-templates.sh` at line 7, The export of HOME masks
mktemp errors; change the single-line "export HOME=$(mktemp -d)" to first run
mktemp -d into a temp variable (e.g., tmp_dir=$(mktemp -d)) check its
exit/status is non-empty, then assign and export HOME (export HOME="$tmp_dir");
update the capture-templates.sh usage of HOME accordingly and mirror this
pattern where HOME is set to ensure failures aren't silently ignored.

Comment thread docs/launch/ph.md
Comment on lines +54 to +66
### Shipped — 6 assets for 5 slots

All 1270×760. Terminal slots captured in real Ghostty (not simulated) via `screencapture -l <windowId>`; hero and changelog via headless Chromium on the live production site. Pick the 5 strongest for the PH upload.

| File | What it shows | Size | Source |
|---|---|---|---|
| [`hero.png`](gallery/hero.png) | Site hero: wordmark + "The context layer your coding agents are missing." | 108 KB | Headless Chromium on `projectspine.dev` |
| [`demo.gif`](gallery/demo.gif) | 60-s CLI demo: init → compile → drift check → drift diff | 1.1 MB, 1100×700 | `docs/demo/demo.tape` (VHS) |
| [`drift-diff.png`](gallery/drift-diff.png) | `spine drift diff` showing the `+# hand edit` that slipped in | 60 KB | Ghostty, `capture-drift-diff.sh` |
| [`tree.png`](gallery/tree.png) | The 3 tool-discovery files + 11 compiled exports (the "19 files per compile" proof) | 128 KB | Ghostty, `capture-tree.sh` |
| [`templates.png`](gallery/templates.png) | `spine template list` — all 6 bundled templates with descriptions | 136 KB | Ghostty, `capture-templates.sh` |
| [`changelog.png`](gallery/changelog.png) | `/changelog` page: "What shipped, when, and what changed" with the 0.9.x-alpha aside | 112 KB | Headless Chromium |
| [`claude.png`](gallery/claude.png) | **The money shot.** `claude -p "Read AGENTS.md and summarise…"` answering with a 3-bullet summary pulled from the Spine-generated AGENTS.md. Closes the loop: brief → compile → AGENTS.md → real Claude consumes it. | 128 KB | Ghostty + claude CLI, `capture-claude.sh` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Asset count mismatch: header says 6, table lists 7.

The heading "Shipped — 6 assets for 5 slots" conflicts with the table, which enumerates 7 files (hero, demo.gif, drift-diff, tree, templates, changelog, claude). The PR description also calls out 6 + claude.png as the money-shot, so the table is correct — update the heading.

📝 Proposed fix
-### Shipped — 6 assets for 5 slots
+### Shipped — 7 assets for 5 slots
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### Shipped — 6 assets for 5 slots
All 1270×760. Terminal slots captured in real Ghostty (not simulated) via `screencapture -l <windowId>`; hero and changelog via headless Chromium on the live production site. Pick the 5 strongest for the PH upload.
| File | What it shows | Size | Source |
|---|---|---|---|
| [`hero.png`](gallery/hero.png) | Site hero: wordmark + "The context layer your coding agents are missing." | 108 KB | Headless Chromium on `projectspine.dev` |
| [`demo.gif`](gallery/demo.gif) | 60-s CLI demo: init → compile → drift check → drift diff | 1.1 MB, 1100×700 | `docs/demo/demo.tape` (VHS) |
| [`drift-diff.png`](gallery/drift-diff.png) | `spine drift diff` showing the `+# hand edit` that slipped in | 60 KB | Ghostty, `capture-drift-diff.sh` |
| [`tree.png`](gallery/tree.png) | The 3 tool-discovery files + 11 compiled exports (the "19 files per compile" proof) | 128 KB | Ghostty, `capture-tree.sh` |
| [`templates.png`](gallery/templates.png) | `spine template list` — all 6 bundled templates with descriptions | 136 KB | Ghostty, `capture-templates.sh` |
| [`changelog.png`](gallery/changelog.png) | `/changelog` page: "What shipped, when, and what changed" with the 0.9.x-alpha aside | 112 KB | Headless Chromium |
| [`claude.png`](gallery/claude.png) | **The money shot.** `claude -p "Read AGENTS.md and summarise…"` answering with a 3-bullet summary pulled from the Spine-generated AGENTS.md. Closes the loop: brief → compile → AGENTS.md → real Claude consumes it. | 128 KB | Ghostty + claude CLI, `capture-claude.sh` |
### Shipped — 7 assets for 5 slots
All 1270×760. Terminal slots captured in real Ghostty (not simulated) via `screencapture -l <windowId>`; hero and changelog via headless Chromium on the live production site. Pick the 5 strongest for the PH upload.
| File | What it shows | Size | Source |
|---|---|---|---|
| [`hero.png`](gallery/hero.png) | Site hero: wordmark + "The context layer your coding agents are missing." | 108 KB | Headless Chromium on `projectspine.dev` |
| [`demo.gif`](gallery/demo.gif) | 60-s CLI demo: init → compile → drift check → drift diff | 1.1 MB, 1100×700 | `docs/demo/demo.tape` (VHS) |
| [`drift-diff.png`](gallery/drift-diff.png) | `spine drift diff` showing the `+# hand edit` that slipped in | 60 KB | Ghostty, `capture-drift-diff.sh` |
| [`tree.png`](gallery/tree.png) | The 3 tool-discovery files + 11 compiled exports (the "19 files per compile" proof) | 128 KB | Ghostty, `capture-tree.sh` |
| [`templates.png`](gallery/templates.png) | `spine template list` — all 6 bundled templates with descriptions | 136 KB | Ghostty, `capture-templates.sh` |
| [`changelog.png`](gallery/changelog.png) | `/changelog` page: "What shipped, when, and what changed" with the 0.9.x-alpha aside | 112 KB | Headless Chromium |
| [`claude.png`](gallery/claude.png) | **The money shot.** `claude -p "Read AGENTS.md and summarise…"` answering with a 3-bullet summary pulled from the Spine-generated AGENTS.md. Closes the loop: brief → compile → AGENTS.md → real Claude consumes it. | 128 KB | Ghostty + claude CLI, `capture-claude.sh` |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/launch/ph.md` around lines 54 - 66, Update the heading "Shipped — 6
assets for 5 slots" to reflect the seven files listed in the table (hero.png,
demo.gif, drift-diff.png, tree.png, templates.png, changelog.png, claude.png);
change the header text to "Shipped — 7 assets for 5 slots" (or equivalent
phrasing that includes claude.png as the money-shot) so the header matches the
table and PR description.

Comment thread src/mcp/server.ts
Comment on lines +274 to +281
// Only run when invoked as a binary. Allows importing buildServer() from tests.
const invokedDirectly = import.meta.url === `file://${process.argv[1]}`;
if (invokedDirectly) {
main().catch((err) => {
process.stderr.write(`[spine-mcp] fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
process.exit(1);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Node.js ESM detect if file is main module import.meta.url process.argv[1] robust

💡 Result:

In Node.js ES modules (ESM), the robust and official way to detect if the current file is the main module (entry point run directly via node filename.js) is to use import.meta.main, available since Node.js v24 (merged May 2025, stable by 2026). Usage: if (import.meta.main) { // This module is the main entry point. main; } This is the direct ESM equivalent of CommonJS's require.main === module. It evaluates to true only for the process's entry point module, false otherwise (including imported modules and workers in some cases). For older Node.js versions or maximum compatibility, use the path comparison method: import { fileURLToPath } from 'node:url'; import { pathToFileURL } from 'node:url'; const currentPath = fileURLToPath(import.meta.url); if (process.argv[1] === currentPath) { // Main module. } With import.meta.filename (stable since v22): if (process.argv[1] === import.meta.filename) { // Main module. } The import.meta.main approach is preferred as it's official, concise, and handles edge cases like workers and data URLs correctly. The path comparison fails if the main script is invoked without extension (e.g., node script vs node script.js) or across symlinks without realpath normalization. Third-party libraries like es-main exist but are unnecessary with native support.

Citations:


🏁 Script executed:

# Check package.json for Node.js version requirements
if [ -f package.json ]; then
  cat package.json | jq -r '.engines.node // "No explicit requirement"' 2>/dev/null || cat package.json | grep -A 5 '"engines"' || echo "Checking for engine field..."
fi

# Also check if there's a .nvmrc or similar
[ -f .nvmrc ] && echo ".nvmrc: $(cat .nvmrc)" || echo "No .nvmrc"
[ -f .node-version ] && echo ".node-version: $(cat .node-version)" || echo "No .node-version"

Repository: PetriLahdelma/project-spine

Length of output: 106


Direct-invocation check fails on Windows and paths with spaces.

The comparison import.meta.url === \file://${process.argv[1]}`` is fragile:

  • Paths with spaces / non-ASCIIimport.meta.url percent-encodes them (%20), but the template literal does not.
  • Windowsimport.meta.url uses forward slashes and proper URL format (file:///C:/...), but the template produces backslashes (file://C:\...).
  • symlink edge casesargv[1] may not be canonicalized, diverging from the resolved import.meta.url.

When the check fails, spine-mcp silently exits without starting the server, breaking the binary entrypoint. The proposed fix using fileURLToPath and realpathSync handles these cases correctly for Node.js >=20.

Alternatively, Node.js >=22 users can use import.meta.filename, which is simpler: if (process.argv[1] === import.meta.filename) { main().catch(...) }.

🔧 Proposed fix
+import { realpathSync } from "node:fs";
+import { fileURLToPath } from "node:url";
 // ...
-// Only run when invoked as a binary. Allows importing buildServer() from tests.
-const invokedDirectly = import.meta.url === `file://${process.argv[1]}`;
+// Only run when invoked as a binary. Allows importing buildServer() from tests.
+let invokedDirectly = false;
+try {
+  invokedDirectly =
+    fileURLToPath(import.meta.url) === realpathSync(process.argv[1] ?? "");
+} catch {
+  invokedDirectly = false;
+}
 if (invokedDirectly) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp/server.ts` around lines 274 - 281, The direct-invocation check using
import.meta.url === `file://${process.argv[1]}` is brittle on Windows, paths
with spaces, and symlinks; change the logic that computes invokedDirectly to
resolve and compare canonical file paths: convert import.meta.url to a path with
fileURLToPath(import.meta.url) and canonicalize both that path and
process.argv[1] with fs.realpathSync (or use import.meta.filename on Node>=22)
and then compare those resolved strings before calling main(). Ensure you update
the invokedDirectly variable and keep the existing main().catch(...) error
handling unchanged.

@PetriLahdelma
PetriLahdelma merged commit 29ecb9e into main May 2, 2026
10 checks passed
@PetriLahdelma PetriLahdelma changed the title PH launch readiness: kit + gallery + MCP server + GitHub Action + article Public launch readiness: gallery + MCP server + GitHub Action + article May 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants