Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# CODEOWNERS — gates brand-safety configs per DC17.
#
# PRs touching the listed files require approval from one of the listed owners
# before merging. The weekly AI cycle reads these files as its primary brand
# and content controls; an unreviewed edit would change what the next autonomous
# drop ships. Auto-merge does NOT apply to PRs blocked on CODEOWNERS review.
#
# Update the team handle below to your GitHub org once configured (e.g.,
# @rootvc/partners). The placeholder @rootvc-team should be replaced with the
# actual team handle during U10's Vercel/GitHub setup pass.

config/brand-brief.md @rootvc-team
config/no-fly-list.md @rootvc-team
config/topical-rubric.md @rootvc-team
config/firm.js @rootvc-team
3 changes: 2 additions & 1 deletion .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ jobs:
fetch-depth: 1

- name: Run Claude PR Action
uses: anthropics/claude-code-action@beta
# DC14: pinned to v1 SHA per the cycle plan; previously @beta.
uses: anthropics/claude-code-action@537ffff2eff706bd7e3e1c3daf2d4b39067a9f85 # v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60"
64 changes: 64 additions & 0 deletions .github/workflows/spike.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: U0 Runtime Validation Spike

# Manually-dispatched spike that validates whether claude-code-action can host the
# weekly-cycle architecture (Agent tool, /last30days skill or WebSearch fallback,
# Write, Bash for smoke tests, schedule trigger, token observability).
#
# Run via:
# - GitHub UI: Actions tab → "U0 Runtime Validation Spike" → Run workflow
# - Or: gh workflow run spike.yml
#
# Outputs a JSON capability report as a job artifact.

on:
workflow_dispatch:
inputs:
verbose:
description: "Verbose output"
required: false
default: "false"
type: choice
options: ["true", "false"]

jobs:
spike:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Run claude-code-action capability spike
# Pinned to v1 (SHA 537ffff2eff706bd7e3e1c3daf2d4b39067a9f85 at time of
# writing) per DC14. Dependabot's github-actions ecosystem will alert
# when the upstream tag moves; reviewers should re-verify before
# bumping.
uses: anthropics/claude-code-action@537ffff2eff706bd7e3e1c3daf2d4b39067a9f85 # v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "10"
# Pass the spike prompt as the action's instruction. Claude Code reads
# this file and follows its sequenced capability tests, writing the
# result report to spike-report.json at the repo root.
prompt_file: scripts/cycle/spike-prompt.md

- name: Upload spike report
if: always()
uses: actions/upload-artifact@v4
with:
name: spike-report
path: |
spike-report.json
spike-report.md
if-no-files-found: warn
retention-days: 30
229 changes: 229 additions & 0 deletions .github/workflows/weekly-cycle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
name: Weekly AI Reinvention Cycle

# Sunday-evening cron that drives the autonomous Author + Editor cycle
# documented in docs/plans/2026-06-01-001-feat-weekly-ai-reinvention-plan.md.
#
# DC14: claude-code-action is pinned to @v1. Before enabling the cron in
# production, replace @v1 with a specific commit SHA so a supply-chain update
# can't silently gain access to ANTHROPIC_API_KEY + GITHUB_TOKEN. Dependabot's
# github-actions ecosystem will alert when the upstream tag moves.
#
# DC15: least-privilege permissions block; no PAT used.
# DC9: cost cap is enforced by --max-turns (in claude_args) and
# timeout-minutes (job-level wall-clock cap). Anthropic console per-key
# spending limit is the hard backstop.

on:
schedule:
# Monday 01:00 UTC = Sunday 17:00 PST / Sunday 18:00 PDT. Adjust the hour
# if you want strict year-round consistency.
- cron: "0 1 * * 1"
workflow_dispatch:
inputs:
max_retries:
description: "Author retry limit (default 3)"
required: false
default: "3"
artifact_date:
description: "Override the cycle date (YYYY-MM-DD); blank uses today"
required: false
default: ""

concurrency:
group: weekly-cycle
cancel-in-progress: false

jobs:
cycle:
runs-on: ubuntu-latest
timeout-minutes: 45 # DC9: wall-clock ceiling

permissions:
contents: write
pull-requests: write
issues: write

steps:
- name: Checkout repository at main HEAD
uses: actions/checkout@v4
with:
fetch-depth: 1
ref: main

- name: Capture starting SHA for race detection
id: starting_sha
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install dependencies
run: npm ci

- name: Compute cycle date
id: cycle_date
run: |
if [ -n "${{ github.event.inputs.artifact_date }}" ]; then
echo "date=${{ github.event.inputs.artifact_date }}" >> "$GITHUB_OUTPUT"
else
echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT"
fi

- name: Create cycle branch
run: |
git config user.email "noreply@root.vc"
git config user.name "Root Cycle Bot"
git checkout -b "cycle/${{ steps.cycle_date.outputs.date }}"

- name: Run claude-code-action cycle
# DC14: pinned to v1 SHA 537ffff2eff706bd7e3e1c3daf2d4b39067a9f85.
# Dependabot's github-actions ecosystem will alert when the upstream
# tag moves; reviewers should re-verify before bumping.
uses: anthropics/claude-code-action@537ffff2eff706bd7e3e1c3daf2d4b39067a9f85 # v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# DC9: turn cap; the orchestrator's loop also enforces --max-retries.
claude_args: --max-turns 60 --allowedTools Agent,Read,Write,Edit,Glob,Grep,WebSearch,WebFetch,Skill,Bash
timeout_minutes: "40"
prompt_file: scripts/cycle/prompts/cycle.md

- name: Inspect cycle artifacts
id: inspect
run: |
ART="archive/${{ steps.cycle_date.outputs.date }}"
if [ -d "$ART" ] && [ -f "$ART/index.html" ]; then
echo "artifact_path=$ART" >> "$GITHUB_OUTPUT"
echo "success=true" >> "$GITHUB_OUTPUT"
if [ -f "$ART/meta.json" ]; then
THEME=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$ART/meta.json','utf8')).theme_name || '')")
echo "theme=$THEME" >> "$GITHUB_OUTPUT"
fi
else
echo "success=false" >> "$GITHUB_OUTPUT"
fi

- name: Run smoke tests against the new artifact
if: steps.inspect.outputs.success == 'true'
env:
ARTIFACT_PATH: ${{ steps.inspect.outputs.artifact_path }}
run: npx vitest run tests/cycle/

- name: Check main moved during cycle (race detection)
id: race_check
if: steps.inspect.outputs.success == 'true'
run: |
git fetch origin main
CURRENT_MAIN=$(git rev-parse origin/main)
if [ "$CURRENT_MAIN" != "${{ steps.starting_sha.outputs.sha }}" ]; then
echo "main_moved=true" >> "$GITHUB_OUTPUT"
echo "Main HEAD moved from ${{ steps.starting_sha.outputs.sha }} to $CURRENT_MAIN during the cycle."
else
echo "main_moved=false" >> "$GITHUB_OUTPUT"
fi

- name: Commit and push cycle branch
if: steps.inspect.outputs.success == 'true' && steps.race_check.outputs.main_moved == 'false'
run: |
git add archive/${{ steps.cycle_date.outputs.date }} index.html cli/ 2>/dev/null || true
git add archive/${{ steps.cycle_date.outputs.date }}
if git diff --cached --quiet; then
echo "No changes to commit."
exit 1
fi
git commit -m "drop: ${{ steps.cycle_date.outputs.date }} — ${{ steps.inspect.outputs.theme }}"
git push origin "cycle/${{ steps.cycle_date.outputs.date }}"

- name: Open auto-merge PR
if: steps.inspect.outputs.success == 'true' && steps.race_check.outputs.main_moved == 'false'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_BODY_PATH=$(mktemp)
node -e "
const fs = require('fs');
const meta = JSON.parse(fs.readFileSync('archive/${{ steps.cycle_date.outputs.date }}/meta.json','utf8'));
const social = JSON.parse(fs.readFileSync('archive/${{ steps.cycle_date.outputs.date }}/social.json','utf8'));
const { PR_BODY_TEMPLATE, renderTemplate } = require('./scripts/cycle/run-cycle.js');
const wfl = meta.where_facts_live || {};
const rendered = renderTemplate(PR_BODY_TEMPLATE, {
theme_name: meta.theme_name,
editorial_note: meta.editorial_note,
where_firm_name: wfl.firm_name,
where_mission: wfl.mission,
where_portfolio: wfl.portfolio,
where_team: wfl.team,
where_contact: wfl.contact,
history_view_concept: meta.history_view_concept,
tweet_draft: social.tweet_draft,
screenshot_brief: social.screenshot_brief,
preview_url: 'https://root.vc/archive/${{ steps.cycle_date.outputs.date }}/',
});
fs.writeFileSync('$PR_BODY_PATH', rendered);
"
gh pr create \
--base main \
--head "cycle/${{ steps.cycle_date.outputs.date }}" \
--title "drop: ${{ steps.cycle_date.outputs.date }} — ${{ steps.inspect.outputs.theme }}" \
--body-file "$PR_BODY_PATH"
gh pr merge "cycle/${{ steps.cycle_date.outputs.date }}" --auto --squash

- name: Open post-merge "Drop shipped" issue
if: success() && steps.inspect.outputs.success == 'true' && steps.race_check.outputs.main_moved == 'false'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ISSUE_BODY_PATH=$(mktemp)
node -e "
const fs = require('fs');
const meta = JSON.parse(fs.readFileSync('archive/${{ steps.cycle_date.outputs.date }}/meta.json','utf8'));
const social = JSON.parse(fs.readFileSync('archive/${{ steps.cycle_date.outputs.date }}/social.json','utf8'));
const { SUCCESS_ISSUE_TEMPLATE, renderTemplate } = require('./scripts/cycle/run-cycle.js');
const rendered = renderTemplate(SUCCESS_ISSUE_TEMPLATE, {
cycle_date: '${{ steps.cycle_date.outputs.date }}',
theme_name: meta.theme_name,
topical_hook_or_none: meta.topical && meta.topical_hook ? meta.topical_hook : '(no topical seed this week)',
editorial_note: meta.editorial_note,
live_url: 'https://root.vc/archive/${{ steps.cycle_date.outputs.date }}/',
tweet_draft: social.tweet_draft,
linkedin_draft: social.linkedin_draft,
screenshot_brief: social.screenshot_brief,
});
fs.writeFileSync('$ISSUE_BODY_PATH', rendered);
"
gh issue create \
--title "Drop shipped: ${{ steps.cycle_date.outputs.date }} — ${{ steps.inspect.outputs.theme }}" \
--body-file "$ISSUE_BODY_PATH"

- name: Open "Cycle failed" issue
if: failure() || (steps.inspect.outputs.success == 'false') || (steps.race_check.outputs.main_moved == 'true')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ISSUE_BODY_PATH=$(mktemp)
REASON="cycle failure"
if [ "${{ steps.race_check.outputs.main_moved }}" = "true" ]; then
REASON="raced with human commit (main moved during cycle)"
elif [ "${{ steps.inspect.outputs.success }}" = "false" ]; then
REASON="orchestrator did not produce an archive directory"
fi
node -e "
const fs = require('fs');
const { FAILURE_ISSUE_TEMPLATE, renderTemplate } = require('./scripts/cycle/run-cycle.js');
const rendered = renderTemplate(FAILURE_ISSUE_TEMPLATE, {
cycle_date: '${{ steps.cycle_date.outputs.date }}',
failure_reason: '$REASON',
retries_used: 'unknown',
max_retries: '3',
workflow_run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}',
last_attempt_summary: 'See the workflow logs linked above for the orchestrator\\'s structured stdout JSON.',
});
fs.writeFileSync('$ISSUE_BODY_PATH', rendered);
"
gh issue create \
--title "Cycle failed: ${{ steps.cycle_date.outputs.date }}" \
--body-file "$ISSUE_BODY_PATH" \
--label "cycle-failure"
# Best-effort: delete the cycle branch so it doesn't pile up
git push origin --delete "cycle/${{ steps.cycle_date.outputs.date }}" 2>/dev/null || true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,4 @@ yarn-error.log*

.DS_Store
**/.DS_Store
.vercel
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# cli-website
Who needs a website when you have a terminal.

[![Netlify Status](https://api.netlify.com/api/v1/badges/f3bfb854-9bc6-40a7-8d4c-2cccd3850764/deploy-status)](https://app.netlify.com/sites/rootvc-cli-website/deploys)
Hosted on Vercel; the weekly AI reinvention cycle (in progress) runs on GitHub Actions.

## Basic Commands
- help: list all commands
Expand Down Expand Up @@ -90,6 +90,9 @@ That build now:
- bundles the app boot/runtime code into `js/app.bundle.js`
- emits a minified lazy-load asset for the RickRoll animation

## Local Dev
`npm start` runs `vercel dev` — Vercel's local dev server. It serves the static root and exposes API routes from `api/` at `/api/*`. Requires the Vercel CLI to be authenticated (`vercel login`) and the project linked (`vercel link`).

## Performance Notes
The terminal now initializes on `DOMContentLoaded` instead of waiting for `window.onload`, and optional work such as ASCII art preloading happens after the terminal is already usable.

Expand Down
46 changes: 46 additions & 0 deletions api/submit-application.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Vercel API Route — accepts POSTed job applications from the CLI's `apply` command
// and forwards them to Attio. Ported from netlify/functions/submit-application.js
// (the Netlify function pattern was `exports.handler(event, context)`; Vercel uses
// `default export(req, res)`).
//
// Env: ATTIO_WEBHOOK_URL must be configured in the Vercel project's env vars.

export default async function handler(req, res) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}

try {
const data = req.body;

if (!data || !data.name || !data.email) {
return res.status(400).json({ error: "Name and email are required" });
}

const attioWebhookUrl = process.env.ATTIO_WEBHOOK_URL;
if (!attioWebhookUrl) {
throw new Error("ATTIO_WEBHOOK_URL environment variable not set");
}

const attioResponse = await fetch(attioWebhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});

if (!attioResponse.ok) {
throw new Error(`Attio API error: ${attioResponse.status}`);
}

return res.status(200).json({
success: true,
message: "Application submitted successfully",
});
} catch (error) {
console.error("Error submitting application:", error);
return res.status(500).json({
error: "Failed to submit application",
details: error.message,
});
}
}
Empty file added archive/.gitkeep
Empty file.
Loading
Loading