diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..bbda5546 --- /dev/null +++ b/.github/CODEOWNERS @@ -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 diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index d4a716b7..735bfece 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -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" diff --git a/.github/workflows/spike.yml b/.github/workflows/spike.yml new file mode 100644 index 00000000..a5d05b57 --- /dev/null +++ b/.github/workflows/spike.yml @@ -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 diff --git a/.github/workflows/weekly-cycle.yml b/.github/workflows/weekly-cycle.yml new file mode 100644 index 00000000..a8ebae51 --- /dev/null +++ b/.github/workflows/weekly-cycle.yml @@ -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 diff --git a/.gitignore b/.gitignore index 021fc6ae..f1f223dd 100644 --- a/.gitignore +++ b/.gitignore @@ -140,3 +140,4 @@ yarn-error.log* .DS_Store **/.DS_Store +.vercel diff --git a/README.md b/README.md index ab908b0c..1243ae04 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # cli-website Who needs a website when you have a terminal. -[](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 @@ -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. diff --git a/api/submit-application.js b/api/submit-application.js new file mode 100644 index 00000000..49086acd --- /dev/null +++ b/api/submit-application.js @@ -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, + }); + } +} diff --git a/archive/.gitkeep b/archive/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/archive/_legacy/01-cli-terminal/css/styles.css b/archive/_legacy/01-cli-terminal/css/styles.css new file mode 100644 index 00000000..b11a1fff --- /dev/null +++ b/archive/_legacy/01-cli-terminal/css/styles.css @@ -0,0 +1,27 @@ +@import url(https://fonts.googleapis.com/css?family=Abel|Source+Code+Pro&display=swap); + +body, html { + height: 100%; + overflow: hidden; +} + +body { + background-color: #2c2c2c; + font-family: "Abel"; + margin: 0; +} + +.terminal { + height: 100%; + padding: 24px; +} + +#terminal { + height: 100%; +} + +:root { + --brand-bg-color: #2c2c2c; + --brand-txt-color: #fff; + --brand-link-color: #fff; +} \ No newline at end of file diff --git a/archive/_legacy/01-cli-terminal/css/xterm.css b/archive/_legacy/01-cli-terminal/css/xterm.css new file mode 100644 index 00000000..e97b6439 --- /dev/null +++ b/archive/_legacy/01-cli-terminal/css/xterm.css @@ -0,0 +1,218 @@ +/** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * https://github.com/chjj/term.js + * @license MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * Originally forked from (with the author's permission): + * Fabrice Bellard's javascript vt100 for jslinux: + * http://bellard.org/jslinux/ + * Copyright (c) 2011 Fabrice Bellard + * The original design remains. The terminal itself + * has been extended to include xterm CSI codes, among + * other features. + */ + +/** + * Default styles for xterm.js + */ + +.xterm { + cursor: text; + position: relative; + user-select: none; + -ms-user-select: none; + -webkit-user-select: none; +} + +.xterm.focus, +.xterm:focus { + outline: none; +} + +.xterm .xterm-helpers { + position: absolute; + top: 0; + /** + * The z-index of the helpers must be higher than the canvases in order for + * IMEs to appear on top. + */ + z-index: 5; +} + +.xterm .xterm-helper-textarea { + padding: 0; + border: 0; + margin: 0; + /* Move textarea out of the screen to the far left, so that the cursor is not visible */ + position: absolute; + opacity: 0; + left: -9999em; + top: 0; + width: 0; + height: 0; + z-index: -5; + /** Prevent wrapping so the IME appears against the textarea at the correct position */ + white-space: nowrap; + overflow: hidden; + resize: none; +} + +.xterm .composition-view { + /* TODO: Composition position got messed up somewhere */ + background: #000; + color: #FFF; + display: none; + position: absolute; + white-space: nowrap; + z-index: 1; +} + +.xterm .composition-view.active { + display: block; +} + +.xterm .xterm-viewport { + /* On OS X this is required in order for the scroll bar to appear fully opaque */ + background-color: #000; + overflow-y: scroll; + cursor: default; + position: absolute; + right: 0; + left: 0; + top: 0; + bottom: 0; +} + +.xterm .xterm-screen { + position: relative; +} + +.xterm .xterm-screen canvas { + position: absolute; + left: 0; + top: 0; +} + +.xterm .xterm-scroll-area { + visibility: hidden; +} + +.xterm-char-measure-element { + display: inline-block; + visibility: hidden; + position: absolute; + top: 0; + left: -9999em; + line-height: normal; +} + +.xterm.enable-mouse-events { + /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ + cursor: default; +} + +.xterm.xterm-cursor-pointer, +.xterm .xterm-cursor-pointer { + cursor: pointer; +} + +.xterm.column-select.focus { + /* Column selection mode */ + cursor: crosshair; +} + +.xterm .xterm-accessibility:not(.debug), +.xterm .xterm-message { + position: absolute; + left: 0; + top: 0; + bottom: 0; + right: 0; + z-index: 10; + color: transparent; + pointer-events: none; +} + +.xterm .xterm-accessibility-tree:not(.debug) *::selection { + color: transparent; +} + +.xterm .xterm-accessibility-tree { + user-select: text; + white-space: pre; +} + +.xterm .live-region { + position: absolute; + left: -9999px; + width: 1px; + height: 1px; + overflow: hidden; +} + +.xterm-dim { + /* Dim should not apply to background, so the opacity of the foreground color is applied + * explicitly in the generated class and reset to 1 here */ + opacity: 1 !important; +} + +.xterm-underline-1 { text-decoration: underline; } +.xterm-underline-2 { text-decoration: double underline; } +.xterm-underline-3 { text-decoration: wavy underline; } +.xterm-underline-4 { text-decoration: dotted underline; } +.xterm-underline-5 { text-decoration: dashed underline; } + +.xterm-overline { + text-decoration: overline; +} + +.xterm-overline.xterm-underline-1 { text-decoration: overline underline; } +.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; } +.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; } +.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; } +.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; } + +.xterm-strikethrough { + text-decoration: line-through; +} + +.xterm-screen .xterm-decoration-container .xterm-decoration { + z-index: 6; + position: absolute; +} + +.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer { + z-index: 7; +} + +.xterm-decoration-overview-ruler { + z-index: 8; + position: absolute; + top: 0; + right: 0; + pointer-events: none; +} + +.xterm-decoration-top { + z-index: 2; + position: relative; +} diff --git a/archive/_legacy/01-cli-terminal/favicon.png b/archive/_legacy/01-cli-terminal/favicon.png new file mode 100644 index 00000000..553b9b3e Binary files /dev/null and b/archive/_legacy/01-cli-terminal/favicon.png differ diff --git a/archive/_legacy/01-cli-terminal/index.html b/archive/_legacy/01-cli-terminal/index.html new file mode 100644 index 00000000..2dbdaf91 --- /dev/null +++ b/archive/_legacy/01-cli-terminal/index.html @@ -0,0 +1,76 @@ + + +
+ + + + + +
+
+
+
+
+
+
San Francisco, CA
+
+
+
+
+ Portfolio
+
+
+
+
+
+
+