diff --git a/.github/workflows/squad-docs.yml b/.github/workflows/squad-docs.yml new file mode 100644 index 000000000..307d502c5 --- /dev/null +++ b/.github/workflows/squad-docs.yml @@ -0,0 +1,50 @@ +name: Squad Docs — Build & Deploy + +on: + workflow_dispatch: + push: + branches: [preview] + paths: + - 'docs/**' + - '.github/workflows/squad-docs.yml' + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install build dependencies + run: npm install --no-save markdown-it markdown-it-anchor + + - name: Build docs site + run: node docs/build.js --out _site --base /squad + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/squad-main-guard.yml b/.github/workflows/squad-main-guard.yml index 9f7f65117..cc927f78a 100644 --- a/.github/workflows/squad-main-guard.yml +++ b/.github/workflows/squad-main-guard.yml @@ -36,16 +36,15 @@ jobs: } // Check each file against forbidden path rules + // Allow removals — deleting forbidden files from protected branches is fine const forbidden = files + .filter(f => f.status !== 'removed') .map(f => f.filename) .filter(f => { // .ai-team/** — ALL team state files, zero exceptions if (f === '.ai-team' || f.startsWith('.ai-team/')) return true; - // team-docs/** EXCEPT team-docs/blog/** — blog content is allowed - if (f.startsWith('team-docs/')) { - if (f.startsWith('team-docs/blog/')) return false; - return true; - } + // team-docs/** — ALL internal team docs, zero exceptions + if (f.startsWith('team-docs/')) return true; return false; }); @@ -60,7 +59,7 @@ jobs: '', 'The following files must NOT be merged into `main`.', '`.ai-team/` is runtime team state — it belongs on dev branches only.', - '`team-docs/` internal content should stay on dev (only `team-docs/blog/` is allowed on main).', + '`team-docs/` is internal team content — it belongs on dev branches only.', '', '### Forbidden files found:', '', @@ -72,17 +71,17 @@ jobs: '# Remove tracked .ai-team/ files (keeps local copies):', 'git rm --cached -r .ai-team/', '', - '# Remove tracked team-docs/ files (except blog/):', + '# Remove tracked team-docs/ files:', 'git rm --cached -r team-docs/', - 'git checkout HEAD -- team-docs/blog/', '', '# Commit the removal and push:', 'git commit -m "chore: remove forbidden paths from PR"', 'git push', '```', '', - '> ⚠️ `.ai-team/` is in `.gitignore` but files previously force-added with `git add -f`', - '> remain tracked. `git rm --cached` untracks them without deleting your local copies.', + '> ⚠️ `.ai-team/` is committed on `dev` and feature branches by design.', + '> The guard workflow is the enforcement mechanism that keeps these files off `main` and `preview`.', + '> `git rm --cached` untracks them from this PR without deleting your local copies.', ]; core.setFailed(lines.join('\n')); diff --git a/.gitignore b/.gitignore index 712e81311..d1cfda57c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,11 +10,13 @@ Thumbs.db # Node (if we add tooling later) node_modules/ -# Squad team state (created at runtime, never committed) -# ⚠️ NEVER force-add these files (git add -f .ai-team/). -# Previously force-added files remain tracked even with this rule. -# If tracked files leak in, remove them: git rm --cached -r .ai-team/ -.ai-team/ +# Built docs site (generated, not committed) +_site/ + +# Squad team state — committed on dev and feature branches, +# blocked from main/preview by squad-main-guard.yml workflow. +# ⚠️ NEVER merge .ai-team/ into main or preview. +# The guard workflow will fail the PR if you try. # Squad Squad templates (internal, not distributed) .ai-team-templates/ diff --git a/.npmignore b/.npmignore index 2099314bf..9aae519b2 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,6 @@ # Squad Squad — team state and development files # These are excluded from the distributed package. -# The product ships: index.js, templates/, docs/, CHANGELOG.md, .github/agents/squad.agent.md +# The product ships: index.js, templates/, CHANGELOG.md, .github/agents/squad.agent.md # (also enforced by package.json "files" field) # Squad Squad (the AI team that builds Squad) @@ -10,13 +10,20 @@ # Internal team docs (proposals, sprint plans — never shipped) team-docs/ +# Public docs (read on GitHub, not needed at runtime) +docs/ + # Tests test/ # Git/repo config not needed at runtime +.gitignore .gitattributes .github/workflows/ +# Repo contributor docs (not needed by users) +CONTRIBUTING.md + # Editor / OS .vscode/ .DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e047ffdb1..389e93d83 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,8 +62,8 @@ Squad uses a three-tier branch structure to protect production and staging while | Branch | Purpose | Protection | Files Allowed | |--------|---------|------------|---------------| -| **`dev`** | Development & integration | None | ✅ Everything | -| **`feature/squad/{issue}-{slug}`** | Feature work | None — merge to dev | ✅ Everything | +| **`dev`** | Development & integration | None | ✅ Everything (including `.ai-team/`) | +| **`feature/squad/{issue}-{slug}`** | Feature work | None — merge to dev | ✅ Everything (including `.ai-team/`) | | **`preview`** | Staging & release candidate | Guard checks for `.ai-team/`, `team-docs/` (except blog/) | ✅ Most files — see [Protected Files](#whats-protected) | | **`main`** | Production & releases | Guard checks for `.ai-team/`, `team-docs/` (except blog/) | ✅ Most files — see [Protected Files](#whats-protected) | @@ -97,15 +97,14 @@ git push origin squad/42-auth-refresh ### 🚫 CRITICAL: Files Blocked from `main` and `preview` -These files are **runtime team state** and belong on development branches only: +These files are **runtime team state** and live on `dev` and feature branches. They are committed to git and flow freely between `dev` and feature branches, but the **guard workflow** (`squad-main-guard.yml`) blocks them from reaching `main` or `preview`: -| Path | Reason | Merged to main? | -|------|--------|-----------------| -| **`.ai-team/**`** | Agent charters, routing, decisions, history, casting registry | ❌ NEVER | -| **`team-docs/` (except `team-docs/blog/`** | Internal team documentation, sprint plans, notes | ❌ NEVER | -| **`team-docs/blog/**`** | Public blog content | ✅ YES — blog posts are public | +| Path | Reason | Committed to `dev`? | Merged to `main`/`preview`? | +|------|--------|---------------------|---------------------------| +| **`.ai-team/**`** | Agent charters, routing, decisions, history, casting registry | ✅ YES | ❌ NEVER — guard blocks | +| **`team-docs/**`** | Internal team documentation, sprint plans, notes | ✅ YES | ❌ NEVER — guard blocks | -**Why?** `.ai-team/` contains persistent agent knowledge, routing rules, and decision history. It's internal infrastructure. If it leaks to `main`, you're shipping developer metadata as product. `team-docs/` is working notes — only blog content is publication-ready. +**Why?** `.ai-team/` contains persistent agent knowledge, routing rules, and decision history. `team-docs/` contains internal proposals, sprint plans, and working notes. Both are internal infrastructure that belongs on development branches — not in production. The guard workflow is the enforcement mechanism, not `.gitignore`. `.ai-team/` is NOT in `.gitignore` — it's a normal part of the `dev` branch. The `.npmignore` file ensures both are excluded from the published npm package. Blog posts live in `docs/blog/` and flow freely to all branches. ### ✅ Files That Flow Freely @@ -114,10 +113,9 @@ These files move between `dev` → `preview` → `main` with no restrictions: - `index.js` — CLI entry point - `squad.agent.md` — Squad coordinator - `templates/` — Agent templates -- `docs/` — Public documentation +- `docs/` — Public documentation (including `docs/blog/`) - `test/` — Test suite - `.github/workflows/` — GitHub Actions workflows -- `team-docs/blog/` — Blog posts - `package.json` — Dependencies - `README.md`, `LICENSE` — Project metadata - `CHANGELOG.md` — Release history @@ -171,19 +169,18 @@ If you accidentally (or intentionally) target `preview` or `main`, the **guard w If the guard blocks your PR because it contains `.ai-team/` or `team-docs/` files: ```bash -# Remove .ai-team/ from git (keeps local copies safe) +# Remove .ai-team/ from this PR (keeps local copies and dev branch copies safe) git rm --cached -r .ai-team/ -# Remove team-docs/ except blog/ +# Remove team-docs/ from this PR git rm --cached -r team-docs/ -git checkout HEAD -- team-docs/blog/ # Commit and push git commit -m "chore: remove internal team files from PR" git push ``` -The workflow will re-run and pass. Your local `.ai-team/` and `team-docs/` files remain untouched. +The workflow will re-run and pass. Your local `.ai-team/` and `team-docs/` files remain untouched, and they continue to exist on `dev` normally. --- @@ -289,18 +286,18 @@ You don't need to add these yourself — the Lead will triage and label issues. ``` squad/ -├── .ai-team/ 🚫 BLOCKED from main/preview -│ ├── agents/ -│ │ ├── {name}/charter.md 🚫 BLOCKED -│ │ └── {name}/history.md 🚫 BLOCKED -│ ├── team.md 🚫 BLOCKED -│ ├── routing.md 🚫 BLOCKED -│ ├── decisions.md 🚫 BLOCKED +├── .ai-team/ ✅ Committed on dev & feature branches +│ ├── agents/ 🚫 Guard blocks from main/preview +│ │ ├── {name}/charter.md 🚫 Guard blocks from main/preview +│ │ └── {name}/history.md 🚫 Guard blocks from main/preview +│ ├── team.md 🚫 Guard blocks from main/preview +│ ├── routing.md 🚫 Guard blocks from main/preview +│ ├── decisions.md 🚫 Guard blocks from main/preview │ └── ... │ -├── team-docs/ 🚫 BLOCKED (except blog/) -│ ├── sprint-plan.md 🚫 BLOCKED -│ ├── roadmap.md 🚫 BLOCKED +├── team-docs/ 🚫 Guard blocks from main/preview (except blog/) +│ ├── sprint-plan.md 🚫 Guard blocks from main/preview +│ ├── roadmap.md 🚫 Guard blocks from main/preview │ └── blog/ ✅ ALLOWED (public content) │ └── 001-launch.md ✅ ALLOWED │ @@ -336,13 +333,13 @@ When you open a PR to `main` or `preview`, the workflow `.github/workflows/squad 1. **Fetches all files changed in your PR** (paginated for large PRs) 2. **Checks each file against forbidden path rules:** - If filename starts with `.ai-team/` → BLOCKED - - If filename starts with `team-docs/` AND not `team-docs/blog/` → BLOCKED + - If filename starts with `team-docs/` → BLOCKED - Otherwise → ALLOWED 3. **Reports results:** - ✅ **Pass:** "No forbidden paths found" — you're good to merge - ❌ **Fail:** Lists forbidden files and shows `git rm --cached` fix -The guard is **not a suggestion** — it's a hard stop. But it's easy to fix if it blocks you (see [Fixing a Blocked PR](#fixing-a-blocked-pr)). +The guard is **not a suggestion** — it's a hard stop. This is the primary enforcement mechanism that keeps `.ai-team/` and internal `team-docs/` off `main` and `preview`. But it's easy to fix if it blocks you (see [Fixing a Blocked PR](#fixing-a-blocked-pr)). --- @@ -350,12 +347,14 @@ The guard is **not a suggestion** — it's a hard stop. But it's easy to fix if ### Q: I accidentally committed `.ai-team/` to my feature branch. Do I have to delete it? -**A:** No! `.ai-team/` is in `.gitignore`, so it won't push to GitHub unless you force-add it. Just keep your feature branch clean: +**A:** Nope — `.ai-team/` files are **supposed** to be committed on `dev` and feature branches! They're part of the normal development workflow. The guard workflow (`squad-main-guard.yml`) prevents them from reaching `main` or `preview`. Just don't PR them to those branches. + +If you're creating a PR to `main` or `preview` and the guard blocks it, remove the files from that PR only: ```bash -git rm --cached -r .ai-team/ # Untrack if force-added -git checkout dev # Switch back to dev -# Your local .ai-team/ remains untouched +git rm --cached -r .ai-team/ # Untrack from this PR +git commit -m "chore: remove .ai-team/ from release PR" +git push ``` ### Q: Can I PR to `main` directly? @@ -364,11 +363,11 @@ git checkout dev # Switch back to dev ### Q: The guard blocked my PR. What now? -**A:** Your PR contains `.ai-team/` or `team-docs/` files that shouldn't be in `main`/`preview`. Follow [Fixing a Blocked PR](#fixing-a-blocked-pr) — it's three `git rm --cached` commands and a push. The workflow will re-run and pass. +**A:** Your PR targets `main` or `preview` and contains `.ai-team/` or `team-docs/` files. These files live on `dev` and feature branches but must not reach production. Follow [Fixing a Blocked PR](#fixing-a-blocked-pr) — it's three `git rm --cached` commands and a push. The workflow will re-run and pass. ### Q: I want to commit `team-docs/sprint-plan.md` — can I do that? -**A:** Not to `main` or `preview` — it's internal. Commit it to `dev` and feature branches, and the guard will block it if you accidentally PR it to `main`. If it's public content (blog, guides, etc.), put it in `team-docs/blog/` and it flows freely. +**A:** Not to `main` or `preview` — it's internal. Commit it to `dev` and feature branches, and the guard will block it if you accidentally PR it to `main`. If it's public content (blog, guides, etc.), put it in `docs/blog/` and it flows freely. ### Q: What if I disagree with the branch protection? @@ -389,9 +388,9 @@ Welcome aboard. Make Squad better. 🚀 ## Summary: What You Need to Know 1. **Clone from `dev`, create `squad/{issue}-{slug}` branch, PR back to `dev`** -2. **Never commit `.ai-team/` or internal `team-docs/` to `main` or `preview` — the guard will block it** +2. **`.ai-team/` files are committed on `dev` and feature branches — the guard workflow blocks them from `main`/`preview`** 3. **Run `npm test` before pushing** 4. **Follow conventional commits (feat:, fix:, docs:, etc.)** -5. **If the guard blocks your PR, run `git rm --cached` and push again** +5. **If the guard blocks your PR to `main`/`preview`, run `git rm --cached` and push again** That's it. Happy contributing. diff --git a/docs/assets/script.js b/docs/assets/script.js new file mode 100644 index 000000000..39b6a597a --- /dev/null +++ b/docs/assets/script.js @@ -0,0 +1,41 @@ +(function(){ + var saved=localStorage.getItem("squad-theme"); + if(saved) document.documentElement.setAttribute("data-theme",saved); + updateThemeBtn(); +})(); +function toggleTheme(){ + var html=document.documentElement; + var c=html.getAttribute("data-theme"); + var n=c==="dark"?"light":c==="light"?"auto":"dark"; + html.setAttribute("data-theme",n); + localStorage.setItem("squad-theme",n); + updateThemeBtn(); +} +function updateThemeBtn(){ + var t=document.documentElement.getAttribute("data-theme"); + var b=document.getElementById("theme-btn"); + if(!b) return; + b.textContent=t==="dark"?"\u2600\uFE0F":t==="light"?"\uD83C\uDF19":"\uD83D\uDCBB"; +} +function toggleSidebar(){ + document.getElementById("sidebar").classList.toggle("open"); +} +(function(){ + var input=document.getElementById("search"); + var results=document.getElementById("search-results"); + function esc(s){ return s.replace(/&/g,"&").replace(//g,">"); } + input.addEventListener("input",function(){ + var q=this.value.toLowerCase().trim(); + if(q.length<2){ results.classList.remove("visible"); return; } + var matches=searchIndex.filter(function(e){ + return e.title.toLowerCase().indexOf(q)!==-1 || e.preview.toLowerCase().indexOf(q)!==-1; + }).slice(0,10); + if(!matches.length){ results.innerHTML='
No results
'; } + else { results.innerHTML=matches.map(function(m){ + return ''+esc(m.title)+'
'+esc(m.preview.substring(0,100))+'
'; + }).join(""); } + results.classList.add("visible"); + }); + input.addEventListener("blur",function(){ setTimeout(function(){ results.classList.remove("visible"); },200); }); + input.addEventListener("keydown",function(e){ if(e.key==="Escape"){ this.value=""; results.classList.remove("visible"); } }); +})(); diff --git a/docs/assets/style.css b/docs/assets/style.css new file mode 100644 index 000000000..f1f18ed3f --- /dev/null +++ b/docs/assets/style.css @@ -0,0 +1,83 @@ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +:root { --bg:#fff; --bg2:#f6f8fa; --text:#1f2328; --muted:#656d76; --border:#d1d9e0; + --accent:#0969da; --accent2:#ddf4ff; --code-bg:#f6f8fa; --code-border:#d1d9e0; + --sidebar-bg:#f6f8fa; --sidebar-w:280px; --shadow:0 1px 3px rgba(0,0,0,.08); --radius:8px; } +@media(prefers-color-scheme:dark){ :root:not([data-theme="light"]){ + --bg:#0d1117; --bg2:#161b22; --text:#e6edf3; --muted:#8b949e; --border:#30363d; + --accent:#58a6ff; --accent2:#1a3a5c; --code-bg:#161b22; --code-border:#30363d; + --sidebar-bg:#161b22; --shadow:0 1px 3px rgba(0,0,0,.3); } } +[data-theme="dark"]{ --bg:#0d1117; --bg2:#161b22; --text:#e6edf3; --muted:#8b949e; --border:#30363d; + --accent:#58a6ff; --accent2:#1a3a5c; --code-bg:#161b22; --code-border:#30363d; + --sidebar-bg:#161b22; --shadow:0 1px 3px rgba(0,0,0,.3); } +body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Noto Sans,Helvetica,Arial,sans-serif; + font-size:16px; line-height:1.6; color:var(--text); background:var(--bg); } +.layout { display:flex; min-height:100vh; } +.sidebar { width:var(--sidebar-w); background:var(--sidebar-bg); border-right:1px solid var(--border); + position:fixed; top:0; left:0; bottom:0; overflow-y:auto; z-index:100; display:flex; flex-direction:column; } +.sidebar-header { padding:20px 16px 12px; border-bottom:1px solid var(--border); + display:flex; align-items:center; justify-content:space-between; } +.sidebar-header .logo { font-size:20px; font-weight:700; color:var(--text); text-decoration:none; } +.sidebar-close { display:none; background:none; border:none; font-size:20px; color:var(--muted); cursor:pointer; } +.sidebar-content { padding:12px 0; flex:1; overflow-y:auto; } +.sidebar-content>a, .nav-section a { display:block; padding:6px 16px; color:var(--muted); + text-decoration:none; font-size:14px; border-left:3px solid transparent; transition:all .15s; } +.sidebar-content>a:hover, .nav-section a:hover { color:var(--text); background:var(--accent2); } +.sidebar-content>a.active, .nav-section a.active { color:var(--accent); border-left-color:var(--accent); font-weight:600; } +.nav-section { margin:4px 0; } +.nav-section summary { padding:6px 16px; font-size:12px; font-weight:600; text-transform:uppercase; + letter-spacing:.05em; color:var(--muted); cursor:pointer; list-style:none; user-select:none; } +.nav-section summary::before { content:"\25B8 "; } +.nav-section[open] summary::before { content:"\25BE "; } +.nav-section summary::-webkit-details-marker { display:none; } +.nav-section a { padding-left:28px; } +.main { margin-left:var(--sidebar-w); flex:1; min-width:0; } +.topbar { position:sticky; top:0; background:var(--bg); border-bottom:1px solid var(--border); + padding:8px 24px; display:flex; align-items:center; justify-content:space-between; z-index:50; } +.menu-btn { display:none; background:none; border:1px solid var(--border); border-radius:6px; + padding:4px 10px; font-size:18px; cursor:pointer; color:var(--text); } +.search-box { flex:1; max-width:400px; position:relative; } +.search-box input { width:100%; padding:6px 12px; border:1px solid var(--border); border-radius:6px; + background:var(--bg2); color:var(--text); font-size:14px; outline:none; } +.search-box input:focus { border-color:var(--accent); box-shadow:0 0 0 3px var(--accent2); } +.search-results { position:absolute; top:100%; left:0; right:0; background:var(--bg); + border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); + max-height:400px; overflow-y:auto; display:none; z-index:200; } +.search-results.visible { display:block; } +.search-results a { display:block; padding:8px 12px; color:var(--text); text-decoration:none; + font-size:14px; border-bottom:1px solid var(--border); } +.search-results a:hover { background:var(--accent2); } +.search-results a:last-child { border-bottom:none; } +.search-results .no-results { padding:12px; color:var(--muted); font-size:14px; } +.theme-toggle { background:none; border:1px solid var(--border); border-radius:6px; + padding:4px 10px; font-size:18px; cursor:pointer; margin-left:12px; color:var(--text); } +.content { max-width:820px; margin:0 auto; padding:32px 40px 80px; } +.content h1 { font-size:2em; margin:0 0 16px; border-bottom:1px solid var(--border); padding-bottom:8px; } +.content h2 { font-size:1.5em; margin:32px 0 12px; border-bottom:1px solid var(--border); padding-bottom:6px; } +.content h3 { font-size:1.25em; margin:24px 0 8px; } +.content h4 { font-size:1em; margin:20px 0 6px; } +.content p { margin:0 0 16px; } +.content ul, .content ol { margin:0 0 16px; padding-left:2em; } +.content li { margin:4px 0; } +.content li>p { margin:4px 0; } +.content a { color:var(--accent); text-decoration:none; } +.content a:hover { text-decoration:underline; } +.content blockquote { margin:0 0 16px; padding:8px 16px; border-left:4px solid var(--accent); + background:var(--bg2); border-radius:0 var(--radius) var(--radius) 0; color:var(--muted); } +.content table { width:100%; border-collapse:collapse; margin:0 0 16px; font-size:14px; } +.content th, .content td { padding:8px 12px; border:1px solid var(--border); text-align:left; } +.content th { background:var(--bg2); font-weight:600; } +.content code { font-family:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,monospace; + font-size:.875em; background:var(--code-bg); border:1px solid var(--code-border); border-radius:4px; padding:2px 6px; } +.content pre { margin:0 0 16px; padding:16px; background:var(--code-bg); border:1px solid var(--code-border); + border-radius:var(--radius); overflow-x:auto; font-size:14px; line-height:1.5; } +.content pre code { background:none; border:none; padding:0; font-size:inherit; } +.content img { max-width:100%; border-radius:var(--radius); } +.content hr { border:none; border-top:1px solid var(--border); margin:32px 0; } +@media(max-width:768px){ + .sidebar { transform:translateX(-100%); transition:transform .25s ease; width:280px; } + .sidebar.open { transform:translateX(0); box-shadow:4px 0 20px rgba(0,0,0,.2); } + .sidebar-close { display:block; } + .main { margin-left:0; } + .menu-btn { display:block; } + .content { padding:24px 16px 60px; } } +@media print { .sidebar, .topbar { display:none !important; } .main { margin-left:0; } } diff --git a/docs/assets/template.html b/docs/assets/template.html new file mode 100644 index 000000000..04b70bcfd --- /dev/null +++ b/docs/assets/template.html @@ -0,0 +1,26 @@ + + + + + +{{title}} - Squad Docs + + + +
+{{nav}} +
+
+ + + +
+
+{{content}} +
+
+
+ + + + diff --git a/team-docs/blog/001-wave-0-the-team-that-built-itself.md b/docs/blog/001-wave-0-the-team-that-built-itself.md similarity index 100% rename from team-docs/blog/001-wave-0-the-team-that-built-itself.md rename to docs/blog/001-wave-0-the-team-that-built-itself.md diff --git a/team-docs/blog/001a-the-squad-squad-problem.md b/docs/blog/001a-the-squad-squad-problem.md similarity index 100% rename from team-docs/blog/001a-the-squad-squad-problem.md rename to docs/blog/001a-the-squad-squad-problem.md diff --git a/team-docs/blog/001b-meet-the-squad.md b/docs/blog/001b-meet-the-squad.md similarity index 100% rename from team-docs/blog/001b-meet-the-squad.md rename to docs/blog/001b-meet-the-squad.md diff --git a/team-docs/blog/001c-first-pr-amolchanov.md b/docs/blog/001c-first-pr-amolchanov.md similarity index 100% rename from team-docs/blog/001c-first-pr-amolchanov.md rename to docs/blog/001c-first-pr-amolchanov.md diff --git a/team-docs/blog/002-first-community-pr.md b/docs/blog/002-first-community-pr.md similarity index 100% rename from team-docs/blog/002-first-community-pr.md rename to docs/blog/002-first-community-pr.md diff --git a/team-docs/blog/003-super-bowl-weekend.md b/docs/blog/003-super-bowl-weekend.md similarity index 100% rename from team-docs/blog/003-super-bowl-weekend.md rename to docs/blog/003-super-bowl-weekend.md diff --git a/team-docs/blog/004-v020-release.md b/docs/blog/004-v020-release.md similarity index 100% rename from team-docs/blog/004-v020-release.md rename to docs/blog/004-v020-release.md diff --git a/team-docs/blog/005-v030-give-it-a-brain.md b/docs/blog/005-v030-give-it-a-brain.md similarity index 100% rename from team-docs/blog/005-v030-give-it-a-brain.md rename to docs/blog/005-v030-give-it-a-brain.md diff --git a/team-docs/blog/006-first-external-deployment.md b/docs/blog/006-first-external-deployment.md similarity index 100% rename from team-docs/blog/006-first-external-deployment.md rename to docs/blog/006-first-external-deployment.md diff --git a/team-docs/blog/007-first-video-coverage.md b/docs/blog/007-first-video-coverage.md similarity index 100% rename from team-docs/blog/007-first-video-coverage.md rename to docs/blog/007-first-video-coverage.md diff --git a/team-docs/blog/008-v040-release.md b/docs/blog/008-v040-release.md similarity index 100% rename from team-docs/blog/008-v040-release.md rename to docs/blog/008-v040-release.md diff --git a/docs/blog/2026-02-13-v040-sprint-progress.md b/docs/blog/009-v040-sprint-progress.md similarity index 100% rename from docs/blog/2026-02-13-v040-sprint-progress.md rename to docs/blog/009-v040-sprint-progress.md diff --git a/docs/build.js b/docs/build.js new file mode 100644 index 000000000..80d2235f8 --- /dev/null +++ b/docs/build.js @@ -0,0 +1,175 @@ +#!/usr/bin/env node +// Squad docs site builder - converts docs/**/*.md to static HTML. +// Dependencies: markdown-it, markdown-it-anchor +// Run: node docs/build.js [--out _site] + +const fs = require('fs'); +const path = require('path'); +const markdownIt = require('markdown-it'); +const markdownItAnchor = require('markdown-it-anchor'); + +const DOCS_DIR = path.join(__dirname); +const OUT_DIR = process.argv.includes('--out') + ? path.resolve(process.argv[process.argv.indexOf('--out') + 1]) + : path.join(__dirname, '..', '_site'); + +// Base path prefix for deployment under a subpath (e.g., /squad for GitHub Pages) +const BASE_PATH = process.argv.includes('--base') + ? process.argv[process.argv.indexOf('--base') + 1].replace(/\/+$/, '') + : ''; + +const md = markdownIt({ html: true, linkify: true, typographer: true }) + .use(markdownItAnchor, { permalink: false }); + +function walk(dir, base) { + base = base || dir; + let results = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory() && entry.name !== 'assets' && entry.name !== 'node_modules') { + results = results.concat(walk(full, base)); + } else if (entry.isFile() && entry.name.endsWith('.md') && entry.name !== 'build.js') { + results.push({ abs: full, rel: path.relative(base, full) }); + } + } + return results; +} + +function toHtmlPath(rel) { + return rel.replace(/\\/g, '/').replace(/README\.md$/i, 'index.html').replace(/\.md$/, '.html'); +} + +function extractTitle(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const fmMatch = content.match(/^---\s*\n[\s\S]*?title:\s*"?([^"\n]+)"?\s*\n[\s\S]*?---/); + if (fmMatch) return fmMatch[1].trim(); + const h1 = content.match(/^#\s+(.+)$/m); + if (h1) return h1[1].trim(); + return null; +} + +function nameFromFile(rel) { + const base = path.basename(rel, '.md'); + if (base.toLowerCase() === 'readme') return 'Overview'; + return base.replace(/[-_]/g, ' ').replace(/^\d+-/, '').replace(/\b\w/g, function(c) { return c.toUpperCase(); }); +} + +function fileSorter(a, b) { + var nameA = path.basename(a.rel).toLowerCase(); + var nameB = path.basename(b.rel).toLowerCase(); + if (nameA === 'readme.md') return -1; + if (nameB === 'readme.md') return 1; + return nameA.localeCompare(nameB); +} + +function escapeHtml(s) { + return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} + +function buildNav(files, currentRel) { + var sections = {}; + var topLevel = []; + for (var i = 0; i < files.length; i++) { + var f = files[i]; + var parts = f.rel.replace(/\\/g, '/').split('/'); + if (parts.length === 1) topLevel.push(f); + else { + var section = parts[0]; + if (!sections[section]) sections[section] = []; + sections[section].push(f); + } + } + var sectionLabels = { features: 'Features', scenarios: 'Scenarios', blog: 'Blog' }; + var nav = ''; + return nav; +} + +function rewriteLinks(html) { + return html + .replace(/href="([^"]*?)README\.md"/g, 'href="$1index.html"') + .replace(/href="([^"]*?)\.md"/g, 'href="$1.html"') + .replace(/href="([^"]*?)\.md#/g, 'href="$1.html#'); +} + +function copyDir(src, dest) { + fs.mkdirSync(dest, { recursive: true }); + for (const entry of fs.readdirSync(src, { withFileTypes: true })) { + var s = path.join(src, entry.name); + var d = path.join(dest, entry.name); + if (entry.isDirectory()) copyDir(s, d); + else fs.copyFileSync(s, d); + } +} + +function build() { + var files = walk(DOCS_DIR); + console.log('Found ' + files.length + ' markdown files'); + var searchIndex = files.map(function(f) { + var raw = fs.readFileSync(f.abs, 'utf8'); + var title = extractTitle(f.abs) || nameFromFile(f.rel); + var preview = raw.replace(/^---[\s\S]*?---\n?/, '').replace(/[#*`>\[\]|_~\-]/g, '').replace(/\n+/g, ' ').trim().substring(0, 200); + return { title: title, href: BASE_PATH + '/' + toHtmlPath(f.rel), preview: preview }; + }); + for (var i = 0; i < files.length; i++) { + var f = files[i]; + var raw = fs.readFileSync(f.abs, 'utf8'); + var stripped = raw.replace(/^---\s*\n[\s\S]*?---\s*\n/, ''); + var rendered = md.render(stripped); + var rewritten = rewriteLinks(rendered); + var title = extractTitle(f.abs) || nameFromFile(f.rel); + var nav = buildNav(files, f.rel); + var html = getTemplate(title, rewritten, nav, searchIndex); + var outPath = path.join(OUT_DIR, toHtmlPath(f.rel)); + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, html); + } + var assetsDir = path.join(DOCS_DIR, 'assets'); + if (fs.existsSync(assetsDir)) copyDir(assetsDir, path.join(OUT_DIR, 'assets')); + console.log('Built ' + files.length + ' pages to ' + OUT_DIR); +} + +// Read template files from docs/assets/ +var TEMPLATE_HTML = fs.readFileSync(path.join(DOCS_DIR, 'assets', 'template.html'), 'utf8'); + +function getTemplate(title, content, nav, searchIndex) { + return TEMPLATE_HTML + .replace('{{title}}', escapeHtml(title)) + .replace('{{nav}}', nav) + .replace('{{content}}', content) + .replace('{{searchIndex}}', JSON.stringify(searchIndex)) + .replace(/\{\{basePath\}\}/g, BASE_PATH); +} + +build(); diff --git a/docs/scenarios/release-process.md b/docs/scenarios/release-process.md index 565a999de..851910fc4 100644 --- a/docs/scenarios/release-process.md +++ b/docs/scenarios/release-process.md @@ -60,9 +60,8 @@ Before pushing to `preview`, remove forbidden paths that the guard workflow will # Remove .ai-team/ (keep local copy via .gitignore) git rm --cached -r .ai-team/ -# Remove team-docs/ except blog/ +# Remove team-docs/ git rm --cached -r team-docs/ -git checkout HEAD -- team-docs/blog/ ``` If nothing was removed, that's fine: @@ -288,7 +287,6 @@ git checkout release/preview git reset --hard origin/dev git rm --cached -r .ai-team/ 2>/dev/null git rm --cached -r team-docs/ 2>/dev/null -git checkout HEAD -- team-docs/blog/ git commit -m "chore: remove forbidden paths for preview" 2>/dev/null git push -f origin release/preview # Wait for guard workflow to pass @@ -401,7 +399,7 @@ The repository enforces protection rules on `main` and `preview` to prevent acci ```javascript // Forbidden on main and preview: ✗ .ai-team/** (zero exceptions — team state never ships) -✗ team-docs/** (except team-docs/blog/**) +✗ team-docs/** (zero exceptions — internal team docs never ship) // Allowed everywhere: ✓ index.js (distribution) @@ -422,7 +420,7 @@ The repository enforces protection rules on `main` and `preview` to prevent acci The following files must NOT be merged into `main`. `.ai-team/` is runtime team state — it belongs on dev branches only. -`team-docs/` internal content should stay on dev (only `team-docs/blog/` is allowed on main). +`team-docs/` is internal team content — it belongs on dev branches only. ### Forbidden files found: - `.ai-team/agents/neo/history.md` @@ -432,7 +430,6 @@ The following files must NOT be merged into `main`. ### How to fix: git rm --cached -r .ai-team/ git rm --cached -r team-docs/ -git checkout HEAD -- team-docs/blog/ git commit -m "chore: remove forbidden paths from PR" git push ``` @@ -544,32 +541,32 @@ Expected: ✅ Guard passes. gh pr close --delete-branch ``` -### Test 3: Verify Guard Allows team-docs/blog/ +### Test 3: Verify Guard Blocks team-docs/ 1. **Create a test branch:** ```bash -git checkout -b test/guard-allows-blog +git checkout -b test/guard-blocks-team-docs ``` -2. **Add a blog file:** +2. **Add a team-docs file:** ```bash -mkdir -p team-docs/blog -echo "# My Blog Post" > team-docs/blog/my-post.md -git add team-docs/blog/my-post.md -git commit -m "test: add blog post (should be allowed)" -git push -u origin test/guard-allows-blog +mkdir -p team-docs +echo "# Internal Notes" > team-docs/notes.md +git add team-docs/notes.md +git commit -m "test: add team-docs file (should be blocked)" +git push -u origin test/guard-blocks-team-docs ``` 3. **Create PR to main:** ```bash -gh pr create --base main --title "test: guard should allow team-docs/blog/" \ - --body "This PR tests that blog files are allowed" +gh pr create --base main --title "test: guard should block team-docs/" \ + --body "This PR tests that all team-docs files are blocked" ``` -Expected: ✅ Guard passes. `team-docs/blog/` is allowed on main. +Expected: ❌ Guard fails. ALL `team-docs/` content is blocked on main and preview. 4. **Delete the branch:** @@ -593,9 +590,8 @@ gh pr close --delete-branch # Remove .ai-team/ (keeps local copy) git rm --cached -r .ai-team/ -# Remove internal team-docs/ (keeps blog) +# Remove team-docs/ git rm --cached -r team-docs/ -git checkout HEAD -- team-docs/blog/ # Commit and push git commit -m "chore: remove forbidden paths" @@ -780,10 +776,9 @@ Kobayashi, test the guard workflow: Kobayashi, fix this blocked PR: 1. Fetch the current PR state 2. Remove all .ai-team/ and team-docs/ files -3. Keep team-docs/blog/ if it exists -4. Commit the removal -5. Push to update the PR -6. Wait for guard to pass +3. Commit the removal +4. Push to update the PR +5. Wait for guard to pass ``` --- diff --git a/package.json b/package.json index 1c3289287..ed50bc5ab 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "index.js", ".github/agents/squad.agent.md", "templates/**/*", - "docs/**/*", "CHANGELOG.md" ], "scripts": { diff --git a/team-docs/blog/template.md b/team-docs/blog/template.md deleted file mode 100644 index 75787a1c6..000000000 --- a/team-docs/blog/template.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: "Wave X: [Title]" -date: YYYY-MM-DD -author: "[Agent Name] ([Role])" -wave: X -tags: [squad, wave-X, topic1, topic2] -status: published -hero: "[One-sentence hook ΓÇö what happened and why it matters]" ---- - -# Wave X: [Title] - -> _[Hero sentence repeated ΓÇö the scroll-stopper. One line that makes someone keep reading.]_ - -## What Shipped - - - -- **[Feature/Fix]** ΓÇö [One-sentence description]. _(Built by [Agent Name])_ -- **[Feature/Fix]** ΓÇö [One-sentence description]. _(Built by [Agent Name])_ -- **[Feature/Fix]** ΓÇö [One-sentence description]. _(Built by [Agent Name])_ - -## The Story - - - -[Narrative here.] - -## By the Numbers - -| Metric | Value | -|--------|-------| -| Proposals written | X | -| Tests passing | X | -| Agents active | X | -| Human messages | X | -| [Custom metric] | X | - -## What We Learned - - - -- **[Learning]** ΓÇö [Why it matters] -- **[Learning]** ΓÇö [Why it matters] - -## What's Next - - - -[Next wave preview.] - ---- - -_This post was written by [Agent Name], the [Role] on Squad's own team. Squad is an open source project by [@bradygaster](https://github.com/bradygaster). [Try it ΓåÆ](https://github.com/bradygaster/squad)_ diff --git a/templates/workflows/squad-docs.yml b/templates/workflows/squad-docs.yml new file mode 100644 index 000000000..307d502c5 --- /dev/null +++ b/templates/workflows/squad-docs.yml @@ -0,0 +1,50 @@ +name: Squad Docs — Build & Deploy + +on: + workflow_dispatch: + push: + branches: [preview] + paths: + - 'docs/**' + - '.github/workflows/squad-docs.yml' + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install build dependencies + run: npm install --no-save markdown-it markdown-it-anchor + + - name: Build docs site + run: node docs/build.js --out _site --base /squad + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/templates/workflows/squad-main-guard.yml b/templates/workflows/squad-main-guard.yml index 9f7f65117..cc927f78a 100644 --- a/templates/workflows/squad-main-guard.yml +++ b/templates/workflows/squad-main-guard.yml @@ -36,16 +36,15 @@ jobs: } // Check each file against forbidden path rules + // Allow removals — deleting forbidden files from protected branches is fine const forbidden = files + .filter(f => f.status !== 'removed') .map(f => f.filename) .filter(f => { // .ai-team/** — ALL team state files, zero exceptions if (f === '.ai-team' || f.startsWith('.ai-team/')) return true; - // team-docs/** EXCEPT team-docs/blog/** — blog content is allowed - if (f.startsWith('team-docs/')) { - if (f.startsWith('team-docs/blog/')) return false; - return true; - } + // team-docs/** — ALL internal team docs, zero exceptions + if (f.startsWith('team-docs/')) return true; return false; }); @@ -60,7 +59,7 @@ jobs: '', 'The following files must NOT be merged into `main`.', '`.ai-team/` is runtime team state — it belongs on dev branches only.', - '`team-docs/` internal content should stay on dev (only `team-docs/blog/` is allowed on main).', + '`team-docs/` is internal team content — it belongs on dev branches only.', '', '### Forbidden files found:', '', @@ -72,17 +71,17 @@ jobs: '# Remove tracked .ai-team/ files (keeps local copies):', 'git rm --cached -r .ai-team/', '', - '# Remove tracked team-docs/ files (except blog/):', + '# Remove tracked team-docs/ files:', 'git rm --cached -r team-docs/', - 'git checkout HEAD -- team-docs/blog/', '', '# Commit the removal and push:', 'git commit -m "chore: remove forbidden paths from PR"', 'git push', '```', '', - '> ⚠️ `.ai-team/` is in `.gitignore` but files previously force-added with `git add -f`', - '> remain tracked. `git rm --cached` untracks them without deleting your local copies.', + '> ⚠️ `.ai-team/` is committed on `dev` and feature branches by design.', + '> The guard workflow is the enforcement mechanism that keeps these files off `main` and `preview`.', + '> `git rm --cached` untracks them from this PR without deleting your local copies.', ]; core.setFailed(lines.join('\n'));